Note tabs + Jump to Reply changes + other stuff (#10157)

This commit is contained in:
Kainoa Kanter 2023-05-19 21:20:15 +00:00
commit ad0ef2911c
12 changed files with 348 additions and 248 deletions

View file

@ -57,8 +57,11 @@ sendMessage: "Send a message"
copyUsername: "Copy username" copyUsername: "Copy username"
searchUser: "Search for a user" searchUser: "Search for a user"
reply: "Reply" reply: "Reply"
jumpToReply: "Jump to Reply"
loadMore: "Load more" loadMore: "Load more"
showMore: "Show more" showMore: "Show more"
newer: "newer"
older: "older"
showLess: "Close" showLess: "Close"
youGotNewFollower: "followed you" youGotNewFollower: "followed you"
receiveFollowRequest: "Follow request received" receiveFollowRequest: "Follow request received"

View file

@ -2,7 +2,7 @@
<button <button
v-if="!link" v-if="!link"
class="bghgjjyj _button" class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }" :class="{ inline, primary, gradate, danger, rounded, full, mini }"
:type="type" :type="type"
@click="emit('click', $event)" @click="emit('click', $event)"
@mousedown="onMousedown" @mousedown="onMousedown"
@ -15,7 +15,7 @@
<MkA <MkA
v-else v-else
class="bghgjjyj _button" class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }" :class="{ inline, primary, gradate, danger, rounded, full, mini }"
:to="to" :to="to"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
@ -41,6 +41,7 @@ const props = defineProps<{
wait?: boolean; wait?: boolean;
danger?: boolean; danger?: boolean;
full?: boolean; full?: boolean;
mini: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -190,6 +191,12 @@ function onMousedown(evt: MouseEvent): void {
} }
} }
&.mini {
padding: 4px 8px;
font-size: .9em;
border-radius: 100px;
}
&:disabled { &:disabled {
opacity: 0.7; opacity: 0.7;
} }

View file

@ -5,7 +5,7 @@
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }" v-size="{ max: [500, 450, 350, 300] }"
class="tkcbzcuz" class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
@ -104,6 +104,11 @@
/> />
</div> </div>
</div> </div>
</div>
<div v-if="detailedView" class="info">
<MkA class="created-at" :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA>
<MkA <MkA
v-if="appearNote.channel && !inChannel" v-if="appearNote.channel && !inChannel"
class="channel" class="channel"
@ -113,11 +118,6 @@
{{ appearNote.channel.name }}</MkA {{ appearNote.channel.name }}</MkA
> >
</div> </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"> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
@ -130,7 +130,7 @@
@click="reply()" @click="reply()"
> >
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <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> <p class="count">{{ appearNote.repliesCount }}</p>
</template> </template>
</button> </button>
@ -139,6 +139,7 @@
class="button" class="button"
:note="appearNote" :note="appearNote"
:count="appearNote.renoteCount" :count="appearNote.renoteCount"
:detailedView="detailedView"
/> />
<XStarButtonNoEmoji <XStarButtonNoEmoji
v-if="!enableEmojiReactions" v-if="!enableEmojiReactions"
@ -450,6 +451,10 @@ function focusAfter() {
focusNext(el.value); focusNext(el.value);
} }
function scrollIntoView() {
el.value.scrollIntoView();
}
function noteClick(e) { function noteClick(e) {
if (document.getSelection().type === "Range" || props.detailedView) { if (document.getSelection().type === "Range" || props.detailedView) {
e.stopPropagation(); e.stopPropagation();
@ -464,6 +469,12 @@ function readPromo() {
}); });
isDeleted.value = true; isDeleted.value = true;
} }
defineExpose({
focus,
blur,
scrollIntoView,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -656,14 +667,13 @@ function readPromo() {
} }
} }
} }
> .channel {
opacity: 0.7;
font-size: 80%;
}
} }
> .info { > .info {
margin-block: 16px; display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: .7em;
margin-top: 16px;
opacity: 0.7; opacity: 0.7;
font-size: 0.9em; font-size: 0.9em;
} }

View file

@ -15,32 +15,128 @@
:key="note.id" :key="note.id"
class="reply-to" class="reply-to"
:note="note" :note="note"
:detailedView="true"
/> />
<MkLoading v-else-if="appearNote.reply" mini /> <MkLoading v-else-if="appearNote.reply" mini />
<MkNoteSub <MkNoteSub
v-if="appearNote.reply" v-if="appearNote.reply"
:note="appearNote.reply" :note="appearNote.reply"
class="reply-to" class="reply-to"
:detailedView="true"
/> />
<div ref="noteEl" class="article" tabindex="-1">
<MkNote <MkNote
ref="noteEl"
@contextmenu.stop="onContextmenu" @contextmenu.stop="onContextmenu"
tabindex="-1" tabindex="-1"
:note="appearNote" :note="appearNote"
:detailedView="true" detailedView
></MkNote> ></MkNote>
</div>
<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 <MkNoteSub
v-if="directReplies" v-if="directReplies && tab === 'replies'"
v-for="note in directReplies" v-for="note in directReplies"
:key="note.id" :key="note.id"
:note="note" :note="note"
class="reply" class="reply"
:conversation="replies" :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>
<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">
@ -70,15 +166,17 @@ import {
reactive, reactive,
ref, ref,
} from "vue"; } 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 MkNote from "@/components/MkNote.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XStarButton from "@/components/MkStarButton.vue"; 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 MkUserCardMini from "@/components/MkUserCardMini.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";
import { useRouter } from "@/router";
import * as os from "@/os"; import * as os from "@/os";
import { defaultStore, noteViewInterruptors } from "@/store"; import { defaultStore, noteViewInterruptors } from "@/store";
import { reactionPicker } from "@/scripts/reaction-picker"; import { reactionPicker } from "@/scripts/reaction-picker";
@ -89,12 +187,15 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types"; import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
import appear from "@/directives/appear";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
}>(); }>();
let tab = $ref("replies");
let note = $ref(deepClone(props.note)); let note = $ref(deepClone(props.note));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
@ -143,6 +244,9 @@ const translating = ref(false);
let conversation = $ref<null | misskey.entities.Note[]>([]); let conversation = $ref<null | misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]); const replies = ref<misskey.entities.Note[]>([]);
let directReplies = $ref<null | 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; let isScrolling;
const keymap = { const keymap = {
@ -256,10 +360,14 @@ os.api("notes/children", {
directReplies = res directReplies = res
.filter( .filter(
(note) => (note) =>
note.replyId === appearNote.id || note.replyId === appearNote.id
note.renoteId === appearNote.id
) )
.reverse(); .reverse();
directQuotes = res
.filter(
(note) =>
note.renoteId === appearNote.id
);
}); });
conversation = null; 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> { async function onNoteUpdated(noteData: NoteUpdatedEvent): Promise<void> {
const { type, id, body } = noteData; const { type, id, body } = noteData;
@ -323,12 +458,15 @@ document.addEventListener("wheel", () => {
onMounted(() => { onMounted(() => {
stream.on("noteUpdated", onNoteUpdated); stream.on("noteUpdated", onNoteUpdated);
isScrolling = false; isScrolling = false;
noteEl?.scrollIntoView(); noteEl.scrollIntoView();
}); });
onUpdated(() => { onUpdated(() => {
if (!isScrolling) { 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 { > .reply-to {
margin-bottom: -16px; margin-bottom: -16px;
padding-bottom: 16px; padding-bottom: 16px;
} }
> .renote { > :deep(.note-container) {
display: flex; padding-block: 28px 0;
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;
padding-top: 12px; padding-top: 12px;
font-size: 1.1rem; font-size: 1.1rem;
overflow: clip; overflow: clip;
outline: none; outline: none;
scroll-margin-top: calc(var(--stickyTop) + 20vh); scroll-margin-top: calc(var(--stickyTop) + 20vh);
:deep(.article) { .article {
cursor: unset; cursor: unset;
padding-bottom: 0;
} }
&:first-of-type { &:first-of-type {
padding-top: 28px; 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 { > .reply {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
cursor: pointer; 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 { &.max-width_500px {
font-size: 0.9em; font-size: 0.9em;
@ -518,46 +620,20 @@ onUnmounted(() => {
> .reply-to:first-child { > .reply-to:first-child {
padding-top: 14px; padding-top: 14px;
} }
> .renote {
padding: 8px 16px 0 16px;
}
> .article { > :deep(.note-container) {
padding: 6px 0 0 0; padding: 6px 0 0 0;
> .header > .body { > .header > .body {
padding-left: 10px; padding-left: 10px;
} }
} }
} > .clips, > .chips, > :deep(.user-card-mini) {
padding-inline: 16px !important;
&.max-width_350px {
> .article {
> .main {
> .footer {
> .button {
&:not(:last-child) {
margin-right: 18px;
}
}
}
}
} }
} }
&.max-width_300px { &.max-width_300px {
font-size: 0.825em; font-size: 0.825em;
> .article {
> .main {
> .footer {
> .button {
&:not(:last-child) {
margin-right: 12px;
}
}
}
}
}
} }
} }
@ -566,4 +642,36 @@ onUnmounted(() => {
text-align: center; text-align: center;
opacity: 0.7; 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> </style>

View file

@ -4,6 +4,8 @@
ref="el" ref="el"
v-size="{ max: [450, 500] }" v-size="{ max: [450, 500] }"
class="wrpstxzv" class="wrpstxzv"
:id="detailedView ? appearNote.id : null"
tabindex="-1"
:class="{ :class="{
children: depth > 1, children: depth > 1,
singleStart: replies.length == 1, singleStart: replies.length == 1,
@ -127,32 +129,19 @@
</div> </div>
</div> </div>
<template v-if="conversation"> <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 <MkNoteSub
v-if="replyLevel < 11 && depth < 5"
v-for="reply in replies" v-for="reply in replies"
:key="reply.id" :key="reply.id"
:note="reply" :note="reply"
class="reply" class="reply"
:class="{single: replies.length == 1}"
:conversation="conversation" :conversation="conversation"
:depth="depth + 1" :depth="replies.lenght == 1 ? depth : depth + 1"
:replyLevel="replyLevel + 1" :replyLevel="replyLevel + 1"
:parentId="appearNote.replyId" :parentId="appearNote.replyId"
:detailedView="detailedView"
/> />
</template>
</template>
<div v-else-if="replies.length > 0" class="more"> <div v-else-if="replies.length > 0" class="more">
<div class="line"></div> <div class="line"></div>
<MkA class="text _link" :to="notePage(note)" <MkA class="text _link" :to="notePage(note)"
@ -212,6 +201,7 @@ const props = withDefaults(
note: misskey.entities.Note; note: misskey.entities.Note;
conversation?: misskey.entities.Note[]; conversation?: misskey.entities.Note[];
parentId?; parentId?;
detailedView?;
// how many notes are in between this one and the note being viewed in detail // how many notes are in between this one and the note being viewed in detail
depth?: number; depth?: number;
@ -348,6 +338,7 @@ function noteClick(e) {
<style lang="scss" scoped> <style lang="scss" scoped>
.wrpstxzv { .wrpstxzv {
padding: 16px 32px; padding: 16px 32px;
outline: none;
&.children { &.children {
padding: 10px 0 0 var(--indent); padding: 10px 0 0 var(--indent);
padding-left: var(--indent) !important; padding-left: var(--indent) !important;

View file

@ -7,7 +7,7 @@
@click="renote(false, $event)" @click="renote(false, $event)"
> >
<i class="ph-repeat ph-bold ph-lg"></i> <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>
<button v-else class="eddddedb _button"> <button v-else class="eddddedb _button">
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -30,6 +30,7 @@ import { MenuItem } from "@/types/menu";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
count: number; count: number;
detailedView?;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();

View file

@ -2,7 +2,8 @@
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<MkA <MkA
v-if="!detailed && note.replyId" v-if="!detailed && note.replyId"
:to="`/notes/${note.replyId}`" :to="`#${note.replyId}`"
behavior="browser"
class="reply-icon" class="reply-icon"
@click.stop @click.stop
> >
@ -16,6 +17,7 @@
!note.replyId !note.replyId
" "
:to="`/notes/${note.renoteId}`" :to="`/notes/${note.renoteId}`"
v-tooltip="i18n.ts.jumpToReply"
class="reply-icon" class="reply-icon"
@click.stop @click.stop
> >
@ -66,7 +68,9 @@
<template v-if="!note.cw"> <template v-if="!note.cw">
<MkA <MkA
v-if="!detailed && note.replyId" v-if="!detailed && note.replyId"
:to="`/notes/${note.replyId}`" :to="`#${note.replyId}`"
behavior="browser"
v-tooltip="i18n.ts.jumpToReply"
class="reply-icon" class="reply-icon"
@click.stop @click.stop
> >
@ -135,6 +139,7 @@
<MkButton <MkButton
v-if="hasMfm && defaultStore.state.animatedMfm" v-if="hasMfm && defaultStore.state.animatedMfm"
@click.stop="toggleMfm" @click.stop="toggleMfm"
:mini="true"
> >
<template v-if="disableMfm"> <template v-if="disableMfm">
<i class="ph-play ph-bold"></i> {{ i18n.ts._mfm.play }} <i class="ph-play ph-bold"></i> {{ i18n.ts._mfm.play }}

View file

@ -6,6 +6,9 @@ export default defineComponent({
modelValue: { modelValue: {
required: true, required: true,
}, },
style: {
required: false,
},
}, },
render() { render() {
const options = this.$slots.default(); const options = this.$slots.default();
@ -13,22 +16,21 @@ export default defineComponent({
return h( return h(
"div", "div",
{ {
class: "pxhvhrfw", class: [
"pxhvhrfw",
{ chips: this.style === "chips" },
],
role: "tablist",
}, },
options.map((option) => options.map((option) =>
withDirectives( withDirectives(
h( h(
"button", "button",
{ {
class: [ class: "_button",
"_button", role: "tab",
{
active:
this.modelValue === option.props?.value,
},
],
key: option.key, key: option.key,
disabled: this.modelValue === option.props?.value, 'aria-selected': this.modelValue === option.props?.value ? "true" : "false",
onClick: () => { onClick: () => {
this.$emit( this.$emit(
"update:modelValue", "update:modelValue",
@ -64,12 +66,12 @@ export default defineComponent({
cursor: default; cursor: default;
} }
&.active { &[aria-selected="true"] {
color: var(--accent); color: var(--accent);
background: var(--accentedBg); background: var(--accentedBg) !important;
} }
&:not(.active):hover { &:not([aria-selected="true"]):hover {
color: var(--fgHighlighted); color: var(--fgHighlighted);
background: var(--panelHighlight); 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 { &.max-width_500px {
font-size: 80%; font-size: 80%;

View file

@ -1,9 +1,11 @@
<template> <template>
<div <MkA
class="user-card-mini"
:class="[ :class="[
$style.root, $style.root,
{ yellow: user.isSilenced, red: user.isSuspended, gray: false }, { yellow: user.isSilenced, red: user.isSuspended, gray: false },
]" ]"
:to="userPage(user)"
> >
<MkAvatar <MkAvatar
class="avatar" class="avatar"
@ -18,30 +20,37 @@
> >
</div> </div>
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues" /> <MkMiniChart v-if="chartValues" class="chart" :src="chartValues" />
</div> </MkA>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue"; import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os"; import * as os from "@/os";
import { acct } from "@/filters/user"; import { acct, userPage } from "@/filters/user";
const props = defineProps<{ const props = withDefaults(defineProps<{
user: misskey.entities.User; user: misskey.entities.User;
}>(); withChart?: boolean;
}>(),
{
withChart: true,
}
);
let chartValues = $ref<number[] | null>(null); let chartValues = $ref<number[] | null>(null);
os.apiGet("charts/user/notes", { if (props.withChart) {
os.apiGet("charts/user/notes", {
userId: props.user.id, userId: props.user.id,
limit: 16 + 1, limit: 16 + 1,
span: "day", span: "day",
}).then((res) => { }).then((res) => {
// //
res.inc.splice(0, 1); res.inc.splice(0, 1);
chartValues = res.inc; chartValues = res.inc;
}); });
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@ -54,6 +63,7 @@ os.apiGet("charts/user/notes", {
padding: 16px; padding: 16px;
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
transition: background .2s;
> :global(.avatar) { > :global(.avatar) {
display: block; display: block;
@ -94,6 +104,10 @@ os.apiGet("charts/user/notes", {
height: 30px; height: 30px;
} }
&:hover, &:focus {
background: var(--panelHighlight);
}
&:global(.yellow) { &:global(.yellow) {
--c: rgb(255 196 0 / 15%); --c: rgb(255 196 0 / 15%);
background-image: linear-gradient( background-image: linear-gradient(

View file

@ -2,9 +2,8 @@
<a <a
:href="to" :href="to"
:class="active ? activeClass : null" :class="active ? activeClass : null"
@click="nav"
@contextmenu.prevent.stop="onContextmenu" @contextmenu.prevent.stop="onContextmenu"
@click.stop @click.stop="nav"
> >
<slot></slot> <slot></slot>
</a> </a>
@ -99,13 +98,9 @@ function popout() {
} }
function nav(ev: MouseEvent) { function nav(ev: MouseEvent) {
if (!ev.ctrlKey) { if (!ev.ctrlKey && props.behavior !== "browser") {
ev.preventDefault();
if (props.behavior === "browser") { ev.preventDefault();
location.href = props.to;
return;
}
if (props.behavior) { if (props.behavior) {
if (props.behavior === "window") { if (props.behavior === "window") {

View file

@ -139,6 +139,7 @@ const props = defineProps<{
thin?: boolean; thin?: boolean;
displayMyAvatar?: boolean; displayMyAvatar?: boolean;
displayBackButton?: boolean; displayBackButton?: boolean;
to?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -193,7 +194,11 @@ const preventDrag = (ev: TouchEvent) => {
}; };
const onClick = () => { const onClick = () => {
if (props.to) {
location.href = props.to;
} else {
scrollToTop(el, { behavior: "smooth" }); scrollToTop(el, { behavior: "smooth" });
}
}; };
function onTabMousedown(tab: Tab, ev: MouseEvent): void { function onTabMousedown(tab: Tab, ev: MouseEvent): void {

View file

@ -5,6 +5,7 @@
:actions="headerActions" :actions="headerActions"
:tabs="headerTabs" :tabs="headerTabs"
:display-back-button="true" :display-back-button="true"
:to="`#${noteId}`"
/></template> /></template>
<MkSpacer :content-max="800" :marginMin="6"> <MkSpacer :content-max="800" :marginMin="6">
<div class="fcuexfpr"> <div class="fcuexfpr">
@ -26,6 +27,7 @@
v-if="!showNext && hasNext" v-if="!showNext && hasNext"
class="load next" class="load next"
@click="showNext = true" @click="showNext = true"
v-tooltip="`${i18n.ts.loadMore} (${i18n.ts.newer})`"
><i class="ph-caret-up ph-bold ph-lg"></i ><i class="ph-caret-up ph-bold ph-lg"></i
></MkButton> ></MkButton>
<div class="note _gap"> <div class="note _gap">
@ -39,41 +41,11 @@
class="note" class="note"
/> />
</div> </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 <MkButton
v-if="!showPrev && hasPrev" v-if="!showPrev && hasPrev"
class="load prev" class="load prev"
@click="showPrev = true" @click="showPrev = true"
v-tooltip="`${i18n.ts.loadMore} (${i18n.ts.older})`"
><i class="ph-caret-down ph-bold ph-lg"></i ><i class="ph-caret-down ph-bold ph-lg"></i
></MkButton> ></MkButton>
</div> </div>
@ -111,7 +83,6 @@ const props = defineProps<{
}>(); }>();
let note = $ref<null | misskey.entities.Note>(); let note = $ref<null | misskey.entities.Note>();
let clips = $ref();
let hasPrev = $ref(false); let hasPrev = $ref(false);
let hasNext = $ref(false); let hasNext = $ref(false);
let showPrev = $ref(false); let showPrev = $ref(false);
@ -157,9 +128,6 @@ function fetchNote() {
.then((res) => { .then((res) => {
note = res; note = res;
Promise.all([ Promise.all([
os.api("notes/clips", {
noteId: note.id,
}),
os.api("users/notes", { os.api("users/notes", {
userId: note.userId, userId: note.userId,
untilId: note.id, untilId: note.id,
@ -170,8 +138,7 @@ function fetchNote() {
sinceId: note.id, sinceId: note.id,
limit: 1, limit: 1,
}), }),
]).then(([_clips, prev, next]) => { ]).then(([prev, next]) => {
clips = _clips;
hasPrev = prev.length !== 0; hasPrev = prev.length !== 0;
hasNext = next.length !== 0; hasNext = next.length !== 0;
}); });
@ -193,7 +160,7 @@ definePageMetadata(
computed(() => computed(() =>
note note
? { ? {
title: i18n.ts.note, title: i18n.t("noteOf", { user: note.user.name }),
subtitle: new Date(note.createdAt).toLocaleString(), subtitle: new Date(note.createdAt).toLocaleString(),
avatar: note.user, avatar: note.user,
path: `/notes/${note.id}`, path: `/notes/${note.id}`,
@ -244,34 +211,6 @@ definePageMetadata(
background: var(--panel); 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;
}
}
}
}
} }
} }
} }