Merge pull request 'Add reactions tab to detailed notes view' (#10161) from Freeplay/calckey:notes into develop

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/10161
This commit is contained in:
Kainoa Kanter 2023-05-20 04:57:44 +00:00
commit ca686382b9
6 changed files with 138 additions and 152 deletions

View File

@ -55,6 +55,13 @@
</template> </template>
{{ i18n.ts._notification._types.quote }} {{ i18n.ts._notification._types.quote }}
</option> </option>
<option value="reactions">
<i class="ph-smiley ph-bold ph-lg"></i>
<template v-if="reactionsCount > 0">
<span class="count">{{ reactionsCount }}</span>
</template>
{{ i18n.ts.reaction }}
</option>
<option value="clips"> <option value="clips">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
<template v-if="clips?.length > 0"> <template v-if="clips?.length > 0">
@ -128,6 +135,11 @@
</MkA> </MkA>
</div> </div>
<MkLoading v-else-if="tab === 'clips' && clips.length > 0" /> <MkLoading v-else-if="tab === 'clips' && clips.length > 0" />
<MkReactedUsers
v-if="tab === 'reactions' && reactionsCount > 0"
:note-id="appearNote.id"
></MkReactedUsers>
</div> </div>
<div v-else class="_panel muted" @click="muted.muted = false"> <div v-else class="_panel muted" @click="muted.muted = false">
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small"> <I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
@ -165,6 +177,7 @@ import XStarButton from "@/components/MkStarButton.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MkRenoteButton.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue"; import MkUserCardMini from "@/components/MkUserCardMini.vue";
import MkReactedUsers from "@/components/MkReactedUsers.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import { getWordSoftMute } from "@/scripts/check-word-mute"; import { getWordSoftMute } from "@/scripts/check-word-mute";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
@ -240,6 +253,8 @@ let clips = $ref();
let renotes = $ref(); let renotes = $ref();
let isScrolling; let isScrolling;
const reactionsCount = Object.values(props.note.reactions).reduce((x,y) => x + y, 0);
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
@ -509,14 +524,22 @@ onUnmounted(() => {
} }
} }
> :deep(.chips) { > :deep(.chips), {
padding: 6px 32px 12px; padding-block: 6px 12px;
padding-left: 32px;
&:last-child {
margin-bottom: 12px;
}
} }
> :deep(.user-card-mini) { > :deep(.user-card-mini),
> :deep(.reacted-users > *) {
padding-inline: 32px; padding-inline: 32px;
border-top: 1px solid var(--divider); border-top: 1px solid var(--divider);
border-radius: 0; border-radius: 0;
} }
> :deep(.reacted-users > div) {
padding-block: 12px;
}
> .reply { > .reply {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
@ -612,10 +635,13 @@ onUnmounted(() => {
} }
} }
> .clips, > .clips,
> .chips, > :deep(.user-card-mini),
> :deep(.user-card-mini) { > :deep(.reacted-users > *) {
padding-inline: 16px !important; padding-inline: 16px !important;
} }
> .chips {
padding-left: 16px !important;
}
} }
&.max-width_300px { &.max-width_300px {

View File

@ -137,7 +137,7 @@
class="reply" class="reply"
:class="{ single: replies.length == 1 }" :class="{ single: replies.length == 1 }"
:conversation="conversation" :conversation="conversation"
:depth="replies.lenght == 1 ? depth : depth + 1" :depth="replies.length == 1 ? depth : depth + 1"
:replyLevel="replyLevel + 1" :replyLevel="replyLevel + 1"
:parentId="appearNote.replyId" :parentId="appearNote.replyId"
:detailedView="detailedView" :detailedView="detailedView"

View File

@ -0,0 +1,97 @@
<template>
<div v-if="note" class="_gaps reacted-users">
<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
ref="reactionRef"
:reaction="
reaction
? reaction.replace(
/^:(\w+):$/,
':$1@.:'
)
: reaction
"
:custom-emojis="note.emojis"
/>
<span style="margin-left: 4px">{{
note.reactions[reaction]
}}</span>
</button>
</div>
<MkUserCardMini
v-for="user in users"
:key="user.id"
:user="user"
:with-chart="false"
/>
</div>
<div v-else>
<MkLoading />
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch } from "vue";
import * as misskey from "calckey-js";
import MkReactionIcon from "@/components/MkReactionIcon.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue";
import { i18n } from "@/i18n";
import * as os from "@/os";
const props = defineProps<{
noteId: misskey.entities.Note["id"];
}>();
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>

View File

@ -1,128 +0,0 @@
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.reaction }}</template>
<MkSpacer :margin-min="20" :margin-max="28">
<div v-if="note" class="_gaps">
<div v-if="reactions.length === 0" class="_fullinfo">
<img
src="/static-assets/badges/info.png"
class="_ghost"
alt="Info"
/>
<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
ref="reactionRef"
:reaction="
reaction
? reaction.replace(
/^:(\w+):$/,
':$1@.:'
)
: reaction
"
:custom-emojis="note.emojis"
/>
<span style="margin-left: 4px">{{
note.reactions[reaction]
}}</span>
</button>
</div>
<MkA
v-for="user in users"
:key="user.id"
:to="userPage(user)"
>
<MkUserCardMini :user="user" :with-chart="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 "calckey-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";
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>

View File

@ -89,6 +89,12 @@ export default defineComponent({
padding: 12px 32px; padding: 12px 32px;
font-size: 0.85em; font-size: 0.85em;
overflow-x: auto; overflow-x: auto;
mask: linear-gradient(to right, black calc(100% - 90px), transparent);
-webkit-mask: linear-gradient(to right, black calc(100% - 90px), transparent);
padding-right: 90px !important;
&::-webkit-scrollbar {
display: none;
}
> button { > button {
display: flex; display: flex;
gap: 6px; gap: 6px;
@ -102,6 +108,9 @@ export default defineComponent({
> i { > i {
margin-top: -0.1em; margin-top: -0.1em;
} }
> .count {
margin-right: -.2em;
}
} }
} }

View File

@ -230,19 +230,6 @@ export function getNoteMenu(props: {
}); });
} }
function showReactions(): void {
os.popup(
defineAsyncComponent(
() => import("@/components/MkReactedUsersDialog.vue"),
),
{
noteId: appearNote.id,
},
{},
"closed",
);
}
async function translate(): Promise<void> { async function translate(): Promise<void> {
if (props.translation.value != null) return; if (props.translation.value != null) return;
props.translating.value = true; props.translating.value = true;
@ -282,11 +269,6 @@ export function getNoteMenu(props: {
action: edit, action: edit,
} }
: undefined, : undefined,
{
icon: "ph-smiley ph-bold ph-lg",
text: i18n.ts.reaction,
action: showReactions,
},
{ {
icon: "ph-clipboard-text ph-bold ph-lg", icon: "ph-clipboard-text ph-bold ph-lg",
text: i18n.ts.copyContent, text: i18n.ts.copyContent,