add pinned section to "notes" tab on user profiles

This commit is contained in:
Hazelnoot 2024-10-13 21:07:50 -04:00
parent 5b64b9001d
commit 9d3aa6bb41
4 changed files with 198 additions and 113 deletions

View file

@ -0,0 +1,84 @@
<!--
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.tabstrip">
<button
v-for="tab of tabs"
:key="tab.key"
:disabled="modelValue === tab.key"
:class="{ [$style.button]: true, [$style.active]: modelValue === tab.key }"
class="_button"
click-anime
@click="emit('update:modelValue', tab.key)"
>
{{ tab.label ?? tab.key }}
</button>
</div>
</template>
<script lang="ts">
export interface Tab {
key: string;
label?: string;
}
</script>
<script setup lang="ts">
defineProps<{
modelValue: string;
tabs: Tab[];
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: string): void;
}>();
</script>
<style module lang="scss">
.tabstrip {
display: flex;
font-size: 90%;
padding: calc(var(--margin) / 2) 0;
background: color-mix(in srgb, var(--bg) 65%, transparent);
backdrop-filter: var(--blur, blur(15px));
border-radius: var(--radius-sm);
> * {
flex: 1;
}
}
.button {
padding: 10px 8px;
margin-left: 0.4rem;
margin-right: 0.4rem;
border-radius: var(--radius-sm);
&:disabled {
cursor: default;
}
&.active {
color: var(--accent);
background: var(--accentedBg);
}
&:not(.active):hover {
color: var(--fgHighlighted);
background: var(--panelHighlight);
}
&:not(:first-child) {
margin-left: 8px;
}
}
@container (max-width: 500px) {
.tabstrip {
font-size: 80%;
}
}
</style>

View file

@ -145,36 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XListenBrainz :key="user.id" :user="user" :collapsed="true"/> <XListenBrainz :key="user.id" :user="user" :collapsed="true"/>
</MkLazy> </MkLazy>
</template> </template>
<!-- <div v-if="!disableNotes"> <notes-container :user="user" :includeFeatured="false" :includePinned="user.pinnedNotes.length > 0"/>
<MkLazy>
<XTimeline :user="user"/>
</MkLazy>
</div> -->
<MkStickyContainer>
<template #header>
<!-- You can't use v-if on these, as MkTab first *deletes* and replaces all children with native HTML elements. -->
<!-- Instead, we add a "no notes" placeholder and default to null (all notes) if there's nothing pinned. -->
<!-- It also converts all comments into text! -->
<MkTab v-model="noteview" :class="$style.tab">
<option value="pinned">{{ i18n.ts.pinnedOnly }}</option>
<option :value="null">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
<MkLazy>
<div v-if="noteview === 'pinned'" class="_gaps">
<div v-if="user.pinnedNotes.length < 1" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost" aria-hidden="true" :alt="i18n.ts.noNotes"/>
<div>{{ i18n.ts.noNotes }}</div>
</div>
<div v-else class="_panel">
<MkNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true"/>
</div>
</div>
<MkNotes v-else :class="$style.tl" :noGap="true" :pagination="AllPagination"/>
</MkLazy>
</MkStickyContainer>
</div> </div>
</div> </div>
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
@ -190,8 +161,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkTab from '@/components/MkTab.vue';
import MkNotes from '@/components/MkNotes.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';
import MkAccountMoved from '@/components/MkAccountMoved.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
@ -212,6 +181,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import NotesContainer from '@/pages/user/notes-container.vue';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
const MkNote = defineAsyncComponent(() => const MkNote = defineAsyncComponent(() =>
@ -260,7 +230,6 @@ const memoDraft = ref(props.user.memo);
const isEditingMemo = ref(false); const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote); const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false); const editModerationNote = ref(false);
const noteview = ref<string | null>(null);
const listenbrainzdata = ref(false); const listenbrainzdata = ref(false);
if (props.user.listenbrainz) { if (props.user.listenbrainz) {
@ -299,26 +268,6 @@ watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
}); });
const pagination = {
endpoint: 'users/featured-notes' as const,
limit: 10,
params: computed(() => ({
userId: props.user.id,
})),
};
const AllPagination = {
endpoint: 'users/notes' as const,
limit: 10,
params: computed(() => ({
userId: props.user.id,
withRenotes: noteview.value === 'all',
withReplies: noteview.value === 'all',
withChannelNotes: noteview.value === 'all',
withFiles: noteview.value === 'files',
})),
};
const style = computed(() => { const style = computed(() => {
if (props.user.bannerUrl == null) return {}; if (props.user.bannerUrl == null) return {};
if (defaultStore.state.disableShowingAnimatedImages) { if (defaultStore.state.disableShowingAnimatedImages) {
@ -794,13 +743,6 @@ onUnmounted(() => {
</style> </style>
<style lang="scss" module> <style lang="scss" module>
.tl {
background-color: rgba(0, 0, 0, 0);
border-radius: var(--radius);
overflow: clip;
z-index: 0;
}
.tab { .tab {
margin-bottom: calc(var(--margin) / 2); margin-bottom: calc(var(--margin) / 2);
padding: calc(var(--margin) / 2) 0; padding: calc(var(--margin) / 2) 0;
@ -820,10 +762,6 @@ onUnmounted(() => {
color: var(--success); color: var(--success);
} }
.pinnedNote:not(:last-child) {
border-bottom: solid 0.5px var(--divider);
}
.infoBadges { .infoBadges {
position: absolute; position: absolute;
top: 12px; top: 12px;

View file

@ -4,60 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkStickyContainer> <notes-container :user="user"/>
<template #header>
<MkTab v-model="tab" :class="$style.tab">
<option value="featured">{{ i18n.ts.featured }}</option>
<option :value="null">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue'; import NotesContainer from '@/pages/user/notes-container.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{ defineProps<{
user: Misskey.entities.UserDetailed; user: Misskey.entities.UserDetailed;
}>(); }>();
const tab = ref<string | null>('all');
const pagination = computed(() => tab.value === 'featured' ? {
endpoint: 'users/featured-notes' as const,
limit: 10,
params: {
userId: props.user.id,
},
} : {
endpoint: 'users/notes' as const,
limit: 10,
params: {
userId: props.user.id,
withRenotes: tab.value === 'all',
withReplies: tab.value === 'all',
withChannelNotes: tab.value === 'all',
withFiles: tab.value === 'files',
},
});
</script> </script>
<style lang="scss" module>
.tab {
padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
.tl {
background: var(--bg);
border-radius: var(--radius);
overflow: clip;
}
</style>

View file

@ -0,0 +1,109 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header>
<SkTab v-model="tab" :tabs="tabs"/>
</template>
<MkLazy>
<div v-if="tab === 'pinned'" class="_gaps">
<div v-if="user.pinnedNotes.length < 1" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost" aria-hidden="true" :alt="i18n.ts.noNotes"/>
<div>{{ i18n.ts.noNotes }}</div>
</div>
<div v-else class="_panel">
<MkNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true"/>
</div>
</div>
<MkNotes v-else :class="$style.tl" :noGap="true" :pagination="pagination"/>
</MkLazy>
</MkStickyContainer>
</template>
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import SkTab from '@/components/SkTab.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import { Paging } from '@/components/MkPagination.vue';
import { Tab } from '@/components/SkTab.vue';
import { defaultStore } from '@/store.js';
const MkNote = defineAsyncComponent(() => defaultStore.state.noteDesign === 'sharkey' ? import('@/components/SkNote.vue') : import('@/components/MkNote.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
includeFeatured: boolean;
includePinned: boolean;
}>(), {
includeFeatured: true,
includePinned: true,
});
const tab = ref<string | null>('notes');
const tabs = computed(() => {
const t: Tab[] = [
{ key: 'notes', label: i18n.ts.notes },
{ key: 'all', label: i18n.ts.all },
{ key: 'files', label: i18n.ts.withFiles },
];
if (props.includeFeatured) {
t.unshift({
key: 'featured',
label: i18n.ts.featured,
});
}
if (props.includePinned) {
t.unshift({
key: 'pinned',
label: i18n.ts.pinnedOnly,
});
}
return t;
});
const pagination = computed(() => {
if (tab.value === 'featured') {
return {
endpoint: 'users/featured-notes' as const,
limit: 10,
params: {
userId: props.user.id,
},
} satisfies Paging<'users/featured-notes'>;
} else {
return {
endpoint: 'users/notes' as const,
limit: 10,
params: {
userId: props.user.id,
withRenotes: tab.value === 'all',
withReplies: tab.value === 'all',
withChannelNotes: tab.value === 'all',
withFiles: tab.value === 'files',
},
} satisfies Paging<'users/notes'>;
}
});
</script>
<style module lang="scss">
.tl {
background: var(--bg);
border-radius: var(--radius);
overflow: clip;
}
.pinnedNote:not(:last-child) {
border-bottom: solid 0.5px var(--divider);
}
</style>