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
This commit is contained in:
commit
40975719ec
82 changed files with 1563 additions and 298 deletions
|
|
@ -394,7 +394,7 @@ export async function mainBoot() {
|
|||
|
||||
main.on('newChatMessage', () => {
|
||||
updateCurrentAccountPartial({ hasUnreadChatMessages: true });
|
||||
sound.playMisskeySfx('chat');
|
||||
sound.playMisskeySfx('chatMessage');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -354,7 +354,6 @@ defineExpose({
|
|||
&.wide {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
|
||||
> .nav {
|
||||
position: sticky;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const headerTabs = computed(() => [{
|
|||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.chat + ' (beta)',
|
||||
icon: 'ti ti-message',
|
||||
icon: 'ti ti-messages',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
21
packages/frontend/src/use/use-mutation-observer.ts
Normal file
21
packages/frontend/src/use/use-mutation-observer.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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'],
|
||||
},
|
||||
|
|
@ -587,6 +604,20 @@ export const searchIndexes: SearchIndexItem[] = [
|
|||
path: '/settings/other',
|
||||
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: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue