diff options
| -rw-r--r-- | packages/backend/src/server/web/ClientServerService.ts | 1 | ||||
| -rw-r--r-- | packages/backend/src/server/web/views/info-card.pug | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNote.vue | 11 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteDetailed.vue | 15 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteSub.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPoll.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNote.vue | 11 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNoteDetailed.vue | 15 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNoteSub.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkOldNoteWindow.vue | 15 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/XMessage.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/note.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/utility/extract-preview-urls.ts | 37 | ||||
| -rw-r--r-- | packages/frontend/src/utility/extract-url-from-mfm.ts | 35 | ||||
| -rw-r--r-- | packages/frontend/src/utility/get-self-note-ids.ts | 21 |
15 files changed, 133 insertions, 48 deletions
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 1321cf6338..c40d042fa4 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -890,6 +890,7 @@ export class ClientServerService { return await reply.view('info-card', { version: this.config.version, host: this.config.host, + url: this.config.url, meta: this.meta, originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug index 4a9d00a596..0a95ea7b17 100644 --- a/packages/backend/src/server/web/views/info-card.pug +++ b/packages/backend/src/server/web/views/info-card.pug @@ -43,7 +43,7 @@ html } body - a#a(href=`https://${host}` target="_blank") + a#a(href=url target="_blank") header#banner(style=`background-image: url(${meta.bannerUrl})`) div#title= meta.name || host div#content diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 3418676d58..6a356ffd37 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/> <div v-if="isEnabledUrlPreview"> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/> </div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false"> @@ -184,7 +184,7 @@ import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import type { Ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; @@ -235,6 +235,8 @@ import { DI } from '@/di.js'; import { useRouter } from '@/router.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; +import { getSelfNoteIds } from '@/utility/get-self-note-ids.js'; +import { extractPreviewUrls } from '@/utility/extract-preview-urls.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -302,7 +304,8 @@ const galleryEl = useTemplateRef('galleryEl'); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(prefer.s.uncollapseCW); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); -const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null); +const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null); +const selfNoteIds = computed(() => getSelfNoteIds(props.note)); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong); const isDeleted = ref(false); @@ -325,7 +328,7 @@ const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm); const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); const mergedCW = computed(() => computeMergedCw(appearNote.value)); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index bde2086508..92a3c7b5cc 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -112,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/> <div v-if="isEnabledUrlPreview"> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/> </div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> </div> @@ -236,7 +236,7 @@ import { computed, inject, onMounted, provide, ref, useTemplateRef, watch } from import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { Paging } from '@/components/MkPagination.vue'; @@ -285,6 +285,8 @@ import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; +import { getSelfNoteIds } from '@/utility/get-self-note-ids.js'; +import { extractPreviewUrls } from '@/utility/extract-preview-urls.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -337,9 +339,10 @@ const isDeleted = ref(false); const renoted = ref(false); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translating = ref(false); -const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; -const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; -const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); +const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null); +const selfNoteIds = computed(() => getSelfNoteIds(props.note)); +const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm); const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); @@ -372,7 +375,7 @@ let renoting = false; const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); const keymap = { diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index eb72939bf1..b6a18ccab6 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import type { Visibility } from '@/utility/boost-quote.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; @@ -150,7 +150,7 @@ const isRenote = ( const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); async function addReplyTo(replyNote: Misskey.entities.Note) { diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 07febacc36..72f3ced088 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import { sum } from '@/utility/array.js'; @@ -72,7 +72,7 @@ const showResult = ref(props.readOnly || isVoted.value); const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: `https://${host}/notes/${props.noteId}`, + url: `${config.url}/notes/${props.noteId}`, })); // 期限付きアンケート diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 197d0ecc0d..99cd85d123 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/> <div v-if="isEnabledUrlPreview"> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" :class="$style.urlPreview" @click.stop/> </div> <div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false"> @@ -185,7 +185,7 @@ import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import type { Ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; @@ -235,6 +235,8 @@ import { DI } from '@/di.js'; import { useRouter } from '@/router.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; +import { getSelfNoteIds } from '@/utility/get-self-note-ids.js'; +import { extractPreviewUrls } from '@/utility/extract-preview-urls.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -302,7 +304,8 @@ const galleryEl = useTemplateRef('galleryEl'); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(prefer.s.uncollapseCW); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); -const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null); +const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null); +const selfNoteIds = computed(() => getSelfNoteIds(props.note)); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong); const isDeleted = ref(false); @@ -325,7 +328,7 @@ const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm); const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); const mergedCW = computed(() => computeMergedCw(appearNote.value)); diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 02cc53fc24..9ca36c2fc9 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/> <div v-if="isEnabledUrlPreview"> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/> </div> <div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> </div> @@ -241,7 +241,7 @@ import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, useT import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { Paging } from '@/components/MkPagination.vue'; @@ -290,6 +290,8 @@ import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; +import { getSelfNoteIds } from '@/utility/get-self-note-ids.js'; +import { extractPreviewUrls } from '@/utility/extract-preview-urls'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -343,9 +345,10 @@ const isDeleted = ref(false); const renoted = ref(false); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translating = ref(false); -const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; -const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; -const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); +const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null); +const selfNoteIds = computed(() => getSelfNoteIds(props.note)); +const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm ? true : false); const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); @@ -378,7 +381,7 @@ let renoting = false; const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); const keymap = { diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index 434af87ed9..12e12ad028 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import type { Visibility } from '@/utility/boost-quote.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import SkNoteHeader from '@/components/SkNoteHeader.vue'; @@ -164,7 +164,7 @@ const isRenote = ( const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: appearNote.value.url ?? appearNote.value.uri ?? `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); async function addReplyTo(replyNote: Misskey.entities.Note) { diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue index 608722def0..b6dbec81c5 100644 --- a/packages/frontend/src/components/SkOldNoteWindow.vue +++ b/packages/frontend/src/components/SkOldNoteWindow.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> - <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> + <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/> </div> </header> <div :class="$style.noteContent"> @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkMediaList :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="selfNoteIds" style="margin-top: 6px;"/> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> @@ -86,13 +86,14 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import { userPage } from '@/filters/user.js'; -import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/utility/clone.js'; import { dateTimeFormat } from '@/utility/intl-const.js'; import { prefer } from '@/preferences'; import { getPluginHandlers } from '@/plugin.js'; import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; +import { getSelfNoteIds } from '@/utility/get-self-note-ids'; +import { extractPreviewUrls } from '@/utility/extract-preview-urls.js'; const props = defineProps<{ note: Misskey.entities.Note; @@ -141,14 +142,14 @@ const isRenote = ( ); const el = shallowRef<HTMLElement>(); -let appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); -const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; -const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); const showContent = ref(false); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translating = ref(false); -const urls = appearNote.value.text ? extractUrlFromMfm(mfm.parse(appearNote.value.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null; +const urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null); +const selfNoteIds = computed(() => getSelfNoteIds(props.note)); const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); </script> diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 78c1c66f52..1a80f6fef1 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only /> <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/> </MkFukidashi> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :showAsQuote="!message.fromUser.rejectQuotes" style="margin: 8px 0;"/> <div :class="$style.footer"> <button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button> <MkTime :class="$style.time" :time="message.createdAt"/> diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index d801db017e..85befbb18b 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { host } from '@@/js/config.js'; +import * as config from '@@/js/config.js'; import type { Paging } from '@/components/MkPagination.vue'; import DynamicNoteDetailed from '@/components/DynamicNoteDetailed.vue'; import MkNotes from '@/components/MkNotes.vue'; @@ -151,7 +151,7 @@ function fetchNote() { message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor, openOnRemote: { type: 'lookup', - url: `https://${host}/notes/${props.noteId}`, + url: `${config.url}/notes/${props.noteId}`, }, }); } diff --git a/packages/frontend/src/utility/extract-preview-urls.ts b/packages/frontend/src/utility/extract-preview-urls.ts new file mode 100644 index 0000000000..5fc9c87a32 --- /dev/null +++ b/packages/frontend/src/utility/extract-preview-urls.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as config from '@@/js/config.js'; +import type * as Misskey from 'misskey-js'; +import type * as mfm from '@transfem-org/sfm-js'; +import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; + +/** + * Extracts all previewable URLs from a note. + */ +export function extractPreviewUrls(note: Misskey.entities.Note, contents: mfm.MfmNode[]): string[] { + const links = extractUrlFromMfm(contents); + return links.filter(url => + // Remote note + url !== note.url && + url !== note.uri && + // Local note + url !== `${config.url}/notes/${note.id}` && + // Remote reply + url !== note.reply?.url && + url !== note.reply?.uri && + // Local reply + url !== `${config.url}/notes/${note.reply?.id}` && + // Remote renote or quote + url !== note.renote?.url && + url !== note.renote?.uri && + // Local renote or quote + url !== `${config.url}/notes/${note.renote?.id}` && + // Remote renote *of* a quote + url !== note.renote?.renote?.url && + url !== note.renote?.renote?.uri && + // Local renote *of* a quote + url !== `${config.url}/notes/${note.renote?.renote?.id}`); +} diff --git a/packages/frontend/src/utility/extract-url-from-mfm.ts b/packages/frontend/src/utility/extract-url-from-mfm.ts index baebbff8ae..260dba030e 100644 --- a/packages/frontend/src/utility/extract-url-from-mfm.ts +++ b/packages/frontend/src/utility/extract-url-from-mfm.ts @@ -4,21 +4,34 @@ */ import * as mfm from '@transfem-org/sfm-js'; -import { unique } from '@/utility/array.js'; // unique without hash // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] -const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); +const removeHash = (x: string) => { + if (URL.canParse(x)) { + const url = new URL(x); + url.hash = ''; + return url.toString(); + } else { + return x.replace(/#[^#]*$/, ''); + } +}; export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { - const urlNodes = mfm.extract(nodes, (node) => { - return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); - }); - const urls: string[] = unique(urlNodes.map(x => x.props.url)); + const urls = new Map<string, string>(); - return urls.reduce((array, url) => { - const urlWithoutHash = removeHash(url); - if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); - return array; - }, [] as string[]); + // Single iteration pass to avoid potential DoS in maliciously-constructed notes. + for (const node of nodes) { + if ((node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent))) { + const url = (node as mfm.MfmUrl | mfm.MfmLink).props.url; + const key = removeHash(url); + + // Keep the first match only, to preserve existing behavior. + if (!urls.has(key)) { + urls.set(key, url); + } + } + } + + return Array.from(urls.values()); } diff --git a/packages/frontend/src/utility/get-self-note-ids.ts b/packages/frontend/src/utility/get-self-note-ids.ts new file mode 100644 index 0000000000..7cd771e8a0 --- /dev/null +++ b/packages/frontend/src/utility/get-self-note-ids.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type * as Misskey from 'misskey-js'; + +/** + * Gets IDs of notes that are visibly the "same" as the current note. + * These are IDs that should not be recursively resolved when starting from the provided note as entry. + */ +export function getSelfNoteIds(note: Misskey.entities.Note): string[] { + const ids = [note.id]; // Regular note + if (note.reply) ids.push(note.reply.id); // Reply + else if (note.replyId) ids.push(note.replyId); // Reply (not packed) + if (note.renote) ids.push(note.renote.id); // Renote or quote + else if (note.renoteId) ids.push(note.renoteId); // Renote or quote (not packed) + if (note.renote?.renote) ids.push(note.renote.renote.id); // Renote *of* a quote + else if (note.renote?.renoteId) ids.push(note.renote.renoteId); // Renote *of* a quote (not packed) + return ids; +} |