diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index b1ceee3cf7..664d55725f 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -52,6 +52,7 @@ - Feat: Scratchpad에서 Async: 계열 함수나 버튼 콜백 등의 오류에도 대화창을 띄우도록(시험적이라 Play 등에는 미구현) (misskey-dev/misskey#11850) - Feat: 민감한 미디어를 돋보이게 하는 설정 추가 (misskey-dev/misskey#11851) - Feat: 알림에서 답글이 달린 노트의 상위 노트를 표시하지 않도록 하는 설정 추가 +- Feat: 리노트와 인용 버튼을 표시하는 방법을 선택할 수 있음 - Spec: 사용자 정의 이모티콘 라이센스를 여러 항목으로 추가할 수 있도록 (MisskeyIO/misskey#130) - Enhance: 새로운 신고가 있는 경우, 네비게이션 바의 제어판 아이콘과 제어판 페이지의 신고 섹션에 점을 표시 - Enhance: 스크롤 시 요소 표시 기능을 Friendly 이외의 UI에도 대응 diff --git a/locales/en-US.yml b/locales/en-US.yml index 180000f6cc..faf579f5bd 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1223,6 +1223,7 @@ _cherrypick: renameTheButtonInPostFormToNya: "Change the \"Note\" button on the note-posting form to \"Nyan!\"" renameTheButtonInPostFormToNyaDescription: "Outside of the note-posting form, they are still as \"Note\"." showReplyInNotification: "Show parent note of notes with replies in notifications" + renoteQuoteButtonSeparation: "Separate Renote and Quote buttons" _displayHeaderNavBarWhenScroll: all: "Display all" hideHeaderOnly: "Hide header only" diff --git a/locales/index.d.ts b/locales/index.d.ts index 0182556d61..d9e12bb769 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1234,6 +1234,7 @@ export interface Locale { "renameTheButtonInPostFormToNya": string; "renameTheButtonInPostFormToNyaDescription": string; "showReplyInNotification": string; + "renoteQuoteButtonSeparation": string; }; "_displayHeaderNavBarWhenScroll": { "all": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 12ba5f9b69..a4fdb7a3e4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1232,6 +1232,7 @@ _cherrypick: renameTheButtonInPostFormToNya: "ノート作成画面の「ノート」ボタンを「にゃ!」に変更する" renameTheButtonInPostFormToNyaDescription: "にゃあにゃんにゃんにゃんにゃにゃん?" showReplyInNotification: "通知で返信があるノートの親ノートを表示する" + renoteQuoteButtonSeparation: "リノートと引用ボタンを分けて表示する" _displayHeaderNavBarWhenScroll: all: "全て表示" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index deea3a7dc2..5fa16166ee 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1219,6 +1219,7 @@ _cherrypick: renameTheButtonInPostFormToNya: "노트 작성 화면의 \"노트\" 버튼을 \"냥!\"으로 변경" renameTheButtonInPostFormToNyaDescription: "냐앙냥냥냥냐냥?" showReplyInNotification: "알림에서 답글이 달린 노트의 상위 노트 표시하기" + renoteQuoteButtonSeparation: "리노트와 인용 버튼을 분리해서 표시하기" _displayHeaderNavBarWhenScroll: all: "모두 표시" hideHeaderOnly: "헤더만 숨기기" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index dca5eeca05..8699a920da 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -125,7 +125,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-tooltip="i18n.ts.renote" :class="$style.footerButton" class="_button" - @mousedown="renote()" + @mousedown="defaultStore.state.renoteQuoteButtonSeparation ? renoteOnly() : renote()" >

{{ appearNote.renoteCount }}

@@ -143,7 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -216,7 +216,7 @@ const props = withDefaults(defineProps<{ pinned?: boolean; notification?: boolean; }>(), { - notification: false, + notification: false, }); const inChannel = inject('inChannel', null); @@ -275,7 +275,7 @@ const keymap = { }; onMounted(() => { - globalEvents.on('showEl', (showEl_receive) => { + globalEvents.on('showEl', (showEl_receive) => { showEl = showEl_receive; }); }); @@ -315,7 +315,89 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi return 'public'; } -async function renote() { +function renote(viaKeyboard = false) { + pleaseLogin(); + showMovedDialog(); + + let items = [] as MenuItem[]; + + if (appearNote.channel) { + items = items.concat([{ + text: i18n.ts.inChannelRenote, + icon: 'ti ti-repeat', + 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: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + }, + }, null]); + } + + items = items.concat([{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + 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.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + }); + }, + }]); + + os.popupMenu(items, renoteButton.value, { + viaKeyboard, + }); +} + +async function renoteOnly() { pleaseLogin(); showMovedDialog(); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index c1a1cc3bcd..7a7202fd51 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -139,7 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-tooltip="i18n.ts.renote" class="_button" :class="$style.noteFooterButton" - @mousedown="renote()" + @mousedown="defaultStore.state.renoteQuoteButtonSeparation ? renoteOnly() : renote()" >

{{ appearNote.renoteCount }}

@@ -157,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -353,7 +353,78 @@ useTooltip(renoteButton, async (showing) => { }, {}, 'closed'); }); -async function renote() { +function renote(viaKeyboard = false) { + pleaseLogin(); + showMovedDialog(); + + let items = [] as MenuItem[]; + + if (appearNote.channel) { + items = items.concat([{ + text: i18n.ts.inChannelRenote, + icon: 'ti ti-repeat', + 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: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + }, + }, null]); + } + + items = items.concat([{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + 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: appearNote.id, + }).then(() => { + os.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + }); + }, + }]); + + os.popupMenu(items, renoteButton.value, { + viaKeyboard, + }); +} + +async function renoteOnly() { pleaseLogin(); showMovedDialog(); diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index ede99496e3..5ffd1ccefd 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-tooltip="i18n.ts.renote" :class="$style.footerButton" class="_button" - @mousedown="renote()" + @mousedown="defaultStore.state.renoteQuoteButtonSeparation ? renoteOnly() : renote()" >

{{ note.renoteCount }}

@@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -120,6 +120,7 @@ import { deepClone } from '@/scripts/clone.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; +import { MenuItem } from '@/types/menu.js'; const el = shallowRef(); const menuButton = shallowRef(); @@ -192,7 +193,78 @@ useTooltip(renoteButton, async (showing) => { }, {}, 'closed'); }); -async function renote() { +function renote(viaKeyboard = false) { + pleaseLogin(); + showMovedDialog(); + + let items = [] as MenuItem[]; + + if (props.note.channel) { + items = items.concat([{ + text: i18n.ts.inChannelRenote, + icon: 'ti ti-repeat', + 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, + channelId: props.note.channelId, + }).then(() => { + os.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: props.note, + channel: props.note.channel, + }); + }, + }, null]); + } + + items = items.concat([{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + 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.noteToast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: props.note, + }); + }, + }]); + + os.popupMenu(items, renoteButton.value, { + viaKeyboard, + }); +} + +async function renoteOnly() { pleaseLogin(); showMovedDialog(); @@ -200,7 +272,7 @@ async function renote() { const { canceled } = await os.confirm({ type: 'info', text: i18n.ts.renoteConfirm, - caption: i18n.ts.renoteConfirmDescription, + caption: i18n.ts.renoteConfirmDescription, }); if (canceled) return; } diff --git a/packages/frontend/src/pages/settings/cherrypick.vue b/packages/frontend/src/pages/settings/cherrypick.vue index aeb443084f..3fc3fba690 100644 --- a/packages/frontend/src/pages/settings/cherrypick.vue +++ b/packages/frontend/src/pages/settings/cherrypick.vue @@ -59,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._cherrypick.showReplyInNotification }} + {{ i18n.ts._cherrypick.renoteQuoteButtonSeparation }}
@@ -96,12 +97,14 @@ const mobileTimelineHeaderChange = computed(defaultStore.makeGetterSetter('mobil const displayHeaderNavBarWhenScroll = computed(defaultStore.makeGetterSetter('displayHeaderNavBarWhenScroll')); const renameTheButtonInPostFormToNya = computed(defaultStore.makeGetterSetter('renameTheButtonInPostFormToNya')); const showReplyInNotification = computed(defaultStore.makeGetterSetter('showReplyInNotification')); +const renoteQuoteButtonSeparation = computed(defaultStore.makeGetterSetter('renoteQuoteButtonSeparation')); watch([ infoButtonForNoteActionsEnabled, reactableRemoteReactionEnabled, renameTheButtonInPostFormToNya, showReplyInNotification, + renoteQuoteButtonSeparation, ], async () => { await reloadAsk(); }); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 33cf3e9d79..08a3a1aa1e 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -121,6 +121,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'mobileTimelineHeaderChange', 'renameTheButtonInPostFormToNya', 'showReplyInNotification', + 'renoteQuoteButtonSeparation', ]; const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ 'lightTheme', diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 423f77f317..05fe9b2a71 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -521,6 +521,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + renoteQuoteButtonSeparation: { + where: 'device', + default: true, + }, // - etc friendlyEnableNotifications: {