Merge branch 'develop' into future-2024-05-31
This commit is contained in:
		
						commit
						5dc8c2827c
					
				
					 32 changed files with 415 additions and 222 deletions
				
			
		| 
						 | 
					@ -302,5 +302,10 @@ checkActivityPubGetSignature: false
 | 
				
			||||||
# Upload or download file size limits (bytes)
 | 
					# Upload or download file size limits (bytes)
 | 
				
			||||||
#maxFileSize: 262144000
 | 
					#maxFileSize: 262144000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# timeout and maximum size for imports (e.g. note imports)
 | 
				
			||||||
 | 
					#import:
 | 
				
			||||||
 | 
					#  downloadTimeout: 30
 | 
				
			||||||
 | 
					#  maxFileSize: 262144000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# PID File of master process
 | 
					# PID File of master process
 | 
				
			||||||
#pidFile: /tmp/misskey.pid
 | 
					#pidFile: /tmp/misskey.pid
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -694,6 +694,7 @@ channel: "Channels"
 | 
				
			||||||
create: "Create"
 | 
					create: "Create"
 | 
				
			||||||
notificationSetting: "Notification settings"
 | 
					notificationSetting: "Notification settings"
 | 
				
			||||||
notificationSettingDesc: "Select the types of notification to display."
 | 
					notificationSettingDesc: "Select the types of notification to display."
 | 
				
			||||||
 | 
					enableFaviconNotificationDot: "Enable favicon notification dot"
 | 
				
			||||||
useGlobalSetting: "Use global settings"
 | 
					useGlobalSetting: "Use global settings"
 | 
				
			||||||
useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
 | 
					useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
 | 
				
			||||||
other: "Other"
 | 
					other: "Other"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -2792,6 +2792,10 @@ export interface Locale extends ILocale {
 | 
				
			||||||
     * 表示する通知の種別を選択してください。
 | 
					     * 表示する通知の種別を選択してください。
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "notificationSettingDesc": string;
 | 
					    "notificationSettingDesc": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ファビコン通知ドットを有効にする
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "enableFaviconNotificationDot": string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * グローバル設定を使う
 | 
					     * グローバル設定を使う
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -694,6 +694,7 @@ channel: "チャンネル"
 | 
				
			||||||
create: "作成"
 | 
					create: "作成"
 | 
				
			||||||
notificationSetting: "通知設定"
 | 
					notificationSetting: "通知設定"
 | 
				
			||||||
notificationSettingDesc: "表示する通知の種別を選択してください。"
 | 
					notificationSettingDesc: "表示する通知の種別を選択してください。"
 | 
				
			||||||
 | 
					enableFaviconNotificationDot: "ファビコン通知ドットを有効にする"
 | 
				
			||||||
useGlobalSetting: "グローバル設定を使う"
 | 
					useGlobalSetting: "グローバル設定を使う"
 | 
				
			||||||
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
 | 
					useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
 | 
				
			||||||
other: "その他"
 | 
					other: "その他"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,6 +100,12 @@ type Source = {
 | 
				
			||||||
	perChannelMaxNoteCacheCount?: number;
 | 
						perChannelMaxNoteCacheCount?: number;
 | 
				
			||||||
	perUserNotificationsMaxCount?: number;
 | 
						perUserNotificationsMaxCount?: number;
 | 
				
			||||||
	deactivateAntennaThreshold?: number;
 | 
						deactivateAntennaThreshold?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import?: {
 | 
				
			||||||
 | 
							downloadTimeout: number;
 | 
				
			||||||
 | 
							maxFileSize: number;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pidFile: string;
 | 
						pidFile: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -182,6 +188,12 @@ export type Config = {
 | 
				
			||||||
	perChannelMaxNoteCacheCount: number;
 | 
						perChannelMaxNoteCacheCount: number;
 | 
				
			||||||
	perUserNotificationsMaxCount: number;
 | 
						perUserNotificationsMaxCount: number;
 | 
				
			||||||
	deactivateAntennaThreshold: number;
 | 
						deactivateAntennaThreshold: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import: {
 | 
				
			||||||
 | 
							downloadTimeout: number;
 | 
				
			||||||
 | 
							maxFileSize: number;
 | 
				
			||||||
 | 
						} | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pidFile: string;
 | 
						pidFile: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,6 +303,7 @@ export function loadConfig(): Config {
 | 
				
			||||||
		perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
 | 
							perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
 | 
				
			||||||
		perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
 | 
							perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
 | 
				
			||||||
		deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
 | 
							deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
 | 
				
			||||||
 | 
							import: config.import,
 | 
				
			||||||
		pidFile: config.pidFile,
 | 
							pidFile: config.pidFile,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -436,4 +449,5 @@ function applyEnvOverrides(config: Source) {
 | 
				
			||||||
	_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
 | 
						_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
 | 
				
			||||||
	_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]);
 | 
						_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]);
 | 
				
			||||||
	_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
 | 
						_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
 | 
				
			||||||
 | 
						_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,14 +35,14 @@ export class DownloadService {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async downloadUrl(url: string, path: string): Promise<{
 | 
						public async downloadUrl(url: string, path: string, options: { timeout?: number, operationTimeout?: number, maxSize?: number} = {} ): Promise<{
 | 
				
			||||||
		filename: string;
 | 
							filename: string;
 | 
				
			||||||
	}> {
 | 
						}> {
 | 
				
			||||||
		this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`);
 | 
							this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const timeout = 30 * 1000;
 | 
							const timeout = options.timeout ?? 30 * 1000;
 | 
				
			||||||
		const operationTimeout = 60 * 1000;
 | 
							const operationTimeout = options.operationTimeout ?? 60 * 1000;
 | 
				
			||||||
		const maxSize = this.config.maxFileSize ?? 262144000;
 | 
							const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const urlObj = new URL(url);
 | 
							const urlObj = new URL(url);
 | 
				
			||||||
		let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
 | 
							let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -464,10 +464,10 @@ export class MfmService {
 | 
				
			||||||
		return new XMLSerializer().serializeToString(body);
 | 
							return new XMLSerializer().serializeToString(body);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// the toMastoHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
 | 
						// the toMastoApiHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async toMastoHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) {
 | 
						public async toMastoApiHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) {
 | 
				
			||||||
		if (nodes == null) {
 | 
							if (nodes == null) {
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -485,174 +485,174 @@ export class MfmService {
 | 
				
			||||||
		const handlers: {
 | 
							const handlers: {
 | 
				
			||||||
            [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any;
 | 
					            [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any;
 | 
				
			||||||
    } = {
 | 
					    } = {
 | 
				
			||||||
			async bold(node) {
 | 
					    	async bold(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				el.textContent = '**';
 | 
					    		el.textContent = '**';
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				el.textContent += '**';
 | 
					    		el.textContent += '**';
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async small(node) {
 | 
					    	async small(node) {
 | 
				
			||||||
				const el = doc.createElement('small');
 | 
					    		const el = doc.createElement('small');
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async strike(node) {
 | 
					    	async strike(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				el.textContent = '~~';
 | 
					    		el.textContent = '~~';
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				el.textContent += '~~';
 | 
					    		el.textContent += '~~';
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async italic(node) {
 | 
					    	async italic(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				el.textContent = '*';
 | 
					    		el.textContent = '*';
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				el.textContent += '*';
 | 
					    		el.textContent += '*';
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async fn(node) {
 | 
					    	async fn(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				el.textContent = '*';
 | 
					    		el.textContent = '*';
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				el.textContent += '*';
 | 
					    		el.textContent += '*';
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			blockCode(node) {
 | 
					    	blockCode(node) {
 | 
				
			||||||
				const pre = doc.createElement('pre');
 | 
					    		const pre = doc.createElement('pre');
 | 
				
			||||||
				const inner = doc.createElement('code');
 | 
					    		const inner = doc.createElement('code');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const nodes = node.props.code
 | 
					    		const nodes = node.props.code
 | 
				
			||||||
					.split(/\r\n|\r|\n/)
 | 
					    			.split(/\r\n|\r|\n/)
 | 
				
			||||||
					.map((x) => doc.createTextNode(x));
 | 
					    			.map((x) => doc.createTextNode(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
 | 
					    		for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
 | 
				
			||||||
					inner.appendChild(x === 'br' ? doc.createElement('br') : x);
 | 
					    			inner.appendChild(x === 'br' ? doc.createElement('br') : x);
 | 
				
			||||||
				}
 | 
					    		}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				pre.appendChild(inner);
 | 
					    		pre.appendChild(inner);
 | 
				
			||||||
				return pre;
 | 
					    		return pre;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async center(node) {
 | 
					    	async center(node) {
 | 
				
			||||||
				const el = doc.createElement('div');
 | 
					    		const el = doc.createElement('div');
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			emojiCode(node) {
 | 
					    	emojiCode(node) {
 | 
				
			||||||
				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
 | 
					    		return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			unicodeEmoji(node) {
 | 
					    	unicodeEmoji(node) {
 | 
				
			||||||
				return doc.createTextNode(node.props.emoji);
 | 
					    		return doc.createTextNode(node.props.emoji);
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			hashtag: (node) => {
 | 
					    	hashtag: (node) => {
 | 
				
			||||||
				const a = doc.createElement('a');
 | 
					    		const a = doc.createElement('a');
 | 
				
			||||||
				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
 | 
					    		a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
 | 
				
			||||||
				a.textContent = `#${node.props.hashtag}`;
 | 
					    		a.textContent = `#${node.props.hashtag}`;
 | 
				
			||||||
				a.setAttribute('rel', 'tag');
 | 
					    		a.setAttribute('rel', 'tag');
 | 
				
			||||||
				a.setAttribute('class', 'hashtag');
 | 
					    		a.setAttribute('class', 'hashtag');
 | 
				
			||||||
				return a;
 | 
					    		return a;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			inlineCode(node) {
 | 
					    	inlineCode(node) {
 | 
				
			||||||
				const el = doc.createElement('code');
 | 
					    		const el = doc.createElement('code');
 | 
				
			||||||
				el.textContent = node.props.code;
 | 
					    		el.textContent = node.props.code;
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			mathInline(node) {
 | 
					    	mathInline(node) {
 | 
				
			||||||
				const el = doc.createElement('code');
 | 
					    		const el = doc.createElement('code');
 | 
				
			||||||
				el.textContent = node.props.formula;
 | 
					    		el.textContent = node.props.formula;
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			mathBlock(node) {
 | 
					    	mathBlock(node) {
 | 
				
			||||||
				const el = doc.createElement('code');
 | 
					    		const el = doc.createElement('code');
 | 
				
			||||||
				el.textContent = node.props.formula;
 | 
					    		el.textContent = node.props.formula;
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async link(node) {
 | 
					    	async link(node) {
 | 
				
			||||||
				const a = doc.createElement('a');
 | 
					    		const a = doc.createElement('a');
 | 
				
			||||||
				a.setAttribute('rel', 'nofollow noopener noreferrer');
 | 
					    		a.setAttribute('rel', 'nofollow noopener noreferrer');
 | 
				
			||||||
				a.setAttribute('target', '_blank');
 | 
					    		a.setAttribute('target', '_blank');
 | 
				
			||||||
				a.setAttribute('href', node.props.url);
 | 
					    		a.setAttribute('href', node.props.url);
 | 
				
			||||||
				await appendChildren(node.children, a);
 | 
					    		await appendChildren(node.children, a);
 | 
				
			||||||
				return a;
 | 
					    		return a;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async mention(node) {
 | 
					    	async mention(node) {
 | 
				
			||||||
				const { username, host, acct } = node.props;
 | 
					    		const { username, host, acct } = node.props;
 | 
				
			||||||
				const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
 | 
					    		const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				if (!resolved) {
 | 
					    		if (!resolved) {
 | 
				
			||||||
					el.textContent = acct;
 | 
					    			el.textContent = acct;
 | 
				
			||||||
				} else {
 | 
					    		} else {
 | 
				
			||||||
					el.setAttribute('class', 'h-card');
 | 
					    			el.setAttribute('class', 'h-card');
 | 
				
			||||||
					el.setAttribute('translate', 'no');
 | 
					    			el.setAttribute('translate', 'no');
 | 
				
			||||||
					const a = doc.createElement('a');
 | 
					    			const a = doc.createElement('a');
 | 
				
			||||||
					a.setAttribute('href', resolved.url ? resolved.url : resolved.uri);
 | 
					    			a.setAttribute('href', resolved.url ? resolved.url : resolved.uri);
 | 
				
			||||||
					a.className = 'u-url mention';
 | 
					    			a.className = 'u-url mention';
 | 
				
			||||||
					const span = doc.createElement('span');
 | 
					    			const span = doc.createElement('span');
 | 
				
			||||||
					span.textContent = resolved.username || username;
 | 
					    			span.textContent = resolved.username || username;
 | 
				
			||||||
					a.textContent = '@';
 | 
					    			a.textContent = '@';
 | 
				
			||||||
					a.appendChild(span);
 | 
					    			a.appendChild(span);
 | 
				
			||||||
					el.appendChild(a);
 | 
					    			el.appendChild(a);
 | 
				
			||||||
				}
 | 
					    		}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async quote(node) {
 | 
					    	async quote(node) {
 | 
				
			||||||
				const el = doc.createElement('blockquote');
 | 
					    		const el = doc.createElement('blockquote');
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			text(node) {
 | 
					    	text(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				const nodes = node.props.text
 | 
					    		const nodes = node.props.text
 | 
				
			||||||
					.split(/\r\n|\r|\n/)
 | 
					    			.split(/\r\n|\r|\n/)
 | 
				
			||||||
					.map((x) => doc.createTextNode(x));
 | 
					    			.map((x) => doc.createTextNode(x));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
 | 
					    		for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
 | 
				
			||||||
					el.appendChild(x === 'br' ? doc.createElement('br') : x);
 | 
					    			el.appendChild(x === 'br' ? doc.createElement('br') : x);
 | 
				
			||||||
				}
 | 
					    		}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			url(node) {
 | 
					    	url(node) {
 | 
				
			||||||
				const a = doc.createElement('a');
 | 
					    		const a = doc.createElement('a');
 | 
				
			||||||
				a.setAttribute('rel', 'nofollow noopener noreferrer');
 | 
					    		a.setAttribute('rel', 'nofollow noopener noreferrer');
 | 
				
			||||||
				a.setAttribute('target', '_blank');
 | 
					    		a.setAttribute('target', '_blank');
 | 
				
			||||||
				a.setAttribute('href', node.props.url);
 | 
					    		a.setAttribute('href', node.props.url);
 | 
				
			||||||
				a.textContent = node.props.url.replace(/^https?:\/\//, '');
 | 
					    		a.textContent = node.props.url.replace(/^https?:\/\//, '');
 | 
				
			||||||
				return a;
 | 
					    		return a;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			search: (node) => {
 | 
					    	search: (node) => {
 | 
				
			||||||
				const a = doc.createElement('a');
 | 
					    		const a = doc.createElement('a');
 | 
				
			||||||
				a.setAttribute('href', `https"google.com/${node.props.query}`);
 | 
					    		a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
 | 
				
			||||||
				a.textContent = node.props.content;
 | 
					    		a.textContent = node.props.content;
 | 
				
			||||||
				return a;
 | 
					    		return a;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async plain(node) {
 | 
					    	async plain(node) {
 | 
				
			||||||
				const el = doc.createElement('span');
 | 
					    		const el = doc.createElement('span');
 | 
				
			||||||
				await appendChildren(node.children, el);
 | 
					    		await appendChildren(node.children, el);
 | 
				
			||||||
				return el;
 | 
					    		return el;
 | 
				
			||||||
			},
 | 
					    	},
 | 
				
			||||||
		};
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await appendChildren(nodes, doc.body);
 | 
							await appendChildren(nodes, doc.body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -627,6 +627,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
				
			||||||
			userHost: user.host,
 | 
								userHost: user.host,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// should really not happen, but better safe than sorry
 | 
				
			||||||
 | 
							if (data.reply?.id === insert.id) {
 | 
				
			||||||
 | 
								throw new Error("A note can't reply to itself");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (data.renote?.id === insert.id) {
 | 
				
			||||||
 | 
								throw new Error("A note can't renote itself");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (data.uri != null) insert.uri = data.uri;
 | 
							if (data.uri != null) insert.uri = data.uri;
 | 
				
			||||||
		if (data.url != null) insert.url = data.url;
 | 
							if (data.url != null) insert.url = data.url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -299,6 +299,10 @@ export class NoteEditService implements OnApplicationShutdown {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (this.isRenote(data)) {
 | 
							if (this.isRenote(data)) {
 | 
				
			||||||
 | 
								if (data.renote.id === oldnote.id) {
 | 
				
			||||||
 | 
									throw new Error("A note can't renote itself");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			switch (data.renote.visibility) {
 | 
								switch (data.renote.visibility) {
 | 
				
			||||||
				case 'public':
 | 
									case 'public':
 | 
				
			||||||
					// public noteは無条件にrenote可能
 | 
										// public noteは無条件にrenote可能
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -248,7 +248,7 @@ export class ApNoteService {
 | 
				
			||||||
			> => {
 | 
								> => {
 | 
				
			||||||
				if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 | 
									if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					const res = await this.resolveNote(uri);
 | 
										const res = await this.resolveNote(uri, { resolver });
 | 
				
			||||||
					if (res == null) return { status: 'permerror' };
 | 
										if (res == null) return { status: 'permerror' };
 | 
				
			||||||
					return { status: 'ok', res };
 | 
										return { status: 'ok', res };
 | 
				
			||||||
				} catch (e) {
 | 
									} catch (e) {
 | 
				
			||||||
| 
						 | 
					@ -473,7 +473,7 @@ export class ApNoteService {
 | 
				
			||||||
			> => {
 | 
								> => {
 | 
				
			||||||
				if (!/^https?:/.test(uri)) return { status: 'permerror' };
 | 
									if (!/^https?:/.test(uri)) return { status: 'permerror' };
 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					const res = await this.resolveNote(uri);
 | 
										const res = await this.resolveNote(uri, { resolver });
 | 
				
			||||||
					if (res == null) return { status: 'permerror' };
 | 
										if (res == null) return { status: 'permerror' };
 | 
				
			||||||
					return { status: 'ok', res };
 | 
										return { status: 'ok', res };
 | 
				
			||||||
				} catch (e) {
 | 
									} catch (e) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,5 +4,5 @@
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function sqlLikeEscape(s: string) {
 | 
					export function sqlLikeEscape(s: string) {
 | 
				
			||||||
	return s.replace(/([%_])/g, '\\$1');
 | 
						return s.replace(/([%_\\])/g, '\\$1');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,12 +19,16 @@ import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
					import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
				
			||||||
import type * as Bull from 'bullmq';
 | 
					import type * as Bull from 'bullmq';
 | 
				
			||||||
import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
 | 
					import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
 | 
				
			||||||
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class ImportNotesProcessorService {
 | 
					export class ImportNotesProcessorService {
 | 
				
			||||||
	private logger: Logger;
 | 
						private logger: Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
 | 
							@Inject(DI.config)
 | 
				
			||||||
 | 
							private config: Config,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		@Inject(DI.usersRepository)
 | 
							@Inject(DI.usersRepository)
 | 
				
			||||||
		private usersRepository: UsersRepository,
 | 
							private usersRepository: UsersRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,6 +77,11 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@bindThis
 | 
				
			||||||
 | 
						private downloadUrl(url: string, path:string): Promise<{filename: string}> {
 | 
				
			||||||
 | 
							return this.downloadService.downloadUrl(url, path, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
 | 
						private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
 | 
				
			||||||
		type NotesMap = {
 | 
							type NotesMap = {
 | 
				
			||||||
| 
						 | 
					@ -176,7 +185,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await fsp.writeFile(destPath, '', 'binary');
 | 
									await fsp.writeFile(destPath, '', 'binary');
 | 
				
			||||||
				await this.downloadService.downloadUrl(file.url, destPath);
 | 
									await this.downloadUrl(file.url, destPath);
 | 
				
			||||||
			} catch (e) { // TODO: 何度か再試行
 | 
								} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
				if (e instanceof Error || typeof e === 'string') {
 | 
									if (e instanceof Error || typeof e === 'string') {
 | 
				
			||||||
					this.logger.error(e);
 | 
										this.logger.error(e);
 | 
				
			||||||
| 
						 | 
					@ -206,7 +215,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await fsp.writeFile(destPath, '', 'binary');
 | 
									await fsp.writeFile(destPath, '', 'binary');
 | 
				
			||||||
				await this.downloadService.downloadUrl(file.url, destPath);
 | 
									await this.downloadUrl(file.url, destPath);
 | 
				
			||||||
			} catch (e) { // TODO: 何度か再試行
 | 
								} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
				if (e instanceof Error || typeof e === 'string') {
 | 
									if (e instanceof Error || typeof e === 'string') {
 | 
				
			||||||
					this.logger.error(e);
 | 
										this.logger.error(e);
 | 
				
			||||||
| 
						 | 
					@ -239,7 +248,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await fsp.writeFile(destPath, '', 'binary');
 | 
									await fsp.writeFile(destPath, '', 'binary');
 | 
				
			||||||
				await this.downloadService.downloadUrl(file.url, destPath);
 | 
									await this.downloadUrl(file.url, destPath);
 | 
				
			||||||
			} catch (e) { // TODO: 何度か再試行
 | 
								} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
				if (e instanceof Error || typeof e === 'string') {
 | 
									if (e instanceof Error || typeof e === 'string') {
 | 
				
			||||||
					this.logger.error(e);
 | 
										this.logger.error(e);
 | 
				
			||||||
| 
						 | 
					@ -297,7 +306,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await fsp.writeFile(path, '', 'utf-8');
 | 
									await fsp.writeFile(path, '', 'utf-8');
 | 
				
			||||||
				await this.downloadService.downloadUrl(file.url, path);
 | 
									await this.downloadUrl(file.url, path);
 | 
				
			||||||
			} catch (e) { // TODO: 何度か再試行
 | 
								} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
				if (e instanceof Error || typeof e === 'string') {
 | 
									if (e instanceof Error || typeof e === 'string') {
 | 
				
			||||||
					this.logger.error(e);
 | 
										this.logger.error(e);
 | 
				
			||||||
| 
						 | 
					@ -349,7 +358,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!exists) {
 | 
									if (!exists) {
 | 
				
			||||||
					try {
 | 
										try {
 | 
				
			||||||
						await this.downloadService.downloadUrl(file.url, filePath);
 | 
											await this.downloadUrl(file.url, filePath);
 | 
				
			||||||
					} catch (e) { // TODO: 何度か再試行
 | 
										} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
						this.logger.error(e instanceof Error ? e : new Error(e as string));
 | 
											this.logger.error(e instanceof Error ? e : new Error(e as string));
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					@ -488,7 +497,7 @@ export class ImportNotesProcessorService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!exists) {
 | 
									if (!exists) {
 | 
				
			||||||
					try {
 | 
										try {
 | 
				
			||||||
						await this.downloadService.downloadUrl(file.url, filePath);
 | 
											await this.downloadUrl(file.url, filePath);
 | 
				
			||||||
					} catch (e) { // TODO: 何度か再試行
 | 
										} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
						this.logger.error(e instanceof Error ? e : new Error(e as string));
 | 
											this.logger.error(e instanceof Error ? e : new Error(e as string));
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,6 +66,7 @@ export const paramDef = {
 | 
				
			||||||
	properties: {
 | 
						properties: {
 | 
				
			||||||
		query: { type: 'string', nullable: true, default: null },
 | 
							query: { type: 'string', nullable: true, default: null },
 | 
				
			||||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
							limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
				
			||||||
 | 
							offset: { type: 'integer', minimum: 1, nullable: true, default: null },
 | 
				
			||||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
							sinceId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
							untilId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					@ -91,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
				
			||||||
				//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
 | 
									//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
 | 
				
			||||||
				//const emojis = await q.limit(ps.limit).getMany();
 | 
									//const emojis = await q.limit(ps.limit).getMany();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany();
 | 
									emojis = await q.orderBy('length(emoji.name)', 'ASC').addOrderBy('id', 'DESC').getMany();
 | 
				
			||||||
				const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug);
 | 
									const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (queryarry) {
 | 
									if (queryarry) {
 | 
				
			||||||
| 
						 | 
					@ -105,9 +106,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
				
			||||||
						emoji.aliases.some(a => a.includes(queryNfc)) ||
 | 
											emoji.aliases.some(a => a.includes(queryNfc)) ||
 | 
				
			||||||
						emoji.category?.includes(queryNfc));
 | 
											emoji.category?.includes(queryNfc));
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				emojis.splice(ps.limit + 1);
 | 
									emojis = emojis.slice((ps.offset ?? 0), ((ps.offset ?? 0) + ps.limit));
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				emojis = await q.limit(ps.limit).getMany();
 | 
									emojis = await q.take(ps.limit).skip(ps.offset ?? 0).getMany();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return this.emojiEntityService.packDetailedMany(emojis);
 | 
								return this.emojiEntityService.packDetailedMany(emojis);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,8 +30,8 @@ export const meta = {
 | 
				
			||||||
	prohibitMoved: true,
 | 
						prohibitMoved: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	limit: {
 | 
						limit: {
 | 
				
			||||||
		duration: ms('1hour'),
 | 
							duration: ms('1minute'),
 | 
				
			||||||
		max: 300,
 | 
							max: 5,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	kind: 'write:notes',
 | 
						kind: 'write:notes',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,7 +110,7 @@ export class MastoConverters {
 | 
				
			||||||
	private async encodeField(f: Entity.Field): Promise<Entity.Field> {
 | 
						private async encodeField(f: Entity.Field): Promise<Entity.Field> {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			name: f.name,
 | 
								name: f.name,
 | 
				
			||||||
			value: await this.mfmService.toMastoHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
 | 
								value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
 | 
				
			||||||
			verified_at: null,
 | 
								verified_at: null,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -179,7 +179,7 @@ export class MastoConverters {
 | 
				
			||||||
			const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
 | 
								const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
 | 
				
			||||||
			const item = {
 | 
								const item = {
 | 
				
			||||||
				account: noteUser,
 | 
									account: noteUser,
 | 
				
			||||||
				content: this.mfmService.toMastoHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
 | 
									content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
 | 
				
			||||||
				created_at: lastDate.toISOString(),
 | 
									created_at: lastDate.toISOString(),
 | 
				
			||||||
				emojis: [],
 | 
									emojis: [],
 | 
				
			||||||
				sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
 | 
									sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
 | 
				
			||||||
| 
						 | 
					@ -240,7 +240,7 @@ export class MastoConverters {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const content = note.text !== null
 | 
							const content = note.text !== null
 | 
				
			||||||
			? quoteUri.then(quoteUri => this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
 | 
								? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
 | 
				
			||||||
				.then(p => p ?? escapeMFM(note.text!))
 | 
									.then(p => p ?? escapeMFM(note.text!))
 | 
				
			||||||
			: '';
 | 
								: '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -334,10 +334,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 | 
				
			||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renoting = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keymap = {
 | 
					const keymap = {
 | 
				
			||||||
	'r': () => reply(true),
 | 
						'r': () => reply(true),
 | 
				
			||||||
	'e|a|plus': () => react(true),
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
	'q': () => renote(appearNote.value.visibility),
 | 
						'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 | 
				
			||||||
	'up|k|shift+tab': focusBefore,
 | 
						'up|k|shift+tab': focusBefore,
 | 
				
			||||||
	'down|j|tab': focusAfter,
 | 
						'down|j|tab': focusAfter,
 | 
				
			||||||
	'esc': blur,
 | 
						'esc': blur,
 | 
				
			||||||
| 
						 | 
					@ -464,7 +466,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				os.toast(i18n.ts.renoted);
 | 
									os.toast(i18n.ts.renoted);
 | 
				
			||||||
				renoted.value = true;
 | 
									renoted.value = true;
 | 
				
			||||||
			});
 | 
								}).finally(() => { renoting = false });
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
						} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
				
			||||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
							const el = renoteButton.value as HTMLElement | null | undefined;
 | 
				
			||||||
| 
						 | 
					@ -483,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				os.toast(i18n.ts.renoted);
 | 
									os.toast(i18n.ts.renoted);
 | 
				
			||||||
				renoted.value = true;
 | 
									renoted.value = true;
 | 
				
			||||||
			});
 | 
								}).finally(() => renoting = false);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -346,10 +346,12 @@ if ($i) {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renoting = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keymap = {
 | 
					const keymap = {
 | 
				
			||||||
	'r': () => reply(true),
 | 
						'r': () => reply(true),
 | 
				
			||||||
	'e|a|plus': () => react(true),
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
	'q': () => renote(appearNote.value.visibility),
 | 
						'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 | 
				
			||||||
	'esc': blur,
 | 
						'esc': blur,
 | 
				
			||||||
	'm|o': () => showMenu(true),
 | 
						'm|o': () => showMenu(true),
 | 
				
			||||||
	's': () => showContent.value !== showContent.value,
 | 
						's': () => showContent.value !== showContent.value,
 | 
				
			||||||
| 
						 | 
					@ -489,7 +491,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
		}).then(() => {
 | 
							}).then(() => {
 | 
				
			||||||
			os.toast(i18n.ts.renoted);
 | 
								os.toast(i18n.ts.renoted);
 | 
				
			||||||
			renoted.value = true;
 | 
								renoted.value = true;
 | 
				
			||||||
		});
 | 
							}).finally(() => { renoting = false });
 | 
				
			||||||
	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
						} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
				
			||||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
							const el = renoteButton.value as HTMLElement | null | undefined;
 | 
				
			||||||
		if (el) {
 | 
							if (el) {
 | 
				
			||||||
| 
						 | 
					@ -506,7 +508,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
		}).then(() => {
 | 
							}).then(() => {
 | 
				
			||||||
			os.toast(i18n.ts.renoted);
 | 
								os.toast(i18n.ts.renoted);
 | 
				
			||||||
			renoted.value = true;
 | 
								renoted.value = true;
 | 
				
			||||||
		});
 | 
							}).finally(() => { renoting = false });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,7 +73,7 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints>
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	reversed?: boolean;
 | 
						reversed?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	offsetMode?: boolean;
 | 
						offsetMode?: boolean | ComputedRef<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pageEl?: HTMLElement;
 | 
						pageEl?: HTMLElement;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -240,10 +240,11 @@ const fetchMore = async (): Promise<void> => {
 | 
				
			||||||
	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 | 
						if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 | 
				
			||||||
	moreFetching.value = true;
 | 
						moreFetching.value = true;
 | 
				
			||||||
	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 | 
						const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 | 
				
			||||||
 | 
						const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
 | 
				
			||||||
	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 | 
						await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 | 
				
			||||||
		...params,
 | 
							...params,
 | 
				
			||||||
		limit: SECOND_FETCH_LIMIT,
 | 
							limit: SECOND_FETCH_LIMIT,
 | 
				
			||||||
		...(props.pagination.offsetMode ? {
 | 
							...(offsetMode ? {
 | 
				
			||||||
			offset: offset.value,
 | 
								offset: offset.value,
 | 
				
			||||||
		} : {
 | 
							} : {
 | 
				
			||||||
			untilId: Array.from(items.value.keys()).at(-1),
 | 
								untilId: Array.from(items.value.keys()).at(-1),
 | 
				
			||||||
| 
						 | 
					@ -304,10 +305,11 @@ const fetchMoreAhead = async (): Promise<void> => {
 | 
				
			||||||
	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 | 
						if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 | 
				
			||||||
	moreFetching.value = true;
 | 
						moreFetching.value = true;
 | 
				
			||||||
	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 | 
						const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 | 
				
			||||||
 | 
						const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
 | 
				
			||||||
	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 | 
						await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 | 
				
			||||||
		...params,
 | 
							...params,
 | 
				
			||||||
		limit: SECOND_FETCH_LIMIT,
 | 
							limit: SECOND_FETCH_LIMIT,
 | 
				
			||||||
		...(props.pagination.offsetMode ? {
 | 
							...(offsetMode ? {
 | 
				
			||||||
			offset: offset.value,
 | 
								offset: offset.value,
 | 
				
			||||||
		} : {
 | 
							} : {
 | 
				
			||||||
			sinceId: Array.from(items.value.keys()).at(-1),
 | 
								sinceId: Array.from(items.value.keys()).at(-1),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
	<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 | 
						<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 | 
				
			||||||
	<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 | 
						<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 | 
				
			||||||
		<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
 | 
							<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
 | 
				
			||||||
		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 | 
							<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 | 
				
			||||||
		<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 | 
							<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
 | 
						<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -335,10 +335,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 | 
				
			||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renoting = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keymap = {
 | 
					const keymap = {
 | 
				
			||||||
	'r': () => reply(true),
 | 
						'r': () => reply(true),
 | 
				
			||||||
	'e|a|plus': () => react(true),
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
	'q': () => renote(appearNote.value.visibility),
 | 
						'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 | 
				
			||||||
	'up|k|shift+tab': focusBefore,
 | 
						'up|k|shift+tab': focusBefore,
 | 
				
			||||||
	'down|j|tab': focusAfter,
 | 
						'down|j|tab': focusAfter,
 | 
				
			||||||
	'esc': blur,
 | 
						'esc': blur,
 | 
				
			||||||
| 
						 | 
					@ -465,7 +467,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				os.toast(i18n.ts.renoted);
 | 
									os.toast(i18n.ts.renoted);
 | 
				
			||||||
				renoted.value = true;
 | 
									renoted.value = true;
 | 
				
			||||||
			});
 | 
								}).finally(() => { renoting = false });
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
						} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
				
			||||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
							const el = renoteButton.value as HTMLElement | null | undefined;
 | 
				
			||||||
| 
						 | 
					@ -484,7 +486,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				os.toast(i18n.ts.renoted);
 | 
									os.toast(i18n.ts.renoted);
 | 
				
			||||||
				renoted.value = true;
 | 
									renoted.value = true;
 | 
				
			||||||
			});
 | 
								}).finally(() => { renoting = false });
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -355,10 +355,12 @@ if ($i) {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renoting = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keymap = {
 | 
					const keymap = {
 | 
				
			||||||
	'r': () => reply(true),
 | 
						'r': () => reply(true),
 | 
				
			||||||
	'e|a|plus': () => react(true),
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
	'q': () => renote(appearNote.value.visibility),
 | 
						'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
 | 
				
			||||||
	'esc': blur,
 | 
						'esc': blur,
 | 
				
			||||||
	'm|o': () => showMenu(true),
 | 
						'm|o': () => showMenu(true),
 | 
				
			||||||
	's': () => showContent.value !== showContent.value,
 | 
						's': () => showContent.value !== showContent.value,
 | 
				
			||||||
| 
						 | 
					@ -498,7 +500,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
		}).then(() => {
 | 
							}).then(() => {
 | 
				
			||||||
			os.toast(i18n.ts.renoted);
 | 
								os.toast(i18n.ts.renoted);
 | 
				
			||||||
			renoted.value = true;
 | 
								renoted.value = true;
 | 
				
			||||||
		});
 | 
							}).finally(() => { renoting = false });
 | 
				
			||||||
	} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
						} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
 | 
				
			||||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
							const el = renoteButton.value as HTMLElement | null | undefined;
 | 
				
			||||||
		if (el) {
 | 
							if (el) {
 | 
				
			||||||
| 
						 | 
					@ -515,7 +517,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
 | 
				
			||||||
		}).then(() => {
 | 
							}).then(() => {
 | 
				
			||||||
			os.toast(i18n.ts.renoted);
 | 
								os.toast(i18n.ts.renoted);
 | 
				
			||||||
			renoted.value = true;
 | 
								renoted.value = true;
 | 
				
			||||||
		});
 | 
							}).finally(() => { renoting = false });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -393,67 +393,67 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'center': {
 | 
								case 'center': {
 | 
				
			||||||
				return [h('div', {
 | 
									return [h('bdi',h('div', {
 | 
				
			||||||
					style: 'text-align:center;',
 | 
										style: 'text-align:center;',
 | 
				
			||||||
				}, genEl(token.children, scale))];
 | 
									}, genEl(token.children, scale)))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'url': {
 | 
								case 'url': {
 | 
				
			||||||
				return [h(MkUrl, {
 | 
									return [h('bdi',h(MkUrl, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					url: token.props.url,
 | 
										url: token.props.url,
 | 
				
			||||||
					rel: 'nofollow noopener',
 | 
										rel: 'nofollow noopener',
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'link': {
 | 
								case 'link': {
 | 
				
			||||||
				return [h(MkLink, {
 | 
									return [h('bdi',h(MkLink, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					url: token.props.url,
 | 
										url: token.props.url,
 | 
				
			||||||
					rel: 'nofollow noopener',
 | 
										rel: 'nofollow noopener',
 | 
				
			||||||
				}, genEl(token.children, scale, true))];
 | 
									}, genEl(token.children, scale, true)))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'mention': {
 | 
								case 'mention': {
 | 
				
			||||||
				return [h(MkMention, {
 | 
									return [h('bdi',h(MkMention, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
 | 
										host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
 | 
				
			||||||
					username: token.props.username,
 | 
										username: token.props.username,
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'hashtag': {
 | 
								case 'hashtag': {
 | 
				
			||||||
				return [h(MkA, {
 | 
									return [h('bdi',h(MkA, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
 | 
										to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
 | 
				
			||||||
					style: 'color:var(--hashtag);',
 | 
										style: 'color:var(--hashtag);',
 | 
				
			||||||
				}, `#${token.props.hashtag}`)];
 | 
									}, `#${token.props.hashtag}`))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'blockCode': {
 | 
								case 'blockCode': {
 | 
				
			||||||
				return [h(MkCode, {
 | 
									return [h('bdi',h(MkCode, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					code: token.props.code,
 | 
										code: token.props.code,
 | 
				
			||||||
					lang: token.props.lang ?? undefined,
 | 
										lang: token.props.lang ?? undefined,
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'inlineCode': {
 | 
								case 'inlineCode': {
 | 
				
			||||||
				return [h(MkCodeInline, {
 | 
									return [h('bdi',h(MkCodeInline, {
 | 
				
			||||||
					key: Math.random(),
 | 
										key: Math.random(),
 | 
				
			||||||
					code: token.props.code,
 | 
										code: token.props.code,
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'quote': {
 | 
								case 'quote': {
 | 
				
			||||||
				if (!props.nowrap) {
 | 
									if (!props.nowrap) {
 | 
				
			||||||
					return [h('div', {
 | 
										return [h('bdi',h('div', {
 | 
				
			||||||
						style: QUOTE_STYLE,
 | 
											style: QUOTE_STYLE,
 | 
				
			||||||
					}, genEl(token.children, scale, true))];
 | 
										}, genEl(token.children, scale, true)))];
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					return [h('span', {
 | 
										return [h('bdi',h('span', {
 | 
				
			||||||
						style: QUOTE_STYLE,
 | 
											style: QUOTE_STYLE,
 | 
				
			||||||
					}, genEl(token.children, scale, true))];
 | 
										}, genEl(token.children, scale, true)))];
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -497,17 +497,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'mathInline': {
 | 
								case 'mathInline': {
 | 
				
			||||||
				return [h(MkFormula, {
 | 
									return [h('bdi',h(MkFormula, {
 | 
				
			||||||
					formula: token.props.formula,
 | 
										formula: token.props.formula,
 | 
				
			||||||
					block: false,
 | 
										block: false,
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'mathBlock': {
 | 
								case 'mathBlock': {
 | 
				
			||||||
				return [h(MkFormula, {
 | 
									return [h('bdi',h(MkFormula, {
 | 
				
			||||||
					formula: token.props.formula,
 | 
										formula: token.props.formula,
 | 
				
			||||||
					block: true,
 | 
										block: true,
 | 
				
			||||||
				})];
 | 
									}))];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			case 'search': {
 | 
								case 'search': {
 | 
				
			||||||
| 
						 | 
					@ -530,8 +530,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}).flat(Infinity) as (VNode | string)[];
 | 
						}).flat(Infinity) as (VNode | string)[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return h('span', {
 | 
						return h('bdi', h('span', {
 | 
				
			||||||
		// https://codeday.me/jp/qa/20190424/690106.html
 | 
							// https://codeday.me/jp/qa/20190424/690106.html
 | 
				
			||||||
		style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
 | 
							style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
 | 
				
			||||||
	}, genEl(rootAst, props.rootScale ?? 1));
 | 
						}, genEl(rootAst, props.rootScale ?? 1)));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@
 | 
				
			||||||
			style-src 'self' 'unsafe-inline';
 | 
								style-src 'self' 'unsafe-inline';
 | 
				
			||||||
			img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
 | 
								img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
 | 
				
			||||||
			media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
 | 
								media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
 | 
				
			||||||
			connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
 | 
								connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org;
 | 
				
			||||||
			frame-src *;"
 | 
								frame-src *;"
 | 
				
			||||||
	/>
 | 
						/>
 | 
				
			||||||
	<meta property="og:site_name" content="[DEV BUILD] Misskey" />
 | 
						<meta property="og:site_name" content="[DEV BUILD] Misskey" />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -215,7 +215,7 @@ function gravity() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function iLoveMisskey() {
 | 
					function iLoveMisskey() {
 | 
				
			||||||
	os.post({
 | 
						os.post({
 | 
				
			||||||
		initialText: 'I $[jelly ❤] #Misskey',
 | 
							initialText: 'I $[jelly ❤] #Sharkey',
 | 
				
			||||||
		instant: true,
 | 
							instant: true,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,6 +98,9 @@ const selectedEmojis = ref<string[]>([]);
 | 
				
			||||||
const pagination = {
 | 
					const pagination = {
 | 
				
			||||||
	endpoint: 'admin/emoji/list' as const,
 | 
						endpoint: 'admin/emoji/list' as const,
 | 
				
			||||||
	limit: 30,
 | 
						limit: 30,
 | 
				
			||||||
 | 
						offsetMode: computed(() => (
 | 
				
			||||||
 | 
							(query.value && query.value !== '') ? true : false
 | 
				
			||||||
 | 
						)),
 | 
				
			||||||
	params: computed(() => ({
 | 
						params: computed(() => ({
 | 
				
			||||||
		query: (query.value && query.value !== '') ? query.value : null,
 | 
							query: (query.value && query.value !== '') ? query.value : null,
 | 
				
			||||||
	})),
 | 
						})),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,6 +117,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
		<div class="_gaps_m">
 | 
							<div class="_gaps_m">
 | 
				
			||||||
			<MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
 | 
								<MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<MkRadios v-model="notificationPosition">
 | 
								<MkRadios v-model="notificationPosition">
 | 
				
			||||||
				<template #label>{{ i18n.ts.position }}</template>
 | 
									<template #label>{{ i18n.ts.position }}</template>
 | 
				
			||||||
				<option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option>
 | 
									<option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option>
 | 
				
			||||||
| 
						 | 
					@ -353,6 +355,7 @@ const oneko = computed(defaultStore.makeGetterSetter('oneko'));
 | 
				
			||||||
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
 | 
					const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
 | 
				
			||||||
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
 | 
					const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
 | 
				
			||||||
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
 | 
					const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
 | 
				
			||||||
 | 
					const enableFaviconNotificationDot = computed(defaultStore.makeGetterSetter('enableFaviconNotificationDot'));
 | 
				
			||||||
const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText'));
 | 
					const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText'));
 | 
				
			||||||
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
 | 
					const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
 | 
				
			||||||
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
 | 
					const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,6 +73,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 | 
				
			||||||
	'showReactionsCount',
 | 
						'showReactionsCount',
 | 
				
			||||||
	'loadRawImages',
 | 
						'loadRawImages',
 | 
				
			||||||
	'warnMissingAltText',
 | 
						'warnMissingAltText',
 | 
				
			||||||
 | 
						'enableFaviconNotificationDot',
 | 
				
			||||||
	'imageNewTab',
 | 
						'imageNewTab',
 | 
				
			||||||
	'dataSaver',
 | 
						'dataSaver',
 | 
				
			||||||
	'disableShowingAnimatedImages',
 | 
						'disableShowingAnimatedImages',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -236,22 +236,24 @@ const moderationNote = ref(props.user.moderationNote);
 | 
				
			||||||
const editModerationNote = ref(false);
 | 
					const editModerationNote = ref(false);
 | 
				
			||||||
const noteview = ref<string | null>(null);
 | 
					const noteview = ref<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let listenbrainzdata = false;
 | 
					const listenbrainzdata = ref(false);
 | 
				
			||||||
if (props.user.listenbrainz) {
 | 
					if (props.user.listenbrainz) {
 | 
				
			||||||
	try {
 | 
						(async function() {
 | 
				
			||||||
		const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
 | 
							try {
 | 
				
			||||||
			method: 'GET',
 | 
								const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
 | 
				
			||||||
			headers: {
 | 
									method: 'GET',
 | 
				
			||||||
				'Content-Type': 'application/json'
 | 
									headers: {
 | 
				
			||||||
			},
 | 
										'Content-Type': 'application/json'
 | 
				
			||||||
		});
 | 
									},
 | 
				
			||||||
		const data = await response.json();
 | 
								});
 | 
				
			||||||
		if (data.payload.listens && data.payload.listens.length !== 0) {
 | 
								const data = await response.json();
 | 
				
			||||||
			listenbrainzdata = true;
 | 
								if (data.payload.listens && data.payload.listens.length !== 0) {
 | 
				
			||||||
 | 
									listenbrainzdata.value = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								listenbrainzdata.value = false;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} catch (err) {
 | 
						})()
 | 
				
			||||||
		listenbrainzdata = false;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const background = computed(() => {
 | 
					const background = computed(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										114
									
								
								packages/frontend/src/scripts/favicon-dot.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								packages/frontend/src/scripts/favicon-dot.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					import tinycolor from 'tinycolor2';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FavIconDot {
 | 
				
			||||||
 | 
						canvas: HTMLCanvasElement;
 | 
				
			||||||
 | 
						src: string | null = null;
 | 
				
			||||||
 | 
						ctx: CanvasRenderingContext2D | null = null;
 | 
				
			||||||
 | 
						faviconImage: HTMLImageElement | null = null;
 | 
				
			||||||
 | 
						faviconEL: HTMLLinkElement | undefined;
 | 
				
			||||||
 | 
						hasLoaded: Promise<void> | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							this.canvas = document.createElement('canvas');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Must be called before calling any other functions
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public async setup() {
 | 
				
			||||||
 | 
							const element: HTMLLinkElement = await this.getOrMakeFaviconElement();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.faviconEL = element;
 | 
				
			||||||
 | 
							this.src = this.faviconEL.getAttribute('href');
 | 
				
			||||||
 | 
							this.ctx = this.canvas.getContext('2d');
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
							this.faviconImage = document.createElement('img');
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							this.hasLoaded = new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
								(this.faviconImage as HTMLImageElement).addEventListener('load', () => {
 | 
				
			||||||
 | 
									this.canvas.width = (this.faviconImage as HTMLImageElement).width;
 | 
				
			||||||
 | 
									this.canvas.height = (this.faviconImage as HTMLImageElement).height;
 | 
				
			||||||
 | 
									resolve();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								(this.faviconImage as HTMLImageElement).addEventListener('error', () => {
 | 
				
			||||||
 | 
									reject('Failed to create favicon img element');
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.faviconImage.src = this.faviconEL.href;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async getOrMakeFaviconElement(): Promise<HTMLLinkElement> {
 | 
				
			||||||
 | 
							return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
								const favicon = (document.querySelector('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement;
 | 
				
			||||||
 | 
								favicon.addEventListener('load', () => {
 | 
				
			||||||
 | 
									resolve(favicon);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								favicon.onerror = () => {
 | 
				
			||||||
 | 
									reject('Failed to load favicon');
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
								resolve(favicon);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private createFaviconElem() {
 | 
				
			||||||
 | 
							const newLink = document.createElement('link');
 | 
				
			||||||
 | 
							newLink.setAttribute('rel', 'icon');
 | 
				
			||||||
 | 
							newLink.setAttribute('href', '/favicon.ico');
 | 
				
			||||||
 | 
							newLink.setAttribute('type', 'image/x-icon');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							document.head.appendChild(newLink);
 | 
				
			||||||
 | 
							return newLink;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private drawIcon() {
 | 
				
			||||||
 | 
							if (!this.ctx || !this.faviconImage) return;
 | 
				
			||||||
 | 
							this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
 | 
				
			||||||
 | 
							this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private drawDot() {
 | 
				
			||||||
 | 
							if (!this.ctx || !this.faviconImage) return;
 | 
				
			||||||
 | 
							this.ctx.beginPath();
 | 
				
			||||||
 | 
							this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
 | 
				
			||||||
 | 
							const computedStyle = getComputedStyle(document.documentElement);
 | 
				
			||||||
 | 
							this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString();
 | 
				
			||||||
 | 
							this.ctx.strokeStyle = 'white';
 | 
				
			||||||
 | 
							this.ctx.fill();
 | 
				
			||||||
 | 
							this.ctx.stroke();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private setFavicon() {
 | 
				
			||||||
 | 
							if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async setVisible(isVisible: boolean) {
 | 
				
			||||||
 | 
							// Wait for it to have loaded the icon
 | 
				
			||||||
 | 
							await this.hasLoaded;
 | 
				
			||||||
 | 
							this.drawIcon();
 | 
				
			||||||
 | 
							if (isVisible) this.drawDot();
 | 
				
			||||||
 | 
							this.setFavicon();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let icon: FavIconDot | undefined = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setFavIconDot(visible: boolean) {
 | 
				
			||||||
 | 
						const setIconVisibility = async () => {
 | 
				
			||||||
 | 
							if (!icon) {
 | 
				
			||||||
 | 
								icon = new FavIconDot();
 | 
				
			||||||
 | 
								await icon.setup();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							(icon as FavIconDot).setVisible(visible);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If document is already loaded, set visibility immediately
 | 
				
			||||||
 | 
						if (document.readyState === 'complete') {
 | 
				
			||||||
 | 
							setIconVisibility();
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Otherwise, set visibility when window loads
 | 
				
			||||||
 | 
							window.addEventListener('load', setIconVisibility);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -271,6 +271,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
				
			||||||
		where: 'device',
 | 
							where: 'device',
 | 
				
			||||||
		default: true,
 | 
							default: true,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						enableFaviconNotificationDot: {
 | 
				
			||||||
 | 
							where: 'device',
 | 
				
			||||||
 | 
							default: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	imageNewTab: {
 | 
						imageNewTab: {
 | 
				
			||||||
		where: 'device',
 | 
							where: 'device',
 | 
				
			||||||
		default: false,
 | 
							default: false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -565,6 +565,8 @@ html[data-color-mode=dark] ._woodenFrame {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MFM -----------------------------
 | 
					// MFM -----------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div > bdi, p > bdi { display: block }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
._mfm_blur_ {
 | 
					._mfm_blur_ {
 | 
				
			||||||
	filter: blur(6px);
 | 
						filter: blur(6px);
 | 
				
			||||||
	transition: filter 0.3s;
 | 
						transition: filter 0.3s;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,8 +47,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineAsyncComponent, ref } from 'vue';
 | 
					import { defineAsyncComponent, ref, watch } from 'vue';
 | 
				
			||||||
import * as Misskey from 'misskey-js';
 | 
					import * as Misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import { setFavIconDot } from '../../scripts/favicon-dot';
 | 
				
			||||||
import { swInject } from './sw-inject.js';
 | 
					import { swInject } from './sw-inject.js';
 | 
				
			||||||
import XNotification from './notification.vue';
 | 
					import XNotification from './notification.vue';
 | 
				
			||||||
import { popups } from '@/os.js';
 | 
					import { popups } from '@/os.js';
 | 
				
			||||||
| 
						 | 
					@ -93,6 +94,12 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
 | 
				
			||||||
if ($i) {
 | 
					if ($i) {
 | 
				
			||||||
	const connection = useStream().useChannel('main', null, 'UI');
 | 
						const connection = useStream().useChannel('main', null, 'UI');
 | 
				
			||||||
	connection.on('notification', onNotification);
 | 
						connection.on('notification', onNotification);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// For the favicon notification dot
 | 
				
			||||||
 | 
						watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	globalEvents.on('clientNotification', notification => onNotification(notification, true));
 | 
						globalEvents.on('clientNotification', notification => onNotification(notification, true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//#region Listen message from SW
 | 
						//#region Listen message from SW
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue