diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-11-17 18:32:42 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-17 18:32:42 +0900 |
| commit | 9784d10c62e294b32cf62b7374bed7ce57a42b9d (patch) | |
| tree | 02a917ca83373cfeb9dc516c07810fdf8f8e5b30 /packages/frontend/src/components | |
| parent | Merge pull request #12177 from misskey-dev/develop (diff) | |
| parent | Revert "chore(frontend): tweak rt style for safari" (diff) | |
| download | misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.tar.gz misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.tar.bz2 misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.zip | |
Merge pull request #12330 from misskey-dev/develop
Release: 2023.11.1
Diffstat (limited to 'packages/frontend/src/components')
14 files changed, 215 insertions, 116 deletions
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 7c4f910559..7e0c219045 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -45,12 +45,12 @@ import contains from '@/scripts/contains.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; -import { MFM_TAGS } from '@/scripts/mfm-tags.js'; import { defaultStore } from '@/store.js'; import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; +import { MFM_TAGS } from '@/const.js'; type EmojiDef = { emoji: string; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 0ae3423a21..e300ef88a5 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget"> <MkAvatar :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/> - <Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'account'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/> + <Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/> </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" - :nyaize="'account'" + :nyaize="'respect'" :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" @@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> </div> @@ -202,11 +202,17 @@ let note = $ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result:Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note); for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(result); - - if (result === null) return isDeleted.value = true; + try { + result = await interruptor.handler(result); + if (result === null) { + isDeleted.value = true; + return; + } + } catch (err) { + console.error(err); + } } note = result; }); @@ -228,8 +234,8 @@ const clipButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); -const parsed = appearNote.text ? mfm.parse(appearNote.text) : null; -const urls = parsed ? extractUrlFromMfm(parsed) : null; +const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); +const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null); const isLong = shouldCollapsed(appearNote, urls ?? []); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); @@ -298,7 +304,7 @@ function renote(viaKeyboard = false) { const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock }); os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }).then(focus); + }); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 1d8049934a..d1bc3f676f 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="appearNote"/> </p> <div v-show="appearNote.cw == null || showContent"> @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" - :nyaize="'account'" + :nyaize="'respect'" :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" @@ -88,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> <div v-if="appearNote.files.length > 0"> @@ -239,11 +239,17 @@ let note = $ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result:Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note); for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(result); - - if (result === null) return isDeleted.value = true; + try { + result = await interruptor.handler(result); + if (result === null) { + isDeleted.value = true; + return; + } + } catch (err) { + console.error(err); + } } note = result; }); @@ -344,7 +350,7 @@ function renote(viaKeyboard = false) { const { menu } = getRenoteMenu({ note: note, renoteButton }); os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }).then(focus); + }); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 79ce60baff..9b7a39b537 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div> <div> - <Mfm :text="text.trim()" :author="user" :nyaize="'account'" :i="user"/> + <Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/> </div> </div> </div> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 28b00af246..a40dcaf003 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index f61b963d9b..422e9094cc 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 77e66f0165..0c817bd64c 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -96,6 +96,10 @@ onUnmounted(() => { onDeactivated(() => { if (connection) connection.dispose(); }); + +defineExpose({ + reload, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 5643de7683..e7796dfcb5 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -43,12 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; +import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; -import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list'; import { i18n } from '@/i18n.js'; @@ -91,6 +90,7 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M </script> <script lang="ts" setup> import { infoImageUrl } from '@/instance.js'; +import MkButton from '@/components/MkButton.vue'; const props = withDefaults(defineProps<{ pagination: Paging; @@ -185,9 +185,8 @@ watch([$$(backed), $$(contentEl)], () => { } }); -if (props.pagination.params && isRef(props.pagination.params)) { - watch(props.pagination.params, init, { deep: true }); -} +// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど) +watch(() => props.pagination.params, init, { deep: true }); watch(queue, (a, b) => { if (a.size === 0 && b.size === 0) return; @@ -440,8 +439,6 @@ const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => M if (queueItem) queue.value.set(id, replacer(queueItem)); }; -const inited = init(); - onActivated(() => { isBackTop.value = false; }); @@ -454,8 +451,8 @@ function toBottom() { scrollToBottom(contentEl!); } -onMounted(() => { - inited.then(() => { +onBeforeMount(() => { + init().then(() => { if (props.pagination.reversed) { nextTick(() => { setTimeout(toBottom, 800); @@ -487,7 +484,6 @@ defineExpose({ queue, backed, more, - inited, reload, prepend, append: appendItem, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c0fd1c14d7..d163ea2487 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -750,7 +750,11 @@ async function post(ev?: MouseEvent) { // plugin if (notePostInterruptors.length > 0) { for (const interruptor of notePostInterruptors) { - postData = await interruptor.handler(deepClone(postData)); + try { + postData = await interruptor.handler(deepClone(postData)); + } catch (err) { + console.error(err); + } } } @@ -1068,6 +1072,7 @@ defineExpose({ .preview { padding: 16px 20px 0 20px; + min-height: 75px; max-height: 150px; overflow: auto; } diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 17cd083561..1b0d8f74a3 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -73,7 +73,6 @@ function getReactionName(reaction: string): string { } .users { - contain: content; flex: 1; min-width: 0; margin: -4px 14px 0 10px; @@ -85,7 +84,7 @@ function getReactionName(reaction: string): string { line-height: 24px; padding-top: 4px; white-space: nowrap; - overflow: hidden; + overflow: visible; text-overflow: ellipsis; } diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index e9f2b838d2..638407872f 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :emojiUrls="note.emojis"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 845c7a414c..04716ebab2 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -5,19 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()"> - <MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/> + <MkNotes + v-if="paginationQuery" + ref="tlComponent" + :pagination="paginationQuery" + :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" + @queue="emit('queue', $event)" + @status="prComponent.setDisabled($event)" + /> </MkPullToRefresh> </template> <script lang="ts" setup> -import { computed, provide, onUnmounted } from 'vue'; +import { computed, watch, onUnmounted, provide } from 'vue'; +import { Connection } from 'misskey-js/built/streaming.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; -import { useStream, reloadStream } from '@/stream.js'; +import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; +import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ src: string; @@ -42,6 +51,17 @@ const emit = defineEmits<{ provide('inChannel', computed(() => props.src === 'channel')); +type TimelineQueryType = { + antennaId?: string, + withRenotes?: boolean, + withReplies?: boolean, + withFiles?: boolean, + visibility?: string, + listId?: string, + channelId?: string, + roleId?: string +} + const prComponent: InstanceType<typeof MkPullToRefresh> = $ref(); const tlComponent: InstanceType<typeof MkNotes> = $ref(); @@ -63,13 +83,13 @@ const prepend = note => { } }; -let endpoint; -let query; -let connection; -let connection2; +let connection: Connection; +let connection2: Connection; +let paginationQuery: Paging | null = null; const stream = useStream(); -const connectChannel = () => { + +function connectChannel() { if (props.src === 'antenna') { connection = stream.useChannel('antenna', { antennaId: props.antenna, @@ -123,85 +143,112 @@ const connectChannel = () => { }); } if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend); -}; +} -if (props.src === 'antenna') { - endpoint = 'antennas/notes'; - query = { - antennaId: props.antenna, - }; -} else if (props.src === 'home') { - endpoint = 'notes/timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'local') { - endpoint = 'notes/local-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'social') { - endpoint = 'notes/hybrid-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'global') { - endpoint = 'notes/global-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'mentions') { - endpoint = 'notes/mentions'; -} else if (props.src === 'directs') { - endpoint = 'notes/mentions'; - query = { - visibility: 'specified', - }; -} else if (props.src === 'list') { - endpoint = 'notes/user-list-timeline'; - query = { - withFiles: props.onlyFiles ? true : undefined, - listId: props.list, - }; -} else if (props.src === 'channel') { - endpoint = 'channels/timeline'; - query = { - channelId: props.channel, - }; -} else if (props.src === 'role') { - endpoint = 'roles/notes'; - query = { - roleId: props.role, - }; +function disconnectChannel() { + if (connection) connection.dispose(); + if (connection2) connection2.dispose(); } -if (!defaultStore.state.disableStreamingTimeline) { - connectChannel(); +function updatePaginationQuery() { + let endpoint: string | null; + let query: TimelineQueryType | null; - onUnmounted(() => { - connection.dispose(); - if (connection2) connection2.dispose(); - }); + if (props.src === 'antenna') { + endpoint = 'antennas/notes'; + query = { + antennaId: props.antenna, + }; + } else if (props.src === 'home') { + endpoint = 'notes/timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'local') { + endpoint = 'notes/local-timeline'; + query = { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'social') { + endpoint = 'notes/hybrid-timeline'; + query = { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'global') { + endpoint = 'notes/global-timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'mentions') { + endpoint = 'notes/mentions'; + query = null; + } else if (props.src === 'directs') { + endpoint = 'notes/mentions'; + query = { + visibility: 'specified', + }; + } else if (props.src === 'list') { + endpoint = 'notes/user-list-timeline'; + query = { + withFiles: props.onlyFiles ? true : undefined, + listId: props.list, + }; + } else if (props.src === 'channel') { + endpoint = 'channels/timeline'; + query = { + channelId: props.channel, + }; + } else if (props.src === 'role') { + endpoint = 'roles/notes'; + query = { + roleId: props.role, + }; + } else { + endpoint = null; + query = null; + } + + if (endpoint && query) { + paginationQuery = { + endpoint: endpoint, + limit: 10, + params: query, + }; + } else { + paginationQuery = null; + } } -const pagination = { - endpoint: endpoint, - limit: 10, - params: query, -}; +function refreshEndpointAndChannel() { + if (!defaultStore.state.disableStreamingTimeline) { + disconnectChannel(); + connectChannel(); + } + + updatePaginationQuery(); +} + +// IDが切り替わったら切り替え先のTLを表示させたい +watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel); + +// 初回表示用 +refreshEndpointAndChannel(); + +onUnmounted(() => { + disconnectChannel(); +}); function reloadTimeline() { return new Promise<void>((res) => { tlNotesCount = 0; tlComponent.pagingComponent?.reload().then(() => { - reloadStream(); res(); }); }); diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 441731d7ca..c5f247bce9 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -7,6 +7,7 @@ import { VNode, h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkUrl from '@/components/global/MkUrl.vue'; +import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; import MkMention from '@/components/MkMention.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; @@ -36,7 +37,7 @@ type MfmProps = { isNote?: boolean; emojiUrls?: string[]; rootScale?: number; - nyaize: boolean | 'account'; + nyaize: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; @@ -45,7 +46,7 @@ type MfmProps = { // eslint-disable-next-line import/no-default-export export default function(props: MfmProps) { const isNote = props.isNote ?? true; - const shouldNyaize = props.nyaize ? props.nyaize === 'account' ? props.author?.isCat : false : false; + const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.text == null || props.text === '') return; @@ -238,6 +239,34 @@ export default function(props: MfmProps) { style = `background-color: #${color};`; break; } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + const text = rt.type === 'text' ? rt.props.text : ''; + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(MkTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } } if (style == null) { return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 5ba13ca3f3..f08d538fc0 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -49,8 +49,15 @@ const relative = $computed<string>(() => { ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? i18n.ts._ago.justNow : - i18n.ts._ago.future); + ago >= -3 ? i18n.ts._ago.justNow : + ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) : + ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) : + ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) : + ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) : + ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) : + ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) : + i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() }) + ); }); let tickId: number; |