summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkNoteSub.vue
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
committerJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
commit6b554c178b81f13f83a69b19d44b72b282a0c119 (patch)
treef5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/frontend/src/components/MkNoteSub.vue
parentmerge: Security fixes (!970) (diff)
parentbump version for release (diff)
downloadsharkey-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.vue198
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>