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;