Note tabs + Jump to Reply changes + other stuff (#10157)
This commit is contained in:
commit
722489821c
12 changed files with 348 additions and 248 deletions
|
@ -57,8 +57,11 @@ sendMessage: "Send a message"
|
|||
copyUsername: "Copy username"
|
||||
searchUser: "Search for a user"
|
||||
reply: "Reply"
|
||||
jumpToReply: "Jump to Reply"
|
||||
loadMore: "Load more"
|
||||
showMore: "Show more"
|
||||
newer: "newer"
|
||||
older: "older"
|
||||
showLess: "Close"
|
||||
youGotNewFollower: "followed you"
|
||||
receiveFollowRequest: "Follow request received"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<button
|
||||
v-if="!link"
|
||||
class="bghgjjyj _button"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full }"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, mini }"
|
||||
:type="type"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
|
@ -15,7 +15,7 @@
|
|||
<MkA
|
||||
v-else
|
||||
class="bghgjjyj _button"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full }"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, mini }"
|
||||
:to="to"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
|
@ -41,6 +41,7 @@ const props = defineProps<{
|
|||
wait?: boolean;
|
||||
danger?: boolean;
|
||||
full?: boolean;
|
||||
mini: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -190,6 +191,12 @@ function onMousedown(evt: MouseEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
&.mini {
|
||||
padding: 4px 8px;
|
||||
font-size: .9em;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
v-size="{ max: [500, 450, 350, 300] }"
|
||||
class="tkcbzcuz"
|
||||
class="tkcbzcuz note-container"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
>
|
||||
|
@ -104,6 +104,11 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailedView" class="info">
|
||||
<MkA class="created-at" :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
||||
</MkA>
|
||||
<MkA
|
||||
v-if="appearNote.channel && !inChannel"
|
||||
class="channel"
|
||||
|
@ -113,11 +118,6 @@
|
|||
{{ appearNote.channel.name }}</MkA
|
||||
>
|
||||
</div>
|
||||
<div v-if="detailedView" class="info">
|
||||
<MkA class="created-at" :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
||||
</MkA>
|
||||
</div>
|
||||
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
||||
<XReactionsViewer
|
||||
v-if="enableEmojiReactions"
|
||||
|
@ -130,7 +130,7 @@
|
|||
@click="reply()"
|
||||
>
|
||||
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||
<template v-if="appearNote.repliesCount > 0">
|
||||
<template v-if="appearNote.repliesCount > 0 && !detailedView">
|
||||
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||
</template>
|
||||
</button>
|
||||
|
@ -139,6 +139,7 @@
|
|||
class="button"
|
||||
:note="appearNote"
|
||||
:count="appearNote.renoteCount"
|
||||
:detailedView="detailedView"
|
||||
/>
|
||||
<XStarButtonNoEmoji
|
||||
v-if="!enableEmojiReactions"
|
||||
|
@ -450,6 +451,10 @@ function focusAfter() {
|
|||
focusNext(el.value);
|
||||
}
|
||||
|
||||
function scrollIntoView() {
|
||||
el.value.scrollIntoView();
|
||||
}
|
||||
|
||||
function noteClick(e) {
|
||||
if (document.getSelection().type === "Range" || props.detailedView) {
|
||||
e.stopPropagation();
|
||||
|
@ -464,6 +469,12 @@ function readPromo() {
|
|||
});
|
||||
isDeleted.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
blur,
|
||||
scrollIntoView,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -656,14 +667,13 @@ function readPromo() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .channel {
|
||||
opacity: 0.7;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
> .info {
|
||||
margin-block: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: .7em;
|
||||
margin-top: 16px;
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
|
|
@ -15,32 +15,128 @@
|
|||
:key="note.id"
|
||||
class="reply-to"
|
||||
:note="note"
|
||||
:detailedView="true"
|
||||
/>
|
||||
<MkLoading v-else-if="appearNote.reply" mini />
|
||||
<MkNoteSub
|
||||
v-if="appearNote.reply"
|
||||
:note="appearNote.reply"
|
||||
class="reply-to"
|
||||
:detailedView="true"
|
||||
/>
|
||||
|
||||
<div ref="noteEl" class="article" tabindex="-1">
|
||||
<MkNote
|
||||
@contextmenu.stop="onContextmenu"
|
||||
tabindex="-1"
|
||||
:note="appearNote"
|
||||
:detailedView="true"
|
||||
></MkNote>
|
||||
</div>
|
||||
<MkNote
|
||||
ref="noteEl"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
tabindex="-1"
|
||||
:note="appearNote"
|
||||
detailedView
|
||||
></MkNote>
|
||||
|
||||
<MkTab
|
||||
v-model="tab"
|
||||
:style="'chips'"
|
||||
@update:modelValue="loadTab"
|
||||
>
|
||||
<option value="replies">
|
||||
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||
<template v-if="appearNote.repliesCount > 0">
|
||||
<span class="count">{{ appearNote.repliesCount }}</span>
|
||||
</template>
|
||||
{{ i18n.ts._notification._types.reply }}
|
||||
</option>
|
||||
<option value="renotes">
|
||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||
<template v-if="appearNote.renoteCount > 0">
|
||||
<span class="count">{{ appearNote.renoteCount }}</span>
|
||||
</template>
|
||||
{{ i18n.ts._notification._types.renote }}
|
||||
</option>
|
||||
<option value="quotes">
|
||||
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||
<template v-if="directQuotes?.length > 0">
|
||||
<span class="count">{{ directQuotes.length }}</span>
|
||||
</template>
|
||||
{{ i18n.ts._notification._types.quote }}
|
||||
</option>
|
||||
<option value="clips">
|
||||
<i class="ph-paperclip ph-bold ph-lg"></i>
|
||||
<template v-if="clips?.length > 0">
|
||||
<span class="count">{{ clips.length }}</span>
|
||||
</template>
|
||||
{{ i18n.ts.clips }}
|
||||
</option>
|
||||
</MkTab>
|
||||
|
||||
<MkNoteSub
|
||||
v-if="directReplies"
|
||||
v-if="directReplies && tab === 'replies'"
|
||||
v-for="note in directReplies"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
class="reply"
|
||||
:conversation="replies"
|
||||
:detailedView="true"
|
||||
/>
|
||||
<MkLoading v-else-if="appearNote.repliesCount > 0" />
|
||||
<MkLoading v-else-if="tab === 'replies' && appearNote.repliesCount > 0" />
|
||||
|
||||
<MkNoteSub
|
||||
v-if="directQuotes && tab === 'quotes'"
|
||||
v-for="note in directQuotes"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
class="reply"
|
||||
:conversation="directQuotes"
|
||||
:detailedView="true"
|
||||
/>
|
||||
<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" />
|
||||
|
||||
<!-- <MkPagination
|
||||
v-if="tab === 'renotes'"
|
||||
v-slot="{ items }"
|
||||
ref="pagingComponent"
|
||||
:pagination="pagination"
|
||||
> -->
|
||||
<MkUserCardMini
|
||||
v-if="tab === 'renotes' && renotes"
|
||||
v-for="item in renotes"
|
||||
:key="item.user.id"
|
||||
:user="item.user"
|
||||
:with-chart="false"
|
||||
/>
|
||||
<!-- </MkPagination> -->
|
||||
<MkLoading v-else-if="tab === 'renotes' && appearNote.renoteCount > 0" />
|
||||
|
||||
<div
|
||||
v-if="tab === 'clips' && clips.length > 0"
|
||||
class="_content clips"
|
||||
>
|
||||
<MkA
|
||||
v-for="item in clips"
|
||||
:key="item.id"
|
||||
:to="`/clips/${item.id}`"
|
||||
class="item _panel"
|
||||
>
|
||||
<b>{{ item.name }}</b>
|
||||
<div
|
||||
v-if="item.description"
|
||||
class="description"
|
||||
>
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar
|
||||
:user="item.user"
|
||||
class="avatar"
|
||||
:show-indicator="true"
|
||||
/>
|
||||
<MkUserName
|
||||
:user="item.user"
|
||||
:nowrap="false"
|
||||
/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkLoading v-else-if="tab === 'clips' && clips.length > 0" />
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted.muted = false">
|
||||
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
|
||||
|
@ -70,15 +166,17 @@ import {
|
|||
reactive,
|
||||
ref,
|
||||
} from "vue";
|
||||
import type * as misskey from "calckey-js";
|
||||
import * as misskey from "calckey-js";
|
||||
import MkTab from "@/components/MkTab.vue";
|
||||
import MkNote from "@/components/MkNote.vue";
|
||||
import MkNoteSub from "@/components/MkNoteSub.vue";
|
||||
import XStarButton from "@/components/MkStarButton.vue";
|
||||
import XRenoteButton from "@/components/MkRenoteButton.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import MkUserCardMini from "@/components/MkUserCardMini.vue";
|
||||
import { pleaseLogin } from "@/scripts/please-login";
|
||||
import { getWordSoftMute } from "@/scripts/check-word-mute";
|
||||
import { userPage } from "@/filters/user";
|
||||
import { useRouter } from "@/router";
|
||||
import * as os from "@/os";
|
||||
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||
|
@ -89,12 +187,15 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
|
|||
import { deepClone } from "@/scripts/clone";
|
||||
import { stream } from "@/stream";
|
||||
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
|
||||
import appear from "@/directives/appear";
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
}>();
|
||||
|
||||
let tab = $ref("replies");
|
||||
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
const softMuteReasonI18nSrc = (what?: string) => {
|
||||
|
@ -143,6 +244,9 @@ const translating = ref(false);
|
|||
let conversation = $ref<null | misskey.entities.Note[]>([]);
|
||||
const replies = ref<misskey.entities.Note[]>([]);
|
||||
let directReplies = $ref<null | misskey.entities.Note[]>([]);
|
||||
let directQuotes = $ref<null | misskey.entities.Note[]>([]);
|
||||
let clips = $ref();
|
||||
let renotes = $ref();
|
||||
let isScrolling;
|
||||
|
||||
const keymap = {
|
||||
|
@ -256,10 +360,14 @@ os.api("notes/children", {
|
|||
directReplies = res
|
||||
.filter(
|
||||
(note) =>
|
||||
note.replyId === appearNote.id ||
|
||||
note.renoteId === appearNote.id
|
||||
note.replyId === appearNote.id
|
||||
)
|
||||
.reverse();
|
||||
directQuotes = res
|
||||
.filter(
|
||||
(note) =>
|
||||
note.renoteId === appearNote.id
|
||||
);
|
||||
});
|
||||
|
||||
conversation = null;
|
||||
|
@ -273,6 +381,33 @@ if (appearNote.replyId) {
|
|||
});
|
||||
}
|
||||
|
||||
clips = null;
|
||||
os.api("notes/clips", {
|
||||
noteId: appearNote.id,
|
||||
}).then((res) => {
|
||||
clips = res;
|
||||
});
|
||||
|
||||
// const pagination = {
|
||||
// endpoint: "notes/renotes",
|
||||
// noteId: appearNote.id,
|
||||
// limit: 10,
|
||||
// };
|
||||
|
||||
// const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
renotes = null;
|
||||
function loadTab() {
|
||||
if (tab === "renotes" && !renotes) {
|
||||
os.api("notes/renotes", {
|
||||
noteId: appearNote.id,
|
||||
limit: 100,
|
||||
}).then((res) => {
|
||||
renotes = res;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onNoteUpdated(noteData: NoteUpdatedEvent): Promise<void> {
|
||||
const { type, id, body } = noteData;
|
||||
|
||||
|
@ -323,12 +458,15 @@ document.addEventListener("wheel", () => {
|
|||
onMounted(() => {
|
||||
stream.on("noteUpdated", onNoteUpdated);
|
||||
isScrolling = false;
|
||||
noteEl?.scrollIntoView();
|
||||
noteEl.scrollIntoView();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
if (!isScrolling) {
|
||||
noteEl?.scrollIntoView();
|
||||
noteEl.scrollIntoView();
|
||||
if (location.hash) {
|
||||
location.replace(location.hash); // Jump to highlighted reply
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -366,80 +504,36 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
&:hover > .article > .main > .footer > .button {
|
||||
opacity: 1;
|
||||
}
|
||||
> .reply-to {
|
||||
margin-bottom: -16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
> .renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
line-height: 28px;
|
||||
white-space: pre;
|
||||
color: var(--renote);
|
||||
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 8px 0 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .time {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
|
||||
> .dropdownIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .renote + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
padding-block: 28px 6px;
|
||||
> :deep(.note-container) {
|
||||
padding-block: 28px 0;
|
||||
padding-top: 12px;
|
||||
font-size: 1.1rem;
|
||||
overflow: clip;
|
||||
outline: none;
|
||||
scroll-margin-top: calc(var(--stickyTop) + 20vh);
|
||||
:deep(.article) {
|
||||
.article {
|
||||
cursor: unset;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&:first-of-type {
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
> :deep(.chips) {
|
||||
padding: 6px 32px 12px;
|
||||
}
|
||||
> :deep(.user-card-mini) {
|
||||
padding-inline: 32px;
|
||||
border-top: 1px solid var(--divider);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
> .reply {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
cursor: pointer;
|
||||
|
@ -510,6 +604,14 @@ onUnmounted(() => {
|
|||
// }
|
||||
// }
|
||||
}
|
||||
:deep(.reply:target > .main),
|
||||
:deep(.reply-to:target) {
|
||||
z-index: 2;
|
||||
&::before {
|
||||
outline: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
font-size: 0.9em;
|
||||
|
@ -518,46 +620,20 @@ onUnmounted(() => {
|
|||
> .reply-to:first-child {
|
||||
padding-top: 14px;
|
||||
}
|
||||
> .renote {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
> :deep(.note-container) {
|
||||
padding: 6px 0 0 0;
|
||||
> .header > .body {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_350px {
|
||||
> .article {
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> .clips, > .chips, > :deep(.user-card-mini) {
|
||||
padding-inline: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_300px {
|
||||
font-size: 0.825em;
|
||||
|
||||
> .article {
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -566,4 +642,36 @@ onUnmounted(() => {
|
|||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.clips { // want to redesign at some point
|
||||
padding: 24px 32px;
|
||||
padding-top: 0;
|
||||
> .item {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
// background: var(--buttonBg);
|
||||
border: 1px solid var(--divider);
|
||||
margin-bottom: var(--margin);
|
||||
transition: background .2s;
|
||||
&:hover, &:focus-within {
|
||||
background: var(--panelHighlight);
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
> .user {
|
||||
$height: 32px;
|
||||
padding-top: 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
line-height: $height;
|
||||
|
||||
> .avatar {
|
||||
width: $height;
|
||||
height: $height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
ref="el"
|
||||
v-size="{ max: [450, 500] }"
|
||||
class="wrpstxzv"
|
||||
:id="detailedView ? appearNote.id : null"
|
||||
tabindex="-1"
|
||||
:class="{
|
||||
children: depth > 1,
|
||||
singleStart: replies.length == 1,
|
||||
|
@ -127,32 +129,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<template v-if="conversation">
|
||||
<template v-if="replyLevel < 11 && depth < 5">
|
||||
<template v-if="replies.length == 1">
|
||||
<MkNoteSub
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:note="reply"
|
||||
class="reply single"
|
||||
:conversation="conversation"
|
||||
:depth="depth"
|
||||
:replyLevel="replyLevel + 1"
|
||||
:parentId="appearNote.replyId"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MkNoteSub
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:note="reply"
|
||||
class="reply"
|
||||
:conversation="conversation"
|
||||
:depth="depth + 1"
|
||||
:replyLevel="replyLevel + 1"
|
||||
:parentId="appearNote.replyId"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<MkNoteSub
|
||||
v-if="replyLevel < 11 && depth < 5"
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:note="reply"
|
||||
class="reply"
|
||||
:class="{single: replies.length == 1}"
|
||||
:conversation="conversation"
|
||||
:depth="replies.lenght == 1 ? depth : depth + 1"
|
||||
:replyLevel="replyLevel + 1"
|
||||
:parentId="appearNote.replyId"
|
||||
:detailedView="detailedView"
|
||||
/>
|
||||
<div v-else-if="replies.length > 0" class="more">
|
||||
<div class="line"></div>
|
||||
<MkA class="text _link" :to="notePage(note)"
|
||||
|
@ -212,6 +201,7 @@ const props = withDefaults(
|
|||
note: misskey.entities.Note;
|
||||
conversation?: misskey.entities.Note[];
|
||||
parentId?;
|
||||
detailedView?;
|
||||
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
depth?: number;
|
||||
|
@ -348,6 +338,7 @@ function noteClick(e) {
|
|||
<style lang="scss" scoped>
|
||||
.wrpstxzv {
|
||||
padding: 16px 32px;
|
||||
outline: none;
|
||||
&.children {
|
||||
padding: 10px 0 0 var(--indent);
|
||||
padding-left: var(--indent) !important;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@click="renote(false, $event)"
|
||||
>
|
||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||
<p v-if="count > 0" class="count">{{ count }}</p>
|
||||
<p v-if="count > 0 && !detailedView" class="count">{{ count }}</p>
|
||||
</button>
|
||||
<button v-else class="eddddedb _button">
|
||||
<i class="ph-prohibit ph-bold ph-lg"></i>
|
||||
|
@ -30,6 +30,7 @@ import { MenuItem } from "@/types/menu";
|
|||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
count: number;
|
||||
detailedView?;
|
||||
}>();
|
||||
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<p v-if="note.cw != null" class="cw">
|
||||
<MkA
|
||||
v-if="!detailed && note.replyId"
|
||||
:to="`/notes/${note.replyId}`"
|
||||
:to="`#${note.replyId}`"
|
||||
behavior="browser"
|
||||
class="reply-icon"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -16,6 +17,7 @@
|
|||
!note.replyId
|
||||
"
|
||||
:to="`/notes/${note.renoteId}`"
|
||||
v-tooltip="i18n.ts.jumpToReply"
|
||||
class="reply-icon"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -66,7 +68,9 @@
|
|||
<template v-if="!note.cw">
|
||||
<MkA
|
||||
v-if="!detailed && note.replyId"
|
||||
:to="`/notes/${note.replyId}`"
|
||||
:to="`#${note.replyId}`"
|
||||
behavior="browser"
|
||||
v-tooltip="i18n.ts.jumpToReply"
|
||||
class="reply-icon"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -135,6 +139,7 @@
|
|||
<MkButton
|
||||
v-if="hasMfm && defaultStore.state.animatedMfm"
|
||||
@click.stop="toggleMfm"
|
||||
:mini="true"
|
||||
>
|
||||
<template v-if="disableMfm">
|
||||
<i class="ph-play ph-bold"></i> {{ i18n.ts._mfm.play }}
|
||||
|
|
|
@ -6,6 +6,9 @@ export default defineComponent({
|
|||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
style: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const options = this.$slots.default();
|
||||
|
@ -13,22 +16,21 @@ export default defineComponent({
|
|||
return h(
|
||||
"div",
|
||||
{
|
||||
class: "pxhvhrfw",
|
||||
class: [
|
||||
"pxhvhrfw",
|
||||
{ chips: this.style === "chips" },
|
||||
],
|
||||
role: "tablist",
|
||||
},
|
||||
options.map((option) =>
|
||||
withDirectives(
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
class: [
|
||||
"_button",
|
||||
{
|
||||
active:
|
||||
this.modelValue === option.props?.value,
|
||||
},
|
||||
],
|
||||
class: "_button",
|
||||
role: "tab",
|
||||
key: option.key,
|
||||
disabled: this.modelValue === option.props?.value,
|
||||
'aria-selected': this.modelValue === option.props?.value ? "true" : "false",
|
||||
onClick: () => {
|
||||
this.$emit(
|
||||
"update:modelValue",
|
||||
|
@ -64,12 +66,12 @@ export default defineComponent({
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&[aria-selected="true"] {
|
||||
color: var(--accent);
|
||||
background: var(--accentedBg);
|
||||
background: var(--accentedBg) !important;
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
&:not([aria-selected="true"]):hover {
|
||||
color: var(--fgHighlighted);
|
||||
background: var(--panelHighlight);
|
||||
}
|
||||
|
@ -83,6 +85,26 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
&.chips {
|
||||
padding: 12px 32px;
|
||||
font-size: .85em;
|
||||
overflow-x: auto;
|
||||
> button {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex: unset;
|
||||
margin: 0;
|
||||
margin-right: 8px;
|
||||
padding: .5em 1em;
|
||||
border-radius: 100px;
|
||||
background: var(--buttonBg);
|
||||
> i {
|
||||
margin-top: -.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
font-size: 80%;
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
<MkA
|
||||
class="user-card-mini"
|
||||
:class="[
|
||||
$style.root,
|
||||
{ yellow: user.isSilenced, red: user.isSuspended, gray: false },
|
||||
]"
|
||||
:to="userPage(user)"
|
||||
>
|
||||
<MkAvatar
|
||||
class="avatar"
|
||||
|
@ -18,30 +20,37 @@
|
|||
>
|
||||
</div>
|
||||
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues" />
|
||||
</div>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from "calckey-js";
|
||||
import MkMiniChart from "@/components/MkMiniChart.vue";
|
||||
import * as os from "@/os";
|
||||
import { acct } from "@/filters/user";
|
||||
import { acct, userPage } from "@/filters/user";
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User;
|
||||
}>();
|
||||
const props = withDefaults(defineProps<{
|
||||
user: misskey.entities.User;
|
||||
withChart?: boolean;
|
||||
}>(),
|
||||
{
|
||||
withChart: true,
|
||||
}
|
||||
);
|
||||
|
||||
let chartValues = $ref<number[] | null>(null);
|
||||
|
||||
os.apiGet("charts/user/notes", {
|
||||
userId: props.user.id,
|
||||
limit: 16 + 1,
|
||||
span: "day",
|
||||
}).then((res) => {
|
||||
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
|
||||
res.inc.splice(0, 1);
|
||||
chartValues = res.inc;
|
||||
});
|
||||
if (props.withChart) {
|
||||
os.apiGet("charts/user/notes", {
|
||||
userId: props.user.id,
|
||||
limit: 16 + 1,
|
||||
span: "day",
|
||||
}).then((res) => {
|
||||
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
|
||||
res.inc.splice(0, 1);
|
||||
chartValues = res.inc;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -54,6 +63,7 @@ os.apiGet("charts/user/notes", {
|
|||
padding: 16px;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
transition: background .2s;
|
||||
|
||||
> :global(.avatar) {
|
||||
display: block;
|
||||
|
@ -94,6 +104,10 @@ os.apiGet("charts/user/notes", {
|
|||
height: 30px;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: var(--panelHighlight);
|
||||
}
|
||||
|
||||
&:global(.yellow) {
|
||||
--c: rgb(255 196 0 / 15%);
|
||||
background-image: linear-gradient(
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
<a
|
||||
:href="to"
|
||||
:class="active ? activeClass : null"
|
||||
@click="nav"
|
||||
@contextmenu.prevent.stop="onContextmenu"
|
||||
@click.stop
|
||||
@click.stop="nav"
|
||||
>
|
||||
<slot></slot>
|
||||
</a>
|
||||
|
@ -99,13 +98,9 @@ function popout() {
|
|||
}
|
||||
|
||||
function nav(ev: MouseEvent) {
|
||||
if (!ev.ctrlKey) {
|
||||
ev.preventDefault();
|
||||
if (!ev.ctrlKey && props.behavior !== "browser") {
|
||||
|
||||
if (props.behavior === "browser") {
|
||||
location.href = props.to;
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
if (props.behavior) {
|
||||
if (props.behavior === "window") {
|
||||
|
|
|
@ -139,6 +139,7 @@ const props = defineProps<{
|
|||
thin?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
displayBackButton?: boolean;
|
||||
to?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -193,7 +194,11 @@ const preventDrag = (ev: TouchEvent) => {
|
|||
};
|
||||
|
||||
const onClick = () => {
|
||||
scrollToTop(el, { behavior: "smooth" });
|
||||
if (props.to) {
|
||||
location.href = props.to;
|
||||
} else {
|
||||
scrollToTop(el, { behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-back-button="true"
|
||||
:to="`#${noteId}`"
|
||||
/></template>
|
||||
<MkSpacer :content-max="800" :marginMin="6">
|
||||
<div class="fcuexfpr">
|
||||
|
@ -26,6 +27,7 @@
|
|||
v-if="!showNext && hasNext"
|
||||
class="load next"
|
||||
@click="showNext = true"
|
||||
v-tooltip="`${i18n.ts.loadMore} (${i18n.ts.newer})`"
|
||||
><i class="ph-caret-up ph-bold ph-lg"></i
|
||||
></MkButton>
|
||||
<div class="note _gap">
|
||||
|
@ -39,41 +41,11 @@
|
|||
class="note"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="clips && clips.length > 0"
|
||||
class="_content clips _gap"
|
||||
>
|
||||
<div class="title">{{ i18n.ts.clip }}</div>
|
||||
<MkA
|
||||
v-for="item in clips"
|
||||
:key="item.id"
|
||||
:to="`/clips/${item.id}`"
|
||||
class="item _panel _gap"
|
||||
>
|
||||
<b>{{ item.name }}</b>
|
||||
<div
|
||||
v-if="item.description"
|
||||
class="description"
|
||||
>
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar
|
||||
:user="item.user"
|
||||
class="avatar"
|
||||
:show-indicator="true"
|
||||
/>
|
||||
<MkUserName
|
||||
:user="item.user"
|
||||
:nowrap="false"
|
||||
/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkButton
|
||||
v-if="!showPrev && hasPrev"
|
||||
class="load prev"
|
||||
@click="showPrev = true"
|
||||
v-tooltip="`${i18n.ts.loadMore} (${i18n.ts.older})`"
|
||||
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||
></MkButton>
|
||||
</div>
|
||||
|
@ -111,7 +83,6 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
let note = $ref<null | misskey.entities.Note>();
|
||||
let clips = $ref();
|
||||
let hasPrev = $ref(false);
|
||||
let hasNext = $ref(false);
|
||||
let showPrev = $ref(false);
|
||||
|
@ -157,9 +128,6 @@ function fetchNote() {
|
|||
.then((res) => {
|
||||
note = res;
|
||||
Promise.all([
|
||||
os.api("notes/clips", {
|
||||
noteId: note.id,
|
||||
}),
|
||||
os.api("users/notes", {
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
|
@ -170,8 +138,7 @@ function fetchNote() {
|
|||
sinceId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
]).then(([_clips, prev, next]) => {
|
||||
clips = _clips;
|
||||
]).then(([prev, next]) => {
|
||||
hasPrev = prev.length !== 0;
|
||||
hasNext = next.length !== 0;
|
||||
});
|
||||
|
@ -193,7 +160,7 @@ definePageMetadata(
|
|||
computed(() =>
|
||||
note
|
||||
? {
|
||||
title: i18n.ts.note,
|
||||
title: i18n.t("noteOf", { user: note.user.name }),
|
||||
subtitle: new Date(note.createdAt).toLocaleString(),
|
||||
avatar: note.user,
|
||||
path: `/notes/${note.id}`,
|
||||
|
@ -244,34 +211,6 @@ definePageMetadata(
|
|||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
|
||||
> .clips {
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
> .item {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
|
||||
> .description {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
> .user {
|
||||
$height: 32px;
|
||||
padding-top: 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
line-height: $height;
|
||||
|
||||
> .avatar {
|
||||
width: $height;
|
||||
height: $height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue