mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-25 07:25:12 +00:00
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;
|
||||||
}
|
}
|
||||||
|
@ -642,7 +642,7 @@ export class MfmService {
|
||||||
|
|
||||||
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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,8 +236,9 @@ 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) {
|
||||||
|
(async function() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
|
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -247,11 +248,12 @@ if (props.user.listenbrainz) {
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.payload.listens && data.payload.listens.length !== 0) {
|
if (data.payload.listens && data.payload.listens.length !== 0) {
|
||||||
listenbrainzdata = true;
|
listenbrainzdata.value = true;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
listenbrainzdata = false;
|
listenbrainzdata.value = 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…
Reference in a new issue