多分できたかも
This commit is contained in:
parent
742da2f1e9
commit
3bed53187a
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/>
|
||||
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)" :lastReadedAt="miLocalStorage.getItemAsJson(`channelLastReadedAt:${item.id}`) ?? undefined"/>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
@ -23,6 +23,7 @@ import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
|||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
|
@ -4,49 +4,67 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||
<div class="banner" :style="bannerStyle">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<i class="ti ti-users ti-fw"></i>
|
||||
<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.usersCount }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div>
|
||||
<i class="ti ti-pencil ti-fw"></i>
|
||||
<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.notesCount }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<div style="margin: 0; display: flex;">
|
||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
|
||||
<div class="banner" :style="bannerStyle">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<i class="ti ti-users ti-fw"></i>
|
||||
<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.usersCount }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
<div>
|
||||
<i class="ti ti-pencil ti-fw"></i>
|
||||
<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.notesCount }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<article v-if="channel.description">
|
||||
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
|
||||
</article>
|
||||
<footer>
|
||||
<span v-if="channel.lastNotedAt">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
|
||||
</span>
|
||||
</footer>
|
||||
</MkA>
|
||||
<article v-if="channel.description">
|
||||
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
|
||||
</article>
|
||||
<footer>
|
||||
<span v-if="channel.lastNotedAt">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
|
||||
</span>
|
||||
</footer>
|
||||
</MkA>
|
||||
<div v-if="lastReadedAt && channel.isFavorited && channel.lastNotedAt && Date.parse(channel.lastNotedAt) > lastReadedAt" style="position: absolute; top: -0.5rem; right: -0.5rem; background-color: var(--accent); border-radius: 100%; aspect-ratio: 1 / 1; width: 1.5rem;"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const props = defineProps<{
|
||||
channel: Record<string, any>;
|
||||
}>();
|
||||
|
||||
const getLastReadedAt = (): number | null => {
|
||||
return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null;
|
||||
};
|
||||
|
||||
const lastReadedAt = ref(getLastReadedAt());
|
||||
|
||||
watch(() => props.channel.id, () => {
|
||||
lastReadedAt.value = getLastReadedAt();
|
||||
});
|
||||
|
||||
const updateLastReadedAt = () => {
|
||||
lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now();
|
||||
};
|
||||
|
||||
const bannerStyle = computed(() => {
|
||||
if (props.channel.bannerUrl) {
|
||||
return { backgroundImage: `url(${props.channel.bannerUrl})` };
|
||||
|
|
|
@ -35,7 +35,8 @@ type Keys =
|
|||
`themes:${string}` |
|
||||
`aiscript:${string}` |
|
||||
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
|
||||
'emojis' // DEPRECATED, stored in indexeddb (13.9.0~);
|
||||
'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
|
||||
`channelLastReadedAt:${string}`
|
||||
|
||||
export const miLocalStorage = {
|
||||
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
|
||||
|
|
|
@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions ?? undefined" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :class="$style.main">
|
||||
<div v-if="channel && tab === 'overview'" class="_gaps">
|
||||
<div class="_panel" :class="$style.bannerContainer">
|
||||
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
|
||||
<div :class="$style.bannerStatus">
|
||||
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkFoldableSection>
|
||||
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
|
||||
<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
|
||||
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
|
||||
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
@ -69,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
|
@ -89,6 +90,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -97,7 +99,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const tab = ref('overview');
|
||||
const channel = ref(null);
|
||||
const channel = ref<entities.Channel | null>(null);
|
||||
const favorited = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const searchPagination = ref();
|
||||
|
@ -114,14 +116,23 @@ watch(() => props.channelId, async () => {
|
|||
channel.value = await os.api('channels/show', {
|
||||
channelId: props.channelId,
|
||||
});
|
||||
favorited.value = channel.value.isFavorited;
|
||||
favorited.value = channel.value.isFavorited ?? false;
|
||||
if (favorited.value || channel.value.isFollowing) {
|
||||
tab.value = 'timeline';
|
||||
}
|
||||
|
||||
if (favorited.value && channel.value.lastNotedAt) {
|
||||
const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
|
||||
const lastNotedAt = Date.parse(channel.value.lastNotedAt);
|
||||
|
||||
if (lastNotedAt > lastReadedAt) {
|
||||
miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
function edit() {
|
||||
router.push(`/channels/${channel.value.id}/edit`);
|
||||
router.push(`/channels/${channel.value?.id}/edit`);
|
||||
}
|
||||
|
||||
function openPostForm() {
|
||||
|
@ -131,6 +142,8 @@ function openPostForm() {
|
|||
}
|
||||
|
||||
function favorite() {
|
||||
if (!channel.value) return;
|
||||
|
||||
os.apiWithDialog('channels/favorite', {
|
||||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
|
@ -139,6 +152,8 @@ function favorite() {
|
|||
}
|
||||
|
||||
async function unfavorite() {
|
||||
if (!channel.value) return;
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unfavoriteConfirm,
|
||||
|
@ -152,6 +167,8 @@ async function unfavorite() {
|
|||
}
|
||||
|
||||
async function search() {
|
||||
if (!channel.value) return;
|
||||
|
||||
const query = searchQuery.value.toString().trim();
|
||||
|
||||
if (query == null) return;
|
||||
|
@ -176,6 +193,10 @@ const headerActions = computed(() => {
|
|||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyUrl,
|
||||
handler: async (): Promise<void> => {
|
||||
if (!channel.value) {
|
||||
console.warn('failed to copy channel URL. channel.value is null.');
|
||||
return;
|
||||
}
|
||||
copyToClipboard(`${url}/channels/${channel.value.id}`);
|
||||
os.success();
|
||||
},
|
||||
|
@ -186,9 +207,14 @@ const headerActions = computed(() => {
|
|||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: async (): Promise<void> => {
|
||||
if (!channel.value) {
|
||||
console.warn('failed to share channel. channel.value is null.');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.share({
|
||||
title: channel.value.name,
|
||||
text: channel.value.description,
|
||||
text: channel.value.description ?? undefined,
|
||||
url: `${url}/channels/${channel.value.id}`,
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue