多分できたかも
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