diff options
| author | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
|---|---|---|
| committer | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
| commit | 6b554c178b81f13f83a69b19d44b72b282a0c119 (patch) | |
| tree | f5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/frontend/src/components/MkNoteSub.vue | |
| parent | merge: Security fixes (!970) (diff) | |
| parent | bump version for release (diff) | |
| download | sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.gz sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.bz2 sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.zip | |
merge: release 2025.4.2 (!1051)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1051
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/frontend/src/components/MkNoteSub.vue')
| -rw-r--r-- | packages/frontend/src/components/MkNoteSub.vue | 198 |
1 files changed, 95 insertions, 103 deletions
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index b2967a7cf3..282854c6a8 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :class="$style.noteFooterButton" :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" - @mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)" + @click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)" > <i class="ph-rocket-launch ph-bold ph-lg"></i> <p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p> @@ -42,30 +42,36 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" class="_button" :class="$style.noteFooterButton" - @mousedown="quote()" + @click.stop="quote()" > <i class="ph-quotes ph-bold ph-lg"></i> </button> <button v-else class="_button" :class="$style.noteFooterButton" disabled> <i class="ph-prohibit ph-bold ph-lg"></i> </button> - <button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()"> + <button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @click.stop="like()"> <i class="ph-heart ph-bold ph-lg"></i> </button> - <button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> + <button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @click.stop="react()"> <i v-if="note.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> </button> <button v-if="note.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(note)"> <i class="ph-minus ph-bold ph-lg"></i> </button> - <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()"> + <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()"> + <i class="ti ti-paperclip"></i> + </button> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> + <i class="ti ti-language-hiragana"></i> + </button> + <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()"> <i class="ph-dots-three ph-bold ph-lg"></i> </button> </footer> </div> </div> - <template v-if="depth < numberOfReplies"> + <template v-if="depth < prefer.s.numberOfReplies"> <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/> </template> <div v-else :class="$style.more"> @@ -73,44 +79,41 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-else :class="$style.muted" @click="muted = false"> - <I18n :src="i18n.ts.userSaysSomething" tag="small"> - <template #name> - <MkA v-user-preview="note.userId" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - </template> - </I18n> + <SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote> </div> </template> <script lang="ts" setup> -import { computed, ref, shallowRef, watch } from 'vue'; +import { computed, inject, ref, shallowRef, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js'; +import * as config from '@@/js/config.js'; +import type { Ref } from 'vue'; +import type { Visibility } from '@/utility/boost-quote.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import { notePage } from '@/filters/note.js'; import * as os from '@/os.js'; -import * as sound from '@/scripts/sound.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import * as sound from '@/utility/sound.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import { userPage } from '@/filters/user.js'; -import { checkWordMute } from '@/scripts/check-word-mute.js'; -import { defaultStore } from '@/store.js'; -import { host } from '@@/js/config.js'; -import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { checkMutes } from '@/utility/check-word-mute.js'; +import { pleaseLogin } from '@/utility/please-login.js'; +import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { reactionPicker } from '@/scripts/reaction-picker.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { getNoteMenu } from '@/scripts/get-note-menu.js'; -import { useNoteCapture } from '@/scripts/use-note-capture.js'; -import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js'; - -const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); +import { reactionPicker } from '@/utility/reaction-picker.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { getNoteClipMenu, getNoteMenu, translateNote } from '@/utility/get-note-menu.js'; +import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; +import { prefer } from '@/preferences.js'; +import { useNoteCapture } from '@/use/use-note-capture.js'; +import SkMutedNote from '@/components/SkMutedNote.vue'; +import { instance } from '@/instance'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -122,25 +125,27 @@ const props = withDefaults(defineProps<{ depth?: number; }>(), { depth: 1, + onDeleteCallback: undefined, }); +const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id); + const el = shallowRef<HTMLElement>(); -const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false); -const translation = ref<any>(null); +const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null); const translating = ref(false); const isDeleted = ref(false); const renoted = ref(false); -const numberOfReplies = ref(defaultStore.state.numberOfReplies); const reactButton = shallowRef<HTMLElement>(); +const clipButton = useTemplateRef('clipButton'); const renoteButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -const renoteTooltip = computeRenoteTooltip(computed); +const renoteTooltip = computeRenoteTooltip(renoted); -let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); -const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); +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<Misskey.entities.Note[]>([]); const mergedCW = computed(() => computeMergedCw(appearNote.value)); @@ -154,9 +159,11 @@ const isRenote = ( const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: `https://${host}/notes/${appearNote.value.id}`, + url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`, })); +const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); + async function addReplyTo(replyNote: Misskey.entities.Note) { replies.value.unshift(replyNote); appearNote.value.repliesCount += 1; @@ -170,13 +177,15 @@ async function removeReply(id: Misskey.entities.Note['id']) { } } +const { muted } = checkMutes(appearNote.value); + useNoteCapture({ rootEl: el, note: appearNote, isDeletedRef: isDeleted, // only update replies if we are, in fact, showing replies - onReplyCallback: props.detail && props.depth < numberOfReplies.value ? addReplyTo : undefined, - onDeleteCallback: props.detail && props.depth < numberOfReplies.value ? props.onDeleteCallback : undefined, + onReplyCallback: props.detail && props.depth < prefer.s.numberOfReplies ? addReplyTo : undefined, + onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined, }); if ($i) { @@ -190,22 +199,21 @@ if ($i) { } function focus() { - el.value.focus(); + el.value?.focus(); } -function reply(viaKeyboard = false): void { +async function reply(viaKeyboard = false): Promise<void> { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); - os.post({ + await os.post({ reply: props.note, - channel: props.note.channel, + channel: props.note.channel ?? undefined, animation: !viaKeyboard, - }, () => { - focus(); }); + focus(); } -function react(viaKeyboard = false): void { +function react(): void { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); @@ -285,15 +293,15 @@ function undoRenote() : void { } } -let showContent = ref(defaultStore.state.uncollapseCW); +let showContent = ref(prefer.s.uncollapseCW); watch(() => props.expandAllCws, (expandAllCws) => { if (expandAllCws !== showContent.value) showContent.value = expandAllCws; }); function boostVisibility(forceMenu: boolean = false) { - if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) { - renote(defaultStore.state.visibilityOnBoost); + if (!prefer.s.showVisibilitySelectorOnBoost && !forceMenu) { + renote(prefer.s.visibilityOnBoost); } else { os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value); } @@ -347,71 +355,50 @@ function quote() { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); - if (appearNote.value.channel) { - os.post({ - renote: appearNote.value, - channel: appearNote.value.channel, - }).then((cancelled) => { - if (cancelled) return; - misskeyApi('notes/renotes', { - noteId: props.note.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - if (!(res.length > 0)) return; - 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); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } + os.post({ + renote: appearNote.value, + channel: appearNote.value.channel ?? undefined, + }).then((cancelled) => { + if (cancelled) return; + misskeyApi('notes/renotes', { + noteId: props.note.id, + userId: $i?.id, + limit: 1, + quote: true, + }).then((res) => { + if (!(res.length > 0)) return; + const popupEl = quoteButton.value as HTMLElement | null | undefined; + if (popupEl && res.length > 0) { + const rect = popupEl.getBoundingClientRect(); + const x = rect.left + (popupEl.offsetWidth / 2); + const y = rect.top + (popupEl.offsetHeight / 2); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); + } - os.toast(i18n.ts.quoted); - }); + os.toast(i18n.ts.quoted); }); - } else { - os.post({ - renote: appearNote.value, - }).then((cancelled) => { - if (cancelled) return; - misskeyApi('notes/renotes', { - noteId: props.note.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - if (!(res.length > 0)) return; - 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); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } + }); +} - os.toast(i18n.ts.quoted); - }); - }); - } +function menu(): void { + const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, isDeleted }); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); +} + +async function clip(): Promise<void> { + os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } -function menu(viaKeyboard = false): void { - const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, menuButton, isDeleted }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); +async function translate() { + await translateNote(appearNote.value.id, translation, translating); } if (props.detail) { misskeyApi('notes/children', { noteId: props.note.id, - limit: numberOfReplies.value, + limit: prefer.s.numberOfReplies, showQuotes: false, }).then(res => { replies.value = res; @@ -546,5 +533,10 @@ if (props.detail) { border: 1px solid var(--MI_THEME-divider); margin: 8px 8px 0 8px; border-radius: var(--MI-radius-sm); + cursor: pointer; +} + +.muted:hover { + background: var(--MI_THEME-buttonBg); } </style> |