From 2ea7e799fe997ccd090ee54844f7dade79f59e60 Mon Sep 17 00:00:00 2001 From: Mar0xy <marie@kaifa.ch> Date: Sun, 1 Oct 2023 00:51:57 +0200 Subject: [PATCH] upd: add buttons to replies --- packages/frontend/src/components/MkNote.vue | 2 +- .../src/components/MkNoteDetailed.vue | 2 +- .../frontend/src/components/MkNoteSub.vue | 205 +++++++++++++++++- 3 files changed, 206 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index ded378aa28..5e26f0a0e2 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> - <i v-else class="ph-plus ph-bold ph-lg"></i> + <i v-else class="ph-smiley ph-bold ph-lg"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)"> <i class="ph-minus ph-bold ph-lg"></i> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 725464e53b..0ac0a822aa 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -118,7 +118,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> - <i v-else class="ph-plus ph-bold ph-lg"></i> + <i v-else class="ph-smiley ph-bold ph-lg"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)"> <i class="ph-minus ph-bold ph-lg"></i> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 2a3cd9bf02..4283d84b2c 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -19,6 +19,36 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSubNoteContent :class="$style.text" :note="note"/> </div> </div> + <footer> + <MkReactionsViewer ref="reactionsViewer" :note="note"/> + <button class="_button" :class="$style.noteFooterButton" @click="reply()"> + <i class="ph-arrow-u-up-left ph-bold pg-lg"></i> + <p v-if="note.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ note.repliesCount }}</p> + </button> + <button + v-if="canRenote" + ref="renoteButton" + class="_button" + :class="$style.noteFooterButton" + @mousedown="renote()" + > + <i class="ph-repeat ph-bold ph-lg"></i> + <p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p> + </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" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="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()"> + <i class="ph-dots-three ph-bold ph-lg"></i> + </button> + </footer> </div> </div> <template v-if="depth < 5"> @@ -40,9 +70,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { computed, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-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'; @@ -52,6 +83,14 @@ import { $i } from '@/account.js'; import { userPage } from "@/filters/user"; import { checkWordMute } from "@/scripts/check-word-mute"; import { defaultStore } from "@/store"; +import { pleaseLogin } from '@/scripts/please-login.js'; +import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { reactionPicker } from '@/scripts/reaction-picker.js'; +import { claimAchievement } from '@/scripts/achievements.js'; +import type { MenuItem } from '@/types/menu.js'; +import { getNoteMenu } from '@/scripts/get-note-menu.js'; +const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -63,11 +102,150 @@ const props = withDefaults(defineProps<{ depth: 1, }); +function focus() { + el.value.focus(); +} + const muted = ref(checkWordMute(props.note, $i, defaultStore.state.mutedWords)); +const translation = ref(null); +const translating = ref(false); +const isDeleted = ref(false); +const reactButton = shallowRef<HTMLElement>(); +const renoteButton = shallowRef<HTMLElement>(); +const menuButton = shallowRef<HTMLElement>(); + +function reply(viaKeyboard = false): void { + pleaseLogin(); + showMovedDialog(); + os.post({ + reply: props.note, + channel: props.note.channel, + animation: !viaKeyboard, + }, () => { + focus(); + }); +} + +function react(viaKeyboard = false): void { + pleaseLogin(); + showMovedDialog(); + if (props.note.reactionAcceptance === 'likeOnly') { + os.api('notes/reactions/create', { + noteId: props.note.id, + reaction: '❤️', + }); + const el = reactButton.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'); + } + } else { + blur(); + reactionPicker.show(reactButton.value, reaction => { + os.api('notes/reactions/create', { + noteId: props.note.id, + reaction: reaction, + }); + if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } + }, () => { + focus(); + }); + } +} + +function undoReact(note): void { + const oldReaction = note.myReaction; + if (!oldReaction) return; + os.api('notes/reactions/delete', { + noteId: note.id, + }); +} let showContent = $ref(false); let replies: Misskey.entities.Note[] = $ref([]); +function renote(viaKeyboard = false) { + pleaseLogin(); + showMovedDialog(); + + let items = [] as MenuItem[]; + + if (props.note.channel) { + items = items.concat([{ + text: i18n.ts.inChannelRenote, + icon: 'ph-repeat 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, + channelId: props.note.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ph-quotes ph-bold ph-lg', + action: () => { + os.post({ + renote: props.note, + channel: props.note.channel, + }); + }, + }, null]); + } + + items = items.concat([{ + text: i18n.ts.renote, + icon: 'ph-repeat 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); + }); + }, + }, { + 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 { + const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, menuButton, isDeleted }); + os.popupMenu(menu, menuButton.value, { + viaKeyboard, + }).then(focus).finally(cleanup); +} + if (props.detail) { os.api('notes/children', { noteId: props.note.id, @@ -122,6 +300,31 @@ if (props.detail) { margin-bottom: 2px; } +.noteFooterButton { + margin: 0; + padding: 8px; + padding-top: 10px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 14px; + } + + &:hover { + color: var(--fgHighlighted); + } +} + +.noteFooterButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; + + &.reacted { + color: var(--accent); + } +} + .cw { cursor: default; display: block;