Compare commits

...

21 commits

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

Closes #253
2024-04-07 15:10:05 +00:00
dakkar
0690b9a429 merge: fix: load libopenmpt on demand (!469)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/469

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-04-07 14:56:16 +00:00
Alina Sireneva
ecfaf7ff7a chore: added license and patch info 2024-03-14 21:39:34 +03:00
Alina Sireneva
a69315a24b fix: added wasm in vite config 2024-03-14 14:41:24 +03:00
Alina Sireneva
d991eccd3f fix: Promise.resolve 2024-03-11 16:42:10 +03:00
Alina Sireneva
0085305579 fix: load libopenmpt on demand 2024-03-11 15:32:59 +03: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
45 changed files with 670 additions and 80 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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -43,7 +43,6 @@ html
link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css') link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css')
link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css') link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
script(src='/client-assets/libopenmpt.js')
if !config.clientManifestExists if !config.clientManifestExists
script(type="module" src="/vite/@vite/client") script(type="module" src="/vite/@vite/client")
@ -73,7 +72,6 @@ html
script. script.
var VERSION = "#{version}"; var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}"; var CLIENT_ENTRY = "#{clientEntry.file}";
window.libopenmpt = window.Module;
script(type='application/json' id='misskey_meta' data-generated-at=now) script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson != metaJson

File diff suppressed because one or more lines are too long

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

@ -1,9 +1,12 @@
/* global libopenmpt UTF8ToString writeAsciiToMemory */ // @ts-nocheck
/* eslint-disable */ /* eslint-disable */
const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext; const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext;
export function ChiptuneJsConfig (repeatCount: number, context: AudioContext) { let libopenmpt
let libopenmptLoadPromise
export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) {
this.repeatCount = repeatCount; this.repeatCount = repeatCount;
this.context = context; this.context = context;
} }
@ -20,6 +23,28 @@ export function ChiptuneJsPlayer (config: object) {
this.volume = 1; this.volume = 1;
} }
ChiptuneJsPlayer.prototype.initialize = function() {
if (libopenmptLoadPromise) return libopenmptLoadPromise;
if (libopenmpt) return Promise.resolve();
libopenmptLoadPromise = new Promise(async (resolve, reject) => {
try {
const { Module } = await import('./libopenmpt/libopenmpt.js');
await new Promise((resolve) => {
Module['onRuntimeInitialized'] = resolve;
})
libopenmpt = Module;
resolve()
} catch (e) {
reject(e)
} finally {
libopenmptLoadPromise = undefined;
}
})
return libopenmptLoadPromise;
}
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer; ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) { ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
@ -61,12 +86,12 @@ ChiptuneJsPlayer.prototype.seek = function (position: number) {
ChiptuneJsPlayer.prototype.metadata = function () { ChiptuneJsPlayer.prototype.metadata = function () {
const data = {}; const data = {};
const keys = UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';'); const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
let keyNameBuffer = 0; let keyNameBuffer = 0;
for (const key of keys) { for (const key of keys) {
keyNameBuffer = libopenmpt._malloc(key.length + 1); keyNameBuffer = libopenmpt._malloc(key.length + 1);
writeAsciiToMemory(key, keyNameBuffer); libopenmpt.writeAsciiToMemory(key, keyNameBuffer);
data[key] = UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer)); data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
libopenmpt._free(keyNameBuffer); libopenmpt._free(keyNameBuffer);
} }
return data; return data;
@ -84,7 +109,7 @@ ChiptuneJsPlayer.prototype.unlock = function () {
}; };
ChiptuneJsPlayer.prototype.load = function (input) { ChiptuneJsPlayer.prototype.load = function (input) {
return new Promise((resolve, reject) => { return this.initialize().then(() => new Promise((resolve, reject) => {
if(this.touchLocked) { if(this.touchLocked) {
this.unlock(); this.unlock();
} }
@ -106,7 +131,7 @@ ChiptuneJsPlayer.prototype.load = function (input) {
reject(error); reject(error);
}); });
} }
}); }));
}; };
ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) { ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) {
@ -180,7 +205,7 @@ ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) { ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) {
if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) {
return UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true)); return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
} }
return ''; return '';
}; };

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

@ -0,0 +1,25 @@
Copyright (c) 2004-2024, OpenMPT Project Developers and Contributors
Copyright (c) 1997-2003, Olivier Lapicque
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the OpenMPT project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,23 @@
modifications made to `libopenmpt.js` (can be taken from https://lib.openmpt.org/libopenmpt/download/):
at the beginning of the file:
```js
// @ts-nocheck
/* eslint-disable */
```
at the end of the file:
```js
Module.UTF8ToString = UTF8ToString;
Module.writeAsciiToMemory = writeAsciiToMemory;
export { Module }
```
replace
```
wasmBinaryFile="libopenmpt.wasm"
```
with
```
wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href
```

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

@ -8,7 +8,7 @@ import meta from '../../package.json';
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js'; import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js'; import pluginJson5 from './vite.json5.js';
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm'];
const hash = (str: string, seed = 0): number => { const hash = (str: string, seed = 0): number => {
let h1 = 0xdeadbeef ^ seed, let h1 = 0xdeadbeef ^ seed,

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';