Merge branch 'develop' into notification-read-api
This commit is contained in:
		| @@ -7,6 +7,13 @@ | ||||
|  | ||||
| --> | ||||
|  | ||||
| ## 12.x.x (unreleased) | ||||
|  | ||||
| ### Improvements | ||||
|  | ||||
| ### Bugfixes | ||||
| - クライアント: 一部のコンポーネントが裏に隠れるのを修正 | ||||
|  | ||||
| ## 12.100.2 (2021/12/18) | ||||
|  | ||||
| ### Bugfixes | ||||
|   | ||||
| @@ -87,7 +87,7 @@ Configuration files are located in [`/.github/workflows`](/.github/workflows). | ||||
|  | ||||
| ## Vue | ||||
| Misskey uses Vue(v3) as its front-end framework. | ||||
| **When creating a new component, please use the Composition API instead of the Options API.** | ||||
| **When creating a new component, please use the Composition API (and [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html)) instead of the Options API.** | ||||
| Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome. | ||||
|  | ||||
| ## Adding MisskeyRoom items | ||||
|   | ||||
| @@ -614,7 +614,6 @@ regenerateLoginToken: "ログイントークンを再生成" | ||||
| regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" | ||||
| setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" | ||||
| fileIdOrUrl: "ファイルIDまたはURL" | ||||
| chatOpenBehavior: "チャットを開くときの動作" | ||||
| behavior: "動作" | ||||
| sample: "サンプル" | ||||
| abuseReports: "通報" | ||||
|   | ||||
| @@ -4,6 +4,7 @@ block vars | ||||
| 	- const user = note.user; | ||||
| 	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; | ||||
| 	- const url = `${config.url}/notes/${note.id}`; | ||||
| 	- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; | ||||
|  | ||||
| block title | ||||
| 	= `${title} | ${instanceName}` | ||||
| @@ -19,7 +20,7 @@ block og | ||||
| 	meta(property='og:image'       content= user.avatarUrl) | ||||
|  | ||||
| block meta | ||||
| 	if user.host || profile.noCrawle | ||||
| 	if user.host || isRenote || profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
|   | ||||
| @@ -3,10 +3,33 @@ import config from '@/config/index'; | ||||
| import { SwSubscriptions } from '@/models/index'; | ||||
| import { fetchMeta } from '@/misc/fetch-meta'; | ||||
| import { Packed } from '@/misc/schema'; | ||||
| import { getNoteSummary } from '@/misc/get-note-summary'; | ||||
|  | ||||
| type notificationType = 'notification' | 'unreadMessagingMessage'; | ||||
| type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>; | ||||
|  | ||||
| // プッシュメッセージサーバーには文字数制限があるため、内容を削減します | ||||
| function truncateNotification(notification: Packed<'Notification'>): any { | ||||
| 	if (notification.note) { | ||||
| 		return { | ||||
| 			...notification, | ||||
| 			note: { | ||||
| 				...notification.note, | ||||
| 				// textをgetNoteSummaryしたものに置き換える | ||||
| 				text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), | ||||
| 				...{ | ||||
| 					cw: undefined, | ||||
| 					reply: undefined, | ||||
| 					renote: undefined, | ||||
| 					user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	return notification; | ||||
| } | ||||
|  | ||||
| export default async function(userId: string, type: notificationType, body: notificationBody) { | ||||
| 	const meta = await fetchMeta(); | ||||
|  | ||||
| @@ -32,7 +55,9 @@ export default async function(userId: string, type: notificationType, body: noti | ||||
| 		}; | ||||
|  | ||||
| 		push.sendNotification(pushSubscription, JSON.stringify({ | ||||
| 			type, body, | ||||
| 			type, | ||||
| 			body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body, | ||||
| 			userId, | ||||
| 		}), { | ||||
| 			proxy: config.proxy, | ||||
| 		}).catch((err: any) => { | ||||
|   | ||||
| @@ -106,11 +106,6 @@ export default defineComponent({ | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if (this.to.startsWith('/my/messaging')) { | ||||
| 				if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window(); | ||||
| 				if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout(); | ||||
| 			} | ||||
|  | ||||
| 			if (this.behavior) { | ||||
| 				if (this.behavior === 'window') { | ||||
| 					return this.window(); | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| <template> | ||||
| <component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" | ||||
| 	@mouseover="onMouseover" | ||||
| 	@mouseleave="onMouseleave" | ||||
| <component :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" | ||||
| 	@contextmenu.stop="() => {}" | ||||
| > | ||||
| 	<template v-if="!self"> | ||||
| @@ -20,11 +18,11 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { defineComponent, ref } from 'vue'; | ||||
| import { toUnicode as decodePunycode } from 'punycode/'; | ||||
| import { url as local } from '@/config'; | ||||
| import { isTouchUsing } from '@/scripts/touch'; | ||||
| import * as os from '@/os'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
|  | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| @@ -35,74 +33,36 @@ export default defineComponent({ | ||||
| 		rel: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		const self = this.url.startsWith(local); | ||||
| 	setup(props) { | ||||
| 		const self = props.url.startsWith(local); | ||||
| 		const url = new URL(props.url); | ||||
| 		const el = ref(); | ||||
| 		 | ||||
| 		useTooltip(el, (showing) => { | ||||
| 			os.popup(import('@/components/url-preview-popup.vue'), { | ||||
| 				showing, | ||||
| 				url: props.url, | ||||
| 				source: el.value, | ||||
| 			}, {}, 'closed'); | ||||
| 		}); | ||||
|  | ||||
| 		return { | ||||
| 			local, | ||||
| 			schema: null as string | null, | ||||
| 			hostname: null as string | null, | ||||
| 			port: null as string | null, | ||||
| 			pathname: null as string | null, | ||||
| 			query: null as string | null, | ||||
| 			hash: null as string | null, | ||||
| 			schema: url.protocol, | ||||
| 			hostname: decodePunycode(url.hostname), | ||||
| 			port: url.port, | ||||
| 			pathname: decodeURIComponent(url.pathname), | ||||
| 			query: decodeURIComponent(url.search), | ||||
| 			hash: decodeURIComponent(url.hash), | ||||
| 			self: self, | ||||
| 			attr: self ? 'to' : 'href', | ||||
| 			target: self ? null : '_blank', | ||||
| 			showTimer: null, | ||||
| 			hideTimer: null, | ||||
| 			checkTimer: null, | ||||
| 			close: null, | ||||
| 			el, | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		const url = new URL(this.url); | ||||
| 		this.schema = url.protocol; | ||||
| 		this.hostname = decodePunycode(url.hostname); | ||||
| 		this.port = url.port; | ||||
| 		this.pathname = decodeURIComponent(url.pathname); | ||||
| 		this.query = decodeURIComponent(url.search); | ||||
| 		this.hash = decodeURIComponent(url.hash); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		async showPreview() { | ||||
| 			if (!document.body.contains(this.$el)) return; | ||||
| 			if (this.close) return; | ||||
|  | ||||
| 			const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), { | ||||
| 				url: this.url, | ||||
| 				source: this.$el | ||||
| 			}); | ||||
|  | ||||
| 			this.close = () => { | ||||
| 				dispose(); | ||||
| 			}; | ||||
|  | ||||
| 			this.checkTimer = setInterval(() => { | ||||
| 				if (!document.body.contains(this.$el)) this.closePreview(); | ||||
| 			}, 1000); | ||||
| 		}, | ||||
| 		closePreview() { | ||||
| 			if (this.close) { | ||||
| 				clearInterval(this.checkTimer); | ||||
| 				this.close(); | ||||
| 				this.close = null; | ||||
| 			} | ||||
| 		}, | ||||
| 		onMouseover() { | ||||
| 			if (isTouchUsing) return; | ||||
| 			clearTimeout(this.showTimer); | ||||
| 			clearTimeout(this.hideTimer); | ||||
| 			this.showTimer = setTimeout(this.showPreview, 500); | ||||
| 		}, | ||||
| 		onMouseleave() { | ||||
| 			if (isTouchUsing) return; | ||||
| 			clearTimeout(this.showTimer); | ||||
| 			clearTimeout(this.hideTimer); | ||||
| 			this.hideTimer = setTimeout(this.closePreview, 500); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -105,6 +105,7 @@ export default defineComponent({ | ||||
| 		return { | ||||
| 			previewable, | ||||
| 			gallery, | ||||
| 			pswpZIndex: os.claimZIndex('middle'), | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| @@ -188,3 +189,11 @@ export default defineComponent({ | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss"> | ||||
| .pswp { | ||||
| 	// なぜか機能しない | ||||
|   //z-index: v-bind(pswpZIndex); | ||||
| 	z-index: 2000000; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -16,7 +16,13 @@ | ||||
| 	<template #headerLeft> | ||||
| 		<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button> | ||||
| 	</template> | ||||
| 	<div class="yrolvcoq"> | ||||
| 	<template #headerRight> | ||||
| 		<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button> | ||||
| 		<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button> | ||||
| 		<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button> | ||||
| 	</template> | ||||
|  | ||||
| 	<div class="yrolvcoq" :style="{ background: pageInfo?.bg }"> | ||||
| 		<MkStickyContainer> | ||||
| 			<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> | ||||
| 			<component :is="component" v-bind="props" :ref="changePage"/> | ||||
| @@ -33,6 +39,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import { resolve } from '@/router'; | ||||
| import { url } from '@/config'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as os from '@/os'; | ||||
|  | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| @@ -139,6 +146,23 @@ export default defineComponent({ | ||||
| 			this.props = props; | ||||
| 		}, | ||||
|  | ||||
| 		menu(ev) { | ||||
| 			os.popupMenu([{ | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.openInNewTab, | ||||
| 				action: () => { | ||||
| 					window.open(this.url, '_blank'); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: 'fas fa-link', | ||||
| 				text: this.$ts.copyLink, | ||||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				} | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 		}, | ||||
|  | ||||
| 		back() { | ||||
| 			this.navigate(this.history.pop(), false); | ||||
| 		}, | ||||
|   | ||||
| @@ -284,7 +284,7 @@ export default defineComponent({ | ||||
| 	} | ||||
|  | ||||
| 	&.asDrawer { | ||||
| 		padding: 12px 0; | ||||
| 		padding: 12px 0 calc(env(safe-area-inset-bottom, 0px) + 12px) 0; | ||||
| 		width: 100%; | ||||
|  | ||||
| 		> .item { | ||||
|   | ||||
| @@ -5,7 +5,12 @@ | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
|  | ||||
| 	<div v-else-if="empty" key="_empty_" class="empty"> | ||||
| 		<slot name="empty"></slot> | ||||
| 		<slot name="empty"> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 				<div>{{ $ts.nothing }}</div> | ||||
| 			</div> | ||||
| 		</slot> | ||||
| 	</div> | ||||
|  | ||||
| 	<div v-else class="cxiknjgy"> | ||||
|   | ||||
| @@ -414,6 +414,10 @@ export default defineComponent({ | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			> .left { | ||||
| 				min-width: 16px; | ||||
| 			} | ||||
|  | ||||
| 			> .title { | ||||
| 				flex: 1; | ||||
| 				position: relative; | ||||
| @@ -421,7 +425,6 @@ export default defineComponent({ | ||||
| 				white-space: nowrap; | ||||
| 				overflow: hidden; | ||||
| 				text-overflow: ellipsis; | ||||
| 				text-align: center; | ||||
| 				cursor: move; | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -1,28 +1,26 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkInput v-model="name"> | ||||
| 				<template #label>{{ $ts.name }}</template> | ||||
| 			</MkInput> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div class="_formRoot"> | ||||
| 		<MkInput v-model="name" class="_formBlock"> | ||||
| 			<template #label>{{ $ts.name }}</template> | ||||
| 		</MkInput> | ||||
|  | ||||
| 			<MkTextarea v-model="description"> | ||||
| 				<template #label>{{ $ts.description }}</template> | ||||
| 			</MkTextarea> | ||||
| 		<MkTextarea v-model="description" class="_formBlock"> | ||||
| 			<template #label>{{ $ts.description }}</template> | ||||
| 		</MkTextarea> | ||||
|  | ||||
| 			<div class="banner"> | ||||
| 				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> | ||||
| 				<div v-else-if="bannerUrl"> | ||||
| 					<img :src="bannerUrl" style="width: 100%;"/> | ||||
| 					<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> | ||||
| 				</div> | ||||
| 		<div class="banner"> | ||||
| 			<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> | ||||
| 			<div v-else-if="bannerUrl"> | ||||
| 				<img :src="bannerUrl" style="width: 100%;"/> | ||||
| 				<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_footer"> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </MkSpacer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -51,9 +49,11 @@ export default defineComponent({ | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.channelId ? { | ||||
| 				title: this.$ts._channel.edit, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 			} : { | ||||
| 				title: this.$ts._channel.create, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}), | ||||
| 			channel: null, | ||||
| 			name: null, | ||||
|   | ||||
| @@ -1,29 +1,31 @@ | ||||
| <template> | ||||
| <div v-if="channel" class="_section"> | ||||
| 	<div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }"> | ||||
| 		<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | ||||
| 		<button class="_button toggle" @click="() => showBanner = !showBanner"> | ||||
| 			<template v-if="showBanner"><i class="fas fa-angle-up"></i></template> | ||||
| 			<template v-else><i class="fas fa-angle-down"></i></template> | ||||
| 		</button> | ||||
| 		<div v-if="!showBanner" class="hideOverlay"> | ||||
| 		</div> | ||||
| 		<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> | ||||
| 			<div class="status"> | ||||
| 				<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 				<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="channel"> | ||||
| 		<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> | ||||
| 			<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | ||||
| 			<button class="_button toggle" @click="() => showBanner = !showBanner"> | ||||
| 				<template v-if="showBanner"><i class="fas fa-angle-up"></i></template> | ||||
| 				<template v-else><i class="fas fa-angle-down"></i></template> | ||||
| 			</button> | ||||
| 			<div v-if="!showBanner" class="hideOverlay"> | ||||
| 			</div> | ||||
| 			<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> | ||||
| 				<div class="status"> | ||||
| 					<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 					<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| 				</div> | ||||
| 				<div class="fade"></div> | ||||
| 			</div> | ||||
| 			<div v-if="channel.description" class="description"> | ||||
| 				<Mfm :text="channel.description" :is-note="false" :i="$i"/> | ||||
| 			</div> | ||||
| 			<div class="fade"></div> | ||||
| 		</div> | ||||
| 		<div v-if="channel.description" class="description"> | ||||
| 			<Mfm :text="channel.description" :is-note="false" :i="$i"/> | ||||
| 		</div> | ||||
|  | ||||
| 		<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/> | ||||
|  | ||||
| 		<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> | ||||
| 	</div> | ||||
|  | ||||
| 	<XPostForm v-if="$i" :channel="channel" class="post-form _content _panel _gap" fixed/> | ||||
|  | ||||
| 	<XTimeline :key="channelId" class="_content _gap" src="channel" :channel="channelId" @before="before" @after="after"/> | ||||
| </div> | ||||
| </MkSpacer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -55,6 +57,12 @@ export default defineComponent({ | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.channel ? { | ||||
| 				title: this.channel.name, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [...(this.$i && this.$i.id === this.channel.userId ? [{ | ||||
| 					icon: 'fas fa-cog', | ||||
| 					text: this.$ts.edit, | ||||
| 					handler: this.edit, | ||||
| 				}] : [])], | ||||
| 			} : null), | ||||
| 			channel: null, | ||||
| 			showBanner: true, | ||||
| @@ -79,8 +87,10 @@ export default defineComponent({ | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
|  | ||||
| 	methods: { | ||||
| 		edit() { | ||||
| 			this.$router.push(`/channels/${this.channel.id}/edit`); | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,58 +1,63 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<div v-if="$i" class="_section" style="padding: 0;"> | ||||
| 		<MkTab v-model="tab" class="_content"> | ||||
| 			<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option> | ||||
| 			<option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option> | ||||
| 			<option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option> | ||||
| 		</MkTab> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
|  | ||||
| 	<div class="_section"> | ||||
| 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
|  | ||||
| 		<div v-if="tab === 'following'" class="_content grwlizim following"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
|  | ||||
| 		<div v-if="tab === 'owned'" class="_content grwlizim owned"> | ||||
| 			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 			<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	<div v-else-if="tab === 'following'" class="_content grwlizim following"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </div> | ||||
| 	<div v-else-if="tab === 'owned'" class="_content grwlizim owned"> | ||||
| 		<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 		<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| import MkChannelPreview from '@/components/channel-preview.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkTab from '@/components/tab.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
|  | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkChannelPreview, MkPagination, MkButton, MkTab | ||||
| 		MkChannelPreview, MkPagination, MkButton, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.channel, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				action: { | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-plus', | ||||
| 					handler: this.create | ||||
| 				} | ||||
| 			}, | ||||
| 					text: this.$ts.create, | ||||
| 					handler: this.create, | ||||
| 				}], | ||||
| 				tabs: [{ | ||||
| 					active: this.tab === 'featured', | ||||
| 					title: this.$ts._channel.featured, | ||||
| 					icon: 'fas fa-fire-alt', | ||||
| 					onClick: () => { this.tab = 'featured'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'following', | ||||
| 					title: this.$ts._channel.following, | ||||
| 					icon: 'fas fa-heart', | ||||
| 					onClick: () => { this.tab = 'following'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'owned', | ||||
| 					title: this.$ts._channel.owned, | ||||
| 					icon: 'fas fa-edit', | ||||
| 					onClick: () => { this.tab = 'owned'; }, | ||||
| 				},] | ||||
| 			})), | ||||
| 			tab: 'featured', | ||||
| 			featuredPagination: { | ||||
| 				endpoint: 'channels/featured', | ||||
|   | ||||
| @@ -77,13 +77,6 @@ | ||||
| 		<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch> | ||||
| 	</FormGroup> | ||||
|  | ||||
| 	<FormSelect v-model="chatOpenBehavior" class="_formBlock"> | ||||
| 		<template #label>{{ $ts.chatOpenBehavior }}</template> | ||||
| 		<option value="page">{{ $ts.showInPage }}</option> | ||||
| 		<option value="window">{{ $ts.openInWindow }}</option> | ||||
| 		<option value="popout">{{ $ts.popout }}</option> | ||||
| 	</FormSelect> | ||||
|  | ||||
| 	<FormLink to="/settings/deck" class="_formBlock">{{ $ts.deck }}</FormLink> | ||||
|  | ||||
| 	<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink> | ||||
| @@ -149,7 +142,6 @@ export default defineComponent({ | ||||
| 		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), | ||||
| 		showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'), | ||||
| 		defaultSideView: defaultStore.makeGetterSetter('defaultSideView'), | ||||
| 		chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'), | ||||
| 		instanceTicker: defaultStore.makeGetterSetter('instanceTicker'), | ||||
| 		enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'), | ||||
| 		useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'), | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Ref, ref, watch } from 'vue'; | ||||
| import { Ref, ref, watch, onUnmounted } from 'vue'; | ||||
|  | ||||
| export function useTooltip( | ||||
| 	elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>, | ||||
| @@ -18,6 +18,9 @@ export function useTooltip( | ||||
| 	const open = () => { | ||||
| 		close(); | ||||
| 		if (!isHovering) return; | ||||
| 		if (elRef.value == null) return; | ||||
| 		const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; | ||||
| 		if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため | ||||
|  | ||||
| 		const showing = ref(true); | ||||
| 		onShow(showing); | ||||
| @@ -69,9 +72,14 @@ export function useTooltip( | ||||
| 			el.addEventListener('mouseleave', onMouseleave, { passive: true }); | ||||
| 			el.addEventListener('touchstart', onTouchstart, { passive: true }); | ||||
| 			el.addEventListener('touchend', onTouchend, { passive: true }); | ||||
| 			el.addEventListener('click', close, { passive: true }); | ||||
| 		} | ||||
| 	}, { | ||||
| 		immediate: true, | ||||
| 		flush: 'post', | ||||
| 	}); | ||||
|  | ||||
| 	onUnmounted(() => { | ||||
| 		close(); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -245,7 +245,6 @@ export class ColdDeviceStorage { | ||||
| 		lightTheme: require('@/themes/l-light.json5') as Theme, | ||||
| 		darkTheme: require('@/themes/d-dark.json5') as Theme, | ||||
| 		syncDeviceDarkMode: true, | ||||
| 		chatOpenBehavior: 'page' as 'page' | 'window' | 'popout', | ||||
| 		plugins: [] as Plugin[], | ||||
| 		mediaVolume: 0.5, | ||||
| 		sound_masterVolume: 0.3, | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
|  */ | ||||
| declare var self: ServiceWorkerGlobalScope; | ||||
|  | ||||
| import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||
| import * as misskey from 'misskey-js'; | ||||
|  | ||||
| function getUserName(user: misskey.entities.User): string { | ||||
| @@ -26,37 +25,37 @@ export default async function(type, data, i18n): Promise<[string, NotificationOp | ||||
| 			switch (data.type) { | ||||
| 				case 'mention': | ||||
| 					return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
| 				case 'reply': | ||||
| 					return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
| 				case 'renote': | ||||
| 					return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
| 				case 'quote': | ||||
| 					return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
| 				case 'reaction': | ||||
| 					return [`${data.reaction} ${getUserName(data.user)}`, { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
| 				case 'pollVote': | ||||
| 					return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), { | ||||
| 						body: getNoteSummary(data.note, i18n.locale), | ||||
| 						body: data.note.text, | ||||
| 						icon: data.user.avatarUrl | ||||
| 					}]; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina