From 83e9057b27e47171a4f78fcb11d2550478a8ce14 Mon Sep 17 00:00:00 2001 From: dakkar Date: Fri, 26 Jan 2024 18:07:34 +0000 Subject: apply to NoteEditService all recent changes from NoteCreateService --- packages/backend/src/core/NoteEditService.ts | 123 +++++++++++++++++++-------- 1 file changed, 87 insertions(+), 36 deletions(-) (limited to 'packages/backend/src/core/NoteEditService.ts') diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index dae3f485ff..d7a79920eb 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -46,6 +46,11 @@ import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isReply } from '@/misc/is-reply.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -206,6 +211,8 @@ export class NoteEditService implements OnApplicationShutdown { private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, private utilityService: UtilityService, + private userBlockingService: UserBlockingService, + private cacheService: CacheService, ) { } @bindThis @@ -222,7 +229,7 @@ export class NoteEditService implements OnApplicationShutdown { const oldnote = await this.notesRepository.findOneBy({ id: editid, - }); + }); if (oldnote == null) { throw new Error('no such note'); @@ -255,16 +262,18 @@ export class NoteEditService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.updatedAt == null) data.updatedAt = new Date(); + const meta = await this.metaService.fetch(); + if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = (await this.metaService.fetch()).sensitiveWords; - if (this.isSensitive(data, sensitiveWords)) { + const sensitiveWords = meta.sensitiveWords; + if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } - const inSilencedInstance = this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, user.host); + const inSilencedInstance = this.utilityService.isSilencedHost((meta).silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { data.visibility = 'home'; @@ -296,6 +305,18 @@ export class NoteEditService implements OnApplicationShutdown { } } + // Check blocking + if (data.renote && !this.isQuote(data)) { + if (data.renote.userHost === null) { + if (data.renote.userId !== user.id) { + const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); + if (blocked) { + throw new Error('blocked'); + } + } + } + } + // 返信対象がpublicではないならhomeにする if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { data.visibility = 'home'; @@ -316,6 +337,9 @@ export class NoteEditService implements OnApplicationShutdown { data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); } data.text = data.text.trim(); + if (data.text === '') { + data.text = null; + } } else { data.text = null; } @@ -361,6 +385,14 @@ export class NoteEditService implements OnApplicationShutdown { } } + if (user.host && !data.cw) { + await this.federatedInstanceService.fetch(user.host).then(async i => { + if (i.isNSFW) { + data.cw = 'Instance is marked as NSFW'; + } + }); + } + const update: Partial = {}; if (data.text !== oldnote.text) { update.text = data.text; @@ -587,7 +619,15 @@ export class NoteEditService implements OnApplicationShutdown { }, }); - if (!isThreadMuted) { + const [ + userIdsWhoMeMuting, + ] = data.reply.userId ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(data.reply.userId), + ]) : [new Set()]; + + const muted = isUserRelated(note, userIdsWhoMeMuting); + + if (!isThreadMuted || !muted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); @@ -603,11 +643,28 @@ export class NoteEditService implements OnApplicationShutdown { // If it is renote if (data.renote) { - const type = data.text ? 'quote' : 'renote'; + const type = this.isQuote(data) ? 'quote' : 'renote'; // Notify if (data.renote.userHost === null) { - nm.push(data.renote.userId, type); + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: data.renote.userId, + threadId: data.renote.threadId ?? data.renote.id, + }, + }); + + const [ + userIdsWhoMeMuting, + ] = data.renote.userId ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(data.renote.userId), + ]) : [new Set()]; + + const muted = isUserRelated(note, userIdsWhoMeMuting); + + if (!isThreadMuted || !muted) { + nm.push(data.renote.userId, type); + } } // Publish event @@ -657,7 +714,7 @@ export class NoteEditService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - dm.execute(); + trackPromise(dm.execute()); })(); } //#endregion @@ -686,28 +743,9 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - private isSensitive(note: Option, sensitiveWord: string[]): boolean { - if (sensitiveWord.length > 0) { - const text = note.cw ?? note.text ?? ''; - if (text === '') return false; - const matched = sensitiveWord.some(filter => { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - // This should never happen due to input sanitisation. - if (!regexp) { - const words = filter.split(' '); - return words.every(keyword => text.includes(keyword)); - } - try { - return new RE2(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - }); - if (matched) return true; - } - return false; + private isQuote(note: Option): note is Option & { renote: MiNote } { + // sync with misc/is-quote.ts + return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll); } @bindThis @@ -720,7 +758,15 @@ export class NoteEditService implements OnApplicationShutdown { }, }); - if (isThreadMuted) { + const [ + userIdsWhoMeMuting, + ] = u.id ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(u.id), + ]) : [new Set()]; + + const muted = isUserRelated(note, userIdsWhoMeMuting); + + if (isThreadMuted || muted) { continue; } @@ -748,7 +794,7 @@ export class NoteEditService implements OnApplicationShutdown { const user = await this.usersRepository.findOneBy({ id: note.userId }); if (user == null) throw new Error('user not found'); - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) + const content = data.renote && !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, false), user); @@ -782,6 +828,7 @@ export class NoteEditService implements OnApplicationShutdown { @bindThis private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { const meta = await this.metaService.fetch(); + if (!meta.enableFanoutTimeline) return; const r = this.redisForTimelines.pipeline(); @@ -825,7 +872,7 @@ export class NoteEditService implements OnApplicationShutdown { if (note.visibility === 'followers') { // TODO: 重そうだから何とかしたい Set 使う? - userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId)); + userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId)); } // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする @@ -834,7 +881,7 @@ export class NoteEditService implements OnApplicationShutdown { if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) { + if (isReply(note, following.followerId)) { if (!following.withReplies) continue; } @@ -848,11 +895,12 @@ export class NoteEditService implements OnApplicationShutdown { // ダイレクトのとき、そのリストが対象外のユーザーの場合 if ( note.visibility === 'specified' && + note.userId !== userListMembership.userListUserId && !note.visibleUserIds.some(v => v === userListMembership.userListUserId) ) continue; // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) { + if (isReply(note, userListMembership.userListUserId)) { if (!userListMembership.withReplies) continue; } @@ -870,11 +918,14 @@ export class NoteEditService implements OnApplicationShutdown { } // 自分自身以外への返信 - if (note.replyId && note.replyUserId !== note.userId) { + if (isReply(note)) { this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + if (note.replyUserHost == null) { + this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); + } } } else { this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); -- cgit v1.2.3-freya From e93e73673ab7cc3f761eeef651295efaac3708a5 Mon Sep 17 00:00:00 2001 From: dakkar Date: Fri, 26 Jan 2024 18:09:25 +0000 Subject: probably more correct muting logic we want to notify if neither the thread nor the user are muted; otherwise, for example, notes in a muted thread from a non-muted user would get notified --- packages/backend/src/core/NoteCreateService.ts | 4 ++-- packages/backend/src/core/NoteEditService.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/core/NoteEditService.ts') diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 39c2a89387..52e8795c37 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -810,7 +810,7 @@ export class NoteCreateService implements OnApplicationShutdown { const muted = isUserRelated(note, userIdsWhoMeMuting); - if (!isThreadMuted || !muted) { + if (!isThreadMuted && !muted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); @@ -845,7 +845,7 @@ export class NoteCreateService implements OnApplicationShutdown { const muted = isUserRelated(note, userIdsWhoMeMuting); - if (!isThreadMuted || !muted) { + if (!isThreadMuted && !muted) { nm.push(data.renote.userId, type); } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index d7a79920eb..bfadb1d18d 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -627,7 +627,7 @@ export class NoteEditService implements OnApplicationShutdown { const muted = isUserRelated(note, userIdsWhoMeMuting); - if (!isThreadMuted || !muted) { + if (!isThreadMuted && !muted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); @@ -662,7 +662,7 @@ export class NoteEditService implements OnApplicationShutdown { const muted = isUserRelated(note, userIdsWhoMeMuting); - if (!isThreadMuted || !muted) { + if (!isThreadMuted && !muted) { nm.push(data.renote.userId, type); } } -- cgit v1.2.3-freya From f091b84c6ed19519505594bcb9f2e802876e61fe Mon Sep 17 00:00:00 2001 From: Marie Date: Sat, 3 Feb 2024 15:01:09 +0100 Subject: chore: change sfm registry and name --- .npmrc | 2 +- packages/backend/package.json | 2 +- packages/backend/src/core/MfmService.ts | 336 ++++++++++----------- packages/backend/src/core/NoteCreateService.ts | 2 +- packages/backend/src/core/NoteEditService.ts | 4 +- .../backend/src/core/activitypub/ApMfmService.ts | 2 +- .../src/core/activitypub/ApRendererService.ts | 14 +- .../src/misc/extract-custom-emojis-from-mfm.ts | 2 +- packages/backend/src/misc/extract-hashtags.ts | 2 +- packages/backend/src/misc/extract-mentions.ts | 2 +- .../backend/src/server/api/endpoints/i/update.ts | 2 +- .../backend/src/server/api/mastodon/converters.ts | 10 +- packages/backend/test/unit/MfmService.ts | 2 +- packages/backend/test/unit/extract-mentions.ts | 2 +- packages/frontend/package.json | 2 +- packages/frontend/src/components/MkNote.vue | 4 +- .../frontend/src/components/MkNoteDetailed.vue | 6 +- packages/frontend/src/components/MkPostForm.vue | 2 +- .../frontend/src/components/MkSubNoteContent.vue | 2 +- packages/frontend/src/components/SkNote.vue | 4 +- .../frontend/src/components/SkNoteDetailed.vue | 4 +- .../frontend/src/components/SkOldNoteWindow.vue | 2 +- .../components/global/MkMisskeyFlavoredMarkdown.ts | 4 +- .../frontend/src/components/page/page.text.vue | 2 +- .../frontend/src/scripts/check-animated-mfm.ts | 2 +- packages/frontend/src/scripts/extract-mentions.ts | 2 +- .../frontend/src/scripts/extract-url-from-mfm.ts | 2 +- pnpm-lock.yaml | 24 +- 28 files changed, 223 insertions(+), 223 deletions(-) (limited to 'packages/backend/src/core/NoteEditService.ts') diff --git a/.npmrc b/.npmrc index da702c3af5..b949431c77 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -@sharkey:registry=https://git.joinsharkey.org/api/packages/Sharkey/npm/ +@transfem-org:registry=https://activitypub.software/api/v4/packages/npm/ engine-strict = true diff --git a/packages/backend/package.json b/packages/backend/package.json index 839deaf909..f4865b3d05 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -83,7 +83,7 @@ "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", - "@sharkey/sfm-js": "0.24.4", + "@transfem-org/sfm-js": "0.24.4", "@simplewebauthn/server": "9.0.0", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.10", diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 60bf8b3c2a..51c4252715 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -13,7 +13,7 @@ import { intersperse } from '@/misc/prelude/array.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; -import type * as mfm from '@sharkey/sfm-js'; +import type * as mfm from '@transfem-org/sfm-js'; const treeAdapter = TreeAdapter.defaultTreeAdapter; @@ -476,173 +476,173 @@ export class MfmService { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any; } = { - async bold(node) { - const el = doc.createElement('span'); - el.textContent = '**'; - await appendChildren(node.children, el); - el.textContent += '**'; - return el; - }, - - async small(node) { - const el = doc.createElement('small'); - await appendChildren(node.children, el); - return el; - }, - - async strike(node) { - const el = doc.createElement('span'); - el.textContent = '~~'; - await appendChildren(node.children, el); - el.textContent += '~~'; - return el; - }, - - async italic(node) { - const el = doc.createElement('span'); - el.textContent = '*'; - await appendChildren(node.children, el); - el.textContent += '*'; - return el; - }, - - async fn(node) { - const el = doc.createElement('span'); - el.textContent = '*'; - await appendChildren(node.children, el); - el.textContent += '*'; - return el; - }, - - blockCode(node) { - const pre = doc.createElement('pre'); - const inner = doc.createElement('code'); - - const nodes = node.props.code - .split(/\r\n|\r|\n/) - .map((x) => doc.createTextNode(x)); - - for (const x of intersperse('br', nodes)) { - inner.appendChild(x === 'br' ? doc.createElement('br') : x); - } - - pre.appendChild(inner); - return pre; - }, - - async center(node) { - const el = doc.createElement('div'); - await appendChildren(node.children, el); - return el; - }, - - emojiCode(node) { - return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); - }, - - unicodeEmoji(node) { - return doc.createTextNode(node.props.emoji); - }, - - hashtag: (node) => { - const a = doc.createElement('a'); - a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); - a.textContent = `#${node.props.hashtag}`; - a.setAttribute('rel', 'tag'); - a.setAttribute('class', 'hashtag'); - return a; - }, - - inlineCode(node) { - const el = doc.createElement('code'); - el.textContent = node.props.code; - return el; - }, - - mathInline(node) { - const el = doc.createElement('code'); - el.textContent = node.props.formula; - return el; - }, - - mathBlock(node) { - const el = doc.createElement('code'); - el.textContent = node.props.formula; - return el; - }, - - async link(node) { - const a = doc.createElement('a'); - a.setAttribute('rel', 'nofollow noopener noreferrer'); - a.setAttribute('target', '_blank'); - a.setAttribute('href', node.props.url); - await appendChildren(node.children, a); - return a; - }, - - async mention(node) { - const { username, host, acct } = node.props; - const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - - const el = doc.createElement('span'); - if (!resolved) { - el.textContent = acct; - } else { - el.setAttribute('class', 'h-card'); - el.setAttribute('translate', 'no'); - const a = doc.createElement('a'); - a.setAttribute('href', resolved.url ? resolved.url : resolved.uri); - a.className = 'u-url mention'; - const span = doc.createElement('span'); - span.textContent = resolved.username || username; - a.textContent = '@'; - a.appendChild(span); - el.appendChild(a); - } - - return el; - }, - - async quote(node) { - const el = doc.createElement('blockquote'); - await appendChildren(node.children, el); - return el; - }, - - text(node) { - const el = doc.createElement('span'); - const nodes = node.props.text - .split(/\r\n|\r|\n/) - .map((x) => doc.createTextNode(x)); - - for (const x of intersperse('br', nodes)) { - el.appendChild(x === 'br' ? doc.createElement('br') : x); - } - - return el; - }, - - url(node) { - const a = doc.createElement('a'); - a.setAttribute('rel', 'nofollow noopener noreferrer'); - a.setAttribute('target', '_blank'); - a.setAttribute('href', node.props.url); - a.textContent = node.props.url.replace(/^https?:\/\//, ''); - return a; - }, - - search: (node) => { - const a = doc.createElement('a'); - a.setAttribute('href', `https"google.com/${node.props.query}`); - a.textContent = node.props.content; - return a; - }, - - async plain(node) { - const el = doc.createElement('span'); - await appendChildren(node.children, el); - return el; - }, + async bold(node) { + const el = doc.createElement('span'); + el.textContent = '**'; + await appendChildren(node.children, el); + el.textContent += '**'; + return el; + }, + + async small(node) { + const el = doc.createElement('small'); + await appendChildren(node.children, el); + return el; + }, + + async strike(node) { + const el = doc.createElement('span'); + el.textContent = '~~'; + await appendChildren(node.children, el); + el.textContent += '~~'; + return el; + }, + + async italic(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, + + async fn(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, + + blockCode(node) { + const pre = doc.createElement('pre'); + const inner = doc.createElement('code'); + + const nodes = node.props.code + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); + + for (const x of intersperse('br', nodes)) { + inner.appendChild(x === 'br' ? doc.createElement('br') : x); + } + + pre.appendChild(inner); + return pre; + }, + + async center(node) { + const el = doc.createElement('div'); + await appendChildren(node.children, el); + return el; + }, + + emojiCode(node) { + return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); + }, + + unicodeEmoji(node) { + return doc.createTextNode(node.props.emoji); + }, + + hashtag: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); + a.textContent = `#${node.props.hashtag}`; + a.setAttribute('rel', 'tag'); + a.setAttribute('class', 'hashtag'); + return a; + }, + + inlineCode(node) { + const el = doc.createElement('code'); + el.textContent = node.props.code; + return el; + }, + + mathInline(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, + + mathBlock(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, + + async link(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + await appendChildren(node.children, a); + return a; + }, + + async mention(node) { + const { username, host, acct } = node.props; + const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); + + const el = doc.createElement('span'); + if (!resolved) { + el.textContent = acct; + } else { + el.setAttribute('class', 'h-card'); + el.setAttribute('translate', 'no'); + const a = doc.createElement('a'); + a.setAttribute('href', resolved.url ? resolved.url : resolved.uri); + a.className = 'u-url mention'; + const span = doc.createElement('span'); + span.textContent = resolved.username || username; + a.textContent = '@'; + a.appendChild(span); + el.appendChild(a); + } + + return el; + }, + + async quote(node) { + const el = doc.createElement('blockquote'); + await appendChildren(node.children, el); + return el; + }, + + text(node) { + const el = doc.createElement('span'); + const nodes = node.props.text + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); + + for (const x of intersperse('br', nodes)) { + el.appendChild(x === 'br' ? doc.createElement('br') : x); + } + + return el; + }, + + url(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + a.textContent = node.props.url.replace(/^https?:\/\//, ''); + return a; + }, + + search: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `https"google.com/${node.props.query}`); + a.textContent = node.props.content; + return a; + }, + + async plain(node) { + const el = doc.createElement('span'); + await appendChildren(node.children, el); + return el; + }, }; await appendChildren(nodes, doc.body); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 419e58ec62..deddfbe02a 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -4,7 +4,7 @@ */ import { setImmediate } from 'node:timers/promises'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index dae3f485ff..80ba367c7e 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -4,7 +4,7 @@ */ import { setImmediate } from 'node:timers/promises'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { DataSource, In, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; @@ -557,7 +557,7 @@ export class NoteEditService implements OnApplicationShutdown { } else { this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, - text: note.text! + text: note.text!, }); } diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index c19eb310d2..b900def2e4 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -4,7 +4,7 @@ */ import { Injectable } from '@nestjs/common'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { MfmService } from '@/core/MfmService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index f4d39d2408..19899629c3 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -6,7 +6,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -28,10 +28,10 @@ import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; +import { MetaService } from '../MetaService.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; -import { MetaService } from '../MetaService.js'; @Injectable() export class ApRendererService { @@ -821,12 +821,12 @@ export class ApRendererService { '_misskey_summary': 'misskey:_misskey_summary', 'isCat': 'misskey:isCat', // Firefish - firefish: "https://joinfirefish.org/ns#", - speakAsCat: "firefish:speakAsCat", + firefish: 'https://joinfirefish.org/ns#', + speakAsCat: 'firefish:speakAsCat', // Sharkey - sharkey: "https://joinsharkey.org/ns#", - backgroundUrl: "sharkey:backgroundUrl", - listenbrainz: "sharkey:listenbrainz", + sharkey: 'https://joinsharkey.org/ns#', + backgroundUrl: 'sharkey:backgroundUrl', + listenbrainz: 'sharkey:listenbrainz', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', }, diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 0e8dfd21f8..99d70799d0 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { unique } from '@/misc/prelude/array.js'; export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index 3598d90093..e8a7910907 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { unique } from '@/misc/prelude/array.js'; export function extractHashtags(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index b0897b05a8..fd2741f78d 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -5,7 +5,7 @@ // test is located in test/extract-mentions -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 22079de042..bfb3fffdc6 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -4,7 +4,7 @@ */ import RE2 from 're2'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { JSDOM } from 'jsdom'; diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index cce7f798fe..ca6f233b7f 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Entity } from 'megalodon'; -import mfm from '@sharkey/sfm-js'; +import mfm from '@transfem-org/sfm-js'; import { DI } from '@/di-symbols.js'; import { MfmService } from '@/core/MfmService.js'; import type { Config } from '@/config.js'; @@ -9,9 +9,9 @@ import type { MiUser } from '@/models/User.js'; import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { GetterService } from '../GetterService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { GetterService } from '../GetterService.js'; export enum IdConvertType { MastodonId, @@ -94,10 +94,10 @@ export class MastoConverters { text_url: f.url, meta: { width: f.properties.width, - height: f.properties.height + height: f.properties.height, }, description: f.comment ? f.comment : null, - blurhash: f.blurhash ? f.blurhash : null + blurhash: f.blurhash ? f.blurhash : null, }; } @@ -185,7 +185,7 @@ export class MastoConverters { sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false), spoiler_text: edit.cw ?? '', poll: null, - media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []) + media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []), }; lastDate = edit.updatedAt; history.push(awaitAll(item)); diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 49e84ccec8..8f40ae4935 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert'; -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 195e9b8198..683d9eb67c 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; -import { parse } from '@sharkey/sfm-js'; +import { parse } from '@transfem-org/sfm-js'; import { extractMentions } from '@/misc/extract-mentions.js'; describe('Extract mentions', () => { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 54c9b83298..f0bb1bb769 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,7 +24,7 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.1.0", - "@sharkey/sfm-js": "0.24.4", + "@transfem-org/sfm-js": "0.24.4", "@syuilo/aiscript": "0.17.0", "@phosphor-icons/web": "^2.0.3", "@twemoji/parser": "15.0.0", diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 22e686dc1b..824818756c 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -178,7 +178,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 1af336d868..a5839586b6 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -16,10 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only :key="emoji" :data-emoji="emoji" class="_button item" + :disabled="disabledEmojis?.value.includes(emoji)" @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > - + @@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only :key="emoji" :data-emoji="emoji" class="_button item" + :disabled="disabledEmojis?.value.includes(emoji)" @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > @@ -67,6 +69,7 @@ import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; const props = defineProps<{ emojis: string[] | Ref; + disabledEmojis?: Ref; initialShown?: boolean; hasChildSection?: boolean; customEmojiTree?: CustomEmojiFolderTree[]; @@ -84,7 +87,7 @@ const shown = ref(!!props.initialShown); function computeButtonTitle(ev: MouseEvent): void { const elm = ev.target as HTMLElement; const emoji = elm.dataset.emoji as string; - elm.title = getEmojiName(emoji) ?? emoji; + elm.title = getEmojiName(emoji); } function nestedChosen(emoji: any, ev: MouseEvent) { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index fadc1b34b9..1219a29d85 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -14,11 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="emoji in searchResultCustom" :key="emoji.name" class="_button item" + :disabled="!canReact(emoji)" :title="emoji.name" tabindex="0" @click="chosen(emoji, $event)" > - +
@@ -39,16 +40,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -57,15 +59,16 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.recentUsed }}
@@ -76,7 +79,8 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="child in customEmojiFolderRoot.children" :key="`custom:${child.value}`" :initialShown="false" - :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))" + :emojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).map(e => `:${e.name}:`))" + :disabledEmojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).filter(e => !canReact(e)).map(e => `:${e.name}:`))" :hasChildSection="child.children.length !== 0" :customEmojiTree="child.children" @chosen="chosen" @@ -109,6 +113,7 @@ import { unicodeEmojiCategories as categories, getEmojiName, CustomEmojiFolderTree, + getUnicodeEmoji, } from '@/scripts/emojilist.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; @@ -146,6 +151,13 @@ const { recentlyUsedEmojis, } = defaultStore.reactiveState; +const recentlyUsedEmojisDef = computed(() => { + return recentlyUsedEmojis.value.map(getDef).filter(x => x != null); +}); +const pinnedEmojisDef = computed(() => { + return pinned.value?.map(getDef).filter(x => x != null); +}); + const pinned = computed(() => props.pinnedEmojis); const size = computed(() => emojiPickerScale.value); const width = computed(() => emojiPickerWidth.value); @@ -337,14 +349,18 @@ watch(q, () => { return matches; }; - searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable); + searchResultCustom.value = Array.from(searchCustom()); searchResultUnicode.value = Array.from(searchUnicode()); }); -function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { +function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji); } +function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean { + return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category; +} + function focus() { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { searchEl.value?.focus({ @@ -362,11 +378,22 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; } +function getDef(emoji: string): string | Misskey.entities.EmojiSimple | UnicodeEmojiDef { + if (emoji.includes(':')) { + // カスタム絵文字が存在する場合はその情報を持つオブジェクトを返し、 + // サーバの管理画面から削除された等で情報が見つからない場合は名前の文字列をそのまま返しておく(undefinedを返すとエラーになるため) + const name = emoji.replaceAll(':', ''); + return customEmojisMap.get(name) ?? emoji; + } else { + return getUnicodeEmoji(emoji); + } +} + /** @see MkEmojiPicker.section.vue */ function computeButtonTitle(ev: MouseEvent): void { const elm = ev.target as HTMLElement; const emoji = elm.dataset.emoji as string; - elm.title = getEmojiName(emoji) ?? emoji; + elm.title = getEmojiName(emoji); } function chosen(emoji: any, ev?: MouseEvent) { @@ -526,6 +553,18 @@ defineExpose({ width: auto; height: auto; min-width: 0; + + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } } } } @@ -548,6 +587,18 @@ defineExpose({ width: auto; height: auto; min-width: 0; + + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } } } } @@ -663,6 +714,18 @@ defineExpose({ box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } + > .emoji { height: 1.25em; vertical-align: -.25em; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 59577f8070..c6b3896989 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', v: any): void; + (ev: 'done', v: string): void; (ev: 'close'): void; (ev: 'closed'): void; }>(); @@ -64,7 +64,7 @@ const emit = defineEmits<{ const modal = shallowRef>(); const picker = shallowRef>(); -function chosen(emoji: any) { +function chosen(emoji: string) { emit('done', emoji); if (props.choseAndClose) { modal.value?.close(); diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue deleted file mode 100644 index 6952943345..0000000000 --- a/packages/frontend/src/components/MkEmojiPickerWindow.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 0d8734799c..deedc5badb 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -21,37 +21,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
{{ i18n.ts.loadReplies }}
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index bbfc14502e..68bf1bf3d8 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -40,6 +40,7 @@ import { notificationTypes } from '@/const.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; +import * as Misskey from 'misskey-js'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; @@ -80,17 +81,19 @@ function reload() { }); } -let connection; +let connection: Misskey.ChannelConnection; onMounted(() => { connection = useStream().useChannel('main'); connection.on('notification', onNotification); + connection.on('notificationFlushed', reload); }); onActivated(() => { pagingComponent.value?.reload(); connection = useStream().useChannel('main'); connection.on('notification', onNotification); + connection.on('notificationFlushed', reload); }); onUnmounted(() => { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index aad2f80871..d9e50fbb79 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -174,7 +174,7 @@ const emit = defineEmits<{ const textareaEl = shallowRef(null); const cwInputEl = shallowRef(null); const hashtagsInputEl = shallowRef(null); -const visibilityButton = shallowRef(null); +const visibilityButton = shallowRef(); const posting = ref(false); const posted = ref(false); @@ -467,6 +467,7 @@ function setVisibility() { isSilenced: $i.isSilenced, localOnly: localOnly.value, src: visibilityButton.value, + ...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}), }, { changeVisibility: v => { visibility.value = v; diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index 59ceab27dc..068a2968db 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 3158ba436e..8b5e6efdf3 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -44,7 +44,7 @@ function getReactionName(reaction: string): string { if (trimLocal.startsWith(':')) { return trimLocal; } - return getEmojiName(reaction) ?? reaction; + return getEmojiName(reaction); } diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index e9b73fb77a..2464d21b6a 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -33,7 +33,8 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; -import { customEmojis } from '@/custom-emojis.js'; +import { customEmojisMap } from '@/custom-emojis.js'; +import { getUnicodeEmoji } from '@/scripts/emojilist.js'; const props = defineProps<{ reaction: string; @@ -50,13 +51,11 @@ const emit = defineEmits<{ const buttonEl = shallowRef(); -const isCustomEmoji = computed(() => props.reaction.includes(':')); -const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null); +const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); +const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); const canToggle = computed(() => { - return !props.reaction.match(/@\w/) && $i - && (emoji.value && checkReactionPermissions($i, props.note, emoji.value)) - || !isCustomEmoji.value; + return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 6d2531a8ea..6550fc4ec1 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index c4a3e4dc4a..bd6edad663 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -9,21 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.visibility }}
- - -
@@ -174,7 +174,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.loadReplies }}
@@ -191,7 +191,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.loadReplies }}
@@ -798,10 +798,6 @@ onUnmounted(() => { padding-bottom: 0; } -.replyToMore { - -} - .renote { display: flex; align-items: center; diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts index 9d57841f04..c1d8cf0ca6 100644 --- a/packages/frontend/src/components/global/MkA.stories.impl.ts +++ b/packages/frontend/src/components/global/MkA.stories.impl.ts @@ -32,7 +32,8 @@ export const Default = { async play({ canvasElement }) { const canvas = within(canvasElement); const a = canvas.getByRole('link'); - await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + // FIXME: 通るけどその後落ちるのでコメントアウト + // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); await userEvent.pointer({ keys: '[MouseRight]', target: a }); await tick(); const menu = canvas.getByRole('menu'); @@ -44,6 +45,7 @@ export const Default = { }, args: { to: '#test', + behavior: 'browser', }, parameters: { layout: 'centered', diff --git a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts index e0da6a4a13..9e6177045d 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts +++ b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts @@ -48,3 +48,18 @@ export const Missing = { name: Default.args.name, }, } satisfies StoryObj; +export const ErrorToText = { + ...Default, + args: { + url: 'https://example.com/404', + name: Default.args.name, + }, +} satisfies StoryObj; +export const ErrorToImage = { + ...Default, + args: { + url: 'https://example.com/404', + name: Default.args.name, + fallbackToImage: true, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 2c65920957..b57a311c0c 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only -->