diff --git a/Dockerfile b/Dockerfile index 72f934b3ce..e6e60992e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ ARG UID="991" ARG GID="991" ENV COREPACK_DEFAULT_TO_LATEST=0 -RUN apk add ffmpeg tini jemalloc pixman pango cairo libpng \ +RUN apk add ffmpeg tini jemalloc pixman pango cairo libpng librsvg font-noto font-noto-cjk font-noto-thai \ && corepack enable \ && addgroup -g "${GID}" sharkey \ && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 0dd0a9b822..f4159facc3 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -731,7 +731,7 @@ export class NoteCreateService implements OnApplicationShutdown { //#region AP deliver if (!data.localOnly && this.userEntityService.isLocalUser(user)) { trackTask(async () => { - const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); + const noteActivity = await this.apRendererService.renderNoteOrRenoteActivity(note, user, { renote: data.renote }); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); // メンションされたリモートユーザーに配送 @@ -874,17 +874,6 @@ export class NoteCreateService implements OnApplicationShutdown { this.notesRepository.increment({ id: reply.id }, 'repliesCount', 1); } - @bindThis - private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) { - if (data.localOnly) return null; - - const content = this.isRenote(data) && !this.isQuote(data) - ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) - : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, user, false), note); - - return this.apRendererService.addContext(content); - } - @bindThis private index(note: MiNote) { if (note.text == null && note.cw == null) return; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 533ee7942d..4be097465d 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -675,7 +675,7 @@ export class NoteEditService implements OnApplicationShutdown { //#region AP deliver if (!data.localOnly && this.userEntityService.isLocalUser(user)) { trackTask(async () => { - const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); + const noteActivity = await this.apRendererService.renderNoteOrRenoteActivity(note, user, { renote: data.renote }); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); // メンションされたリモートユーザーに配送 @@ -770,17 +770,6 @@ export class NoteEditService implements OnApplicationShutdown { (note.files != null && note.files.length > 0); } - @bindThis - private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) { - if (data.localOnly) return null; - - const content = this.isRenote(data) && !this.isQuote(data) - ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) - : this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user); - - return this.apRendererService.addContext(content); - } - @bindThis private index(note: MiNote) { if (note.text == null && note.cw == null) return; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 08a8f30049..623e7002cd 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -32,6 +32,8 @@ import { IdService } from '@/core/IdService.js'; import { appendContentWarning } from '@/misc/append-content-warning.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; @@ -75,6 +77,7 @@ export class ApRendererService { private idService: IdService, private readonly queryService: QueryService, private utilityService: UtilityService, + private readonly cacheService: CacheService, ) { } @@ -232,7 +235,7 @@ export class ApRendererService { */ @bindThis public async renderFollowUser(id: MiUser['id']): Promise { - const user = await this.usersRepository.findOneByOrFail({ id: id }) as MiPartialLocalUser | MiPartialRemoteUser; + const user = await this.cacheService.findUserById(id) as MiPartialLocalUser | MiPartialRemoteUser; return this.userEntityService.getUserUri(user); } @@ -402,7 +405,7 @@ export class ApRendererService { inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { - const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); + const inReplyToUser = await this.cacheService.findUserById(inReplyToNote.userId); if (inReplyToUser) { if (inReplyToNote.uri) { @@ -422,7 +425,7 @@ export class ApRendererService { let quote: string | undefined = undefined; - if (note.renoteId) { + if (isRenote(note) && isQuote(note)) { const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); if (renote) { @@ -542,6 +545,7 @@ export class ApRendererService { attributedTo, summary: summary ?? undefined, content: content ?? undefined, + updated: note.updatedAt?.toISOString() ?? undefined, _misskey_content: text, source: { content: text, @@ -756,176 +760,6 @@ export class ApRendererService { }; } - @bindThis - public async renderUpNote(note: MiNote, author: MiUser, dive = true): Promise { - const getPromisedFiles = async (ids: string[]): Promise => { - if (ids.length === 0) return []; - const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); - }; - - let inReplyTo; - let inReplyToNote: MiNote | null; - - if (note.replyId) { - inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); - - if (inReplyToNote != null) { - const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); - - if (inReplyToUser) { - if (inReplyToNote.uri) { - inReplyTo = inReplyToNote.uri; - } else { - if (dive) { - inReplyTo = await this.renderUpNote(inReplyToNote, inReplyToUser, false); - } else { - inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`; - } - } - } - } - } else { - inReplyTo = null; - } - - let quote: string | undefined = undefined; - - if (note.renoteId) { - const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); - - if (renote) { - quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`; - } - } - - const attributedTo = this.userEntityService.genLocalUserUri(note.userId); - - const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : []; - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`].concat(mentions); - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); - } else if (note.visibility === 'followers') { - to = [`${attributedTo}/followers`]; - cc = mentions; - } else { - to = mentions; - } - - const mentionedUsers = note.mentions && note.mentions.length > 0 ? await this.usersRepository.findBy({ - id: In(note.mentions), - }) : []; - - const hashtagTags = note.tags.map(tag => this.renderHashtag(tag)); - const mentionTags = mentionedUsers.map(u => this.renderMention(u as MiLocalUser | MiRemoteUser)); - - const files = await getPromisedFiles(note.fileIds); - - const text = note.text ?? ''; - let poll: MiPoll | null = null; - - if (note.hasPoll) { - poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - } - - const apAppend: Appender[] = []; - - if (quote) { - // Append quote link as `

RE: ...` - // the claas name `quote-inline` is used in non-misskey clients for styling quote notes. - // For compatibility, the span part should be kept as possible. - apAppend.push((doc, body) => { - body.childNodes.push(new Element('br', {})); - body.childNodes.push(new Element('br', {})); - const span = new Element('span', { - class: 'quote-inline', - }); - span.childNodes.push(new Text('RE: ')); - const link = new Element('a', { - href: quote, - }); - link.childNodes.push(new Text(quote)); - span.childNodes.push(link); - body.childNodes.push(span); - }); - } - - let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - - // Apply mandatory CW, if applicable - if (author.mandatoryCW) { - summary = appendContentWarning(summary, author.mandatoryCW); - } - - const { content } = this.apMfmService.getNoteHtml(note, apAppend); - - const emojis = await this.getEmojis(note.emojis); - const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - - const tag: IObject[] = [ - ...hashtagTags, - ...mentionTags, - ...apemojis, - ]; - - // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md - if (quote) { - tag.push({ - type: 'Link', - mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - rel: 'https://misskey-hub.net/ns#_misskey_quote', - href: quote, - } satisfies ILink); - } - - const asPoll = poll ? { - type: 'Question', - [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, - [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ - type: 'Note', - name: text, - replies: { - type: 'Collection', - totalItems: poll!.votes[i], - }, - })), - } as const : {}; - - return { - id: `${this.config.url}/notes/${note.id}`, - type: 'Note', - attributedTo, - summary: summary ?? undefined, - content: content ?? undefined, - updated: note.updatedAt?.toISOString(), - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - _misskey_quote: quote, - quoteUrl: quote, - quoteUri: quote, - // https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md - quote: quote, - published: this.idService.parse(note.id).date.toISOString(), - to, - cc, - inReplyTo, - attachment: files.map(x => this.renderDocument(x)), - sensitive: note.cw != null || files.some(file => file.isSensitive), - tag, - ...asPoll, - }; - } - @bindThis public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate { return { @@ -1079,6 +913,27 @@ export class ApRendererService { }; } + @bindThis + public async renderNoteOrRenoteActivity(note: MiNote, user: MiUser, hint?: { renote?: MiNote | null }) { + if (note.localOnly) return null; + + if (isPureRenote(note)) { + const renote = hint?.renote ?? note.renote ?? await this.notesRepository.findOneByOrFail({ id: note.renoteId }); + const apAnnounce = this.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note); + return this.addContext(apAnnounce); + } + + const apNote = await this.renderNote(note, user, false); + + if (note.updatedAt != null) { + const apUpdate = this.renderUpdate(apNote, user); + return this.addContext(apUpdate); + } else { + const apCreate = this.renderCreate(apNote, note); + return this.addContext(apCreate); + } + } + @bindThis private async getEmojis(names: string[]): Promise { if (names.length === 0) return []; diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 201920612c..d53e265d36 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -21,6 +21,8 @@ import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { toArray } from '@/misc/prelude/array.js'; +import { isPureRenote } from '@/misc/is-renote.js'; +import { CacheService } from '@/core/CacheService.js'; import { AnyCollection, getApId, getNullableApId, IObjectWithId, isCollection, isCollectionOrOrderedCollection, isCollectionPage, isOrderedCollection, isOrderedCollectionPage } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; @@ -49,6 +51,7 @@ export class Resolver { private loggerService: LoggerService, private readonly apLogService: ApLogService, private readonly apUtilityService: ApUtilityService, + private readonly cacheService: CacheService, private recursionLimit = 256, ) { this.history = new Set(); @@ -355,18 +358,20 @@ export class Resolver { switch (parsed.type) { case 'notes': - return this.notesRepository.findOneByOrFail({ id: parsed.id, userHost: IsNull() }) + return this.notesRepository.findOneOrFail({ where: { id: parsed.id, userHost: IsNull() }, relations: { user: true, renote: true } }) .then(async note => { - const author = await this.usersRepository.findOneByOrFail({ id: note.userId }); + const author = note.user ?? await this.cacheService.findUserById(note.userId); if (parsed.rest === 'activity') { - // this refers to the create activity and not the note itself - return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note)); + return await this.apRendererService.renderNoteOrRenoteActivity(note, author); + } else if (!isPureRenote(note)) { + const apNote = await this.apRendererService.renderNote(note, author); + return this.apRendererService.addContext(apNote); } else { - return this.apRendererService.renderNote(note, author); + throw new IdentifiableError('732c2633-3395-4d51-a9b7-c7084774e3e7', `Failed to resolve local ${url}: cannot resolve a boost as note`); } }) as Promise; case 'users': - return this.usersRepository.findOneByOrFail({ id: parsed.id, host: IsNull() }) + return this.cacheService.findLocalUserById(parsed.id) .then(user => this.apRendererService.renderPerson(user as MiLocalUser)); case 'questions': // Polls are indexed by the note they are attached to. @@ -387,14 +392,8 @@ export class Resolver { .then(async followRequest => { if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `failed to resolve local ${url}: invalid follow request ID`); const [follower, followee] = await Promise.all([ - this.usersRepository.findOneBy({ - id: followRequest.followerId, - host: IsNull(), - }), - this.usersRepository.findOneBy({ - id: followRequest.followeeId, - host: Not(IsNull()), - }), + this.cacheService.findLocalUserById(followRequest.followerId), + this.cacheService.findLocalUserById(followRequest.followeeId), ]); if (follower == null || followee == null) { throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `failed to resolve local ${url}: follower or followee does not exist`); @@ -440,6 +439,7 @@ export class ApResolverService { private loggerService: LoggerService, private readonly apLogService: ApLogService, private readonly apUtilityService: ApUtilityService, + private readonly cacheService: CacheService, ) { } @@ -465,6 +465,7 @@ export class ApResolverService { this.loggerService, this.apLogService, this.apUtilityService, + this.cacheService, opts?.recursionLimit, ); } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index a362308b17..27d25d2152 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -33,7 +33,7 @@ import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { IActivity, IAnnounce, ICreate } from '@/core/activitypub/type.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; import * as Acct from '@/misc/acct.js'; import { CacheService } from '@/core/CacheService.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; @@ -571,7 +571,7 @@ export class ActivityPubServerService { const pinnedNotes = (await Promise.all(pinings.map(pining => this.notesRepository.findOneByOrFail({ id: pining.noteId })))) - .filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility)); + .filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility) && !isPureRenote(note)); const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note, user))); @@ -842,6 +842,11 @@ export class ActivityPubServerService { return; } + // Boosts don't federate directly - they should only be referenced as an activity + if (isPureRenote(note)) { + return 404; + } + this.setResponseType(request, reply); const author = await this.usersRepository.findOneByOrFail({ id: note.userId }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 0bf85ef8eb..34241d13cb 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -19,6 +19,7 @@ import type { PollsRepository, UsersRepository, } from '@/models/_.js'; +import type { CacheService } from '@/core/CacheService.js'; import { ApLogService } from '@/core/ApLogService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { fromTuple } from '@/misc/from-tuple.js'; @@ -53,6 +54,7 @@ export class MockResolver extends Resolver { loggerService, {} as ApLogService, {} as ApUtilityService, + {} as CacheService, ); } diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 3ad9cdc4d5..ff93e1be07 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -674,59 +674,6 @@ describe('ActivityPub', () => { }); }); - describe('renderUpnote', () => { - describe('summary', () => { - // I actually don't know why it does this, but the logic was already there so I've preserved it. - it('should be zero-width space when CW is empty string', async () => { - note.cw = ''; - - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBe(String.fromCharCode(0x200B)); - }); - - it('should be undefined when CW is null', async () => { - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBeUndefined(); - }); - - it('should be CW when present without mandatoryCW', async () => { - note.cw = 'original'; - - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBe('original'); - }); - - it('should be mandatoryCW when present without CW', async () => { - author.mandatoryCW = 'mandatory'; - - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBe('mandatory'); - }); - - it('should be merged when CW and mandatoryCW are both present', async () => { - note.cw = 'original'; - author.mandatoryCW = 'mandatory'; - - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBe('original, mandatory'); - }); - - it('should be CW when CW includes mandatoryCW', async () => { - note.cw = 'original and mandatory'; - author.mandatoryCW = 'mandatory'; - - const result = await rendererService.renderUpNote(note, author, false); - - expect(result.summary).toBe('original and mandatory'); - }); - }); - }); - describe('renderPersonRedacted', () => { it('should include minimal properties', async () => { const result = await rendererService.renderPersonRedacted(author); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 133ccababf..56bfa5de94 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -306,7 +306,7 @@ 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 ? extractPreviewUrls(props.note, parsed.value) : []); +const urls = computed(() => parsed.value ? extractPreviewUrls(appearNote.value, parsed.value) : []); 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); diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 6b70fddecf..58de5bd5a7 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -114,6 +114,7 @@ import { prefer } from '@/preferences.js'; import { useNoteCapture } from '@/use/use-note-capture.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import { instance, policies } from '@/instance'; +import { getAppearNote } from '@/utility/get-appear-note'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -128,7 +129,9 @@ const props = withDefaults(defineProps<{ onDeleteCallback: undefined, }); -const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id); +const appearNote = computed(() => getAppearNote(props.note)); + +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const el = shallowRef(); const translation = ref(null); @@ -144,19 +147,11 @@ const likeButton = shallowRef(); const renoteTooltip = computeRenoteTooltip(renoted); -const appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null); const replies = ref([]); const mergedCW = computed(() => computeMergedCw(appearNote.value)); -const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds && props.note.fileIds.length === 0 && - props.note.poll == null -); - const pleaseLoginContext = computed(() => ({ type: 'lookup', url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, @@ -206,8 +201,8 @@ async function reply(viaKeyboard = false): Promise { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); await os.post({ - reply: props.note, - channel: props.note.channel ?? undefined, + reply: appearNote.value, + channel: appearNote.value.channel ?? undefined, animation: !viaKeyboard, }); focus(); @@ -217,9 +212,9 @@ function react(): void { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); - if (props.note.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { misskeyApi('notes/like', { - noteId: props.note.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -233,12 +228,12 @@ function react(): void { } } else { blur(); - reactionPicker.show(reactButton.value ?? null, props.note, reaction => { + reactionPicker.show(reactButton.value ?? null, appearNote.value, reaction => { misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -252,7 +247,7 @@ function like(): void { showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { - noteId: props.note.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -361,7 +356,7 @@ function quote() { }).then((cancelled) => { if (cancelled) return; misskeyApi('notes/renotes', { - noteId: props.note.id, + noteId: appearNote.value.id, userId: $i?.id, limit: 1, quote: true, @@ -383,12 +378,12 @@ function quote() { } function menu(): void { - const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: appearNote.value, translating, translation, isDeleted }); os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } async function clip(): Promise { - os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: appearNote.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } async function translate() { @@ -397,7 +392,7 @@ async function translate() { if (props.detail) { misskeyApi('notes/children', { - noteId: props.note.id, + noteId: appearNote.value.id, limit: prefer.s.numberOfReplies, showQuotes: false, }).then(res => { diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index c554319c30..4d6d080ddf 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -305,7 +305,7 @@ 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 ? extractPreviewUrls(props.note, parsed.value) : []); +const urls = computed(() => parsed.value ? extractPreviewUrls(appearNote.value, parsed.value) : []); 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); diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index a520c9744e..4e8a3147ad 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -122,6 +122,7 @@ import { prefer } from '@/preferences.js'; import { useNoteCapture } from '@/use/use-note-capture.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; import { instance, policies } from '@/instance'; +import { getAppearNote } from '@/utility/get-appear-note'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -141,7 +142,9 @@ const props = withDefaults(defineProps<{ onDeleteCallback: undefined, }); -const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id); +const appearNote = computed(() => getAppearNote(props.note)); + +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const hideLine = computed(() => props.detail); const el = shallowRef(); @@ -158,19 +161,11 @@ const likeButton = shallowRef(); const renoteTooltip = computeRenoteTooltip(renoted); -let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null); const replies = ref([]); const mergedCW = computed(() => computeMergedCw(appearNote.value)); -const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds && props.note.fileIds.length === 0 && - props.note.poll == null -); - const pleaseLoginContext = computed(() => ({ type: 'lookup', url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, @@ -220,8 +215,8 @@ async function reply(viaKeyboard = false): Promise { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); await os.post({ - reply: props.note, - channel: props.note.channel ?? undefined, + reply: appearNote.value, + channel: appearNote.value.channel ?? undefined, animation: !viaKeyboard, }); focus(); @@ -231,9 +226,9 @@ function react(): void { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); - if (props.note.reactionAcceptance === 'likeOnly') { + if (appearNote.value.reactionAcceptance === 'likeOnly') { misskeyApi('notes/like', { - noteId: props.note.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = reactButton.value as HTMLElement | null | undefined; @@ -247,12 +242,12 @@ function react(): void { } } else { blur(); - reactionPicker.show(reactButton.value ?? null, props.note, reaction => { + reactionPicker.show(reactButton.value ?? null, appearNote.value, reaction => { misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: appearNote.value.id, reaction: reaction, }); - if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -266,7 +261,7 @@ function like(): void { showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { - noteId: props.note.id, + noteId: appearNote.value.id, override: defaultLike.value, }); const el = likeButton.value as HTMLElement | null | undefined; @@ -375,7 +370,7 @@ function quote() { }).then((cancelled) => { if (cancelled) return; misskeyApi('notes/renotes', { - noteId: props.note.id, + noteId: appearNote.value.id, userId: $i?.id, limit: 1, quote: true, @@ -397,12 +392,12 @@ function quote() { } function menu(): void { - const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, isDeleted }); + const { menu, cleanup } = getNoteMenu({ note: appearNote.value, translating, translation, isDeleted }); os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } async function clip(): Promise { - os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: appearNote.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } async function translate() { @@ -411,7 +406,7 @@ async function translate() { if (props.detail) { misskeyApi('notes/children', { - noteId: props.note.id, + noteId: appearNote.value.id, limit: prefer.s.numberOfReplies, showQuotes: false, }).then(res => {