diff --git a/locales/index.d.ts b/locales/index.d.ts index e564b47270..c07e564dd7 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2322,6 +2322,10 @@ export interface Locale extends ILocale { * 新しいノートがあります */ "newNoteRecived": string; + /** + * 新しいノート + */ + "newNote": string; /** * サウンド */ @@ -4970,10 +4974,6 @@ export interface Locale extends ILocale { * 引っ張ってリロード */ "pullDownToRefresh": string; - /** - * タイムラインのリアルタイム更新を無効にする - */ - "disableStreamingTimeline": string; /** * 通知をグルーピング */ @@ -5417,6 +5417,18 @@ export interface Locale extends ILocale { * スクロールして閉じる */ "scrollToClose": string; + /** + * リアルタイムモード + */ + "realtimeMode": string; + /** + * オンにする + */ + "turnItOn": string; + /** + * オフにする + */ + "turnItOff": string; "_chat": { /** * まだメッセージはありません @@ -5721,6 +5733,22 @@ export interface Locale extends ILocale { * マウスでは、ホイールを押し込みながらドラッグします。 */ "enablePullToRefresh_description": string; + /** + * サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。 + */ + "realtimeMode_description": string; + /** + * コンテンツの取得頻度 + */ + "contentsUpdateFrequency": string; + /** + * 高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。 + */ + "contentsUpdateFrequency_description": string; + /** + * リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。 + */ + "contentsUpdateFrequency_description2": string; "_chat": { /** * 送信者の名前を表示 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7d2edf7194..0e338c091e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -576,6 +576,7 @@ showFixedPostForm: "タイムライン上部に投稿フォームを表示する showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)" withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする" newNoteRecived: "新しいノートがあります" +newNote: "新しいノート" sounds: "サウンド" sound: "サウンド" listen: "聴く" @@ -1238,7 +1239,6 @@ showAvatarDecorations: "アイコンのデコレーションを表示" releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" -disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" useGroupedNotifications: "通知をグルーピング" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" @@ -1349,6 +1349,9 @@ goToDeck: "デッキへ戻る" federationJobs: "連合ジョブ" driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。
\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。
\nファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。
\nフォルダを作って整理することもできます。" scrollToClose: "スクロールして閉じる" +realtimeMode: "リアルタイムモード" +turnItOn: "オンにする" +turnItOff: "オフにする" _chat: noMessagesYet: "まだメッセージはありません" @@ -1430,6 +1433,10 @@ _settings: enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期" enablePullToRefresh: "ひっぱって更新" enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。" + realtimeMode_description: "サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。" + contentsUpdateFrequency: "コンテンツの取得頻度" + contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。" + contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。" _chat: showSenderName: "送信者の名前を表示" diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 97f1c3d739..491e63d417 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -593,4 +593,42 @@ export class NoteEntityService implements OnModuleInit { relations: ['user'], }); } + + @bindThis + public async fetchDiffs(noteIds: MiNote['id'][]) { + if (noteIds.length === 0) return []; + + const notes = await this.notesRepository.find({ + where: { + id: In(noteIds), + }, + select: { + id: true, + userHost: true, + reactions: true, + reactionAndUserPairCache: true, + }, + }); + + const bufferedReactionsMap = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(noteIds) : null; + + const packings = notes.map(note => { + const bufferedReactions = bufferedReactionsMap?.get(note.id); + //const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); + + const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.deltas ?? {})); + + const reactionEmojiNames = Object.keys(reactions) + .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ + .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); + + return this.customEmojiService.populateEmojis(reactionEmojiNames, note.userHost).then(reactionEmojis => ({ + id: note.id, + reactions, + reactionEmojis, + })); + }); + + return await Promise.all(packings); + } } diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index e5170aa2dc..bd466b3cad 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -323,6 +323,7 @@ export * as 'notes/replies' from './endpoints/notes/replies.js'; export * as 'notes/search' from './endpoints/notes/search.js'; export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js'; export * as 'notes/show' from './endpoints/notes/show.js'; +export * as 'notes/show-partial-bulk' from './endpoints/notes/show-partial-bulk.js'; export * as 'notes/state' from './endpoints/notes/state.js'; export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js'; export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts new file mode 100644 index 0000000000..87b368e17e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' }, maxItems: 100, minItems: 1 }, + }, + required: ['noteIds'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private noteEntityService: NoteEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.noteEntityService.fetchDiffs(ps.noteIds); + }); + } +} diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index fad6ce3825..ae4e0445db 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -79,39 +79,6 @@ export async function mainBoot() { } } - const stream = useStream(); - - let reloadDialogShowing = false; - stream.on('_disconnected_', async () => { - if (prefer.s.serverDisconnectedBehavior === 'reload') { - window.location.reload(); - } else if (prefer.s.serverDisconnectedBehavior === 'dialog') { - if (reloadDialogShowing) return; - reloadDialogShowing = true; - const { canceled } = await confirm({ - type: 'warning', - title: i18n.ts.disconnectedFromServer, - text: i18n.ts.reloadConfirm, - }); - reloadDialogShowing = false; - if (!canceled) { - window.location.reload(); - } - } - }); - - stream.on('emojiAdded', emojiData => { - addCustomEmoji(emojiData.emoji); - }); - - stream.on('emojiUpdated', emojiData => { - updateCustomEmojis(emojiData.emojis); - }); - - stream.on('emojiDeleted', emojiData => { - removeCustomEmojis(emojiData.emojis); - }); - launchPlugins(); try { @@ -169,8 +136,6 @@ export async function mainBoot() { } } - stream.on('announcementCreated', onAnnouncementCreated); - if ($i.isDeleted) { alert({ type: 'warning', @@ -348,50 +313,81 @@ export async function mainBoot() { } } - const main = markRaw(stream.useChannel('main', null, 'System')); + if (store.s.realtimeMode) { + const stream = useStream(); - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - updateCurrentAccountPartial(i); - }); - - main.on('readAllNotifications', () => { - updateCurrentAccountPartial({ - hasUnreadNotification: false, - unreadNotificationsCount: 0, + let reloadDialogShowing = false; + stream.on('_disconnected_', async () => { + if (prefer.s.serverDisconnectedBehavior === 'reload') { + window.location.reload(); + } else if (prefer.s.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await confirm({ + type: 'warning', + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, + }); + reloadDialogShowing = false; + if (!canceled) { + window.location.reload(); + } + } }); - }); - main.on('unreadNotification', () => { - const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; - updateCurrentAccountPartial({ - hasUnreadNotification: true, - unreadNotificationsCount, + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); }); - }); - main.on('unreadAntenna', () => { - updateCurrentAccountPartial({ hasUnreadAntenna: true }); - sound.playMisskeySfx('antenna'); - }); + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); - main.on('newChatMessage', () => { - updateCurrentAccountPartial({ hasUnreadChatMessages: true }); - sound.playMisskeySfx('chatMessage'); - }); + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); - main.on('readAllAnnouncements', () => { - updateCurrentAccountPartial({ hasUnreadAnnouncement: false }); - }); + stream.on('announcementCreated', onAnnouncementCreated); - // 個人宛てお知らせが発行されたとき - main.on('announcementCreated', onAnnouncementCreated); + const main = markRaw(stream.useChannel('main', null, 'System')); - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - signout(); - }); + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + updateCurrentAccountPartial(i); + }); + + main.on('readAllNotifications', () => { + updateCurrentAccountPartial({ + hasUnreadNotification: false, + unreadNotificationsCount: 0, + }); + }); + + main.on('unreadNotification', () => { + const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; + updateCurrentAccountPartial({ + hasUnreadNotification: true, + unreadNotificationsCount, + }); + }); + + main.on('unreadAntenna', () => { + updateCurrentAccountPartial({ hasUnreadAntenna: true }); + sound.playMisskeySfx('antenna'); + }); + + main.on('newChatMessage', () => { + updateCurrentAccountPartial({ hasUnreadChatMessages: true }); + sound.playMisskeySfx('chatMessage'); + }); + + main.on('readAllAnnouncements', () => { + updateCurrentAccountPartial({ hasUnreadAnnouncement: false }); + }); + + // 個人宛てお知らせが発行されたとき + main.on('announcementCreated', onAnnouncementCreated); + } } // shortcut diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue index fdb7d2a1c4..272fab194a 100644 --- a/packages/frontend/src/components/MkChannelList.vue +++ b/packages/frontend/src/components/MkChannelList.vue @@ -19,14 +19,14 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 9adc3d98da..bc4cafb520 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -4,489 +4,81 @@ SPDX-License-Identifier: AGPL-3.0-only --> - diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 2d3ec45bca..359ee08812 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -29,19 +29,21 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { useTooltip } from '@/use/use-tooltip.js'; import { $i } from '@/i.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; -import { claimAchievement } from '@/utility/achievements.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/utility/sound.js'; import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; +import { noteEvents } from '@/use/use-note-capture.js'; const props = defineProps<{ + noteId: Misskey.entities.Note['id']; reaction: string; + reactionEmojis: Misskey.entities.Note['reactionEmojis']; + myReaction: Misskey.entities.Note['myReaction']; count: number; isInitial: boolean; - note: Misskey.entities.Note; }>(); const mock = inject(DI.mock, false); @@ -56,14 +58,16 @@ const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); const canToggle = computed(() => { - return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); + // TODO + //return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); + return !props.reaction.match(/@\w/) && $i && emoji.value; }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); async function toggleReaction() { if (!canToggle.value) return; - const oldReaction = props.note.myReaction; + const oldReaction = props.myReaction; if (oldReaction) { const confirm = await os.confirm({ type: 'warning', @@ -81,12 +85,23 @@ async function toggleReaction() { } misskeyApi('notes/reactions/delete', { - noteId: props.note.id, + noteId: props.noteId, }).then(() => { + noteEvents.emit(`unreacted:${props.noteId}`, { + userId: $i!.id, + reaction: props.reaction, + emoji: emoji.value, + }); if (oldReaction !== props.reaction) { misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: props.noteId, reaction: props.reaction, + }).then(() => { + noteEvents.emit(`reacted:${props.noteId}`, { + userId: $i!.id, + reaction: props.reaction, + emoji: emoji.value, + }); }); } }); @@ -108,12 +123,19 @@ async function toggleReaction() { } misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: props.noteId, reaction: props.reaction, + }).then(() => { + noteEvents.emit(`reacted:${props.noteId}`, { + userId: $i!.id, + reaction: props.reaction, + emoji: emoji.value, + }); }); - if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { - claimAchievement('reactWithoutRead'); - } + // TODO: 上位コンポーネントでやる + //if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + // claimAchievement('reactWithoutRead'); + //} } } @@ -157,7 +179,7 @@ onMounted(() => { if (!mock) { useTooltip(buttonEl, async (showing) => { const reactions = await misskeyApiGet('notes/reactions', { - noteId: props.note.id, + noteId: props.noteId, type: props.reaction, limit: 10, _cacheKey_: props.count, diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index e8cf6c36db..725978179e 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -13,7 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_x_move" tag="div" :class="$style.root" > - + @@ -27,7 +37,10 @@ import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ - note: Misskey.entities.Note; + noteId: Misskey.entities.Note['id']; + reactions: Misskey.entities.Note['reactions']; + reactionEmojis: Misskey.entities.Note['reactionEmojis']; + myReaction: Misskey.entities.Note['myReaction']; maxNumber?: number; }>(), { maxNumber: Infinity, @@ -39,33 +52,33 @@ const emit = defineEmits<{ (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; }>(); -const initialReactions = new Set(Object.keys(props.note.reactions)); +const initialReactions = new Set(Object.keys(props.reactions)); -const reactions = ref<[string, number][]>([]); +const _reactions = ref<[string, number][]>([]); const hasMoreReactions = ref(false); -if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { - reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; +if (props.myReaction && !Object.keys(_reactions.value).includes(props.myReaction)) { + _reactions.value[props.myReaction] = props.reactions[props.myReaction]; } function onMockToggleReaction(emoji: string, count: number) { if (!mock) return; - const i = reactions.value.findIndex((item) => item[0] === emoji); + const i = _reactions.value.findIndex((item) => item[0] === emoji); if (i < 0) return; - emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); + emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1])); } -watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { +watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions.value = Object.keys(newSource).length > maxNumber; - for (let i = 0; i < reactions.value.length; i++) { - const reaction = reactions.value[i][0]; + for (let i = 0; i < _reactions.value.length; i++) { + const reaction = _reactions.value[i][0]; if (reaction in newSource && newSource[reaction] !== 0) { - reactions.value[i][1] = newSource[reaction]; - newReactions.push(reactions.value[i]); + _reactions.value[i][1] = newSource[reaction]; + newReactions.push(_reactions.value[i]); } } @@ -79,11 +92,11 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe newReactions = newReactions.slice(0, props.maxNumber); - if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { - newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); + if (props.myReaction && !newReactions.map(([x]) => x).includes(props.myReaction)) { + newReactions.push([props.myReaction, newSource[props.myReaction]]); } - reactions.value = newReactions; + _reactions.value = newReactions; }, { immediate: true, deep: true }); diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue index cb50df1743..abe6466971 100644 --- a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue +++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index 59e1b096ae..95f53e7635 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -76,8 +76,6 @@ const onceReacted = ref(false); function addReaction(emoji) { onceReacted.value = true; emit('reacted'); - exampleNote.reactions[emoji] = 1; - exampleNote.myReaction = emoji; doNotification(emoji); } diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index aaefa5036a..8ec48dcc3f 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index 680fe08c14..62a220d2f1 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -27,7 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
diff --git a/packages/frontend/src/pref-migrate.ts b/packages/frontend/src/pref-migrate.ts index 414bb9c5aa..a5e915f66e 100644 --- a/packages/frontend/src/pref-migrate.ts +++ b/packages/frontend/src/pref-migrate.ts @@ -115,7 +115,6 @@ export function migrateOldSettings() { prefer.commit('notificationStackAxis', store.s.notificationStackAxis); prefer.commit('enableCondensedLine', store.s.enableCondensedLine); prefer.commit('keepScreenOn', store.s.keepScreenOn); - prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline); prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications); prefer.commit('dataSaver', store.s.dataSaver); prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect); diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 37c7098511..62f4ff8e1a 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -241,6 +241,12 @@ export const PREF_DEF = { numberOfPageCache: { default: 3, }, + pollingInterval: { + // 1 ... 低 + // 2 ... 中 + // 3 ... 高 + default: 2, + }, showNoteActionsOnlyHover: { default: false, }, @@ -277,9 +283,6 @@ export const PREF_DEF = { keepScreenOn: { default: false, }, - disableStreamingTimeline: { - default: false, - }, useGroupedNotifications: { default: true, }, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 5ff9c1c7fe..06c2f9149c 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -80,6 +80,10 @@ export const store = markRaw(new Pizzax('base', { where: 'device', default: false, }, + realtimeMode: { + where: 'device', + default: true, + }, recentlyUsedEmojis: { where: 'device', default: [] as string[], @@ -378,10 +382,6 @@ export const store = markRaw(new Pizzax('base', { where: 'device', default: false, }, - disableStreamingTimeline: { - where: 'device', - default: false, - }, useGroupedNotifications: { where: 'device', default: true, diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 32db5cebf9..dda4163abe 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -309,7 +309,6 @@ rt { max-width: 100%; &:disabled { - opacity: 0.5; cursor: default; } } diff --git a/packages/frontend/src/types/date-separated-list.ts b/packages/frontend/src/types/date-separated-list.ts deleted file mode 100644 index af685cff12..0000000000 --- a/packages/frontend/src/types/date-separated-list.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type MisskeyEntity = { - id: string; - createdAt: string; - _shouldInsertAd_?: boolean; - [x: string]: any; -}; diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 5fe99e0d14..fcf9fb234d 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" >
- +
@@ -112,7 +112,8 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; import { globalEvents } from '@/events.js'; -import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; +import { store } from '@/store.js'; +import XNavbar from '@/ui/_common_/navbar.vue'; const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); const XUpload = defineAsyncComponent(() => import('./upload.vue')); @@ -129,7 +130,9 @@ function onNotification(notification: Misskey.entities.Notification, isClient = if (window.document.visibilityState === 'visible') { if (!isClient && notification.type !== 'test') { // サーバーサイドのテスト通知の際は自動で既読をつけない(テストできないので) - useStream().send('readNotification'); + if (store.s.realtimeMode) { + useStream().send('readNotification'); + } } notifications.value.unshift(notification); @@ -146,11 +149,12 @@ function onNotification(notification: Misskey.entities.Notification, isClient = } if ($i) { - const connection = useStream().useChannel('main', null, 'UI'); - connection.on('notification', onNotification); + if (store.s.realtimeMode) { + const connection = useStream().useChannel('main'); + connection.on('notification', onNotification); + } globalEvents.on('clientNotification', notification => onNotification(notification, true)); - //#region Listen message from SW if ('serviceWorker' in navigator) { swInject(); } @@ -226,12 +230,6 @@ if ($i) { left: 0; z-index: 1001; height: 100dvh; - width: 240px; - box-sizing: border-box; - contain: strict; - overflow: auto; - overscroll-behavior: contain; - background: var(--MI_THEME-navBg); } .widgetsDrawerBg { diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue deleted file mode 100644 index 826e03751a..0000000000 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index ce8efa3324..7cfedc939f 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -10,6 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -50,6 +53,9 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -76,16 +82,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
- - - - - - -
+ @@ -108,15 +116,16 @@ const router = useRouter(); const props = defineProps<{ showWidgetButton?: boolean; + asDrawer?: boolean; }>(); const emit = defineEmits<{ (ev: 'widgetButtonClick'): void; }>(); -const forceIconOnly = ref(window.innerWidth <= 1279); +const forceIconOnly = ref(!props.asDrawer && window.innerWidth <= 1279); const iconOnly = computed(() => { - return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon'); + return !props.asDrawer && (forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon')); }); const otherMenuItemIndicated = computed(() => { @@ -147,6 +156,20 @@ function toggleIconOnly() { } } +function toggleRealtimeMode(ev: MouseEvent) { + os.popupMenu([{ + type: 'label', + text: i18n.ts.realtimeMode, + }, { + text: store.s.realtimeMode ? i18n.ts.turnItOff : i18n.ts.turnItOn, + icon: store.s.realtimeMode ? 'ti ti-bolt-off' : 'ti ti-bolt', + action: () => { + store.set('realtimeMode', !store.s.realtimeMode); + window.location.reload(); + }, + }], ev.currentTarget ?? ev.target); +} + function openAccountMenu(ev: MouseEvent) { openAccountMenu_({ withExtraOperation: true, @@ -191,21 +214,108 @@ function menuEdit() { overscroll-behavior: contain; background: var(--MI_THEME-navBg); contain: strict; + + /* 画面が縦に長い、設置している項目数が少ないなどの環境においても確実にbottomを最下部に表示するため */ display: flex; flex-direction: column; - direction: rtl; // スクロールバーを左に表示したいため + + direction: rtl; /* スクロールバーを左に表示したいため */ } .top { + flex-shrink: 0; direction: ltr; + + /* 疑似progressive blur */ + &::before { + position: absolute; + z-index: -1; + inset: 0; + content: ""; + backdrop-filter: blur(8px); + mask-image: linear-gradient( + to top, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 7.75%, + rgb(0 0 0 / 10.4%) 11.25%, + rgb(0 0 0 / 45%) 23.55%, + rgb(0 0 0 / 55%) 26.45%, + rgb(0 0 0 / 89.6%) 38.75%, + rgb(0 0 0 / 95.1%) 42.25%, + rgb(0 0 0 / 100%) 50% + ); + } + + &::after { + position: absolute; + z-index: -1; + inset: 0; + bottom: 25%; + content: ""; + backdrop-filter: blur(16px); + mask-image: linear-gradient( + to top, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 15.5%, + rgb(0 0 0 / 10.4%) 22.5%, + rgb(0 0 0 / 45%) 47.1%, + rgb(0 0 0 / 55%) 52.9%, + rgb(0 0 0 / 89.6%) 77.5%, + rgb(0 0 0 / 95.1%) 91.9%, + rgb(0 0 0 / 100%) 100% + ); + } } .middle { + flex: 1; direction: ltr; } .bottom { + flex-shrink: 0; direction: ltr; + + /* 疑似progressive blur */ + &::before { + position: absolute; + z-index: -1; + inset: -30px 0 0 0; + content: ""; + backdrop-filter: blur(8px); + mask-image: linear-gradient( + to bottom, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 7.75%, + rgb(0 0 0 / 10.4%) 11.25%, + rgb(0 0 0 / 45%) 23.55%, + rgb(0 0 0 / 55%) 26.45%, + rgb(0 0 0 / 89.6%) 38.75%, + rgb(0 0 0 / 95.1%) 42.25%, + rgb(0 0 0 / 100%) 50% + ); + pointer-events: none; + } + + &::after { + position: absolute; + z-index: -1; + inset: 0; + top: 25%; + content: ""; + backdrop-filter: blur(16px); + mask-image: linear-gradient( + to bottom, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 15.5%, + rgb(0 0 0 / 10.4%) 22.5%, + rgb(0 0 0 / 45%) 47.1%, + rgb(0 0 0 / 55%) 52.9%, + rgb(0 0 0 / 89.6%) 77.5%, + rgb(0 0 0 / 95.1%) 91.9%, + rgb(0 0 0 / 100%) 100% + ); + } } .subButtons { @@ -290,29 +400,18 @@ function menuEdit() { } .top { + --top-height: 80px; + position: sticky; top: 0; z-index: 1; - padding: 20px 0; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); + display: flex; + height: var(--top-height); } .instance { position: relative; - display: block; - text-align: center; - width: 100%; - - &:focus-visible { - outline: none; - - > .instanceIcon { - outline: 2px solid var(--MI_THEME-focus); - outline-offset: 2px; - } - } + width: var(--top-height); } .instanceIcon { @@ -322,13 +421,20 @@ function menuEdit() { border-radius: 8px; } + .realtimeMode { + display: inline-block; + width: var(--top-height); + margin-left: auto; + + &.on { + color: var(--MI_THEME-accent); + } + } + .bottom { position: sticky; bottom: 0; padding-top: 20px; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); } .post { @@ -416,10 +522,6 @@ function menuEdit() { padding-right: 8px; } - .middle { - flex: 1; - } - .divider { margin: 16px 16px; border-top: solid 0.5px var(--MI_THEME-divider); @@ -520,9 +622,6 @@ function menuEdit() { top: 0; z-index: 1; padding: 20px 0; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); } .instance { @@ -551,9 +650,6 @@ function menuEdit() { position: sticky; bottom: 0; padding-top: 20px; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); } .widget { @@ -564,6 +660,18 @@ function menuEdit() { text-align: center; } + .realtimeMode { + display: block; + position: relative; + width: 100%; + height: 52px; + text-align: center; + + &.on { + color: var(--MI_THEME-accent); + } + } + .post { display: block; position: relative; @@ -637,10 +745,6 @@ function menuEdit() { display: none; } - .middle { - flex: 1; - } - .divider { margin: 8px auto; width: calc(100% - 32px); @@ -650,7 +754,7 @@ function menuEdit() { .item { display: block; position: relative; - padding: 18px 0; + padding: 16px 0; width: 100%; text-align: center; diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 5f7600881f..35508b7ce6 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -20,6 +20,7 @@ import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { prefer } from '@/preferences.js'; +import { store } from '@/store.js'; const zIndex = os.claimZIndex('high'); @@ -37,11 +38,13 @@ function reload() { window.location.reload(); } -useStream().on('_disconnected_', onDisconnected); +if (store.s.realtimeMode) { + useStream().on('_disconnected_', onDisconnected); -onUnmounted(() => { - useStream().off('_disconnected_', onDisconnected); -}); + onUnmounted(() => { + useStream().off('_disconnected_', onDisconnected); + }); +}