diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 6022bc6a8c..b371a6b025 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1335,6 +1335,7 @@ information: "Informació" chat: "Xat" migrateOldSettings: "Migració de la configuració antiga " migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual." +compress: "Comprimir " _chat: noMessagesYet: "Encara no tens missatges " newMessage: "Missatge nou" @@ -1363,6 +1364,8 @@ _chat: newline: "Línia nova " muteThisRoom: "Silenciar aquesta sala" deleteRoom: "Esborrar la sala" + chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte." + chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari." cannotChatWithTheUser: "No pots xatejar amb aquest usuari" cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert." chatWithThisUser: "Xateja amb aquest usuari" diff --git a/locales/en-US.yml b/locales/en-US.yml index ae84b53bfe..c780cd9fb7 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "It may take some time until the upload is complete." explore: "Explore" messageRead: "Read" noMoreHistory: "There is no further history" +startChat: "Start chat" nUsersRead: "read by {n}" agreeTo: "I agree to {0}" agree: "Agree" @@ -1333,12 +1334,55 @@ emojiPalette: "Emoji palette" postForm: "Posting form" textCount: "Character count" information: "About" +chat: "Chat" +migrateOldSettings: "Migrate old client settings" +migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten." +compress: "Compress" _chat: + noMessagesYet: "No messages yet" + newMessage: "New message" + individualChat: "Private Chat" + individualChat_description: "Have a private chat with another person." + roomChat: "Room Chat" + roomChat_description: "A chat room which can have multiple people.\nYou can also invite people who don't allow private chats if they accept the invite." + createRoom: "Create Room" + inviteUserToChat: "Invite users to start chatting" + yourRooms: "Created rooms" + joiningRooms: "Joined rooms" invitations: "Invite" + noInvitations: "No invitations" + history: "History" noHistory: "No history available" + noRooms: "No rooms found" + inviteUser: "Invite Users" + sentInvitations: "Sent Invites" + join: "Join" + ignore: "Ignore" + leave: "Leave room" members: "Members" + searchMessages: "Search messages" home: "Home" send: "Send" + newline: "New line" + muteThisRoom: "Mute room" + deleteRoom: "Delete room" + chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account." + chatNotAvailableInOtherAccount: "The chat function is disabled for the other user." + cannotChatWithTheUser: "Cannot start a chat with this user" + cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat." + chatWithThisUser: "Chat with user" + thisUserAllowsChatOnlyFromFollowers: "This user accepts chats from followers only." + thisUserAllowsChatOnlyFromFollowing: "This user accepts chats only from users they follow." + thisUserAllowsChatOnlyFromMutualFollowing: "This user only accepts chats from users who are mutual followers." + thisUserNotAllowedChatAnyone: "This user is not accepting chats from anyone." + chatAllowedUsers: "Who to allow chatting with" + chatAllowedUsers_note: "You can chat with anyone to whom you have sent a chat message regardless of this setting." + _chatAllowedUsers: + everyone: "Everyone" + followers: "Only your followers" + following: "Only users you are following" + mutual: "Mutual followers only" + none: "Nobody" _emojiPalette: palettes: "Palette" enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices" @@ -1364,6 +1408,12 @@ _settings: timelineAndNote: "Timeline and note" makeEveryTextElementsSelectable: "Make all text elements selectable" makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations." + showNavbarSubButtons: "Show sub-buttons on the navigation bar" + ifOn: "When turned on" + ifOff: "When turned off" + _chat: + showSenderName: "Show sender's name" + sendOnEnter: "Press Enter to send" _preferencesProfile: profileName: "Profile name" profileNameDescription: "Set a name that identifies this device." @@ -1949,6 +1999,7 @@ _role: canImportFollowing: "Allow importing following" canImportMuting: "Allow importing muting" canImportUserLists: "Allow importing lists" + canChat: "Allow Chat" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" @@ -2181,6 +2232,7 @@ _sfx: noteMy: "Own note" notification: "Notifications" reaction: "On choosing a reaction" + chatMessage: "Chat Messages" _soundSettings: driveFile: "Use an audio file in Drive." driveFileWarn: "Select an audio file from Drive." @@ -2328,6 +2380,7 @@ _permissions: "read:federation": "Get federation data" "write:report-abuse": "Report violation" "write:chat": "Compose or delete chat messages" + "read:chat": "Browse Chat" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -2576,6 +2629,7 @@ _notification: newNote: "New note" unreadAntennaNote: "Antenna {name}" roleAssigned: "Role given" + chatRoomInvitationReceived: "You have been invited to a chat room" emptyPushNotificationMessage: "Push notifications have been updated" achievementEarned: "Achievement unlocked" testNotification: "Test notification" @@ -2604,6 +2658,7 @@ _notification: receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" roleAssigned: "Role given" + chatRoomInvitationReceived: "Invited to chat room" achievementEarned: "Achievement unlocked" exportCompleted: "The export has been completed" login: "Sign In" @@ -2744,6 +2799,7 @@ _moderationLogTypes: deletePage: "Page deleted" deleteFlash: "Play deleted" deleteGalleryPost: "Gallery post deleted" + deleteChatRoom: "Deleted Chat Room" updateProxyAccountDescription: "Update the description of the proxy account" _fileViewer: title: "File details" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 5335ea6f0b..3ec8414ded 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1335,6 +1335,7 @@ information: "Informazioni" chat: "Chat" migrateOldSettings: "Migrare le vecchie impostazioni" migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali." +compress: "Comprimi" _chat: noMessagesYet: "Ancora nessun messaggio" newMessage: "Nuovo messaggio" @@ -1363,6 +1364,8 @@ _chat: newline: "Nuova riga" muteThisRoom: "Silenzia stanza" deleteRoom: "Elimina stanza" + chatNotAvailableForThisAccountOrServer: "Questo server, o questo profilo ha disabilitato la chat." + chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona." cannotChatWithTheUser: "Impossibile chattare con questa persona" cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo." chatWithThisUser: "Chatta con questa persona" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index bf50e409ca..d42eddf400 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1337,6 +1337,7 @@ information: "关于" chat: "聊天" migrateOldSettings: "迁移旧设置信息" migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。" +compress: "压缩" _chat: noMessagesYet: "还没有消息" newMessage: "新消息" @@ -1365,6 +1366,8 @@ _chat: newline: "换行" muteThisRoom: "静音此房间" deleteRoom: "删除房间" + chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" + chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" cannotChatWithTheUser: "无法与此用户聊天" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" chatWithThisUser: "聊天" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index bc23551936..a7dc2c0da5 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1337,6 +1337,7 @@ information: "關於" chat: "聊天" migrateOldSettings: "遷移舊設定資訊" migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。" +compress: "壓縮" _chat: noMessagesYet: "尚無訊息" newMessage: "新訊息" @@ -1365,6 +1366,8 @@ _chat: newline: "換行" muteThisRoom: "此聊天室已靜音" deleteRoom: "刪除聊天室" + chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。" + chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。" cannotChatWithTheUser: "無法與此使用者聊天" cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。" chatWithThisUser: "聊天" diff --git a/package.json b/package.json index 2440345648..941956a533 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.3.1-pari-alpha.14", + "version": "2025.3.1-pari-alpha.15", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index c309bfc32e..4858c19155 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -465,6 +465,7 @@ export class WebhookTestService { followersVisibility: 'public', followingVisibility: 'public', chatScope: 'mutual', + canChat: true, twoFactorEnabled: false, usePasswordLessLogin: false, securityKeys: false, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index e04d258c0d..a342bba64c 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -84,6 +84,7 @@ describe('ユーザー', () => { followingVisibility: user.followingVisibility, followersVisibility: user.followersVisibility, chatScope: user.chatScope, + canChat: user.canChat, roles: user.roles, memo: user.memo, }); @@ -346,6 +347,7 @@ describe('ユーザー', () => { assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.chatScope, 'mutual'); + assert.strictEqual(response.canChat, true); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index 4cf156ba23..94a91305f4 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible, isHeadVisible } from '@@/js/scroll.js'; +import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible, isHeadVisible } from '@@/js/scroll.js'; import type { ComputedRef } from 'vue'; import { misskeyApi } from '@/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -252,7 +252,7 @@ const fetchMore = async (): Promise<void> => { return nextTick(() => { if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts index 6c61c582e1..9057b896c6 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -93,7 +93,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 return removeListener; } -export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { +export function scrollInContainer(el: HTMLElement, options: ScrollToOptions | undefined) { const container = getScrollContainer(el); if (container == null) { window.scroll(options); @@ -108,7 +108,7 @@ export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { * @param options Scroll options */ export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); + scrollInContainer(el, { top: 0, ...options }); } /** diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index 05ac002b53..1b9a9b68c0 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -58,7 +58,12 @@ export default [ // location ... window.locationと衝突 or 紛らわしい // document ... window.documentと衝突 or 紛らわしい // history ... window.historyと衝突 or 紛らわしい - 'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history'], + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], 'no-restricted-globals': [ 'error', { @@ -85,6 +90,26 @@ export default [ 'name': 'history', 'message': 'Use `window.history`.', }, + { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, { 'name': 'name', 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 9178960492..60ed71cb91 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -30,7 +30,7 @@ import { loadFontStyle } from '@/utility/load-font.js'; import { prefer } from '@/preferences.js'; import { $i } from '@/i.js'; -export async function common(createVue: () => App<Element>) { +export async function common(createVue: () => Promise<App<Element>>) { console.info(`Misskey v${version}`); if (_DEV_) { @@ -272,7 +272,7 @@ export async function common(createVue: () => App<Element>) { }); }); - const app = createVue(); + const app = await createVue(); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index d7c30bf219..8132aacbfc 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -32,7 +32,7 @@ import { signout } from '@/signout.js'; import { migrateOldSettings } from '@/pref-migrate.js'; export async function mainBoot() { - const { isClientUpdated, lastVersion } = await common(() => { + const { isClientUpdated, lastVersion } = await common(async () => { let uiStyle = ui; const searchParams = new URLSearchParams(window.location.search); @@ -46,19 +46,19 @@ export async function mainBoot() { let rootComponent: Component; switch (uiStyle) { case 'zen': - rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue')); + rootComponent = await import('@/ui/zen.vue').then(x => x.default); break; case 'deck': - rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue')); + rootComponent = await import('@/ui/deck.vue').then(x => x.default); break; case 'visitor': - rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue')); + rootComponent = await import('@/ui/visitor.vue').then(x => x.default); break; case 'classic': - rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue')); + rootComponent = await import('@/ui/classic.vue').then(x => x.default); break; default: - rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue')); + rootComponent = await import('@/ui/universal.vue').then(x => x.default); break; } diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts index e24c324dfb..036142bc4d 100644 --- a/packages/frontend/src/boot/sub-boot.ts +++ b/packages/frontend/src/boot/sub-boot.ts @@ -6,11 +6,10 @@ import { createApp, defineAsyncComponent } from 'vue'; import { common } from './common.js'; import { emojiPicker } from '@/utility/emoji-picker.js'; +import UiMinimum from '@/ui/minimum.vue'; export async function subBoot() { - const { isClientUpdated } = await common(() => createApp( - defineAsyncComponent(() => import('@/ui/minimum.vue')), - )); + const { isClientUpdated } = await common(async () => createApp(UiMinimum)); emojiPicker.init(); } diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts index a42e80c27a..4304c2e2b7 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -2,20 +2,18 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { channel } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkChannelFollowButton from './MkChannelFollowButton.vue'; +import type { StoryObj } from '@storybook/vue3'; import { i18n } from '@/i18n.js'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts index eb7e61f294..6e1eb13d61 100644 --- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -2,18 +2,16 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkClickerGame from './MkClickerGame.vue'; +import type { StoryObj } from '@storybook/vue3'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 4978933f73..662e2a118d 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -79,7 +79,7 @@ function opening() { picker.value?.focus(); // 何故かちょっと待たないとフォーカスされない - setTimeout(() => { + window.setTimeout(() => { picker.value?.focus(); }, 10); } diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue index bc63bef0b6..1d0ffaea11 100644 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) { pullDistance.value = 0; isSwiping.value = false; - setTimeout(() => { + window.setTimeout(() => { isSwipingForClass.value = false; }, 400); diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 629679a971..1cd88fb1ab 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -339,7 +339,7 @@ const bufferedDataRatio = computed(() => { // MediaControl Events function onMouseOver() { if (controlStateTimer) { - clearTimeout(controlStateTimer); + window.clearTimeout(controlStateTimer); } isHoverring.value = true; } diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index b5c93df4ed..3bcf835ec9 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js'; import { focusTrap } from '@/utility/focus-trap.js'; import { focusParent } from '@/utility/focus.js'; import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -94,7 +95,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -provide('modal', true); +provide(DI.inModal, true); const maxHeight = ref<number>(); const fixed = ref(false); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index da7a26dbce..2ceea876bb 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -18,8 +18,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> - <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>--> - <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>--> <div v-if="isRenote" :class="$style.renote"> <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> <i class="ti ti-repeat" style="margin-right: 4px;"></i> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index dc6095d136..da3d6cf149 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -13,32 +13,26 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notes }"> - <div :class="[$style.root, { [$style.noGap]: noGap }]"> - <MkDateSeparatedList - ref="notes" - v-slot="{ item: note }" - :items="notes" - :direction="pagination.reversed ? 'up' : 'down'" - :reversed="pagination.reversed" - :noGap="noGap" - :ad="true" - :class="$style.notes" - > - <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> - </MkDateSeparatedList> + <div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"> + <template v-for="(note, i) in notes" :key="note.id"> + <MkNote :class="$style.note" :note="note" :withHardMute="true"/> + <div v-if="note._shouldInsertAd_" :class="$style.ad"> + <MkAd :prefer="['horizontal', 'horizontal-big']"/> + </div> + </template> </div> </template> </MkPagination> </template> <script lang="ts" setup> -import { useTemplateRef } from 'vue'; +import { useTemplateRef, TransitionGroup } from 'vue'; import type { Paging } from '@/components/MkPagination.vue'; import MkNote from '@/components/MkNote.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkPagination from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ pagination: Paging; @@ -55,21 +49,30 @@ defineExpose({ <style lang="scss" module> .root { + container-type: inline-size; + &.noGap { - > .notes { - background: var(--MI_THEME-panel); + background: var(--MI_THEME-panel); + + .note { + border-bottom: solid 0.5px var(--MI_THEME-divider); + } + + .ad { + padding: 8px; + background-size: auto auto; + background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px); + border-bottom: solid 0.5px var(--MI_THEME-divider); } } &:not(.noGap) { - > .notes { - background: var(--MI_THEME-bg); + background: var(--MI_THEME-bg); - .note { - box-shadow: 0 4px 25px #0000000a; - background: var(--MI_THEME-panel); - border-radius: var(--MI-radius); - } + .note { + box-shadow: 0 4px 25px #0000000a; + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } } } diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 2d81e6f818..38ff142ba1 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -14,10 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> + <div :class="$style.notifications"> + <template v-for="(notification, i) in notifications" :key="notification.id"> + <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/> + <XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/> + </template> + </div> </template> </MkPagination> </MkPullToRefresh> @@ -29,7 +31,6 @@ import * as Misskey from 'misskey-js'; import type { notificationTypes } from '@@/js/const.js'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; @@ -102,28 +103,22 @@ onMounted(() => { connection.on('notificationFlushed', reload); }); -onActivated(() => { - pagingComponent.value?.reload(); - connection = useStream().useChannel('main'); - connection.on('notification', onNotification); - connection.on('notificationFlushed', reload); -}); - onUnmounted(() => { if (connection) connection.dispose(); }); -onDeactivated(() => { - if (connection) connection.dispose(); -}); - defineExpose({ reload, }); </script> <style lang="scss" module> -.list { +.notifications { + container-type: inline-size; background: var(--MI_THEME-panel); } + +.item { + border-bottom: solid 0.5px var(--MI_THEME-divider); +} </style> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 0cef47da28..e5faff3739 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkError v-else-if="error" @retry="init()"/> - <div v-else-if="empty" key="_empty_" class="empty"> + <div v-else-if="empty" key="_empty_"> <slot name="empty"> <div class="_fullinfo"> <img :src="infoImageUrl" draggable="false"/> @@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> <slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot> <div v-show="!pagination.reversed && more" key="_more_"> <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> </div> </Transition> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js'; +import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js'; import type { ComputedRef, Ref } from 'vue'; import type { MisskeyEntity } from '@/types/date-separated-list.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -212,7 +212,7 @@ async function init(): Promise<void> { const params = getActualValue<Paging['params']>(props.pagination.params, {}); await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, - limit: props.pagination.limit ?? 10, + limit: props.pagination.limit, allowPartial: true, }).then(res => { for (let i = 0; i < res.length; i++) { @@ -270,7 +270,7 @@ const fetchMore = async (): Promise<void> => { return nextTick(() => { if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } @@ -370,7 +370,7 @@ watch(visibility, () => { BACKGROUND_PAUSE_WAIT_SEC * 1000); } else { // 'visible' if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } else { isPausingUpdate = false; @@ -466,11 +466,11 @@ onBeforeMount(() => { init().then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + window.setTimeout(toBottom, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる - setTimeout(() => { + window.setTimeout(() => { moreFetching.value = false; }, 2000); }); @@ -480,11 +480,11 @@ onBeforeMount(() => { onBeforeUnmount(() => { if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } if (preventAppearFetchMoreTimer.value) { - clearTimeout(preventAppearFetchMoreTimer.value); + window.clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } scrollObserver.value?.disconnect(); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 52d8c4abe9..954c3d0c78 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -140,7 +140,7 @@ import { DI } from '@/di.js'; const $i = ensureSignin(); -const modal = inject('modal'); +const modal = inject(DI.inModal, false); const props = withDefaults(defineProps<PostFormProps & { fixed?: boolean; diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 1fbf00d212..22ae563d13 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - <div :class="{ [$style.slotClip]: isPullStart }"> - <slot/> - </div> + + <slot/> </div> </template> @@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> { return; } const startTime = Date.now(); - let intervalId = setInterval(() => { + let intervalId = window.setInterval(() => { const time = Date.now() - startTime; if (time > RELEASE_TRANSITION_DURATION) { pullDistance.value = to; - clearInterval(intervalId); + window.clearInterval(intervalId); r(); return; } @@ -261,8 +260,4 @@ defineExpose({ margin: 5px 0; } } - -.slotClip { - overflow-y: clip; -} </style> diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index b7b1bf6750..8798c595d7 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -25,7 +25,7 @@ const props = defineProps<{ menuReaction?: boolean; }>(); -const react = inject(DI.mfmEmojiReactCallback); +const react = inject(DI.mfmEmojiReactCallback, null); const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts index c9af5f4ea4..15938d0495 100644 --- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -2,11 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { waitFor } from '@storybook/test'; -import type { StoryObj } from '@storybook/vue3'; import MkPageHeader from './MkPageHeader.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Empty = { render(args) { return { @@ -29,7 +28,7 @@ export const Empty = { }; }, async play() { - const wait = new Promise((resolve) => setTimeout(resolve, 800)); + const wait = new Promise((resolve) => window.setTimeout(resolve, 800)); await waitFor(async () => await wait); }, args: { diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 358a17d3e8..255fca8f86 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -133,7 +133,7 @@ async function enter(el: Element) { entering = false; }); - setTimeout(renderTab, 170); + window.setTimeout(renderTab, 170); } function afterEnter(el: Element) { diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 6a926f7718..6b524785e4 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -69,7 +69,6 @@ const emit = defineEmits<{ }>(); const viewId = inject(DI.viewId); -const viewTransitionName = computed(() => `${viewId}---pageHeader`); const injectedPageMetadata = inject(DI.pageMetadata); const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); @@ -130,7 +129,6 @@ onUnmounted(() => { backdrop-filter: var(--MI-blur, blur(15px)); border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; - view-transition-name: v-bind(viewTransitionName); } .upper, diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue index fb813689ba..7ea0b5c97f 100644 --- a/packages/frontend/src/components/global/PageWithHeader.vue +++ b/packages/frontend/src/components/global/PageWithHeader.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']"> +<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']"> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template> <div :class="$style.body"> @@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { useTemplateRef } from 'vue'; +import { scrollInContainer } from '@@/js/scroll.js'; import type { PageHeaderItem } from '@/types/page-header.js'; import type { Tab } from './MkPageHeader.tabs.vue'; @@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{ }); const tab = defineModel<string>('tab'); +const rootEl = useTemplateRef('rootEl'); + +defineExpose({ + scrollToTop: () => { + if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' }); + }, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 1c0c35f34e..78ac6900a3 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1); const rootEl = useTemplateRef('rootEl'); onMounted(() => { - rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 + if (prefer.s.animation) { + rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 + } }); // view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成 diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index 94f4f3dab1..c37f3df0d3 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, toRefs, watch } from 'vue'; +import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; +import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; +import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import type { GridColumn } from '@/components/grid/column.js'; +import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; +import type { MenuItem } from '@/types/menu.js'; import { GridEventEmitter } from '@/components/grid/grid.js'; import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; @@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js'; import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js'; import { handleKeyEvent } from '@/utility/key-event.js'; -import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; -import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; -import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; -import type { GridColumn } from '@/components/grid/column.js'; -import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; -import type { MenuItem } from '@/types/menu.js'; - type RowHolder = { row: GridRow, cells: GridCell[], @@ -130,7 +129,7 @@ const bus = new GridEventEmitter(); * * @see {@link onResize} */ -const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries))); +const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries))); const rootEl = ref<InstanceType<typeof HTMLTableElement>>(); /** diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index b58c8c9659..541cdb76a8 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -15,4 +15,5 @@ export const DI = { currentStickyTop: Symbol() as InjectionKey<Ref<number>>, currentStickyBottom: Symbol() as InjectionKey<Ref<number>>, mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>, + inModal: Symbol() as InjectionKey<boolean>, }; diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 7b25c394f3..ce219512da 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -49,7 +49,7 @@ function queueUpdated(q) { } function top() { - scroll(rootEl.value, { top: 0 }); + scrollInContainer(rootEl.value, { top: 0 }); } async function timetravel() { diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 843d2fd79b..ab57020613 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -242,6 +242,10 @@ function showMenu(ev: MouseEvent, contextmenu = false) { font-size: 80%; } +.fukidashi { + text-align: left; +} + .content { overflow: clip; overflow-wrap: break-word; diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index a7ff519a34..0a2bc02de5 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -6,17 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"> <MkSpacer :contentMax="800"> - <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> - <div v-if="tab === 'all'"> - <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> - </div> - <div v-else-if="tab === 'mentions'"> - <MkNotes :pagination="mentionsPagination"/> - </div> - <div v-else-if="tab === 'directNotes'"> - <MkNotes :pagination="directNotesPagination"/> - </div> - </MkHorizontalSwipe> + <div v-if="tab === 'all'"> + <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> + </div> + <div v-else-if="tab === 'mentions'"> + <MkNotes :pagination="mentionsPagination"/> + </div> + <div v-else-if="tab === 'directNotes'"> + <MkNotes :pagination="directNotesPagination"/> + </div> </MkSpacer> </PageWithHeader> </template> diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 403a760521..b7434bff9f 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -145,13 +145,13 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as Reversi from 'misskey-reversi'; +import { useInterval } from '@@/js/use-interval.js'; +import { url } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/utility/clone.js'; -import { useInterval } from '@@/js/use-interval.js'; import { ensureSignin } from '@/i.js'; -import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { userPage } from '@/filters/user.js'; @@ -301,7 +301,7 @@ if (!props.game.isEnded) { if (iAmPlayer.value) { if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { - props.connection!.send('claimTimeIsUp', {}); + props.connection!.send('claimTimeIsUp', {}); } } }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); @@ -424,7 +424,7 @@ function autoplay() { const tick = () => { const log = logs[i]; const time = log.time - previousLog.time; - setTimeout(() => { + window.setTimeout(() => { i++; logPos.value++; previousLog = log; diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 21dd2a1321..f7ab6f5f8c 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -788,7 +788,7 @@ function testNotification(): void { smashCount = 0; } if (smashTimer) { - clearTimeout(smashTimer); + window.clearTimeout(smashTimer); } smashTimer = window.setTimeout(() => { smashCount = 0; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index c9c1ababf2..0c80ae44e6 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -4,47 +4,43 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"> - <MkSpacer :contentMax="800"> - <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> - <div ref="rootEl"> - <MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> - {{ i18n.ts._timelineDescription[src] }} - </MkInfo> - <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div :class="$style.tl"> - <MkTimeline - ref="tlComponent" - :key="src + withRenotes + withReplies + onlyFiles + withSensitive" - :src="src.split(':')[0]" - :list="src.split(':')[1]" - :withRenotes="withRenotes" - :withReplies="withReplies" - :withSensitive="withSensitive" - :onlyFiles="onlyFiles" - :sound="true" - @queue="queueUpdated" - /> - </div> - </div> - </MkHorizontalSwipe> - </MkSpacer> -</PageWithHeader> +<div ref="rootEl" class="_pageScrollable"> + <MkStickyContainer> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"/></template> + <MkSpacer :contentMax="800"> + <MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> + {{ i18n.ts._timelineDescription[src] }} + </MkInfo> + <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="_panel" fixed style="margin-bottom: var(--MI-margin);"/> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <MkTimeline + ref="tlComponent" + :key="src + withRenotes + withReplies + onlyFiles + withSensitive" + :class="$style.tl" + :src="src.split(':')[0]" + :list="src.split(':')[1]" + :withRenotes="withRenotes" + :withReplies="withReplies" + :withSensitive="withSensitive" + :onlyFiles="onlyFiles" + :sound="true" + @queue="queueUpdated" + /> + </MkSpacer> + </MkStickyContainer> +</div> </template> <script lang="ts" setup> import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } from 'vue'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import type { MenuItem } from '@/types/menu.js'; import type { BasicTimelineType } from '@/timelines.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/utility/misskey-api.js'; import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/i.js'; @@ -133,7 +129,7 @@ function queueUpdated(q: number): void { } function top(): void { - if (rootEl.value) scroll(rootEl.value, { top: 0, behavior: 'smooth' }); + if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'instant' }); } async function chooseList(ev: MouseEvent): Promise<void> { diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 8f5244ca1a..53081b0f16 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import MkTimeline from '@/components/MkTimeline.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; @@ -54,7 +54,7 @@ function queueUpdated(q) { } function top() { - scroll(rootEl.value, { top: 0 }); + scrollInContainer(rootEl.value, { top: 0 }); } function settings() { diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 4841a78323..8ecac1dc8e 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -377,7 +377,7 @@ function onDrop(ev) { font-size: 0.9em; color: var(--MI_THEME-panelHeaderFg); background: var(--MI_THEME-panelHeaderBg); - box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider); + box-shadow: 0 0.5px 0 0 var(--MI_THEME-panelHeaderDivider); cursor: pointer; user-select: none; } diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 5e806cd73b..d367137692 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -13,10 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <XAnnouncements v-if="$i"/> <XStatusBars :class="$style.statusbars"/> </div> - <div :class="$style.content"> - <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/> - <RouterView v-else/> - </div> + <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/> + <RouterView v-else :class="$style.content"/> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> @@ -216,9 +214,6 @@ html, body { width: 100%; height: 100%; - position: fixed; - top: 0; - left: 0; overscroll-behavior: none; } diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts index f6ab587ae1..06b445ab0d 100644 --- a/packages/frontend/src/utility/achievements.ts +++ b/packages/frontend/src/utility/achievements.ts @@ -497,7 +497,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { if (claimedAchievements.includes(type)) return; claimingQueue.add(type); claimedAchievements.push(type); - await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500)); + await new Promise(resolve => window.setTimeout(resolve, (claimingQueue.size - 1) * 500)); window.setTimeout(() => { claimingQueue.delete(type); }, 500); diff --git a/packages/frontend/src/utility/confetti.ts b/packages/frontend/src/utility/confetti.ts index 8e53a6ceeb..c19149875f 100644 --- a/packages/frontend/src/utility/confetti.ts +++ b/packages/frontend/src/utility/confetti.ts @@ -15,11 +15,11 @@ export function confetti(options: { duration?: number; } = {}) { return Math.random() * (max - min) + min; } - const interval = setInterval(() => { + const interval = window.setInterval(() => { const timeLeft = animationEnd - Date.now(); if (timeLeft <= 0) { - return clearInterval(interval); + return window.clearInterval(interval); } const particleCount = 50 * (timeLeft / duration); diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts index 81fc28d7c8..d728cdfcb0 100644 --- a/packages/frontend/src/utility/hotkey.ts +++ b/packages/frontend/src/utility/hotkey.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHTMLElementOrNull } from "@/utility/get-dom-node-or-null.js"; +import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; //#region types export type Keymap = Record<string, CallbackFunction | CallbackObject>; @@ -136,7 +136,7 @@ let lastHotKeyStoreTimer: number | null = null; const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => { if (lastHotKeyStoreTimer != null) { - clearTimeout(lastHotKeyStoreTimer); + window.clearTimeout(lastHotKeyStoreTimer); } latestHotkey = { diff --git a/packages/frontend/src/utility/idle-render.ts b/packages/frontend/src/utility/idle-render.ts index 6adfedcb9f..32daa1df02 100644 --- a/packages/frontend/src/utility/idle-render.ts +++ b/packages/frontend/src/utility/idle-render.ts @@ -5,7 +5,7 @@ const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => { const start = performance.now(); - const timeoutId = setTimeout(() => { + const timeoutId = window.setTimeout(() => { callback({ didTimeout: false, // polyfill でタイムアウト発火することはない timeRemaining() { @@ -17,7 +17,7 @@ const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.re return timeoutId; }); const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => { - clearTimeout(timeoutId); + window.clearTimeout(timeoutId); }); class IdlingRenderScheduler { diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts index f217bdfcd5..d3f82a37f2 100644 --- a/packages/frontend/src/utility/sound.ts +++ b/packages/frontend/src/utility/sound.ts @@ -158,7 +158,7 @@ export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolea canPlay = false; return await playMisskeySfxFileInternal(soundStore).finally(() => { // ごく短時間に音が重複しないように - setTimeout(() => { + window.setTimeout(() => { canPlay = true; }, 25); }); @@ -230,10 +230,10 @@ export async function getSoundDuration(file: string): Promise<number> { const audioEl = window.document.createElement('audio'); audioEl.src = file; return new Promise((resolve) => { - const si = setInterval(() => { + const si = window.setInterval(() => { if (audioEl.readyState > 0) { resolve(audioEl.duration * 1000); - clearInterval(si); + window.clearInterval(si); audioEl.remove(); } }, 100); diff --git a/packages/frontend/src/utility/test-utils.ts b/packages/frontend/src/utility/test-utils.ts index 52bb2d94e0..54742c1a9e 100644 --- a/packages/frontend/src/utility/test-utils.ts +++ b/packages/frontend/src/utility/test-utils.ts @@ -5,5 +5,5 @@ export async function tick(): Promise<void> { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never); + await new Promise((globalThis.requestIdleCallback ?? window.setTimeout) as never); } diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index a1f46e3dd8..89955e0f01 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.3.2-beta.15", + "version": "2025.3.2-beta.16", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js",