summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkNote.vue2
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue205
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;