diff --git a/locales/index.d.ts b/locales/index.d.ts index 4bc906b3c..ca4b75b51 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5371,6 +5371,14 @@ export interface Locale extends ILocale { * この情報は他のユーザーには公開されません。 */ "thisInfoIsNotVisibleOtherUser": string; + /** + * 水に流す + */ + "flushItAway": string; + /** + * 削除をしても全てが水に流れるわけではありませんが + */ + "deleteNotWash": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1d1dc7857..2aad7056b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1336,6 +1336,8 @@ setScheduledTime: "予約日時を設定" willBePostedAt: "{x}に投稿されます" sensitiveByModerator: "管理者によって、ドライブのファイルがセンシティブとして設定されました。\n詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。" thisInfoIsNotVisibleOtherUser: "この情報は他のユーザーには公開されません。" +flushItAway: "水に流す" +deleteNotWash: "削除をしても全てが水に流れるわけではありませんが" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/frontend/assets/sounds/flush.mp3 b/packages/frontend/assets/sounds/flush.mp3 new file mode 100644 index 000000000..9ec98f9fd Binary files /dev/null and b/packages/frontend/assets/sounds/flush.mp3 differ diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 7c9463af2..8345f1b51 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -11,6 +11,7 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; +import { isAprilFoolsDay } from '@/scripts/seasonal-events'; export default defineComponent({ props: { @@ -119,12 +120,16 @@ export default defineComponent({ }; function onBeforeLeave(element: Element) { + if (isAprilFoolsDay()) return; + const el = element as HTMLElement; el.style.top = `${el.offsetTop}px`; el.style.left = `${el.offsetLeft}px`; } function onLeaveCancelled(element: Element) { + if (isAprilFoolsDay()) return; + const el = element as HTMLElement; el.style.top = ''; el.style.left = ''; @@ -136,6 +141,7 @@ export default defineComponent({ [$style['date-separated-list-nogap']]: props.noGap, [$style['direction-down']]: props.direction === 'down', [$style['direction-up']]: props.direction === 'up', + [$style['april-fool']]: isAprilFoolsDay(), }; return () => defaultStore.state.animation ? h(TransitionGroup, { @@ -210,6 +216,14 @@ export default defineComponent({ } } +.direction-up.april-fool , +.direction-down.april-fool { + &:global > .list-enter-from, + &:global > .list-leave-to { + animation: components-MkDateSeparatedList-spin-shrink 3s ease-in forwards; + } +} + .separator { text-align: center; } @@ -240,5 +254,16 @@ export default defineComponent({ .date-2-icon { margin-left: 8px; } + +@keyframes spin-shrink { + 0% { + transform: rotate(0deg) scale(1); + opacity: 1; + } + 100% { + transform: rotate(2160deg) scale(0); + opacity: 0; + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b7d3c1abf..3d3f40b29 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -20,6 +20,8 @@ import { clipsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; +import { isMute, playUrl } from '@/scripts/sound'; +import { isAprilFoolsDay } from '@/scripts/seasonal-events'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -180,10 +182,14 @@ export function getNoteMenu(props: { function del(): void { os.confirm({ type: 'warning', - text: i18n.ts.noteDeleteConfirm, + text: isAprilFoolsDay() ? i18n.ts.deleteNotWash + '\n' + i18n.ts.noteDeleteConfirm : i18n.ts.noteDeleteConfirm, }).then(({ canceled }) => { if (canceled) return; + if (isAprilFoolsDay()) { + if (!isMute()) playUrl('/client-assets/sounds/flush.mp3', {}); + } + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -435,8 +441,8 @@ export function getNoteMenu(props: { action: delEdit, } : undefined, { - icon: 'ti ti-trash', - text: i18n.ts.delete, + icon: isAprilFoolsDay() ? 'ti ti-whirl' : 'ti ti-trash', + text: isAprilFoolsDay() ? i18n.ts.flushItAway : i18n.ts.delete, danger: true, action: del, }] diff --git a/packages/frontend/src/scripts/seasonal-events.ts b/packages/frontend/src/scripts/seasonal-events.ts new file mode 100644 index 000000000..8209517c8 --- /dev/null +++ b/packages/frontend/src/scripts/seasonal-events.ts @@ -0,0 +1,47 @@ +enum EventTypes { + Christmas = 0, + NewYear = 1, + ValentinesDay = 2, + Halloween = 3, + AprilFoolsDay = 4, + Unknown = 5 +} + +export const isEventDay = (): EventTypes => { + const date = new Date(); + const month = date.getMonth(); + const day = date.getDate(); + + // jsで月を0から始まる + // 例えば4月は月の値が3になる + if (month === 11 && day === 25) return EventTypes.Christmas; + if (month === 0 && day === 1) return EventTypes.NewYear; + if (month === 1 && day === 14) return EventTypes.ValentinesDay; + if (month === 9 && day === 31) return EventTypes.Halloween; + if (month === 3 && day === 1) return EventTypes.AprilFoolsDay; + return EventTypes.Unknown; +}; + +export const isAprilFoolsDay = (): boolean => { + return isEventDay() === EventTypes.AprilFoolsDay; +}; + +export const isChristmas = (): boolean => { + return isEventDay() === EventTypes.Christmas; +}; + +export const isNewYear = (): boolean => { + return isEventDay() === EventTypes.NewYear; +}; + +export const isValentinesDay = (): boolean => { + return isEventDay() === EventTypes.ValentinesDay; +}; + +export const isHalloween = (): boolean => { + return isEventDay() === EventTypes.Halloween; +}; + +export const isUnknown = (): boolean => { + return isEventDay() === EventTypes.Unknown; +};