Merge branch 'develop' into ssmucny-events
This commit is contained in:
		
						commit
						b862aa9027
					
				|  | @ -27,6 +27,8 @@ | |||
| - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように | ||||
| - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように | ||||
| - プロフィールURLをコピーできるボタンを追加 #11190 | ||||
| - ユーザーのContextMenuに「アンテナに追加」ボタンを追加 | ||||
| - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように | ||||
| - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように | ||||
| - Fix: サーバーメトリクスが90度傾いている | ||||
| - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 | ||||
|  |  | |||
|  | @ -52,6 +52,7 @@ export interface Locale { | |||
|     "deleteAndEdit": string; | ||||
|     "deleteAndEditConfirm": string; | ||||
|     "addToList": string; | ||||
|     "addToAntenna": string; | ||||
|     "sendMessage": string; | ||||
|     "copyRSS": string; | ||||
|     "copyUsername": string; | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ delete: "削除" | |||
| deleteAndEdit: "削除して編集" | ||||
| deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。" | ||||
| addToList: "リストに追加" | ||||
| addToAntenna: "アンテナに追加" | ||||
| sendMessage: "メッセージを送信" | ||||
| copyRSS: "RSSをコピー" | ||||
| copyUsername: "ユーザー名をコピー" | ||||
|  |  | |||
|  | @ -4,3 +4,4 @@ import { Cache } from '@/scripts/cache'; | |||
| export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity); | ||||
| export const rolesCache = new Cache(Infinity); | ||||
| export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity); | ||||
| export const antennasCache = new Cache<misskey.entities.Antenna[]>(Infinity); | ||||
|  |  | |||
|  | @ -1,24 +1,29 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | ||||
| 	<div v-for="user in users.slice(0, limit)" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | ||||
| 		<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/> | ||||
| 	</div> | ||||
| 	<div v-if="users.length > limit" style="display: inline-block;">...</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, ref } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import { UserLite } from 'misskey-js/built/entities'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	userIds: string[]; | ||||
| }>(); | ||||
| 	limit?: number; | ||||
| }>(), { | ||||
| 	limit: Infinity, | ||||
| }); | ||||
| 
 | ||||
| const users = ref([]); | ||||
| const users = ref<UserLite[]>([]); | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	users.value = await os.api('users/show', { | ||||
| 		userIds: props.userIds, | ||||
| 	}); | ||||
| 	}) as unknown as UserLite[]; | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -87,7 +87,7 @@ const props = defineProps<{ | |||
| 	channelId: string; | ||||
| }>(); | ||||
| 
 | ||||
| let tab = $ref('timeline'); | ||||
| let tab = $ref('overview'); | ||||
| let channel = $ref(null); | ||||
| let favorited = $ref(false); | ||||
| let searchQuery = $ref(''); | ||||
|  | @ -107,6 +107,9 @@ watch(() => props.channelId, async () => { | |||
| 		channelId: props.channelId, | ||||
| 	}); | ||||
| 	favorited = channel.isFavorited; | ||||
| 	if (favorited || channel.isFollowing) { | ||||
| 		tab = 'timeline'; | ||||
| 	} | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| function edit() { | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import XAntenna from './editor.vue'; | |||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { antennasCache } from '@/cache'; | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| 
 | ||||
|  | @ -26,13 +27,10 @@ let draft = $ref({ | |||
| }); | ||||
| 
 | ||||
| function onAntennaCreated() { | ||||
| 	antennasCache.delete(); | ||||
| 	router.push('/my/antennas'); | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'ti ti-antenna', | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import * as os from '@/os'; | |||
| import { i18n } from '@/i18n'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { antennasCache } from '@/cache'; | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| 
 | ||||
|  | @ -20,6 +21,7 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| function onAntennaUpdated() { | ||||
| 	antennasCache.delete(); | ||||
| 	router.push('/my/antennas'); | ||||
| } | ||||
| 
 | ||||
|  | @ -27,10 +29,6 @@ os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) = | |||
| 	antenna = antennaResponse; | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'ti ti-antenna', | ||||
|  |  | |||
|  | @ -2,15 +2,20 @@ | |||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div class="ieepwinx"> | ||||
| 			<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 		<div> | ||||
| 			<div v-if="antennas.length === 0" class="empty"> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img :src="infoImageUrl" class="_ghost"/> | ||||
| 					<div>{{ i18n.ts.nothing }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=""> | ||||
| 				<MkPagination v-slot="{items}" ref="list" :pagination="pagination"> | ||||
| 					<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`"> | ||||
| 						<div class="name">{{ antenna.name }}</div> | ||||
| 					</MkA> | ||||
| 				</MkPagination> | ||||
| 			<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 
 | ||||
| 			<div v-if="antennas.length > 0" class="_gaps"> | ||||
| 				<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`"> | ||||
| 					<div class="name">{{ antenna.name }}</div> | ||||
| 				</MkA> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
|  | @ -18,19 +23,31 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { antennasCache } from '@/cache'; | ||||
| import { api } from '@/os'; | ||||
| import { onActivated } from 'vue'; | ||||
| import { infoImageUrl } from '@/instance'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'antennas/list' as const, | ||||
| 	noPaging: true, | ||||
| 	limit: 10, | ||||
| }; | ||||
| const antennas = $computed(() => antennasCache.value.value ?? []); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| function fetch() { | ||||
| 	antennasCache.fetch(() => api('antennas/list')); | ||||
| } | ||||
| 
 | ||||
| fetch(); | ||||
| 
 | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'ti ti-refresh', | ||||
| 	text: i18n.ts.reload, | ||||
| 	handler: () => { | ||||
| 		antennasCache.delete(); | ||||
| 		fetch(); | ||||
| 	}, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
|  | @ -38,30 +55,30 @@ definePageMetadata({ | |||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'ti ti-antenna', | ||||
| }); | ||||
| 
 | ||||
| onActivated(() => { | ||||
| 	antennasCache.fetch(() => api('antennas/list')); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ieepwinx { | ||||
| <style lang="scss" module> | ||||
| .add { | ||||
| 	margin: 0 auto 16px auto; | ||||
| } | ||||
| 
 | ||||
| 	> .add { | ||||
| 		margin: 0 auto 16px auto; | ||||
| 	} | ||||
| .antenna { | ||||
| 	display: block; | ||||
| 	padding: 16px; | ||||
| 	border: solid 1px var(--divider); | ||||
| 	border-radius: 6px; | ||||
| 
 | ||||
| 	.ljoevbzj { | ||||
| 		display: block; | ||||
| 		padding: 16px; | ||||
| 		margin-bottom: 8px; | ||||
| 		border: solid 1px var(--divider); | ||||
| 		border-radius: 6px; | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			border: solid 1px var(--accent); | ||||
| 			text-decoration: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .name { | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
| 	&:hover { | ||||
| 		border: solid 1px var(--accent); | ||||
| 		text-decoration: none; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .name { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -3,38 +3,43 @@ | |||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div class="_gaps"> | ||||
| 			<div v-if="items.length === 0" class="empty"> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img :src="infoImageUrl" class="_ghost"/> | ||||
| 					<div>{{ i18n.ts.nothing }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> | ||||
| 
 | ||||
| 			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> | ||||
| 						<div style="margin-bottom: 4px;">{{ list.name }}</div> | ||||
| 						<MkAvatars :userIds="list.userIds"/> | ||||
| 					</MkA> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 			<div v-if="items.length > 0" class="_gaps"> | ||||
| 				<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> | ||||
| 					<div style="margin-bottom: 4px;">{{ list.name }}</div> | ||||
| 					<MkAvatars :userIds="list.userIds" :limit="10"/> | ||||
| 				</MkA> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import { onActivated } from 'vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkAvatars from '@/components/MkAvatars.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { userListsCache } from '@/cache'; | ||||
| import { infoImageUrl } from '@/instance'; | ||||
| 
 | ||||
| const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); | ||||
| const items = $computed(() => userListsCache.value.value ?? []); | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'users/lists/list' as const, | ||||
| 	noPaging: true, | ||||
| 	limit: 10, | ||||
| }; | ||||
| function fetch() { | ||||
| 	userListsCache.fetch(() => os.api('users/lists/list')); | ||||
| } | ||||
| 
 | ||||
| fetch(); | ||||
| 
 | ||||
| async function create() { | ||||
| 	const { canceled, result: name } = await os.inputText({ | ||||
|  | @ -43,10 +48,18 @@ async function create() { | |||
| 	if (canceled) return; | ||||
| 	await os.apiWithDialog('users/lists/create', { name: name }); | ||||
| 	userListsCache.delete(); | ||||
| 	pagingComponent.reload(); | ||||
| 	fetch(); | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'ti ti-refresh', | ||||
| 	text: i18n.ts.reload, | ||||
| 	handler: () => { | ||||
| 		userListsCache.delete(); | ||||
| 		fetch(); | ||||
| 	}, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
|  | @ -58,6 +71,10 @@ definePageMetadata({ | |||
| 		handler: create, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
| onActivated(() => { | ||||
| 	fetch(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| import { ref } from "vue"; | ||||
| 
 | ||||
| export class Cache<T> { | ||||
| 	private cachedAt: number | null = null; | ||||
| 	private value: T | undefined; | ||||
| 	public value = ref<T | undefined>(); | ||||
| 	private lifetime: number; | ||||
| 
 | ||||
| 	constructor(lifetime: Cache<never>['lifetime']) { | ||||
|  | @ -10,21 +11,20 @@ export class Cache<T> { | |||
| 
 | ||||
| 	public set(value: T): void { | ||||
| 		this.cachedAt = Date.now(); | ||||
| 		this.value = value; | ||||
| 		this.value.value = value; | ||||
| 	} | ||||
| 
 | ||||
| 	public get(): T | undefined { | ||||
| 	private get(): T | undefined { | ||||
| 		if (this.cachedAt == null) return undefined; | ||||
| 		if ((Date.now() - this.cachedAt) > this.lifetime) { | ||||
| 			this.value = undefined; | ||||
| 			this.value.value = undefined; | ||||
| 			this.cachedAt = null; | ||||
| 			return undefined; | ||||
| 		} | ||||
| 		return this.value; | ||||
| 		return this.value.value; | ||||
| 	} | ||||
| 
 | ||||
| 	public delete() { | ||||
| 		this.value = undefined; | ||||
| 		this.cachedAt = null; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { toUnicode } from 'punycode'; | ||||
| import { defineAsyncComponent } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | @ -8,8 +9,7 @@ import { defaultStore, userActions } from '@/store'; | |||
| import { $i, iAmModerator } from '@/account'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { Router } from '@/nirax'; | ||||
| import { rolesCache, userListsCache } from '@/cache'; | ||||
| import { toUnicode } from 'punycode'; | ||||
| import { antennasCache, rolesCache, userListsCache } from '@/cache'; | ||||
| 
 | ||||
| export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { | ||||
| 	const meId = $i ? $i.id : null; | ||||
|  | @ -166,11 +166,39 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router | |||
| 
 | ||||
| 			return lists.map(list => ({ | ||||
| 				text: list.name, | ||||
| 				action: () => { | ||||
| 					os.apiWithDialog('users/lists/push', { | ||||
| 				action: async () => { | ||||
| 					await os.apiWithDialog('users/lists/push', { | ||||
| 						listId: list.id, | ||||
| 						userId: user.id, | ||||
| 					}); | ||||
| 					userListsCache.delete(); | ||||
| 				}, | ||||
| 			})); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		type: 'parent', | ||||
| 		icon: 'ti ti-antenna', | ||||
| 		text: i18n.ts.addToAntenna, | ||||
| 		children: async () => { | ||||
| 			const antennas = await antennasCache.fetch(() => os.api('antennas/list')); | ||||
| 			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; | ||||
| 			return antennas.filter((a) => a.src === 'users').map(antenna => ({ | ||||
| 				text: antenna.name, | ||||
| 				action: async () => { | ||||
| 					await os.apiWithDialog('antennas/update', { | ||||
| 						antennaId: antenna.id, | ||||
| 						name: antenna.name, | ||||
| 						keywords: antenna.keywords, | ||||
| 						excludeKeywords: antenna.excludeKeywords, | ||||
| 						src: antenna.src, | ||||
| 						userListId: antenna.userListId, | ||||
| 						users: [...antenna.users, canonical], | ||||
| 						caseSensitive: antenna.caseSensitive, | ||||
| 						withReplies: antenna.withReplies, | ||||
| 						withFile: antenna.withFile, | ||||
| 						notify: antenna.notify, | ||||
| 					}); | ||||
| 					antennasCache.delete(); | ||||
| 				}, | ||||
| 			})); | ||||
| 		}, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue