多分できたかも

This commit is contained in:
ikasoba 2023-12-16 18:38:24 +09:00
parent 742da2f1e9
commit 3bed53187a
4 changed files with 87 additions and 41 deletions

View File

@ -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;

View File

@ -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})` };

View File

@ -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),

View File

@ -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}`,
});
},