Compare commits

...

18 commits

Author SHA1 Message Date
Essem
50a93e453e merge: feat: Add language metadata to notes (!401)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/401

Closes #253
2024-03-31 08:44:43 +00:00
Amelia Yukii
126248e58d merge: some validation fixes (!484)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/484

Closes #469

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-03-30 11:05:58 +00:00
dakkar
074de82bf7 some validation fixes 2024-03-30 11:05:58 +00:00
Essem
184707e048 Merge branch 'develop' into 'feat/note-lang'
# Conflicts:
#   locales/en-US.yml
#   packages/backend/src/core/activitypub/models/ApNoteService.ts
#   packages/frontend/src/components/MkPostForm.vue
2024-03-02 19:50:43 +00:00
Essem
8ccb89fb42
Revert langmap change in i/update 2024-02-25 22:13:39 -06:00
Essem
6998d4f6d2
Revert languages in profile settings 2024-02-25 21:59:14 -06:00
Essem
5bd2272afa
Fix oddity 2024-02-25 16:14:05 -06:00
Essem
03876616d2
feat: Add lang parameter to post elements 2024-02-25 16:07:55 -06:00
Essem
8860e6866b
refactor: Simplify some parts of the post form language handling 2024-02-25 16:07:55 -06:00
Essem
d534a1cded
refactor: Revert langmap 2024-02-25 16:07:55 -06:00
Essem
b427b1ae73
fix: Use proper source text for note updates 2024-02-25 16:07:55 -06:00
Essem
f1be178b52
refactor: Smarter contentMap checking 2024-02-25 16:07:54 -06:00
Essem
212aaab5b7
fix: Add some more regional language variants to langmap 2024-02-25 16:07:14 -06:00
Essem
8a416cd302
feat: Add langPref config option 2024-02-25 16:07:14 -06:00
Essem
4f45e72799
fix: Do not convert langs to lowercase 2024-02-25 16:06:49 -06:00
Essem
112272c254
fix: Do not discard region tag in post form unless necessary 2024-02-25 16:05:28 -06:00
Essem
91ba7a45c6
fix: Add ja-JP locale for noLanguage 2024-02-25 16:05:28 -06:00
Essem
a76d3cf861
feat: Add language metadata to notes 2024-02-25 16:04:53 -06:00
44 changed files with 652 additions and 82 deletions

View file

@ -106,7 +106,7 @@ redis:
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global # You can set scope to local (default value) or global
# (include notes from remote). # (include notes from remote).
#meilisearch: #meilisearch:
@ -204,7 +204,7 @@ signToActivityPubGet: true
checkActivityPubGetSignature: false checkActivityPubGetSignature: false
# For security reasons, uploading attachments from the intranet is prohibited, # For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined". # but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [ #allowedPrivateNetworks: [
# '127.0.0.1/32' # '127.0.0.1/32'
@ -212,5 +212,9 @@ checkActivityPubGetSignature: false
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# Prefer these languages for remote notes with language-specific content
# Must be valid language codes according to BCP 47
#langPref: ['en', 'ja']
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View file

@ -163,7 +163,7 @@ redis:
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global # You can set scope to local (default value) or global
# (include notes from remote). # (include notes from remote).
#meilisearch: #meilisearch:
@ -261,7 +261,7 @@ signToActivityPubGet: true
checkActivityPubGetSignature: false checkActivityPubGetSignature: false
# For security reasons, uploading attachments from the intranet is prohibited, # For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined". # but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [ #allowedPrivateNetworks: [
# '127.0.0.1/32' # '127.0.0.1/32'
@ -269,5 +269,9 @@ checkActivityPubGetSignature: false
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# Prefer these languages for remote notes with language-specific content
# Must be valid language codes according to BCP 47
#langPref: ['en', 'ja']
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View file

@ -284,6 +284,10 @@ checkActivityPubGetSignature: false
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# Prefer these languages for remote notes with language-specific content
# Must be valid language codes according to BCP 47
#langPref: ['en', 'ja']
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View file

@ -1270,6 +1270,7 @@ hemisphere: "Where are you located"
withSensitive: "Include notes with sensitive files" withSensitive: "Include notes with sensitive files"
userSaysSomethingSensitive: "Post by {name} contains sensitive content" userSaysSomethingSensitive: "Post by {name} contains sensitive content"
enableHorizontalSwipe: "Swipe to switch tabs" enableHorizontalSwipe: "Swipe to switch tabs"
noLanguage: "No language"
loading: "Loading" loading: "Loading"
surrender: "Cancel" surrender: "Cancel"
gameRetry: "Retry" gameRetry: "Retry"

4
locales/index.d.ts vendored
View file

@ -5093,6 +5093,10 @@ export interface Locale extends ILocale {
* *
*/ */
"enableHorizontalSwipe": string; "enableHorizontalSwipe": string;
/**
*
*/
"noLanguage": string;
/** /**
* *
*/ */

View file

@ -1269,6 +1269,7 @@ hemisphere: "お住まいの地域"
withSensitive: "センシティブなファイルを含むノートを表示" withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える" enableHorizontalSwipe: "スワイプしてタブを切り替える"
noLanguage: "言語なし"
loading: "読み込み中" loading: "読み込み中"
surrender: "やめる" surrender: "やめる"
gameRetry: "リトライ" gameRetry: "リトライ"

View file

@ -0,0 +1,13 @@
export class AddPostLang1706852162173 {
name = 'AddPostLang1706852162173'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "lang" character varying(10)`);
await queryRunner.query(`ALTER TABLE "note_edit" ADD "lang" character varying(10)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`);
await queryRunner.query(`ALTER TABLE "note_edit" DROP COLUMN "lang"`);
}
}

View file

@ -91,6 +91,8 @@ type Source = {
customMOTD?: string[]; customMOTD?: string[];
langPref?: string[];
signToActivityPubGet?: boolean; signToActivityPubGet?: boolean;
checkActivityPubGetSignature?: boolean; checkActivityPubGetSignature?: boolean;
@ -153,6 +155,7 @@ export type Config = {
customMOTD: string[] | undefined; customMOTD: string[] | undefined;
signToActivityPubGet: boolean; signToActivityPubGet: boolean;
checkActivityPubGetSignature: boolean | undefined; checkActivityPubGetSignature: boolean | undefined;
langPref: string[];
version: string; version: string;
publishTarballInsteadOfProvideRepositoryUrl: boolean; publishTarballInsteadOfProvideRepositoryUrl: boolean;
@ -269,6 +272,7 @@ export function loadConfig(): Config {
inboxJobMaxAttempts: config.inboxJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles, proxyRemoteFiles: config.proxyRemoteFiles,
customMOTD: config.customMOTD, customMOTD: config.customMOTD,
langPref: config.langPref ?? ['en', 'ja'],
signToActivityPubGet: config.signToActivityPubGet ?? true, signToActivityPubGet: config.signToActivityPubGet ?? true,
checkActivityPubGetSignature: config.checkActivityPubGetSignature, checkActivityPubGetSignature: config.checkActivityPubGetSignature,
mediaProxy: externalMediaProxy ?? internalMediaProxy, mediaProxy: externalMediaProxy ?? internalMediaProxy,

View file

@ -15,6 +15,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js'; import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import type { URL } from 'node:url';
@ -125,7 +126,12 @@ export class HttpRequestService {
validators: [validateContentTypeSetAsActivityPub], validators: [validateContentTypeSetAsActivityPub],
}); });
return await res.json() as IObject; const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(activity, [url, finalUrl]);
return activity;
} }
@bindThis @bindThis

View file

@ -133,6 +133,7 @@ type Option = {
createdAt?: Date | null; createdAt?: Date | null;
name?: string | null; name?: string | null;
text?: string | null; text?: string | null;
lang?: string | null;
reply?: MiNote | null; reply?: MiNote | null;
renote?: MiNote | null; renote?: MiNote | null;
files?: MiDriveFile[] | null; files?: MiDriveFile[] | null;
@ -603,6 +604,7 @@ export class NoteCreateService implements OnApplicationShutdown {
: null, : null,
name: data.name, name: data.name,
text: data.text, text: data.text,
lang: data.lang,
hasPoll: data.poll != null, hasPoll: data.poll != null,
cw: data.cw ?? null, cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)), tags: tags.map(tag => normalizeForSearch(tag)),

View file

@ -123,6 +123,7 @@ type Option = {
createdAt?: Date | null; createdAt?: Date | null;
name?: string | null; name?: string | null;
text?: string | null; text?: string | null;
lang?: string | null;
reply?: MiNote | null; reply?: MiNote | null;
renote?: MiNote | null; renote?: MiNote | null;
files?: MiDriveFile[] | null; files?: MiDriveFile[] | null;
@ -429,6 +430,9 @@ export class NoteEditService implements OnApplicationShutdown {
if (oldnote.hasPoll !== !!data.poll) { if (oldnote.hasPoll !== !!data.poll) {
update.hasPoll = !!data.poll; update.hasPoll = !!data.poll;
} }
if (data.lang !== oldnote.lang) {
update.lang = data.lang;
}
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
@ -443,6 +447,7 @@ export class NoteEditService implements OnApplicationShutdown {
oldText: oldnote.text || undefined, oldText: oldnote.text || undefined,
newText: update.text || undefined, newText: update.text || undefined,
cw: update.cw || undefined, cw: update.cw || undefined,
lang: update.lang || undefined,
fileIds: undefined, fileIds: undefined,
oldDate: exists ? oldnote.updatedAt as Date : this.idService.parse(oldnote.id).date, oldDate: exists ? oldnote.updatedAt as Date : this.idService.parse(oldnote.id).date,
updatedAt: new Date(), updatedAt: new Date(),
@ -462,6 +467,7 @@ export class NoteEditService implements OnApplicationShutdown {
: null, : null,
name: data.name, name: data.name,
text: data.text, text: data.text,
lang: data.lang,
hasPoll: data.poll != null, hasPoll: data.poll != null,
cw: data.cw ?? null, cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)), tags: tags.map(tag => normalizeForSearch(tag)),

View file

@ -86,7 +86,7 @@ export class UtilityService {
@bindThis @bindThis
public extractDbHost(uri: string): string { public extractDbHost(uri: string): string {
const url = new URL(uri); const url = new URL(uri);
return this.toPuny(url.hostname); return this.toPuny(url.host);
} }
@bindThis @bindThis
@ -99,4 +99,11 @@ export class UtilityService {
if (host == null) return null; if (host == null) return null;
return toASCII(host.toLowerCase()); return toASCII(host.toLowerCase());
} }
@bindThis
public punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
} }

View file

@ -285,7 +285,7 @@ export class ApRendererService {
if (instance && instance.softwareName === 'pleroma') isMastodon = true; if (instance && instance.softwareName === 'pleroma') isMastodon = true;
} }
} }
const object: ILike = { const object: ILike = {
type: 'Like', type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`, id: `${this.config.url}/likes/${noteReaction.id}`,
@ -451,9 +451,15 @@ export class ApRendererService {
_misskey_content: text, _misskey_content: text,
source: { source: {
content: text, content: text,
contentMap: note.lang ? {
[note.lang]: text,
} : undefined,
mediaType: 'text/x.misskeymarkdown', mediaType: 'text/x.misskeymarkdown',
}, },
}), }),
contentMap: note.lang && content ? {
[note.lang]: content,
} : undefined,
_misskey_quote: quote, _misskey_quote: quote,
quoteUrl: quote, quoteUrl: quote,
quoteUri: quote, quoteUri: quote,
@ -743,9 +749,15 @@ export class ApRendererService {
_misskey_content: text, _misskey_content: text,
source: { source: {
content: text, content: text,
contentMap: note.lang ? {
[note.lang]: text,
} : undefined,
mediaType: 'text/x.misskeymarkdown', mediaType: 'text/x.misskeymarkdown',
}, },
}), }),
contentMap: note.lang && content ? {
[note.lang]: content,
} : undefined,
_misskey_quote: quote, _misskey_quote: quote,
quoteUrl: quote, quoteUrl: quote,
quoteUri: quote, quoteUri: quote,

View file

@ -14,7 +14,9 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import type { IObject } from './type.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
type Request = { type Request = {
url: string; url: string;
@ -201,6 +203,11 @@ export class ApRequestService {
validators: [validateContentTypeSetAsActivityPub], validators: [validateContentTypeSetAsActivityPub],
}); });
return await res.json(); const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(activity, [url, finalUrl]);
return activity;
} }
} }

View file

@ -115,6 +115,14 @@ export class Resolver {
throw new Error('invalid response'); throw new Error('invalid response');
} }
// HttpRequestService / ApRequestService have already checked that
// `object.id` or `object.url` matches the URL used to fetch the
// object after redirects; here we double-check that no redirects
// bounced between hosts
if (object.id && (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value))) {
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
}
return object; return object;
} }

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: dakkar and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { IObject } from '../type.js';
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
const idOk = activity.id !== undefined && urls.includes(activity.id);
// technically `activity.url` could be an `ApObject = IObject |
// string | (IObject | string)[]`, but if it's a complicated thing
// and the `activity.id` doesn't match, I think we're fine
// rejecting the activity
const urlOk = typeof(activity.url) === 'string' && urls.includes(activity.url);
if (!idOk && !urlOk) {
throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`);
}
}

View file

@ -25,6 +25,7 @@ import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { langs } from '@/misc/langmap.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
@ -39,7 +40,7 @@ import { ApMentionService } from './ApMentionService.js';
import { ApQuestionService } from './ApQuestionService.js'; import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js'; import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js'; import type { IObject, IPost, Obj } from '../type.js';
@Injectable() @Injectable()
export class ApNoteService { export class ApNoteService {
@ -173,12 +174,17 @@ export class ApNoteService {
// テキストのパース // テキストのパース
let text: string | null = null; let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { let lang: string | null = null;
text = note.source.content; if (note.source?.mediaType === 'text/x.misskeymarkdown' && (typeof note.source.content === 'string' || note.source.contentMap)) {
const guessed = this.guessLang(note.source);
text = guessed.text;
lang = guessed.lang;
} else if (typeof note._misskey_content !== 'undefined') { } else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content; text = note._misskey_content;
} else if (typeof note.content === 'string') { } else if (typeof note.content === 'string' || note.contentMap) {
text = this.apMfmService.htmlToMfm(note.content, note.tag); const guessed = this.guessLang(note);
lang = guessed.lang;
if (guessed.text) text = this.apMfmService.htmlToMfm(guessed.text, note.tag);
} }
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
@ -310,6 +316,7 @@ export class ApNoteService {
name: note.name, name: note.name,
cw, cw,
text, text,
lang,
localOnly: false, localOnly: false,
visibility, visibility,
visibleUsers, visibleUsers,
@ -400,12 +407,17 @@ export class ApNoteService {
// テキストのパース // テキストのパース
let text: string | null = null; let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { let lang: string | null = null;
text = note.source.content; if (note.source?.mediaType === 'text/x.misskeymarkdown' && (typeof note.source.content === 'string' || note.source.contentMap)) {
const guessed = this.guessLang(note.source);
text = guessed.text;
lang = guessed.lang;
} else if (typeof note._misskey_content !== 'undefined') { } else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content; text = note._misskey_content;
} else if (typeof note.content === 'string') { } else if (typeof note.content === 'string' || note.contentMap) {
text = this.apMfmService.htmlToMfm(note.content, note.tag); const guessed = this.guessLang(note);
lang = guessed.lang;
if (guessed.text) text = this.apMfmService.htmlToMfm(guessed.text, note.tag);
} }
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
@ -537,6 +549,7 @@ export class ApNoteService {
name: note.name, name: note.name,
cw, cw,
text, text,
lang,
localOnly: false, localOnly: false,
visibility, visibility,
visibleUsers, visibleUsers,
@ -654,4 +667,40 @@ export class ApNoteService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
})); }));
} }
@bindThis
private guessLang(source: { contentMap?: Obj | null, content?: string | null }): { lang: string | null, text: string | null } {
// do we have a map?
if (source.contentMap) {
const entries = Object.entries(source.contentMap);
// only one entry: take that
if (entries.length === 1) {
return { lang: entries[0][0], text: entries[0][1] };
}
// did the sender indicate a preferred language?
if (source.content) {
for (const e of entries) {
if (e[1] === source.content) {
return { lang: e[0], text: e[1] };
}
}
}
// can we find one of *our* preferred languages?
for (const prefLang of this.config.langPref) {
if (source.contentMap[prefLang]) {
return { lang: prefLang, text: source.contentMap[prefLang] };
}
}
// bah, just pick one
return { lang: entries[0][0], text: entries[0][1] };
}
// no map, so we don't know the language, just take whatever
// content we got
return { lang: null, text: source.content ?? null };
}
} }

View file

@ -127,12 +127,6 @@ export class ApPersonService implements OnModuleInit {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
private punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
/** /**
* Validate and convert to actor object * Validate and convert to actor object
* @param x Fetched object * @param x Fetched object
@ -140,7 +134,7 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
private validateActor(x: IObject, uri: string): IActor { private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri); const expectHost = this.utilityService.punyHost(uri);
if (!isActor(x)) { if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`); throw new Error(`invalid Actor type '${x.type}'`);
@ -154,6 +148,19 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: wrong inbox'); throw new Error('invalid Actor: wrong inbox');
} }
if (this.utilityService.punyHost(x.inbox) !== expectHost) {
throw new Error('invalid Actor: inbox has different host');
}
for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) {
const collectionUri = (x as IActor)[collection];
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
if (this.utilityService.punyHost(collectionUri) !== expectHost) {
throw new Error(`invalid Actor: ${collection} has different host`);
}
}
}
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
throw new Error('invalid Actor: wrong username'); throw new Error('invalid Actor: wrong username');
} }
@ -177,7 +184,7 @@ export class ApPersonService implements OnModuleInit {
x.summary = truncate(x.summary, summaryLength); x.summary = truncate(x.summary, summaryLength);
} }
const idHost = this.punyHost(x.id); const idHost = this.utilityService.punyHost(x.id);
if (idHost !== expectHost) { if (idHost !== expectHost) {
throw new Error('invalid Actor: id has different host'); throw new Error('invalid Actor: id has different host');
} }
@ -187,7 +194,7 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: publicKey.id is not a string'); throw new Error('invalid Actor: publicKey.id is not a string');
} }
const publicKeyIdHost = this.punyHost(x.publicKey.id); const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
if (publicKeyIdHost !== expectHost) { if (publicKeyIdHost !== expectHost) {
throw new Error('invalid Actor: publicKey.id has different host'); throw new Error('invalid Actor: publicKey.id has different host');
} }
@ -286,7 +293,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Creating the Person: ${person.id}`); this.logger.info(`Creating the Person: ${person.id}`);
const host = this.punyHost(object.id); const host = this.utilityService.punyHost(object.id);
const fields = this.analyzeAttachments(person.attachment ?? []); const fields = this.analyzeAttachments(person.attachment ?? []);

View file

@ -21,6 +21,7 @@ export interface IObject {
inReplyTo?: any; inReplyTo?: any;
replies?: ICollection; replies?: ICollection;
content?: string | null; content?: string | null;
contentMap?: Obj | null;
startTime?: Date; startTime?: Date;
endTime?: Date; endTime?: Date;
icon?: any; icon?: any;
@ -114,6 +115,7 @@ export interface IPost extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
source?: { source?: {
content: string; content: string;
contentMap?: Obj | null;
mediaType: string; mediaType: string;
}; };
_misskey_quote?: string; _misskey_quote?: string;
@ -128,6 +130,7 @@ export interface IQuestion extends IObject {
actor: string; actor: string;
source?: { source?: {
content: string; content: string;
contentMap?: Obj | null;
mediaType: string; mediaType: string;
}; };
_misskey_quote?: string; _misskey_quote?: string;

View file

@ -17,12 +17,12 @@ import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js'; import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Config } from '@/config.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js'; import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { Config } from '@/config.js';
@Injectable() @Injectable()
export class NoteEntityService implements OnModuleInit { export class NoteEntityService implements OnModuleInit {
@ -120,7 +120,7 @@ export class NoteEntityService implements OnModuleInit {
followerId: meId, followerId: meId,
}, },
}); });
hide = !isFollowing; hide = !isFollowing;
} else { } else {
// フォロワーかどうか // フォロワーかどうか
@ -373,6 +373,7 @@ export class NoteEntityService implements OnModuleInit {
uri: note.uri ?? undefined, uri: note.uri ?? undefined,
url: note.url ?? undefined, url: note.url ?? undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
lang: note.lang,
...(meId && Object.keys(note.reactions).length > 0 ? { ...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note, meId, options?._hint_), myReaction: this.populateMyReaction(note, meId, options?._hint_),
} : {}), } : {}),

View file

@ -35,6 +35,9 @@ export const langmap = {
'ar-SA': { 'ar-SA': {
nativeName: 'العربية (السعودية)', nativeName: 'العربية (السعودية)',
}, },
'ay': {
nativeName: 'Aymar aru',
},
'ay-BO': { 'ay-BO': {
nativeName: 'Aymar aru', nativeName: 'Aymar aru',
}, },
@ -44,6 +47,9 @@ export const langmap = {
'az-AZ': { 'az-AZ': {
nativeName: 'Azərbaycan dili', nativeName: 'Azərbaycan dili',
}, },
'be': {
nativeName: 'Беларуская',
},
'be-BY': { 'be-BY': {
nativeName: 'Беларуская', nativeName: 'Беларуская',
}, },
@ -65,6 +71,9 @@ export const langmap = {
'br': { 'br': {
nativeName: 'Brezhoneg', nativeName: 'Brezhoneg',
}, },
'bs': {
nativeName: 'Bosanski',
},
'bs-BA': { 'bs-BA': {
nativeName: 'Bosanski', nativeName: 'Bosanski',
}, },
@ -77,7 +86,7 @@ export const langmap = {
'cak': { 'cak': {
nativeName: 'Maya Kaqchikel', nativeName: 'Maya Kaqchikel',
}, },
'ck-US': { 'chr': {
nativeName: 'ᏣᎳᎩ (tsalagi)', nativeName: 'ᏣᎳᎩ (tsalagi)',
}, },
'cs': { 'cs': {
@ -152,9 +161,6 @@ export const langmap = {
'en-ZA': { 'en-ZA': {
nativeName: 'English (South Africa)', nativeName: 'English (South Africa)',
}, },
'en@pirate': {
nativeName: 'English (Pirate)',
},
'eo': { 'eo': {
nativeName: 'Esperanto', nativeName: 'Esperanto',
}, },
@ -248,6 +254,9 @@ export const langmap = {
'fr-CH': { 'fr-CH': {
nativeName: 'Français (Suisse)', nativeName: 'Français (Suisse)',
}, },
'fy': {
nativeName: 'Frysk',
},
'fy-NL': { 'fy-NL': {
nativeName: 'Frysk', nativeName: 'Frysk',
}, },
@ -266,16 +275,22 @@ export const langmap = {
'gl-ES': { 'gl-ES': {
nativeName: 'Galego', nativeName: 'Galego',
}, },
'gn': {
nativeName: 'Avañe\'ẽ',
},
'gn-PY': { 'gn-PY': {
nativeName: 'Avañe\'ẽ', nativeName: 'Avañe\'ẽ',
}, },
'gu': {
nativeName: 'ગુજરાતી',
},
'gu-IN': { 'gu-IN': {
nativeName: 'ગુજરાતી', nativeName: 'ગુજરાતી',
}, },
'gv': { 'gv': {
nativeName: 'Gaelg', nativeName: 'Gaelg',
}, },
'gx-GR': { 'grc': {
nativeName: 'Ἑλληνική ἀρχαία', nativeName: 'Ἑλληνική ἀρχαία',
}, },
'he': { 'he': {
@ -338,12 +353,21 @@ export const langmap = {
'ja-JP': { 'ja-JP': {
nativeName: '日本語 (日本)', nativeName: '日本語 (日本)',
}, },
'jv': {
nativeName: 'Basa Jawa',
},
'jv-ID': { 'jv-ID': {
nativeName: 'Basa Jawa', nativeName: 'Basa Jawa',
}, },
'ka': {
nativeName: 'ქართული',
},
'ka-GE': { 'ka-GE': {
nativeName: 'ქართული', nativeName: 'ქართული',
}, },
'kk': {
nativeName: 'Қазақша',
},
'kk-KZ': { 'kk-KZ': {
nativeName: 'Қазақша', nativeName: 'Қазақша',
}, },
@ -371,6 +395,9 @@ export const langmap = {
'ko-KR': { 'ko-KR': {
nativeName: '한국어 (한국)', nativeName: '한국어 (한국)',
}, },
'ku': {
nativeName: 'Kurdî',
},
'ku-TR': { 'ku-TR': {
nativeName: 'Kurdî', nativeName: 'Kurdî',
}, },
@ -386,6 +413,9 @@ export const langmap = {
'lb': { 'lb': {
nativeName: 'Lëtzebuergesch', nativeName: 'Lëtzebuergesch',
}, },
'li': {
nativeName: 'Lèmbörgs',
},
'li-NL': { 'li-NL': {
nativeName: 'Lèmbörgs', nativeName: 'Lèmbörgs',
}, },
@ -404,6 +434,9 @@ export const langmap = {
'mai': { 'mai': {
nativeName: 'मैथिली, মৈথিলী', nativeName: 'मैथिली, মৈথিলী',
}, },
'mg': {
nativeName: 'Malagasy',
},
'mg-MG': { 'mg-MG': {
nativeName: 'Malagasy', nativeName: 'Malagasy',
}, },
@ -419,6 +452,9 @@ export const langmap = {
'ml-IN': { 'ml-IN': {
nativeName: 'മലയാളം', nativeName: 'മലയാളം',
}, },
'mn': {
nativeName: 'Монгол',
},
'mn-MN': { 'mn-MN': {
nativeName: 'Монгол', nativeName: 'Монгол',
}, },
@ -443,6 +479,9 @@ export const langmap = {
'my': { 'my': {
nativeName: 'ဗမာစကာ', nativeName: 'ဗမာစကာ',
}, },
'nan': {
nativeName: '閩南語',
},
'no': { 'no': {
nativeName: 'Norsk', nativeName: 'Norsk',
}, },
@ -467,12 +506,18 @@ export const langmap = {
'nl-NL': { 'nl-NL': {
nativeName: 'Nederlands (Nederland)', nativeName: 'Nederlands (Nederland)',
}, },
'nn': {
nativeName: 'Norsk (nynorsk)',
},
'nn-NO': { 'nn-NO': {
nativeName: 'Norsk (nynorsk)', nativeName: 'Norsk (nynorsk)',
}, },
'oc': { 'oc': {
nativeName: 'Occitan', nativeName: 'Occitan',
}, },
'or': {
nativeName: 'ଓଡ଼ିଆ',
},
'or-IN': { 'or-IN': {
nativeName: 'ଓଡ଼ିଆ', nativeName: 'ଓଡ଼ିଆ',
}, },
@ -488,6 +533,9 @@ export const langmap = {
'pl-PL': { 'pl-PL': {
nativeName: 'Polski', nativeName: 'Polski',
}, },
'ps': {
nativeName: 'پښتو',
},
'ps-AF': { 'ps-AF': {
nativeName: 'پښتو', nativeName: 'پښتو',
}, },
@ -500,9 +548,15 @@ export const langmap = {
'pt-PT': { 'pt-PT': {
nativeName: 'Português (Portugal)', nativeName: 'Português (Portugal)',
}, },
'qu': {
nativeName: 'Qhichwa',
},
'qu-PE': { 'qu-PE': {
nativeName: 'Qhichwa', nativeName: 'Qhichwa',
}, },
'rm': {
nativeName: 'Rumantsch',
},
'rm-CH': { 'rm-CH': {
nativeName: 'Rumantsch', nativeName: 'Rumantsch',
}, },
@ -518,15 +572,24 @@ export const langmap = {
'ru-RU': { 'ru-RU': {
nativeName: 'Русский', nativeName: 'Русский',
}, },
'sa': {
nativeName: 'संस्कृतम्',
},
'sa-IN': { 'sa-IN': {
nativeName: 'संस्कृतम्', nativeName: 'संस्कृतम्',
}, },
'se': {
nativeName: 'Davvisámegiella',
},
'se-NO': { 'se-NO': {
nativeName: 'Davvisámegiella', nativeName: 'Davvisámegiella',
}, },
'sh': { 'sh': {
nativeName: 'српскохрватски', nativeName: 'српскохрватски',
}, },
'si': {
nativeName: 'සිංහල',
},
'si-LK': { 'si-LK': {
nativeName: 'සිංහල', nativeName: 'සිංහල',
}, },
@ -542,6 +605,9 @@ export const langmap = {
'sl-SI': { 'sl-SI': {
nativeName: 'Slovenščina', nativeName: 'Slovenščina',
}, },
'so': {
nativeName: 'Soomaaliga',
},
'so-SO': { 'so-SO': {
nativeName: 'Soomaaliga', nativeName: 'Soomaaliga',
}, },
@ -602,12 +668,18 @@ export const langmap = {
'tlh': { 'tlh': {
nativeName: 'tlhIngan-Hol', nativeName: 'tlhIngan-Hol',
}, },
'tok': {
nativeName: 'Toki Pona',
},
'tr': { 'tr': {
nativeName: 'Türkçe', nativeName: 'Türkçe',
}, },
'tr-TR': { 'tr-TR': {
nativeName: 'Türkçe', nativeName: 'Türkçe',
}, },
'tt': {
nativeName: 'татарча',
},
'tt-RU': { 'tt-RU': {
nativeName: 'татарча', nativeName: 'татарча',
}, },
@ -635,6 +707,9 @@ export const langmap = {
'vi-VN': { 'vi-VN': {
nativeName: 'Tiếng Việt', nativeName: 'Tiếng Việt',
}, },
'xh': {
nativeName: 'isiXhosa',
},
'xh-ZA': { 'xh-ZA': {
nativeName: 'isiXhosa', nativeName: 'isiXhosa',
}, },
@ -644,6 +719,9 @@ export const langmap = {
'yi-DE': { 'yi-DE': {
nativeName: 'ייִדיש (German)', nativeName: 'ייִדיש (German)',
}, },
'yue': {
nativeName: '粵語',
},
'zh': { 'zh': {
nativeName: '中文', nativeName: '中文',
}, },
@ -665,7 +743,16 @@ export const langmap = {
'zh-TW': { 'zh-TW': {
nativeName: '中文(台灣)', nativeName: '中文(台灣)',
}, },
'zu': {
nativeName: 'isiZulu',
},
'zu-ZA': { 'zu-ZA': {
nativeName: 'isiZulu', nativeName: 'isiZulu',
}, },
}; };
export const langs: string[] = [
...(Object.keys(langmap).filter(tag => tag.indexOf('-') < 0)),
'zh-Hans',
'zh-Hant',
];

View file

@ -61,6 +61,12 @@ export class MiNote {
}) })
public text: string | null; public text: string | null;
@Column('varchar', {
length: 10,
nullable: true,
})
public lang: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 256, nullable: true,
}) })

View file

@ -1,4 +1,4 @@
import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from "typeorm"; import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from 'typeorm';
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import type { MiDriveFile } from './DriveFile.js'; import type { MiDriveFile } from './DriveFile.js';
@ -11,46 +11,52 @@ export class NoteEdit {
@Index() @Index()
@Column({ @Column({
...id(), ...id(),
comment: "The ID of note.", comment: 'The ID of note.',
}) })
public noteId: MiNote["id"]; public noteId: MiNote['id'];
@ManyToOne((type) => MiNote, { @ManyToOne((type) => MiNote, {
onDelete: "CASCADE", onDelete: 'CASCADE',
}) })
@JoinColumn() @JoinColumn()
public note: MiNote | null; public note: MiNote | null;
@Column("text", { @Column('text', {
nullable: true, nullable: true,
}) })
public oldText: string | null; public oldText: string | null;
@Column("text", { @Column('text', {
nullable: true, nullable: true,
}) })
public newText: string | null; public newText: string | null;
@Column("varchar", { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,
}) })
public cw: string | null; public cw: string | null;
@Column('varchar', {
length: 10,
nullable: true,
})
public lang: string | null;
@Column({ @Column({
...id(), ...id(),
array: true, array: true,
default: "{}", default: '{}',
}) })
public fileIds: MiDriveFile["id"][]; public fileIds: MiDriveFile['id'][];
@Column("timestamp with time zone", { @Column('timestamp with time zone', {
comment: "The updated date of the Note.", comment: 'The updated date of the Note.',
}) })
public updatedAt: Date; public updatedAt: Date;
@Column("timestamp with time zone", { @Column('timestamp with time zone', {
comment: "The old date from before the edit", comment: 'The old date from before the edit',
}) })
public oldDate: Date; public oldDate: Date;
} }

View file

@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { langs } from '@/misc/langmap.js';
export const packedNoteSchema = { export const packedNoteSchema = {
type: 'object', type: 'object',
@ -26,6 +27,11 @@ export const packedNoteSchema = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
lang: {
type: 'string',
enum: langs,
nullable: true,
},
cw: { cw: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,

View file

@ -113,8 +113,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@bindThis @bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断 // ブロックしてたら中断
const host = this.utilityService.extractDbHost(uri);
const fetchedMeta = await this.metaService.fetch(); const fetchedMeta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null;
let local = await this.mergePack(me, ...await Promise.all([ let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri), this.apDbResolverService.getUserFromApId(uri),
@ -122,6 +123,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
])); ]));
if (local != null) return local; if (local != null) return local;
// local object, not found in db? fail
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ // リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri) as any; const object = await resolver.resolve(uri) as any;

View file

@ -382,7 +382,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.backgroundUrl = null; updates.backgroundUrl = null;
updates.backgroundBlurhash = null; updates.backgroundBlurhash = null;
} }
if (ps.avatarDecorations) { if (ps.avatarDecorations) {
const decorations = await this.avatarDecorationService.getAll(true); const decorations = await this.avatarDecorationService.getAll(true);
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);

View file

@ -20,6 +20,7 @@ import { isPureRenote } from '@/misc/is-pure-renote.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { langs } from '@/misc/langmap.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -148,6 +149,7 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: { visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
lang: { type: 'string', enum: langs, nullable: true, maxLength: 10 },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
localOnly: { type: 'boolean', default: false }, localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
@ -384,6 +386,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined, } : undefined,
text: ps.text ?? undefined, text: ps.text ?? undefined,
lang: ps.lang,
reply, reply,
renote, renote,
cw: ps.cw, cw: ps.cw,

View file

@ -13,6 +13,7 @@ import { NoteEditService } from '@/core/NoteEditService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isPureRenote } from '@/misc/is-pure-renote.js'; import { isPureRenote } from '@/misc/is-pure-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { langs } from '@/misc/langmap.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -197,6 +198,7 @@ export const paramDef = {
format: 'misskey:id', format: 'misskey:id',
}, },
}, },
lang: { type: 'string', enum: langs, nullable: true, maxLength: 10 },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
localOnly: { type: 'boolean', default: false }, localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
@ -436,6 +438,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined, } : undefined,
text: ps.text ?? undefined, text: ps.text ?? undefined,
lang: ps.lang,
reply, reply,
renote, renote,
cw: ps.cw, cw: ps.cw,

View file

@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;"> <div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw"> <p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :lang="appearNote.lang" :author="appearNote.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p> </p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
@ -65,6 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="appearNote.text" v-if="appearNote.text"
:parsedNodes="parsed" :parsedNodes="parsed"
:text="appearNote.text" :text="appearNote.text"
:lang="appearNote.lang"
:author="appearNote.user" :author="appearNote.user"
:nyaize="'respect'" :nyaize="'respect'"
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
@ -76,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :lang="nativeLang" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
@ -217,6 +218,7 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { miLocalStorage } from '@/local-storage.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -305,11 +307,12 @@ const renoteCollapsed = ref(
defaultStore.state.collapseRenotes && isRenote && ( defaultStore.state.collapseRenotes && isRenote && (
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
(appearNote.value.myReaction != null) (appearNote.value.myReaction != null)
) ),
); );
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const nativeLang = ref(miLocalStorage.getItem('lang') ?? window.navigator.language);
/* Overload FunctionLint /* Overload FunctionLint
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
@ -416,7 +419,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</header> </header>
<div :class="$style.noteContent"> <div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw"> <p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :lang="appearNote.lang" :author="appearNote.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/> <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p> </p>
<div v-show="appearNote.cw == null || showContent"> <div v-show="appearNote.cw == null || showContent">
@ -78,6 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="appearNote.text" v-if="appearNote.text"
:parsedNodes="parsed" :parsedNodes="parsed"
:text="appearNote.text" :text="appearNote.text"
:lang="appearNote.lang"
:author="appearNote.user" :author="appearNote.user"
:nyaize="'respect'" :nyaize="'respect'"
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
@ -90,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :lang="nativeLang" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
@ -258,6 +259,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -321,6 +323,7 @@ const replies = ref<Misskey.entities.Note[]>([]);
const quotes = ref<Misskey.entities.Note[]>([]); const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const nativeLang = ref(miLocalStorage.getItem('lang') ?? window.navigator.language);
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws; if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
@ -438,7 +441,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div :class="$style.content"> <div :class="$style.content">
<p v-if="note.cw != null" :class="$style.cw"> <p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :lang="note.lang" :author="note.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/> <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p> </p>
<div v-show="note.cw == null || showContent"> <div v-show="note.cw == null || showContent">
@ -281,7 +281,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -32,6 +32,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.headerRightButtonText">{{ channel.name }}</span> <span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button> </button>
</template> </template>
<button v-click-anime v-tooltip="i18n.ts.language" :class="['_button', $style.headerRightItem]" @click="setLanguage">
<span><i class="ph-translate ph-bold ph-lg"></i></span>
<span v-if="language" :class="$style.headerRightButtonText">{{ language }}</span>
</button>
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ph-rocket-launch ph-bold ph-lg"></i></span> <span v-if="!localOnly"><i class="ph-rocket-launch ph-bold ph-lg"></i></span>
<span v-else><i class="ph-rocket ph-bold ph-lg"></i></span> <span v-else><i class="ph-rocket ph-bold ph-lg"></i></span>
@ -65,16 +69,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<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" :lang="language ?? undefined" @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" :lang="language ?? undefined" data-cy-post-form-text @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">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkPollEditor v-if="poll" v-model="poll" :lang="language ?? undefined" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :lang="language ?? undefined" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;"> <div v-if="showingOptions" style="padding: 8px 16px;">
</div> </div>
<footer :class="$style.footer"> <footer :class="$style.footer">
@ -130,6 +134,8 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import { langmap, langs } from '@/scripts/langmap.js';
import { MenuItem } from '@/types/menu.js';
const $i = signinRequired(); const $i = signinRequired();
@ -144,6 +150,7 @@ const props = withDefaults(defineProps<{
initialText?: string; initialText?: string;
initialCw?: string; initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialLanguage?: (typeof Misskey.languages)[number];
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[]; initialVisibleUsers?: Misskey.entities.UserDetailed[];
@ -202,6 +209,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
const imeText = ref(''); const imeText = ref('');
const showingOptions = ref(false); const showingOptions = ref(false);
const textAreaReadOnly = ref(false); const textAreaReadOnly = ref(false);
const language = ref<string | null>(props.initialLanguage ?? defaultStore.state.recentlyUsedPostLanguages[0] ?? attemptNormalizeLang(localStorage.getItem('lang')));
const draftKey = computed((): string => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; let key = props.channel ? `channel:${props.channel.id}` : '';
@ -362,6 +370,7 @@ function watchForDraft() {
watch(files, () => saveDraft(), { deep: true }); watch(files, () => saveDraft(), { deep: true });
watch(visibility, () => saveDraft()); watch(visibility, () => saveDraft());
watch(localOnly, () => saveDraft()); watch(localOnly, () => saveDraft());
watch(language, () => saveDraft());
} }
function MFMWindow() { function MFMWindow() {
@ -536,6 +545,64 @@ async function toggleReactionAcceptance() {
reactionAcceptance.value = select.result; reactionAcceptance.value = select.result;
} }
function attemptNormalizeLang(lang: string | null) {
if (lang == null) return null;
if (!langs[lang]) lang = lang.split('-')[0];
return lang;
}
function setLanguage(ev: MouseEvent) {
const actions: Array<MenuItem> = [];
if (language.value != null) actions.push({
text: langmap[language.value].nativeName,
danger: false,
active: true,
action: () => {},
});
// Show recently used language first
let recentlyUsedLanguagesExist = false;
for (const lang of defaultStore.state.recentlyUsedPostLanguages) {
if (lang === language.value) continue;
if (!langs.includes(lang)) continue;
actions.push({
text: langmap[lang].nativeName,
danger: false,
active: false,
action: () => {
language.value = lang;
},
});
recentlyUsedLanguagesExist = true;
}
if (recentlyUsedLanguagesExist) actions.push({ type: 'divider' });
actions.push({
text: i18n.ts.noLanguage,
danger: false,
active: false,
action: () => {
language.value = null;
},
});
for (const lang of langs) {
if (lang === language.value) continue;
if (defaultStore.state.recentlyUsedPostLanguages.includes(lang)) continue;
actions.push({
text: langmap[lang].nativeName,
danger: false,
active: false,
action: () => {
language.value = lang;
},
});
}
os.popupMenu(actions, ev.currentTarget ?? ev.target);
}
function pushVisibleUser(user: Misskey.entities.UserDetailed) { function pushVisibleUser(user: Misskey.entities.UserDetailed) {
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.value.push(user); visibleUsers.value.push(user);
@ -677,6 +744,7 @@ function saveDraft() {
cw: cw.value, cw: cw.value,
visibility: visibility.value, visibility: visibility.value,
localOnly: localOnly.value, localOnly: localOnly.value,
lang: language.value,
files: files.value, files: files.value,
poll: poll.value, poll: poll.value,
}, },
@ -745,7 +813,7 @@ async function post(ev?: MouseEvent) {
visibility.value = 'home'; visibility.value = 'home';
} }
} }
if (defaultStore.state.warnMissingAltText) { if (defaultStore.state.warnMissingAltText) {
const filesData = toRaw(files.value); const filesData = toRaw(files.value);
@ -765,7 +833,7 @@ async function post(ev?: MouseEvent) {
}); });
if (canceled) return; if (canceled) return;
if (result === 'cancel') return; if (result === 'cancel') return;
} }
} }
@ -777,6 +845,7 @@ async function post(ev?: MouseEvent) {
channelId: props.channel ? props.channel.id : undefined, channelId: props.channel ? props.channel.id : undefined,
poll: poll.value, poll: poll.value,
cw: useCw.value ? cw.value ?? '' : null, cw: useCw.value ? cw.value ?? '' : null,
lang: language.value ?? null,
localOnly: localOnly.value, localOnly: localOnly.value,
visibility: visibility.value, visibility: visibility.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
@ -884,6 +953,17 @@ async function post(ev?: MouseEvent) {
text: err.message + '\n' + (err as any).id, text: err.message + '\n' + (err as any).id,
}); });
}); });
// update recentlyUsedLanguages
if (language.value != null) {
const maxLength = 6;
const filteredRecentlyUsed = defaultStore.state.recentlyUsedPostLanguages.filter((lang) => {
return (lang !== language.value && langs.includes(lang));
});
const recentlyUsedLangs = [language.value].concat(filteredRecentlyUsed).slice(0, maxLength);
defaultStore.set('recentlyUsedPostLanguages', recentlyUsedLangs);
}
} }
function cancel() { function cancel() {
@ -986,6 +1066,7 @@ onMounted(() => {
useCw.value = draft.data.useCw; useCw.value = draft.data.useCw;
visibility.value = draft.data.visibility; visibility.value = draft.data.visibility;
localOnly.value = draft.data.localOnly; localOnly.value = draft.data.localOnly;
language.value = draft.data.lang;
files.value = (draft.data.files || []).filter(draftFile => draftFile); files.value = (draft.data.files || []).filter(draftFile => draftFile);
if (draft.data.poll) { if (draft.data.poll) {
@ -1011,6 +1092,7 @@ onMounted(() => {
} }
visibility.value = init.visibility; visibility.value = init.visibility;
localOnly.value = init.localOnly ?? false; localOnly.value = init.localOnly ?? false;
language.value = init.lang;
quoteId.value = init.renote ? init.renote.id : null; quoteId.value = init.renote ? init.renote.id : null;
} }

View file

@ -24,6 +24,7 @@ const props = defineProps<{
initialText?: string; initialText?: string;
initialCw?: string; initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialLanguage?: (typeof Misskey.languages)[number];
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[]; initialVisibleUsers?: Misskey.entities.UserDetailed[];
@ -31,7 +32,7 @@ const props = defineProps<{
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
editId?: Misskey.entities.Note["id"]; editId?: Misskey.entities.Note['id'];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View file

@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/> <Mfm v-if="note.text" :text="note.text" :lang="note.lang" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
<MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="note.text && translating || note.text && translation" :class="$style.translation"> <div v-if="note.text && translating || note.text && translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else> <div v-else>
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <Mfm :text="translation.text" :lang="nativeLang" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
</div> </div>
</div> </div>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA>
@ -51,6 +51,7 @@ import { defaultStore } from '@/store.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js'; import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -72,6 +73,7 @@ function noteclick(id: string) {
const parsed = computed(() => props.note.text ? mfm.parse(props.note.text) : null); const parsed = computed(() => props.note.text ? mfm.parse(props.note.text) : null);
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
let allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); let allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const nativeLang = ref(miLocalStorage.getItem('lang') ?? window.navigator.language);
const isLong = defaultStore.state.expandLongNote && !props.hideFiles ? false : shouldCollapsed(props.note, []); const isLong = defaultStore.state.expandLongNote && !props.hideFiles ? false : shouldCollapsed(props.note, []);

View file

@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[{ [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined"> <div :class="[{ [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
<div style="container-type: inline-size;"> <div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw"> <p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :lang="appearNote.lang" :author="appearNote.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p> </p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
@ -67,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="appearNote.text" v-if="appearNote.text"
:parsedNodes="parsed" :parsedNodes="parsed"
:text="appearNote.text" :text="appearNote.text"
:lang="appearNote.lang"
:author="appearNote.user" :author="appearNote.user"
:nyaize="'respect'" :nyaize="'respect'"
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
@ -78,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :lang="nativeLang" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
@ -218,6 +219,7 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { miLocalStorage } from '@/local-storage.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -306,11 +308,12 @@ const renoteCollapsed = ref(
defaultStore.state.collapseRenotes && isRenote && ( defaultStore.state.collapseRenotes && isRenote && (
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
(appearNote.value.myReaction != null) (appearNote.value.myReaction != null)
) ),
); );
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const nativeLang = ref(miLocalStorage.getItem('lang') ?? window.navigator.language);
/* Overload FunctionLint /* Overload FunctionLint
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
@ -417,7 +420,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</header> </header>
<div :class="$style.noteContent"> <div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw"> <p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :lang="appearNote.lang" :author="appearNote.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/> <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p> </p>
<div v-show="appearNote.cw == null || showContent"> <div v-show="appearNote.cw == null || showContent">
@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="appearNote.text" v-if="appearNote.text"
:parsedNodes="parsed" :parsedNodes="parsed"
:text="appearNote.text" :text="appearNote.text"
:lang="appearNote.lang"
:author="appearNote.user" :author="appearNote.user"
:nyaize="'respect'" :nyaize="'respect'"
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
@ -98,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :lang="nativeLang" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
@ -266,6 +267,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -330,6 +332,7 @@ const replies = ref<Misskey.entities.Note[]>([]);
const quotes = ref<Misskey.entities.Note[]>([]); const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const nativeLang = ref(miLocalStorage.getItem('lang') ?? window.navigator.language);
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws; if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
@ -447,7 +450,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SkNoteHeader :class="$style.header" :note="note" :classic="true" :mini="true"/> <SkNoteHeader :class="$style.header" :note="note" :classic="true" :mini="true"/>
<div :class="$style.content"> <div :class="$style.content">
<p v-if="note.cw != null" :class="$style.cw"> <p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :lang="note.lang" :author="note.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/> <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p> </p>
<div v-show="note.cw == null || showContent"> <div v-show="note.cw == null || showContent">
@ -295,7 +295,7 @@ function boostVisibility() {
} }
} }
function renote(visibility: Visibility, localOnly: boolean = false) { function renote(visibility: Visibility, localOnly = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();

View file

@ -44,6 +44,7 @@ type MfmProps = {
enableEmojiMenu?: boolean; enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean; enableEmojiMenuReaction?: boolean;
isAnim?: boolean; isAnim?: boolean;
lang?: Misskey.entities.Note['lang'];
}; };
type MfmEvents = { type MfmEvents = {
@ -475,5 +476,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return h('span', { return 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;',
lang: props.lang ?? undefined,
}, genEl(rootAst, props.rootScale ?? 1)); }, genEl(rootAst, props.rootScale ?? 1));
} }

View file

@ -35,6 +35,9 @@ export const langmap = {
'ar-SA': { 'ar-SA': {
nativeName: 'العربية (السعودية)', nativeName: 'العربية (السعودية)',
}, },
'ay': {
nativeName: 'Aymar aru',
},
'ay-BO': { 'ay-BO': {
nativeName: 'Aymar aru', nativeName: 'Aymar aru',
}, },
@ -44,6 +47,9 @@ export const langmap = {
'az-AZ': { 'az-AZ': {
nativeName: 'Azərbaycan dili', nativeName: 'Azərbaycan dili',
}, },
'be': {
nativeName: 'Беларуская',
},
'be-BY': { 'be-BY': {
nativeName: 'Беларуская', nativeName: 'Беларуская',
}, },
@ -65,6 +71,9 @@ export const langmap = {
'br': { 'br': {
nativeName: 'Brezhoneg', nativeName: 'Brezhoneg',
}, },
'bs': {
nativeName: 'Bosanski',
},
'bs-BA': { 'bs-BA': {
nativeName: 'Bosanski', nativeName: 'Bosanski',
}, },
@ -77,7 +86,7 @@ export const langmap = {
'cak': { 'cak': {
nativeName: 'Maya Kaqchikel', nativeName: 'Maya Kaqchikel',
}, },
'ck-US': { 'chr': {
nativeName: 'ᏣᎳᎩ (tsalagi)', nativeName: 'ᏣᎳᎩ (tsalagi)',
}, },
'cs': { 'cs': {
@ -152,9 +161,6 @@ export const langmap = {
'en-ZA': { 'en-ZA': {
nativeName: 'English (South Africa)', nativeName: 'English (South Africa)',
}, },
'en@pirate': {
nativeName: 'English (Pirate)',
},
'eo': { 'eo': {
nativeName: 'Esperanto', nativeName: 'Esperanto',
}, },
@ -248,6 +254,9 @@ export const langmap = {
'fr-CH': { 'fr-CH': {
nativeName: 'Français (Suisse)', nativeName: 'Français (Suisse)',
}, },
'fy': {
nativeName: 'Frysk',
},
'fy-NL': { 'fy-NL': {
nativeName: 'Frysk', nativeName: 'Frysk',
}, },
@ -266,16 +275,22 @@ export const langmap = {
'gl-ES': { 'gl-ES': {
nativeName: 'Galego', nativeName: 'Galego',
}, },
'gn': {
nativeName: 'Avañe\'ẽ',
},
'gn-PY': { 'gn-PY': {
nativeName: 'Avañe\'ẽ', nativeName: 'Avañe\'ẽ',
}, },
'gu': {
nativeName: 'ગુજરાતી',
},
'gu-IN': { 'gu-IN': {
nativeName: 'ગુજરાતી', nativeName: 'ગુજરાતી',
}, },
'gv': { 'gv': {
nativeName: 'Gaelg', nativeName: 'Gaelg',
}, },
'gx-GR': { 'grc': {
nativeName: 'Ἑλληνική ἀρχαία', nativeName: 'Ἑλληνική ἀρχαία',
}, },
'he': { 'he': {
@ -338,12 +353,21 @@ export const langmap = {
'ja-JP': { 'ja-JP': {
nativeName: '日本語 (日本)', nativeName: '日本語 (日本)',
}, },
'jv': {
nativeName: 'Basa Jawa',
},
'jv-ID': { 'jv-ID': {
nativeName: 'Basa Jawa', nativeName: 'Basa Jawa',
}, },
'ka': {
nativeName: 'ქართული',
},
'ka-GE': { 'ka-GE': {
nativeName: 'ქართული', nativeName: 'ქართული',
}, },
'kk': {
nativeName: 'Қазақша',
},
'kk-KZ': { 'kk-KZ': {
nativeName: 'Қазақша', nativeName: 'Қазақша',
}, },
@ -371,6 +395,9 @@ export const langmap = {
'ko-KR': { 'ko-KR': {
nativeName: '한국어 (한국)', nativeName: '한국어 (한국)',
}, },
'ku': {
nativeName: 'Kurdî',
},
'ku-TR': { 'ku-TR': {
nativeName: 'Kurdî', nativeName: 'Kurdî',
}, },
@ -386,6 +413,9 @@ export const langmap = {
'lb': { 'lb': {
nativeName: 'Lëtzebuergesch', nativeName: 'Lëtzebuergesch',
}, },
'li': {
nativeName: 'Lèmbörgs',
},
'li-NL': { 'li-NL': {
nativeName: 'Lèmbörgs', nativeName: 'Lèmbörgs',
}, },
@ -404,6 +434,9 @@ export const langmap = {
'mai': { 'mai': {
nativeName: 'मैथिली, মৈথিলী', nativeName: 'मैथिली, মৈথিলী',
}, },
'mg': {
nativeName: 'Malagasy',
},
'mg-MG': { 'mg-MG': {
nativeName: 'Malagasy', nativeName: 'Malagasy',
}, },
@ -419,6 +452,9 @@ export const langmap = {
'ml-IN': { 'ml-IN': {
nativeName: 'മലയാളം', nativeName: 'മലയാളം',
}, },
'mn': {
nativeName: 'Монгол',
},
'mn-MN': { 'mn-MN': {
nativeName: 'Монгол', nativeName: 'Монгол',
}, },
@ -443,6 +479,9 @@ export const langmap = {
'my': { 'my': {
nativeName: 'ဗမာစကာ', nativeName: 'ဗမာစကာ',
}, },
'nan': {
nativeName: '閩南語',
},
'no': { 'no': {
nativeName: 'Norsk', nativeName: 'Norsk',
}, },
@ -467,12 +506,18 @@ export const langmap = {
'nl-NL': { 'nl-NL': {
nativeName: 'Nederlands (Nederland)', nativeName: 'Nederlands (Nederland)',
}, },
'nn': {
nativeName: 'Norsk (nynorsk)',
},
'nn-NO': { 'nn-NO': {
nativeName: 'Norsk (nynorsk)', nativeName: 'Norsk (nynorsk)',
}, },
'oc': { 'oc': {
nativeName: 'Occitan', nativeName: 'Occitan',
}, },
'or': {
nativeName: 'ଓଡ଼ିଆ',
},
'or-IN': { 'or-IN': {
nativeName: 'ଓଡ଼ିଆ', nativeName: 'ଓଡ଼ିଆ',
}, },
@ -488,6 +533,9 @@ export const langmap = {
'pl-PL': { 'pl-PL': {
nativeName: 'Polski', nativeName: 'Polski',
}, },
'ps': {
nativeName: 'پښتو',
},
'ps-AF': { 'ps-AF': {
nativeName: 'پښتو', nativeName: 'پښتو',
}, },
@ -500,9 +548,15 @@ export const langmap = {
'pt-PT': { 'pt-PT': {
nativeName: 'Português (Portugal)', nativeName: 'Português (Portugal)',
}, },
'qu': {
nativeName: 'Qhichwa',
},
'qu-PE': { 'qu-PE': {
nativeName: 'Qhichwa', nativeName: 'Qhichwa',
}, },
'rm': {
nativeName: 'Rumantsch',
},
'rm-CH': { 'rm-CH': {
nativeName: 'Rumantsch', nativeName: 'Rumantsch',
}, },
@ -518,15 +572,24 @@ export const langmap = {
'ru-RU': { 'ru-RU': {
nativeName: 'Русский', nativeName: 'Русский',
}, },
'sa': {
nativeName: 'संस्कृतम्',
},
'sa-IN': { 'sa-IN': {
nativeName: 'संस्कृतम्', nativeName: 'संस्कृतम्',
}, },
'se': {
nativeName: 'Davvisámegiella',
},
'se-NO': { 'se-NO': {
nativeName: 'Davvisámegiella', nativeName: 'Davvisámegiella',
}, },
'sh': { 'sh': {
nativeName: 'српскохрватски', nativeName: 'српскохрватски',
}, },
'si': {
nativeName: 'සිංහල',
},
'si-LK': { 'si-LK': {
nativeName: 'සිංහල', nativeName: 'සිංහල',
}, },
@ -542,6 +605,9 @@ export const langmap = {
'sl-SI': { 'sl-SI': {
nativeName: 'Slovenščina', nativeName: 'Slovenščina',
}, },
'so': {
nativeName: 'Soomaaliga',
},
'so-SO': { 'so-SO': {
nativeName: 'Soomaaliga', nativeName: 'Soomaaliga',
}, },
@ -602,12 +668,18 @@ export const langmap = {
'tlh': { 'tlh': {
nativeName: 'tlhIngan-Hol', nativeName: 'tlhIngan-Hol',
}, },
'tok': {
nativeName: 'Toki Pona',
},
'tr': { 'tr': {
nativeName: 'Türkçe', nativeName: 'Türkçe',
}, },
'tr-TR': { 'tr-TR': {
nativeName: 'Türkçe', nativeName: 'Türkçe',
}, },
'tt': {
nativeName: 'татарча',
},
'tt-RU': { 'tt-RU': {
nativeName: 'татарча', nativeName: 'татарча',
}, },
@ -635,6 +707,9 @@ export const langmap = {
'vi-VN': { 'vi-VN': {
nativeName: 'Tiếng Việt', nativeName: 'Tiếng Việt',
}, },
'xh': {
nativeName: 'isiXhosa',
},
'xh-ZA': { 'xh-ZA': {
nativeName: 'isiXhosa', nativeName: 'isiXhosa',
}, },
@ -644,6 +719,9 @@ export const langmap = {
'yi-DE': { 'yi-DE': {
nativeName: 'ייִדיש (German)', nativeName: 'ייִדיש (German)',
}, },
'yue': {
nativeName: '粵語',
},
'zh': { 'zh': {
nativeName: '中文', nativeName: '中文',
}, },
@ -665,7 +743,16 @@ export const langmap = {
'zh-TW': { 'zh-TW': {
nativeName: '中文(台灣)', nativeName: '中文(台灣)',
}, },
'zu': {
nativeName: 'isiZulu',
},
'zu-ZA': { 'zu-ZA': {
nativeName: 'isiZulu', nativeName: 'isiZulu',
}, },
}; };
export const langs: string[] = [
...(Object.keys(langmap).filter(tag => tag.indexOf('-') < 0)),
'zh-Hans',
'zh-Hant',
];

View file

@ -529,6 +529,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
}, },
recentlyUsedPostLanguages: {
where: 'account',
default: [] as string[],
},
})); }));
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期

View file

@ -2278,6 +2278,9 @@ type IWebhooksShowResponse = operations['i/webhooks/show']['responses']['200']['
// @public (undocumented) // @public (undocumented)
type IWebhooksUpdateRequest = operations['i/webhooks/update']['requestBody']['content']['application/json']; type IWebhooksUpdateRequest = operations['i/webhooks/update']['requestBody']['content']['application/json'];
// @public (undocumented)
export const languages: readonly ["ach", "ady", "af", "ak", "ar", "az", "bg", "bn", "br", "ca", "cak", "cs", "cy", "da", "de", "dsb", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fo", "fr", "ga", "gd", "gl", "gv", "he", "hi", "hr", "hsb", "ht", "hu", "hy", "id", "is", "it", "ja", "km", "kl", "kab", "kn", "ko", "kw", "la", "lb", "lt", "lv", "mai", "mk", "ml", "mr", "ms", "mt", "my", "no", "nb", "ne", "nl", "oc", "pa", "pl", "pt", "ro", "ru", "sh", "sk", "sl", "sq", "sr", "su", "sv", "sw", "ta", "te", "tg", "th", "fil", "tlh", "tr", "uk", "ur", "uz", "vi", "yi", "zh"];
// @public (undocumented) // @public (undocumented)
type MeDetailed = components['schemas']['MeDetailed']; type MeDetailed = components['schemas']['MeDetailed'];

View file

@ -4061,6 +4061,8 @@ export type components = {
/** Format: date-time */ /** Format: date-time */
deletedAt?: string | null; deletedAt?: string | null;
text: string | null; text: string | null;
/** @enum {string|null} */
lang: 'ach' | 'ady' | 'af' | 'ak' | 'ar' | 'ay' | 'az' | 'be' | 'bg' | 'bn' | 'br' | 'bs' | 'ca' | 'cak' | 'chr' | 'cs' | 'cy' | 'da' | 'de' | 'dsb' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fa' | 'ff' | 'fi' | 'fo' | 'fr' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'gv' | 'grc' | 'he' | 'hi' | 'hr' | 'hsb' | 'ht' | 'hu' | 'hy' | 'id' | 'is' | 'it' | 'ja' | 'jv' | 'ka' | 'kk' | 'km' | 'kl' | 'kab' | 'kn' | 'ko' | 'ku' | 'kw' | 'la' | 'lb' | 'li' | 'lt' | 'lv' | 'mai' | 'mg' | 'mk' | 'ml' | 'mn' | 'mr' | 'ms' | 'mt' | 'my' | 'nan' | 'no' | 'nb' | 'ne' | 'nl' | 'nn' | 'oc' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'qu' | 'rm' | 'ro' | 'ru' | 'sa' | 'se' | 'sh' | 'si' | 'sk' | 'sl' | 'so' | 'sq' | 'sr' | 'su' | 'sv' | 'sw' | 'ta' | 'te' | 'tg' | 'th' | 'fil' | 'tlh' | 'tok' | 'tr' | 'tt' | 'uk' | 'ur' | 'uz' | 'vi' | 'xh' | 'yi' | 'yue' | 'zh' | 'zu' | 'zh-Hans' | 'zh-Hant';
cw?: string | null; cw?: string | null;
/** Format: id */ /** Format: id */
userId: string; userId: string;
@ -19347,7 +19349,7 @@ export type operations = {
birthday?: string | null; birthday?: string | null;
listenbrainz?: string | null; listenbrainz?: string | null;
/** @enum {string|null} */ /** @enum {string|null} */
lang?: null | 'ach' | 'ady' | 'af' | 'af-NA' | 'af-ZA' | 'ak' | 'ar' | 'ar-AR' | 'ar-MA' | 'ar-SA' | 'ay-BO' | 'az' | 'az-AZ' | 'be-BY' | 'bg' | 'bg-BG' | 'bn' | 'bn-IN' | 'bn-BD' | 'br' | 'bs-BA' | 'ca' | 'ca-ES' | 'cak' | 'ck-US' | 'cs' | 'cs-CZ' | 'cy' | 'cy-GB' | 'da' | 'da-DK' | 'de' | 'de-AT' | 'de-DE' | 'de-CH' | 'dsb' | 'el' | 'el-GR' | 'en' | 'en-GB' | 'en-AU' | 'en-CA' | 'en-IE' | 'en-IN' | 'en-PI' | 'en-SG' | 'en-UD' | 'en-US' | 'en-ZA' | 'en@pirate' | 'eo' | 'eo-EO' | 'es' | 'es-AR' | 'es-419' | 'es-CL' | 'es-CO' | 'es-EC' | 'es-ES' | 'es-LA' | 'es-NI' | 'es-MX' | 'es-US' | 'es-VE' | 'et' | 'et-EE' | 'eu' | 'eu-ES' | 'fa' | 'fa-IR' | 'fb-LT' | 'ff' | 'fi' | 'fi-FI' | 'fo' | 'fo-FO' | 'fr' | 'fr-CA' | 'fr-FR' | 'fr-BE' | 'fr-CH' | 'fy-NL' | 'ga' | 'ga-IE' | 'gd' | 'gl' | 'gl-ES' | 'gn-PY' | 'gu-IN' | 'gv' | 'gx-GR' | 'he' | 'he-IL' | 'hi' | 'hi-IN' | 'hr' | 'hr-HR' | 'hsb' | 'ht' | 'hu' | 'hu-HU' | 'hy' | 'hy-AM' | 'id' | 'id-ID' | 'is' | 'is-IS' | 'it' | 'it-IT' | 'ja' | 'ja-JP' | 'jv-ID' | 'ka-GE' | 'kk-KZ' | 'km' | 'kl' | 'km-KH' | 'kab' | 'kn' | 'kn-IN' | 'ko' | 'ko-KR' | 'ku-TR' | 'kw' | 'la' | 'la-VA' | 'lb' | 'li-NL' | 'lt' | 'lt-LT' | 'lv' | 'lv-LV' | 'mai' | 'mg-MG' | 'mk' | 'mk-MK' | 'ml' | 'ml-IN' | 'mn-MN' | 'mr' | 'mr-IN' | 'ms' | 'ms-MY' | 'mt' | 'mt-MT' | 'my' | 'no' | 'nb' | 'nb-NO' | 'ne' | 'ne-NP' | 'nl' | 'nl-BE' | 'nl-NL' | 'nn-NO' | 'oc' | 'or-IN' | 'pa' | 'pa-IN' | 'pl' | 'pl-PL' | 'ps-AF' | 'pt' | 'pt-BR' | 'pt-PT' | 'qu-PE' | 'rm-CH' | 'ro' | 'ro-RO' | 'ru' | 'ru-RU' | 'sa-IN' | 'se-NO' | 'sh' | 'si-LK' | 'sk' | 'sk-SK' | 'sl' | 'sl-SI' | 'so-SO' | 'sq' | 'sq-AL' | 'sr' | 'sr-RS' | 'su' | 'sv' | 'sv-SE' | 'sw' | 'sw-KE' | 'ta' | 'ta-IN' | 'te' | 'te-IN' | 'tg' | 'tg-TJ' | 'th' | 'th-TH' | 'fil' | 'tlh' | 'tr' | 'tr-TR' | 'tt-RU' | 'uk' | 'uk-UA' | 'ur' | 'ur-PK' | 'uz' | 'uz-UZ' | 'vi' | 'vi-VN' | 'xh-ZA' | 'yi' | 'yi-DE' | 'zh' | 'zh-Hans' | 'zh-Hant' | 'zh-CN' | 'zh-HK' | 'zh-SG' | 'zh-TW' | 'zu-ZA'; lang?: null | 'ach' | 'ady' | 'af' | 'ak' | 'ar' | 'ay' | 'az' | 'be' | 'bg' | 'bn' | 'br' | 'bs' | 'ca' | 'cak' | 'chr' | 'cs' | 'cy' | 'da' | 'de' | 'dsb' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fa' | 'ff' | 'fi' | 'fo' | 'fr' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'gv' | 'grc' | 'he' | 'hi' | 'hr' | 'hsb' | 'ht' | 'hu' | 'hy' | 'id' | 'is' | 'it' | 'ja' | 'jv' | 'ka' | 'kk' | 'km' | 'kl' | 'kab' | 'kn' | 'ko' | 'ku' | 'kw' | 'la' | 'lb' | 'li' | 'lt' | 'lv' | 'mai' | 'mg' | 'mk' | 'ml' | 'mn' | 'mr' | 'ms' | 'mt' | 'my' | 'nan' | 'no' | 'nb' | 'ne' | 'nl' | 'nn' | 'oc' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'qu' | 'rm' | 'ro' | 'ru' | 'sa' | 'se' | 'sh' | 'si' | 'sk' | 'sl' | 'so' | 'sq' | 'sr' | 'su' | 'sv' | 'sw' | 'ta' | 'te' | 'tg' | 'th' | 'fil' | 'tlh' | 'tok' | 'tr' | 'tt' | 'uk' | 'ur' | 'uz' | 'vi' | 'xh' | 'yi' | 'yue' | 'zh' | 'zu' | 'zh-Hans' | 'zh-Hant';
/** Format: misskey:id */ /** Format: misskey:id */
avatarId?: string | null; avatarId?: string | null;
avatarDecorations?: ({ avatarDecorations?: ({
@ -21002,6 +21004,8 @@ export type operations = {
*/ */
visibility?: 'public' | 'home' | 'followers' | 'specified'; visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[]; visibleUserIds?: string[];
/** @enum {string|null} */
lang?: 'ach' | 'ady' | 'af' | 'ak' | 'ar' | 'ay' | 'az' | 'be' | 'bg' | 'bn' | 'br' | 'bs' | 'ca' | 'cak' | 'chr' | 'cs' | 'cy' | 'da' | 'de' | 'dsb' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fa' | 'ff' | 'fi' | 'fo' | 'fr' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'gv' | 'grc' | 'he' | 'hi' | 'hr' | 'hsb' | 'ht' | 'hu' | 'hy' | 'id' | 'is' | 'it' | 'ja' | 'jv' | 'ka' | 'kk' | 'km' | 'kl' | 'kab' | 'kn' | 'ko' | 'ku' | 'kw' | 'la' | 'lb' | 'li' | 'lt' | 'lv' | 'mai' | 'mg' | 'mk' | 'ml' | 'mn' | 'mr' | 'ms' | 'mt' | 'my' | 'nan' | 'no' | 'nb' | 'ne' | 'nl' | 'nn' | 'oc' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'qu' | 'rm' | 'ro' | 'ru' | 'sa' | 'se' | 'sh' | 'si' | 'sk' | 'sl' | 'so' | 'sq' | 'sr' | 'su' | 'sv' | 'sw' | 'ta' | 'te' | 'tg' | 'th' | 'fil' | 'tlh' | 'tok' | 'tr' | 'tt' | 'uk' | 'ur' | 'uz' | 'vi' | 'xh' | 'yi' | 'yue' | 'zh' | 'zu' | 'zh-Hans' | 'zh-Hant';
cw?: string | null; cw?: string | null;
/** @default false */ /** @default false */
localOnly?: boolean; localOnly?: boolean;
@ -22759,6 +22763,8 @@ export type operations = {
*/ */
visibility?: 'public' | 'home' | 'followers' | 'specified'; visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[]; visibleUserIds?: string[];
/** @enum {string|null} */
lang?: 'ach' | 'ady' | 'af' | 'ak' | 'ar' | 'ay' | 'az' | 'be' | 'bg' | 'bn' | 'br' | 'bs' | 'ca' | 'cak' | 'chr' | 'cs' | 'cy' | 'da' | 'de' | 'dsb' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fa' | 'ff' | 'fi' | 'fo' | 'fr' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'gv' | 'grc' | 'he' | 'hi' | 'hr' | 'hsb' | 'ht' | 'hu' | 'hy' | 'id' | 'is' | 'it' | 'ja' | 'jv' | 'ka' | 'kk' | 'km' | 'kl' | 'kab' | 'kn' | 'ko' | 'ku' | 'kw' | 'la' | 'lb' | 'li' | 'lt' | 'lv' | 'mai' | 'mg' | 'mk' | 'ml' | 'mn' | 'mr' | 'ms' | 'mt' | 'my' | 'nan' | 'no' | 'nb' | 'ne' | 'nl' | 'nn' | 'oc' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'qu' | 'rm' | 'ro' | 'ru' | 'sa' | 'se' | 'sh' | 'si' | 'sk' | 'sl' | 'so' | 'sq' | 'sr' | 'su' | 'sv' | 'sw' | 'ta' | 'te' | 'tg' | 'th' | 'fil' | 'tlh' | 'tok' | 'tr' | 'tt' | 'uk' | 'ur' | 'uz' | 'vi' | 'xh' | 'yi' | 'yue' | 'zh' | 'zu' | 'zh-Hans' | 'zh-Hant';
cw?: string | null; cw?: string | null;
/** @default false */ /** @default false */
localOnly?: boolean; localOnly?: boolean;

View file

@ -338,3 +338,96 @@ export type ModerationLogPayloads = {
fileId: string; fileId: string;
}; };
}; };
export const languages = [
'ach',
'ady',
'af',
'ak',
'ar',
'az',
'bg',
'bn',
'br',
'ca',
'cak',
'cs',
'cy',
'da',
'de',
'dsb',
'el',
'en',
'eo',
'es',
'et',
'eu',
'fa',
'ff',
'fi',
'fo',
'fr',
'ga',
'gd',
'gl',
'gv',
'he',
'hi',
'hr',
'hsb',
'ht',
'hu',
'hy',
'id',
'is',
'it',
'ja',
'km',
'kl',
'kab',
'kn',
'ko',
'kw',
'la',
'lb',
'lt',
'lv',
'mai',
'mk',
'ml',
'mr',
'ms',
'mt',
'my',
'no',
'nb',
'ne',
'nl',
'oc',
'pa',
'pl',
'pt',
'ro',
'ru',
'sh',
'sk',
'sl',
'sq',
'sr',
'su',
'sv',
'sw',
'ta',
'te',
'tg',
'th',
'fil',
'tlh',
'tr',
'uk',
'ur',
'uz',
'vi',
'yi',
'zh',
] as const;

View file

@ -19,6 +19,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
export const followingVisibilities = consts.followingVisibilities; export const followingVisibilities = consts.followingVisibilities;
export const followersVisibilities = consts.followersVisibilities; export const followersVisibilities = consts.followersVisibilities;
export const moderationLogTypes = consts.moderationLogTypes; export const moderationLogTypes = consts.moderationLogTypes;
export const languages = consts.languages;
// api extractor not supported yet // api extractor not supported yet
//export * as api from './api.js'; //export * as api from './api.js';