diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:17:34 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:17:34 -0400 |
| commit | 40975719ec5bb889ab011bbc464dd0fdb09fdb68 (patch) | |
| tree | 1b8521df694b869b26c6d75c99ef885704b013b0 /packages/frontend/src | |
| parent | merge upstream (diff) | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.gz sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.bz2 sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.zip | |
Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts:
# package.json
# packages/backend/src/core/entities/NotificationEntityService.ts
# packages/backend/src/types.ts
# packages/frontend/src/pages/admin/modlog.ModLog.vue
# packages/misskey-js/src/consts.ts
# packages/misskey-js/src/entities.ts
Diffstat (limited to 'packages/frontend/src')
26 files changed, 430 insertions, 139 deletions
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 38471cd86a..537d61d1a1 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -394,7 +394,7 @@ export async function mainBoot() { main.on('newChatMessage', () => { updateCurrentAccountPartial({ hasUnreadChatMessages: true }); - sound.playMisskeySfx('chat'); + sound.playMisskeySfx('chatMessage'); }); main.on('readAllAnnouncements', () => { diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 43eb2e5f80..c52fdb898e 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template> <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template> - <div style="container-type: inline-size;"> + <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;"> <RouterView :router="targetRouter"/> </div> </MkFolder> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template> <template #suffix>#{{ report.reporterId.toUpperCase() }}</template> - <div style="container-type: inline-size;"> + <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;"> <RouterView :router="reporterRouter"/> </div> </MkFolder> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index fed3dafeea..2e2693d319 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -437,7 +437,8 @@ const keymap = { }, } as const satisfies Keymap; -provide('react', (reaction: string) => { +provide(DI.mfmEmojiReactCallback, (reaction) => { + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 531232f86a..d42c46fba0 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -290,6 +290,7 @@ import { isEnabledUrlPreview } from '@/instance.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -400,7 +401,8 @@ const keymap = { }, } as const satisfies Keymap; -provide('react', (reaction: string) => { +provide(DI.mfmEmojiReactCallback, (reaction) => { + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 8e86d67ab9..ab3947adfb 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i> + <i v-else-if="notification.type === 'chatRoomInvitationReceived'" class="ti ti-messages"></i> <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -68,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> + <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span> <span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span> @@ -114,6 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> {{ notification.role.name }} </div> + <div v-else-if="notification.type === 'chatRoomInvitationReceived'" :class="$style.text"> + {{ notification.invitation.room.name }} + </div> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 70af2c7962..bd57e72dcc 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -35,11 +35,11 @@ import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; -import * as sound from '@/utility/sound.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; import { $i } from '@/i.js'; import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = defineProps<{ name: string; @@ -53,7 +53,7 @@ const props = defineProps<{ fallbackToImage?: boolean; }>(); -const react = inject<((name: string) => void) | null>('react', null); +const react = inject(DI.mfmEmojiReactCallback); const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); @@ -111,7 +111,6 @@ function onClick(ev: MouseEvent) { icon: 'ph-smiley ph-bold ph-lg', action: () => { react(`:${props.name}:`); - sound.playMisskeySfx('reaction'); }, }); } diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index 432de24478..f5323690d0 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -15,9 +15,9 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; -import * as sound from '@/utility/sound.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = defineProps<{ emoji: string; @@ -25,7 +25,7 @@ const props = defineProps<{ menuReaction?: boolean; }>(); -const react = inject<((name: string) => void) | null>('react', null); +const react = inject(DI.mfmEmojiReactCallback); const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : prefer.s.emojiStyle === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath; @@ -61,7 +61,6 @@ function onClick(ev: MouseEvent) { icon: 'ph-smiley ph-bold ph-lg', action: () => { react(props.emoji); - sound.playMisskeySfx('reaction'); }, }); } diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index f9fc282315..b58c8c9659 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -14,4 +14,5 @@ export const DI = { viewId: Symbol() as InjectionKey<string>, currentStickyTop: Symbol() as InjectionKey<Ref<number>>, currentStickyBottom: Symbol() as InjectionKey<Ref<number>>, + mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>, }; diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 9d4988338a..7c15c9666a 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -132,7 +132,7 @@ export const navbarItemDef = reactive({ }, chat: { title: i18n.ts.chat, - icon: 'ti ti-message', + icon: 'ti ti-messages', to: '/chat', indicated: computed(() => $i != null && $i.hasUnreadChatMessages), }, diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 3142c5a45d..b1bd45baab 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -354,7 +354,6 @@ defineExpose({ &.wide { display: flex; margin: 0 auto; - height: 100%; > .nav { position: sticky; diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 67db9ed6d3..5ea1c7f599 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only 'deletePage', 'deleteFlash', 'deleteGalleryPost', + 'deleteChatRoom', 'clearUserFiles', 'clearRemoteFiles', 'clearOwnerlessFiles', @@ -115,6 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span> <span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span> <span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span> + <span v-else-if="log.type === 'deleteChatRoom'">: @{{ log.info.room.name }}</span> <span v-else-if="log.type === 'clearUserFiles'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'nsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'unNsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 1e7f8e20ea..cbb817de05 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -7,9 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[$style.root, { [$style.isMe]: isMe }]"> <MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/> <div :class="$style.body"> + <div v-if="!isMe && prefer.s['chat.showSenderName']" :class="$style.header"><MkUserName :user="message.fromUser"/></div> <MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe"> <div v-if="!message.isDeleted" :class="$style.content"> - <Mfm v-if="message.text" ref="text" class="_selectable" :text="message.text" :i="$i"/> + <Mfm + v-if="message.text" + ref="text" + class="_selectable" + :text="message.text" + :i="$i" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/> </div> <div v-else :class="$style.content"> @@ -31,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="prefer.s.animation ? $style.transition_reaction_move : ''" tag="div" :class="$style.reactions" > - <div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="$style.reaction"> + <div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="[$style.reaction, record.user.id === $i.id ? $style.reactionMy : null]" @click="onReactionClick(record)"> <MkAvatar :user="record.user" :link="false" :class="$style.reactionAvatar"/> <MkReactionIcon :withTooltip="true" @@ -46,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent } from 'vue'; +import { computed, defineAsyncComponent, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; @@ -64,6 +74,7 @@ import { reactionPicker } from '@/utility/reaction-picker.js'; import * as sound from '@/utility/sound.js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const $i = ensureSignin(); @@ -75,10 +86,17 @@ const props = defineProps<{ const isMe = computed(() => props.message.fromUserId === $i.id); const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); +provide(DI.mfmEmojiReactCallback, (reaction) => { + sound.playMisskeySfx('reaction'); + misskeyApi('chat/messages/react', { + messageId: props.message.id, + reaction: reaction, + }); +}); + function react(ev: MouseEvent) { reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => { sound.playMisskeySfx('reaction'); - misskeyApi('chat/messages/react', { messageId: props.message.id, reaction: reaction, @@ -86,6 +104,23 @@ function react(ev: MouseEvent) { }); } +function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) { + if (record.user.id === $i.id) { + misskeyApi('chat/messages/unreact', { + messageId: props.message.id, + reaction: record.reaction, + }); + } else { + if (!props.message.reactions.some(r => r.user.id === $i.id && r.reaction === record.reaction)) { + sound.playMisskeySfx('reaction'); + misskeyApi('chat/messages/react', { + messageId: props.message.id, + reaction: record.reaction, + }); + } + } +} + function showMenu(ev: MouseEvent) { const menu: MenuItem[] = []; @@ -191,6 +226,10 @@ function showMenu(ev: MouseEvent) { margin: 0 12px; } +.header { + font-size: 80%; +} + .content { overflow: clip; overflow-wrap: break-word; @@ -230,6 +269,10 @@ function showMenu(ev: MouseEvent) { border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 8px; + + &.reactionMy { + border-color: var(--MI_THEME-accent); + } } .reactionAvatar { diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue index 1d0605136c..0affef6333 100644 --- a/packages/frontend/src/pages/chat/home.home.vue +++ b/packages/frontend/src/pages/chat/home.home.vue @@ -40,10 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only class="_panel" :to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`" > - <MkAvatar v-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/> + <MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/> + <MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/> <div :class="$style.messageBody"> <header v-if="item.message.toRoom" :class="$style.messageHeader"> - <span :class="$style.messageHeaderName">{{ item.message.toRoom.name }}</span> + <span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span> <MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/> </header> <header v-else :class="$style.messageHeader"> @@ -55,17 +56,18 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkA> </div> - <div v-if="!fetching && history.length == 0" class="_fullinfo"> + <div v-if="!initializing && history.length == 0" class="_fullinfo"> <div>{{ i18n.ts._chat.noHistory }}</div> </div> - <MkLoading v-if="fetching"/> + <MkLoading v-if="initializing"/> </MkFoldableSection> </div> </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import XMessage from './XMessage.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; @@ -81,7 +83,8 @@ const $i = ensureSignin(); const router = useRouter(); -const fetching = ref(true); +const initializing = ref(true); +const fetching = ref(false); const history = ref<{ id: string; message: Misskey.entities.ChatMessage; @@ -142,6 +145,8 @@ async function search() { } async function fetchHistory() { + if (fetching.value) return; + fetching.value = true; const [userMessages, roomMessages] = await Promise.all([ @@ -159,10 +164,35 @@ async function fetchHistory() { })); fetching.value = false; + initializing.value = false; updateCurrentAccountPartial({ hasUnreadChatMessages: false }); } +let isActivated = true; + +onActivated(() => { + isActivated = true; +}); + +onDeactivated(() => { + isActivated = false; +}); + +useInterval(() => { + // TODO: DOM的にバックグラウンドになっていないかどうかも考慮する + if (!window.document.hidden && isActivated) { + fetchHistory(); + } +}, 1000 * 10, { + immediate: false, + afterMounted: true, +}); + +onActivated(() => { + fetchHistory(); +}); + onMounted(() => { fetchHistory(); }); diff --git a/packages/frontend/src/pages/chat/home.vue b/packages/frontend/src/pages/chat/home.vue index c2b272a42d..9bb7235a64 100644 --- a/packages/frontend/src/pages/chat/home.vue +++ b/packages/frontend/src/pages/chat/home.vue @@ -52,7 +52,7 @@ const headerTabs = computed(() => [{ definePage(() => ({ title: i18n.ts.chat + ' (beta)', - icon: 'ti ti-message', + icon: 'ti ti-messages', })); </script> diff --git a/packages/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue index be8be7e5d1..975d1a2be9 100644 --- a/packages/frontend/src/pages/chat/message.vue +++ b/packages/frontend/src/pages/chat/message.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading/> </div> <div v-else> - <XMessage :message="message"/> + <XMessage :message="message" :isSearchResult="true"/> </div> </MkSpacer> </PageWithHeader> diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index aba9d6061f..27ddbeb565 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -151,8 +151,16 @@ function onDrop(ev: DragEvent): void { } function onKeydown(ev: KeyboardEvent) { - if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey)) { - send(); + if (ev.key === 'Enter') { + if (prefer.s['chat.sendOnEnter']) { + if (!(ev.ctrlKey || ev.metaKey || ev.shiftKey)) { + send(); + } + } else { + if ((ev.ctrlKey || ev.metaKey)) { + send(); + } + } } } diff --git a/packages/frontend/src/pages/chat/room.info.vue b/packages/frontend/src/pages/chat/room.info.vue index 7d38d07b3a..7e10336fd3 100644 --- a/packages/frontend/src/pages/chat/room.info.vue +++ b/packages/frontend/src/pages/chat/room.info.vue @@ -17,6 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <hr> + <MkButton v-if="isOwner || ($i.isAdmin || $i.isModerator)" danger @click="del">{{ i18n.ts._chat.deleteRoom }}</MkButton> + <MkSwitch v-if="!isOwner" v-model="isMuted"> <template #label>{{ i18n.ts._chat.muteThisRoom }}</template> </MkSwitch> @@ -34,7 +36,9 @@ import { ensureSignin } from '@/i.js'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; +import { useRouter } from '@/router.js'; +const router = useRouter(); const $i = ensureSignin(); const props = defineProps<{ @@ -56,6 +60,19 @@ function save() { }); } +async function del() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }); + if (canceled) return; + + misskeyApi('chat/rooms/delete', { + roomId: props.room.id, + }); + router.push('/chat'); +} + const isMuted = ref(props.room.isMuted); watch(isMuted, async () => { diff --git a/packages/frontend/src/pages/chat/room.members.vue b/packages/frontend/src/pages/chat/room.members.vue index d20216a81c..2b31efab38 100644 --- a/packages/frontend/src/pages/chat/room.members.vue +++ b/packages/frontend/src/pages/chat/room.members.vue @@ -18,6 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserCardMini :user="membership.user"/> </MkA> </div> + + <template v-if="isOwner"> + <hr> + + <div>{{ i18n.ts._chat.sentInvitations }}</div> + + <div v-for="invitation in invitations" :key="invitation.id" :class="$style.invitation"> + <MkA :class="$style.invitationBody" :to="`${userPage(invitation.user)}`"> + <MkUserCardMini :user="invitation.user"/> + </MkA> + </div> + </template> </div> </template> @@ -47,12 +59,20 @@ const isOwner = computed(() => { }); const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]); +const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]); onMounted(async () => { memberships.value = await misskeyApi('chat/rooms/members', { roomId: props.room.id, limit: 50, }); + + if (isOwner.value) { + invitations.value = await misskeyApi('chat/rooms/invitations/outbox', { + roomId: props.room.id, + limit: 50, + }); + } }); </script> @@ -65,9 +85,15 @@ onMounted(async () => { flex: 1; min-width: 0; margin-right: 8px; +} - &:hover { - text-decoration: none; - } +.invitation { + display: flex; +} + +.invitationBody { + flex: 1; + min-width: 0; + margin-right: 8px; } </style> diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue index 15e9f43db2..5938fd2688 100644 --- a/packages/frontend/src/pages/chat/room.vue +++ b/packages/frontend/src/pages/chat/room.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div> <div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div> <div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div> - <div v-else>{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div> + <div v-else-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div> </template> <template v-else-if="room"> <div>{{ i18n.ts._chat.inviteUserToChat }}</div> @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <div v-else class="_gaps"> + <div v-else ref="timelineEl" class="_gaps"> <div v-if="canFetchMore"> <MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton> </div> @@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue'; import * as Misskey from 'misskey-js'; -import { isTailVisible } from '@@/js/scroll.js'; +import { getScrollContainer, isTailVisible } from '@@/js/scroll.js'; import XMessage from './XMessage.vue'; import XForm from './room.form.vue'; import XSearch from './room.search.vue'; @@ -92,6 +92,7 @@ import { definePage } from '@/page.js'; import { prefer } from '@/preferences.js'; import MkButton from '@/components/MkButton.vue'; import { useRouter } from '@/router.js'; +import { useMutationObserver } from '@/use/use-mutation-observer.js'; const $i = ensureSignin(); const router = useRouter(); @@ -109,6 +110,26 @@ const user = ref<Misskey.entities.UserDetailed | null>(null); const room = ref<Misskey.entities.ChatRoom | null>(null); const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null); const showIndicator = ref(false); +const timelineEl = useTemplateRef('timelineEl'); + +const SCROLL_HEAD_THRESHOLD = 200; + +// column-reverseなので本来はスクロール位置の最下部への追従は不要なはずだが、おそらくブラウザのバグにより、最下部にスクロールした状態でも追従されない場合がある(スクロール位置が少数になることがあるのが関わっていそう) +// そのため補助としてMutationObserverを使って追従を行う +useMutationObserver(timelineEl, { + subtree: true, + childList: true, + attributes: false, +}, () => { + const scrollContainer = getScrollContainer(timelineEl.value)!; + // column-reverseなのでscrollTopは負になる + if (-scrollContainer.scrollTop < SCROLL_HEAD_THRESHOLD) { + scrollContainer.scrollTo({ + top: 0, + behavior: 'instant', + }); + } +}); function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) { const reactions = [...message.reactions]; @@ -149,6 +170,7 @@ async function initialize() { connection.value.on('message', onMessage); connection.value.on('deleted', onDeleted); connection.value.on('react', onReact); + connection.value.on('unreact', onUnreact); } else { const [r, m] = await Promise.all([ misskeyApi('chat/rooms/show', { roomId: props.roomId }), @@ -168,6 +190,7 @@ async function initialize() { connection.value.on('message', onMessage); connection.value.on('deleted', onDeleted); connection.value.on('react', onReact); + connection.value.on('unreact', onUnreact); } window.document.addEventListener('visibilitychange', onVisibilitychange); @@ -247,6 +270,16 @@ function onReact(ctx) { } } +function onUnreact(ctx) { + const message = messages.value.find(m => m.id === ctx.messageId); + if (message) { + const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id); + if (index !== -1) { + message.reactions.splice(index, 1); + } + } +} + function onIndicatorClick() { showIndicator.value = false; } diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index 706cb731eb..f944490a66 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -4,45 +4,55 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps_m"> - <FormSlot> - <template #label>{{ i18n.ts.navbar }}</template> - <MkContainer :showHeader="false"> - <Sortable - v-model="items" - itemKey="id" - :animation="150" - :handle="'.' + $style.itemHandle" - @start="e => e.item.classList.add('active')" - @end="e => e.item.classList.remove('active')" - > - <template #item="{element,index}"> - <div - v-if="element.type === '-' || navbarItemDef[element.type]" - :class="$style.item" - > - <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> - <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span> - <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> - </div> - </template> - </Sortable> - </MkContainer> - </FormSlot> - <div class="_buttons"> - <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> - <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> - <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> +<SearchMarker path="/settings/navbar" :label="i18n.ts.navbar" icon="ti ti-list" :keywords="['navbar', 'menu', 'sidebar']"> + <div class="_gaps_m"> + <FormSlot> + <template #label>{{ i18n.ts.navbar }}</template> + <MkContainer :showHeader="false"> + <Sortable + v-model="items" + itemKey="id" + :animation="150" + :handle="'.' + $style.itemHandle" + @start="e => e.item.classList.add('active')" + @end="e => e.item.classList.remove('active')" + > + <template #item="{element,index}"> + <div + v-if="element.type === '-' || navbarItemDef[element.type]" + :class="$style.item" + > + <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span> + <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> + </div> + </template> + </Sortable> + </MkContainer> + </FormSlot> + <div class="_buttons"> + <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> - <MkRadios v-model="menuDisplay"> - <template #label>{{ i18n.ts.display }}</template> - <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option> - <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option> - <option value="top">{{ i18n.ts._menuDisplay.top }}</option> + <MkRadios v-model="menuDisplay"> + <template #label>{{ i18n.ts.display }}</template> + <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option> + <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option> + <option value="top">{{ i18n.ts._menuDisplay.top }}</option> <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> - </MkRadios> -</div> + </MkRadios> + + <SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']"> + <MkPreferenceContainer k="showNavbarSubButtons"> + <MkSwitch v-model="showNavbarSubButtons"> + <template #label><SearchLabel>{{ i18n.ts._settings.showNavbarSubButtons }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + </div> +</SearchMarker> </template> <script lang="ts" setup> @@ -51,6 +61,8 @@ import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import FormSlot from '@/components/form/slot.vue'; import MkContainer from '@/components/MkContainer.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { store } from '@/store.js'; @@ -68,6 +80,7 @@ const items = ref(prefer.s.menu.map(x => ({ }))); const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); +const showNavbarSubButtons = prefer.model('showNavbarSubButtons'); async function addItem() { const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k)); diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 9256a565c4..816f8d7435 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SearchMarker :keywords="['general']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.general }}</SearchLabel></template> + <template #icon><i class="ti ti-settings"></i></template> <div class="_gaps_m"> <SearchMarker :keywords="['language']"> @@ -135,6 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SearchMarker :keywords="['timeline', 'note']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts._settings.timelineAndNote }}</SearchLabel></template> + <template #icon><i class="ti ti-notes"></i></template> <div class="_gaps_m"> <div class="_gaps_s"> @@ -293,6 +295,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SearchMarker :keywords="['post', 'form']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.postForm }}</SearchLabel></template> + <template #icon><i class="ti ti-edit"></i></template> <div class="_gaps_m"> <div class="_gaps_s"> @@ -354,6 +357,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SearchMarker :keywords="['notification']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template> + <template #icon><i class="ti ti-bell"></i></template> <div class="_gaps_m"> <SearchMarker :keywords="['group']"> @@ -394,6 +398,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> @@ -424,9 +429,49 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </SearchMarker> + <SearchMarker :keywords="['chat', 'messaging']"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template> + <template #icon><i class="ti ti-messages"></i></template> + + <div class="_gaps_s"> + <SearchMarker :keywords="['show', 'sender', 'name']"> + <MkPreferenceContainer k="chat.showSenderName"> + <MkSwitch v-model="chatShowSenderName"> + <template #label><SearchLabel>{{ i18n.ts._settings._chat.showSenderName }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['send', 'enter', 'newline']"> + <MkPreferenceContainer k="chat.sendOnEnter"> + <MkSwitch v-model="chatSendOnEnter"> + <template #label><SearchLabel>{{ i18n.ts._settings._chat.sendOnEnter }}</SearchLabel></template> + <template #caption> + <div class="_gaps_s"> + <div> + <b>{{ i18n.ts._settings.ifOn }}:</b> + <div>{{ i18n.ts._chat.send }}: Enter</div> + <div>{{ i18n.ts._chat.newline }}: Shift + Enter</div> + </div> + <div> + <b>{{ i18n.ts._settings.ifOff }}:</b> + <div>{{ i18n.ts._chat.send }}: Ctrl + Enter</div> + <div>{{ i18n.ts._chat.newline }}: Enter</div> + </div> + </div> + </template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + </div> + </MkFolder> + </SearchMarker> + <SearchMarker :keywords="['other']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template> + <template #icon><i class="ti ti-settings-cog"></i></template> <div class="_gaps_m"> <div class="_gaps_s"> @@ -603,6 +648,8 @@ const emojiStyle = prefer.model('emojiStyle'); const useBlurEffectForModal = prefer.model('useBlurEffectForModal'); const useBlurEffect = prefer.model('useBlurEffect'); const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies'); +const chatShowSenderName = prefer.model('chat.showSenderName'); +const chatSendOnEnter = prefer.model('chat.sendOnEnter'); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -630,6 +677,7 @@ watch([ squareAvatars, highlightSensitiveMedia, enableSeasonalScreenEffect, + chatShowSenderName, ], async () => { await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 127ebeef0c..b588cc3b5f 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -329,6 +329,9 @@ export const PREF_DEF = { makeEveryTextElementsSelectable: { default: DEFAULT_DEVICE_KIND === 'desktop', }, + showNavbarSubButtons: { + default: true, + }, plugins: { default: [] as Plugin[], }, @@ -371,6 +374,13 @@ export const PREF_DEF = { default: 'left' as 'left' | 'right' | 'center', }, + 'chat.showSenderName': { + default: false, + }, + 'chat.sendOnEnter': { + default: false, + }, + 'game.dropAndFusion': { default: { bgmVolume: 0.25, diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index d590455ae5..db5ba75b2a 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -48,6 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </div> <div :class="$style.bottom"> + <button v-if="showWidgetButton" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')"> + <i class="ti ti-apps ti-fw"></i> + </button> <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }"> <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> </button> @@ -65,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only </svg> --> - <div v-if="!forceIconOnly" :class="$style.subButtons"> + <div v-if="!forceIconOnly && prefer.r.showNavbarSubButtons.value" :class="$style.subButtons"> <div :class="[$style.subButton, $style.menuEditButton]"> <svg viewBox="0 0 16 64" :class="$style.subButtonShape"> <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> @@ -104,6 +107,14 @@ import { $i } from '@/i.js'; const router = useRouter(); +const props = defineProps<{ + showWidgetButton?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'widgetButtonClick'): void; +}>(); + const forceIconOnly = ref(window.innerWidth <= 1279); const iconOnly = computed(() => { return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon'); @@ -567,6 +578,14 @@ function menuEdit() { backdrop-filter: var(--MI-blur, blur(8px)); } + .widget { + display: block; + position: relative; + width: 100%; + height: 52px; + text-align: center; + } + .post { display: block; position: relative; diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 6724c6f6c9..cc3836c646 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <XSidebar v-if="!isMobile" :class="$style.sidebar"/> + <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/> <div :class="$style.contents" @contextmenu.stop="onContextmenu"> <div> @@ -35,8 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only <XWidgets/> </div> - <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> - <Transition :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''" :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''" @@ -280,7 +278,7 @@ $widgets-hide-threshold: 1090px; .transition_widgetsDrawer_enterFrom, .transition_widgetsDrawer_leaveTo { opacity: 0; - transform: translateX(240px); + transform: translateX(-240px); } .root { @@ -414,20 +412,6 @@ $widgets-hide-threshold: 1090px; } } -.widgetButton { - display: block; - position: fixed; - z-index: 1000; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - border-radius: 100%; - box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); - font-size: 22px; - background: var(--MI_THEME-panel); -} - .widgetsDrawerBg { z-index: 1001; } @@ -435,7 +419,7 @@ $widgets-hide-threshold: 1090px; .widgetsDrawer { position: fixed; top: 0; - right: 0; + left: 0; z-index: 1001; width: 310px; height: 100dvh; diff --git a/packages/frontend/src/use/use-mutation-observer.ts b/packages/frontend/src/use/use-mutation-observer.ts new file mode 100644 index 0000000000..b35dbcd7a8 --- /dev/null +++ b/packages/frontend/src/use/use-mutation-observer.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { onUnmounted, watch } from 'vue'; +import type { Ref, ShallowRef } from 'vue'; + +export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void { + const observer = new MutationObserver(callback); + + watch(targetNodeRef, (targetNode) => { + if (targetNode) { + observer.observe(targetNode, options); + } + }, { immediate: true }); + + onUnmounted(() => { + observer.disconnect(); + }); +} diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts index 64fe328478..c939c93425 100644 --- a/packages/frontend/src/utility/autogen/settings-search-index.ts +++ b/packages/frontend/src/utility/autogen/settings-search-index.ts @@ -279,62 +279,62 @@ export const searchIndexes: SearchIndexItem[] = [ id: 'AKvDrxSj5', children: [ { - id: 'cAszhShB0', + id: 'a5b9RjEvq', label: i18n.ts.uiLanguage, keywords: ['language'], }, { - id: 'apz9AutPm', + id: '9ragaff40', label: i18n.ts.overridedDeviceKind, keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'], }, { - id: 'nqRVtw1xw', + id: 'lfI3yMX9g', label: i18n.ts.useBlurEffect, keywords: ['blur'], }, { - id: 'EO5WHBeG8', + id: '31Y4IcGEf', label: i18n.ts.useBlurEffectForModal, keywords: ['blur', 'modal'], }, { - id: 'CWpyT9vLK', + id: '78q2asrLS', label: i18n.ts.showAvatarDecorations, keywords: ['avatar', 'icon', 'decoration', 'show'], }, { - id: '1wwACqQz1', + id: 'zydOfGYip', label: i18n.ts.alwaysConfirmFollow, keywords: ['follow', 'confirm', 'always'], }, { - id: '1x3JNXj8N', + id: 'wqpOC22Zm', label: i18n.ts.highlightSensitiveMedia, keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'], }, { - id: 'CfAg0Qekq', + id: 'c98gbF9c6', label: i18n.ts.confirmWhenRevealingSensitiveMedia, keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'], }, { - id: 'aefexW9fD', + id: '4LxdiOMNh', label: i18n.ts.enableAdvancedMfm, keywords: ['mfm', 'enable', 'show', 'advanced'], }, { - id: 'lu9v5Spqg', + id: '9gTCaLkIf', label: i18n.ts.enableInfiniteScroll, keywords: ['auto', 'load', 'auto', 'more', 'scroll'], }, { - id: '6kMj4HVOg', + id: 'jmJT0twuJ', label: i18n.ts.emojiStyle, keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'], }, { - id: 'DftdlLbNu', + id: 'igFN7RIUa', label: i18n.ts.pinnedList, keywords: ['pinned', 'list'], }, @@ -343,85 +343,85 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['general'], }, { - id: 'CQldliCSi', + id: 'ufc2X9voy', children: [ { - id: 'kMB2hPyq3', + id: 'd2H4E5ys6', label: i18n.ts.showFixedPostForm, keywords: ['post', 'form', 'timeline'], }, { - id: 'jC7LtTnmc', + id: '1LHOhDKGW', label: i18n.ts.showFixedPostFormInChannel, keywords: ['post', 'form', 'timeline', 'channel'], }, { - id: 'p2wlrnwLo', + id: 'DSzwvTp7i', label: i18n.ts.collapseRenotes, keywords: ['renote', i18n.ts.collapseRenotesDescription], }, { - id: '6SFn3t8VS', + id: 'jb3HUeyrx', label: i18n.ts.showGapBetweenNotesInTimeline, keywords: ['note', 'timeline', 'gap'], }, { - id: 'nygexkaUk', + id: '2LNjwv1cr', label: i18n.ts.disableStreamingTimeline, keywords: ['disable', 'streaming', 'timeline'], }, { - id: '7vnQgR42v', + id: '7W6g8Dcqz', label: i18n.ts.showNoteActionsOnlyHover, keywords: ['hover', 'show', 'footer', 'action'], }, { - id: 'x5q4XZ7Kv', + id: 'uAOoH3LFF', label: i18n.ts.showClipButtonInNoteFooter, keywords: ['footer', 'action', 'clip', 'show'], }, { - id: 'x9irZWjaF', + id: 'eCiyZLC8n', label: i18n.ts.showReactionsCount, keywords: ['reaction', 'count', 'show'], }, { - id: 'dHPv9mrxi', + id: '68u9uRmFP', label: i18n.ts.confirmOnReact, keywords: ['reaction', 'confirm'], }, { - id: 'bj42W4cvN', + id: 'rHWm4sXIe', label: i18n.ts.loadRawImages, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'], }, { - id: 'fzPca1Gk9', + id: '9L2XGJw7e', label: i18n.ts.useReactionPickerForContextMenu, keywords: ['reaction', 'picker', 'contextmenu', 'open'], }, { - id: 'mNU5IBln7', + id: 'uIMCIK7kG', label: i18n.ts.reactionsDisplaySize, keywords: ['reaction', 'size', 'scale', 'display'], }, { - id: 'kYgorbLUy', + id: 'uMckjO9bz', label: i18n.ts.limitWidthOfReaction, keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'], }, { - id: 'm75VEWI3S', + id: 'yeghU4qiH', label: i18n.ts.mediaListWithOneImageAppearance, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'], }, { - id: 'CA42sC9Mx', + id: 'yYSOPoAKE', label: i18n.ts.instanceTicker, keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'], }, { - id: 'knEhibyFp', + id: 'iOHiIu32L', label: i18n.ts.displayOfSensitiveMedia, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'], }, @@ -430,25 +430,25 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['timeline', 'note'], }, { - id: 'yIR4YP0yU', + id: 'eROFRMtXv', children: [ { - id: 'cBkUgQNpH', + id: 'BaQfrVO82', label: i18n.ts.keepCw, keywords: ['remember', 'keep', 'note', 'cw'], }, { - id: 'Bv4YywaKL', + id: 'vFerPo2he', label: i18n.ts.rememberNoteVisibility, keywords: ['remember', 'keep', 'note', 'visibility'], }, { - id: 'F3kpUNvSQ', + id: 'dcAC0yJcH', label: i18n.ts.enableQuickAddMfmFunction, keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'], }, { - id: 'BBxwy4F6E', + id: 'bECeWZVMb', label: i18n.ts.defaultNoteVisibility, keywords: ['default', 'note', 'visibility'], }, @@ -457,20 +457,20 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['post', 'form'], }, { - id: 'e5XnQWk68', + id: 'tsSP93Cc6', children: [ { - id: 'rOttgccaS', + id: 'dtw8FepYL', label: i18n.ts.useGroupedNotifications, keywords: ['group'], }, { - id: 'Ek4Cw3VPq', + id: 'eb0yCYJTn', label: i18n.ts.position, keywords: ['position'], }, { - id: 'pZLzt3i0s', + id: '1Spt4Gpr5', label: i18n.ts.stackAxis, keywords: ['stack', 'axis', 'direction'], }, @@ -479,55 +479,72 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['notification'], }, { - id: 'c9mbgmHQp', + id: 'SYmWxGOF', label: i18n.ts.dataSaver, keywords: ['datasaver'], }, { - id: '5h8vhCX1S', + id: 'vPQPvmntL', children: [ { - id: 'bDv03znUy', + id: 'zZxyXHk3A', + label: i18n.ts._settings._chat.showSenderName, + keywords: ['show', 'sender', 'name'], + }, + { + id: 'omEy5Q3Ev', + label: i18n.ts._settings._chat.sendOnEnter, + keywords: ['send', 'enter', 'newline'], + }, + ], + label: i18n.ts.chat, + keywords: ['chat', 'messaging'], + }, + { + id: '5fy7VEy6i', + children: [ + { + id: 'EosiWZvak', label: i18n.ts.squareAvatars, keywords: ['avatar', 'icon', 'square'], }, { - id: 'nkR2LWURW', + id: 'qY5xTzl35', label: i18n.ts.seasonalScreenEffect, keywords: ['effect', 'show'], }, { - id: 'sCscGhMmH', + id: '2VSnj81vC', label: i18n.ts.openImageInNewTab, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'], }, { - id: '4yCgcFElF', + id: 'hdQa7W2H1', label: i18n.ts.withRepliesByDefaultForNewlyFollowed, keywords: ['follow', 'replies'], }, { - id: '5iMpm5rES', + id: 'nnj4DkjhP', label: i18n.ts.whenServerDisconnected, keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'], }, { - id: 'dlQjnWBVU', + id: 'Eh7vTluDO', label: i18n.ts.numberOfPageCache, keywords: ['cache', 'page'], }, { - id: 'qY5xTzl35', + id: 'vTRSKf1JA', label: i18n.ts.forceShowAds, keywords: ['ad', 'show'], }, { - id: '2VSnj81vC', + id: 'dwhQfcLGt', label: i18n.ts.hemisphere, keywords: [], }, { - id: 'vuG3aG3IE', + id: 'Ar1lj7f7U', label: i18n.ts.additionalEmojiDictionary, keywords: ['emoji', 'dictionary', 'additional', 'extra'], }, @@ -588,6 +605,20 @@ export const searchIndexes: SearchIndexItem[] = [ icon: 'ti ti-dots', }, { + id: '9bNikHWzQ', + children: [ + { + id: 'appYJbpkK', + label: i18n.ts._settings.showNavbarSubButtons, + keywords: ['navbar', 'sidebar', 'toggle', 'button', 'sub'], + }, + ], + label: i18n.ts.navbar, + keywords: ['navbar', 'menu', 'sidebar'], + path: '/settings/navbar', + icon: 'ti ti-list', + }, + { id: '3icEvyv2D', children: [ { |