Compare commits
	
		
			24 Commits
		
	
	
		
			13.0.0-bet
			...
			13.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1f6a41cea7 | ||
|   | 0d7ee20a77 | ||
|   | dcca2350dd | ||
|   | 1cfdd4c41a | ||
|   | 25f4ee7030 | ||
|   | 5320f23017 | ||
|   | 4ffbbbe6d8 | ||
|   | 132e45dff4 | ||
|   | 01652b72b3 | ||
|   | 8b1fdb5a3b | ||
|   | 192add376c | ||
|   | 244ea9593a | ||
|   | f20d7cba74 | ||
|   | a3e282bc75 | ||
|   | 49a95c34bf | ||
|   | ecbefce2aa | ||
|   | 91356b1805 | ||
|   | 2e2ed1385f | ||
|   | 49f3090edd | ||
|   | 4594fb11de | ||
|   | b93e56d2e5 | ||
|   | c550dafb81 | ||
|   | 8709574f3d | ||
|   | 1b7043fa79 | 
| @@ -30,6 +30,7 @@ You should also include the user name that made the change. | ||||
|  | ||||
| #### For users | ||||
| - ノートのウォッチ機能が削除されました | ||||
| - アンケートに投票された際に通知が作成されなくなりました | ||||
| - 新たに動的なPagesを作ることはできなくなりました | ||||
| 	- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。 | ||||
| - AiScriptが0.12.2にアップデートされました | ||||
| @@ -77,9 +78,12 @@ You should also include the user name that made the change. | ||||
| - Client: Improve RSS widget @tamaina | ||||
| - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz | ||||
| - Client: OpenSearch support @SoniEx2 @chaoticryptidz | ||||
| - Client: Support remote objects in search @SoniEx2 | ||||
| - Client: user activity page @syuilo | ||||
| - Client: add user list widget @syuilo | ||||
| - Client: add heatmap of daily active users to about page @syuilo | ||||
| - Client: introduce fluent emoji @syuilo | ||||
| - Client: add new theme @syuilo | ||||
| - Client: show fireworks when visit user who today is birthday @syuilo | ||||
| - Client: show bot warning on screen when logged in as bot account @syuilo | ||||
| - Client: improve overall performance of client @syuilo | ||||
|   | ||||
| @@ -920,6 +920,10 @@ like: "Gefällt mir" | ||||
| unlike: "\"Gefällt mir\" entfernen" | ||||
| numberOfLikes: "\"Gefällt mir\"-Anzahl" | ||||
| show: "Anzeigen" | ||||
| neverShow: "Nicht wieder anzeigen" | ||||
| remindMeLater: "Vielleicht später" | ||||
| didYouLikeMisskey: "Gefällt dir Misskey?" | ||||
| pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." | ||||
|   sensitivity: "Erkennungssensitivität" | ||||
|   | ||||
| @@ -920,6 +920,10 @@ like: "Like" | ||||
| unlike: "Unlike" | ||||
| numberOfLikes: "Likes" | ||||
| show: "Show" | ||||
| neverShow: "Don't show again" | ||||
| remindMeLater: "Maybe later" | ||||
| didYouLikeMisskey: "Have you taken a liking to Misskey?" | ||||
| pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." | ||||
|   sensitivity: "Detection sensitivity" | ||||
|   | ||||
| @@ -28,7 +28,7 @@ timeline: "Timeline" | ||||
| noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." | ||||
| login: "Accedi" | ||||
| loggingIn: "Accesso in corso..." | ||||
| logout: "Esci" | ||||
| logout: "Uscita" | ||||
| signup: "Iscriviti" | ||||
| uploading: "Caricamento..." | ||||
| save: "Salva" | ||||
|   | ||||
| @@ -1550,7 +1550,6 @@ _notification: | ||||
|   youGotReply: "{name}からのリプライ" | ||||
|   youGotQuote: "{name}による引用" | ||||
|   youRenoted: "{name}がRenoteしました" | ||||
|   youGotPoll: "{name}が投票しました" | ||||
|   youGotMessagingMessageFromUser: "{name}からのチャットがあります" | ||||
|   youGotMessagingMessageFromGroup: "{name}のチャットがあります" | ||||
|   youWereFollowed: "フォローされました" | ||||
| @@ -1569,7 +1568,6 @@ _notification: | ||||
|     renote: "Renote" | ||||
|     quote: "引用" | ||||
|     reaction: "リアクション" | ||||
|     pollVote: "アンケートに投票された" | ||||
|     pollEnded: "アンケートが終了" | ||||
|     receiveFollowRequest: "フォロー申請を受け取った" | ||||
|     followRequestAccepted: "フォローが受理された" | ||||
|   | ||||
| @@ -920,6 +920,10 @@ like: "좋아요!" | ||||
| unlike: "좋아요 취소" | ||||
| numberOfLikes: "좋아요 수" | ||||
| show: "표시" | ||||
| neverShow: "다시 보지 않기" | ||||
| remindMeLater: "나중에 알림" | ||||
| didYouLikeMisskey: "Misskey가 마음에 드시나요?" | ||||
| pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." | ||||
|   sensitivity: "탐지 민감도" | ||||
|   | ||||
| @@ -913,6 +913,10 @@ tools: "Nástroje" | ||||
| cannotLoad: "Nedá sa načítať." | ||||
| like: "Páči sa mi" | ||||
| show: "Zobraziť" | ||||
| neverShow: "Nabudúce nezobrazovať" | ||||
| remindMeLater: "Pripomenúť neskôr" | ||||
| didYouLikeMisskey: "Páči sa vám Misskey?" | ||||
| pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera." | ||||
|   sensitivity: "Citlivosť detekcie" | ||||
|   | ||||
| @@ -917,6 +917,8 @@ tools: "เครื่องมือ" | ||||
| cannotLoad: "ไม่สามารถโหลดได้" | ||||
| numberOfProfileView: "มุมมองโปรไฟล์" | ||||
| like: "ชื่นชอบ" | ||||
| unlike: "ไม่ชอบ" | ||||
| numberOfLikes: "จำนวนไลค์" | ||||
| show: "แสดงผล" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" | ||||
| @@ -1317,6 +1319,7 @@ _widgets: | ||||
|   jobQueue: "คิวงาน" | ||||
|   serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" | ||||
|   aiscript: "AiScript คอนโซล" | ||||
|   aiscriptApp: "AiScript แอพ" | ||||
|   aichan: "เอไอ" | ||||
|   userList: "รายชื่อผู้ใช้" | ||||
|   _userList: | ||||
| @@ -1423,7 +1426,16 @@ _timelines: | ||||
|   social: "โซเชี่ยล" | ||||
|   global: "ทั่วโลก" | ||||
| _play: | ||||
|   new: "สร้างการเล่น" | ||||
|   edit: "แก้ไขเล่น" | ||||
|   created: "สร้างการเล่นแล้ว" | ||||
|   updated: "แก้ไขการเล่นแล้ว" | ||||
|   deleted: "ลบการเล่นแล้ว" | ||||
|   pageSetting: "ตั้งค่าการเล่น" | ||||
|   editThisPage: "แก้ไข Play นี้" | ||||
|   viewSource: "ดูต้นฉบับ" | ||||
|   my: "มาย เพลย์" | ||||
|   liked: "ไลค์ เพลย์" | ||||
|   featured: "เป็นที่นิยม" | ||||
|   title: "หัวข้อ" | ||||
|   script: "สคริปต์" | ||||
|   | ||||
| @@ -918,7 +918,11 @@ cannotLoad: "無法載入" | ||||
| numberOfProfileView: "個人檔案檢視次數" | ||||
| like: "讚" | ||||
| unlike: "收回讚" | ||||
| numberOfLikes: "讚數" | ||||
| show: "檢視" | ||||
| neverShow: "不再顯示" | ||||
| remindMeLater: "以後再說" | ||||
| didYouLikeMisskey: "您是否喜愛Misskey呢?" | ||||
| _sensitiveMediaDetection: | ||||
|   description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" | ||||
|   sensitivity: "檢測敏感度" | ||||
| @@ -1318,6 +1322,7 @@ _widgets: | ||||
|   jobQueue: "佇列" | ||||
|   serverMetric: "服務器指標 " | ||||
|   aiscript: "AiScript控制台" | ||||
|   aiscriptApp: "AiScript App" | ||||
|   aichan: "小藍" | ||||
|   userList: "使用者列表" | ||||
|   _userList: | ||||
| @@ -1424,7 +1429,16 @@ _timelines: | ||||
|   social: "社群" | ||||
|   global: "公開" | ||||
| _play: | ||||
|   new: "新增Play" | ||||
|   edit: "編輯Play" | ||||
|   created: "已新增Play" | ||||
|   updated: "已更新Play" | ||||
|   deleted: "已刪除Play" | ||||
|   pageSetting: "Play設定" | ||||
|   editThisPage: "編輯這個Play" | ||||
|   viewSource: "檢視原始碼" | ||||
|   my: "自己的Play" | ||||
|   liked: "按了讚的Play" | ||||
|   featured: "人氣" | ||||
|   title: "標題" | ||||
|   script: "腳本" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "13.0.0-beta.27", | ||||
| 	"version": "13.0.0-beta.30", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|   | ||||
| @@ -92,13 +92,6 @@ export class PollService { | ||||
| 			choice: choice, | ||||
| 			userId: user.id, | ||||
| 		}); | ||||
| 	 | ||||
| 		// Notify | ||||
| 		this.createNotificationService.createNotification(note.userId, 'pollVote', { | ||||
| 			notifierId: user.id, | ||||
| 			noteId: note.id, | ||||
| 			choice: choice, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
|   | ||||
| @@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit { | ||||
| 				}), | ||||
| 				reaction: notification.reaction, | ||||
| 			} : {}), | ||||
| 			...(notification.type === 'pollVote' ? { | ||||
| 			...(notification.type === 'pollVote' ? { // TODO: そのうち消す | ||||
| 				note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { | ||||
| 					detail: true, | ||||
| 					_hint_: options._hintForEachNotes_, | ||||
|   | ||||
| @@ -55,11 +55,11 @@ export class Notification { | ||||
| 	 * 通知の種類。 | ||||
| 	 * follow - フォローされた | ||||
| 	 * mention - 投稿で自分が言及された | ||||
| 	 * reply - (自分または自分がWatchしている)投稿が返信された | ||||
| 	 * renote - (自分または自分がWatchしている)投稿がRenoteされた | ||||
| 	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた | ||||
| 	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた | ||||
| 	 * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された | ||||
| 	 * reply - 投稿に返信された | ||||
| 	 * renote - 投稿がRenoteされた | ||||
| 	 * quote - 投稿が引用Renoteされた | ||||
| 	 * reaction - 投稿にリアクションされた | ||||
| 	 * pollVote - 投稿のアンケートに投票された (廃止) | ||||
| 	 * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した | ||||
| 	 * receiveFollowRequest - フォローリクエストされた | ||||
| 	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された | ||||
|   | ||||
| @@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 				userId: me.id, | ||||
| 			}); | ||||
|  | ||||
| 			// Notify | ||||
| 			this.createNotificationService.createNotification(note.userId, 'pollVote', { | ||||
| 				notifierId: me.id, | ||||
| 				noteId: note.id, | ||||
| 				choice: ps.choice, | ||||
| 			}); | ||||
|  | ||||
| 			// リモート投票の場合リプライ送信 | ||||
| 			if (note.userHost != null) { | ||||
| 				const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; | ||||
|   | ||||
| @@ -74,7 +74,7 @@ function onMousedown(evt: Event) { | ||||
| } | ||||
|  | ||||
| .fade-enter-active, .fade-leave-active { | ||||
| 	transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); | ||||
| 	transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); | ||||
| 	transform-origin: left top; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="ssazuxis"> | ||||
| 	<header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<div class="title"><div><slot name="header"></slot></div></div> | ||||
| 		<div class="divider"></div> | ||||
| 		<button class="_button"> | ||||
| 			<template v-if="showBody"><i class="ti ti-chevron-up"></i></template> | ||||
| @@ -127,14 +127,6 @@ export default defineComponent({ | ||||
| 			place-content: center; | ||||
| 			margin: 0; | ||||
| 			padding: 12px 16px 12px 0; | ||||
|  | ||||
| 			> i { | ||||
| 				margin-right: 6px; | ||||
| 			} | ||||
|  | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .divider { | ||||
|   | ||||
| @@ -251,17 +251,18 @@ onBeforeUnmount(() => { | ||||
| 				color: #fff; | ||||
|  | ||||
| 				&:before { | ||||
| 					background: #d42e2e; | ||||
| 					background: #d42e2e !important; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		&:active, | ||||
| 		&.active { | ||||
| 			color: var(--fgOnAccent); | ||||
| 			color: var(--fgOnAccent) !important; | ||||
| 			opacity: 1; | ||||
|  | ||||
| 			&:before { | ||||
| 				background: var(--accent); | ||||
| 				background: var(--accent) !important; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> | ||||
| 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> | ||||
| 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> | ||||
| 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> | ||||
| 		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> | ||||
| 			<slot :max-height="maxHeight" :type="type"></slot> | ||||
| 		</div> | ||||
| @@ -63,6 +63,7 @@ let transformOrigin = $ref('center'); | ||||
| let showing = $ref(true); | ||||
| let content = $shallowRef<HTMLElement>(); | ||||
| const zIndex = os.claimZIndex(props.zPriority); | ||||
| let useSendAnime = $ref(false); | ||||
| const type = $computed<ModalTypes>(() => { | ||||
| 	if (props.preferType === 'auto') { | ||||
| 		if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { | ||||
| @@ -74,15 +75,34 @@ const type = $computed<ModalTypes>(() => { | ||||
| 		return props.preferType!; | ||||
| 	} | ||||
| }); | ||||
| let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''); | ||||
| let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0); | ||||
| let transitionName = $computed((() => | ||||
| 	defaultStore.state.animation | ||||
| 		? useSendAnime | ||||
| 			? 'send' | ||||
| 			: type === 'drawer' | ||||
| 				? 'modal-drawer' | ||||
| 				: type === 'popup' | ||||
| 					? 'modal-popup' | ||||
| 					: 'modal' | ||||
| 		: '' | ||||
| )); | ||||
| let transitionDuration = $computed((() => | ||||
| 	transitionName === 'send' | ||||
| 		? 400 | ||||
| 		: transitionName === 'modal-popup' | ||||
| 			? 100 | ||||
| 			: transitionName === 'modal' | ||||
| 				? 200 | ||||
| 				: transitionName === 'modal-drawer' | ||||
| 					? 200 | ||||
| 					: 0 | ||||
| )); | ||||
|  | ||||
| let contentClicking = false; | ||||
|  | ||||
| function close(opts: { useSendAnimation?: boolean } = {}) { | ||||
| 	if (opts.useSendAnimation) { | ||||
| 		transitionName = 'send'; | ||||
| 		transitionDuration = 400; | ||||
| 		useSendAnime = true; | ||||
| 	} | ||||
|  | ||||
| 	// eslint-disable-next-line vue/no-mutating-props | ||||
| @@ -308,12 +328,12 @@ defineExpose({ | ||||
|  | ||||
| .modal-popup-enter-active, .modal-popup-leave-active { | ||||
| 	> .bg { | ||||
| 		transition: opacity 0.2s !important; | ||||
| 		transition: opacity 0.1s !important; | ||||
| 	} | ||||
|  | ||||
| 	> .content { | ||||
| 		transform-origin: var(--transformOrigin); | ||||
| 		transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important; | ||||
| 		transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; | ||||
| 	} | ||||
| } | ||||
| .modal-popup-enter-from, .modal-popup-leave-to { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
| 	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div> | ||||
| 	<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div> | ||||
| 	<div v-if="isRenote" class="renote"> | ||||
| 		<MkAvatar class="avatar" :user="note.user"/> | ||||
| 		<MkAvatar v-once class="avatar" :user="note.user"/> | ||||
| 		<i class="ti ti-repeat"></i> | ||||
| 		<I18n :src="i18n.ts.renotedBy" tag="span"> | ||||
| 			<template #user> | ||||
| @@ -27,11 +27,16 @@ | ||||
| 				<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> | ||||
| 				<MkTime :time="note.createdAt"/> | ||||
| 			</button> | ||||
| 			<MkVisibility :note="note"/> | ||||
| 			<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> | ||||
| 				<i v-if="note.visibility === 'home'" class="ti ti-home"></i> | ||||
| 				<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> | ||||
| 				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> | ||||
| 			</span> | ||||
| 			<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<article class="article" @contextmenu.stop="onContextmenu"> | ||||
| 		<MkAvatar class="avatar" :user="appearNote.user"/> | ||||
| 		<MkAvatar v-once class="avatar" :user="appearNote.user"/> | ||||
| 		<div class="main"> | ||||
| 			<MkNoteHeader class="header" :note="appearNote" :mini="true"/> | ||||
| 			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> | ||||
| @@ -44,7 +49,7 @@ | ||||
| 					<div class="text"> | ||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||
| 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||
| 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/> | ||||
| 						<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/> | ||||
| 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | ||||
| 						<div v-if="translating || translation" class="translation"> | ||||
| 							<MkLoading v-if="translating" mini/> | ||||
| @@ -75,14 +80,25 @@ | ||||
| 					<i class="ti ti-arrow-back-up"></i> | ||||
| 					<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> | ||||
| 				</button> | ||||
| 				<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> | ||||
| 				<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> | ||||
| 				<button | ||||
| 					v-if="canRenote" | ||||
| 					ref="renoteButton" | ||||
| 					class="button _button" | ||||
| 					@mousedown="renote()" | ||||
| 				> | ||||
| 					<i class="ti ti-repeat"></i> | ||||
| 					<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> | ||||
| 				</button> | ||||
| 				<button v-else class="button _button" disabled> | ||||
| 					<i class="ti ti-ban"></i> | ||||
| 				</button> | ||||
| 				<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> | ||||
| 					<i class="ti ti-plus"></i> | ||||
| 				</button> | ||||
| 				<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> | ||||
| 					<i class="ti ti-minus"></i> | ||||
| 				</button> | ||||
| 				<button ref="menuButton" class="button _button" @click="menu()"> | ||||
| 				<button ref="menuButton" class="button _button" @mousedown="menu()"> | ||||
| 					<i class="ti ti-dots"></i> | ||||
| 				</button> | ||||
| 			</footer> | ||||
| @@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; | ||||
| import MkMediaList from '@/components/MkMediaList.vue'; | ||||
| import MkCwButton from '@/components/MkCwButton.vue'; | ||||
| import MkPoll from '@/components/MkPoll.vue'; | ||||
| import MkRenoteButton from '@/components/MkRenoteButton.vue'; | ||||
| import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; | ||||
| import MkUrlPreview from '@/components/MkUrlPreview.vue'; | ||||
| import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; | ||||
| import MkVisibility from '@/components/MkVisibility.vue'; | ||||
| import { pleaseLogin } from '@/scripts/please-login'; | ||||
| import { focusPrev, focusNext } from '@/scripts/focus'; | ||||
| import { checkWordMute } from '@/scripts/check-word-mute'; | ||||
| @@ -128,6 +143,7 @@ import { i18n } from '@/i18n'; | ||||
| import { getNoteMenu } from '@/scripts/get-note-menu'; | ||||
| import { useNoteCapture } from '@/scripts/use-note-capture'; | ||||
| import { deepClone } from '@/scripts/clone'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| @@ -158,7 +174,7 @@ const isRenote = ( | ||||
|  | ||||
| const el = shallowRef<HTMLElement>(); | ||||
| const menuButton = shallowRef<HTMLElement>(); | ||||
| const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); | ||||
| const renoteButton = shallowRef<HTMLElement>(); | ||||
| const renoteTime = shallowRef<HTMLElement>(); | ||||
| const reactButton = shallowRef<HTMLElement>(); | ||||
| let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); | ||||
| @@ -175,6 +191,7 @@ const translation = ref(null); | ||||
| const translating = ref(false); | ||||
| const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; | ||||
| const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); | ||||
| const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); | ||||
|  | ||||
| const keymap = { | ||||
| 	'r': () => reply(true), | ||||
| @@ -193,6 +210,47 @@ useNoteCapture({ | ||||
| 	isDeletedRef: isDeleted, | ||||
| }); | ||||
|  | ||||
| useTooltip(renoteButton, async (showing) => { | ||||
| 	const renotes = await os.api('notes/renotes', { | ||||
| 		noteId: appearNote.id, | ||||
| 		limit: 11, | ||||
| 	}); | ||||
|  | ||||
| 	const users = renotes.map(x => x.user); | ||||
|  | ||||
| 	if (users.length < 1) return; | ||||
|  | ||||
| 	os.popup(MkUsersTooltip, { | ||||
| 		showing, | ||||
| 		users, | ||||
| 		count: appearNote.renoteCount, | ||||
| 		targetElement: renoteButton.value, | ||||
| 	}, {}, 'closed'); | ||||
| }); | ||||
|  | ||||
| function renote(viaKeyboard = false) { | ||||
| 	pleaseLogin(); | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.renote, | ||||
| 		icon: 'ti ti-repeat', | ||||
| 		action: () => { | ||||
| 			os.api('notes/create', { | ||||
| 				renoteId: appearNote.id, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.quote, | ||||
| 		icon: 'ti ti-quote', | ||||
| 		action: () => { | ||||
| 			os.post({ | ||||
| 				renote: appearNote, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}], renoteButton.value, { | ||||
| 		viaKeyboard, | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function reply(viaKeyboard = false): void { | ||||
| 	pleaseLogin(); | ||||
| 	os.post({ | ||||
|   | ||||
| @@ -25,7 +25,12 @@ | ||||
| 				<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> | ||||
| 				<MkTime :time="note.createdAt"/> | ||||
| 			</button> | ||||
| 			<MkVisibility :note="note"/> | ||||
| 			<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> | ||||
| 				<i v-if="note.visibility === 'home'" class="ti ti-home"></i> | ||||
| 				<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> | ||||
| 				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> | ||||
| 			</span> | ||||
| 			<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<article class="article" @contextmenu.stop="onContextmenu"> | ||||
| @@ -38,7 +43,12 @@ | ||||
| 					</MkA> | ||||
| 					<span v-if="appearNote.user.isBot" class="is-bot">bot</span> | ||||
| 					<div class="info"> | ||||
| 						<MkVisibility :note="appearNote"/> | ||||
| 						<span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]"> | ||||
| 							<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> | ||||
| 							<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i> | ||||
| 							<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> | ||||
| 						</span> | ||||
| 						<span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="username"><MkAcct :user="appearNote.user"/></div> | ||||
| @@ -85,14 +95,25 @@ | ||||
| 					<i class="ti ti-arrow-back-up"></i> | ||||
| 					<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> | ||||
| 				</button> | ||||
| 				<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> | ||||
| 				<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> | ||||
| 				<button | ||||
| 					v-if="canRenote" | ||||
| 					ref="renoteButton" | ||||
| 					class="button _button" | ||||
| 					@mousedown="renote()" | ||||
| 				> | ||||
| 					<i class="ti ti-repeat"></i> | ||||
| 					<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> | ||||
| 				</button> | ||||
| 				<button v-else class="button _button" disabled> | ||||
| 					<i class="ti ti-ban"></i> | ||||
| 				</button> | ||||
| 				<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> | ||||
| 					<i class="ti ti-plus"></i> | ||||
| 				</button> | ||||
| 				<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> | ||||
| 					<i class="ti ti-minus"></i> | ||||
| 				</button> | ||||
| 				<button ref="menuButton" class="button _button" @click="menu()"> | ||||
| 				<button ref="menuButton" class="button _button" @mousedown="menu()"> | ||||
| 					<i class="ti ti-dots"></i> | ||||
| 				</button> | ||||
| 			</footer> | ||||
| @@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; | ||||
| import MkMediaList from '@/components/MkMediaList.vue'; | ||||
| import MkCwButton from '@/components/MkCwButton.vue'; | ||||
| import MkPoll from '@/components/MkPoll.vue'; | ||||
| import MkRenoteButton from '@/components/MkRenoteButton.vue'; | ||||
| import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; | ||||
| import MkUrlPreview from '@/components/MkUrlPreview.vue'; | ||||
| import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; | ||||
| import MkVisibility from '@/components/MkVisibility.vue'; | ||||
| import { pleaseLogin } from '@/scripts/please-login'; | ||||
| import { checkWordMute } from '@/scripts/check-word-mute'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| @@ -138,6 +158,7 @@ import { i18n } from '@/i18n'; | ||||
| import { getNoteMenu } from '@/scripts/get-note-menu'; | ||||
| import { useNoteCapture } from '@/scripts/use-note-capture'; | ||||
| import { deepClone } from '@/scripts/clone'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| @@ -168,7 +189,7 @@ const isRenote = ( | ||||
|  | ||||
| const el = shallowRef<HTMLElement>(); | ||||
| const menuButton = shallowRef<HTMLElement>(); | ||||
| const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); | ||||
| const renoteButton = shallowRef<HTMLElement>(); | ||||
| const renoteTime = shallowRef<HTMLElement>(); | ||||
| const reactButton = shallowRef<HTMLElement>(); | ||||
| let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); | ||||
| @@ -182,6 +203,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n | ||||
| const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); | ||||
| const conversation = ref<misskey.entities.Note[]>([]); | ||||
| const replies = ref<misskey.entities.Note[]>([]); | ||||
| const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); | ||||
|  | ||||
| const keymap = { | ||||
| 	'r': () => reply(true), | ||||
| @@ -198,6 +220,47 @@ useNoteCapture({ | ||||
| 	isDeletedRef: isDeleted, | ||||
| }); | ||||
|  | ||||
| useTooltip(renoteButton, async (showing) => { | ||||
| 	const renotes = await os.api('notes/renotes', { | ||||
| 		noteId: appearNote.id, | ||||
| 		limit: 11, | ||||
| 	}); | ||||
|  | ||||
| 	const users = renotes.map(x => x.user); | ||||
|  | ||||
| 	if (users.length < 1) return; | ||||
|  | ||||
| 	os.popup(MkUsersTooltip, { | ||||
| 		showing, | ||||
| 		users, | ||||
| 		count: appearNote.renoteCount, | ||||
| 		targetElement: renoteButton.value, | ||||
| 	}, {}, 'closed'); | ||||
| }); | ||||
|  | ||||
| function renote(viaKeyboard = false) { | ||||
| 	pleaseLogin(); | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.renote, | ||||
| 		icon: 'ti ti-repeat', | ||||
| 		action: () => { | ||||
| 			os.api('notes/create', { | ||||
| 				renoteId: appearNote.id, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.quote, | ||||
| 		icon: 'ti ti-quote', | ||||
| 		action: () => { | ||||
| 			os.post({ | ||||
| 				renote: appearNote, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}], renoteButton.value, { | ||||
| 		viaKeyboard, | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function reply(viaKeyboard = false): void { | ||||
| 	pleaseLogin(); | ||||
| 	os.post({ | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <header class="kkwtjztg"> | ||||
| 	<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> | ||||
| 	<MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> | ||||
| 		<MkUserName :user="note.user"/> | ||||
| 	</MkA> | ||||
| 	<div v-if="note.user.isBot" class="is-bot">bot</div> | ||||
| @@ -9,7 +9,12 @@ | ||||
| 		<MkA class="created-at" :to="notePage(note)"> | ||||
| 			<MkTime :time="note.createdAt"/> | ||||
| 		</MkA> | ||||
| 		<MkVisibility :note="note"/> | ||||
| 		<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> | ||||
| 			<i v-if="note.visibility === 'home'" class="ti ti-home"></i> | ||||
| 			<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> | ||||
| 			<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> | ||||
| 		</span> | ||||
| 		<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> | ||||
| 	</div> | ||||
| </header> | ||||
| </template> | ||||
| @@ -17,7 +22,7 @@ | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import MkVisibility from '@/components/MkVisibility.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import { userPage } from '@/filters/user'; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div ref="elRef" class="qglefbjs" :class="notification.type"> | ||||
| 	<div class="head"> | ||||
| 	<div v-once class="head"> | ||||
| 		<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/> | ||||
| 		<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/> | ||||
| 		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> | ||||
| @@ -13,10 +13,9 @@ | ||||
| 			<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> | ||||
| 			<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> | ||||
| 			<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> | ||||
| 			<i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i> | ||||
| 			<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> | ||||
| 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> | ||||
| 			<XReactionIcon | ||||
| 			<MkReactionIcon | ||||
| 				v-else-if="notification.type === 'reaction'" | ||||
| 				ref="reactionRef" | ||||
| 				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" | ||||
| @@ -32,42 +31,39 @@ | ||||
| 			<span v-else>{{ notification.header }}</span> | ||||
| 			<MkTime v-if="withTime" :time="notification.createdAt" class="time"/> | ||||
| 		</header> | ||||
| 		<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 		</MkA> | ||||
| 		<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			<i class="ti ti-quote"></i> | ||||
| 		</MkA> | ||||
| 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | ||||
| 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> | ||||
| 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> | ||||
| 		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> | ||||
| 		<span v-if="notification.type === 'app'" class="text"> | ||||
| 			<Mfm :text="notification.body" :nowrap="!full"/> | ||||
| 		</span> | ||||
| 		<div v-once class="content"> | ||||
| 			<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 			</MkA> | ||||
| 			<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 				<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 			</MkA> | ||||
| 			<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			</MkA> | ||||
| 			<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			</MkA> | ||||
| 			<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 			</MkA> | ||||
| 			<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 				<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||
| 				<i class="ti ti-quote"></i> | ||||
| 			</MkA> | ||||
| 			<span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | ||||
| 			<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> | ||||
| 			<span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> | ||||
| 			<span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> | ||||
| 			<span v-else-if="notification.type === 'app'" class="text"> | ||||
| 				<Mfm :text="notification.body" :nowrap="!full"/> | ||||
| 			</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -75,7 +71,7 @@ | ||||
| <script lang="ts" setup> | ||||
| import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkFollowButton from '@/components/MkFollowButton.vue'; | ||||
| import XReactionTooltip from '@/components/MkReactionTooltip.vue'; | ||||
| import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||
| @@ -239,12 +235,6 @@ useTooltip(reactionRef, (showing) => { | ||||
| 				pointer-events: none; | ||||
| 			} | ||||
|  | ||||
| 			&.pollVote { | ||||
| 				padding: 3px; | ||||
| 				background: #88a6b7; | ||||
| 				pointer-events: none; | ||||
| 			} | ||||
|  | ||||
| 			&.pollEnded { | ||||
| 				padding: 3px; | ||||
| 				background: #88a6b7; | ||||
| @@ -275,23 +265,25 @@ useTooltip(reactionRef, (showing) => { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .text { | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 		> .content { | ||||
| 			> .text { | ||||
| 				white-space: nowrap; | ||||
| 				overflow: hidden; | ||||
| 				text-overflow: ellipsis; | ||||
|  | ||||
| 			> i { | ||||
| 				vertical-align: super; | ||||
| 				font-size: 50%; | ||||
| 				opacity: 0.5; | ||||
| 			} | ||||
| 				> i { | ||||
| 					vertical-align: super; | ||||
| 					font-size: 50%; | ||||
| 					opacity: 0.5; | ||||
| 				} | ||||
|  | ||||
| 			> i:first-child { | ||||
| 				margin-right: 4px; | ||||
| 			} | ||||
| 				> i:first-child { | ||||
| 					margin-right: 4px; | ||||
| 				} | ||||
|  | ||||
| 			> i:last-child { | ||||
| 				margin-left: 4px; | ||||
| 				> i:last-child { | ||||
| 					margin-left: 4px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> | ||||
| 	<span class="text" :class="{ up }"> | ||||
| 		<XReactionIcon class="icon" :reaction="reaction"/> | ||||
| 		<MkReactionIcon class="icon" :reaction="reaction"/> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
| @@ -9,7 +9,7 @@ | ||||
| <script lang="ts" setup> | ||||
| import { onMounted } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	reaction: string; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="beeadbfb"> | ||||
| 		<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||
| 		<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||
| 		<div class="name">{{ reaction.replace('@.', '') }}</div> | ||||
| 	</div> | ||||
| </MkTooltip> | ||||
| @@ -10,7 +10,7 @@ | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkTooltip from './MkTooltip.vue'; | ||||
| import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
|  | ||||
| defineProps<{ | ||||
| 	showing: boolean; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="bqxuuuey"> | ||||
| 		<div class="reaction"> | ||||
| 			<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||
| 			<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||
| 			<div class="name">{{ getReactionName(reaction) }}</div> | ||||
| 		</div> | ||||
| 		<div class="users"> | ||||
| @@ -19,7 +19,7 @@ | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkTooltip from './MkTooltip.vue'; | ||||
| import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import { getEmojiName } from '@/scripts/emojilist'; | ||||
|  | ||||
| defineProps<{ | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| <template> | ||||
| <button | ||||
| 	ref="buttonRef" | ||||
| 	ref="buttonEl" | ||||
| 	v-ripple="canToggle" | ||||
| 	class="hkzvhatu _button" | ||||
| 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||
| 	@click="toggleReaction()" | ||||
| > | ||||
| 	<XReactionIcon class="icon" :reaction="reaction"/> | ||||
| 	<MkReactionIcon class="icon" :reaction="reaction"/> | ||||
| 	<span class="count">{{ count }}</span> | ||||
| </button> | ||||
| </template> | ||||
| @@ -15,7 +15,7 @@ | ||||
| import { computed, onMounted, ref, shallowRef, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XDetails from '@/components/MkReactionsViewer.details.vue'; | ||||
| import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import MkReactionIcon from '@/components/MkReactionIcon.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
| import { $i } from '@/account'; | ||||
| @@ -28,7 +28,7 @@ const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| }>(); | ||||
|  | ||||
| const buttonRef = shallowRef<HTMLElement>(); | ||||
| const buttonEl = shallowRef<HTMLElement>(); | ||||
|  | ||||
| const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); | ||||
|  | ||||
| @@ -58,9 +58,9 @@ const toggleReaction = () => { | ||||
| const anime = () => { | ||||
| 	if (document.hidden) return; | ||||
|  | ||||
| 	const rect = buttonRef.value.getBoundingClientRect(); | ||||
| 	const x = rect.left + (buttonRef.value.offsetWidth / 2); | ||||
| 	const y = rect.top + (buttonRef.value.offsetHeight / 2); | ||||
| 	const rect = buttonEl.value.getBoundingClientRect(); | ||||
| 	const x = rect.left + 16; | ||||
| 	const y = rect.top + (buttonEl.value.offsetHeight / 2); | ||||
| 	os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end'); | ||||
| }; | ||||
|  | ||||
| @@ -72,7 +72,7 @@ onMounted(() => { | ||||
| 	if (!props.isInitial) anime(); | ||||
| }); | ||||
|  | ||||
| useTooltip(buttonRef, async (showing) => { | ||||
| useTooltip(buttonEl, async (showing) => { | ||||
| 	const reactions = await os.apiGet('notes/reactions', { | ||||
| 		noteId: props.note.id, | ||||
| 		type: props.reaction, | ||||
| @@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => { | ||||
| 		reaction: props.reaction, | ||||
| 		users, | ||||
| 		count: props.count, | ||||
| 		targetElement: buttonRef.value, | ||||
| 		targetElement: buttonEl.value, | ||||
| 	}, {}, 'closed'); | ||||
| }, 100); | ||||
| </script> | ||||
|   | ||||
| @@ -1,99 +0,0 @@ | ||||
| <template> | ||||
| <button | ||||
| 	v-if="canRenote" | ||||
| 	ref="buttonRef" | ||||
| 	class="eddddedb _button canRenote" | ||||
| 	@click="renote()" | ||||
| > | ||||
| 	<i class="ti ti-repeat"></i> | ||||
| 	<p v-if="count > 0" class="count">{{ count }}</p> | ||||
| </button> | ||||
| <button v-else class="eddddedb _button"> | ||||
| 	<i class="ti ti-ban"></i> | ||||
| </button> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, ref, shallowRef } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XDetails from '@/components/MkUsersTooltip.vue'; | ||||
| import { pleaseLogin } from '@/scripts/please-login'; | ||||
| import * as os from '@/os'; | ||||
| import { $i } from '@/account'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| 	count: number; | ||||
| }>(); | ||||
|  | ||||
| const buttonRef = shallowRef<HTMLElement>(); | ||||
|  | ||||
| const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); | ||||
|  | ||||
| useTooltip(buttonRef, async (showing) => { | ||||
| 	const renotes = await os.api('notes/renotes', { | ||||
| 		noteId: props.note.id, | ||||
| 		limit: 11, | ||||
| 	}); | ||||
|  | ||||
| 	const users = renotes.map(x => x.user); | ||||
|  | ||||
| 	if (users.length < 1) return; | ||||
|  | ||||
| 	os.popup(XDetails, { | ||||
| 		showing, | ||||
| 		users, | ||||
| 		count: props.count, | ||||
| 		targetElement: buttonRef.value, | ||||
| 	}, {}, 'closed'); | ||||
| }); | ||||
|  | ||||
| const renote = (viaKeyboard = false) => { | ||||
| 	pleaseLogin(); | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.renote, | ||||
| 		icon: 'ti ti-repeat', | ||||
| 		action: () => { | ||||
| 			os.api('notes/create', { | ||||
| 				renoteId: props.note.id, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.quote, | ||||
| 		icon: 'ti ti-quote', | ||||
| 		action: () => { | ||||
| 			os.post({ | ||||
| 				renote: props.note, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}], buttonRef.value, { | ||||
| 		viaKeyboard, | ||||
| 	}); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .eddddedb { | ||||
| 	display: inline-block; | ||||
| 	height: 32px; | ||||
| 	margin: 2px; | ||||
| 	padding: 0 6px; | ||||
| 	border-radius: 4px; | ||||
|  | ||||
| 	&:not(.canRenote) { | ||||
| 		cursor: default; | ||||
| 	} | ||||
|  | ||||
| 	&.renoted { | ||||
| 		background: var(--accent); | ||||
| 	} | ||||
|  | ||||
| 	> .count { | ||||
| 		display: inline; | ||||
| 		margin-left: 8px; | ||||
| 		opacity: 0.7; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div class="vblkjoeq"> | ||||
| 	<div class="label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick"> | ||||
| 	<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show"> | ||||
| 		<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> | ||||
| 		<select | ||||
| 			ref="inputEl" | ||||
| @@ -118,7 +118,7 @@ onMounted(() => { | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| const onClick = (ev: MouseEvent) => { | ||||
| function show(ev: MouseEvent) { | ||||
| 	focused.value = true; | ||||
| 	opening.value = true; | ||||
|  | ||||
| @@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => { | ||||
| 	}).then(() => { | ||||
| 		focused.value = false; | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => { | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .chevron { | ||||
| 	transition: transform 0.5s ease; | ||||
| 	transition: transform 0.1s ease-out; | ||||
| } | ||||
|  | ||||
| .chevronOpening { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
| 				<template #prefix><i class="ti ti-lock"></i></template> | ||||
| 				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> | ||||
| 			</MkInput> | ||||
| 			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 			<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 		</div> | ||||
| 		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> | ||||
| 			<div v-if="user && user.securityKeys" class="twofa-group tap-group"> | ||||
| @@ -36,7 +36,7 @@ | ||||
| 					<template #label>{{ i18n.ts.token }}</template> | ||||
| 					<template #prefix><i class="ti ti-123"></i></template> | ||||
| 				</MkInput> | ||||
| 				<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||
| 		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> | ||||
| 		<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||
| 		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> | ||||
| 		<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/> | ||||
| 		<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> | ||||
| 	</div> | ||||
| 	<details v-if="note.files.length > 0"> | ||||
|   | ||||
| @@ -1,48 +0,0 @@ | ||||
| <template> | ||||
| <span v-if="note.visibility !== 'public'" :class="$style.visibility" :title="i18n.ts._visibility[note.visibility]"> | ||||
| 	<i v-if="note.visibility === 'home'" class="ti ti-home"></i> | ||||
| 	<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> | ||||
| 	<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> | ||||
| </span> | ||||
| <span v-if="note.localOnly" :class="$style.localOnly" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref } from 'vue'; | ||||
| import XDetails from '@/components/MkUsersTooltip.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { useTooltip } from '@/scripts/use-tooltip'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: { | ||||
| 		visibility: string; | ||||
| 		localOnly?: boolean; | ||||
| 		visibleUserIds?: string[]; | ||||
| 	}, | ||||
| }>(); | ||||
|  | ||||
| const specified = $shallowRef<HTMLElement>(); | ||||
|  | ||||
| if (props.note.visibility === 'specified') { | ||||
| 	useTooltip($$(specified), async (showing) => { | ||||
| 		const users = await os.api('users/show', { | ||||
| 			userIds: props.note.visibleUserIds, | ||||
| 			limit: 10, | ||||
| 		}); | ||||
|  | ||||
| 		os.popup(XDetails, { | ||||
| 			showing, | ||||
| 			users, | ||||
| 			count: props.note.visibleUserIds.length, | ||||
| 			targetElement: specified, | ||||
| 		}, {}, 'closed'); | ||||
| 	}); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .visibility, .localOnly { | ||||
| 	margin-left: 0.5em; | ||||
| } | ||||
| </style> | ||||
| @@ -22,7 +22,7 @@ | ||||
| 	<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis"> | ||||
| 		<template #header>{{ category || $ts.other }}</template> | ||||
| 		<div class="zuvgdzyt"> | ||||
| 			<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> | ||||
| 			<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" v-once :key="emoji.name" class="emoji" :emoji="emoji"/> | ||||
| 		</div> | ||||
| 	</MkFolder> | ||||
| </div> | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|  | ||||
| 	<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 		<div class="dqokceoi"> | ||||
| 			<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 			<MkA v-for="instance in items" v-once :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 				<MkInstanceCardMini :instance="instance"/> | ||||
| 			</MkA> | ||||
| 		</div> | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
| 					</div> | ||||
|  | ||||
| 					<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> | ||||
| 						<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> | ||||
| 						<MkA v-for="user in items" v-once :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> | ||||
| 							<MkUserCardMini :user="user"/> | ||||
| 						</MkA> | ||||
| 					</MkPagination> | ||||
|   | ||||
| @@ -53,14 +53,14 @@ definePageMetadata({ | ||||
| <style lang="scss" scoped> | ||||
| .ruryvtyk { | ||||
| 	> .announcement { | ||||
| 		padding: 16px; | ||||
|  | ||||
| 		> .header { | ||||
| 			padding: 16px; | ||||
| 			margin-bottom: 16px; | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
|  | ||||
| 		> .content { | ||||
| 			padding: 0 16px; | ||||
| 		 | ||||
| 			> img { | ||||
| 				display: block; | ||||
| 				max-height: 300px; | ||||
| @@ -69,7 +69,7 @@ definePageMetadata({ | ||||
| 		} | ||||
|  | ||||
| 		> .footer { | ||||
| 			padding: 16px; | ||||
| 			margin-top: 16px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ const headerActions = $computed(() => [{ | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	key: 'featured', | ||||
| 	title: i18n.ts._play.featured, | ||||
| 	icon: 'fas fa-fire-alt', | ||||
| 	icon: 'ti ti-flare', | ||||
| }, { | ||||
| 	key: 'my', | ||||
| 	title: i18n.ts._play.my, | ||||
|   | ||||
| @@ -63,7 +63,7 @@ const headerActions = $computed(() => [{ | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	key: 'featured', | ||||
| 	title: i18n.ts._pages.featured, | ||||
| 	icon: 'fas fa-fire-alt', | ||||
| 	icon: 'ti ti-flare', | ||||
| }, { | ||||
| 	key: 'my', | ||||
| 	title: i18n.ts._pages.my, | ||||
|   | ||||
| @@ -12,12 +12,37 @@ import { computed } from 'vue'; | ||||
| import XNotes from '@/components/MkNotes.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import * as os from '@/os'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { $i } from '@/account'; | ||||
|  | ||||
| const router = useRouter(); | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	query: string; | ||||
| 	channel?: string; | ||||
| }>(); | ||||
|  | ||||
| const query = props.query; | ||||
|  | ||||
| if ($i != null) { | ||||
| 	if (query.startsWith('https://') || (query.startsWith('@') && !query.includes(' '))) { | ||||
| 		const promise = os.api('ap/show', { | ||||
| 			uri: props.query, | ||||
| 		}); | ||||
|  | ||||
| 		os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); | ||||
|  | ||||
| 		const res = await promise; | ||||
|  | ||||
| 		if (res.type === 'User') { | ||||
| 			router.replace(`/@${res.object.username}@${res.object.host}`); | ||||
| 		} else if (res.type === 'Note') { | ||||
| 			router.replace(`/notes/${res.object.id}`); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const pagination = { | ||||
| 	endpoint: 'notes/search' as const, | ||||
| 	limit: 10, | ||||
|   | ||||
| @@ -1,18 +1,20 @@ | ||||
| <template> | ||||
| <div class="_gaps_m"> | ||||
| <div class=""> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> | ||||
| 		<div class="_gaps"> | ||||
| 			<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> | ||||
|  | ||||
| 		<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> | ||||
| 			<div class="avatar"> | ||||
| 				<MkAvatar :user="account" class="avatar"/> | ||||
| 			</div> | ||||
| 			<div class="body"> | ||||
| 				<div class="name"> | ||||
| 					<MkUserName :user="account"/> | ||||
| 			<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> | ||||
| 				<div class="avatar"> | ||||
| 					<MkAvatar :user="account" class="avatar"/> | ||||
| 				</div> | ||||
| 				<div class="acct"> | ||||
| 					<MkAcct :user="account"/> | ||||
| 				<div class="body"> | ||||
| 					<div class="name"> | ||||
| 						<MkUserName :user="account"/> | ||||
| 					</div> | ||||
| 					<div class="acct"> | ||||
| 						<MkAcct :user="account"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|   | ||||
							
								
								
									
										174
									
								
								packages/frontend/src/pages/user/activity.following.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								packages/frontend/src/pages/user/activity.following.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-show="!fetching" :class="$style.root" class="_panel"> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 		<MkChartLegend ref="legendEl" style="margin-top: 8px;"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { satisfies } from 'compare-versions'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| import { chartLegend } from '@/scripts/chart-legend'; | ||||
| import MkChartLegend from '@/components/MkChartLegend.vue'; | ||||
|  | ||||
| initChart(); | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
|  | ||||
| const chartEl = $shallowRef<HTMLCanvasElement>(null); | ||||
| let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const chartLimit = 50; | ||||
| let fetching = $ref(true); | ||||
|  | ||||
| const { handler: externalTooltipHandler } = useChartTooltip(); | ||||
|  | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
|  | ||||
| 	const getDate = (ago: number) => { | ||||
| 		const y = now.getFullYear(); | ||||
| 		const m = now.getMonth(); | ||||
| 		const d = now.getDate(); | ||||
|  | ||||
| 		return new Date(y, m, d - ago); | ||||
| 	}; | ||||
|  | ||||
| 	const format = (arr) => { | ||||
| 		return arr.map((v, i) => ({ | ||||
| 			x: getDate(i).getTime(), | ||||
| 			y: v, | ||||
| 		})); | ||||
| 	}; | ||||
|  | ||||
| 	const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
|  | ||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
|  | ||||
| 	const colorFollowLocal = '#008FFB'; | ||||
| 	const colorFollowRemote = '#008FFB88'; | ||||
| 	const colorFollowedLocal = '#2ecc71'; | ||||
| 	const colorFollowedRemote = '#2ecc7188'; | ||||
|  | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.9, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
|  | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [ | ||||
| 				makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }), | ||||
| 				makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }), | ||||
| 				makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }), | ||||
| 				makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
| 					right: 8, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					stacked: true, | ||||
| 					time: { | ||||
| 						stepSize: 1, | ||||
| 						unit: 'day', | ||||
| 						displayFormats: { | ||||
| 							day: 'M/d', | ||||
| 							month: 'Y/M', | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					position: 'left', | ||||
| 					stacked: true, | ||||
| 					suggestedMax: 10, | ||||
| 					grid: { | ||||
| 						display: true, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						//mirror: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			interaction: { | ||||
| 				intersect: false, | ||||
| 				mode: 'index', | ||||
| 			}, | ||||
| 			plugins: { | ||||
| 				legend: { | ||||
| 					display: false, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 				gradient, | ||||
| 			}, | ||||
| 		}, | ||||
| 		plugins: [chartVLine(vLineColor), chartLegend(legendEl)], | ||||
| 	}); | ||||
|  | ||||
| 	fetching = false; | ||||
| } | ||||
|  | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										174
									
								
								packages/frontend/src/pages/user/activity.notes.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								packages/frontend/src/pages/user/activity.notes.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-show="!fetching" :class="$style.root" class="_panel"> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 		<MkChartLegend ref="legendEl" style="margin-top: 8px;"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| import { satisfies } from 'compare-versions'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||
| import { chartVLine } from '@/scripts/chart-vline'; | ||||
| import { alpha } from '@/scripts/color'; | ||||
| import { initChart } from '@/scripts/init-chart'; | ||||
| import { chartLegend } from '@/scripts/chart-legend'; | ||||
| import MkChartLegend from '@/components/MkChartLegend.vue'; | ||||
|  | ||||
| initChart(); | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.User; | ||||
| }>(); | ||||
|  | ||||
| const chartEl = $shallowRef<HTMLCanvasElement>(null); | ||||
| let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const chartLimit = 50; | ||||
| let fetching = $ref(true); | ||||
|  | ||||
| const { handler: externalTooltipHandler } = useChartTooltip(); | ||||
|  | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
|  | ||||
| 	const getDate = (ago: number) => { | ||||
| 		const y = now.getFullYear(); | ||||
| 		const m = now.getMonth(); | ||||
| 		const d = now.getDate(); | ||||
|  | ||||
| 		return new Date(y, m, d - ago); | ||||
| 	}; | ||||
|  | ||||
| 	const format = (arr) => { | ||||
| 		return arr.map((v, i) => ({ | ||||
| 			x: getDate(i).getTime(), | ||||
| 			y: v, | ||||
| 		})); | ||||
| 	}; | ||||
|  | ||||
| 	const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
|  | ||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
|  | ||||
| 	const colorNormal = '#008FFB'; | ||||
| 	const colorReply = '#FEB019'; | ||||
| 	const colorRenote = '#00E396'; | ||||
| 	const colorFile = '#e300db'; | ||||
|  | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.9, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
|  | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [ | ||||
| 				makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), | ||||
| 				makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), | ||||
| 				makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), | ||||
| 				makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
| 					right: 8, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					stacked: true, | ||||
| 					time: { | ||||
| 						stepSize: 1, | ||||
| 						unit: 'day', | ||||
| 						displayFormats: { | ||||
| 							day: 'M/d', | ||||
| 							month: 'Y/M', | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					position: 'left', | ||||
| 					stacked: true, | ||||
| 					suggestedMax: 10, | ||||
| 					grid: { | ||||
| 						display: true, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						//mirror: true, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			interaction: { | ||||
| 				intersect: false, | ||||
| 				mode: 'index', | ||||
| 			}, | ||||
| 			plugins: { | ||||
| 				legend: { | ||||
| 					display: false, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 				gradient, | ||||
| 			}, | ||||
| 		}, | ||||
| 		plugins: [chartVLine(vLineColor), chartLegend(legendEl)], | ||||
| 	}); | ||||
|  | ||||
| 	fetching = false; | ||||
| } | ||||
|  | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
| @@ -10,7 +10,7 @@ | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import { Chart, ChartDataset } from 'chart.js'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import gradient from 'chartjs-plugin-gradient'; | ||||
| @@ -67,65 +67,33 @@ async function renderChart() { | ||||
| 	const colorUser2 = '#3498db88'; | ||||
| 	const colorVisitor2 = '#2ecc7188'; | ||||
|  | ||||
| 	function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { | ||||
| 		return Object.assign({ | ||||
| 			label: label, | ||||
| 			data: data, | ||||
| 			parsing: false, | ||||
| 			pointRadius: 0, | ||||
| 			borderWidth: 0, | ||||
| 			borderJoinStyle: 'round', | ||||
| 			borderRadius: 4, | ||||
| 			barPercentage: 0.7, | ||||
| 			categoryPercentage: 0.7, | ||||
| 			fill: true, | ||||
| 		} satisfies ChartDataset, extra); | ||||
| 	} | ||||
|  | ||||
| 	chartInstance = new Chart(chartEl, { | ||||
| 		type: 'bar', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (user)', | ||||
| 				data: format(raw.upv.user).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorUser, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'u', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'UPV (visitor)', | ||||
| 				data: format(raw.upv.visitor).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorVisitor, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'u', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'NPV (user)', | ||||
| 				data: format(raw.pv.user).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorUser2, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'n', | ||||
| 			}, { | ||||
| 				parsing: false, | ||||
| 				label: 'NPV (visitor)', | ||||
| 				data: format(raw.pv.visitor).slice().reverse(), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 4, | ||||
| 				backgroundColor: colorVisitor2, | ||||
| 				barPercentage: 0.7, | ||||
| 				categoryPercentage: 0.7, | ||||
| 				fill: true, | ||||
| 				stack: 'n', | ||||
| 			}], | ||||
| 			datasets: [ | ||||
| 				makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }), | ||||
| 				makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }), | ||||
| 				makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }), | ||||
| 				makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), | ||||
| 			], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: 2.5, | ||||
| 			aspectRatio: 3, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 0, | ||||
|   | ||||
| @@ -2,11 +2,19 @@ | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div class="_gaps"> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header>Heatmap</template> | ||||
| 			<template #header><i class="ti ti-activity"></i> Heatmap</template> | ||||
| 			<XHeatmap :user="user" :src="'notes'"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header>PV</template> | ||||
| 			<template #header><i class="ti ti-pencil"></i> Notes</template> | ||||
| 			<XNotes :user="user"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header><i class="ti ti-users"></i> Following</template> | ||||
| 			<XFollowing :user="user"/> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="item"> | ||||
| 			<template #header><i class="ti ti-eye"></i> PV</template> | ||||
| 			<XPv :user="user"/> | ||||
| 		</MkFolder> | ||||
| 	</div> | ||||
| @@ -18,6 +26,8 @@ import { computed } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XHeatmap from './activity.heatmap.vue'; | ||||
| import XPv from './activity.pv.vue'; | ||||
| import XNotes from './activity.notes.vue'; | ||||
| import XFollowing from './activity.following.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   | ||||
| @@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all( | ||||
| 		'l-coffee', | ||||
| 		'l-apricot', | ||||
| 		'l-rainy', | ||||
| 		'l-botanical', | ||||
| 		'l-vivid', | ||||
| 		'l-cherry', | ||||
| 		'l-sushi', | ||||
|   | ||||
							
								
								
									
										29
									
								
								packages/frontend/src/themes/l-botanical.json5
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								packages/frontend/src/themes/l-botanical.json5
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
| 	id: '1100673c-f902-4ccd-93aa-7cb88be56178', | ||||
|  | ||||
| 	name: 'Mi Botanical Light', | ||||
| 	author: 'ThinaticSystem', | ||||
|  | ||||
|   base: 'light', | ||||
| 	 | ||||
| 	props: { | ||||
| 		accent: '#77b58c', | ||||
| 		bg: 'e2deda', | ||||
| 		fg: '#3d3d3d', | ||||
| 		fgHighlighted: '#6bc9a0', | ||||
| 		divider: '#cfcfcf', | ||||
| 		panel: '@X14', | ||||
| 		panelHeaderBg: '@panel', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		header: ':alpha<0.7<@panel', | ||||
| 		navBg: '@X14', | ||||
| 		renote: '#229e92', | ||||
| 		mention: '#da6d35', | ||||
| 		mentionMe: '#d44c4c', | ||||
| 		hashtag: '#4cb8d4', | ||||
| 		link: '@accent', | ||||
| 		buttonGradateB: ':hue<-70<@accent', | ||||
| 		success: '#86b300', | ||||
|     X14: '#ebe7e5' | ||||
| 	}, | ||||
| } | ||||
| @@ -34,7 +34,7 @@ | ||||
| 		<div v-if="showMenu" class="menu"> | ||||
| 			<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> | ||||
| 			<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> | ||||
| 			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> | ||||
| 			<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> | ||||
| 			<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> | ||||
| 			<div class="action"> | ||||
| 				<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<div class="content"> | ||||
| 			<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> | ||||
| 			<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> | ||||
| 			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> | ||||
| 			<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> | ||||
| 			<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> | ||||
| 			<div v-if="info" class="page active link"> | ||||
| 				<div class="title"> | ||||
|   | ||||
| @@ -11,19 +11,19 @@ | ||||
| 	</div> | ||||
| 	<div class="info"> | ||||
| 		<div> | ||||
| 			<p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p> | ||||
| 			<p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p> | ||||
| 			<div class="meter"> | ||||
| 				<div class="val" :style="{ width: `${dayP}%` }"></div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p> | ||||
| 			<p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p> | ||||
| 			<div class="meter"> | ||||
| 				<div class="val" :style="{ width: `${monthP}%` }"></div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p> | ||||
| 			<p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p> | ||||
| 			<div class="meter"> | ||||
| 				<div class="val" :style="{ width: `${yearP}%` }"></div> | ||||
| 			</div> | ||||
| @@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({ | ||||
| 			} | ||||
|  | ||||
| 			> p { | ||||
| 				display: flex; | ||||
| 				margin: 0 0 2px 0; | ||||
| 				font-size: 0.75em; | ||||
| 				line-height: 18px; | ||||
| 				opacity: 0.8; | ||||
|  | ||||
| 				> b { | ||||
| 					margin-left: 2px; | ||||
| 					margin-left: auto; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -51,8 +51,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: userDetail.isFollowing ? [] : [ | ||||
| 							{ | ||||
| 								action: 'follow', | ||||
| 								title: t('_notification._actions.followBack') | ||||
| 							} | ||||
| 								title: t('_notification._actions.followBack'), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
| 				} | ||||
| @@ -66,8 +66,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'reply', | ||||
| 								title: t('_notification._actions.reply') | ||||
| 							} | ||||
| 								title: t('_notification._actions.reply'), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| @@ -80,8 +80,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'reply', | ||||
| 								title: t('_notification._actions.reply') | ||||
| 							} | ||||
| 								title: t('_notification._actions.reply'), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| @@ -94,8 +94,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'showUser', | ||||
| 								title: getUserName(data.body.user) | ||||
| 							} | ||||
| 								title: getUserName(data.body.user), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| @@ -108,14 +108,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'reply', | ||||
| 								title: t('_notification._actions.reply') | ||||
| 								title: t('_notification._actions.reply'), | ||||
| 							}, | ||||
| 							...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ | ||||
| 							{ | ||||
| 								action: 'renote', | ||||
| 								title: t('_notification._actions.renote') | ||||
| 							} | ||||
| 							] : []) | ||||
| 								{ | ||||
| 									action: 'renote', | ||||
| 									title: t('_notification._actions.renote'), | ||||
| 								}, | ||||
| 							] : []), | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| @@ -141,7 +141,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 								const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.png`; | ||||
| 								badge = `${origin}/proxy/${dummy}?${url.query({ | ||||
| 									url: u.href, | ||||
| 									badge: '1' | ||||
| 									badge: '1', | ||||
| 								})}`; | ||||
| 							} | ||||
| 						} | ||||
| @@ -162,20 +162,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'showUser', | ||||
| 								title: getUserName(data.body.user) | ||||
| 							} | ||||
| 								title: getUserName(data.body.user), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
| 				} | ||||
|  | ||||
| 				case 'pollVote': | ||||
| 					return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { | ||||
| 						body: data.body.note.text || '', | ||||
| 						icon: data.body.user.avatarUrl, | ||||
| 						badge: iconUrl('poll-h'), | ||||
| 						data, | ||||
| 					}]; | ||||
|  | ||||
| 				case 'pollEnded': | ||||
| 					return [t('_notification.pollEnded'), { | ||||
| 						body: data.body.note.text || '', | ||||
| @@ -192,12 +184,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'accept', | ||||
| 								title: t('accept') | ||||
| 								title: t('accept'), | ||||
| 							}, | ||||
| 							{ | ||||
| 								action: 'reject', | ||||
| 								title: t('reject') | ||||
| 							} | ||||
| 								title: t('reject'), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| @@ -217,21 +209,21 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data | ||||
| 						actions: [ | ||||
| 							{ | ||||
| 								action: 'accept', | ||||
| 								title: t('accept') | ||||
| 								title: t('accept'), | ||||
| 							}, | ||||
| 							{ | ||||
| 								action: 'reject', | ||||
| 								title: t('reject') | ||||
| 							} | ||||
| 								title: t('reject'), | ||||
| 							}, | ||||
| 						], | ||||
| 					}]; | ||||
|  | ||||
| 				case 'app': | ||||
| 						return [data.body.header || data.body.body, { | ||||
| 							body: data.body.header && data.body.body, | ||||
| 							icon: data.body.icon, | ||||
| 							data | ||||
| 						}]; | ||||
| 					return [data.body.header || data.body.body, { | ||||
| 						body: data.body.header && data.body.body, | ||||
| 						icon: data.body.icon, | ||||
| 						data, | ||||
| 					}]; | ||||
|  | ||||
| 				default: | ||||
| 					return null; | ||||
| @@ -279,7 +271,7 @@ export async function createEmptyNotification() { | ||||
| 				silent: true, | ||||
| 				badge: iconUrl('null'), | ||||
| 				tag: 'read_notification', | ||||
| 			} | ||||
| 			}, | ||||
| 		); | ||||
|  | ||||
| 		res(); | ||||
| @@ -288,7 +280,7 @@ export async function createEmptyNotification() { | ||||
| 			for (const n of | ||||
| 				[ | ||||
| 					...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })), | ||||
| 					...(await self.registration.getNotifications({ tag: 'read_notification' })) | ||||
| 					...(await self.registration.getNotifications({ tag: 'read_notification' })), | ||||
| 				] | ||||
| 			) { | ||||
| 				n.close(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user