Compare commits

...

30 Commits

Author SHA1 Message Date
syuilo cce88c904b
Update CHANGELOG.md 2025-03-29 22:16:22 +09:00
syuilo d866ab12e9
perf(frontend): reduce stacking context in deck 2025-03-29 22:00:01 +09:00
github-actions[bot] df75715d29 Bump version to 2025.3.2-beta.17 2025-03-29 12:23:35 +00:00
syuilo 02da241ec9 Revert "(test)"
This reverts commit eb4062cf63.
2025-03-29 21:16:25 +09:00
syuilo eb4062cf63 (test) 2025-03-29 21:02:31 +09:00
syuilo d9012740a1 enhance(frontend): アイコンのスクロール追従を無効化してパフォーマンス向上できるように 2025-03-29 20:56:59 +09:00
syuilo 1b776a7e7e perf(frontend): reduce stack contexts 2025-03-29 20:02:51 +09:00
syuilo e0b7c56458 Revert "test"
This reverts commit 2787158a04.
2025-03-29 19:23:30 +09:00
syuilo 2787158a04 test 2025-03-29 18:41:52 +09:00
syuilo 2bbc0878e7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-29 18:03:34 +09:00
syuilo fb1542429f 🎨 2025-03-29 18:03:31 +09:00
github-actions[bot] 05b23eda59 Bump version to 2025.3.2-beta.16 2025-03-29 09:01:05 +00:00
syuilo ddd6d72dd7
New Crowdin updates (#15716)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)
2025-03-29 17:59:40 +09:00
syuilo 25db8c2fa9 🎨 2025-03-29 17:59:09 +09:00
syuilo 2ad7b010e4 🎨 2025-03-29 17:57:03 +09:00
syuilo 7c06ffc422 refactor 2025-03-29 17:28:20 +09:00
syuilo df3ed93f84 clean up 2025-03-29 17:15:47 +09:00
syuilo b030e33856 perf(frontend): improve performance of timeline page 2025-03-29 17:15:31 +09:00
syuilo 7fd3adedee fix tests 2025-03-29 17:00:01 +09:00
syuilo ae59578115 refactor 2025-03-29 16:55:12 +09:00
syuilo 609a37742c clean up 2025-03-29 16:11:15 +09:00
syuilo d9d796b204 lint fixes 2025-03-29 16:09:27 +09:00
syuilo 6c2c3f08be refactor(frontend): use symbol for di 2025-03-29 16:04:01 +09:00
syuilo e5e4390494 fix(frontend): suppress inject warn 2025-03-29 16:01:51 +09:00
syuilo 5a09e7a8b4 lint 2025-03-29 15:57:34 +09:00
syuilo 88e6bd1533 Update eslint.config.js 2025-03-29 15:55:22 +09:00
syuilo 7d8c98767a lint 2025-03-29 15:53:44 +09:00
syuilo 490222fb78 perf(frontend): avoid needless AsyncComponentWrapper 2025-03-29 15:33:19 +09:00
syuilo 1af4081090 enhance(frontend): disable horizontal swipe for timeline/notifications to improve ux 2025-03-29 15:00:29 +09:00
syuilo 33e76f9dfc Revert "🎨"
This reverts commit 3451c9a0de.
2025-03-29 13:55:05 +09:00
54 changed files with 434 additions and 286 deletions

View File

@ -38,6 +38,7 @@
- Enhance: プラグインの管理が強化されました
- インストール/アンインストール/設定の変更時にリロード不要になりました
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
- Enhance: アイコンのスクロール追従を無効化してパフォーマンス向上できるように
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
- Enhance: テーマ設定画面のデザインを改善
@ -46,6 +47,8 @@
- 文字数カウントを復活
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
- Enhance: 全体的なブラッシュアップ
- Enhance 全体的なパフォーマンス向上
- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
### Server

View File

@ -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"

View File

@ -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"
@ -1331,12 +1332,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"
@ -1362,6 +1406,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."
@ -1871,6 +1921,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"
@ -2101,6 +2152,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."
@ -2248,6 +2300,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?"
@ -2496,6 +2549,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"
@ -2524,6 +2578,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"
@ -2663,6 +2718,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"

4
locales/index.d.ts vendored
View File

@ -5634,6 +5634,10 @@ export interface Locale extends ILocale {
*
*/
"makeEveryTextElementsSelectable_description": string;
/**
*
*/
"useStickyIcons": string;
/**
*
*/

View File

@ -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"

View File

@ -1409,6 +1409,7 @@ _settings:
timelineAndNote: "タイムラインとノート"
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
useStickyIcons: "アイコンをスクロールに追従させる"
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
ifOn: "オンのとき"
ifOff: "オフのとき"

View File

@ -1335,6 +1335,7 @@ information: "关于"
chat: "聊天"
migrateOldSettings: "迁移旧设置信息"
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
compress: "压缩"
_chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
@ -1363,6 +1364,8 @@ _chat:
newline: "换行"
muteThisRoom: "静音此房间"
deleteRoom: "删除房间"
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。"
cannotChatWithTheUser: "无法与此用户聊天"
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
chatWithThisUser: "聊天"

View File

@ -1335,6 +1335,7 @@ information: "關於"
chat: "聊天"
migrateOldSettings: "遷移舊設定資訊"
migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。"
compress: "壓縮"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@ -1363,6 +1364,8 @@ _chat:
newline: "換行"
muteThisRoom: "此聊天室已靜音"
deleteRoom: "刪除聊天室"
chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。"
chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。"
cannotChatWithTheUser: "無法與此使用者聊天"
cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。"
chatWithThisUser: "聊天"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.3.2-beta.15",
"version": "2025.3.2-beta.17",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -463,6 +463,7 @@ export class WebhookTestService {
followersVisibility: 'public',
followingVisibility: 'public',
chatScope: 'mutual',
canChat: true,
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,

View File

@ -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);

View File

@ -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' });
}

View File

@ -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 });
}
/**

View File

@ -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 という変数名を定義し忘れている',

View File

@ -29,7 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.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_) {
@ -263,7 +263,7 @@ export async function common(createVue: () => App<Element>) {
});
});
const app = createVue();
const app = await createVue();
if (_DEV_) {
app.config.performance = true;

View File

@ -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;
}

View File

@ -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();
}

View File

@ -3,19 +3,17 @@
* 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 = {

View File

@ -3,17 +3,15 @@
* 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 = {

View File

@ -79,7 +79,7 @@ function opening() {
picker.value?.focus();
//
setTimeout(() => {
window.setTimeout(() => {
picker.value?.focus();
}, 10);
}

View File

@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) {
pullDistance.value = 0;
isSwiping.value = false;
setTimeout(() => {
window.setTimeout(() => {
isSwipingForClass.value = false;
}, 400);

View File

@ -339,7 +339,7 @@ const bufferedDataRatio = computed(() => {
// MediaControl Events
function onMouseOver() {
if (controlStateTimer) {
clearTimeout(controlStateTimer);
window.clearTimeout(controlStateTimer);
}
isHoverring.value = true;
}

View File

@ -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);

View File

@ -14,8 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :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>
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@ -47,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="$style.main">
<MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
@ -852,9 +850,12 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 14px 0 0;
width: 58px;
height: 58px;
position: sticky !important;
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
&.useSticky {
position: sticky !important;
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
}
.main {
@ -1041,7 +1042,10 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 10px 0 0;
width: 46px;
height: 46px;
top: calc(14px + var(--MI-stickyTop, 0px));
&.useSticky {
top: calc(14px + var(--MI-stickyTop, 0px));
}
}
}

View File

@ -4,41 +4,49 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkLoading v-if="fetching"/>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
mode="out-in"
>
<MkLoading v-if="fetching"/>
<MkError v-else-if="error" @retry="init()"/>
<MkError v-else-if="error" @retry="init()"/>
<div v-else-if="empty" key="_empty_" class="empty">
<slot name="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/>
<div>{{ i18n.ts.nothing }}</div>
<div v-else-if="empty" key="_empty_">
<slot name="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</slot>
</div>
<div v-else ref="rootEl" class="_gaps">
<div v-show="pagination.reversed && more" key="_more_">
<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/>
</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/>
</div>
</slot>
</div>
<div v-else ref="rootEl" class="_gaps">
<div v-show="pagination.reversed && more" key="_more_">
<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"/>
</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"/>
</div>
</div>
</Transition>
</template>
<script lang="ts">
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 } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -250,7 +258,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' });
}
@ -349,7 +357,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
clearTimeout(timerForSetPause);
window.clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@ -445,11 +453,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
setTimeout(toBottom, 800);
window.setTimeout(toBottom, 800);
// scrollToBottommoreFetching
// more = true
setTimeout(() => {
window.setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@ -459,11 +467,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();
@ -483,6 +491,15 @@ defineExpose({
</script>
<style lang="scss" module>
.transition_fade_enterActive,
.transition_fade_leaveActive {
transition: opacity 0.125s ease;
}
.transition_fade_enterFrom,
.transition_fade_leaveTo {
opacity: 0;
}
.more {
margin-left: auto;
margin-right: auto;

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -3,10 +3,9 @@
* 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: {

View File

@ -133,7 +133,7 @@ async function enter(el: Element) {
entering = false;
});
setTimeout(renderTab, 170);
window.setTimeout(renderTab, 170);
}
function afterEnter(el: Element) {

View File

@ -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,

View File

@ -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>

View File

@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1);
const rootEl = useTemplateRef('rootEl');
onMounted(() => {
rootEl.value.style.viewTransitionName = viewId; // view-transition-namecss var使
if (prefer.s.animation) {
rootEl.value.style.viewTransitionName = viewId; // view-transition-namecss var使
}
});
// view-transition-new<pt-name-selector>css var使v-bind

View File

@ -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>>();
/**

View File

@ -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>,
};

View File

@ -13,6 +13,8 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
import { i18n } from '@/i18n.js';
@ -23,8 +25,6 @@ import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { pleaseLogin } from '@/utility/please-login.js';
import { showMovedDialog } from '@/utility/show-moved-dialog.js';

View File

@ -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() {

View File

@ -242,6 +242,10 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
font-size: 80%;
}
.fukidashi {
text-align: left;
}
.content {
overflow: clip;
overflow-wrap: break-word;

View File

@ -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>

View File

@ -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;

View File

@ -42,22 +42,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<div class="_gaps_s">
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
<MkPreferenceContainer k="showAvatarDecorations">
<MkSwitch v-model="showAvatarDecorations">
@ -395,40 +379,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['datasaver']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
<template #icon><i class="ti ti-antenna-bars-3"></i></template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
<div class="_buttons">
<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
</div>
<div class="_gaps_m">
<MkSwitch v-model="dataSaver.media">
{{ i18n.ts._dataSaver._media.title }}
<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.avatar">
{{ i18n.ts._dataSaver._avatar.title }}
<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.urlPreview">
{{ i18n.ts._dataSaver._urlPreview.title }}
<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.code">
{{ i18n.ts._dataSaver._code.title }}
<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
</MkSwitch>
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['chat', 'messaging']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
@ -468,6 +418,76 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['performance']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.performance }}</SearchLabel></template>
<template #icon><i class="ti ti-battery-vertical-eco"></i></template>
<div class="_gaps_s">
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['sticky']">
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['datasaver']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
<template #icon><i class="ti ti-antenna-bars-3"></i></template>
<div class="_gaps_m">
<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
<div class="_buttons">
<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
</div>
<div class="_gaps_m">
<MkSwitch v-model="dataSaver.media">
{{ i18n.ts._dataSaver._media.title }}
<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.avatar">
{{ i18n.ts._dataSaver._avatar.title }}
<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.urlPreview">
{{ i18n.ts._dataSaver._urlPreview.title }}
<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
</MkSwitch>
<MkSwitch v-model="dataSaver.code">
{{ i18n.ts._dataSaver._code.title }}
<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
</MkSwitch>
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['other']">
<MkFolder>
<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
@ -650,6 +670,7 @@ const useBlurEffect = prefer.model('useBlurEffect');
const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies');
const chatShowSenderName = prefer.model('chat.showSenderName');
const chatSendOnEnter = prefer.model('chat.sendOnEnter');
const useStickyIcons = prefer.model('useStickyIcons');
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@ -678,6 +699,7 @@ watch([
highlightSensitiveMedia,
enableSeasonalScreenEffect,
chatShowSenderName,
useStickyIcons,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
@ -786,7 +808,7 @@ function testNotification(): void {
smashCount = 0;
}
if (smashTimer) {
clearTimeout(smashTimer);
window.clearTimeout(smashTimer);
}
smashTimer = window.setTimeout(() => {
smashCount = 0;

View File

@ -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> {

View File

@ -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() {

View File

@ -198,6 +198,9 @@ export const PREF_DEF = {
useBlurEffect: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
useStickyIcons: {
default: true,
},
showFixedPostForm: {
default: false,
},

View File

@ -226,9 +226,6 @@ html,
body {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overscroll-behavior: none;
}
@ -241,9 +238,6 @@ body {
width: 100%;
height: 100%;
overflow: clip;
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@ -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;
}

View File

@ -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);

View File

@ -290,51 +290,41 @@ export const searchIndexes: SearchIndexItem[] = [
},
{
id: 'lfI3yMX9g',
label: i18n.ts.useBlurEffect,
keywords: ['blur'],
},
{
id: '31Y4IcGEf',
label: i18n.ts.useBlurEffectForModal,
keywords: ['blur', 'modal'],
},
{
id: '78q2asrLS',
label: i18n.ts.showAvatarDecorations,
keywords: ['avatar', 'icon', 'decoration', 'show'],
},
{
id: 'zydOfGYip',
id: '31Y4IcGEf',
label: i18n.ts.alwaysConfirmFollow,
keywords: ['follow', 'confirm', 'always'],
},
{
id: 'wqpOC22Zm',
id: '78q2asrLS',
label: i18n.ts.highlightSensitiveMedia,
keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
},
{
id: 'c98gbF9c6',
id: 'zydOfGYip',
label: i18n.ts.confirmWhenRevealingSensitiveMedia,
keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
},
{
id: '4LxdiOMNh',
id: 'wqpOC22Zm',
label: i18n.ts.enableAdvancedMfm,
keywords: ['mfm', 'enable', 'show', 'advanced'],
},
{
id: '9gTCaLkIf',
id: 'c98gbF9c6',
label: i18n.ts.enableInfiniteScroll,
keywords: ['auto', 'load', 'auto', 'more', 'scroll'],
},
{
id: 'jmJT0twuJ',
id: '6ANRSOaNg',
label: i18n.ts.emojiStyle,
keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
},
{
id: 'igFN7RIUa',
id: 'wo0s0CaI1',
label: i18n.ts.pinnedList,
keywords: ['pinned', 'list'],
},
@ -343,85 +333,85 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['general'],
},
{
id: 'ufc2X9voy',
id: 'l78F2W9Ok',
children: [
{
id: 'd2H4E5ys6',
id: 'iOJ3Crlky',
label: i18n.ts.showFixedPostForm,
keywords: ['post', 'form', 'timeline'],
},
{
id: '1LHOhDKGW',
id: 'CQldliCSi',
label: i18n.ts.showFixedPostFormInChannel,
keywords: ['post', 'form', 'timeline', 'channel'],
},
{
id: 'DSzwvTp7i',
id: 'd2H4E5ys6',
label: i18n.ts.collapseRenotes,
keywords: ['renote', i18n.ts.collapseRenotesDescription],
},
{
id: 'jb3HUeyrx',
id: 'yb11lSY1G',
label: i18n.ts.showGapBetweenNotesInTimeline,
keywords: ['note', 'timeline', 'gap'],
},
{
id: '2LNjwv1cr',
id: 'fL49Zxe9i',
label: i18n.ts.disableStreamingTimeline,
keywords: ['disable', 'streaming', 'timeline'],
},
{
id: '7W6g8Dcqz',
id: 'ykifk3NHS',
label: i18n.ts.showNoteActionsOnlyHover,
keywords: ['hover', 'show', 'footer', 'action'],
},
{
id: 'uAOoH3LFF',
id: 'tLGyaQagB',
label: i18n.ts.showClipButtonInNoteFooter,
keywords: ['footer', 'action', 'clip', 'show'],
},
{
id: 'eCiyZLC8n',
id: '7W6g8Dcqz',
label: i18n.ts.showReactionsCount,
keywords: ['reaction', 'count', 'show'],
},
{
id: '68u9uRmFP',
id: 'uAOoH3LFF',
label: i18n.ts.confirmOnReact,
keywords: ['reaction', 'confirm'],
},
{
id: 'rHWm4sXIe',
id: 'eCiyZLC8n',
label: i18n.ts.loadRawImages,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
},
{
id: '9L2XGJw7e',
id: '68u9uRmFP',
label: i18n.ts.useReactionPickerForContextMenu,
keywords: ['reaction', 'picker', 'contextmenu', 'open'],
},
{
id: 'uIMCIK7kG',
id: 'yxehrHZ6x',
label: i18n.ts.reactionsDisplaySize,
keywords: ['reaction', 'size', 'scale', 'display'],
},
{
id: 'uMckjO9bz',
id: 'gi8ILaE2Z',
label: i18n.ts.limitWidthOfReaction,
keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
},
{
id: 'yeghU4qiH',
id: 'cEQJZ7DQG',
label: i18n.ts.mediaListWithOneImageAppearance,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
},
{
id: 'yYSOPoAKE',
id: 'haX4QVulD',
label: i18n.ts.instanceTicker,
keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
},
{
id: 'iOHiIu32L',
id: 'pneYnQekL',
label: i18n.ts.displayOfSensitiveMedia,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
},
@ -430,25 +420,25 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['timeline', 'note'],
},
{
id: 'eROFRMtXv',
id: 'eJ2jme16W',
children: [
{
id: 'BaQfrVO82',
id: 'ErMQr6LQk',
label: i18n.ts.keepCw,
keywords: ['remember', 'keep', 'note', 'cw'],
},
{
id: 'vFerPo2he',
id: 'zrJicawH9',
label: i18n.ts.rememberNoteVisibility,
keywords: ['remember', 'keep', 'note', 'visibility'],
},
{
id: 'dcAC0yJcH',
id: 'BaQfrVO82',
label: i18n.ts.enableQuickAddMfmFunction,
keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
},
{
id: 'bECeWZVMb',
id: 'C2WYcVM1d',
label: i18n.ts.defaultNoteVisibility,
keywords: ['default', 'note', 'visibility'],
},
@ -457,20 +447,20 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['post', 'form'],
},
{
id: 'tsSP93Cc6',
id: 'sQXSA6gik',
children: [
{
id: 'dtw8FepYL',
id: 'rICn8stqk',
label: i18n.ts.useGroupedNotifications,
keywords: ['group'],
},
{
id: 'eb0yCYJTn',
id: 'xFmAg2tDe',
label: i18n.ts.position,
keywords: ['position'],
},
{
id: '1Spt4Gpr5',
id: 'Ek4Cw3VPq',
label: i18n.ts.stackAxis,
keywords: ['stack', 'axis', 'direction'],
},
@ -479,20 +469,15 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['notification'],
},
{
id: 'SYmWxGOF',
label: i18n.ts.dataSaver,
keywords: ['datasaver'],
},
{
id: 'vPQPvmntL',
id: 'gDVCqZfxm',
children: [
{
id: 'zZxyXHk3A',
id: 'ei8Ix3s4S',
label: i18n.ts._settings._chat.showSenderName,
keywords: ['show', 'sender', 'name'],
},
{
id: 'omEy5Q3Ev',
id: '2E7vdIUQd',
label: i18n.ts._settings._chat.sendOnEnter,
keywords: ['send', 'enter', 'newline'],
},
@ -501,50 +486,77 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['chat', 'messaging'],
},
{
id: '5fy7VEy6i',
id: '96LnS1sxB',
children: [
{
id: 'EosiWZvak',
id: '5h8vhCX1S',
label: i18n.ts.turnOffToImprovePerformance,
keywords: ['blur'],
},
{
id: 'Cbjosj3TG',
label: i18n.ts.turnOffToImprovePerformance,
keywords: ['blur', 'modal'],
},
{
id: 'BKndoHcCj',
label: i18n.ts.turnOffToImprovePerformance,
keywords: ['sticky'],
},
],
label: i18n.ts.performance,
keywords: ['performance'],
},
{
id: '4yCgcFElF',
label: i18n.ts.dataSaver,
keywords: ['datasaver'],
},
{
id: 'DILm2LlCn',
children: [
{
id: 'Fd0rFTSry',
label: i18n.ts.squareAvatars,
keywords: ['avatar', 'icon', 'square'],
},
{
id: 'qY5xTzl35',
id: 'xNsLokqeA',
label: i18n.ts.seasonalScreenEffect,
keywords: ['effect', 'show'],
},
{
id: '2VSnj81vC',
id: 'sZcalFBE8',
label: i18n.ts.openImageInNewTab,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
},
{
id: 'hdQa7W2H1',
id: 'Eh7vTluDO',
label: i18n.ts.withRepliesByDefaultForNewlyFollowed,
keywords: ['follow', 'replies'],
},
{
id: 'nnj4DkjhP',
id: 'vTRSKf1JA',
label: i18n.ts.whenServerDisconnected,
keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
},
{
id: 'Eh7vTluDO',
id: 'zlO5cBZFH',
label: i18n.ts.numberOfPageCache,
keywords: ['cache', 'page'],
},
{
id: 'vTRSKf1JA',
id: 'huQ8nc4iD',
label: i18n.ts.forceShowAds,
keywords: ['ad', 'show'],
},
{
id: 'dwhQfcLGt',
id: 'nJWfqwQ4R',
label: i18n.ts.hemisphere,
keywords: [],
},
{
id: 'Ar1lj7f7U',
id: 'kwEEgTlwR',
label: i18n.ts.additionalEmojiDictionary,
keywords: ['emoji', 'dictionary', 'additional', 'extra'],
},

View File

@ -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);

View File

@ -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 = {

View File

@ -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 {

View File

@ -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);

View File

@ -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);
}

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.3.2-beta.15",
"version": "2025.3.2-beta.17",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",