forked from FoundKeyGang/FoundKey
Compare commits
25 commits
b9e5704103
...
87f1b0cabc
Author | SHA1 | Date | |
---|---|---|---|
87f1b0cabc | |||
9470e12424 | |||
fa0dce6439 | |||
b90f910926 | |||
dba63e4000 | |||
7dabfd4056 | |||
d9a64d0a22 | |||
772d4618a6 | |||
1b92f580cb | |||
0022a7befb | |||
48fda127ca | |||
cc5a197785 | |||
87411a6ed8 | |||
ab84457c0e | |||
7ea052aa25 | |||
321bd24b98 | |||
58aa7d36aa | |||
35fd970c4a | |||
26449d4944 | |||
78fd2ee38b | |||
a0e859ebcb | |||
b42b6d3d9b | |||
9f0f5d1ab1 | |||
17f3dafd6b | |||
d5d8affc33 |
65 changed files with 427 additions and 240 deletions
|
@ -102,7 +102,7 @@ Changelog: Removed
|
|||
|
||||
### Creating a PR
|
||||
|
||||
- Please prefix the title with the part of Misskey you are changing, i.e. `server:` or `client:`
|
||||
- Please prefix the title with the part of FoundKey you are changing, i.e. `server:` or `client:`
|
||||
- The rest of the title should roughly describe what you did.
|
||||
- Make sure that the granularity of this PR is appropriate. Please do not include more than one type of change in a single PR.
|
||||
- If there is an issue which will be resolved by this PR, please include a reference to the Issue in the text.
|
||||
|
|
|
@ -804,6 +804,7 @@ makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktion
|
|||
classic: "Classic"
|
||||
muteThread: "Thread stummschalten"
|
||||
unmuteThread: "Threadstummschaltung aufheben"
|
||||
threadMuteNotificationsDesc: "Wähle die Benachrichtigungen, die du aus diesem Thread erhalten möchtest. Globale Benachrichtigungs-Einstellungen werden zusätzlich angewandt. Das Deaktivieren einer Benachrichtigung hat Vorrang."
|
||||
ffVisibility: "Sichtbarkeit von Gefolgten/Followern"
|
||||
ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt."
|
||||
continueThread: "Weiteren Threadverlauf anzeigen"
|
||||
|
|
|
@ -804,6 +804,7 @@ makeReactionsPublicDescription: "This will make the list of all your past reacti
|
|||
classic: "Classic"
|
||||
muteThread: "Mute thread"
|
||||
unmuteThread: "Unmute thread"
|
||||
threadMuteNotificationsDesc: "Select the notifications you wish to view from this thread. Global notification settings also apply. Disabling takes precedence."
|
||||
ffVisibility: "Follows/Followers Visibility"
|
||||
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
|
||||
continueThread: "View thread continuation"
|
||||
|
|
|
@ -805,6 +805,7 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を
|
|||
classic: "クラシック"
|
||||
muteThread: "スレッドをミュート"
|
||||
unmuteThread: "スレッドのミュートを解除"
|
||||
threadMuteNotificationsDesc: "このスレッドから表示する通知を選択します。グローバル通知設定も適用され、禁止が優先されます。"
|
||||
ffVisibility: "つながりの公開範囲"
|
||||
ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。"
|
||||
continueThread: "さらにスレッドを見る"
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
"mocha": "yarn workspace backend run mocha",
|
||||
"test": "yarn mocha",
|
||||
"format": "gulp format",
|
||||
"clean": "yarn workspaces foreach run clean && rm -rf built/",
|
||||
"clean-all": "yarn workspaces foreach run clean-all && rm -rf built/ node_modules/",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"clean-all": "node ./scripts/clean-all.js",
|
||||
"cleanall": "yarn clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export class threadMuteNotifications1655793461890 {
|
||||
name = 'threadMuteNotifications1655793461890'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD "mutingNotificationTypes" "public"."note_thread_muting_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP COLUMN "mutingNotificationTypes"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"`);
|
||||
}
|
||||
}
|
|
@ -6,8 +6,6 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"clean": "rm -rf built/ tsconfig.tsbuildinfo",
|
||||
"clean-all": "yarn clean && rm -rf node_modules/",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { noteNotificationTypes } from 'foundkey-js';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './user.js';
|
||||
import { Note } from './note.js';
|
||||
|
@ -30,4 +31,11 @@ export class NoteThreadMuting {
|
|||
length: 256,
|
||||
})
|
||||
public threadId: string;
|
||||
|
||||
@Column('enum', {
|
||||
enum: noteNotificationTypes,
|
||||
array: true,
|
||||
default: [],
|
||||
})
|
||||
public mutingNotificationTypes: typeof notificationTypes[number][];
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { In } from 'typeorm';
|
||||
import { noteNotificationTypes } from 'foundkey-js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
@ -28,50 +29,18 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
|||
isRead: notification.isRead,
|
||||
userId: notification.notifierId,
|
||||
user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
|
||||
...(notification.type === 'mention' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
} : {}),
|
||||
...(notification.type === 'reply' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
} : {}),
|
||||
...(notification.type === 'renote' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
} : {}),
|
||||
...(notification.type === 'quote' ? {
|
||||
...(noteNotificationTypes.includes(notification.type) ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
} : {}),
|
||||
...(notification.type === 'reaction' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
reaction: notification.reaction,
|
||||
} : {}),
|
||||
...(notification.type === 'pollVote' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
choice: notification.choice,
|
||||
} : {}),
|
||||
...(notification.type === 'pollEnded' ? {
|
||||
note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
}),
|
||||
} : {}),
|
||||
...(notification.type === 'groupInvited' ? {
|
||||
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
|
||||
} : {}),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import httpSignature from '@peertube/http-signature';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
|
||||
export type DeliverJobData = {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { UserKeypair } from '@/models/entities/user-keypair.js';
|
|||
import { usersChart } from '@/services/chart/index.js';
|
||||
import { UsedUsername } from '@/models/entities/used-username.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import generateUserToken from './generate-native-user-token.js';
|
||||
|
||||
export async function signup(opts: {
|
||||
|
@ -23,13 +24,25 @@ export async function signup(opts: {
|
|||
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername(username)) {
|
||||
throw new Error('INVALID_USERNAME');
|
||||
throw new ApiError({
|
||||
message: 'This username is invalid.',
|
||||
code: 'INVALID_USERNAME',
|
||||
id: 'ece89f3c-d845-4d9a-850b-1735285e8cd4',
|
||||
kind: 'client',
|
||||
httpStatusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword(password)) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
throw new ApiError({
|
||||
message: 'This password is invalid.',
|
||||
code: 'INVALID_PASSWORD',
|
||||
id: 'a941905b-fe7b-43e2-8ecd-50ad3a2287ab',
|
||||
kind: 'client',
|
||||
httpStatusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
|
@ -40,14 +53,22 @@ export async function signup(opts: {
|
|||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
||||
const duplicateUsernameError = {
|
||||
message: 'This username is not available.',
|
||||
code: 'USED_USERNAME',
|
||||
id: '7ddd595e-6860-4593-93c5-9fdbcb80cd81',
|
||||
kind: 'client',
|
||||
httpStatusCode: 409,
|
||||
};
|
||||
|
||||
// Check username duplication
|
||||
if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
throw new Error('DUPLICATED_USERNAME');
|
||||
throw new ApiError(duplicateUsernameError);
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) {
|
||||
throw new Error('USED_USERNAME');
|
||||
throw new ApiError(duplicateUsernameError);
|
||||
}
|
||||
|
||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||
|
@ -76,7 +97,7 @@ export async function signup(opts: {
|
|||
host: IsNull(),
|
||||
});
|
||||
|
||||
if (exist) throw new Error(' the username is already used');
|
||||
if (exist) throw new ApiError(duplicateUsernameError);
|
||||
|
||||
account = await transactionalEntityManager.save(new User({
|
||||
id: genId(),
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Not } from 'typeorm';
|
||||
import { ArrayOverlap, Not } from 'typeorm';
|
||||
import { publishNoteStream } from '@/services/stream.js';
|
||||
import { createNotification } from '@/services/create-notification.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderVote from '@/remote/activitypub/renderer/vote.js';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||
import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js';
|
||||
import { PollVotes, NoteWatchings, Users, Polls, Blockings, NoteThreadMutings } from '@/models/index.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { getNote } from '../../../common/getters.js';
|
||||
|
@ -136,14 +136,24 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice: ps.choice,
|
||||
// check if this thread and notification type is muted
|
||||
const threadMuted = await NoteThreadMutings.findOne({
|
||||
userId: note.userId,
|
||||
threadId: note.threadId || note.id,
|
||||
mutingNotificationTypes: ArrayOverlap(['pollVote']),
|
||||
});
|
||||
// Notify
|
||||
if (!threadMuted) {
|
||||
createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice: ps.choice,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch watchers
|
||||
// checking for mutes is not necessary here, as note watchings will be
|
||||
// deleted when a thread is muted
|
||||
NoteWatchings.findBy({
|
||||
noteId: note.id,
|
||||
userId: Not(user.id),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Notes, NoteThreadMutings } from '@/models/index.js';
|
||||
import { noteNotificationTypes } from 'foundkey-js';
|
||||
import { Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import readNote from '@/services/note/read.js';
|
||||
import define from '../../../define.js';
|
||||
|
@ -25,6 +26,14 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
mutingNotificationTypes: {
|
||||
description: 'Defines which notification types from the thread should be muted. Replies are always muted. Applies in addition to the global settings, muting takes precedence.',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string', enum: noteNotificationTypes,
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
@ -51,5 +60,19 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
createdAt: new Date(),
|
||||
threadId: note.threadId || note.id,
|
||||
userId: user.id,
|
||||
mutingNotificationTypes: ps.mutingNotificationTypes,
|
||||
});
|
||||
|
||||
// remove all note watchings in the muted thread
|
||||
const notesThread = Notes.createQueryBuilder("notes")
|
||||
.select("note.id")
|
||||
.where({
|
||||
threadId: note.threadId ?? note.id,
|
||||
});
|
||||
|
||||
await NoteWatchings.createQueryBuilder()
|
||||
.delete()
|
||||
.where(`"note_watching"."noteId" IN (${ notesThread.getQuery() })`)
|
||||
.setParameters(notesThread.getParameters())
|
||||
.execute();
|
||||
});
|
||||
|
|
|
@ -27,11 +27,79 @@ export const meta = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
// List of permitted languages from https://www.deepl.com/docs-api/translate-text/translate-text/
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
targetLang: { type: 'string' },
|
||||
sourceLang: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'BG',
|
||||
'CS',
|
||||
'DA',
|
||||
'DE',
|
||||
'EL',
|
||||
'EN',
|
||||
'ES',
|
||||
'ET',
|
||||
'FI',
|
||||
'FR',
|
||||
'HU',
|
||||
'ID',
|
||||
'IT',
|
||||
'JA',
|
||||
'LT',
|
||||
'LV',
|
||||
'NL',
|
||||
'PL',
|
||||
'PT',
|
||||
'RO',
|
||||
'RU',
|
||||
'SK',
|
||||
'SL',
|
||||
'SV',
|
||||
'TR',
|
||||
'UK',
|
||||
'ZH',
|
||||
],
|
||||
},
|
||||
targetLang: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'BG',
|
||||
'CS',
|
||||
'DA',
|
||||
'DE',
|
||||
'EL',
|
||||
'EN',
|
||||
'EN-GB',
|
||||
'EN-US',
|
||||
'ES',
|
||||
'ET',
|
||||
'FI',
|
||||
'FR',
|
||||
'HU',
|
||||
'ID',
|
||||
'IT',
|
||||
'JA',
|
||||
'LT',
|
||||
'LV',
|
||||
'NL',
|
||||
'PL',
|
||||
'PT',
|
||||
'PT-BR',
|
||||
'PT-PT',
|
||||
'RO',
|
||||
'RU',
|
||||
'SK',
|
||||
'SL',
|
||||
'SV',
|
||||
'TR',
|
||||
'UK',
|
||||
'ZH',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['noteId', 'targetLang'],
|
||||
} as const;
|
||||
|
@ -53,13 +121,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
let targetLang = ps.targetLang;
|
||||
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
|
||||
const sourceLang = ps.sourceLang;
|
||||
const targetLang = ps.targetLang;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('auth_key', instance.deeplAuthKey);
|
||||
params.append('text', note.text);
|
||||
params.append('target_lang', targetLang);
|
||||
if (sourceLang) params.append('source_lang', sourceLang);
|
||||
|
||||
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ export class ApiError extends Error {
|
|||
public info?: any;
|
||||
|
||||
constructor(
|
||||
e?: E | null | undefined = {
|
||||
e: E = {
|
||||
message: 'Internal error occurred. Please contact us if the error persists.',
|
||||
code: 'INTERNAL_ERROR',
|
||||
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
|
||||
kind: 'server',
|
||||
httpStatusCode: 500,
|
||||
},
|
||||
info?: any | null | undefined,
|
||||
info?: any | null,
|
||||
) {
|
||||
super(e.message);
|
||||
this.message = e.message;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Not, In } from 'typeorm';
|
||||
import { ArrayOverlap, Not, In } from 'typeorm';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import es from '@/db/elasticsearch.js';
|
||||
|
@ -80,15 +80,19 @@ class NotificationManager {
|
|||
|
||||
public async deliver() {
|
||||
for (const x of this.queue) {
|
||||
// ミュート情報を取得
|
||||
const mentioneeMutes = await Mutings.findBy({
|
||||
// check if the sender or thread are muted
|
||||
const userMuted = await Mutings.findOneBy({
|
||||
muterId: x.target,
|
||||
muteeId: this.notifier.id,
|
||||
});
|
||||
|
||||
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
userId: x.target,
|
||||
threadId: this.note.threadId || this.note.id,
|
||||
mutingNotificationTypes: ArrayOverlap([x.reason]),
|
||||
});
|
||||
|
||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||
if (!userMuted && !threadMuted) {
|
||||
createNotification(x.target, x.reason, {
|
||||
notifierId: this.notifier.id,
|
||||
noteId: this.note.id,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Not } from 'typeorm';
|
||||
import { ArrayOverlap, Not } from 'typeorm';
|
||||
import { publishNoteStream } from '@/services/stream.js';
|
||||
import { CacheableUser } from '@/models/entities/user.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index.js';
|
||||
import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { createNotification } from '../../create-notification.js';
|
||||
|
||||
|
@ -57,12 +57,20 @@ export default async function(user: CacheableUser, note: Note, choice: number) {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice,
|
||||
// check if this thread and notification type is muted
|
||||
const muted = await NoteThreadMutings.findOne({
|
||||
userId: note.userId,
|
||||
threadId: note.threadId || note.id,
|
||||
mutingNotificationTypes: ArrayOverlap(['pollVote']),
|
||||
});
|
||||
// Notify
|
||||
if (!muted) {
|
||||
createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice: choice,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch watchers
|
||||
NoteWatchings.findBy({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IsNull, Not } from 'typeorm';
|
||||
import { ArrayOverlap, IsNull, Not } from 'typeorm';
|
||||
import { publishNoteStream } from '@/services/stream.js';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
import DeliverManager from '@/remote/activitypub/deliver-manager.js';
|
||||
|
@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
|||
import { toDbReaction, decodeReaction } from '@/misc/reaction-lib.js';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from '@/models/index.js';
|
||||
import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings, NoteThreadMutings } from '@/models/index.js';
|
||||
import { perUserReactionsChart } from '@/services/chart/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
|
@ -98,8 +98,14 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
// check if this thread is muted
|
||||
const threadMuted = await NoteThreadMutings.findOne({
|
||||
userId: note.userId,
|
||||
threadId: note.threadId || note.id,
|
||||
mutingNotificationTypes: ArrayOverlap(['reaction']),
|
||||
});
|
||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||
if (note.userHost === null) {
|
||||
if (note.userHost === null && !threadMuted) {
|
||||
createNotification(note.userId, 'reaction', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"sourceMap": false,
|
||||
"target": "ES2021",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Node16",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
"scripts": {
|
||||
"watch": "vite build --watch --mode development",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src --ext .ts,.vue",
|
||||
"clean": "rm -rf built/",
|
||||
"clean-all": "yarn clean && rm -rf node_modules/"
|
||||
"lint": "eslint src --ext .ts,.vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
|
|
|
@ -3,35 +3,35 @@
|
|||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="federating">{{ i18n.ts.federating }}</option>
|
||||
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ i18n.ts.lastCommunication }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ i18n.ts.lastCommunication }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div class="text">
|
||||
<div>
|
||||
<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
|
||||
<span>{{ $ts.clickToShow }}</span>
|
||||
<b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.sensitive }}</b>
|
||||
<span>{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
|||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||
</a>
|
||||
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="fas fa-eye-slash"></i></button>
|
||||
<button v-tooltip="i18n.ts.hide" class="_button hide" @click="hide = true"><i class="fas fa-eye-slash"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -26,6 +26,7 @@ import * as foundkey from 'foundkey-js';
|
|||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
image: foundkey.entities.DriveFile;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageMetadata?.value" class="title">
|
||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</MkSwitch>
|
||||
</div>
|
||||
<div v-if="!useGlobalSetting" class="_section">
|
||||
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||
<MkInfo>{{ message }}</MkInfo>
|
||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||
|
@ -28,7 +28,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { notificationTypes } from 'foundkey-js';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInfo from './ui/info.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
|
@ -41,21 +41,25 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
includingTypes?: typeof notificationTypes[number][] | null;
|
||||
includingTypes?: typeof foundkey.notificationTypes[number][] | null;
|
||||
notificationTypes?: typeof foundkey.notificationTypes[number][] | null;
|
||||
showGlobalToggle?: boolean;
|
||||
message?: string,
|
||||
}>(), {
|
||||
includingTypes: () => [],
|
||||
notificationTypes: () => [],
|
||||
showGlobalToggle: true,
|
||||
message: i18n.ts.notificationSettingDesc,
|
||||
});
|
||||
|
||||
let includingTypes = $computed(() => props.includingTypes || []);
|
||||
|
||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||
|
||||
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
|
||||
let typesMap = $ref<Record<typeof foundkey.notificationTypes[number], boolean>>({});
|
||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
||||
|
||||
for (const ntype of notificationTypes) {
|
||||
for (const ntype of props.notificationTypes) {
|
||||
typesMap[ntype] = includingTypes.includes(ntype);
|
||||
}
|
||||
|
||||
|
@ -64,7 +68,7 @@ function ok() {
|
|||
emit('done', { includingTypes: null });
|
||||
} else {
|
||||
emit('done', {
|
||||
includingTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
|
||||
includingTypes: (Object.keys(typesMap) as typeof foundkey.notificationTypes[number][])
|
||||
.filter(type => typesMap[type]),
|
||||
});
|
||||
}
|
||||
|
@ -74,13 +78,13 @@ function ok() {
|
|||
|
||||
function disableAll() {
|
||||
for (const type in typesMap) {
|
||||
typesMap[type as typeof notificationTypes[number]] = false;
|
||||
typesMap[type as typeof foundkey.notificationTypes[number]] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function enableAll() {
|
||||
for (const type in typesMap) {
|
||||
typesMap[type as typeof notificationTypes[number]] = true;
|
||||
typesMap[type as typeof foundkey.notificationTypes[number]] = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="fas fa-times"></i>
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
<span>
|
||||
<template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
||||
<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly">
|
||||
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="social _section">
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<XMediaList :media-list="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div v-show="showBody" ref="content" class="content" :class="{ omitted }">
|
||||
<slot></slot>
|
||||
<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }">
|
||||
<span>{{ $ts.showMore }}</span>
|
||||
<span>{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -29,6 +29,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
|
||||
<p>{{ i18n.ts.notes }}</p><span>{{ user.notesCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
|
||||
<p>{{ i18n.ts.following }}</p><span>{{ user.followingCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
|
||||
<p>{{ i18n.ts.followers }}</p><span>{{ user.followersCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/>
|
||||
|
@ -39,6 +39,7 @@ import MkFollowButton from './follow-button.vue';
|
|||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
|
|
|
@ -3,11 +3,3 @@ import { locale } from '@/config';
|
|||
import { I18n } from '@/scripts/i18n';
|
||||
|
||||
export const i18n = markRaw(new I18n(locale));
|
||||
|
||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: typeof i18n['t'];
|
||||
$ts: typeof i18n['locale'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,8 +184,6 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
$i,
|
||||
$store: defaultStore,
|
||||
$instance: instance,
|
||||
$t: i18n.t,
|
||||
$ts: i18n.ts,
|
||||
};
|
||||
|
||||
widgets(app);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
|
||||
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
|
||||
<i v-else class="fas fa-clock icon requesting"></i>
|
||||
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
|
||||
<span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.isRead" class="_footer">
|
||||
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
|
||||
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</MkPagination>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-size="{ max: [400] }" class="yweeujhr">
|
||||
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
|
||||
<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ i18n.ts.startMessaging }}</MkButton>
|
||||
|
||||
<div v-if="messages.length > 0" class="history">
|
||||
<MkA
|
||||
|
@ -27,14 +27,14 @@
|
|||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
|
||||
<p class="text"><span v-if="isMe(message)" class="me">{{ i18n.ts.you }}:</span>{{ message.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!fetching && messages.length == 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noHistory }}</div>
|
||||
<div>{{ i18n.ts.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
|
||||
<div class="content">
|
||||
<div class="balloon" :class="{ noText: message.text == null }">
|
||||
<button v-if="isMe" class="delete-button" :title="$ts.delete" @click="del">
|
||||
<button v-if="isMe" class="delete-button" :title="i18n.ts.delete" @click="del">
|
||||
<img src="/client-assets/remove.png" alt="Delete"/>
|
||||
</button>
|
||||
<div v-if="!message.isDeleted" class="content">
|
||||
|
@ -16,17 +16,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="content">
|
||||
<p class="is-deleted">{{ $ts.deleted }}</p>
|
||||
<p class="is-deleted">{{ i18n.ts.deleted }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
|
||||
<footer>
|
||||
<template v-if="isGroup">
|
||||
<span v-if="message.reads.length > 0" class="read">{{ $ts.messageRead }} {{ message.reads.length }}</span>
|
||||
<span v-if="message.reads.length > 0" class="read">{{ i18n.ts.messageRead }} {{ message.reads.length }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="isMe && message.isRead" class="read">{{ $ts.messageRead }}</span>
|
||||
<span v-if="isMe && message.isRead" class="read">{{ i18n.ts.messageRead }}</span>
|
||||
</template>
|
||||
<MkTime :time="message.createdAt"/>
|
||||
<template v-if="message.is_edited"><i class="fas fa-pencil-alt"></i></template>
|
||||
|
@ -42,6 +42,7 @@ import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
|||
import MkUrlPreview from '@/components/url-preview.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
message: foundkey.entities.MessagingMessage;
|
||||
|
|
|
@ -300,41 +300,39 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const preview_mention = '@example';
|
||||
const preview_hashtag = '#test';
|
||||
const preview_url = 'https://example.com';
|
||||
const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`;
|
||||
const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:';
|
||||
const preview_bold = `**${i18n.ts._mfm.dummy}**`;
|
||||
const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`;
|
||||
const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`;
|
||||
const preview_inlineCode = '`<: "Hello, world!"`';
|
||||
const preview_blockCode = '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```';
|
||||
const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)';
|
||||
const preview_quote = `> ${i18n.ts._mfm.dummy}`;
|
||||
const preview_search = `${i18n.ts._mfm.dummy} 検索`;
|
||||
const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]';
|
||||
const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]';
|
||||
const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]';
|
||||
const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]';
|
||||
const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]';
|
||||
const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]';
|
||||
const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]';
|
||||
const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_x2 = '$[x2 🍮]';
|
||||
const preview_x3 = '$[x3 🍮]';
|
||||
const preview_x4 = '$[x4 🍮]';
|
||||
const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]';
|
||||
const preview_sparkle = '$[sparkle 🍮]';
|
||||
const preview_rotate = '$[rotate 🍮]';
|
||||
let preview_mention = $ref('@example');
|
||||
let preview_hashtag = $ref('#test');
|
||||
let preview_url = $ref('https://example.com');
|
||||
let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
|
||||
let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:');
|
||||
let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
|
||||
let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
|
||||
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
|
||||
let preview_inlineCode = $ref('`<: "Hello, world!"`');
|
||||
let preview_blockCode = $ref('```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```');
|
||||
let preview_inlineMath = $ref('\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)');
|
||||
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
|
||||
let preview_jelly = $ref('$[jelly 🍮] $[jelly.speed=5s 🍮]');
|
||||
let preview_tada = $ref('$[tada 🍮] $[tada.speed=5s 🍮]');
|
||||
let preview_jump = $ref('$[jump 🍮] $[jump.speed=5s 🍮]');
|
||||
let preview_bounce = $ref('$[bounce 🍮] $[bounce.speed=5s 🍮]');
|
||||
let preview_shake = $ref('$[shake 🍮] $[shake.speed=5s 🍮]');
|
||||
let preview_twitch = $ref('$[twitch 🍮] $[twitch.speed=5s 🍮]');
|
||||
let preview_spin = $ref('$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]');
|
||||
let preview_flip = $ref(`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_font = $ref(`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_x2 = $ref('$[x2 🍮]');
|
||||
let preview_x3 = $ref('$[x3 🍮]');
|
||||
let preview_x4 = $ref('$[x4 🍮]');
|
||||
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_rainbow = $ref('$[rainbow 🍮] $[rainbow.speed=5s 🍮]');
|
||||
let preview_sparkle = $ref('$[sparkle 🍮]');
|
||||
let preview_rotate = $ref('$[rotate 🍮]');
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._mfm.cheatSheet,
|
||||
|
|
|
@ -3,42 +3,42 @@
|
|||
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="jqqmcavi">
|
||||
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
|
||||
<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
|
||||
<MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
|
||||
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ i18n.ts._pages.viewPage }}</MkButton>
|
||||
<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="fas fa-copy"></i> {{ i18n.ts.duplicate }}</MkButton>
|
||||
<MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'settings'">
|
||||
<div class="_formRoot">
|
||||
<MkInput v-model="title" :readonly="readonly" class="_formBlock">
|
||||
<template #label>{{ $ts._pages.title }}</template>
|
||||
<template #label>{{ i18n.ts._pages.title }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="summary" :readonly="readonly" class="_formBlock">
|
||||
<template #label>{{ $ts._pages.summary }}</template>
|
||||
<template #label>{{ i18n.ts._pages.summary }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="name" :readonly="readonly" class="_formBlock">
|
||||
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
|
||||
<template #label>{{ $ts._pages.url }}</template>
|
||||
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="alignCenter" :disabled="readonly" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch>
|
||||
<MkSwitch v-model="alignCenter" :disabled="readonly" class="_formBlock">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
|
||||
|
||||
<MkSelect v-model="font" :readonly="readonly" class="_formBlock">
|
||||
<template #label>{{ $ts._pages.font }}</template>
|
||||
<option value="serif">{{ $ts._pages.fontSerif }}</option>
|
||||
<option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
|
||||
<template #label>{{ i18n.ts._pages.font }}</template>
|
||||
<option value="serif">{{ i18n.ts._pages.fontSerif }}</option>
|
||||
<option value="sans-serif">{{ i18n.ts._pages.fontSansSerif }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSwitch v-model="hideTitleWhenPinned" :disabled="readonly" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
|
||||
<MkSwitch v-model="hideTitleWhenPinned" :disabled="readonly" class="_formBlock">{{ i18n.ts._pages.hideTitleWhenPinned }}</MkSwitch>
|
||||
|
||||
<div class="eyeCatch">
|
||||
<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
|
||||
<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton>
|
||||
<div v-else-if="eyeCatchingImage">
|
||||
<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
|
||||
<MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton>
|
||||
<MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="fas fa-trash-alt"></i> {{ i18n.ts._pages.eyeCatchingImageRemove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
</template>
|
||||
</I18n>
|
||||
</li>
|
||||
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
|
||||
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ i18n.ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
|
||||
<li>
|
||||
{{ i18n.ts._2fa.step3 }}<br>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<details>
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
|
||||
{{ $t('_sfx.' + type) }}
|
||||
{{ i18n.t('_sfx.' + type) }}
|
||||
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
|
||||
<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
|
||||
</FormLink>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkContainer>
|
||||
<template #header><i class="fas fa-chart-simple" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
|
||||
<template #header><i class="fas fa-chart-simple" style="margin-right: 0.5em;"></i>{{ i18n.ts.activity }}</template>
|
||||
<template #func>
|
||||
<button class="_button" @click="showMenu">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkContainer :max-height="300" :foldable="true">
|
||||
<template #header><i class="fas fa-image" style="margin-right: 0.5em;"></i>{{ $ts.images }}</template>
|
||||
<template #header><i class="fas fa-image" style="margin-right: 0.5em;"></i>{{ i18n.ts.images }}</template>
|
||||
<div class="ujigsodd">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-if="!fetching && images.length > 0" class="stream">
|
||||
|
@ -13,7 +13,7 @@
|
|||
<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<p v-if="!fetching && images.length == 0" class="empty">{{ $ts.nothing }}</p>
|
||||
<p v-if="!fetching && images.length == 0" class="empty">{{ i18n.ts.nothing }}</p>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
@ -26,6 +26,7 @@ import * as os from '@/os';
|
|||
import MkContainer from '@/components/ui/container.vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Record<string, any>;
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
<form class="mk-setup" @submit.prevent="submit()">
|
||||
<h1>Welcome to Misskey!</h1>
|
||||
<div class="_formRoot">
|
||||
<p>{{ $ts.intro }}</p>
|
||||
<p>{{ i18n.ts.intro }}</p>
|
||||
<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username class="_formBlock">
|
||||
<template #label>{{ $ts.username }}</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" data-cy-admin-password class="_formBlock">
|
||||
<template #label>{{ $ts.password }}</template>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
</MkInput>
|
||||
<div class="bottom _formBlock">
|
||||
<MkButton gradate type="submit" :disabled="submitting" data-cy-admin-ok>
|
||||
{{ submitting ? $ts.processing : $ts.done }}<MkEllipsis v-if="submitting"/>
|
||||
{{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/>
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -65,9 +65,33 @@ export function getNoteMenu(props: {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleThreadMute(mute: boolean): void {
|
||||
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
|
||||
noteId: appearNote.id,
|
||||
function muteThread(): void {
|
||||
// show global settings by default
|
||||
const includingTypes = foundkey.notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x));
|
||||
os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
|
||||
includingTypes,
|
||||
showGlobalToggle: false,
|
||||
message: i18n.ts.threadMuteNotificationsDesc,
|
||||
notificationTypes: foundkey.noteNotificationTypes,
|
||||
}, {
|
||||
done: async (res) => {
|
||||
const { includingTypes: value } = res;
|
||||
let mutingNotificationTypes: string[] | undefined;
|
||||
if (value != null) {
|
||||
mutingNotificationTypes = foundkey.noteNotificationTypes.filter(x => !value.includes(x))
|
||||
}
|
||||
|
||||
await os.apiWithDialog('notes/thread-muting/create', {
|
||||
noteId: appearNote.id,
|
||||
mutingNotificationTypes,
|
||||
});
|
||||
}
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function unmuteThread(): void {
|
||||
os.apiWithDialog('notes/thread-muting/delete', {
|
||||
noteId: appearNote.id
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -168,9 +192,17 @@ export function getNoteMenu(props: {
|
|||
async function translate(): Promise<void> {
|
||||
if (props.translation.value != null) return;
|
||||
props.translating.value = true;
|
||||
|
||||
let targetLang = localStorage.getItem('lang') || navigator.language;
|
||||
targetLang = targetLang.toUpperCase();
|
||||
if (!['EN-GB', 'EN-US', 'PT-BR', 'PT-PT'].includes(targetLang)) {
|
||||
// only the language code without country code is allowed
|
||||
targetLang = targetLang.split('-', 1)[0];
|
||||
}
|
||||
|
||||
const res = await os.api('notes/translate', {
|
||||
noteId: appearNote.id,
|
||||
targetLang: localStorage.getItem('lang') || navigator.language,
|
||||
targetLang,
|
||||
});
|
||||
props.translating.value = false;
|
||||
props.translation.value = res;
|
||||
|
@ -243,11 +275,11 @@ export function getNoteMenu(props: {
|
|||
statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
action: () => unmuteThread(),
|
||||
} : {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
action: () => muteThread(),
|
||||
}),
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
|
||||
icon: 'fas fa-thumbtack',
|
||||
|
|
|
@ -5,28 +5,28 @@
|
|||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||
</button>
|
||||
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
||||
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
||||
<i class="fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||
</MkA>
|
||||
<template v-for="item in menu" :key="item">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ i18n.ts[menuDef[item].title] }}</span>
|
||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</component>
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
<MkA v-if="iAmModerator" v-click-anime class="item" active-class="active" to="/admin">
|
||||
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
||||
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<button v-click-anime class="item _button" @click="more">
|
||||
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
||||
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
||||
<i class="fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||
</MkA>
|
||||
<button class="item _button post" data-cy-open-post-form @click="post">
|
||||
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
||||
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,6 +38,7 @@ import * as os from '@/os';
|
|||
import { menuDef } from '@/menu';
|
||||
import { openAccountMenu, $i, iAmModerator } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const menu = toRef(defaultStore.state, 'menu');
|
||||
const otherMenuItemIndicated = $computed(() => {
|
||||
|
|
|
@ -5,28 +5,28 @@
|
|||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||
</button>
|
||||
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
||||
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
||||
<i class="fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||
</MkA>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
|
||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ i18n.ts[menuDef[item].title] }}</span>
|
||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</component>
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
||||
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
|
||||
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<button v-click-anime class="item _button" @click="more">
|
||||
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
||||
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
||||
<i class="fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||
</MkA>
|
||||
<button class="item _button post" data-cy-open-post-form @click="os.post">
|
||||
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
|
||||
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,6 +38,7 @@ import * as os from '@/os';
|
|||
import { menuDef } from '@/menu';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const iconOnly = ref(false);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="azykntjl">
|
||||
<div class="body">
|
||||
<div class="left">
|
||||
<MkA v-click-anime v-tooltip="$ts.timeline" class="item index" active-class="active" to="/" exact>
|
||||
<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
|
||||
<i class="fas fa-home fa-fw"></i>
|
||||
</MkA>
|
||||
<template v-for="item in menu">
|
||||
|
@ -22,7 +22,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<MkA v-click-anime v-tooltip="$ts.settings" class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null">
|
||||
<MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null">
|
||||
<i class="fas fa-cog fa-fw"></i>
|
||||
</MkA>
|
||||
<button v-click-anime class="item _button account" @click="openAccountMenuWrapper">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<XColumn :func="{ handler: setAntenna, title: i18n.ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<XColumn :func="{ handler: setList, title: i18n.ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: i18n.ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<XNotifications :include-types="column.includingTypes"/>
|
||||
|
@ -12,6 +12,7 @@ import XColumn from './column.vue';
|
|||
import { updateColumn , Column } from './deck-store';
|
||||
import XNotifications from '@/components/notifications.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<XColumn :func="{ handler: setType, title: i18n.ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
|
||||
|
@ -11,9 +11,9 @@
|
|||
<div v-if="disabled" class="iwaalbte">
|
||||
<p>
|
||||
<i class="fas fa-minus-circle"></i>
|
||||
{{ $t('disabled-timeline.title') }}
|
||||
{{ i18n.t('disabled-timeline.title') }}
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
<p class="desc">{{ i18n.t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||
</XColumn>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<XColumn :func="{ handler: func, title: i18n.ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
|
@ -12,6 +12,7 @@
|
|||
import XColumn from './column.vue';
|
||||
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||
import XWidgets from '@/components/widgets.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
|
||||
<div class="calendar" :class="{ isHoliday }">
|
||||
<p class="month-and-year">
|
||||
<span class="year">{{ $t('yearX', { year }) }}</span>
|
||||
<span class="month">{{ $t('monthX', { month }) }}</span>
|
||||
<span class="year">{{ i18n.t('yearX', { year }) }}</span>
|
||||
<span class="month">{{ i18n.t('monthX', { month }) }}</span>
|
||||
</p>
|
||||
<p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
|
||||
<p v-else class="day">{{ $t('dayX', { day }) }}</p>
|
||||
<p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
|
||||
<p v-else class="day">{{ i18n.t('dayX', { day }) }}</p>
|
||||
<p class="week-day">{{ weekDay }}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<p v-if="widgetProps.folderId == null">
|
||||
{{ i18n.ts.folder }}
|
||||
</p>
|
||||
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
|
||||
<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
|
||||
<div ref="slideA" class="slide a"></div>
|
||||
<div ref="slideB" class="slide b"></div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<i v-else-if="widgetProps.src === 'global'" class="fas fa-globe"></i>
|
||||
<i v-else-if="widgetProps.src === 'list'" class="fas fa-list-ul"></i>
|
||||
<i v-else-if="widgetProps.src === 'antenna'" class="fas fa-satellite"></i>
|
||||
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span>
|
||||
<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
|
||||
<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div v-for="stat in stats" :key="stat.tag">
|
||||
<div class="tag">
|
||||
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
|
||||
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
<p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
|
||||
</div>
|
||||
<MkMiniChart class="chart" :src="stat.chart"/>
|
||||
</div>
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
"api-prod": "npx api-extractor run --verbose",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"jest": "jest --coverage --detectOpenHandles",
|
||||
"test": "yarn jest && yarn tsd",
|
||||
"clean": "rm -rf built/",
|
||||
"clean-all": "yarn clean && rm -rf node_modules/"
|
||||
"test": "yarn jest && yarn tsd"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "^7.19.3",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
||||
|
||||
export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded'] as const;
|
||||
|
||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||
|
||||
export const ffVisibility = ['public', 'followers', 'private'] as const;
|
||||
|
|
|
@ -10,6 +10,7 @@ export {
|
|||
export {
|
||||
permissions,
|
||||
notificationTypes,
|
||||
noteNotificationTypes,
|
||||
mutedNoteReasons,
|
||||
ffVisibility,
|
||||
} from './consts.js';
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
"scripts": {
|
||||
"watch": "node build.js watch",
|
||||
"build": "node build.js",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"clean": "rm -rf built/",
|
||||
"clean-all": "yarn clean && rm -rf node_modules/"
|
||||
"lint": "eslint src --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.14.13",
|
||||
|
|
17
scripts/clean-all.js
Normal file
17
scripts/clean-all.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const fs = require('fs');
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/tsconfig.tsbuildinfo', { force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/node_modules', { recursive: true, force: true });
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/client/node_modules', { recursive: true, force: true });
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/foundkey-js/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/foundkey-js/node_modules', { recursive: true, force: true });
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true });
|
||||
|
||||
fs.rmSync(__dirname + '/../built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true });
|
8
scripts/clean.js
Normal file
8
scripts/clean.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const fs = require('fs');
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/tsconfig.tsbuildinfo', { force: true });
|
||||
fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/foundkey-js/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../built', { recursive: true, force: true });
|
Loading…
Reference in a new issue