merge: Skip preview for recursive links (!1039)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1039

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2025-05-21 16:43:59 +00:00
commit 1e434c8f4c
15 changed files with 133 additions and 48 deletions

View file

@ -890,6 +890,7 @@ export class ClientServerService {
return await reply.view('info-card', { return await reply.view('info-card', {
version: this.config.version, version: this.config.version,
host: this.config.host, host: this.config.host,
url: this.config.url,
meta: this.meta, meta: this.meta,
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),

View file

@ -43,7 +43,7 @@ html
} }
body body
a#a(href=`https://${host}` target="_blank") a#a(href=url target="_blank")
header#banner(style=`background-image: url(${meta.bannerUrl})`) header#banner(style=`background-image: url(${meta.bannerUrl})`)
div#title= meta.name || host div#title= meta.name || host
div#content div#content

View file

@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </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/> <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"> <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>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></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"> <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 * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { shouldCollapsed } from '@@/js/collapsed.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 { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
@ -235,6 +235,8 @@ import { DI } from '@/di.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import SkMutedNote from '@/components/SkMutedNote.vue'; import SkMutedNote from '@/components/SkMutedNote.vue';
import SkNoteTranslation from '@/components/SkNoteTranslation.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<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -302,7 +304,8 @@ const galleryEl = useTemplateRef('galleryEl');
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW); const showContent = ref(prefer.s.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); 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 isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong); const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
@ -325,7 +328,7 @@ const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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)); const mergedCW = computed(() => computeMergedCw(appearNote.value));

View file

@ -112,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </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"/> <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"> <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>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
</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 mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.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 { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { Paging } from '@/components/MkPagination.vue'; import type { Paging } from '@/components/MkPagination.vue';
@ -285,6 +285,8 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import SkMutedNote from '@/components/SkMutedNote.vue'; import SkMutedNote from '@/components/SkMutedNote.vue';
import SkNoteTranslation from '@/components/SkNoteTranslation.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<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -337,9 +339,10 @@ const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false); const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const parsed = computed(() => 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 urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : 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 allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm);
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
@ -372,7 +375,7 @@ let renoting = false;
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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 = { const keymap = {

View file

@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, shallowRef, watch } from 'vue'; import { computed, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.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 { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue';
@ -150,7 +150,7 @@ const isRenote = (
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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) { async function addReplyTo(replyNote: Misskey.entities.Note) {

View file

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js'; 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 { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js'; import { sum } from '@/utility/array.js';
@ -72,7 +72,7 @@ const showResult = ref(props.readOnly || isVoted.value);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', type: 'lookup',
url: `https://${host}/notes/${props.noteId}`, url: `${config.url}/notes/${props.noteId}`,
})); }));
// //

View file

@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </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/> <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"> <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>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></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"> <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 * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { shouldCollapsed } from '@@/js/collapsed.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 { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
@ -235,6 +235,8 @@ import { DI } from '@/di.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import SkMutedNote from '@/components/SkMutedNote.vue'; import SkMutedNote from '@/components/SkMutedNote.vue';
import SkNoteTranslation from '@/components/SkNoteTranslation.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<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -302,7 +304,8 @@ const galleryEl = useTemplateRef('galleryEl');
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW); const showContent = ref(prefer.s.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); 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 isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong); const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
@ -325,7 +328,7 @@ const allowAnim = ref(prefer.s.advancedMfm && prefer.s.animatedMfm);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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)); const mergedCW = computed(() => computeMergedCw(appearNote.value));

View file

@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </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"/> <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"> <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>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> <div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
</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 mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.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 { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { Paging } from '@/components/MkPagination.vue'; import type { Paging } from '@/components/MkPagination.vue';
@ -290,6 +290,8 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import SkMutedNote from '@/components/SkMutedNote.vue'; import SkMutedNote from '@/components/SkMutedNote.vue';
import SkNoteTranslation from '@/components/SkNoteTranslation.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<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -343,9 +345,10 @@ const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false); const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const parsed = computed(() => 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 urls = computed(() => parsed.value ? extractPreviewUrls(props.note, parsed.value) : null);
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : 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 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 showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
@ -378,7 +381,7 @@ let renoting = false;
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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 = { const keymap = {

View file

@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, ref, shallowRef, watch } from 'vue'; import { computed, inject, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.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 { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import SkNoteHeader from '@/components/SkNoteHeader.vue'; import SkNoteHeader from '@/components/SkNoteHeader.vue';
@ -164,7 +164,7 @@ const isRenote = (
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup', 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) { async function addReplyTo(replyNote: Misskey.entities.Note) {

View file

@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></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> </div>
</header> </header>
<div :class="$style.noteContent"> <div :class="$style.noteContent">
@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList :mediaList="appearNote.files"/>
</div> </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"/> <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 v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
</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> <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 MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { dateTimeFormat } from '@/utility/intl-const.js'; import { dateTimeFormat } from '@/utility/intl-const.js';
import { prefer } from '@/preferences'; import { prefer } from '@/preferences';
import { getPluginHandlers } from '@/plugin.js'; import { getPluginHandlers } from '@/plugin.js';
import SkNoteTranslation from '@/components/SkNoteTranslation.vue'; 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<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -141,14 +142,14 @@ const isRenote = (
); );
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
let appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null; const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const showContent = ref(false); const showContent = ref(false);
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false); 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); const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
</script> </script>

View file

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/> />
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/> <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
</MkFukidashi> </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"> <div :class="$style.footer">
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button> <button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
<MkTime :class="$style.time" :time="message.createdAt"/> <MkTime :class="$style.time" :time="message.createdAt"/>

View file

@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, ref } from 'vue'; import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; 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 type { Paging } from '@/components/MkPagination.vue';
import DynamicNoteDetailed from '@/components/DynamicNoteDetailed.vue'; import DynamicNoteDetailed from '@/components/DynamicNoteDetailed.vue';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
@ -151,7 +151,7 @@ function fetchNote() {
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor, message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
openOnRemote: { openOnRemote: {
type: 'lookup', type: 'lookup',
url: `https://${host}/notes/${props.noteId}`, url: `${config.url}/notes/${props.noteId}`,
}, },
}); });
} }

View file

@ -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}`);
}

View file

@ -4,21 +4,34 @@
*/ */
import * as mfm from '@transfem-org/sfm-js'; import * as mfm from '@transfem-org/sfm-js';
import { unique } from '@/utility/array.js';
// unique without hash // unique without hash
// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] // [ 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[] { export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
const urlNodes = mfm.extract(nodes, (node) => { const urls = new Map<string, string>();
return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
});
const urls: string[] = unique(urlNodes.map(x => x.props.url));
return urls.reduce((array, url) => { // Single iteration pass to avoid potential DoS in maliciously-constructed notes.
const urlWithoutHash = removeHash(url); for (const node of nodes) {
if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); if ((node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent))) {
return array; const url = (node as mfm.MfmUrl | mfm.MfmLink).props.url;
}, [] as string[]); 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());
} }

View file

@ -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;
}