summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-09-17 10:33:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-09-17 10:33:33 +0900
commit907d519da36c550dde0a39970057bce22ffdcc5f (patch)
tree72ad0cc084a6b6425b42926bf2c1245205068653 /packages/frontend/src
parentUpdate CHANGELOG.md (diff)
downloadmisskey-907d519da36c550dde0a39970057bce22ffdcc5f.tar.gz
misskey-907d519da36c550dde0a39970057bce22ffdcc5f.tar.bz2
misskey-907d519da36c550dde0a39970057bce22ffdcc5f.zip
enhance(frontend): improve note detail page
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkNote.vue18
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue133
-rw-r--r--packages/frontend/src/components/MkReactedUsersDialog.vue104
-rw-r--r--packages/frontend/src/components/MkRenotedUsersDialog.vue71
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts20
5 files changed, 126 insertions, 220 deletions
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index bedacbce2a..fdf22c5995 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -86,9 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkReactionsViewer :note="appearNote" :maxNumber="16">
<template #more>
- <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
- {{ i18n.ts.more }}
- </button>
+ <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
@@ -457,7 +455,7 @@ function showRenoteMenu(viaKeyboard = false): void {
} else {
os.popupMenu([
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
+ null,
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
@@ -488,12 +486,6 @@ function readPromo() {
});
isDeleted.value = true;
}
-
-function showReactions(): void {
- os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
- noteId: appearNote.id,
- }, {}, 'closed');
-}
</script>
<style lang="scss" module>
@@ -941,7 +933,7 @@ function showReactions(): void {
opacity: 0.7;
}
-.reactionDetailsButton {
+.reactionOmitted {
display: inline-block;
height: 32px;
margin: 2px;
@@ -950,9 +942,5 @@ function showReactions(): void {
border-radius: 4px;
background: transparent;
opacity: .8;
-
- &:hover {
- background: var(--X5);
- }
}
</style>
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index f7578dd390..086667127b 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -11,7 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
v-hotkey="keymap"
:class="$style.root"
>
- <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
+ <div v-if="appearNote.reply.replyId">
+ <div v-if="!conversationLoaded" style="padding: 16px">
+ <MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton>
+ </div>
+ <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
+ </div>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="isRenote" :class="$style.renote">
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@@ -125,7 +130,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</footer>
</article>
- <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
+ <div :class="$style.tabs">
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
+ </div>
+ <div>
+ <div v-if="tab === 'replies'" :class="$style.tab_replies">
+ <div v-if="!repliesLoaded" style="padding: 16px">
+ <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
+ </div>
+ <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
+ </div>
+ <div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
+ <MkPagination :pagination="renotesPagination">
+ <template #default="{ items }">
+ <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
+ <MkUserCardMini :user="item.user" :withChart="false"/>
+ </MkA>
+ </template>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
+ <div :class="$style.reactionTabs">
+ <button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
+ <MkReactionIcon :reaction="reaction"/>
+ <span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
+ </button>
+ </div>
+ <MkPagination :pagination="reactionsPagination">
+ <template #default="{ items }">
+ <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
+ <MkUserCardMini :user="item.user" :withChart="false"/>
+ </MkA>
+ </template>
+ </MkPagination>
+ </div>
+ </div>
</div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
@@ -169,6 +210,10 @@ import { claimAchievement } from '@/scripts/achievements';
import { MenuItem } from '@/types/menu';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog';
+import MkUserCardMini from '@/components/MkUserCardMini.vue';
+import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkReactionIcon from '@/components/MkReactionIcon.vue';
+import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
note: Misskey.entities.Note;
@@ -224,6 +269,26 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
+let tab = $ref('replies');
+let reactionTabType = $ref(null);
+
+const renotesPagination = $computed(() => ({
+ endpoint: 'notes/renotes',
+ limit: 10,
+ params: {
+ noteId: appearNote.id,
+ },
+}));
+
+const reactionsPagination = $computed(() => ({
+ endpoint: 'notes/reactions',
+ limit: 10,
+ params: {
+ noteId: appearNote.id,
+ type: reactionTabType,
+ },
+}));
+
useNoteCapture({
rootEl: el,
note: $$(appearNote),
@@ -426,14 +491,20 @@ function blur() {
el.value.blur();
}
-os.api('notes/children', {
- noteId: appearNote.id,
- limit: 30,
-}).then(res => {
- replies.value = res;
-});
+const repliesLoaded = ref(false);
+function loadReplies() {
+ repliesLoaded.value = true;
+ os.api('notes/children', {
+ noteId: appearNote.id,
+ limit: 30,
+ }).then(res => {
+ replies.value = res;
+ });
+}
-if (appearNote.replyId) {
+const conversationLoaded = ref(false);
+function loadConversation() {
+ conversationLoaded.value = true;
os.api('notes/conversation', {
noteId: appearNote.replyId,
}).then(res => {
@@ -640,10 +711,52 @@ if (appearNote.replyId) {
}
}
-.reply {
+.reply:not(:first-child) {
border-top: solid 0.5px var(--divider);
}
+.tabs {
+ border-top: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--divider);
+ display: flex;
+}
+
+.tab {
+ flex: 1;
+ padding: 12px 8px;
+ border-top: solid 2px transparent;
+ border-bottom: solid 2px transparent;
+}
+
+.tabActive {
+ border-bottom: solid 2px var(--accent);
+}
+
+.tab_renotes {
+ padding: 16px;
+}
+
+.tab_reactions {
+ padding: 16px;
+}
+
+.reactionTabs {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 8px;
+}
+
+.reactionTab {
+ padding: 4px 6px;
+ border: solid 1px var(--divider);
+ border-radius: 6px;
+}
+
+.reactionTabActive {
+ border-color: var(--accent);
+}
+
@container (max-width: 500px) {
.root {
font-size: 0.9em;
diff --git a/packages/frontend/src/components/MkReactedUsersDialog.vue b/packages/frontend/src/components/MkReactedUsersDialog.vue
deleted file mode 100644
index b5f3a634a3..0000000000
--- a/packages/frontend/src/components/MkReactedUsersDialog.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkModalWindow
- ref="dialog"
- :width="400"
- :height="450"
- @close="dialog.close()"
- @closed="emit('closed')"
->
- <template #header>{{ i18n.ts.reactionsList }}</template>
-
- <MkSpacer :marginMin="20" :marginMax="28">
- <div v-if="note" class="_gaps">
- <div v-if="reactions.length === 0" class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.nothing }}</div>
- </div>
- <template v-else>
- <div :class="$style.tabs">
- <button v-for="reaction in reactions" :key="reaction" :class="[$style.tab, { [$style.tabActive]: tab === reaction }]" class="_button" @click="tab = reaction">
- <MkReactionIcon :reaction="reaction"/>
- <span style="margin-left: 4px;">{{ note.reactions[reaction] }}</span>
- </button>
- </div>
- <MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
- <MkUserCardMini :user="user" :withChart="false"/>
- </MkA>
- </template>
- </div>
- <div v-else>
- <MkLoading/>
- </div>
- </MkSpacer>
-</MkModalWindow>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, watch } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkModalWindow from '@/components/MkModalWindow.vue';
-import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import { userPage } from '@/filters/user';
-import { i18n } from '@/i18n';
-import * as os from '@/os';
-import { infoImageUrl } from '@/instance';
-
-const emit = defineEmits<{
- (ev: 'closed'): void,
-}>();
-
-const props = defineProps<{
- noteId: Misskey.entities.Note['id'];
-}>();
-
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-
-let note = $ref<Misskey.entities.Note>();
-let tab = $ref<string>();
-let reactions = $ref<string[]>();
-let users = $ref();
-
-watch($$(tab), async () => {
- const res = await os.api('notes/reactions', {
- noteId: props.noteId,
- type: tab,
- limit: 30,
- });
-
- users = res.map(x => x.user);
-});
-
-onMounted(() => {
- os.api('notes/show', {
- noteId: props.noteId,
- }).then((res) => {
- reactions = Object.keys(res.reactions);
- tab = reactions[0];
- note = res;
- });
-});
-</script>
-
-<style lang="scss" module>
-.tabs {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.tab {
- padding: 4px 6px;
- border: solid 1px var(--divider);
- border-radius: 6px;
-}
-
-.tabActive {
- border-color: var(--accent);
-}
-</style>
diff --git a/packages/frontend/src/components/MkRenotedUsersDialog.vue b/packages/frontend/src/components/MkRenotedUsersDialog.vue
deleted file mode 100644
index 5e6784bb9c..0000000000
--- a/packages/frontend/src/components/MkRenotedUsersDialog.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkModalWindow
- ref="dialog"
- :width="400"
- :height="450"
- @close="dialog.close()"
- @closed="emit('closed')"
->
- <template #header>{{ i18n.ts.renotesList }}</template>
-
- <MkSpacer :marginMin="20" :marginMax="28">
- <div v-if="renotes" class="_gaps">
- <div v-if="renotes.length === 0" class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.nothing }}</div>
- </div>
- <template v-else>
- <MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
- <MkUserCardMini :user="user" :withChart="false"/>
- </MkA>
- </template>
- </div>
- <div v-else>
- <MkLoading/>
- </div>
- </MkSpacer>
-</MkModalWindow>
-</template>
-
-<script lang="ts" setup>
-import { onMounted } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkModalWindow from '@/components/MkModalWindow.vue';
-import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import { userPage } from '@/filters/user';
-import { i18n } from '@/i18n';
-import * as os from '@/os';
-import { infoImageUrl } from '@/instance';
-
-const emit = defineEmits<{
- (ev: 'closed'): void,
-}>();
-
-const props = defineProps<{
- noteId: Misskey.entities.Note['id'];
-}>();
-
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-
-let note = $ref<Misskey.entities.Note>();
-let renotes = $ref();
-let users = $ref();
-
-onMounted(async () => {
- const res = await os.api('notes/renotes', {
- noteId: props.noteId,
- limit: 30,
- });
-
- renotes = res;
- users = res.map(x => x.user);
-});
-</script>
-
-<style lang="scss" module>
-</style>
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 5bda993fff..d9fae946d3 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -238,18 +238,6 @@ export function getNoteMenu(props: {
os.pageWindow(`/notes/${appearNote.id}`);
}
- function showReactions(): void {
- os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
- noteId: appearNote.id,
- }, {}, 'closed');
- }
-
- function showRenotes(): void {
- os.popup(defineAsyncComponent(() => import('@/components/MkRenotedUsersDialog.vue')), {
- noteId: appearNote.id,
- }, {}, 'closed');
- }
-
async function translate(): Promise<void> {
if (props.translation.value != null) return;
props.translating.value = true;
@@ -280,14 +268,6 @@ export function getNoteMenu(props: {
text: i18n.ts.details,
action: openDetail,
}, {
- icon: 'ti ti-repeat',
- text: i18n.ts.renotesList,
- action: showRenotes,
- }, {
- icon: 'ti ti-icons',
- text: i18n.ts.reactionsList,
- action: showReactions,
- }, {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,