From 65cd605b739ae0d213b3502308e9cd523d3e1ae7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 21 Jan 2023 13:14:55 +0900 Subject: Achievements (#9665) * wip * Update ja-JP.yml * wip * wip * Update MkAchievements.vue * wip * :art: * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --- packages/frontend/src/account.ts | 7 +- .../frontend/src/components/MkAchievements.vue | 224 +++++++++++ packages/frontend/src/components/MkClickerGame.vue | 11 +- .../frontend/src/components/MkDrive.folder.vue | 7 +- packages/frontend/src/components/MkDrive.vue | 7 +- .../frontend/src/components/MkFollowButton.vue | 17 + packages/frontend/src/components/MkNote.vue | 4 + .../frontend/src/components/MkNoteDetailed.vue | 4 + .../frontend/src/components/MkNotification.vue | 13 + packages/frontend/src/components/MkPostForm.vue | 31 +- .../src/components/MkReactionsViewer.reaction.vue | 4 + packages/frontend/src/init.ts | 77 ++++ packages/frontend/src/navbar.ts | 8 +- packages/frontend/src/pages/achievements.vue | 25 ++ packages/frontend/src/pages/settings/profile.vue | 9 + packages/frontend/src/router.ts | 4 + packages/frontend/src/scripts/achievements.ts | 425 +++++++++++++++++++++ packages/frontend/src/scripts/get-note-menu.ts | 12 + 18 files changed, 879 insertions(+), 10 deletions(-) create mode 100644 packages/frontend/src/components/MkAchievements.vue create mode 100644 packages/frontend/src/pages/achievements.vue create mode 100644 packages/frontend/src/scripts/achievements.ts (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 93916ccf2f..31c125d3ae 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -2,11 +2,11 @@ import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { i18n } from './i18n'; +import { miLocalStorage } from './local-storage'; import { del, get, set } from '@/scripts/idb-proxy'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; -import { miLocalStorage } from './local-storage'; // TODO: 他のタブと永続化されたstateを同期 @@ -20,6 +20,11 @@ export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : n export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); export const iAmAdmin = $i != null && $i.isAdmin; +export let notesCount = $i == null ? 0 : $i.notesCount; +export function incNotesCount() { + notesCount++; +} + export async function signout() { waiting(); miLocalStorage.removeItem('account'); diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue new file mode 100644 index 0000000000..64fea96354 --- /dev/null +++ b/packages/frontend/src/components/MkAchievements.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 03736ac5e4..68e0f8185d 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -20,6 +20,7 @@ import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; import * as game from '@/scripts/clicker-game'; import number from '@/filters/number'; +import { claimAchievement } from '@/scripts/achievements'; defineProps<{ }>(); @@ -30,14 +31,18 @@ let cps = $ref(0); let prevCookies = $ref(0); function onClick(ev: MouseEvent) { + const x = ev.clientX; + const y = ev.clientY; + os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + saveData.value!.cookies++; saveData.value!.totalCookies++; saveData.value!.totalHandmadeCookies++; saveData.value!.clicked++; - const x = ev.clientX; - const y = ev.clientY; - os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + if (cookies.value === 1) { + claimAchievement('cookieClicked'); + } } useInterval(() => { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 82653ca0b4..156013b9aa 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -33,6 +33,7 @@ import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; +import { claimAchievement } from '@/scripts/achievements'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -159,9 +160,11 @@ function onDrop(ev: DragEvent) { }).then(() => { // noop }).catch(err => { - switch (err) { - case 'detected-circular-definition': + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); os.alert({ + type: 'error', title: i18n.ts.unableToProcess, text: i18n.ts.circularReferenceFolder, }); diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 112a64f52d..af7175e5cd 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -99,6 +99,7 @@ import { stream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { uploadFile, uploads } from '@/scripts/upload'; +import { claimAchievement } from '@/scripts/achievements'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -268,9 +269,11 @@ function onDrop(ev: DragEvent): any { }).then(() => { // noop }).catch(err => { - switch (err) { - case 'detected-circular-definition': + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); os.alert({ + type: 'error', title: i18n.ts.unableToProcess, text: i18n.ts.circularReferenceFolder, }); diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index ee256d9263..de8db54bfa 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -35,6 +35,8 @@ import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; +import { claimAchievement } from '@/scripts/achievements'; +import { $i } from '@/account'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -90,6 +92,21 @@ async function onClick() { userId: props.user.id, }); hasPendingFollowRequestFromYou = true; + + claimAchievement('following1'); + + if ($i.followingCount >= 10) { + claimAchievement('following10'); + } + if ($i.followingCount >= 50) { + claimAchievement('following50'); + } + if ($i.followingCount >= 100) { + claimAchievement('following100'); + } + if ($i.followingCount >= 300) { + claimAchievement('following300'); + } } } } catch (err) { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 9b2501a2ed..1f6a2883d7 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -143,6 +143,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ note: misskey.entities.Note; @@ -268,6 +269,9 @@ function react(viaKeyboard = false): void { noteId: appearNote.id, reaction: reaction, }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } }, () => { focus(); }); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 56061e0e6f..48ace56d9c 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -159,6 +159,7 @@ import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ note: misskey.entities.Note; @@ -279,6 +280,9 @@ function react(viaKeyboard = false): void { noteId: appearNote.id, reaction: reaction, }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } }, () => { focus(); }); diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 5b8041c1d4..e992495a78 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -2,6 +2,7 @@
+
@@ -14,6 +15,7 @@ +
{{ i18n.ts._notification.pollEnded }} + {{ i18n.ts._notification.achievementEarned }} {{ notification.header }} @@ -57,6 +60,9 @@ + + {{ i18n.ts._achievements._types['_' + notification.achievement].title }} + {{ i18n.ts.youGotNewFollower }}
{{ i18n.ts.followRequestAccepted }} {{ i18n.ts.receiveFollowRequest }}
|
@@ -82,6 +88,7 @@ import { i18n } from '@/i18n'; import * as os from '@/os'; import { stream } from '@/stream'; import { useTooltip } from '@/scripts/use-tooltip'; +import { $i } from '@/account'; const props = withDefaults(defineProps<{ notification: misskey.entities.Notification; @@ -240,6 +247,12 @@ useTooltip(reactionRef, (showing) => { pointer-events: none; } +.t_achievementEarned { + padding: 3px; + background: #88a6b7; + pointer-events: none; +} + .tail { flex: 1; min-width: 0; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 6822caf4f4..c7e7e85b2e 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -93,11 +93,12 @@ import { defaultStore, notePostInterruptors, postFormActions } from '@/store'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; -import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; +import { $i, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; import { uploadFile } from '@/scripts/upload'; import { deepClone } from '@/scripts/clone'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage'; +import { claimAchievement } from '@/scripts/achievements'; const modal = inject('modal'); @@ -627,6 +628,34 @@ async function post(ev?: MouseEvent) { } posting = false; postAccount = null; + + incNotesCount(); + if (notesCount === 1) { + claimAchievement('notes1'); + } + + const text = postData.text?.toLowerCase() ?? ''; + if ((text.includes('love') || text.includes('❤')) && text.includes('misskey')) { + claimAchievement('iLoveMisskey'); + } + if (text.includes('Efrlqw8ytg4'.toLowerCase()) || text.includes('XVCwzwxdHuA'.toLowerCase())) { + claimAchievement('brainDiver'); + } + + if (props.renote && (props.renote.userId === $i.id) && text.length > 0) { + claimAchievement('selfQuote'); + } + + const date = new Date(); + const h = date.getHours(); + const m = date.getMinutes(); + const s = date.getSeconds(); + if (h >= 0 && h <= 3) { + claimAchievement('postedAtLateNight'); + } + if (m === 0 && s === 0) { + claimAchievement('postedAt0min0sec'); + } }); }).catch(err => { posting = false; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index e90dd7ea69..ec4042d18c 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,6 +20,7 @@ import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { $i } from '@/account'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; +import { claimAchievement } from '@/scripts/achievements'; const props = defineProps<{ reaction: string; @@ -52,6 +53,9 @@ const toggleReaction = () => { noteId: props.note.id, reaction: props.reaction, }); + if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } } }; diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts index e10315e1ad..a2723d479c 100644 --- a/packages/frontend/src/init.ts +++ b/packages/frontend/src/init.ts @@ -44,6 +44,7 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { getUrlWithoutLoginId } from '@/scripts/login-id'; import { getAccountFromId } from '@/scripts/get-account-from-id'; import { miLocalStorage } from './local-storage'; +import { claimAchievement, claimedAchievements } from './scripts/achievements'; (async () => { console.info(`Misskey v${version}`); @@ -345,6 +346,82 @@ import { miLocalStorage } from './local-storage'; }); } + if ($i.birthday) { + const now = new Date(); + const m = now.getMonth() + 1; + const d = now.getDate(); + const bm = parseInt($i.birthday.split('-')[1]); + const bd = parseInt($i.birthday.split('-')[2]); + if (m === bm && d === bd) { + claimAchievement('loggedInOnBirthday'); + } + } + + if ($i.loggedInDays >= 3) claimAchievement('login3'); + if ($i.loggedInDays >= 7) claimAchievement('login7'); + if ($i.loggedInDays >= 15) claimAchievement('login15'); + if ($i.loggedInDays >= 30) claimAchievement('login30'); + if ($i.loggedInDays >= 60) claimAchievement('login60'); + if ($i.loggedInDays >= 100) claimAchievement('login100'); + if ($i.loggedInDays >= 200) claimAchievement('login200'); + if ($i.loggedInDays >= 300) claimAchievement('login300'); + if ($i.loggedInDays >= 400) claimAchievement('login400'); + if ($i.loggedInDays >= 500) claimAchievement('login500'); + if ($i.loggedInDays >= 600) claimAchievement('login600'); + if ($i.loggedInDays >= 700) claimAchievement('login700'); + if ($i.loggedInDays >= 800) claimAchievement('login800'); + if ($i.loggedInDays >= 900) claimAchievement('login900'); + if ($i.loggedInDays >= 1000) claimAchievement('login1000'); + + if ($i.notesCount > 0) claimAchievement('notes1'); + if ($i.notesCount >= 10) claimAchievement('notes10'); + if ($i.notesCount >= 100) claimAchievement('notes100'); + if ($i.notesCount >= 500) claimAchievement('notes500'); + if ($i.notesCount >= 1000) claimAchievement('notes1000'); + if ($i.notesCount >= 5000) claimAchievement('notes5000'); + if ($i.notesCount >= 10000) claimAchievement('notes10000'); + if ($i.notesCount >= 20000) claimAchievement('notes20000'); + if ($i.notesCount >= 30000) claimAchievement('notes30000'); + if ($i.notesCount >= 40000) claimAchievement('notes40000'); + if ($i.notesCount >= 50000) claimAchievement('notes50000'); + if ($i.notesCount >= 60000) claimAchievement('notes60000'); + if ($i.notesCount >= 70000) claimAchievement('notes70000'); + if ($i.notesCount >= 80000) claimAchievement('notes80000'); + if ($i.notesCount >= 90000) claimAchievement('notes90000'); + if ($i.notesCount >= 100000) claimAchievement('notes100000'); + + if ($i.followersCount > 0) claimAchievement('followers1'); + if ($i.followersCount >= 10) claimAchievement('followers10'); + if ($i.followersCount >= 50) claimAchievement('followers50'); + if ($i.followersCount >= 100) claimAchievement('followers100'); + if ($i.followersCount >= 300) claimAchievement('followers300'); + if ($i.followersCount >= 500) claimAchievement('followers500'); + if ($i.followersCount >= 1000) claimAchievement('followers1000'); + + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { + claimAchievement('passedSinceAccountCreated1'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { + claimAchievement('passedSinceAccountCreated2'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { + claimAchievement('passedSinceAccountCreated3'); + } + + if (claimedAchievements.length >= 30) { + claimAchievement('collectAchievements30'); + } + + window.setInterval(() => { + if (Math.floor(Math.random() * 10000) === 0) { + claimAchievement('justPlainLucky'); + } + }, 1000 * 10); + + window.setTimeout(() => { + claimAchievement('client30min'); + }, 1000 * 60 * 30); + const lastUsed = miLocalStorage.getItem('lastUsed'); if (lastUsed) { const lastUsedDate = parseInt(lastUsed, 10); diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 9ee78741dc..3d16a52e62 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -1,11 +1,11 @@ import { computed, ref, reactive } from 'vue'; import { $i } from './account'; +import { miLocalStorage } from './local-storage'; import { search } from '@/scripts/search'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { ui } from '@/config'; import { unisonReload } from '@/scripts/unison-reload'; -import { miLocalStorage } from './local-storage'; export const navbarItemDef = reactive({ notifications: { @@ -103,6 +103,12 @@ export const navbarItemDef = reactive({ icon: 'ti ti-device-tv', to: '/channels', }, + achievements: { + title: i18n.ts.achievements, + icon: 'ti ti-military-award', + show: computed(() => $i != null), + to: '/my/achievements', + }, ui: { title: i18n.ts.switchUi, icon: 'ti ti-devices', diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue new file mode 100644 index 0000000000..b6cd174b41 --- /dev/null +++ b/packages/frontend/src/pages/achievements.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index ae74224db6..da7d3d3703 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -85,6 +85,7 @@ import { i18n } from '@/i18n'; import { $i } from '@/account'; import { langmap } from '@/scripts/langmap'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { claimAchievement } from '@/scripts/achievements'; const profile = reactive({ name: $i.name, @@ -133,6 +134,13 @@ function save() { isCat: !!profile.isCat, showTimelineReplies: !!profile.showTimelineReplies, }); + claimAchievement('profileFilled'); + if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { + claimAchievement('setNameToSyuilo'); + } + if (profile.isCat) { + claimAchievement('markedAsCat'); + } } function changeAvatar(ev) { @@ -155,6 +163,7 @@ function changeAvatar(ev) { }); $i.avatarId = i.avatarId; $i.avatarUrl = i.avatarUrl; + claimAchievement('profileFilled'); }); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 26c73c610f..22106e1595 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -427,6 +427,10 @@ export const routes = [{ path: '/my/favorites', component: page(() => import('./pages/favorites.vue')), loginRequired: true, +}, { + path: '/my/achievements', + component: page(() => import('./pages/achievements.vue')), + loginRequired: true, }, { name: 'messaging', path: '/my/messaging', diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts new file mode 100644 index 0000000000..c8245ad3db --- /dev/null +++ b/packages/frontend/src/scripts/achievements.ts @@ -0,0 +1,425 @@ +import * as os from '@/os'; +import { $i } from '@/account'; + +export const ACHIEVEMENT_TYPES = [ + 'notes1', + 'notes10', + 'notes100', + 'notes500', + 'notes1000', + 'notes5000', + 'notes10000', + 'notes20000', + 'notes30000', + 'notes40000', + 'notes50000', + 'notes60000', + 'notes70000', + 'notes80000', + 'notes90000', + 'notes100000', + 'login3', + 'login7', + 'login15', + 'login30', + 'login60', + 'login100', + 'login200', + 'login300', + 'login400', + 'login500', + 'login600', + 'login700', + 'login800', + 'login900', + 'login1000', + 'passedSinceAccountCreated1', + 'passedSinceAccountCreated2', + 'passedSinceAccountCreated3', + 'loggedInOnBirthday', + 'noteClipped1', + 'noteFavorited1', + 'profileFilled', + 'markedAsCat', + 'following1', + 'following10', + 'following50', + 'following100', + 'following300', + 'followers1', + 'followers10', + 'followers50', + 'followers100', + 'followers300', + 'followers500', + 'followers1000', + 'collectAchievements30', + 'iLoveMisskey', + 'client30min', + 'noteDeletedWithin1min', + 'postedAtLateNight', + 'postedAt0min0sec', + 'selfQuote', + 'htl20npm', + 'driveFolderCircularReference', + 'reactWithoutRead', + 'clickedClickHere', + 'justPlainLucky', + 'setNameToSyuilo', + 'cookieClicked', + 'brainDiver', +] as const; + +export const ACHIEVEMENT_BADGES = { + 'notes1': { + img: '/fluent-emoji/1f4dd.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'notes10': { + img: '/fluent-emoji/1f4d1.png', + bg: null, + frame: 'bronze', + }, + 'notes100': { + img: '/fluent-emoji/1f4d2.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'notes500': { + img: '/fluent-emoji/1f4da.png', + bg: null, + frame: 'bronze', + }, + 'notes1000': { + img: '/fluent-emoji/1f5c3.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'notes5000': { + img: '/fluent-emoji/1f304.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'notes10000': { + img: '/fluent-emoji/1f3d9.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'silver', + }, + 'notes20000': { + img: '/fluent-emoji/1f307.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'silver', + }, + 'notes30000': { + img: '/fluent-emoji/1f306.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'silver', + }, + 'notes40000': { + img: '/fluent-emoji/1f303.png', + bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', + frame: 'silver', + }, + 'notes50000': { + img: '/fluent-emoji/1fa90.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'gold', + }, + 'notes60000': { + img: '/fluent-emoji/2604.png', + bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', + frame: 'gold', + }, + 'notes70000': { + img: '/fluent-emoji/1f30c.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'gold', + }, + 'notes80000': { + img: '/fluent-emoji/1f30c.png', + bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', + frame: 'gold', + }, + 'notes90000': { + img: '/fluent-emoji/1f30c.png', + bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', + frame: 'gold', + }, + 'notes100000': { + img: '/fluent-emoji/267e.png', + bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', + frame: 'platinum', + }, + 'login3': { + img: '/fluent-emoji/1f331.png', + bg: null, + frame: 'bronze', + }, + 'login7': { + img: '/fluent-emoji/1f331.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'login15': { + img: '/fluent-emoji/1f331.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'bronze', + }, + 'login30': { + img: '/fluent-emoji/1fab4.png', + bg: null, + frame: 'bronze', + }, + 'login60': { + img: '/fluent-emoji/1fab4.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'login100': { + img: '/fluent-emoji/1fab4.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'silver', + }, + 'login200': { + img: '/fluent-emoji/1f333.png', + bg: null, + frame: 'silver', + }, + 'login300': { + img: '/fluent-emoji/1f333.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'silver', + }, + 'login400': { + img: '/fluent-emoji/1f333.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'silver', + }, + 'login500': { + img: '/fluent-emoji/1f304.png', + bg: null, + frame: 'silver', + }, + 'login600': { + img: '/fluent-emoji/1f304.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'gold', + }, + 'login700': { + img: '/fluent-emoji/1f304.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'gold', + }, + 'login800': { + img: '/fluent-emoji/1f307.png', + bg: null, + frame: 'gold', + }, + 'login900': { + img: '/fluent-emoji/1f307.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'gold', + }, + 'login1000': { + img: '/fluent-emoji/1f307.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'platinum', + }, + 'noteClipped1': { + img: '/fluent-emoji/1f587.png', + bg: null, + frame: 'bronze', + }, + 'noteFavorited1': { + img: '/fluent-emoji/1f31f.png', + bg: null, + frame: 'bronze', + }, + 'profileFilled': { + img: '/fluent-emoji/1f44c.png', + bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', + frame: 'bronze', + }, + 'markedAsCat': { + img: '/fluent-emoji/1f408.png', + bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', + frame: 'bronze', + }, + 'following1': { + img: '/fluent-emoji/2618.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'following10': { + img: '/fluent-emoji/1f6b8.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'following50': { + img: '/fluent-emoji/1f91d.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'following100': { + img: '/fluent-emoji/1f4af.png', + bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))', + frame: 'silver', + }, + 'following300': { + img: '/fluent-emoji/1f970.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'silver', + }, + 'followers1': { + img: '/fluent-emoji/2618.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'followers10': { + img: '/fluent-emoji/1f44b.png', + bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', + frame: 'bronze', + }, + 'followers50': { + img: '/fluent-emoji/1f411.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'bronze', + }, + 'followers100': { + img: '/fluent-emoji/1f396.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'silver', + }, + 'followers300': { + img: '/fluent-emoji/1f3c6.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'silver', + }, + 'followers500': { + img: '/fluent-emoji/1f4e1.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'gold', + }, + 'followers1000': { + img: '/fluent-emoji/1f451.png', + bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', + frame: 'platinum', + }, + 'collectAchievements30': { + img: '/fluent-emoji/1f3c5.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, + 'iLoveMisskey': { + img: '/fluent-emoji/2764.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, + 'client30min': { + img: '/fluent-emoji/1f552.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'bronze', + }, + 'noteDeletedWithin1min': { + img: '/fluent-emoji/1f5d1.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'bronze', + }, + 'postedAtLateNight': { + img: '/fluent-emoji/1f319.png', + bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', + frame: 'bronze', + }, + 'postedAt0min0sec': { + img: '/fluent-emoji/1f55b.png', + bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))', + frame: 'bronze', + }, + 'selfQuote': { + img: '/fluent-emoji/1f4dd.png', + bg: null, + frame: 'bronze', + }, + 'htl20npm': { + img: '/fluent-emoji/1f30a.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'bronze', + }, + 'driveFolderCircularReference': { + img: '/fluent-emoji/1f4c2.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'bronze', + }, + 'reactWithoutRead': { + img: '/fluent-emoji/2753.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'bronze', + }, + 'clickedClickHere': { + img: '/fluent-emoji/2757.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'bronze', + }, + 'justPlainLucky': { + img: '/fluent-emoji/1f340.png', + bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', + frame: 'silver', + }, + 'setNameToSyuilo': { + img: '/fluent-emoji/1f36e.png', + bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', + frame: 'bronze', + }, + 'passedSinceAccountCreated1': { + img: '/fluent-emoji/0031-20e3.png', + bg: null, + frame: 'bronze', + }, + 'passedSinceAccountCreated2': { + img: '/fluent-emoji/0032-20e3.png', + bg: null, + frame: 'silver', + }, + 'passedSinceAccountCreated3': { + img: '/fluent-emoji/0033-20e3.png', + bg: null, + frame: 'gold', + }, + 'loggedInOnBirthday': { + img: '/fluent-emoji/1f382.png', + bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', + frame: 'silver', + }, + 'cookieClicked': { + img: '/fluent-emoji/1f36a.png', + bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', + frame: 'bronze', + }, + 'brainDiver': { + img: '/fluent-emoji/1f9e0.png', + bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', + frame: 'bronze', + }, +} as const satisfies Record; + +export const claimedAchievements = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : []; + +export function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { + if (claimedAchievements.includes(type)) return; + os.api('i/claim-achievement', { name: type }); + claimedAchievements.push(type); +} + +if (_DEV_) { + (window as any).unlockAllAchievements = async () => { + for (const t of ACHIEVEMENT_TYPES) { + await new Promise(resolve => setTimeout(resolve, 100)); + claimAchievement(t); + } + }; +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 7a426ec722..da7f2a5c20 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -1,6 +1,7 @@ import { defineAsyncComponent, Ref, inject } from 'vue'; import * as misskey from 'misskey-js'; import { pleaseLogin } from './please-login'; +import { claimAchievement } from './achievements'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; @@ -38,6 +39,10 @@ export function getNoteMenu(props: { os.api('notes/delete', { noteId: appearNote.id, }); + + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + claimAchievement('noteDeletedWithin1min'); + } }); } @@ -53,10 +58,15 @@ export function getNoteMenu(props: { }); os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); + + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + claimAchievement('noteDeletedWithin1min'); + } }); } function toggleFavorite(favorite: boolean): void { + claimAchievement('noteFavorited1'); os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { noteId: appearNote.id, }); @@ -118,11 +128,13 @@ export function getNoteMenu(props: { const clip = await os.apiWithDialog('clips/create', result); + claimAchievement('noteClipped1'); os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); }, }, null, ...clips.map(clip => ({ text: clip.name, action: () => { + claimAchievement('noteClipped1'); os.promiseDialog( os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), null, -- cgit v1.2.3-freya