Compare commits
	
		
			17 Commits
		
	
	
		
			2025.3.2-b
			...
			2025.3.2-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![github-actions[bot]](/assets/img/avatar_default.png)  | fbd9f47182 | ||
|   | 1c9e25470a | ||
|   | ce6b2448ce | ||
|   | 7d44b47fdf | ||
|   | dca42fd6e6 | ||
|   | 43153311c6 | ||
|   | 2b23c7e7f5 | ||
|   | 2d4e85b466 | ||
|   | 7bfada9792 | ||
|   | 4fef9c670a | ||
|   | 9599261fc3 | ||
|   | c2940fd77c | ||
|   | 2ddedd0ce6 | ||
|   | c88f5f5195 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 30de6d80bb | ||
|   | 63993dace6 | ||
|   | 0929410d36 | 
							
								
								
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -24,9 +24,6 @@ updates: | ||||
|     aws-sdk: | ||||
|       patterns: | ||||
|         - "@aws-sdk/*" | ||||
|     bull-board: | ||||
|       patterns: | ||||
|         - "@bull-board/*" | ||||
|     nestjs: | ||||
|       patterns: | ||||
|         - "@nestjs/*" | ||||
|   | ||||
| @@ -1,18 +1,22 @@ | ||||
| ## 2025.3.2 | ||||
|  | ||||
| ### General | ||||
| - | ||||
| - セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 | ||||
|   - Misskeyネイティブでダッシュボードを実装予定です | ||||
|  | ||||
| ### Client | ||||
| - Feat: 設定の管理が強化されました | ||||
|   - 自動でバックアップされるように | ||||
| 	- 任意の設定項目をデバイス間で同期できるように(実験的) | ||||
|   - 任意の設定項目をデバイス間で同期できるように | ||||
| - Enhance: プラグインの管理が強化されました | ||||
|   - インストール/アンインストール/設定の変更時にリロード不要になりました | ||||
| - Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように | ||||
| - Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに | ||||
| - Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように | ||||
| - Enhance: テーマ設定画面のデザインを改善 | ||||
| - Enhance: 投稿フォームの設定メニューを改良 | ||||
|   - 投稿フォームをリセットできるように | ||||
|   - 文字数カウントを復活 | ||||
| - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 | ||||
|  | ||||
| ### Server | ||||
|   | ||||
| @@ -1190,7 +1190,7 @@ pastAnnouncements: "Informes passats" | ||||
| youHaveUnreadAnnouncements: "Tens informes per llegir." | ||||
| useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." | ||||
| replies: "Respostes" | ||||
| renotes: "Impulsar" | ||||
| renotes: "Impulsos" | ||||
| loadReplies: "Mostrar les respostes" | ||||
| loadConversation: "Mostrar la conversació " | ||||
| pinnedList: "Llista fixada" | ||||
| @@ -1326,7 +1326,17 @@ restore: "Restaurar " | ||||
| syncBetweenDevices: "Sincronització entre dispositius" | ||||
| preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu" | ||||
| preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?" | ||||
| preferenceSyncConflictChoiceServer: "Valors de configuració del servidor" | ||||
| preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu " | ||||
| preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització " | ||||
| paste: "Pegar" | ||||
| emojiPalette: "Calaix d'emojis" | ||||
| postForm: "Formulari de publicació" | ||||
| _emojiPalette: | ||||
|   palettes: "Calaixos d'emojis" | ||||
|   enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius" | ||||
|   paletteForMain: "Calaix d'emojis principal" | ||||
|   paletteForReaction: "Calaix d'emojis per reaccions" | ||||
| _settings: | ||||
|   driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius." | ||||
|   pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se." | ||||
| @@ -1344,6 +1354,7 @@ _settings: | ||||
|   preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències." | ||||
|   appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer." | ||||
|   soundsBanner: "Configuració dels sons que reproduirà el client." | ||||
|   timelineAndNote: "Línia de temps i nota" | ||||
| _preferencesProfile: | ||||
|   profileName: "Nom del perfil" | ||||
|   profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu." | ||||
| @@ -2056,7 +2067,7 @@ _theme: | ||||
|     hashtag: "Etiqueta" | ||||
|     mention: "Menció" | ||||
|     mentionMe: "Mencions (jo)" | ||||
|     renote: "Renotar" | ||||
|     renote: "Impulsar" | ||||
|     modalBg: "Fons del modal" | ||||
|     divider: "Divisor" | ||||
|     scrollbarHandle: "Maneta de la barra de desplaçament" | ||||
| @@ -2498,7 +2509,7 @@ _notification: | ||||
|     follow: "Segueix-me" | ||||
|     mention: "Menció" | ||||
|     reply: "Respostes" | ||||
|     renote: "Impulsar" | ||||
|     renote: "Impulsos" | ||||
|     quote: "Citar" | ||||
|     reaction: "Reaccions" | ||||
|     pollEnded: "Enquesta terminada" | ||||
| @@ -2513,7 +2524,7 @@ _notification: | ||||
|   _actions: | ||||
|     followBack: "També et segueix" | ||||
|     reply: "Respondre" | ||||
|     renote: "Impulsos" | ||||
|     renote: "Impulsar" | ||||
| _deck: | ||||
|   alwaysShowMainColumn: "Mostrar sempre la columna principal" | ||||
|   columnAlign: "Alinea les columnes" | ||||
|   | ||||
| @@ -49,7 +49,7 @@ pin: "An dein Profil anheften" | ||||
| unpin: "Von deinem Profil lösen" | ||||
| copyContent: "Inhalt kopieren" | ||||
| copyLink: "Link kopieren" | ||||
| copyRemoteLink: "Renote-Link kopieren" | ||||
| copyRemoteLink: "Remote-Link kopieren" | ||||
| copyLinkRenote: "Renote-Link kopieren" | ||||
| delete: "Löschen" | ||||
| deleteAndEdit: "Löschen und Bearbeiten" | ||||
|   | ||||
							
								
								
									
										16
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -5346,6 +5346,10 @@ export interface Locale extends ILocale { | ||||
|      * 投稿フォーム | ||||
|      */ | ||||
|     "postForm": string; | ||||
|     /** | ||||
|      * 文字数 | ||||
|      */ | ||||
|     "textCount": string; | ||||
|     "_emojiPalette": { | ||||
|         /** | ||||
|          * パレット | ||||
| @@ -5433,6 +5437,14 @@ export interface Locale extends ILocale { | ||||
|          * タイムラインとノート | ||||
|          */ | ||||
|         "timelineAndNote": string; | ||||
|         /** | ||||
|          * 全てのテキスト要素を選択可能にする | ||||
|          */ | ||||
|         "makeEveryTextElementsSelectable": string; | ||||
|         /** | ||||
|          * 有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。 | ||||
|          */ | ||||
|         "makeEveryTextElementsSelectable_description": string; | ||||
|     }; | ||||
|     "_preferencesProfile": { | ||||
|         /** | ||||
| @@ -9777,6 +9789,10 @@ export interface Locale extends ILocale { | ||||
|              * ログイン | ||||
|              */ | ||||
|             "login": string; | ||||
|             /** | ||||
|              * アクセストークンの作成 | ||||
|              */ | ||||
|             "createToken": string; | ||||
|             /** | ||||
|              * 通知のテスト | ||||
|              */ | ||||
|   | ||||
| @@ -1332,6 +1332,7 @@ preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル" | ||||
| paste: "ペースト" | ||||
| emojiPalette: "絵文字パレット" | ||||
| postForm: "投稿フォーム" | ||||
| textCount: "文字数" | ||||
|  | ||||
| _emojiPalette: | ||||
|   palettes: "パレット" | ||||
| @@ -1357,6 +1358,8 @@ _settings: | ||||
|   appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。" | ||||
|   soundsBanner: "クライアントで再生するサウンドの設定が行えます。" | ||||
|   timelineAndNote: "タイムラインとノート" | ||||
|   makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする" | ||||
|   makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。" | ||||
|  | ||||
| _preferencesProfile: | ||||
|   profileName: "プロファイル名" | ||||
| @@ -2584,6 +2587,7 @@ _notification: | ||||
|     achievementEarned: "実績の獲得" | ||||
|     exportCompleted: "エクスポートが完了した" | ||||
|     login: "ログイン" | ||||
|     createToken: "アクセストークンの作成" | ||||
|     test: "通知のテスト" | ||||
|     app: "連携アプリからの通知" | ||||
|  | ||||
|   | ||||
| @@ -746,7 +746,7 @@ confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。 | ||||
| public: "公开" | ||||
| private: "私密" | ||||
| i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。" | ||||
| manageAccessTokens: "管理 Access Tokens" | ||||
| manageAccessTokens: "管理访问令牌" | ||||
| accountInfo: "账户信息" | ||||
| notesCount: "帖子数量" | ||||
| repliesCount: "回复数量" | ||||
| @@ -1339,13 +1339,24 @@ _emojiPalette: | ||||
|   paletteForReaction: "回应用调色板" | ||||
| _settings: | ||||
|   driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。" | ||||
|   pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。" | ||||
|   notificationsBanner: "可在此设置从服务器接收的通知的种类和范围,以及推送通知的设置。" | ||||
|   api: "API" | ||||
|   webhook: "Webhook" | ||||
|   serviceConnection: "连接服务" | ||||
|   serviceConnectionBanner: "可在此管理用于连接外部应用或服务的访问令牌及 Webhook。" | ||||
|   accountData: "账户数据" | ||||
|   accountDataBanner: "可在此导入或导出帐户数据的存档。" | ||||
|   muteAndBlockBanner: "可在此设置隐藏内容,或限制指定用户能进行的操作。" | ||||
|   accessibilityBanner: "可在此设置客户端的显示及动态效果等辅助设置。" | ||||
|   privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。" | ||||
|   securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。" | ||||
|   preferencesBanner: "可在此设置客户端的整体运作行为。" | ||||
|   appearanceBanner: "可在此设置客户端的外观及显示方式。" | ||||
|   soundsBanner: "可在此设置客户端播放的声音。" | ||||
|   timelineAndNote: "时间线和帖子" | ||||
|   makeEveryTextElementsSelectable: "使所有的文字均可选择" | ||||
|   makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。" | ||||
| _preferencesProfile: | ||||
|   profileName: "配置名" | ||||
|   profileNameDescription: "请指定用于识别此设备的名称" | ||||
| @@ -2510,6 +2521,7 @@ _notification: | ||||
|     achievementEarned: "取得的成就" | ||||
|     exportCompleted: "已完成导出" | ||||
|     login: "登录" | ||||
|     createToken: "创建访问令牌" | ||||
|     test: "测试通知" | ||||
|     app: "关联应用的通知" | ||||
|   _actions: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "2025.3.2-beta.0", | ||||
| 	"version": "2025.3.2-beta.2", | ||||
| 	"codename": "nasubi", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|   | ||||
| @@ -69,9 +69,6 @@ | ||||
| 	"dependencies": { | ||||
| 		"@aws-sdk/client-s3": "3.749.0", | ||||
| 		"@aws-sdk/lib-storage": "3.749.0", | ||||
| 		"@bull-board/api": "6.7.7", | ||||
| 		"@bull-board/fastify": "6.7.7", | ||||
| 		"@bull-board/ui": "6.7.7", | ||||
| 		"@discordapp/twemoji": "15.1.0", | ||||
| 		"@fastify/accepts": "5.0.2", | ||||
| 		"@fastify/cookie": "11.0.2", | ||||
|   | ||||
| @@ -6,7 +6,6 @@ | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import cors from '@fastify/cors'; | ||||
| import multipart from '@fastify/multipart'; | ||||
| import fastifyCookie from '@fastify/cookie'; | ||||
| import { ModuleRef } from '@nestjs/core'; | ||||
| import { AuthenticationResponseJSON } from '@simplewebauthn/types'; | ||||
| import type { Config } from '@/config.js'; | ||||
| @@ -57,8 +56,6 @@ export class ApiServerService { | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 		fastify.register(fastifyCookie, {}); | ||||
|  | ||||
| 		// Prevent cache | ||||
| 		fastify.addHook('onRequest', (request, reply, done) => { | ||||
| 			reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); | ||||
|   | ||||
| @@ -7,16 +7,12 @@ import { randomUUID } from 'node:crypto'; | ||||
| import { dirname } from 'node:path'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { createBullBoard } from '@bull-board/api'; | ||||
| import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; | ||||
| import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify'; | ||||
| import ms from 'ms'; | ||||
| import sharp from 'sharp'; | ||||
| import pug from 'pug'; | ||||
| import { In, IsNull } from 'typeorm'; | ||||
| import fastifyStatic from '@fastify/static'; | ||||
| import fastifyView from '@fastify/view'; | ||||
| import fastifyCookie from '@fastify/cookie'; | ||||
| import fastifyProxy from '@fastify/http-proxy'; | ||||
| import vary from 'vary'; | ||||
| import htmlSafeJsonStringify from 'htmlescape'; | ||||
| @@ -221,64 +217,6 @@ export class ClientServerService { | ||||
|  | ||||
| 	@bindThis | ||||
| 	public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { | ||||
| 		fastify.register(fastifyCookie, {}); | ||||
|  | ||||
| 		//#region Bull Dashboard | ||||
| 		const bullBoardPath = '/queue'; | ||||
|  | ||||
| 		// Authenticate | ||||
| 		fastify.addHook('onRequest', async (request, reply) => { | ||||
| 			if (request.routeOptions.url == null) { | ||||
| 				reply.code(404).send('Not found'); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// %71ueueとかでリクエストされたら困るため | ||||
| 			const url = decodeURI(request.routeOptions.url); | ||||
| 			if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { | ||||
| 				if (!url.startsWith(bullBoardPath + '/static/')) { | ||||
| 					reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); | ||||
| 				} | ||||
|  | ||||
| 				const token = request.cookies.token; | ||||
| 				if (token == null) { | ||||
| 					reply.code(401).send('Login required'); | ||||
| 					return; | ||||
| 				} | ||||
| 				const user = await this.usersRepository.findOneBy({ token }); | ||||
| 				if (user == null) { | ||||
| 					reply.code(403).send('No such user'); | ||||
| 					return; | ||||
| 				} | ||||
| 				const isAdministrator = await this.roleService.isAdministrator(user); | ||||
| 				if (!isAdministrator) { | ||||
| 					reply.code(403).send('Access denied'); | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		const bullBoardServerAdapter = new BullBoardFastifyAdapter(); | ||||
|  | ||||
| 		createBullBoard({ | ||||
| 			queues: [ | ||||
| 				this.systemQueue, | ||||
| 				this.endedPollNotificationQueue, | ||||
| 				this.deliverQueue, | ||||
| 				this.inboxQueue, | ||||
| 				this.dbQueue, | ||||
| 				this.relationshipQueue, | ||||
| 				this.objectStorageQueue, | ||||
| 				this.userWebhookDeliverQueue, | ||||
| 				this.systemWebhookDeliverQueue, | ||||
| 			].map(q => new BullMQAdapter(q)), | ||||
| 			serverAdapter: bullBoardServerAdapter, | ||||
| 		}); | ||||
|  | ||||
| 		bullBoardServerAdapter.setBasePath(bullBoardPath); | ||||
| 		(fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath }); | ||||
| 		//#endregion | ||||
|  | ||||
| 		fastify.register(fastifyView, { | ||||
| 			root: _dirname + '/views', | ||||
| 			engine: { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| process.env.NODE_ENV = 'test'; | ||||
|  | ||||
| import * as assert from 'assert'; | ||||
| import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; | ||||
| import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; | ||||
| import type { SimpleGetResponse } from '../utils.js'; | ||||
| import type * as misskey from 'misskey-js'; | ||||
|  | ||||
| @@ -156,20 +156,20 @@ describe('Webリソース', () => { | ||||
|  | ||||
| 		describe(' has entry such ', () => { | ||||
| 			beforeEach(() => { | ||||
| 				post(alice, { text: "**a**" }) | ||||
| 				post(alice, { text: '**a**' }); | ||||
| 			}); | ||||
|  | ||||
| 			test('MFMを含まない。', async () => { | ||||
| 				const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text()); | ||||
| 				const content = await simpleGet(path(alice.username), '*/*', undefined, res => res.text()); | ||||
| 				const _body: unknown = content.body; | ||||
| 				// JSONフィードのときは改めて文字列化する | ||||
| 				const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string; | ||||
| 				const body: string = typeof (_body) === 'object' ? JSON.stringify(_body) : _body as string; | ||||
|  | ||||
| 				if (body.includes("**a**")) { | ||||
| 					throw new Error("MFM shouldn't be included"); | ||||
| 				if (body.includes('**a**')) { | ||||
| 					throw new Error('MFM shouldn\'t be included'); | ||||
| 				} | ||||
| 			}); | ||||
| 		}) | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { | ||||
| @@ -180,24 +180,6 @@ describe('Webリソース', () => { | ||||
| 		})); | ||||
| 	}); | ||||
|  | ||||
| 	describe.each([{ path: '/queue' }])('$path', ({ path }) => { | ||||
| 		test('はログインしないとGETできない。', async () => await notOk({ | ||||
| 			path, | ||||
| 			status: 401, | ||||
| 		})); | ||||
|  | ||||
| 		test('はadminでなければGETできない。', async () => await notOk({ | ||||
| 			path, | ||||
| 			cookie: cookie(bob), | ||||
| 			status: 403, | ||||
| 		})); | ||||
|  | ||||
| 		test('はadminならGETできる。', async () => await ok({ | ||||
| 			path, | ||||
| 			cookie: cookie(alice), | ||||
| 		})); | ||||
| 	}); | ||||
|  | ||||
| 	describe.each([{ path: '/streaming' }])('$path', ({ path }) => { | ||||
| 		test('はGETできない。', async () => await notOk({ | ||||
| 			path, | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export type SystemWebhookPayload = { | ||||
| 	createdAt: string; | ||||
| 	type: string; | ||||
| 	body: any; | ||||
| } | ||||
| }; | ||||
|  | ||||
| const config = loadConfig(); | ||||
| export const port = config.port; | ||||
| @@ -45,10 +45,6 @@ export const host = new URL(config.url).host; | ||||
| export const WEBHOOK_HOST = 'http://localhost:15080'; | ||||
| export const WEBHOOK_PORT = 15080; | ||||
|  | ||||
| export const cookie = (me: UserToken): string => { | ||||
| 	return `token=${me.token};`; | ||||
| }; | ||||
|  | ||||
| export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = { | ||||
| 	endpoint: E, | ||||
| 	parameters: P, | ||||
|   | ||||
| @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { inject, watch, ref } from 'vue'; | ||||
| import { watch, ref } from 'vue'; | ||||
| import XReaction from '@/components/EmReactionsViewer.reaction.vue'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| @@ -22,12 +22,6 @@ const props = withDefaults(defineProps<{ | ||||
| 	maxNumber: Infinity, | ||||
| }); | ||||
|  | ||||
| const mock = inject<boolean>('mock', false); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; | ||||
| }>(); | ||||
|  | ||||
| const initialReactions = new Set(Object.keys(props.note.reactions)); | ||||
|  | ||||
| const reactions = ref<[string, number][]>([]); | ||||
| @@ -38,12 +32,8 @@ if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.m | ||||
| } | ||||
|  | ||||
| function onMockToggleReaction(emoji: string, count: number) { | ||||
| 	if (!mock) return; | ||||
|  | ||||
| 	const i = reactions.value.findIndex((item) => item[0] === emoji); | ||||
| 	if (i < 0) return; | ||||
|  | ||||
| 	emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); | ||||
| } | ||||
|  | ||||
| watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <template> | ||||
| <div> | ||||
| 	<div class="_fullinfo"> | ||||
| 		<img :src="notFoundImageUrl" class="_ghost"/> | ||||
| 		<img :src="notFoundImageUrl" draggable="false"/> | ||||
| 		<div>{{ i18n.ts.notFoundDescription }}</div> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -20,5 +20,5 @@ import { i18n } from '@/i18n.js'; | ||||
|  | ||||
| const serverMetadata = inject(DI.serverMetadata)!; | ||||
|  | ||||
| const notFoundImageUrl = computed(() => serverMetadata?.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); | ||||
| const notFoundImageUrl = computed(() => serverMetadata.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); | ||||
| </script> | ||||
|   | ||||
| @@ -234,6 +234,10 @@ export async function common(createVue: () => App<Element>) { | ||||
| 			}); | ||||
| 	} | ||||
|  | ||||
| 	if (prefer.s.makeEveryTextElementsSelectable) { | ||||
| 		document.documentElement.classList.add('forceSelectableAll'); | ||||
| 	} | ||||
|  | ||||
| 	//#region Fetch user | ||||
| 	if ($i && $i.token) { | ||||
| 		if (_DEV_) { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkPagination :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.notFound }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| @@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import MkChannelPreview from '@/components/MkChannelPreview.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { infoImageUrl } from '@/instance.js'; | ||||
|  | ||||
|   | ||||
| @@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i> | ||||
| 			<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/> | ||||
| 		</div> | ||||
| 		<header v-if="title" :class="$style.title"><Mfm :text="title"/></header> | ||||
| 		<div v-if="text" :class="$style.text"><Mfm :text="text"/></div> | ||||
| 		<header v-if="title" :class="$style.title" class="_selectable"><Mfm :text="title"/></header> | ||||
| 		<div v-if="text" :class="$style.text" class="_selectable"><Mfm :text="text"/></div> | ||||
| 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> | ||||
| 			<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> | ||||
| 			<template #caption> | ||||
|   | ||||
| @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			</template> | ||||
| 		</div> | ||||
| 		<div v-else class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.nothing }}</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
|   | ||||
| @@ -14,8 +14,34 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		:enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined" | ||||
| 		:leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined" | ||||
| 	> | ||||
| 		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> | ||||
| 		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> | ||||
| 		<canvas | ||||
| 			v-show="hide" | ||||
| 			key="canvas" | ||||
| 			ref="canvas" | ||||
| 			:class="$style.canvas" | ||||
| 			:width="canvasWidth" | ||||
| 			:height="canvasHeight" | ||||
| 			:title="title ?? undefined" | ||||
| 			draggable="false" | ||||
| 			tabindex="-1" | ||||
| 			style="-webkit-user-drag: none;" | ||||
| 		/> | ||||
| 		<img | ||||
| 			v-show="!hide" | ||||
| 			key="img" | ||||
| 			ref="img" | ||||
| 			:height="imgHeight ?? undefined" | ||||
| 			:width="imgWidth ?? undefined" | ||||
| 			:class="$style.img" | ||||
| 			:src="src ?? undefined" | ||||
| 			:title="title ?? undefined" | ||||
| 			:alt="alt ?? undefined" | ||||
| 			loading="eager" | ||||
| 			decoding="async" | ||||
| 			draggable="false" | ||||
| 			tabindex="-1" | ||||
| 			style="-webkit-user-drag: none;" | ||||
| 		/> | ||||
| 	</TransitionGroup> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div :class="[$style.root, { [$style.warn]: warn }]"> | ||||
| <div :class="[$style.root, { [$style.warn]: warn }]" class="_selectable"> | ||||
| 	<i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> | ||||
| 	<i v-else class="ti ti-info-circle" :class="$style.i"></i> | ||||
| 	<div><slot></slot></div> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div> | ||||
| <div class="_selectable"> | ||||
| 	<div :class="$style.label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]"> | ||||
| 		<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> | ||||
| @@ -45,13 +45,13 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; | ||||
| import type { InputHTMLAttributes } from 'vue'; | ||||
| import { debounce } from 'throttle-debounce'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import { useInterval } from '@@/js/use-interval.js'; | ||||
| import type { InputHTMLAttributes } from 'vue'; | ||||
| import type { SuggestionType } from '@/utility/autocomplete.js'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { Autocomplete } from '@/utility/autocomplete.js'; | ||||
| import type { SuggestionType } from '@/utility/autocomplete.js'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	modelValue: string | number | null; | ||||
|   | ||||
| @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		<div :class="$style.items"> | ||||
| 			<div> | ||||
| 				<div :class="$style.label">{{ i18n.ts.invitationCode }}</div> | ||||
| 				<div>{{ invite.code }}</div> | ||||
| 				<div class="_selectableAtomic">{{ invite.code }}</div> | ||||
| 			</div> | ||||
| 			<div v-if="moderator"> | ||||
| 				<div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div :class="$style.key"> | ||||
| 		<slot name="key"></slot> | ||||
| 	</div> | ||||
| 	<div :class="$style.value"> | ||||
| 	<div :class="$style.value" class="_selectable"> | ||||
| 		<slot name="value"></slot> | ||||
| 		<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button> | ||||
| 	</div> | ||||
|   | ||||
| @@ -15,9 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	@focusin.passive.stop="() => {}" | ||||
| > | ||||
| 	<div | ||||
| 		ref="itemsEl" | ||||
| 		v-hotkey="keymap" | ||||
| 		tabindex="0" | ||||
| 		class="_popup _shadow" | ||||
| 		:class="$style.menu" | ||||
| 		:style="{ | ||||
| @@ -26,6 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		}" | ||||
| 		@keydown.stop="() => {}" | ||||
| 		@contextmenu.self.prevent="() => {}" | ||||
| 	> | ||||
| 		<slot name="header"></slot> | ||||
| 		<div | ||||
| 			ref="itemsEl" | ||||
| 			v-hotkey="keymap" | ||||
| 			tabindex="0" | ||||
| 			:class="$style.menuItems" | ||||
| 		> | ||||
| 			<template v-for="item in (items2 ?? [])"> | ||||
| 				<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> | ||||
| @@ -169,6 +173,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				<span>{{ i18n.ts.none }}</span> | ||||
| 			</span> | ||||
| 		</div> | ||||
| 		<slot name="footer"></slot> | ||||
| 	</div> | ||||
| 	<div v-if="childMenu"> | ||||
| 		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/> | ||||
| 	</div> | ||||
| @@ -429,7 +435,7 @@ onBeforeUnmount(() => { | ||||
| .root { | ||||
| 	&.center { | ||||
| 		> .menu { | ||||
| 			> .item { | ||||
| 			.item { | ||||
| 				text-align: center; | ||||
| 			} | ||||
| 		} | ||||
| @@ -439,7 +445,7 @@ onBeforeUnmount(() => { | ||||
| 		> .menu { | ||||
| 			min-width: 230px; | ||||
|  | ||||
| 			> .item { | ||||
| 			.item { | ||||
| 				padding: 6px 20px; | ||||
| 				font-size: 0.95em; | ||||
| 				line-height: 24px; | ||||
| @@ -452,12 +458,14 @@ onBeforeUnmount(() => { | ||||
| 		margin: auto; | ||||
|  | ||||
| 		> .menu { | ||||
| 			padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; | ||||
| 			width: 100%; | ||||
| 			border-radius: 24px; | ||||
| 			border-bottom-right-radius: 0; | ||||
| 			border-bottom-left-radius: 0; | ||||
|  | ||||
| 			> .menuItems { | ||||
| 				padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; | ||||
|  | ||||
| 				> .item { | ||||
| 					font-size: 1em; | ||||
| 					padding: 12px 24px; | ||||
| @@ -478,10 +486,10 @@ onBeforeUnmount(() => { | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .menu { | ||||
| 	padding: 8px 0; | ||||
| 	box-sizing: border-box; | ||||
| 	max-width: 100vw; | ||||
| 	min-width: 200px; | ||||
| @@ -493,6 +501,11 @@ onBeforeUnmount(() => { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .menuItems { | ||||
| 	padding: 8px 0; | ||||
| 	box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| .item { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
|   | ||||
| @@ -76,12 +76,13 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 							:emojiUrls="appearNote.emojis" | ||||
| 							:enableEmojiMenu="true" | ||||
| 							:enableEmojiMenuReaction="true" | ||||
| 							class="_selectable" | ||||
| 						/> | ||||
| 						<div v-if="translating || translation" :class="$style.translation"> | ||||
| 							<MkLoading v-if="translating" mini/> | ||||
| 							<div v-else-if="translation"> | ||||
| 								<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> | ||||
| 								<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> | ||||
| 								<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| @@ -223,6 +224,7 @@ import { focusPrev, focusNext } from '@/utility/focus.js'; | ||||
| import { getAppearNote } from '@/utility/get-appear-note.js'; | ||||
| import { prefer } from '@/preferences.js'; | ||||
| import { getPluginHandlers } from '@/plugin.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| @@ -233,7 +235,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	mock: false, | ||||
| }); | ||||
|  | ||||
| provide('mock', props.mock); | ||||
| provide(DI.mock, props.mock); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'reaction', emoji: string): void; | ||||
|   | ||||
| @@ -97,13 +97,14 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					:emojiUrls="appearNote.emojis" | ||||
| 					:enableEmojiMenu="true" | ||||
| 					:enableEmojiMenuReaction="true" | ||||
| 					class="_selectable" | ||||
| 				/> | ||||
| 				<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> | ||||
| 				<div v-if="translating || translation" :class="$style.translation"> | ||||
| 					<MkLoading v-if="translating" mini/> | ||||
| 					<div v-else-if="translation"> | ||||
| 						<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> | ||||
| 						<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> | ||||
| 						<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div v-if="appearNote.files && appearNote.files.length > 0"> | ||||
|   | ||||
| @@ -40,12 +40,13 @@ import * as Misskey from 'misskey-js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { notePage } from '@/filters/note.js'; | ||||
| import { userPage } from '@/filters/user.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| }>(); | ||||
|  | ||||
| const mock = inject<boolean>('mock', false); | ||||
| const mock = inject(DI.mock, false); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.noNotes }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| @@ -33,10 +33,10 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { shallowRef } from 'vue'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import MkNote from '@/components/MkNote.vue'; | ||||
| import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { infoImageUrl } from '@/instance.js'; | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 		<template #empty> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img :src="infoImageUrl" class="_ghost"/> | ||||
| 				<img :src="infoImageUrl" draggable="false"/> | ||||
| 				<div>{{ i18n.ts.noNotifications }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
|   | ||||
| @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div v-else-if="empty" key="_empty_" class="empty"> | ||||
| 		<slot name="empty"> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img :src="infoImageUrl" class="_ghost"/> | ||||
| 				<img :src="infoImageUrl" draggable="false"/> | ||||
| 				<div>{{ i18n.ts.nothing }}</div> | ||||
| 			</div> | ||||
| 		</slot> | ||||
|   | ||||
| @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		</div> | ||||
| 		<div :class="$style.headerRight"> | ||||
| 			<template v-if="!(channel != null && fixed)"> | ||||
| 				<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> | ||||
| 				<button v-if="channel == null" ref="visibilityButton" v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> | ||||
| 					<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> | ||||
| 					<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> | ||||
| 					<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> | ||||
| @@ -32,15 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					<span :class="$style.headerRightButtonText">{{ channel.name }}</span> | ||||
| 				</button> | ||||
| 			</template> | ||||
| 			<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> | ||||
| 			<button v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> | ||||
| 				<span v-if="!localOnly"><i class="ti ti-rocket"></i></span> | ||||
| 				<span v-else><i class="ti ti-rocket-off"></i></span> | ||||
| 			</button> | ||||
| 			<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> | ||||
| 				<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> | ||||
| 				<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> | ||||
| 				<span v-else><i class="ti ti-icons"></i></span> | ||||
| 			</button> | ||||
| 			<button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button> | ||||
| 			<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> | ||||
| 				<div :class="$style.submitInner"> | ||||
| 					<template v-if="posted"></template> | ||||
| @@ -111,6 +107,7 @@ import { toASCII } from 'punycode.js'; | ||||
| import { host, url } from '@@/js/config.js'; | ||||
| import type { ShallowRef } from 'vue'; | ||||
| import type { PostFormProps } from '@/types/post-form.js'; | ||||
| import type { MenuItem } from '@/types/menu.js'; | ||||
| import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; | ||||
| import MkNotePreview from '@/components/MkNotePreview.vue'; | ||||
| import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; | ||||
| @@ -138,6 +135,7 @@ import { emojiPicker } from '@/utility/emoji-picker.js'; | ||||
| import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js'; | ||||
| import { prefer } from '@/preferences.js'; | ||||
| import { getPluginHandlers } from '@/plugin.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| const $i = signinRequired(); | ||||
|  | ||||
| @@ -155,7 +153,7 @@ const props = withDefaults(defineProps<PostFormProps & { | ||||
| 	initialLocalOnly: undefined, | ||||
| }); | ||||
|  | ||||
| provide('mock', props.mock); | ||||
| provide(DI.mock, props.mock); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'posted'): void; | ||||
| @@ -170,6 +168,7 @@ const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); | ||||
| const cwInputEl = shallowRef<HTMLInputElement | null>(null); | ||||
| const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); | ||||
| const visibilityButton = shallowRef<HTMLElement>(); | ||||
| const otherSettingsButton = shallowRef<HTMLElement>(); | ||||
|  | ||||
| const posting = ref(false); | ||||
| const posted = ref(false); | ||||
| @@ -555,6 +554,47 @@ async function toggleReactionAcceptance() { | ||||
| 	reactionAcceptance.value = select.result; | ||||
| } | ||||
|  | ||||
| //#region その他の設定メニューpopup | ||||
| function showOtherSettings() { | ||||
| 	let reactionAcceptanceIcon = 'ti ti-icons'; | ||||
|  | ||||
| 	if (reactionAcceptance.value === 'likeOnly') { | ||||
| 		reactionAcceptanceIcon = 'ti ti-heart _love'; | ||||
| 	} else if (reactionAcceptance.value === 'likeOnlyForRemote') { | ||||
| 		reactionAcceptanceIcon = 'ti ti-heart-plus'; | ||||
| 	} | ||||
|  | ||||
| 	const menuDef = [{ | ||||
| 		icon: reactionAcceptanceIcon, | ||||
| 		text: i18n.ts.reactionAcceptance, | ||||
| 		action: () => { | ||||
| 			toggleReactionAcceptance(); | ||||
| 		}, | ||||
| 	}, { type: 'divider' }, { | ||||
| 		icon: 'ti ti-trash', | ||||
| 		text: i18n.ts.reset, | ||||
| 		danger: true, | ||||
| 		action: async () => { | ||||
| 			if (props.mock) return; | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'question', | ||||
| 				text: i18n.ts.resetAreYouSure, | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			clear(); | ||||
| 		}, | ||||
| 	}] satisfies MenuItem[]; | ||||
|  | ||||
| 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkPostFormOtherMenu.vue')), { | ||||
| 		items: menuDef, | ||||
| 		textLength: textLength.value, | ||||
| 		src: otherSettingsButton.value, | ||||
| 	}, { | ||||
| 		closed: () => dispose(), | ||||
| 	}); | ||||
| } | ||||
| //#endregion | ||||
|  | ||||
| function pushVisibleUser(user: Misskey.entities.UserDetailed) { | ||||
| 	if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { | ||||
| 		visibleUsers.value.push(user); | ||||
|   | ||||
| @@ -42,6 +42,7 @@ import * as os from '@/os.js'; | ||||
| import { misskeyApi } from '@/utility/misskey-api.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { prefer } from '@/preferences.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); | ||||
|  | ||||
| @@ -50,7 +51,7 @@ const props = defineProps<{ | ||||
| 	detachMediaFn?: (id: string) => void; | ||||
| }>(); | ||||
|  | ||||
| const mock = inject<boolean>('mock', false); | ||||
| const mock = inject(DI.mock, false); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void; | ||||
|   | ||||
							
								
								
									
										128
									
								
								packages/frontend/src/components/MkPostFormOtherMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								packages/frontend/src/components/MkPostFormOtherMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and misskey-project | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()"> | ||||
| 	<MkMenu | ||||
| 		:items="items" | ||||
| 		:align="align" | ||||
| 		:width="width" | ||||
| 		:maxHeight="maxHeight" | ||||
| 		:asDrawer="type === 'drawer'" | ||||
| 		@close="modal?.close()" | ||||
| 	> | ||||
| 		<template #header> | ||||
| 			<div :class="[$style.textCountRoot, { [$style.asDrawer]: type === 'drawer' }]"> | ||||
| 				<div :class="$style.textCountLabel">{{ i18n.ts.textCount }}</div> | ||||
| 				<div | ||||
| 					:class="[$style.textCount, | ||||
| 						{ [$style.danger]: textCountPercentage > 100 }, | ||||
| 						{ [$style.warning]: textCountPercentage > 90 && textCountPercentage <= 100 }, | ||||
| 					]" | ||||
| 				> | ||||
| 					<div :class="$style.textCountGraph"></div> | ||||
| 					<div><span :class="$style.textCountCurrent">{{ number(textLength) }}</span> / {{ number(maxTextLength) }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</MkMenu> | ||||
| </MkModal> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, useTemplateRef } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import MkModal from '@/components/MkModal.vue'; | ||||
| import MkMenu from '@/components/MkMenu.vue'; | ||||
| import { instance } from '@/instance.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import number from '@/filters/number.js'; | ||||
| import type { MenuItem } from '@/types/menu.js'; | ||||
|  | ||||
| const modal = useTemplateRef('modal'); | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	items: MenuItem[]; | ||||
| 	textLength: number; | ||||
| 	align?: 'center' | string; | ||||
| 	width?: number; | ||||
| 	src?: HTMLElement; | ||||
| }>(); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
|  | ||||
| const maxTextLength = computed(() => { | ||||
| 	return instance ? instance.maxNoteTextLength : 1000; | ||||
| }); | ||||
|  | ||||
| const textCountPercentage = computed(() => { | ||||
| 	return props.textLength / maxTextLength.value * 100; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .textCountRoot { | ||||
| 	--textCountBg: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 15%); | ||||
| 	background-color: var(--textCountBg); | ||||
| 	padding: 10px 14px; | ||||
|  | ||||
| 	&.asDrawer { | ||||
| 		padding: 12px 24px; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .textCountLabel { | ||||
| 	font-size: 11px; | ||||
| 	opacity: 0.8; | ||||
| 	margin-bottom: 4px; | ||||
| } | ||||
|  | ||||
| .textCount { | ||||
| 	display: flex; | ||||
| 	gap: var(--MI-marginHalf); | ||||
| 	align-items: center; | ||||
| 	font-size: 12px; | ||||
| 	--countColor: var(--MI_THEME-accent); | ||||
|  | ||||
| 	&.danger { | ||||
| 		--countColor: var(--MI_THEME-error); | ||||
| 	} | ||||
|  | ||||
| 	&.warning { | ||||
| 		--countColor: var(--MI_THEME-warn); | ||||
| 	} | ||||
|  | ||||
| 	.textCountGraph { | ||||
| 		position: relative; | ||||
| 		width: 24px; | ||||
| 		height: 24px; | ||||
| 		border-radius: 50%; | ||||
| 		background-image: conic-gradient( | ||||
| 			var(--countColor) 0% v-bind("Math.min(100, textCountPercentage) + '%'"), | ||||
| 			rgba(0, 0, 0, .2) v-bind("Math.min(100, textCountPercentage) + '%'") 100% | ||||
| 		); | ||||
|  | ||||
| 		&::after { | ||||
| 			content: ''; | ||||
| 			position: absolute; | ||||
| 			width: 16px; | ||||
| 			height: 16px; | ||||
| 			border-radius: 50%; | ||||
| 			background-color: var(--textCountBg); | ||||
| 			top: 50%; | ||||
| 			left: 50%; | ||||
| 			transform: translate(-50%, -50%); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	.textCountCurrent { | ||||
| 		color: var(--countColor); | ||||
| 		font-weight: 700; | ||||
| 		font-size: 18px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	@click="toggleReaction()" | ||||
| 	@contextmenu.prevent.stop="menu" | ||||
| > | ||||
| 	<MkReactionIcon :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> | ||||
| 	<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> | ||||
| 	<span :class="$style.count">{{ count }}</span> | ||||
| </button> | ||||
| </template> | ||||
| @@ -35,6 +35,7 @@ import * as sound from '@/utility/sound.js'; | ||||
| import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; | ||||
| import { customEmojisMap } from '@/custom-emojis.js'; | ||||
| import { prefer } from '@/preferences.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
| @@ -43,7 +44,7 @@ const props = defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| }>(); | ||||
|  | ||||
| const mock = inject<boolean>('mock', false); | ||||
| const mock = inject(DI.mock, false); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'reactionToggled', emoji: string, newCount: number): void; | ||||
|   | ||||
| @@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js'; | ||||
| import { inject, watch, ref } from 'vue'; | ||||
| import XReaction from '@/components/MkReactionsViewer.reaction.vue'; | ||||
| import { prefer } from '@/preferences.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| @@ -30,7 +31,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	maxNumber: Infinity, | ||||
| }); | ||||
|  | ||||
| const mock = inject<boolean>('mock', false); | ||||
| const mock = inject(DI.mock, false); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div> | ||||
| <div class="_selectable"> | ||||
| 	<div :class="$style.label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;"> | ||||
| 		<textarea | ||||
| @@ -38,10 +38,10 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; | ||||
| import { debounce } from 'throttle-debounce'; | ||||
| import type { SuggestionType } from '@/utility/autocomplete.js'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { Autocomplete } from '@/utility/autocomplete.js'; | ||||
| import type { SuggestionType } from '@/utility/autocomplete.js'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	modelValue: string | null; | ||||
|   | ||||
| @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkPagination :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.noUsers }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| @@ -21,9 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import MkUserInfo from '@/components/MkUserInfo.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { infoImageUrl } from '@/instance.js'; | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				translate: getDecorationOffset(decoration), | ||||
| 			}" | ||||
| 			alt="" | ||||
| 			draggable="false" | ||||
| 			style="-webkit-user-drag: none;" | ||||
| 		> | ||||
| 	</template> | ||||
| </component> | ||||
|   | ||||
| @@ -9,6 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" | ||||
| 	src="/client-assets/dummy.png" | ||||
| 	:title="alt" | ||||
| 	draggable="false" | ||||
| 	style="-webkit-user-drag: none;" | ||||
| /> | ||||
| <span v-else-if="errored">:{{ customEmojiName }}:</span> | ||||
| <img | ||||
| @@ -18,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	:alt="alt" | ||||
| 	:title="alt" | ||||
| 	decoding="async" | ||||
| 	draggable="false" | ||||
| 	@error="errored = true" | ||||
| 	@load="errored = false" | ||||
| 	@click="onClick" | ||||
| @@ -157,6 +160,7 @@ async function edit(name: string) { | ||||
| .root { | ||||
| 	height: 2em; | ||||
| 	vertical-align: middle; | ||||
| 	-webkit-user-drag: none; | ||||
| 	transition: transform 0.2s ease; | ||||
|  | ||||
| 	&:hover { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <template> | ||||
| <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear> | ||||
| 	<div :class="$style.root"> | ||||
| 		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> | ||||
| 		<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/> | ||||
| 		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> | ||||
| 		<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton> | ||||
| 	</div> | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and misskey-project | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div :class="[$style.spacer, store.r.darkMode.value ? $style.dark : $style.light]"></div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { store } from '@/store.js'; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .spacer { | ||||
| 	box-sizing: border-box; | ||||
| 	padding: 32px; | ||||
| 	margin: 0 auto; | ||||
| 	height: 300px; | ||||
| 	background-clip: content-box; | ||||
| 	background-size: auto auto; | ||||
| 	background-color: rgba(255, 255, 255, 0); | ||||
|  | ||||
| 	&.light { | ||||
| 		background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #00000010 16px, #00000010 20px ); | ||||
| 	} | ||||
|  | ||||
| 	&.dark { | ||||
| 		background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #FFFFFF16 16px, #FFFFFF16 20px ); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| @@ -21,7 +21,6 @@ import MkError from './global/MkError.vue'; | ||||
| import MkAd from './global/MkAd.vue'; | ||||
| import MkPageHeader from './global/MkPageHeader.vue'; | ||||
| import MkSpacer from './global/MkSpacer.vue'; | ||||
| import MkFooterSpacer from './global/MkFooterSpacer.vue'; | ||||
| import MkStickyContainer from './global/MkStickyContainer.vue'; | ||||
| import MkLazy from './global/MkLazy.vue'; | ||||
| import SearchMarker from './global/SearchMarker.vue'; | ||||
| @@ -55,7 +54,6 @@ export const components = { | ||||
| 	MkAd: MkAd, | ||||
| 	MkPageHeader: MkPageHeader, | ||||
| 	MkSpacer: MkSpacer, | ||||
| 	MkFooterSpacer: MkFooterSpacer, | ||||
| 	MkStickyContainer: MkStickyContainer, | ||||
| 	MkLazy: MkLazy, | ||||
| 	SearchMarker: SearchMarker, | ||||
| @@ -83,7 +81,6 @@ declare module '@vue/runtime-core' { | ||||
| 		MkAd: typeof MkAd; | ||||
| 		MkPageHeader: typeof MkPageHeader; | ||||
| 		MkSpacer: typeof MkSpacer; | ||||
| 		MkFooterSpacer: typeof MkFooterSpacer; | ||||
| 		MkStickyContainer: typeof MkStickyContainer; | ||||
| 		MkLazy: typeof MkLazy; | ||||
| 		SearchMarker: typeof SearchMarker; | ||||
|   | ||||
| @@ -9,4 +9,5 @@ import type { IRouter } from '@/nirax.js'; | ||||
| export const DI = { | ||||
| 	routerCurrentDepth: Symbol() as InjectionKey<number>, | ||||
| 	router: Symbol() as InjectionKey<IRouter>, | ||||
| 	mock: Symbol() as InjectionKey<boolean>, | ||||
| }; | ||||
|   | ||||
| @@ -5,4 +5,4 @@ | ||||
|  | ||||
| import { numberFormat } from '@@/js/intl-const.js'; | ||||
|  | ||||
| export default n => n == null ? 'N/A' : numberFormat.format(n); | ||||
| export default (n?: number) => n == null ? 'N/A' : numberFormat.format(n); | ||||
|   | ||||
| @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkLoading v-if="!loaded"/> | ||||
| <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear> | ||||
| 	<div v-show="loaded" :class="$style.root"> | ||||
| 		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/> | ||||
| 		<img :src="serverErrorImageUrl" draggable="false" :class="$style.img"/> | ||||
| 		<div class="_gaps"> | ||||
| 			<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div> | ||||
| 			<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div> | ||||
|   | ||||
| @@ -17,11 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, computed } from 'vue'; | ||||
| import type { Ref } from 'vue'; | ||||
| import * as config from '@@/js/config.js'; | ||||
| import XQueue from './queue.chart.vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import type { Ref } from 'vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import * as config from '@@/js/config.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePage } from '@/page.js'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| @@ -54,14 +54,7 @@ function promoteAllQueues() { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| const headerActions = computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'ti ti-external-link', | ||||
| 	text: i18n.ts.dashboard, | ||||
| 	handler: () => { | ||||
| 		window.open(config.url + '/queue', '_blank', 'noopener'); | ||||
| 	}, | ||||
| }]); | ||||
| const headerActions = computed(() => []); | ||||
|  | ||||
| const headerTabs = computed(() => [{ | ||||
| 	key: 'deliver', | ||||
|   | ||||
| @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 						<MkPagination :pagination="usersPagination"> | ||||
| 							<template #empty> | ||||
| 								<div class="_fullinfo"> | ||||
| 									<img :src="infoImageUrl" class="_ghost"/> | ||||
| 									<img :src="infoImageUrl" draggable="false"/> | ||||
| 									<div>{{ i18n.ts.noUsers }}</div> | ||||
| 								</div> | ||||
| 							</template> | ||||
|   | ||||
| @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div v-else class="_fullinfo"> | ||||
| 		<img :src="infoImageUrl" class="_ghost"/> | ||||
| 		<img :src="infoImageUrl" draggable="false"/> | ||||
| 		<div>{{ i18n.ts.nothing }}</div> | ||||
| 	</div> | ||||
| </div> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		<MkPagination :pagination="pagination"> | ||||
| 			<template #empty> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img :src="infoImageUrl" class="_ghost"/> | ||||
| 					<img :src="infoImageUrl" draggable="false"/> | ||||
| 					<div>{{ i18n.ts.noNotes }}</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
|   | ||||
| @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				<MkPagination ref="paginationComponent" :pagination="pagination"> | ||||
| 					<template #empty> | ||||
| 						<div class="_fullinfo"> | ||||
| 							<img :src="infoImageUrl" class="_ghost"/> | ||||
| 							<img :src="infoImageUrl" draggable="false"/> | ||||
| 							<div>{{ i18n.ts.noFollowRequests }}</div> | ||||
| 						</div> | ||||
| 					</template> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<template #header><MkPageHeader/></template> | ||||
| 	<MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200"> | ||||
| 		<div :class="$style.root"> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/> | ||||
| 			<div :class="$style.text"> | ||||
| 				<i class="ti ti-alert-triangle"></i> | ||||
| 				{{ i18n.ts.nothing }} | ||||
| @@ -36,12 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <script lang="ts" setup> | ||||
| import { computed, ref, shallowRef } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import * as os from '@/os.js'; | ||||
| import { misskeyApi } from '@/utility/misskey-api.js'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import type { Paging } from '@/components/MkPagination.vue'; | ||||
| import MkInviteCode from '@/components/MkInviteCode.vue'; | ||||
| import { definePage } from '@/page.js'; | ||||
| import { serverErrorImageUrl, instance } from '@/instance.js'; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="error != null" :contentMax="1200"> | ||||
| 		<div :class="$style.root"> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/> | ||||
| 			<p :class="$style.text"> | ||||
| 				<i class="ti ti-alert-triangle"></i> | ||||
| 				{{ i18n.ts.nothing }} | ||||
|   | ||||
| @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		<div> | ||||
| 			<div v-if="antennas.length === 0" class="empty"> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img :src="infoImageUrl" class="_ghost"/> | ||||
| 					<img :src="infoImageUrl" draggable="false"/> | ||||
| 					<div>{{ i18n.ts.nothing }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		<div class="_gaps"> | ||||
| 			<div v-if="items.length === 0" class="empty"> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img :src="infoImageUrl" class="_ghost"/> | ||||
| 					<img :src="infoImageUrl" draggable="false"/> | ||||
| 					<div>{{ i18n.ts.nothing }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <template> | ||||
| <div> | ||||
| 	<div class="_fullinfo"> | ||||
| 		<img :src="notFoundImageUrl" class="_ghost"/> | ||||
| 		<img :src="notFoundImageUrl" draggable="false"/> | ||||
| 		<div>{{ i18n.ts.notFoundDescription }}</div> | ||||
| 	</div> | ||||
| </div> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="error != null" :contentMax="1200"> | ||||
| 		<div :class="$style.root"> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> | ||||
| 			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/> | ||||
| 			<p :class="$style.text"> | ||||
| 				<i class="ti ti-alert-triangle"></i> | ||||
| 				{{ error }} | ||||
| @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			<div v-if="role">{{ role.description }}</div> | ||||
| 			<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/> | ||||
| 			<div v-else-if="!visible" class="_fullinfo"> | ||||
| 				<img :src="infoImageUrl" class="_ghost"/> | ||||
| 				<img :src="infoImageUrl" draggable="false"/> | ||||
| 				<div>{{ i18n.ts.nothing }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> | ||||
| 		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/> | ||||
| 		<div v-else-if="!visible" class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.nothing }}</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| @@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <script lang="ts" setup> | ||||
| import { computed, watch, ref } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { instanceName } from '@@/js/config.js'; | ||||
| import { misskeyApi } from '@/utility/misskey-api.js'; | ||||
| import MkUserList from '@/components/MkUserList.vue'; | ||||
| import { definePage } from '@/page.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import MkTimeline from '@/components/MkTimeline.vue'; | ||||
| import { instanceName } from '@@/js/config.js'; | ||||
| import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
|   | ||||
| @@ -58,6 +58,15 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					</MkSwitch> | ||||
| 				</MkPreferenceContainer> | ||||
| 			</SearchMarker> | ||||
|  | ||||
| 			<SearchMarker :keywords="['text', 'selectable']"> | ||||
| 				<MkPreferenceContainer k="makeEveryTextElementsSelectable"> | ||||
| 					<MkSwitch v-model="makeEveryTextElementsSelectable"> | ||||
| 						<template #label><SearchLabel>{{ i18n.ts._settings.makeEveryTextElementsSelectable }}</SearchLabel></template> | ||||
| 						<template #caption>{{ i18n.ts._settings.makeEveryTextElementsSelectable_description }}</template> | ||||
| 					</MkSwitch> | ||||
| 				</MkPreferenceContainer> | ||||
| 			</SearchMarker> | ||||
| 		</div> | ||||
|  | ||||
| 		<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']"> | ||||
| @@ -122,6 +131,7 @@ const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe'); | ||||
| const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer'); | ||||
| const contextMenu = prefer.model('contextMenu'); | ||||
| const menuStyle = prefer.model('menuStyle'); | ||||
| const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable'); | ||||
|  | ||||
| const fontSize = ref(miLocalStorage.getItem('fontSize')); | ||||
| const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); | ||||
| @@ -147,6 +157,7 @@ watch([ | ||||
| 	contextMenu, | ||||
| 	fontSize, | ||||
| 	useSystemFont, | ||||
| 	makeEveryTextElementsSelectable, | ||||
| ], async () => { | ||||
| 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); | ||||
| }); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<FormPagination ref="list" :pagination="pagination"> | ||||
| 		<template #empty> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img :src="infoImageUrl" class="_ghost"/> | ||||
| 				<img :src="infoImageUrl" draggable="false"/> | ||||
| 				<div>{{ i18n.ts.nothing }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
|   | ||||
| @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					</FormLink> | ||||
|  | ||||
| 					<MkFolder :defaultOpen="true"> | ||||
| 						<template #label><SearchLabel>{{ i18n.ts.manage }}</SearchLabel></template> | ||||
| 						<template #label>{{ i18n.ts.manage }}</template> | ||||
|  | ||||
| 						<MkPagination :pagination="pagination"> | ||||
| 							<template #default="{items}"> | ||||
|   | ||||
| @@ -27,7 +27,6 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| 	<MkFooterSpacer/> | ||||
| </mkstickycontainer> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					<MkPagination :pagination="renoteMutingPagination"> | ||||
| 						<template #empty> | ||||
| 							<div class="_fullinfo"> | ||||
| 								<img :src="infoImageUrl" class="_ghost"/> | ||||
| 								<img :src="infoImageUrl" draggable="false"/> | ||||
| 								<div>{{ i18n.ts.noUsers }}</div> | ||||
| 							</div> | ||||
| 						</template> | ||||
| @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					<MkPagination :pagination="mutingPagination"> | ||||
| 						<template #empty> | ||||
| 							<div class="_fullinfo"> | ||||
| 								<img :src="infoImageUrl" class="_ghost"/> | ||||
| 								<img :src="infoImageUrl" draggable="false"/> | ||||
| 								<div>{{ i18n.ts.noUsers }}</div> | ||||
| 							</div> | ||||
| 						</template> | ||||
| @@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					<MkPagination :pagination="blockingPagination"> | ||||
| 						<template #empty> | ||||
| 							<div class="_fullinfo"> | ||||
| 								<img :src="infoImageUrl" class="_ghost"/> | ||||
| 								<img :src="infoImageUrl" draggable="false"/> | ||||
| 								<div>{{ i18n.ts.noUsers }}</div> | ||||
| 							</div> | ||||
| 						</template> | ||||
|   | ||||
| @@ -89,7 +89,6 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 							<template #caption> | ||||
| 								<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div> | ||||
| 								<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div> | ||||
| 								<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div> | ||||
| 							</template> | ||||
| 						</MkSwitch> | ||||
| 					</SearchMarker> | ||||
|   | ||||
| @@ -46,7 +46,16 @@ export const PREF_DEF = { | ||||
| 	}, | ||||
| 	widgets: { | ||||
| 		accountDependent: true, | ||||
| 		default: [] as { | ||||
| 		default: [{ | ||||
| 			name: 'calendar', | ||||
| 			id: 'a', place: 'right', data: {}, | ||||
| 		}, { | ||||
| 			name: 'notifications', | ||||
| 			id: 'b', place: 'right', data: {}, | ||||
| 		}, { | ||||
| 			name: 'trends', | ||||
| 			id: 'c', place: 'right', data: {}, | ||||
| 		}] as { | ||||
| 			name: string; | ||||
| 			id: string; | ||||
| 			place: string | null; | ||||
| @@ -313,6 +322,9 @@ export const PREF_DEF = { | ||||
| 	defaultFollowWithReplies: { | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	makeEveryTextElementsSelectable: { | ||||
| 		default: DEFAULT_DEVICE_KIND === 'desktop', | ||||
| 	}, | ||||
| 	plugins: { | ||||
| 		default: [] as Plugin[], | ||||
| 	}, | ||||
|   | ||||
| @@ -23,11 +23,10 @@ import { deepEqual } from '@/utility/deep-equal.js'; | ||||
|  | ||||
| type PREF = typeof PREF_DEF; | ||||
| type ValueOf<K extends keyof PREF> = PREF[K]['default']; | ||||
| type Account = string; // <host>/<userId> | ||||
|  | ||||
| type Scope = Partial<{ | ||||
| 	server: string | null; // 将来のため | ||||
| 	account: Account | null; | ||||
| 	server: string | null; // host | ||||
| 	account: string | null; // userId | ||||
| 	device: string | null; // 将来のため | ||||
| }>; | ||||
|  | ||||
| @@ -39,7 +38,7 @@ type PrefRecord<K extends keyof PREF> = [scope: Scope, value: ValueOf<K>, meta: | ||||
|  | ||||
| function parseScope(scope: Scope): { | ||||
| 	server: string | null; | ||||
| 	account: Account | null; | ||||
| 	account: string | null; | ||||
| 	device: string | null; | ||||
| } { | ||||
| 	return { | ||||
| @@ -51,7 +50,7 @@ function parseScope(scope: Scope): { | ||||
|  | ||||
| function makeScope(scope: Partial<{ | ||||
| 	server: string | null; | ||||
| 	account: Account | null; | ||||
| 	account: string | null; | ||||
| 	device: string | null; | ||||
| }>): Scope { | ||||
| 	const c = {} as Scope; | ||||
| @@ -130,6 +129,10 @@ export class PreferencesManager { | ||||
| 		return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true; | ||||
| 	} | ||||
|  | ||||
| 	private isServerDependentKey<K extends keyof PREF>(key: K): boolean { | ||||
| 		return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true; | ||||
| 	} | ||||
|  | ||||
| 	private rewriteRawState<K extends keyof PREF>(key: K, value: ValueOf<K>) { | ||||
| 		const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除 | ||||
| 		this.r[key].value = this.s[key] = v; | ||||
| @@ -141,9 +144,19 @@ export class PreferencesManager { | ||||
| 		this.rewriteRawState(key, value); | ||||
|  | ||||
| 		const record = this.getMatchedRecordOf(key); | ||||
|  | ||||
| 		if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) { | ||||
| 			this.profile.preferences[key].push([makeScope({ | ||||
| 				account: `${host}/${$i!.id}`, | ||||
| 				server: host, | ||||
| 				account: $i!.id, | ||||
| 			}), value, {}]); | ||||
| 			this.save(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (parseScope(record[0]).server == null && this.isServerDependentKey(key)) { | ||||
| 			this.profile.preferences[key].push([makeScope({ | ||||
| 				server: host, | ||||
| 			}), value, {}]); | ||||
| 			this.save(); | ||||
| 			return; | ||||
| @@ -291,16 +304,19 @@ export class PreferencesManager { | ||||
|  | ||||
| 		if ($i == null) return records.find(([scope, v]) => parseScope(scope).account == null)!; | ||||
|  | ||||
| 		const accountOverrideRecord = records.find(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`); | ||||
| 		const accountOverrideRecord = records.find(([scope, v]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id); | ||||
| 		if (accountOverrideRecord) return accountOverrideRecord; | ||||
|  | ||||
| 		const serverOverrideRecord = records.find(([scope, v]) => parseScope(scope).server === host && parseScope(scope).account == null); | ||||
| 		if (serverOverrideRecord) return serverOverrideRecord; | ||||
|  | ||||
| 		const record = records.find(([scope, v]) => parseScope(scope).account == null); | ||||
| 		return record!; | ||||
| 	} | ||||
|  | ||||
| 	public isAccountOverrided<K extends keyof PREF>(key: K): boolean { | ||||
| 		if ($i == null) return false; | ||||
| 		return this.profile.preferences[key].some(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`) ?? false; | ||||
| 		return this.profile.preferences[key].some(([scope, v]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id) ?? false; | ||||
| 	} | ||||
|  | ||||
| 	public setAccountOverride<K extends keyof PREF>(key: K) { | ||||
| @@ -310,7 +326,8 @@ export class PreferencesManager { | ||||
|  | ||||
| 		const records = this.profile.preferences[key]; | ||||
| 		records.push([makeScope({ | ||||
| 			account: `${host}/${$i!.id}`, | ||||
| 			server: host, | ||||
| 			account: $i!.id, | ||||
| 		}), this.s[key], {}]); | ||||
|  | ||||
| 		this.save(); | ||||
| @@ -322,7 +339,7 @@ export class PreferencesManager { | ||||
|  | ||||
| 		const records = this.profile.preferences[key]; | ||||
|  | ||||
| 		const index = records.findIndex(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`); | ||||
| 		const index = records.findIndex(([scope, v]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id); | ||||
| 		if (index === -1) return; | ||||
|  | ||||
| 		records.splice(index, 1); | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
|  | ||||
| import { inject } from 'vue'; | ||||
| import type { IRouter } from '@/nirax.js'; | ||||
| import { Router } from '@/nirax.js'; | ||||
| import { mainRouter } from '@/router/main.js'; | ||||
| import { DI } from '@/di.js'; | ||||
|  | ||||
| @@ -14,7 +13,7 @@ import { DI } from '@/di.js'; | ||||
|  * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない) | ||||
|  */ | ||||
| export function useRouter(): IRouter { | ||||
| 	return inject<Router | null>(DI.router, null) ?? mainRouter; | ||||
| 	return inject(DI.router, null) ?? mainRouter; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -178,7 +178,18 @@ export const store = markRaw(new Storage('base', { | ||||
| 	}, | ||||
| 	menu: { | ||||
| 		where: 'deviceAccount', | ||||
| 		default: [], | ||||
| 		default: [ | ||||
| 			'notifications', | ||||
| 			'clips', | ||||
| 			'drive', | ||||
| 			'followRequests', | ||||
| 			'-', | ||||
| 			'explore', | ||||
| 			'announcements', | ||||
| 			'search', | ||||
| 			'-', | ||||
| 			'ui', | ||||
| 		], | ||||
| 	}, | ||||
| 	statusbars: { | ||||
| 		where: 'deviceAccount', | ||||
|   | ||||
| @@ -81,6 +81,11 @@ html { | ||||
| 	&.useSystemFont { | ||||
| 		font-family: system-ui; | ||||
| 	} | ||||
|  | ||||
| 	&:not(.forceSelectableAll) { | ||||
| 		user-select: none; | ||||
| 		-webkit-user-select: none; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| html._themeChanging_ { | ||||
| @@ -120,6 +125,8 @@ a { | ||||
| textarea, input { | ||||
| 	tap-highlight-color: transparent; | ||||
| 	-webkit-tap-highlight-color: transparent; | ||||
| 	user-select: text; | ||||
| 	-webkit-user-select: text; | ||||
| } | ||||
|  | ||||
| optgroup, option { | ||||
| @@ -184,6 +191,16 @@ rt { | ||||
| 	padding: 0.3em 0.5em; | ||||
| } | ||||
|  | ||||
| ._selectable { | ||||
| 	user-select: text; | ||||
| 	-webkit-user-select: text; | ||||
| } | ||||
|  | ||||
| ._selectableAtomic { | ||||
| 	user-select: all; | ||||
| 	-webkit-user-select: all; | ||||
| } | ||||
|  | ||||
| ._noSelect { | ||||
| 	user-select: none; | ||||
| 	-webkit-user-select: none; | ||||
| @@ -197,11 +214,6 @@ rt { | ||||
| 	text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| ._ghost { | ||||
| 	@extend ._noSelect; | ||||
| 	pointer-events: none; | ||||
| } | ||||
|  | ||||
| ._modalBg { | ||||
| 	position: fixed; | ||||
| 	top: 0; | ||||
| @@ -424,6 +436,10 @@ rt { | ||||
| 	color: var(--MI_THEME-link); | ||||
| } | ||||
|  | ||||
| ._love { | ||||
| 	color: var(--MI_THEME-love); | ||||
| } | ||||
|  | ||||
| ._caption { | ||||
| 	font-size: 0.8em; | ||||
| 	opacity: 0.7; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div class="body"> | ||||
| 		<div class="left"> | ||||
| 			<button v-click-anime class="item _button instance" @click="openInstanceMenu"> | ||||
| 				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> | ||||
| 				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/> | ||||
| 			</button> | ||||
| 			<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> | ||||
| 				<i class="ti ti-home ti-fw"></i> | ||||
|   | ||||
| @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div class="divider"></div> | ||||
| 	<div class="about"> | ||||
| 		<button v-click-anime class="item _button" @click="openInstanceMenu"> | ||||
| 			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> | ||||
| 			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/> | ||||
| 		</button> | ||||
| 	</div> | ||||
| 	<!--<MisskeyLogo class="misskey"/>--> | ||||
|   | ||||
| @@ -144,19 +144,6 @@ if (window.innerWidth < 1024) { | ||||
|  | ||||
| document.documentElement.style.overflowY = 'scroll'; | ||||
|  | ||||
| if (prefer.s.widgets.length === 0) { | ||||
| 	prefer.commit('widgets', [{ | ||||
| 		name: 'calendar', | ||||
| 		id: 'a', place: null, data: {}, | ||||
| 	}, { | ||||
| 		name: 'notifications', | ||||
| 		id: 'b', place: null, data: {}, | ||||
| 	}, { | ||||
| 		name: 'trends', | ||||
| 		id: 'c', place: null, data: {}, | ||||
| 	}]); | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
| 	window.addEventListener('resize', () => { | ||||
| 		isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); | ||||
|   | ||||
| @@ -178,19 +178,6 @@ if (window.innerWidth > 1024) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| if (prefer.s.widgets.length === 0) { | ||||
| 	prefer.commit('widgets', [{ | ||||
| 		name: 'calendar', | ||||
| 		id: 'a', place: 'right', data: {}, | ||||
| 	}, { | ||||
| 		name: 'notifications', | ||||
| 		id: 'b', place: 'right', data: {}, | ||||
| 	}, { | ||||
| 		name: 'trends', | ||||
| 		id: 'c', place: 'right', data: {}, | ||||
| 	}]); | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
| 	if (!isDesktop.value) { | ||||
| 		window.addEventListener('resize', () => { | ||||
|   | ||||
| @@ -248,12 +248,12 @@ export const searchIndexes: SearchIndexItem[] = [ | ||||
| 						keywords: ['login', 'signin'], | ||||
| 					}, | ||||
| 					{ | ||||
| 						id: 'lUtOQbnwi', | ||||
| 						id: '5RbESWefG', | ||||
| 						label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore, | ||||
| 						keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription], | ||||
| 					}, | ||||
| 					{ | ||||
| 						id: '83WWcjwS9', | ||||
| 						id: 'hdzwDs3qd', | ||||
| 						label: i18n.ts._accountSettings.makeNotesHiddenBefore, | ||||
| 						keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription], | ||||
| 					}, | ||||
| @@ -791,7 +791,7 @@ export const searchIndexes: SearchIndexItem[] = [ | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: '5VSGOVYR0', | ||||
| 				label: i18n.ts.manage, | ||||
| 				label: i18n.ts._settings.webhook, | ||||
| 				keywords: ['webhook'], | ||||
| 			}, | ||||
| 		], | ||||
| @@ -897,22 +897,27 @@ export const searchIndexes: SearchIndexItem[] = [ | ||||
| 				keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: '1fV9WINCQ', | ||||
| 				id: 'b1GYEEJeh', | ||||
| 				label: i18n.ts._settings.makeEveryTextElementsSelectable, | ||||
| 				keywords: ['text', 'selectable'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: 'vVLxwINTJ', | ||||
| 				label: i18n.ts.menuStyle, | ||||
| 				keywords: ['menu', 'style', 'popup', 'drawer'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: 'mLQzlKUNu', | ||||
| 				id: '14cMhMLHL', | ||||
| 				label: i18n.ts._contextMenu.title, | ||||
| 				keywords: ['contextmenu', 'system', 'native'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: 'yP96aA3j9', | ||||
| 				id: 'oSo4LXMX9', | ||||
| 				label: i18n.ts.fontSize, | ||||
| 				keywords: ['font', 'size'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				id: 'jQeiMopFE', | ||||
| 				id: '7LQSAThST', | ||||
| 				label: i18n.ts.useSystemFont, | ||||
| 				keywords: ['font', 'system', 'native'], | ||||
| 			}, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar> | ||||
| 		</div> | ||||
| 		<div v-else :class="$style.bdayFFallback"> | ||||
| 			<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/> | ||||
| 			<img :src="infoImageUrl" draggable="false" :class="$style.bdayFFallbackImage"/> | ||||
| 			<div>{{ i18n.ts.nothing }}</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
|   | ||||
| @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div class="ekmkgxbj"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo"> | ||||
| 			<img :src="infoImageUrl" class="_ghost"/> | ||||
| 			<img :src="infoImageUrl" draggable="false"/> | ||||
| 			<div>{{ i18n.ts.nothing }}</div> | ||||
| 		</div> | ||||
| 		<div v-else :class="$style.feed"> | ||||
| @@ -25,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <script lang="ts" setup> | ||||
| import { ref, watch, computed } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { url as base } from '@@/js/config.js'; | ||||
| import { useInterval } from '@@/js/use-interval.js'; | ||||
| import { useWidgetPropsManager } from './widget.js'; | ||||
| import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; | ||||
| import type { GetFormResultType } from '@/utility/form.js'; | ||||
| import MkContainer from '@/components/MkContainer.vue'; | ||||
| import { url as base } from '@@/js/config.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { useInterval } from '@@/js/use-interval.js'; | ||||
| import { infoImageUrl } from '@/instance.js'; | ||||
|  | ||||
| const name = 'rss'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
| 	"type": "module", | ||||
| 	"name": "misskey-js", | ||||
| 	"version": "2025.3.2-beta.0", | ||||
| 	"version": "2025.3.2-beta.2", | ||||
| 	"description": "Misskey SDK for JavaScript", | ||||
| 	"license": "MIT", | ||||
| 	"main": "./built/index.js", | ||||
|   | ||||
							
								
								
									
										72
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										72
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -98,15 +98,6 @@ importers: | ||||
|       '@aws-sdk/lib-storage': | ||||
|         specifier: 3.749.0 | ||||
|         version: 3.749.0(@aws-sdk/client-s3@3.749.0) | ||||
|       '@bull-board/api': | ||||
|         specifier: 6.7.7 | ||||
|         version: 6.7.7(@bull-board/ui@6.7.7) | ||||
|       '@bull-board/fastify': | ||||
|         specifier: 6.7.7 | ||||
|         version: 6.7.7 | ||||
|       '@bull-board/ui': | ||||
|         specifier: 6.7.7 | ||||
|         version: 6.7.7 | ||||
|       '@discordapp/twemoji': | ||||
|         specifier: 15.1.0 | ||||
|         version: 15.1.0 | ||||
| @@ -1868,17 +1859,6 @@ packages: | ||||
|     resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} | ||||
|     engines: {node: '>=18'} | ||||
|  | ||||
|   '@bull-board/api@6.7.7': | ||||
|     resolution: {integrity: sha512-jSBe+aeNs41T/BUJNutKSM17hJigDLoOaAzUZyFwT63/Yt00hiqQo90THXmDi3vGdXtTruGlkrC9OhVxBKo1eQ==} | ||||
|     peerDependencies: | ||||
|       '@bull-board/ui': 6.7.7 | ||||
|  | ||||
|   '@bull-board/fastify@6.7.7': | ||||
|     resolution: {integrity: sha512-EVxpRW0ag/tVPqfHm6s/3P6X5DnzKTr0J5lI1EgOvpe+OXavaPWkU0iLPepyyC6ls+k0djdkd1ix1PP/caqufw==} | ||||
|  | ||||
|   '@bull-board/ui@6.7.7': | ||||
|     resolution: {integrity: sha512-QU3OkaJVIUt1SpRRV/XxPSTD9tmJcwBWi1oa4ND+qGWQigQ2H1PYfpQCNFOlyW8qCkBwkSDn8pLwlyGbppWqJg==} | ||||
|  | ||||
|   '@bundled-es-modules/cookie@2.0.1': | ||||
|     resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} | ||||
|  | ||||
| @@ -5911,11 +5891,6 @@ packages: | ||||
|   ee-first@1.1.1: | ||||
|     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} | ||||
|  | ||||
|   ejs@3.1.10: | ||||
|     resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} | ||||
|     engines: {node: '>=0.10.0'} | ||||
|     hasBin: true | ||||
|  | ||||
|   electron-to-chromium@1.5.83: | ||||
|     resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==} | ||||
|  | ||||
| @@ -6366,9 +6341,6 @@ packages: | ||||
|     resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} | ||||
|     engines: {node: '>=18'} | ||||
|  | ||||
|   filelist@1.0.4: | ||||
|     resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} | ||||
|  | ||||
|   filename-reserved-regex@3.0.0: | ||||
|     resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} | ||||
|     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} | ||||
| @@ -7238,11 +7210,6 @@ packages: | ||||
|     resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==} | ||||
|     engines: {node: 20 || >=22} | ||||
|  | ||||
|   jake@10.8.5: | ||||
|     resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} | ||||
|     engines: {node: '>=10'} | ||||
|     hasBin: true | ||||
|  | ||||
|   jest-changed-files@29.7.0: | ||||
|     resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} | ||||
|     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} | ||||
| @@ -9151,9 +9118,6 @@ packages: | ||||
|     resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} | ||||
|     engines: {node: '>=4'} | ||||
|  | ||||
|   redis-info@3.1.0: | ||||
|     resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} | ||||
|  | ||||
|   redis-lock@0.1.4: | ||||
|     resolution: {integrity: sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==} | ||||
|     engines: {node: '>=0.6'} | ||||
| @@ -11618,23 +11582,6 @@ snapshots: | ||||
|  | ||||
|   '@bcoe/v8-coverage@1.0.2': {} | ||||
|  | ||||
|   '@bull-board/api@6.7.7(@bull-board/ui@6.7.7)': | ||||
|     dependencies: | ||||
|       '@bull-board/ui': 6.7.7 | ||||
|       redis-info: 3.1.0 | ||||
|  | ||||
|   '@bull-board/fastify@6.7.7': | ||||
|     dependencies: | ||||
|       '@bull-board/api': 6.7.7(@bull-board/ui@6.7.7) | ||||
|       '@bull-board/ui': 6.7.7 | ||||
|       '@fastify/static': 8.1.0 | ||||
|       '@fastify/view': 10.0.2 | ||||
|       ejs: 3.1.10 | ||||
|  | ||||
|   '@bull-board/ui@6.7.7': | ||||
|     dependencies: | ||||
|       '@bull-board/api': 6.7.7(@bull-board/ui@6.7.7) | ||||
|  | ||||
|   '@bundled-es-modules/cookie@2.0.1': | ||||
|     dependencies: | ||||
|       cookie: 0.7.2 | ||||
| @@ -16450,10 +16397,6 @@ snapshots: | ||||
|  | ||||
|   ee-first@1.1.1: {} | ||||
|  | ||||
|   ejs@3.1.10: | ||||
|     dependencies: | ||||
|       jake: 10.8.5 | ||||
|  | ||||
|   electron-to-chromium@1.5.83: {} | ||||
|  | ||||
|   emittery@0.13.1: {} | ||||
| @@ -17214,10 +17157,6 @@ snapshots: | ||||
|       token-types: 6.0.0 | ||||
|       uint8array-extras: 1.4.0 | ||||
|  | ||||
|   filelist@1.0.4: | ||||
|     dependencies: | ||||
|       minimatch: 5.1.2 | ||||
|  | ||||
|   filename-reserved-regex@3.0.0: {} | ||||
|  | ||||
|   filenamify@6.0.0: | ||||
| @@ -18134,13 +18073,6 @@ snapshots: | ||||
|     optionalDependencies: | ||||
|       '@pkgjs/parseargs': 0.11.0 | ||||
|  | ||||
|   jake@10.8.5: | ||||
|     dependencies: | ||||
|       async: 3.2.4 | ||||
|       chalk: 4.1.2 | ||||
|       filelist: 1.0.4 | ||||
|       minimatch: 3.1.2 | ||||
|  | ||||
|   jest-changed-files@29.7.0: | ||||
|     dependencies: | ||||
|       execa: 5.1.1 | ||||
| @@ -20546,10 +20478,6 @@ snapshots: | ||||
|  | ||||
|   redis-errors@1.2.0: {} | ||||
|  | ||||
|   redis-info@3.1.0: | ||||
|     dependencies: | ||||
|       lodash: 4.17.21 | ||||
|  | ||||
|   redis-lock@0.1.4: {} | ||||
|  | ||||
|   redis-parser@3.0.0: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user