diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 6263c9ebb..4c24f86d1 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -535,7 +535,7 @@ export class NoteCreateService implements OnApplicationShutdown { }); } - if (data.renote && data.renote.userId !== user.id && !user.isBot) { + if (data.renote && data.text == null && data.renote.userId !== user.id && !user.isBot) { this.incRenoteCount(data.renote); } diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 1e569d980..a16740c81 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -34,6 +34,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + showQuotes: { type: 'boolean', default: true }, }, required: ['noteId'], } as const; @@ -51,17 +52,19 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { qb - .where('note.replyId = :noteId', { noteId: ps.noteId }) - .orWhere(new Brackets(qb => { - qb - .where('note.renoteId = :noteId', { noteId: ps.noteId }) - .andWhere(new Brackets(qb => { - qb - .where('note.text IS NOT NULL') - .orWhere('note.fileIds != \'{}\'') - .orWhere('note.hasPoll = TRUE'); - })); - })); + .where('note.replyId = :noteId', { noteId: ps.noteId }); + if (ps.showQuotes) { + qb.orWhere(new Brackets(qb => { + qb + .where('note.renoteId = :noteId', { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { + qb + .where('note.text IS NOT NULL') + .orWhere('note.fileIds != \'{}\'') + .orWhere('note.hasPoll = TRUE'); + })); + })); + } })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 2099701ab..063650b3c 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -44,6 +44,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + quote: { type: 'boolean', default: false }, }, required: ['noteId'], } as const; @@ -74,7 +75,13 @@ export default class extends Endpoint { // eslint- if (ps.userId) { query.andWhere("user.id = :userId", { userId: ps.userId }); - } + } + + if (ps.quote) { + query.andWhere("note.text IS NOT NULL"); + } else { + query.andWhere("note.text IS NULL"); + } this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 1b71de496..249344a6f 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -38,6 +38,7 @@ export const paramDef = { type: 'object', properties: { noteId: { type: 'string', format: 'misskey:id' }, + quote: { type: 'boolean', default: false }, }, required: ['noteId'], } as const; @@ -66,7 +67,11 @@ export default class extends Endpoint { // eslint- }); for (const note of renotes) { - this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: me.id }), note, false); + if (ps.quote) { + if (note.text) this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: me.id }), note, false); + } else { + if (!note.text) this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: me.id }), note, false); + } } }); } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 9e4bc38a8..2f5966be6 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -110,6 +110,17 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -216,6 +227,7 @@ const menuButton = shallowRef(); const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); +const quoteButton = shallowRef(); const clipButton = shallowRef(); const likeButton = shallowRef(); let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); @@ -229,6 +241,7 @@ const isLong = shouldCollapsed(appearNote); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); const renoted = ref(false); +const quoted = ref(false); const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); @@ -271,6 +284,25 @@ useTooltip(renoteButton, async (showing) => { }, {}, 'closed'); }); +useTooltip(quoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + quote: true, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: quoteButton.value, + }, {}, 'closed'); +}); + if ($i) { os.api("notes/renotes", { noteId: appearNote.id, @@ -279,6 +311,15 @@ if ($i) { }).then((res) => { renoted.value = res.length > 0; }); + + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + quoted.value = res.length > 0; + }); } type Visibility = 'public' | 'home' | 'followers' | 'specified'; @@ -292,88 +333,103 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi return 'public'; } -function renote(viaKeyboard = false) { +function renote() { pleaseLogin(); showMovedDialog(); - let items = [] as MenuItem[]; + if (appearNote.channel) { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { + renoteId: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + renoted.value = true; + }); + } else { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; + const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; + + let visibility = appearNote.visibility; + visibility = smallerVisibility(visibility, configuredVisibility); + if (appearNote.channel?.isSensitive) { + visibility = smallerVisibility(visibility, 'home'); + } + + os.api('notes/create', { + localOnly, + visibility, + renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); + renoted.value = true; + }); + } +} + +function quote() { + pleaseLogin(); + showMovedDialog(); if (appearNote.channel) { - items = items.concat([{ - text: i18n.ts.inChannelRenote, - icon: 'ph-rocket-launch ph-bold ph-lg', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }).then(() => { + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + const el = quoteButton.value as HTMLElement | null | undefined; + if (el && res.length > 0) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - renoted.value = true; - }); - }, - }, { - text: i18n.ts.inChannelQuote, - icon: 'ph-quotes ph-bold ph-lg', - action: () => { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); - }, - }, null]); + quoted.value = res.length > 0; + }); + }); + } else { + os.post({ + renote: appearNote, + }).then(() => { + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + const el = quoteButton.value as HTMLElement | null | undefined; + if (el && res.length > 0) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + quoted.value = res.length > 0; + }); + }); } - - items = items.concat([{ - text: i18n.ts.renote, - icon: 'ph-rocket-launch ph-bold ph-lg', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; - const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - - let visibility = appearNote.visibility; - visibility = smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - visibility = smallerVisibility(visibility, 'home'); - } - - os.api('notes/create', { - localOnly, - visibility, - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - renoted.value = true; - }); - }, - }, { - text: i18n.ts.quote, - icon: 'ph-quotes ph-bold ph-lg', - action: () => { - os.post({ - renote: appearNote, - }); - }, - }]); - - os.popupMenu(items, renoteButton.value, { - viaKeyboard, - }); } function reply(viaKeyboard = false): void { @@ -443,13 +499,20 @@ function undoReact(note): void { } function undoRenote(note) : void { - if (!renoted.value) return; os.api("notes/unrenote", { - noteId: note.id, + noteId: note.id }); renoted.value = false; } +function undoQuote(note) : void { + os.api("notes/unrenote", { + noteId: note.id, + quote: true + }); + quoted.value = false; +} + function onContextmenu(ev: MouseEvent): void { const isLink = (el: HTMLElement) => { if (el.tagName === 'A') return true; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 782128d81..32ea1f3e4 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -120,6 +120,16 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -141,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -161,6 +172,12 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts.loadReplies }} +
+ +
+ @@ -114,8 +124,10 @@ const translation = ref(null); const translating = ref(false); const isDeleted = ref(false); const renoted = ref(false); +const quoted = ref(false); const reactButton = shallowRef(); const renoteButton = shallowRef(); +const quoteButton = shallowRef(); const menuButton = shallowRef(); const likeButton = shallowRef(); @@ -142,6 +154,15 @@ if ($i) { }).then((res) => { renoted.value = res.length > 0; }); + + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + quoted.value = res.length > 0; + }); } function focus() { @@ -223,80 +244,103 @@ function undoRenote() : void { renoted.value = false; } +function undoQuote() : void { + os.api("notes/unrenote", { + noteId: appearNote.id, + quote: true + }); + quoted.value = false; +} + let showContent = $ref(false); let replies: Misskey.entities.Note[] = $ref([]); -function renote(viaKeyboard = false) { +function renote() { pleaseLogin(); showMovedDialog(); - let items = [] as MenuItem[]; + if (appearNote.channel) { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } - if (props.note.channel) { - items = items.concat([{ - text: i18n.ts.inChannelRenote, - icon: 'ph-rocket-launch ph-bold ph-lg', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { + os.api('notes/create', { + renoteId: props.note.id, + channelId: props.note.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + renoted.value = true; + }); + } else { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { + renoteId: props.note.id, + }).then(() => { + os.toast(i18n.ts.renoted); + renoted.value = true; + }); + } +} + +function quote() { + pleaseLogin(); + showMovedDialog(); + + if (appearNote.channel) { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }).then(() => { + os.api("notes/renotes", { + noteId: props.note.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + const el = quoteButton.value as HTMLElement | null | undefined; + if (el && res.length > 0) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { - renoteId: props.note.id, - channelId: props.note.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - renoted.value = true; - }); - }, - }, { - text: i18n.ts.inChannelQuote, - icon: 'ph-quotes ph-bold ph-lg', - action: () => { - os.post({ - renote: props.note, - channel: props.note.channel, - }); - }, - }, null]); + quoted.value = res.length > 0; + }); + }); + } else { + os.post({ + renote: appearNote, + }).then(() => { + os.api("notes/renotes", { + noteId: props.note.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + const el = quoteButton.value as HTMLElement | null | undefined; + if (el && res.length > 0) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + quoted.value = res.length > 0; + }); + }); } - - items = items.concat([{ - text: i18n.ts.renote, - icon: 'ph-rocket-launch ph-bold ph-lg', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - os.api('notes/create', { - renoteId: props.note.id, - }).then(() => { - os.toast(i18n.ts.renoted); - renoted.value = true; - }); - }, - }, { - text: i18n.ts.quote, - icon: 'ph-quotes ph-bold ph-lg', - action: () => { - os.post({ - renote: props.note, - }); - }, - }]); - - os.popupMenu(items, renoteButton.value, { - viaKeyboard, - }); } function menu(viaKeyboard = false): void {