Merge branch 'develop' into removed-note-metadata
This commit is contained in:
commit
28a8639f14
|
@ -50,6 +50,7 @@ jobs:
|
||||||
"packages/backend/test"
|
"packages/backend/test"
|
||||||
"packages/frontend-shared/@types"
|
"packages/frontend-shared/@types"
|
||||||
"packages/frontend-shared/js"
|
"packages/frontend-shared/js"
|
||||||
|
"packages/frontend-builder"
|
||||||
"packages/frontend/.storybook"
|
"packages/frontend/.storybook"
|
||||||
"packages/frontend/@types"
|
"packages/frontend/@types"
|
||||||
"packages/frontend/lib"
|
"packages/frontend/lib"
|
||||||
|
|
|
@ -9,6 +9,7 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/frontend-shared/**
|
- packages/frontend-shared/**
|
||||||
|
- packages/frontend-builder/**
|
||||||
- packages/frontend-embed/**
|
- packages/frontend-embed/**
|
||||||
- packages/icons-subsetter/**
|
- packages/icons-subsetter/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
|
@ -22,6 +23,7 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/frontend-shared/**
|
- packages/frontend-shared/**
|
||||||
|
- packages/frontend-builder/**
|
||||||
- packages/frontend-embed/**
|
- packages/frontend-embed/**
|
||||||
- packages/icons-subsetter/**
|
- packages/icons-subsetter/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
|
@ -56,6 +58,7 @@ jobs:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
- frontend-shared
|
- frontend-shared
|
||||||
|
- frontend-builder
|
||||||
- frontend-embed
|
- frontend-embed
|
||||||
- icons-subsetter
|
- icons-subsetter
|
||||||
- sw
|
- sw
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
- Fix: Unicode絵文字に隣接する異体字セレクタ(`U+FE0F`)が絵文字として認識される問題を修正
|
- Fix: Unicode絵文字に隣接する異体字セレクタ(`U+FE0F`)が絵文字として認識される問題を修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: AiScriptが1.0に更新されました
|
||||||
|
- プラグインは1.0に対応したものが必要です
|
||||||
|
- Playはそのまま動作しますが、新規に作られるプリセットは1.0になります
|
||||||
|
- 以前のバージョンから無効化されていた note_view_interruptor が有効になりました
|
||||||
- Feat: セーフモード
|
- Feat: セーフモード
|
||||||
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
|
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
|
||||||
- 以下の方法でセーフモードを起動できます
|
- 以下の方法でセーフモードを起動できます
|
||||||
|
@ -28,6 +32,8 @@
|
||||||
- Feat: ページのタブバーを下部に表示できるように
|
- Feat: ページのタブバーを下部に表示できるように
|
||||||
- Enhance: コントロールパネルを検索できるように
|
- Enhance: コントロールパネルを検索できるように
|
||||||
- Enhance: トルコ語 (tr-TR) に対応
|
- Enhance: トルコ語 (tr-TR) に対応
|
||||||
|
- Enhance: 言語別のスクリプトバンドルを生成するように
|
||||||
|
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
|
||||||
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
||||||
- Fix: テーマエディタが動作しない問題を修正
|
- Fix: テーマエディタが動作しない問題を修正
|
||||||
|
@ -35,6 +41,7 @@
|
||||||
### Server
|
### Server
|
||||||
- Enhance: ノートの削除処理の効率化
|
- Enhance: ノートの削除処理の効率化
|
||||||
- Enhance: 全体的なパフォーマンスの向上
|
- Enhance: 全体的なパフォーマンスの向上
|
||||||
|
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2025.7.0
|
## 2025.7.0
|
||||||
|
|
|
@ -618,3 +618,23 @@ color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||||
color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 考え方
|
||||||
|
### DRYに囚われるな
|
||||||
|
必要なのは一般化ではなく抽象化と考えます。
|
||||||
|
盲信せず、誤った・不必要な共通化は避け、それが自然だと感じる場合は重複させる勇気を持ちましょう。
|
||||||
|
|
||||||
|
### Misskeyを複雑にしない実装
|
||||||
|
それがいくら複雑であっても、Misskey固有のコンテキストと関心が分離されている(もしくは事実上分離されていると見做すことができる)実装であれば、それはMisskeyのコードベースに対する複雑性に影響を与えないと考えます。
|
||||||
|
|
||||||
|
例えるなら、VueやAiScriptといったMisskeyが使用しているライブラリの内部実装がいくら複雑だったとしても、「それを使用しているからMisskeyの実装は複雑である」ということにはならないのと同じです。
|
||||||
|
|
||||||
|
Misskeyのドメイン知識から関心が分離されているということは、Misskeyの実装について考える時にそれらの内部実装を考慮する必要が無く、認知負荷を増やさないからです。
|
||||||
|
|
||||||
|
また重要な点は、その実装が、Misskeyリポジトリの外部にあるか・内部にあるかということや、Misskeyがメンテナンスするものか・第三者がメンテナンスするものかといったことは複雑性を考える上ではほとんど無視できるという点です。
|
||||||
|
|
||||||
|
もちろんその実装がMisskeyリポジトリにあり、Misskeyがメンテナンスしなければならないものは、保守のコストはかかります。
|
||||||
|
しかし、Misskeyの本質的な設計・実装という観点で見たときは、その実装は実質的に外部ライブラリのように振る舞います。
|
||||||
|
換言すれば「たまたまMisskeyの開発者と同じ人たちがメンテナンスしているし、たまたまMisskeyのリポジトリ内に置いてあるだけの外部ライブラリ」です。
|
||||||
|
|
||||||
|
そのため、実装をなるべくMisskeyのドメイン知識から独立したものにすれば、Misskeyのコードベースの複雑性を上げることなく機能実装を行うことができ、お得であると言えます。
|
||||||
|
もちろんそれにこだわって、些細な実装でもそのように分離してしまうとかえって認知負荷が増えたり、実装量が増えてメリットをデメリットが上回る場合もあるので、ケースバイケースではあります。
|
||||||
|
|
|
@ -23,6 +23,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||||
COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
|
COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
|
||||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||||
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
|
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
|
||||||
|
COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"]
|
||||||
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
|
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
|
||||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||||
|
|
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració."
|
contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració."
|
||||||
showUrlPreview: "Mostrar vista prèvia d'URL"
|
showUrlPreview: "Mostrar vista prèvia d'URL"
|
||||||
showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt"
|
showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt"
|
||||||
|
showPageTabBarBottom: "Mostrar les pestanyes de les línies de temps a la part inferior"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostrar el nom del remitent"
|
showSenderName: "Mostrar el nom del remitent"
|
||||||
sendOnEnter: "Introdueix per enviar"
|
sendOnEnter: "Introdueix per enviar"
|
||||||
|
|
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración."
|
contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración."
|
||||||
showUrlPreview: "Mostrar la vista previa de la URL"
|
showUrlPreview: "Mostrar la vista previa de la URL"
|
||||||
showAvailableReactionsFirstInNote: "Mostrar las reacciones disponibles en la parte superior."
|
showAvailableReactionsFirstInNote: "Mostrar las reacciones disponibles en la parte superior."
|
||||||
|
showPageTabBarBottom: "Mostrar la barra de pestañas de la página en la parte inferior."
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostrar el nombre del remitente"
|
showSenderName: "Mostrar el nombre del remitente"
|
||||||
sendOnEnter: "Intro para enviar"
|
sendOnEnter: "Intro para enviar"
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function generateDTS() {
|
||||||
ts.NodeFlags.Const,
|
ts.NodeFlags.Const,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ts.factory.createInterfaceDeclaration(
|
ts.factory.createTypeAliasDeclaration(
|
||||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||||
ts.factory.createIdentifier('ParameterizedString'),
|
ts.factory.createIdentifier('ParameterizedString'),
|
||||||
[
|
[
|
||||||
|
@ -84,20 +84,22 @@ export default function generateDTS() {
|
||||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
undefined,
|
ts.factory.createIntersectionTypeNode([
|
||||||
[
|
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||||
ts.factory.createPropertySignature(
|
ts.factory.createTypeLiteralNode([
|
||||||
undefined,
|
ts.factory.createPropertySignature(
|
||||||
ts.factory.createComputedPropertyName(
|
|
||||||
ts.factory.createIdentifier('kParameters'),
|
|
||||||
),
|
|
||||||
undefined,
|
|
||||||
ts.factory.createTypeReferenceNode(
|
|
||||||
ts.factory.createIdentifier('T'),
|
|
||||||
undefined,
|
undefined,
|
||||||
|
ts.factory.createComputedPropertyName(
|
||||||
|
ts.factory.createIdentifier('kParameters'),
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
ts.factory.createTypeReferenceNode(
|
||||||
|
ts.factory.createIdentifier('T'),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
])
|
||||||
],
|
]),
|
||||||
),
|
),
|
||||||
ts.factory.createInterfaceDeclaration(
|
ts.factory.createInterfaceDeclaration(
|
||||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
// This file is generated by locales/generateDTS.js
|
// This file is generated by locales/generateDTS.js
|
||||||
// Do not edit this file directly.
|
// Do not edit this file directly.
|
||||||
declare const kParameters: unique symbol;
|
declare const kParameters: unique symbol;
|
||||||
export interface ParameterizedString<T extends string = string> {
|
export type ParameterizedString<T extends string = string> = string & {
|
||||||
[kParameters]: T;
|
[kParameters]: T;
|
||||||
}
|
};
|
||||||
export interface ILocale {
|
export interface ILocale {
|
||||||
[_: string]: string | ParameterizedString | ILocale;
|
[_: string]: string | ParameterizedString | ILocale;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1198,7 +1198,7 @@ replies: "Risposte"
|
||||||
renotes: "Rinota"
|
renotes: "Rinota"
|
||||||
loadReplies: "Leggi le risposte"
|
loadReplies: "Leggi le risposte"
|
||||||
loadConversation: "Leggi la conversazione"
|
loadConversation: "Leggi la conversazione"
|
||||||
pinnedList: "Elenco in primo piano"
|
pinnedList: "Lista in primo piano"
|
||||||
keepScreenOn: "Mantenere lo schermo acceso"
|
keepScreenOn: "Mantenere lo schermo acceso"
|
||||||
verifiedLink: "Abbiamo confermato la validità di questo collegamento"
|
verifiedLink: "Abbiamo confermato la validità di questo collegamento"
|
||||||
notifyNotes: "Notifica nuove Note"
|
notifyNotes: "Notifica nuove Note"
|
||||||
|
@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
|
||||||
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
|
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
|
||||||
inMinutes: "min"
|
inMinutes: "min"
|
||||||
inDays: "giorni"
|
inDays: "giorni"
|
||||||
|
safeModeEnabled: "La modalità sicura è attiva"
|
||||||
|
pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la modalità sicura è attiva."
|
||||||
|
customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva."
|
||||||
|
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente."
|
||||||
_order:
|
_order:
|
||||||
newest: "Prima i più recenti"
|
newest: "Prima i più recenti"
|
||||||
oldest: "Meno recenti prima"
|
oldest: "Meno recenti prima"
|
||||||
|
@ -1461,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
|
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
|
||||||
showUrlPreview: "Mostra anteprima dell'URL"
|
showUrlPreview: "Mostra anteprima dell'URL"
|
||||||
showAvailableReactionsFirstInNote: "Mostra le reazioni disponibili in alto"
|
showAvailableReactionsFirstInNote: "Mostra le reazioni disponibili in alto"
|
||||||
|
showPageTabBarBottom: "Visualizza le schede della pagina nella parte inferiore"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostra il nome del mittente"
|
showSenderName: "Mostra il nome del mittente"
|
||||||
sendOnEnter: "Invio spedisce"
|
sendOnEnter: "Invio spedisce"
|
||||||
|
@ -1634,6 +1639,10 @@ _serverSettings:
|
||||||
fanoutTimelineDbFallback: "Elaborazione dati alternativa"
|
fanoutTimelineDbFallback: "Elaborazione dati alternativa"
|
||||||
fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline."
|
fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline."
|
||||||
reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
|
reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
|
||||||
|
remoteNotesCleaning: "Pulizia automatica dei contenuti remoti"
|
||||||
|
remoteNotesCleaning_description: "Se abilitata, verranno periodicamente rimosse le vecchie Note remote senza relazioni, per ridurre il sovraccarico del sistema."
|
||||||
|
remoteNotesCleaningMaxProcessingDuration: "Durata massima del processo di pulizia"
|
||||||
|
remoteNotesCleaningExpiryDaysForEachNotes: "Periodo minimo di conservazione delle note"
|
||||||
inquiryUrl: "URL di contatto"
|
inquiryUrl: "URL di contatto"
|
||||||
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
|
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
|
||||||
openRegistration: "Registrazioni aperte"
|
openRegistration: "Registrazioni aperte"
|
||||||
|
@ -1652,6 +1661,8 @@ _serverSettings:
|
||||||
userGeneratedContentsVisibilityForVisitor: "Visibilità dei contenuti generati dagli utenti ai non utenti"
|
userGeneratedContentsVisibilityForVisitor: "Visibilità dei contenuti generati dagli utenti ai non utenti"
|
||||||
userGeneratedContentsVisibilityForVisitor_description: "Questa funzionalità è utile per impedire che contenuti remoti inappropriati e difficili da moderare vengano inavvertitamente resi pubblici su Internet tramite il proprio server."
|
userGeneratedContentsVisibilityForVisitor_description: "Questa funzionalità è utile per impedire che contenuti remoti inappropriati e difficili da moderare vengano inavvertitamente resi pubblici su Internet tramite il proprio server."
|
||||||
userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server."
|
userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server."
|
||||||
|
restartServerSetupWizardConfirm_title: "Vuoi ripetere la procedura guidata di configurazione iniziale del server?"
|
||||||
|
restartServerSetupWizardConfirm_text: "Verranno ripristinate alcune tue impostazioni personalizzate."
|
||||||
_userGeneratedContentsVisibilityForVisitor:
|
_userGeneratedContentsVisibilityForVisitor:
|
||||||
all: "Tutto pubblico"
|
all: "Tutto pubblico"
|
||||||
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
|
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
|
||||||
|
@ -3062,6 +3073,7 @@ _bootErrors:
|
||||||
otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache"
|
otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache"
|
||||||
otherOption2: "Avviare il client predefinito"
|
otherOption2: "Avviare il client predefinito"
|
||||||
otherOption3: "Avviare lo strumento di riparazione"
|
otherOption3: "Avviare lo strumento di riparazione"
|
||||||
|
otherOption4: "Avvia Misskey in modalità sicura"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Tutte"
|
searchScopeAll: "Tutte"
|
||||||
searchScopeLocal: "Locale"
|
searchScopeLocal: "Locale"
|
||||||
|
@ -3098,6 +3110,8 @@ _serverSetupWizard:
|
||||||
doYouConnectToFediverse_description1: "Collegandosi a una rete di server distribuiti, denominata Fediverso, potrai scambiare contenuti con altri server, tramite il protocollo di comunicazione ActivityPub."
|
doYouConnectToFediverse_description1: "Collegandosi a una rete di server distribuiti, denominata Fediverso, potrai scambiare contenuti con altri server, tramite il protocollo di comunicazione ActivityPub."
|
||||||
doYouConnectToFediverse_description2: "Connettersi al Fediverso è anche detto \"federazione\"."
|
doYouConnectToFediverse_description2: "Connettersi al Fediverso è anche detto \"federazione\"."
|
||||||
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
|
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
|
||||||
|
remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo"
|
||||||
|
remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione."
|
||||||
adminInfo: "Informazioni sull'amministratore"
|
adminInfo: "Informazioni sull'amministratore"
|
||||||
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
|
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
|
||||||
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."
|
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."
|
||||||
|
|
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
|
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
|
||||||
showUrlPreview: "URL 미리보기 표시"
|
showUrlPreview: "URL 미리보기 표시"
|
||||||
showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시"
|
showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시"
|
||||||
|
showPageTabBarBottom: "페이지의 탭 바를 아래쪽에 표시"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "발신자 이름 표시"
|
showSenderName: "발신자 이름 표시"
|
||||||
sendOnEnter: "엔터로 보내기"
|
sendOnEnter: "엔터로 보내기"
|
||||||
|
|
|
@ -1461,6 +1461,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "เมื่อโหมดเรียลไทม์เปิดอยู่ เนื้อหาจะอัปเดตแบบเรียลไทม์โดยไม่ขึ้นกับการตั้งค่านี้"
|
contentsUpdateFrequency_description2: "เมื่อโหมดเรียลไทม์เปิดอยู่ เนื้อหาจะอัปเดตแบบเรียลไทม์โดยไม่ขึ้นกับการตั้งค่านี้"
|
||||||
showUrlPreview: "แสดงตัวอย่าง URL"
|
showUrlPreview: "แสดงตัวอย่าง URL"
|
||||||
showAvailableReactionsFirstInNote: "แสดงรีแอคชั่นที่ใช้ได้ไว้หน้าสุด"
|
showAvailableReactionsFirstInNote: "แสดงรีแอคชั่นที่ใช้ได้ไว้หน้าสุด"
|
||||||
|
showPageTabBarBottom: "แสดงแท็บบาร์ของเพจที่ด้านล่าง"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "แสดงชื่อผู้ส่ง"
|
showSenderName: "แสดงชื่อผู้ส่ง"
|
||||||
sendOnEnter: "กด Enter เพื่อส่ง"
|
sendOnEnter: "กด Enter เพื่อส่ง"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
_lang_: "Türkçe"
|
_lang_: "Türkçe"
|
||||||
headlineMisskey: "Notlarla birbirine bağlanan bir ağ"
|
headlineMisskey: "Notlarla birbirine bağlı bir ağ"
|
||||||
introMisskey: "Hoş geldiniz! Misskey, açık kaynaklı, merkezi olmayan bir mikroblog hizmetidir.\nDüşüncelerinizi çevrenizdeki herkesle paylaşmak için “notlar” oluşturun. 📡\n“Tepkiler” ile herkesin notları hakkındaki duygularınızı hızlıca ifade edebilirsiniz. 👍\nYeni bir dünyayı keşfedelim! 🚀"
|
introMisskey: "Hoş geldiniz! Misskey, açık kaynaklı, merkezi olmayan bir mikroblog hizmetidir.\nDüşüncelerinizi çevrenizdeki herkesle paylaşmak için “notlar” oluşturun. 📡\n“Tepkiler” ile herkesin notları hakkındaki duygularınızı hızlıca ifade edebilirsiniz. 👍\nYeni bir dünyayı keşfedelim! 🚀"
|
||||||
poweredByMisskeyDescription: "{name}, açık kaynak platformu <b>Misskey</b> (kısaca “Misskey örneği” olarak anılır) tarafından desteklenen hizmetlerden biridir."
|
poweredByMisskeyDescription: "{name}, açık kaynak platformu <b>Misskey</b> (kısaca “Misskey örneği” olarak anılır) tarafından desteklenen hizmetlerden biridir."
|
||||||
monthAndDay: "{month}/{day}"
|
monthAndDay: "{month}/{day}"
|
||||||
|
@ -23,7 +23,7 @@ renotedBy: "{user} tarafından renot edildi"
|
||||||
noNotes: "Not yok"
|
noNotes: "Not yok"
|
||||||
noNotifications: "Bildirim yok"
|
noNotifications: "Bildirim yok"
|
||||||
instance: "Sunucu"
|
instance: "Sunucu"
|
||||||
settings: "Ayarlarlar"
|
settings: "Ayarlar"
|
||||||
notificationSettings: "Bildirim Ayarları"
|
notificationSettings: "Bildirim Ayarları"
|
||||||
basicSettings: "Temel Ayarlar"
|
basicSettings: "Temel Ayarlar"
|
||||||
otherSettings: "Diğer Ayarlar"
|
otherSettings: "Diğer Ayarlar"
|
||||||
|
@ -237,7 +237,7 @@ clearQueueConfirmText: "Kuyrukta kalan teslim edilmemiş notlar birleştirilmeye
|
||||||
clearCachedFiles: "Clear cache"
|
clearCachedFiles: "Clear cache"
|
||||||
clearCachedFilesConfirm: "Tüm önbelleğe alınmış uzak dosyaları silmek istediğinizden emin misiniz?"
|
clearCachedFilesConfirm: "Tüm önbelleğe alınmış uzak dosyaları silmek istediğinizden emin misiniz?"
|
||||||
blockedInstances: "Engellenen Sunucu"
|
blockedInstances: "Engellenen Sunucu"
|
||||||
blockedInstancesDescription: "Engellemek istediğiniz örneklerin ana bilgisayar adlarını satır sonlarıyla ayırarak listeleyin. Listelenen örnekler artık bu örnekle iletişim kuramayacaktır."
|
blockedInstancesDescription: "Engellemek istediğiniz sunucuların ana bilgisayar adlarını satır sonlarıyla ayırarak listeleyin. Listelenen örnekler artık bu örnekle iletişim kuramayacaktır."
|
||||||
silencedInstances: "Susturulmuş sunucular"
|
silencedInstances: "Susturulmuş sunucular"
|
||||||
silencedInstancesDescription: "Sessize almak istediğiniz sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak listeleyin. Listelenen sunuculara ait tüm hesaplar sessize alınmış olarak kabul edilecek ve yalnızca takip isteklerinde bulunabilecek, takip edilmedikleri takdirde yerel hesapları etiketleyemeyeceklerdir. Bu, engellenen sunucuları etkilemeyecektir."
|
silencedInstancesDescription: "Sessize almak istediğiniz sunucuların ana bilgisayar adlarını yeni bir satırla ayırarak listeleyin. Listelenen sunuculara ait tüm hesaplar sessize alınmış olarak kabul edilecek ve yalnızca takip isteklerinde bulunabilecek, takip edilmedikleri takdirde yerel hesapları etiketleyemeyeceklerdir. Bu, engellenen sunucuları etkilemeyecektir."
|
||||||
mediaSilencedInstances: "Medya susturulmuş sunucular"
|
mediaSilencedInstances: "Medya susturulmuş sunucular"
|
||||||
|
@ -311,7 +311,7 @@ basicNotesBeforeCreateAccount: "Önemli notlar"
|
||||||
termsOfService: "Hizmet Şartları"
|
termsOfService: "Hizmet Şartları"
|
||||||
start: "Başla"
|
start: "Başla"
|
||||||
home: "Ana sayfa"
|
home: "Ana sayfa"
|
||||||
remoteUserCaution: "Bu kullanıcı uzak bir örnekten geldiği için, gösterilen bilgiler eksik olabilir."
|
remoteUserCaution: "Bu kullanıcı uzak bir sunucudan geldiği için, gösterilen bilgiler eksik olabilir."
|
||||||
activity: "Etkinlik"
|
activity: "Etkinlik"
|
||||||
images: "Görseller"
|
images: "Görseller"
|
||||||
image: "Görsel"
|
image: "Görsel"
|
||||||
|
@ -499,8 +499,8 @@ signinOrContinueOnRemote: "Devam etmek için sunucunuzu taşıyın veya bu sunuc
|
||||||
invitations: "Davetler"
|
invitations: "Davetler"
|
||||||
invitationCode: "Davet kodu"
|
invitationCode: "Davet kodu"
|
||||||
checking: "Kontrol ediliyor..."
|
checking: "Kontrol ediliyor..."
|
||||||
available: "Mevcut"
|
available: "Kullanılabilir"
|
||||||
unavailable: "Mevcut değil"
|
unavailable: "Kullanılamaz"
|
||||||
usernameInvalidFormat: "Büyük ve küçük harfler, rakamlar ve alt çizgi kullanabilirsiniz. (a~z、A~Z、0~9)"
|
usernameInvalidFormat: "Büyük ve küçük harfler, rakamlar ve alt çizgi kullanabilirsiniz. (a~z、A~Z、0~9)"
|
||||||
tooShort: "Çok kısa"
|
tooShort: "Çok kısa"
|
||||||
tooLong: "Çok uzun"
|
tooLong: "Çok uzun"
|
||||||
|
@ -619,7 +619,7 @@ unsetUserBannerConfirm: "Banner'ı kaldırmak istediğinizden emin misiniz?"
|
||||||
deleteAllFiles: "Tüm dosyaları sil"
|
deleteAllFiles: "Tüm dosyaları sil"
|
||||||
deleteAllFilesConfirm: "Tüm dosyaları silmek istediğinizden emin misiniz?"
|
deleteAllFilesConfirm: "Tüm dosyaları silmek istediğinizden emin misiniz?"
|
||||||
removeAllFollowing: "Takip ettiğiniz tüm kullanıcıları takipten çıkarın"
|
removeAllFollowing: "Takip ettiğiniz tüm kullanıcıları takipten çıkarın"
|
||||||
removeAllFollowingDescription: "Bu komutu çalıştırmak, {host} adresindeki tüm hesapları takipten çıkarır. Örneğin, örnek artık mevcut değilse bu komutu çalıştırın."
|
removeAllFollowingDescription: "Bu komutu çalıştırmak, {host} adresindeki tüm hesapları takipten çıkarır. Örneğin, sunucu artık mevcut değilse bu komutu çalıştırın."
|
||||||
userSuspended: "Bu kullanıcı askıya alınmıştır."
|
userSuspended: "Bu kullanıcı askıya alınmıştır."
|
||||||
userSilenced: "Bu kullanıcı susturuluyor."
|
userSilenced: "Bu kullanıcı susturuluyor."
|
||||||
yourAccountSuspendedTitle: "Bu hesap askıya alınmıştır."
|
yourAccountSuspendedTitle: "Bu hesap askıya alınmıştır."
|
||||||
|
@ -1066,19 +1066,19 @@ collapseRenotesDescription: "Daha önce tepki verdiğiniz veya yeniden not aldı
|
||||||
internalServerError: "İç Sunucu Hatası"
|
internalServerError: "İç Sunucu Hatası"
|
||||||
internalServerErrorDescription: "Sunucu beklenmedik bir hatayla karşılaştı."
|
internalServerErrorDescription: "Sunucu beklenmedik bir hatayla karşılaştı."
|
||||||
copyErrorInfo: "Hata ayrıntılarını kopyala"
|
copyErrorInfo: "Hata ayrıntılarını kopyala"
|
||||||
joinThisServer: "Bu örnekte kaydolun"
|
joinThisServer: "Bu sunucuda kaydolun"
|
||||||
exploreOtherServers: "Başka bir örnek arayın"
|
exploreOtherServers: "Başka bir sunucu arayın"
|
||||||
letsLookAtTimeline: "Timeline'a bir göz atın"
|
letsLookAtTimeline: "Timeline'a bir göz atın"
|
||||||
disableFederationConfirm: "Federasyonu gerçekten devre dışı bırakmak mı?"
|
disableFederationConfirm: "Federasyonu gerçekten devre dışı bırakmak mı?"
|
||||||
disableFederationConfirmWarn: "Federasyondan ayrılsa bile, aksi belirtilmedikçe gönderiler herkese açık olmaya devam edecektir. Genellikle bunu yapmanız gerekmez."
|
disableFederationConfirmWarn: "Federasyondan ayrılsa bile, aksi belirtilmedikçe gönderiler herkese açık olmaya devam edecektir. Genellikle bunu yapmanız gerekmez."
|
||||||
disableFederationOk: "Devre Dışı"
|
disableFederationOk: "Devre Dışı"
|
||||||
invitationRequiredToRegister: "Bu etkinlik davetle katılımlıdır. Geçerli bir davet kodu girerek kaydolmanız gerekir."
|
invitationRequiredToRegister: "Bu etkinlik davetle katılımlıdır. Geçerli bir davet kodu girerek kaydolmanız gerekir."
|
||||||
emailNotSupported: "Bu örnek, E-Posta göndermeyi desteklemiyor."
|
emailNotSupported: "Bu sunucu, E-Posta göndermeyi desteklemiyor."
|
||||||
postToTheChannel: "Kanalına gönder"
|
postToTheChannel: "Kanalına gönder"
|
||||||
cannotBeChangedLater: "Bu daha sonra değiştirilemez."
|
cannotBeChangedLater: "Bu daha sonra değiştirilemez."
|
||||||
reactionAcceptance: "Tepki Kabulü"
|
reactionAcceptance: "Tepki Kabulü"
|
||||||
likeOnly: "Sadece beğeniler"
|
likeOnly: "Sadece beğeniler"
|
||||||
likeOnlyForRemote: "Tüm (Yalnızca uzak örnekler için beğeniler)"
|
likeOnlyForRemote: "Tüm (Yalnızca uzak sunucu için beğeniler)"
|
||||||
nonSensitiveOnly: "Hassas olmayanlar için"
|
nonSensitiveOnly: "Hassas olmayanlar için"
|
||||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Yalnızca hassas olmayanlar (Yalnızca uzaktan beğeniler)"
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Yalnızca hassas olmayanlar (Yalnızca uzaktan beğeniler)"
|
||||||
rolesAssignedToMe: "Bana atanan roller"
|
rolesAssignedToMe: "Bana atanan roller"
|
||||||
|
@ -1100,7 +1100,7 @@ retryAllQueuesNow: "Tüm kuyrukları yeniden çalıştırmayı deneyin"
|
||||||
retryAllQueuesConfirmTitle: "Gerçekten hepsini tekrar denemek istiyor musunuz?"
|
retryAllQueuesConfirmTitle: "Gerçekten hepsini tekrar denemek istiyor musunuz?"
|
||||||
retryAllQueuesConfirmText: "Bu, sunucu yükünü geçici olarak artıracaktır."
|
retryAllQueuesConfirmText: "Bu, sunucu yükünü geçici olarak artıracaktır."
|
||||||
enableChartsForRemoteUser: "Uzak kullanıcı veri grafikleri oluşturun"
|
enableChartsForRemoteUser: "Uzak kullanıcı veri grafikleri oluşturun"
|
||||||
enableChartsForFederatedInstances: "Uzak örnek veri grafikleri oluşturun"
|
enableChartsForFederatedInstances: "Uzak sunucu veri grafikleri oluşturun"
|
||||||
enableStatsForFederatedInstances: "Uzak sunucu istatistiklerini alın"
|
enableStatsForFederatedInstances: "Uzak sunucu istatistiklerini alın"
|
||||||
showClipButtonInNoteFooter: "Not eylem menüsüne “Klip” ekle"
|
showClipButtonInNoteFooter: "Not eylem menüsüne “Klip” ekle"
|
||||||
reactionsDisplaySize: "Tepki ekran boyutu"
|
reactionsDisplaySize: "Tepki ekran boyutu"
|
||||||
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "Gerçek zamanlı mod açık olduğunda, bu ayardan bağımsız olarak içerik gerçek zamanlı olarak güncellenir."
|
contentsUpdateFrequency_description2: "Gerçek zamanlı mod açık olduğunda, bu ayardan bağımsız olarak içerik gerçek zamanlı olarak güncellenir."
|
||||||
showUrlPreview: "URL önizlemesini göster"
|
showUrlPreview: "URL önizlemesini göster"
|
||||||
showAvailableReactionsFirstInNote: "Mevcut tepkileri en üstte göster."
|
showAvailableReactionsFirstInNote: "Mevcut tepkileri en üstte göster."
|
||||||
|
showPageTabBarBottom: "Sayfa sekme çubuğunu aşağıda göster"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Gönderenin adını göster"
|
showSenderName: "Gönderenin adını göster"
|
||||||
sendOnEnter: "Enter tuşuna basarak gönderin"
|
sendOnEnter: "Enter tuşuna basarak gönderin"
|
||||||
|
@ -1911,7 +1912,7 @@ _achievements:
|
||||||
_loggedInOnNewYearsDay:
|
_loggedInOnNewYearsDay:
|
||||||
title: "Yeni yılınız kutlu olsun!"
|
title: "Yeni yılınız kutlu olsun!"
|
||||||
description: "Yılın ilk gününde oturum açıldı"
|
description: "Yılın ilk gününde oturum açıldı"
|
||||||
flavor: "Bu örnekte bir başka harika yıla"
|
flavor: "Bu sunucuda bir başka harika yıla"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
title: "Çerezleri tıklayarak oynanan bir oyun"
|
title: "Çerezleri tıklayarak oynanan bir oyun"
|
||||||
description: "Çerezi tıkladı"
|
description: "Çerezi tıkladı"
|
||||||
|
@ -1976,7 +1977,7 @@ _role:
|
||||||
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
|
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
|
||||||
canPublicNote: "Halka açık notlar gönderebilir"
|
canPublicNote: "Halka açık notlar gönderebilir"
|
||||||
mentionMax: "Bir notta maksimum bahsetme sayısı"
|
mentionMax: "Bir notta maksimum bahsetme sayısı"
|
||||||
canInvite: "Örnek davet kodları oluşturabilir"
|
canInvite: "Sunucu davet kodları oluşturabilir"
|
||||||
inviteLimit: "Davet sınırı"
|
inviteLimit: "Davet sınırı"
|
||||||
inviteLimitCycle: "Davet sınırı bekleme süresi"
|
inviteLimitCycle: "Davet sınırı bekleme süresi"
|
||||||
inviteExpirationTime: "Davet süresi dolma aralığı"
|
inviteExpirationTime: "Davet süresi dolma aralığı"
|
||||||
|
@ -2072,7 +2073,7 @@ _ad:
|
||||||
adsTooClose: "Mevcut reklam aralığı çok düşük olduğu için kullanıcı deneyimini önemli ölçüde kötüleştirebilir."
|
adsTooClose: "Mevcut reklam aralığı çok düşük olduğu için kullanıcı deneyimini önemli ölçüde kötüleştirebilir."
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "Kayıt olurken kullandığınız E-Posta adresini girin. Şifrenizi sıfırlayabileceğiniz bir bağlantı bu adrese gönderilecektir."
|
enterEmail: "Kayıt olurken kullandığınız E-Posta adresini girin. Şifrenizi sıfırlayabileceğiniz bir bağlantı bu adrese gönderilecektir."
|
||||||
ifNoEmail: "Kayıt sırasında E-Posta kullanmadıysanız, lütfen bunun yerine örnek yöneticisiyle iletişime geçin."
|
ifNoEmail: "Kayıt sırasında E-Posta kullanmadıysanız, lütfen bunun yerine sunucu yöneticisiyle iletişime geçin."
|
||||||
contactAdmin: "This instance does not support using email addresses, please contact the instance administrator to reset your password instead."
|
contactAdmin: "This instance does not support using email addresses, please contact the instance administrator to reset your password instead."
|
||||||
_gallery:
|
_gallery:
|
||||||
my: "Benim Galerim"
|
my: "Benim Galerim"
|
||||||
|
|
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
|
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
|
||||||
showUrlPreview: "显示 URL 预览"
|
showUrlPreview: "显示 URL 预览"
|
||||||
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
|
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
|
||||||
|
showPageTabBarBottom: "在下方显示页面标签栏"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "显示发送者的名字"
|
showSenderName: "显示发送者的名字"
|
||||||
sendOnEnter: "回车键发送"
|
sendOnEnter: "回车键发送"
|
||||||
|
|
|
@ -1373,7 +1373,7 @@ inDays: "日"
|
||||||
safeModeEnabled: "啟用安全模式"
|
safeModeEnabled: "啟用安全模式"
|
||||||
pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。"
|
pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。"
|
||||||
customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。"
|
customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。"
|
||||||
themeIsDefaultBecauseSafeMode: "啟用安全模式時將使用預設主題,關閉安全模式時將恢復預設主題。"
|
themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。"
|
||||||
_order:
|
_order:
|
||||||
newest: "最新的在前"
|
newest: "最新的在前"
|
||||||
oldest: "最舊的在前"
|
oldest: "最舊的在前"
|
||||||
|
@ -1465,6 +1465,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
|
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
|
||||||
showUrlPreview: "顯示網址預覽"
|
showUrlPreview: "顯示網址預覽"
|
||||||
showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部"
|
showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部"
|
||||||
|
showPageTabBarBottom: "在底部顯示頁面的標籤列"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "顯示發送者的名稱"
|
showSenderName: "顯示發送者的名稱"
|
||||||
sendOnEnter: "按下 Enter 發送訊息"
|
sendOnEnter: "按下 Enter 發送訊息"
|
||||||
|
@ -1549,7 +1550,7 @@ _initialAccountSetting:
|
||||||
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
|
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
|
||||||
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
|
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
|
||||||
followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。"
|
followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。"
|
||||||
pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自{name}的通知了。"
|
pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自 {name} 的通知了。"
|
||||||
initialAccountSettingCompleted: "初始設定完成了!"
|
initialAccountSettingCompleted: "初始設定完成了!"
|
||||||
haveFun: "盡情享受{name}吧!"
|
haveFun: "盡情享受{name}吧!"
|
||||||
youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。"
|
youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.8.0-alpha.4",
|
"version": "2025.8.0-alpha.6",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -42,7 +42,7 @@ async function killProc() {
|
||||||
'./node_modules/nodemon/bin/nodemon.js',
|
'./node_modules/nodemon/bin/nodemon.js',
|
||||||
[
|
[
|
||||||
'-w', 'src',
|
'-w', 'src',
|
||||||
'-e', 'ts,js,mjs,cjs,json',
|
'-e', 'ts,js,mjs,cjs,json,pug',
|
||||||
'--exec', 'pnpm', 'run', 'build',
|
'--exec', 'pnpm', 'run', 'build',
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
|
@ -184,9 +184,9 @@ export type Config = {
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
driveUrl: string;
|
driveUrl: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
frontendEntry: string;
|
frontendEntry: { file: string | null };
|
||||||
frontendManifestExists: boolean;
|
frontendManifestExists: boolean;
|
||||||
frontendEmbedEntry: string;
|
frontendEmbedEntry: { file: string | null };
|
||||||
frontendEmbedManifestExists: boolean;
|
frontendEmbedManifestExists: boolean;
|
||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
externalMediaProxyEnabled: boolean;
|
externalMediaProxyEnabled: boolean;
|
||||||
|
@ -235,10 +235,10 @@ export function loadConfig(): Config {
|
||||||
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
||||||
const frontendManifest = frontendManifestExists ?
|
const frontendManifest = frontendManifestExists ?
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
||||||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
: { 'src/_boot_.ts': { file: null } };
|
||||||
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||||
: { 'src/boot.ts': { file: 'src/boot.ts' } };
|
: { 'src/boot.ts': { file: null } };
|
||||||
|
|
||||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,7 @@
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import type { MiMeta, MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import type { MiMeta, NotesRepository } from '@/models/_.js';
|
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -55,6 +54,69 @@ export class CleanRemoteNotesProcessorService {
|
||||||
|
|
||||||
const MAX_NOTE_COUNT_PER_QUERY = 50;
|
const MAX_NOTE_COUNT_PER_QUERY = 50;
|
||||||
|
|
||||||
|
//#retion queries
|
||||||
|
// We use string literals instead of query builder for several reasons:
|
||||||
|
// - for removeCondition, we need to use it in having clause, which is not supported by Brackets.
|
||||||
|
// - for recursive part, we need to preserve the order of columns, but typeorm query builder does not guarantee the order of columns in the result query
|
||||||
|
|
||||||
|
// The condition for removing the notes.
|
||||||
|
// The note must be:
|
||||||
|
// - old enough (older than the newestLimit)
|
||||||
|
// - a remote note (userHost is not null).
|
||||||
|
// - not have clipped
|
||||||
|
// - not have pinned on the user profile
|
||||||
|
// - not has been favorite by any user
|
||||||
|
const removeCondition = 'note.id < :newestLimit'
|
||||||
|
+ ' AND note."clippedCount" = 0'
|
||||||
|
+ ' AND note."userHost" IS NOT NULL'
|
||||||
|
// using both userId and noteId instead of just noteId to use index on user_note_pining table.
|
||||||
|
// This is safe because notes are only pinned by the user who created them.
|
||||||
|
+ ' AND NOT EXISTS(SELECT 1 FROM "user_note_pining" WHERE "noteId" = note."id" AND "userId" = note."userId")'
|
||||||
|
// We cannot use userId trick because users can favorite notes from other users.
|
||||||
|
+ ' AND NOT EXISTS(SELECT 1 FROM "note_favorite" WHERE "noteId" = note."id")'
|
||||||
|
;
|
||||||
|
|
||||||
|
// The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes
|
||||||
|
const initiatorQuery = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.select('note.id', 'id')
|
||||||
|
.where(removeCondition)
|
||||||
|
.andWhere('note.id > :cursor')
|
||||||
|
.orderBy('note.id', 'ASC')
|
||||||
|
.limit(MAX_NOTE_COUNT_PER_QUERY);
|
||||||
|
|
||||||
|
// The union query queries the related notes and replies related to the initiator query
|
||||||
|
const unionQuery = `
|
||||||
|
SELECT "note"."id", "note"."replyId", "note"."renoteId", rn."initiatorId"
|
||||||
|
FROM "note" "note"
|
||||||
|
INNER JOIN "related_notes" "rn"
|
||||||
|
ON "note"."replyId" = rn.id
|
||||||
|
OR "note"."renoteId" = rn.id
|
||||||
|
OR "note"."id" = rn."replyId"
|
||||||
|
OR "note"."id" = rn."renoteId"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const selectRelatedNotesFromInitiatorIdsQuery = `
|
||||||
|
SELECT "note"."id" AS "id", "note"."replyId" AS "replyId", "note"."renoteId" AS "renoteId", "note"."id" AS "initiatorId"
|
||||||
|
FROM "note" "note" WHERE "note"."id" IN (:...initiatorIds)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const recursiveQuery = `(${selectRelatedNotesFromInitiatorIdsQuery}) UNION (${unionQuery})`;
|
||||||
|
|
||||||
|
const removableInitiatorNotesQuery = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.select('rn."initiatorId"')
|
||||||
|
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
|
||||||
|
.groupBy('rn."initiatorId"')
|
||||||
|
.having(`bool_and(${removeCondition})`);
|
||||||
|
|
||||||
|
const notesQuery = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.addCommonTableExpression(recursiveQuery, 'related_notes', { recursive: true })
|
||||||
|
.select('note.id', 'id')
|
||||||
|
.addSelect('rn."initiatorId"')
|
||||||
|
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
|
||||||
|
.where(`rn."initiatorId" IN (${removableInitiatorNotesQuery.getQuery()})`)
|
||||||
|
.distinctOn(['note.id']);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
deletedCount: 0,
|
deletedCount: 0,
|
||||||
oldest: null as number | null,
|
oldest: null as number | null,
|
||||||
|
@ -68,77 +130,45 @@ export class CleanRemoteNotesProcessorService {
|
||||||
let cursor = '0'; // oldest note ID to start from
|
let cursor = '0'; // oldest note ID to start from
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
//#region check time
|
||||||
const batchBeginAt = Date.now();
|
const batchBeginAt = Date.now();
|
||||||
|
|
||||||
// We use string literals instead of query builder for several reasons:
|
const elapsed = batchBeginAt - startAt;
|
||||||
// - for removeCondition, we need to use it in having clause, which is not supported by Brackets.
|
|
||||||
// - for recursive part, we need to preserve the order of columns, but typeorm query builder does not guarantee the order of columns in the result query
|
|
||||||
|
|
||||||
// The condition for removing the notes.
|
if (elapsed >= maxDuration) {
|
||||||
// The note must be:
|
this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`);
|
||||||
// - old enough (older than the newestLimit)
|
job.log('Reached maximum duration, stopping cleaning.');
|
||||||
// - a remote note (userHost is not null).
|
job.updateProgress(100);
|
||||||
// - not have clipped
|
break;
|
||||||
// - not have pinned on the user profile
|
}
|
||||||
// - not has been favorite by any user
|
|
||||||
const removeCondition = 'note.id < :newestLimit'
|
|
||||||
+ ' AND note."clippedCount" = 0'
|
|
||||||
+ ' AND note."userHost" IS NOT NULL'
|
|
||||||
// using both userId and noteId instead of just noteId to use index on user_note_pining table.
|
|
||||||
// This is safe because notes are only pinned by the user who created them.
|
|
||||||
+ ' AND NOT EXISTS(SELECT 1 FROM "user_note_pining" WHERE "noteId" = note."id" AND "userId" = note."userId")'
|
|
||||||
// We cannot use userId trick because users can favorite notes from other users.
|
|
||||||
+ ' AND NOT EXISTS(SELECT 1 FROM "note_favorite" WHERE "noteId" = note."id")'
|
|
||||||
;
|
|
||||||
|
|
||||||
// The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes
|
job.updateProgress((elapsed / maxDuration) * 100);
|
||||||
const initiatorQuery = `
|
//#endregion
|
||||||
SELECT "note"."id" AS "id", "note"."replyId" AS "replyId", "note"."renoteId" AS "renoteId", "note"."id" AS "initiatorId"
|
|
||||||
FROM "note" "note" WHERE ${removeCondition} AND "note"."id" > :cursor ORDER BY "note"."id" ASC LIMIT ${MAX_NOTE_COUNT_PER_QUERY}`;
|
|
||||||
|
|
||||||
// The union query queries the related notes and replies related to the initiator query
|
// First, we fetch the initiator notes that are older than the newestLimit.
|
||||||
const unionQuery = `
|
const initiatorNotes: { id: MiNote['id'] }[] = await initiatorQuery.setParameters({ cursor, newestLimit }).getRawMany();
|
||||||
SELECT "note"."id", "note"."replyId", "note"."renoteId", rn."initiatorId"
|
|
||||||
FROM "note" "note"
|
|
||||||
INNER JOIN "related_notes" "rn"
|
|
||||||
ON "note"."replyId" = rn.id
|
|
||||||
OR "note"."renoteId" = rn.id
|
|
||||||
OR "note"."id" = rn."replyId"
|
|
||||||
OR "note"."id" = rn."renoteId"
|
|
||||||
`;
|
|
||||||
const recursiveQuery = `(${initiatorQuery}) UNION (${unionQuery})`;
|
|
||||||
|
|
||||||
const removableInitiatorNotesQuery = this.notesRepository.createQueryBuilder('note')
|
|
||||||
.select('rn."initiatorId"')
|
|
||||||
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
|
|
||||||
.groupBy('rn."initiatorId"')
|
|
||||||
.having(`bool_and(${removeCondition})`);
|
|
||||||
|
|
||||||
const notesQuery = this.notesRepository.createQueryBuilder('note')
|
|
||||||
.addCommonTableExpression(recursiveQuery, 'related_notes', { recursive: true })
|
|
||||||
.select('note.id', 'id')
|
|
||||||
.addSelect('rn."initiatorId"')
|
|
||||||
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
|
|
||||||
.where(`rn."initiatorId" IN (${ removableInitiatorNotesQuery.getQuery() })`)
|
|
||||||
.setParameters({ cursor, newestLimit });
|
|
||||||
|
|
||||||
const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.getRawMany();
|
|
||||||
|
|
||||||
const fetchedCount = notes.length;
|
|
||||||
|
|
||||||
// update the cursor to the newest initiatorId found in the fetched notes.
|
// update the cursor to the newest initiatorId found in the fetched notes.
|
||||||
// We don't use 'id' since the note can be newer than the initiator note.
|
const newCursor = initiatorNotes.reduce((max, note) => note.id > max ? note.id : max, cursor);
|
||||||
for (const note of notes) {
|
|
||||||
if (cursor < note.initiatorId) {
|
if (initiatorNotes.length === 0 || cursor === newCursor || newCursor >= newestLimit) {
|
||||||
cursor = note.initiatorId;
|
// If no notes were found or the cursor did not change, we can stop.
|
||||||
}
|
job.log('No more notes to clean. (no initiator notes found or cursor did not change.)');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.setParameters({
|
||||||
|
initiatorIds: initiatorNotes.map(note => note.id),
|
||||||
|
newestLimit,
|
||||||
|
}).getRawMany();
|
||||||
|
|
||||||
|
cursor = newCursor;
|
||||||
|
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
await this.notesRepository.delete(notes.map(note => note.id));
|
await this.notesRepository.delete(notes.map(note => note.id));
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const { id } of notes) {
|
||||||
const t = this.idService.parse(note.id).date.getTime();
|
const t = this.idService.parse(id).date.getTime();
|
||||||
if (stats.oldest === null || t < stats.oldest) {
|
if (stats.oldest === null || t < stats.oldest) {
|
||||||
stats.oldest = t;
|
stats.oldest = t;
|
||||||
}
|
}
|
||||||
|
@ -150,19 +180,14 @@ export class CleanRemoteNotesProcessorService {
|
||||||
stats.deletedCount += notes.length;
|
stats.deletedCount += notes.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
job.log(`Deleted ${notes.length} of ${fetchedCount}; ${Date.now() - batchBeginAt}ms`);
|
job.log(`Deleted ${notes.length} from ${initiatorNotes.length} initiators; ${Date.now() - batchBeginAt}ms`);
|
||||||
|
|
||||||
const elapsed = Date.now() - startAt;
|
if (initiatorNotes.length < MAX_NOTE_COUNT_PER_QUERY) {
|
||||||
|
// If we fetched less than the maximum, it means there are no more notes to process.
|
||||||
if (elapsed >= maxDuration) {
|
job.log(`No more notes to clean. (fewer than MAX_NOTE_COUNT_PER_QUERY =${MAX_NOTE_COUNT_PER_QUERY}.)`);
|
||||||
this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`);
|
|
||||||
job.log('Reached maximum duration, stopping cleaning.');
|
|
||||||
job.updateProgress(100);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
job.updateProgress((elapsed / maxDuration) * 100);
|
|
||||||
|
|
||||||
await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db
|
await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,8 +48,8 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
|
||||||
maxLength: 1024,
|
maxLength: 1024,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: [
|
required: [
|
||||||
|
@ -57,7 +57,6 @@ export const paramDef = {
|
||||||
'name',
|
'name',
|
||||||
'on',
|
'on',
|
||||||
'url',
|
'url',
|
||||||
'secret',
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -52,8 +52,8 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
|
||||||
maxLength: 1024,
|
maxLength: 1024,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: [
|
required: [
|
||||||
|
@ -62,7 +62,6 @@ export const paramDef = {
|
||||||
'name',
|
'name',
|
||||||
'on',
|
'on',
|
||||||
'url',
|
'url',
|
||||||
'secret',
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -32,61 +32,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
if (!localStorage.hasOwnProperty('locale')) {
|
const supportedLangs = LANGS;
|
||||||
const supportedLangs = LANGS;
|
/** @type { string } */
|
||||||
let lang = localStorage.getItem('lang');
|
let lang = localStorage.getItem('lang');
|
||||||
if (lang == null || !supportedLangs.includes(lang)) {
|
if (lang == null || !supportedLangs.includes(lang)) {
|
||||||
if (supportedLangs.includes(navigator.language)) {
|
if (supportedLangs.includes(navigator.language)) {
|
||||||
lang = navigator.language;
|
lang = navigator.language;
|
||||||
} else {
|
|
||||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
if (lang == null) lang = 'en-US';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaRes = await window.fetch('/api/meta', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
credentials: 'omit',
|
|
||||||
cache: 'no-cache',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (metaRes.status !== 200) {
|
|
||||||
renderError('META_FETCH');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const meta = await metaRes.json();
|
|
||||||
const v = meta.version;
|
|
||||||
if (v == null) {
|
|
||||||
renderError('META_FETCH_V');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for https://github.com/misskey-dev/misskey/issues/10202
|
|
||||||
if (lang == null || lang.toString == null || lang.toString() === 'null') {
|
|
||||||
console.error('invalid lang value detected!!!', typeof lang, lang);
|
|
||||||
lang = 'en-US';
|
|
||||||
}
|
|
||||||
|
|
||||||
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
|
||||||
if (localRes.status === 200) {
|
|
||||||
localStorage.setItem('lang', lang);
|
|
||||||
localStorage.setItem('locale', await localRes.text());
|
|
||||||
localStorage.setItem('localeVersion', v);
|
|
||||||
} else {
|
} else {
|
||||||
renderError('LOCALE_FETCH');
|
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||||
return;
|
|
||||||
|
// Fallback
|
||||||
|
if (lang == null) lang = 'en-US';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for https://github.com/misskey-dev/misskey/issues/10202
|
||||||
|
if (lang == null || lang.toString == null || lang.toString() === 'null') {
|
||||||
|
console.error('invalid lang value detected!!!', typeof lang, lang);
|
||||||
|
lang = 'en-US';
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
async function importAppScript() {
|
async function importAppScript() {
|
||||||
await import(`/embed_vite/${CLIENT_ENTRY}`)
|
await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/_boot_.ts')
|
||||||
.catch(async e => {
|
.catch(async e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
renderError('APP_IMPORT');
|
renderError('APP_IMPORT');
|
||||||
|
@ -115,10 +84,26 @@
|
||||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
let messages = null;
|
||||||
|
const bootloaderLocales = localStorage.getItem('bootloaderLocales');
|
||||||
|
if (bootloaderLocales) {
|
||||||
|
messages = JSON.parse(bootloaderLocales);
|
||||||
|
}
|
||||||
|
if (!messages) {
|
||||||
|
// older version of misskey does not store bootloaderLocales, stores locale as a whole
|
||||||
|
const legacyLocale = localStorage.getItem('locale');
|
||||||
|
if (legacyLocale) {
|
||||||
|
const parsed = JSON.parse(legacyLocale);
|
||||||
|
messages = {
|
||||||
|
...(parsed._bootErrors ?? {}),
|
||||||
|
reload: parsed.reload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!messages) messages = {};
|
||||||
|
|
||||||
const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
|
const title = messages?.title || 'Failed to initialize Misskey';
|
||||||
const reload = locale?.reload || 'Reload';
|
const reload = messages?.reload || 'Reload';
|
||||||
|
|
||||||
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
|
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
|
||||||
<div class="message">${title}</div>
|
<div class="message">${title}</div>
|
||||||
|
|
|
@ -22,62 +22,31 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language
|
||||||
if (!localStorage.hasOwnProperty('locale')) {
|
const supportedLangs = LANGS;
|
||||||
const supportedLangs = LANGS;
|
/** @type { string } */
|
||||||
let lang = localStorage.getItem('lang');
|
let lang = localStorage.getItem('lang');
|
||||||
if (lang == null || !supportedLangs.includes(lang)) {
|
if (lang == null || !supportedLangs.includes(lang)) {
|
||||||
if (supportedLangs.includes(navigator.language)) {
|
if (supportedLangs.includes(navigator.language)) {
|
||||||
lang = navigator.language;
|
lang = navigator.language;
|
||||||
} else {
|
|
||||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
if (lang == null) lang = 'en-US';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaRes = await window.fetch('/api/meta', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
credentials: 'omit',
|
|
||||||
cache: 'no-cache',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (metaRes.status !== 200) {
|
|
||||||
renderError('META_FETCH');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const meta = await metaRes.json();
|
|
||||||
const v = meta.version;
|
|
||||||
if (v == null) {
|
|
||||||
renderError('META_FETCH_V');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for https://github.com/misskey-dev/misskey/issues/10202
|
|
||||||
if (lang == null || lang.toString == null || lang.toString() === 'null') {
|
|
||||||
console.error('invalid lang value detected!!!', typeof lang, lang);
|
|
||||||
lang = 'en-US';
|
|
||||||
}
|
|
||||||
|
|
||||||
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
|
||||||
if (localRes.status === 200) {
|
|
||||||
localStorage.setItem('lang', lang);
|
|
||||||
localStorage.setItem('locale', await localRes.text());
|
|
||||||
localStorage.setItem('localeVersion', v);
|
|
||||||
} else {
|
} else {
|
||||||
renderError('LOCALE_FETCH');
|
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||||
return;
|
|
||||||
|
// Fallback
|
||||||
|
if (lang == null) lang = 'en-US';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for https://github.com/misskey-dev/misskey/issues/10202
|
||||||
|
if (lang == null || lang.toString == null || lang.toString() === 'null') {
|
||||||
|
console.error('invalid lang value detected!!!', typeof lang, lang);
|
||||||
|
lang = 'en-US';
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Script
|
//#region Script
|
||||||
async function importAppScript() {
|
async function importAppScript() {
|
||||||
await import(`/vite/${CLIENT_ENTRY}`)
|
await import(CLIENT_ENTRY ? `/vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/vite/src/_boot_.ts')
|
||||||
.catch(async e => {
|
.catch(async e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
renderError('APP_IMPORT', e);
|
renderError('APP_IMPORT', e);
|
||||||
|
@ -162,9 +131,25 @@
|
||||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
let messages = null;
|
||||||
|
const bootloaderLocales = localStorage.getItem('bootloaderLocales');
|
||||||
|
if (bootloaderLocales) {
|
||||||
|
messages = JSON.parse(bootloaderLocales);
|
||||||
|
}
|
||||||
|
if (!messages) {
|
||||||
|
// older version of misskey does not store bootloaderLocales, stores locale as a whole
|
||||||
|
const legacyLocale = localStorage.getItem('locale');
|
||||||
|
if (legacyLocale) {
|
||||||
|
const parsed = JSON.parse(legacyLocale);
|
||||||
|
messages = {
|
||||||
|
...(parsed._bootErrors ?? {}),
|
||||||
|
reload: parsed.reload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!messages) messages = {};
|
||||||
|
|
||||||
const messages = Object.assign({
|
messages = Object.assign({
|
||||||
title: 'Failed to initialize Misskey',
|
title: 'Failed to initialize Misskey',
|
||||||
solution: 'The following actions may solve the problem.',
|
solution: 'The following actions may solve the problem.',
|
||||||
solution1: 'Update your os and browser',
|
solution1: 'Update your os and browser',
|
||||||
|
@ -176,8 +161,8 @@
|
||||||
otherOption2: 'Start the simple client',
|
otherOption2: 'Start the simple client',
|
||||||
otherOption3: 'Start the repair tool',
|
otherOption3: 'Start the repair tool',
|
||||||
otherOption4: 'Start Misskey in safe mode',
|
otherOption4: 'Start Misskey in safe mode',
|
||||||
}, locale?._bootErrors || {});
|
reload: 'Reload',
|
||||||
const reload = locale?.reload || 'Reload';
|
}, messages);
|
||||||
|
|
||||||
const safeModeUrl = new URL(window.location.href);
|
const safeModeUrl = new URL(window.location.href);
|
||||||
safeModeUrl.searchParams.set('safemode', 'true');
|
safeModeUrl.searchParams.set('safemode', 'true');
|
||||||
|
@ -193,7 +178,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<h1>${messages.title}</h1>
|
<h1>${messages.title}</h1>
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
<button class="button-big" onclick="location.reload(true);">
|
||||||
<span class="button-label-big">${reload}</span>
|
<span class="button-label-big">${messages?.reload}</span>
|
||||||
</button>
|
</button>
|
||||||
<p><b>${messages.solution}</b></p>
|
<p><b>${messages.solution}</b></p>
|
||||||
<p>${messages.solution1}</p>
|
<p>${messages.solution1}</p>
|
||||||
|
|
|
@ -19,7 +19,6 @@ html(class='embed')
|
||||||
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
|
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
|
||||||
link(rel='icon' href= icon || '/favicon.ico')
|
link(rel='icon' href= icon || '/favicon.ico')
|
||||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||||
link(rel='modulepreload' href=`/embed_vite/${entry.file}`)
|
|
||||||
|
|
||||||
if !config.frontendEmbedManifestExists
|
if !config.frontendEmbedManifestExists
|
||||||
script(type="module" src="/embed_vite/@vite/client")
|
script(type="module" src="/embed_vite/@vite/client")
|
||||||
|
@ -40,7 +39,7 @@ html(class='embed')
|
||||||
|
|
||||||
script.
|
script.
|
||||||
var VERSION = "#{version}";
|
var VERSION = "#{version}";
|
||||||
var CLIENT_ENTRY = "#{entry.file}";
|
var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
|
||||||
|
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
|
|
@ -37,7 +37,6 @@ html
|
||||||
link(rel='prefetch' href=serverErrorImageUrl)
|
link(rel='prefetch' href=serverErrorImageUrl)
|
||||||
link(rel='prefetch' href=infoImageUrl)
|
link(rel='prefetch' href=infoImageUrl)
|
||||||
link(rel='prefetch' href=notFoundImageUrl)
|
link(rel='prefetch' href=notFoundImageUrl)
|
||||||
link(rel='modulepreload' href=`/vite/${entry.file}`)
|
|
||||||
|
|
||||||
if !config.frontendManifestExists
|
if !config.frontendManifestExists
|
||||||
script(type="module" src="/vite/@vite/client")
|
script(type="module" src="/vite/@vite/client")
|
||||||
|
@ -69,7 +68,7 @@ html
|
||||||
|
|
||||||
script.
|
script.
|
||||||
var VERSION = "#{version}";
|
var VERSION = "#{version}";
|
||||||
var CLIENT_ENTRY = "#{entry.file}";
|
var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
|
||||||
|
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
|
|
@ -0,0 +1,631 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import ms from 'ms';
|
||||||
|
import {
|
||||||
|
type MiNote,
|
||||||
|
type MiUser,
|
||||||
|
type NotesRepository,
|
||||||
|
type NoteFavoritesRepository,
|
||||||
|
type UserNotePiningsRepository,
|
||||||
|
type UsersRepository,
|
||||||
|
type UserProfilesRepository,
|
||||||
|
MiMeta,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { CleanRemoteNotesProcessorService } from '@/queue/processors/CleanRemoteNotesProcessorService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
|
||||||
|
describe('CleanRemoteNotesProcessorService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: CleanRemoteNotesProcessorService;
|
||||||
|
let idService: IdService;
|
||||||
|
let notesRepository: NotesRepository;
|
||||||
|
let noteFavoritesRepository: NoteFavoritesRepository;
|
||||||
|
let userNotePiningsRepository: UserNotePiningsRepository;
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
|
||||||
|
// Local user
|
||||||
|
let alice: MiUser;
|
||||||
|
// Remote user 1
|
||||||
|
let bob: MiUser;
|
||||||
|
// Remote user 2
|
||||||
|
let carol: MiUser;
|
||||||
|
|
||||||
|
const meta = new MiMeta();
|
||||||
|
|
||||||
|
// Mock job object
|
||||||
|
const createMockJob = () => ({
|
||||||
|
log: jest.fn(),
|
||||||
|
updateProgress: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const id = idService.gen();
|
||||||
|
const un = data.username || secureRndstr(16);
|
||||||
|
const user = await usersRepository
|
||||||
|
.insert({
|
||||||
|
id,
|
||||||
|
username: un,
|
||||||
|
usernameLower: un.toLowerCase(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await userProfilesRepository.save({
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNote(data: Partial<MiNote>, user: MiUser, time?: number): Promise<MiNote> {
|
||||||
|
const id = idService.gen(time);
|
||||||
|
const note = await notesRepository
|
||||||
|
.insert({
|
||||||
|
id: id,
|
||||||
|
text: `note_${id}`,
|
||||||
|
userId: user.id,
|
||||||
|
userHost: user.host,
|
||||||
|
visibility: 'public',
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => notesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CleanRemoteNotesProcessorService,
|
||||||
|
IdService,
|
||||||
|
{
|
||||||
|
provide: QueueLoggerService,
|
||||||
|
useFactory: () => ({
|
||||||
|
logger: {
|
||||||
|
createSubLogger: () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
succ: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideProvider(DI.meta).useFactory({ factory: () => meta })
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
service = app.get(CleanRemoteNotesProcessorService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
notesRepository = app.get(DI.notesRepository);
|
||||||
|
noteFavoritesRepository = app.get(DI.noteFavoritesRepository);
|
||||||
|
userNotePiningsRepository = app.get(DI.userNotePiningsRepository);
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||||
|
|
||||||
|
alice = await createUser({ username: 'alice', host: null });
|
||||||
|
bob = await createUser({ username: 'bob', host: 'remote1.example.com' });
|
||||||
|
carol = await createUser({ username: 'carol', host: 'remote2.example.com' });
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Set default meta values
|
||||||
|
meta.enableRemoteNotesCleaning = true;
|
||||||
|
meta.remoteNotesCleaningMaxProcessingDurationInMinutes = 0.3;
|
||||||
|
meta.remoteNotesCleaningExpiryDaysForEachNotes = 30;
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up test data
|
||||||
|
await Promise.all([
|
||||||
|
notesRepository.createQueryBuilder().delete().execute(),
|
||||||
|
userNotePiningsRepository.createQueryBuilder().delete().execute(),
|
||||||
|
noteFavoritesRepository.createQueryBuilder().delete().execute(),
|
||||||
|
]);
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('basic', () => {
|
||||||
|
test('should skip cleaning when enableRemoteNotesCleaning is false', async () => {
|
||||||
|
meta.enableRemoteNotesCleaning = false;
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
deletedCount: 0,
|
||||||
|
oldest: null,
|
||||||
|
newest: null,
|
||||||
|
skipped: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return success result when enableRemoteNotesCleaning is true and no notes to clean', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
await createNote({}, alice);
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
deletedCount: 0,
|
||||||
|
oldest: null,
|
||||||
|
newest: null,
|
||||||
|
skipped: false,
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
test('should clean remote notes and return stats', async () => {
|
||||||
|
// Remote notes
|
||||||
|
const remoteNotes = await Promise.all([
|
||||||
|
createNote({}, bob),
|
||||||
|
createNote({}, carol),
|
||||||
|
createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000),
|
||||||
|
createNote({}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000), // Note older than expiry
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Local notes
|
||||||
|
const localNotes = await Promise.all([
|
||||||
|
createNote({}, alice),
|
||||||
|
createNote({}, alice, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
deletedCount: 2,
|
||||||
|
oldest: expect.any(Number),
|
||||||
|
newest: expect.any(Number),
|
||||||
|
skipped: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check side-by-side from all notes
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.length).toBe(4);
|
||||||
|
expect(remainingNotes.some(n => n.id === remoteNotes[0].id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === remoteNotes[1].id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === remoteNotes[2].id)).toBe(false);
|
||||||
|
expect(remainingNotes.some(n => n.id === remoteNotes[3].id)).toBe(false);
|
||||||
|
expect(remainingNotes.some(n => n.id === localNotes[0].id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === localNotes[1].id)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('advanced', () => {
|
||||||
|
// お気に入り
|
||||||
|
test('should not delete note that is favorited by any user', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Favorite the note
|
||||||
|
await noteFavoritesRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: alice.id,
|
||||||
|
noteId: olderRemoteNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id });
|
||||||
|
expect(remainingNote).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ピン留め
|
||||||
|
test('should not delete note that is pinned by the user', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Pin the note by the user who created it
|
||||||
|
await userNotePiningsRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: bob.id, // Same user as the note creator
|
||||||
|
noteId: olderRemoteNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id });
|
||||||
|
expect(remainingNote).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// クリップ
|
||||||
|
test('should not delete note that is clipped', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that is clipped
|
||||||
|
const clippedNote = await createNote({
|
||||||
|
clippedCount: 1, // Clipped
|
||||||
|
}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNote = await notesRepository.findOneBy({ id: clippedNote.id });
|
||||||
|
expect(remainingNote).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 古いreply, renoteが含まれている時の挙動
|
||||||
|
test('should handle reply/renote relationships correctly', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote notes with reply/renote relationships
|
||||||
|
const originalNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
const replyNote = await createNote({
|
||||||
|
replyId: originalNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
||||||
|
const renoteNote = await createNote({
|
||||||
|
renoteId: originalNote.id,
|
||||||
|
}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 3000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
// Should delete all three notes as they are all old and remote
|
||||||
|
expect(result.deletedCount).toBe(3);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === originalNote.id)).toBe(false);
|
||||||
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(false);
|
||||||
|
expect(remainingNotes.some(n => n.id === renoteNote.id)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 古いリモートノートに新しいリプライがある時、どちらも削除されない
|
||||||
|
test('should not delete both old remote note with new reply', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Create a reply note that is newer than the expiry period
|
||||||
|
const recentReplyNote = await createNote({
|
||||||
|
replyId: oldNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain
|
||||||
|
});
|
||||||
|
|
||||||
|
// 古いリモートノートに新しいリプライと古いリプライがある時、全て残る
|
||||||
|
test('should not delete old remote note with new reply and old reply', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Create a reply note that is newer than the expiry period
|
||||||
|
const recentReplyNote = await createNote({
|
||||||
|
replyId: oldNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000);
|
||||||
|
|
||||||
|
// Create an old reply note that should be deleted
|
||||||
|
const oldReplyNote = await createNote({
|
||||||
|
replyId: oldNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain
|
||||||
|
expect(remainingNotes.some(n => n.id === oldReplyNote.id)).toBe(true); // Old reply note should be deleted
|
||||||
|
});
|
||||||
|
|
||||||
|
// リプライがお気に入りされているとき、どちらも削除されない
|
||||||
|
test('should not delete reply note that is favorited', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Create a reply note that is newer than the expiry period
|
||||||
|
const replyNote = await createNote({
|
||||||
|
replyId: olderRemoteNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
||||||
|
|
||||||
|
// Favorite the reply note
|
||||||
|
await noteFavoritesRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: alice.id,
|
||||||
|
noteId: replyNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Recent reply note should remain
|
||||||
|
});
|
||||||
|
|
||||||
|
// リプライがピン留めされているとき、どちらも削除されない
|
||||||
|
test('should not delete reply note that is pinned', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Create a reply note that is newer than the expiry period
|
||||||
|
const replyNote = await createNote({
|
||||||
|
replyId: olderRemoteNote.id,
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
||||||
|
|
||||||
|
// Pin the reply note
|
||||||
|
await userNotePiningsRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: carol.id,
|
||||||
|
noteId: replyNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Reply note should remain
|
||||||
|
});
|
||||||
|
|
||||||
|
// リプライがクリップされているとき、どちらも削除されない
|
||||||
|
test('should not delete reply note that is clipped', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create old remote note that should be deleted
|
||||||
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
||||||
|
|
||||||
|
// Create a reply note that is old but clipped
|
||||||
|
const replyNote = await createNote({
|
||||||
|
replyId: olderRemoteNote.id,
|
||||||
|
clippedCount: 1, // Clipped
|
||||||
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0); // Both notes should be kept because reply is clipped
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
||||||
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle mixed scenarios with multiple conditions', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create various types of notes
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
|
||||||
|
// Should be deleted: old remote note with no special conditions
|
||||||
|
const deletableNote = await createNote({}, bob, oldTime);
|
||||||
|
|
||||||
|
// Should NOT be deleted: old remote note but favorited
|
||||||
|
const favoritedNote = await createNote({}, carol, oldTime);
|
||||||
|
await noteFavoritesRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: alice.id,
|
||||||
|
noteId: favoritedNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should NOT be deleted: old remote note but pinned
|
||||||
|
const pinnedNote = await createNote({}, bob, oldTime);
|
||||||
|
await userNotePiningsRepository.save({
|
||||||
|
id: idService.gen(),
|
||||||
|
userId: bob.id,
|
||||||
|
noteId: pinnedNote.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should NOT be deleted: old remote note but clipped
|
||||||
|
const clippedNote = await createNote({
|
||||||
|
clippedCount: 2,
|
||||||
|
}, carol, oldTime);
|
||||||
|
|
||||||
|
// Should NOT be deleted: old local note
|
||||||
|
const localNote = await createNote({}, alice, oldTime);
|
||||||
|
|
||||||
|
// Should NOT be deleted: new remote note
|
||||||
|
const newerRemoteNote = await createNote({}, bob);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(1); // Only deletableNote should be deleted
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.length).toBe(5);
|
||||||
|
expect(remainingNotes.some(n => n.id === deletableNote.id)).toBe(false); // Deleted
|
||||||
|
expect(remainingNotes.some(n => n.id === favoritedNote.id)).toBe(true); // Kept
|
||||||
|
expect(remainingNotes.some(n => n.id === pinnedNote.id)).toBe(true); // Kept
|
||||||
|
expect(remainingNotes.some(n => n.id === clippedNote.id)).toBe(true); // Kept
|
||||||
|
expect(remainingNotes.some(n => n.id === localNote.id)).toBe(true); // Kept
|
||||||
|
expect(remainingNotes.some(n => n.id === newerRemoteNote.id)).toBe(true); // Kept
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大量のノート
|
||||||
|
test('should handle large number of notes correctly', async () => {
|
||||||
|
const AMOUNT = 130;
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
const noteIds = [];
|
||||||
|
for (let i = 0; i < AMOUNT; i++) {
|
||||||
|
const note = await createNote({}, bob, oldTime - i);
|
||||||
|
noteIds.push(note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
// Should delete all notes, but may require multiple batches
|
||||||
|
expect(result.deletedCount).toBe(AMOUNT);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大量のノート + リプライ or リノート
|
||||||
|
test('should handle large number of notes with replies correctly', async () => {
|
||||||
|
const AMOUNT = 130;
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
const noteIds = [];
|
||||||
|
for (let i = 0; i < AMOUNT; i++) {
|
||||||
|
const note = await createNote({}, bob, oldTime - i - AMOUNT);
|
||||||
|
noteIds.push(note.id);
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
// Create a reply for every second note
|
||||||
|
await createNote({ replyId: note.id }, carol, oldTime - i);
|
||||||
|
} else {
|
||||||
|
// Create a renote for every second note
|
||||||
|
await createNote({ renoteId: note.id }, bob, oldTime - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
// Should delete all notes, but may require multiple batches
|
||||||
|
expect(result.deletedCount).toBe(AMOUNT * 2);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大量の古いノート + 新しいリプライ or リノート
|
||||||
|
test('should handle large number of old notes with new replies correctly', async () => {
|
||||||
|
const AMOUNT = 130;
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
const newTime = Date.now();
|
||||||
|
const noteIds = [];
|
||||||
|
for (let i = 0; i < AMOUNT; i++) {
|
||||||
|
const note = await createNote({}, bob, oldTime - i);
|
||||||
|
noteIds.push(note.id);
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
// Create a reply for every second note
|
||||||
|
await createNote({ replyId: note.id }, carol, newTime + i);
|
||||||
|
} else {
|
||||||
|
// Create a renote for every second note
|
||||||
|
await createNote({ renoteId: note.id }, bob, newTime + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(0);
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大量の残す対象(clippedCount: 1)と大量の削除対象
|
||||||
|
test('should handle large number of notes, mixed conditions with clippedCount', async () => {
|
||||||
|
const AMOUNT_BASE = 70;
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
const noteIds = [];
|
||||||
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
||||||
|
const note = await createNote({ clippedCount: 1 }, bob, oldTime - i - AMOUNT_BASE);
|
||||||
|
noteIds.push(note.id);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
||||||
|
const note = await createNote({}, carol, oldTime - i);
|
||||||
|
noteIds.push(note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming half are deletable
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 大量の残す対象(リプライ)と大量の削除対象
|
||||||
|
test('should handle large number of notes, mixed conditions with replies', async () => {
|
||||||
|
const AMOUNT_BASE = 70;
|
||||||
|
const job = createMockJob();
|
||||||
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
||||||
|
const newTime = Date.now();
|
||||||
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
||||||
|
// should remain
|
||||||
|
const note = await createNote({}, carol, oldTime - AMOUNT_BASE - i);
|
||||||
|
// should remain
|
||||||
|
await createNote({ replyId: note.id }, bob, newTime + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteIdsExpectedToBeDeleted = [];
|
||||||
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
||||||
|
// should be deleted
|
||||||
|
const note = await createNote({}, bob, oldTime - i);
|
||||||
|
noteIdsExpectedToBeDeleted.push(note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming all replies are deletable
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
|
||||||
|
const remainingNotes = await notesRepository.find();
|
||||||
|
expect(remainingNotes.length).toBe(AMOUNT_BASE * 2); // Only replies should remain
|
||||||
|
noteIdsExpectedToBeDeleted.forEach(id => {
|
||||||
|
expect(remainingNotes.some(n => n.id === id)).toBe(false); // All original notes should be deleted
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update cursor correctly during batch processing', async () => {
|
||||||
|
const job = createMockJob();
|
||||||
|
|
||||||
|
// Create notes with specific timing to test cursor behavior
|
||||||
|
const baseTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 10000;
|
||||||
|
|
||||||
|
const note1 = await createNote({}, bob, baseTime);
|
||||||
|
const note2 = await createNote({}, carol, baseTime - 1000);
|
||||||
|
const note3 = await createNote({}, bob, baseTime - 2000);
|
||||||
|
|
||||||
|
const result = await service.process(job as any);
|
||||||
|
|
||||||
|
expect(result.deletedCount).toBe(3);
|
||||||
|
expect(result.newest).toBe(idService.parse(note1.id).date.getTime());
|
||||||
|
expect(result.oldest).toBe(idService.parse(note3.id).date.getTime());
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
This package contains the common scripts that are used to build the frontend and frontend-embed packages.
|
|
@ -0,0 +1,52 @@
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../shared/eslint.config.js';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.ts',
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
|
||||||
|
...globals.browser,
|
||||||
|
|
||||||
|
// Node.js
|
||||||
|
module: false,
|
||||||
|
require: false,
|
||||||
|
__dirname: false,
|
||||||
|
|
||||||
|
// Misskey
|
||||||
|
_DEV_: false,
|
||||||
|
_LANGS_: false,
|
||||||
|
_VERSION_: false,
|
||||||
|
_ENV_: false,
|
||||||
|
_PERF_PREFIX_: false,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-empty-interface': ['error', {
|
||||||
|
allowSingleExtends: true,
|
||||||
|
}],
|
||||||
|
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||||
|
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
|
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
|
'id-denylist': ['error', 'window', 'e'],
|
||||||
|
'no-shadow': ['warn'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import MagicString from 'magic-string';
|
||||||
|
import { collectModifications } from './locale-inliner/collect-modifications.js';
|
||||||
|
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
|
||||||
|
import { blankLogger } from './logger.js';
|
||||||
|
import type { Logger } from './logger.js';
|
||||||
|
import type { Locale } from '../../locales/index.js';
|
||||||
|
import type { Manifest as ViteManifest } from 'vite';
|
||||||
|
|
||||||
|
export class LocaleInliner {
|
||||||
|
outputDir: string;
|
||||||
|
scriptsDir: string;
|
||||||
|
i18nFile: string;
|
||||||
|
i18nFileName: string;
|
||||||
|
logger: Logger;
|
||||||
|
chunks: ScriptChunk[];
|
||||||
|
|
||||||
|
static async create(options: {
|
||||||
|
outputDir: string,
|
||||||
|
scriptsDir: string,
|
||||||
|
i18nFile: string,
|
||||||
|
logger: Logger,
|
||||||
|
}): Promise<LocaleInliner> {
|
||||||
|
const manifest: ViteManifest = JSON.parse(await fs.readFile(`${options.outputDir}/manifest.json`, 'utf-8'));
|
||||||
|
return new LocaleInliner({ ...options, manifest });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
outputDir: string,
|
||||||
|
scriptsDir: string,
|
||||||
|
i18nFile: string,
|
||||||
|
manifest: ViteManifest,
|
||||||
|
logger: Logger,
|
||||||
|
}) {
|
||||||
|
this.outputDir = options.outputDir;
|
||||||
|
this.scriptsDir = options.scriptsDir;
|
||||||
|
this.i18nFile = options.i18nFile;
|
||||||
|
this.i18nFileName = this.stripScriptDir(options.manifest[this.i18nFile].file);
|
||||||
|
this.logger = options.logger;
|
||||||
|
this.chunks = Object.values(options.manifest).filter(chunk => this.isScriptFile(chunk.file)).map(chunk => ({
|
||||||
|
fileName: this.stripScriptDir(chunk.file),
|
||||||
|
chunkName: chunk.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFiles() {
|
||||||
|
await Promise.all(this.chunks.map(async chunk => {
|
||||||
|
const filePath = path.join(this.outputDir, this.scriptsDir, chunk.fileName);
|
||||||
|
chunk.sourceCode = await fs.readFile(filePath, 'utf-8');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
collectsModifications() {
|
||||||
|
for (const chunk of this.chunks) {
|
||||||
|
if (!chunk.sourceCode) {
|
||||||
|
throw new Error(`Source code for ${chunk.fileName} is not loaded.`);
|
||||||
|
}
|
||||||
|
const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
|
||||||
|
chunk.modifications = collectModifications(chunk.sourceCode, chunk.fileName, fileLogger, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveAllLocales(locales: Record<string, Locale>) {
|
||||||
|
const localeNames = Object.keys(locales);
|
||||||
|
for (const localeName of localeNames) {
|
||||||
|
await this.saveLocale(localeName, locales[localeName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveLocale(localeName: string, localeJson: Locale) {
|
||||||
|
// create directory
|
||||||
|
await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true });
|
||||||
|
const localeLogger = localeName === 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only
|
||||||
|
for (const chunk of this.chunks) {
|
||||||
|
if (!chunk.sourceCode || !chunk.modifications) {
|
||||||
|
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
|
||||||
|
}
|
||||||
|
const fileLogger = localeLogger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
|
||||||
|
const magicString = new MagicString(chunk.sourceCode);
|
||||||
|
applyWithLocale(magicString, chunk.modifications, localeName, localeJson, fileLogger);
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(this.outputDir, localeName, chunk.fileName), magicString.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isScriptFile(fileName: string) {
|
||||||
|
return fileName.startsWith(this.scriptsDir + '/') && fileName.endsWith('.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
stripScriptDir(fileName: string) {
|
||||||
|
if (!fileName.startsWith(this.scriptsDir + '/')) {
|
||||||
|
throw new Error(`${fileName} does not start with ${this.scriptsDir}/`);
|
||||||
|
}
|
||||||
|
return fileName.slice(this.scriptsDir.length + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScriptChunk {
|
||||||
|
fileName: string;
|
||||||
|
chunkName?: string;
|
||||||
|
sourceCode?: string;
|
||||||
|
modifications?: TextModification[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextModification = {
|
||||||
|
type: 'delete';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
localizedOnly: boolean;
|
||||||
|
} | {
|
||||||
|
// can be used later to insert '../scripts' for common files
|
||||||
|
type: 'insert';
|
||||||
|
begin: number;
|
||||||
|
text: string;
|
||||||
|
localizedOnly: boolean;
|
||||||
|
} | {
|
||||||
|
type: 'replace';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
localizedOnly: boolean;
|
||||||
|
} | {
|
||||||
|
type: 'localized';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
localizationKey: string[];
|
||||||
|
localizedOnly: true;
|
||||||
|
} | {
|
||||||
|
type: 'parameterized-function';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
localizationKey: string[];
|
||||||
|
localizedOnly: true;
|
||||||
|
} | {
|
||||||
|
type: 'locale-name';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
literal: boolean;
|
||||||
|
localizedOnly: true;
|
||||||
|
} | {
|
||||||
|
type: 'locale-json';
|
||||||
|
begin: number;
|
||||||
|
end: number;
|
||||||
|
localizedOnly: true;
|
||||||
|
};
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MagicString from 'magic-string';
|
||||||
|
import { assertNever } from '../utils.js';
|
||||||
|
import type { Locale, ILocale } from '../../../locales/index.js';
|
||||||
|
import type { TextModification } from '../locale-inliner.js';
|
||||||
|
import type { Logger } from '../logger.js';
|
||||||
|
|
||||||
|
export function applyWithLocale(
|
||||||
|
sourceCode: MagicString,
|
||||||
|
modifications: TextModification[],
|
||||||
|
localeName: string,
|
||||||
|
localeJson: Locale,
|
||||||
|
fileLogger: Logger,
|
||||||
|
) {
|
||||||
|
for (const modification of modifications) {
|
||||||
|
switch (modification.type) {
|
||||||
|
case 'delete':
|
||||||
|
sourceCode.remove(modification.begin, modification.end);
|
||||||
|
break;
|
||||||
|
case 'insert':
|
||||||
|
sourceCode.appendRight(modification.begin, modification.text);
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
sourceCode.update(modification.begin, modification.end, modification.text);
|
||||||
|
break;
|
||||||
|
case 'localized': {
|
||||||
|
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
||||||
|
if (accessed == null) {
|
||||||
|
fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`);
|
||||||
|
}
|
||||||
|
sourceCode.update(modification.begin, modification.end, JSON.stringify(accessed));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'parameterized-function': {
|
||||||
|
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
||||||
|
let replacement: string;
|
||||||
|
if (typeof accessed === 'string') {
|
||||||
|
replacement = formatFunction(accessed);
|
||||||
|
} else if (typeof accessed === 'object' && accessed !== null) {
|
||||||
|
replacement = `({${Object.entries(accessed).map(([key, value]) => `${JSON.stringify(key)}:${formatFunction(value)}`).join(',')}})`;
|
||||||
|
} else {
|
||||||
|
fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`);
|
||||||
|
replacement = '(() => "")'; // placeholder for missing locale
|
||||||
|
}
|
||||||
|
sourceCode.update(modification.begin, modification.end, replacement);
|
||||||
|
break;
|
||||||
|
|
||||||
|
function formatFunction(format: string): string {
|
||||||
|
const params = new Set<string>();
|
||||||
|
const components: string[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
for (const match of format.matchAll(/\{(.+?)}/g)) {
|
||||||
|
const [fullMatch, paramName] = match;
|
||||||
|
if (lastIndex < match.index) {
|
||||||
|
components.push(JSON.stringify(format.slice(lastIndex, match.index)));
|
||||||
|
}
|
||||||
|
params.add(paramName);
|
||||||
|
components.push(paramName);
|
||||||
|
lastIndex = match.index + fullMatch.length;
|
||||||
|
}
|
||||||
|
components.push(JSON.stringify(format.slice(lastIndex)));
|
||||||
|
|
||||||
|
// we replace with `(({name,count})=>(name+count+"some"))`
|
||||||
|
const paramList = Array.from(params).join(',');
|
||||||
|
let body = components.filter(x => x !== '""').join('+');
|
||||||
|
if (body === '') body = '""'; // if the body is empty, we return empty string
|
||||||
|
return `(({${paramList}})=>(${body}))`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'locale-name': {
|
||||||
|
sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'locale-json': {
|
||||||
|
// locale-json is inlined to place where initialize module-level variable which is executed only once.
|
||||||
|
// In such case we can use JSON.parse to speed up the parsing script.
|
||||||
|
// https://v8.dev/blog/cost-of-javascript-2019#json
|
||||||
|
sourceCode.update(modification.begin, modification.end, `JSON.parse(${JSON.stringify(JSON.stringify(localeJson))})`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(modification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPropertyByPath(localeJson: ILocale, localizationKey: string[]): string | object | null {
|
||||||
|
if (localizationKey.length === 0) return localeJson;
|
||||||
|
let current: ILocale | string = localeJson;
|
||||||
|
for (const key of localizationKey) {
|
||||||
|
if (typeof current !== 'object' || !(key in current)) {
|
||||||
|
return null; // Key not found
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
|
@ -0,0 +1,425 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parseAst } from 'vite';
|
||||||
|
import * as estreeWalker from 'estree-walker';
|
||||||
|
import { assertNever, assertType } from '../utils.js';
|
||||||
|
import type { AstNode, ProgramNode } from 'rollup';
|
||||||
|
import type * as estree from 'estree';
|
||||||
|
import type { LocaleInliner, TextModification } from '../locale-inliner.js';
|
||||||
|
import type { Logger } from '../logger.js';
|
||||||
|
|
||||||
|
// WalkerContext is not exported from estree-walker, so we define it here
|
||||||
|
interface WalkerContext {
|
||||||
|
skip: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] {
|
||||||
|
let programNode: ProgramNode;
|
||||||
|
try {
|
||||||
|
programNode = parseAst(sourceCode);
|
||||||
|
} catch (err) {
|
||||||
|
fileLogger.error(`Failed to parse source code: ${err}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (programNode.sourceType !== 'module') {
|
||||||
|
fileLogger.error('Source code is not a module.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifications: TextModification[] = [];
|
||||||
|
|
||||||
|
// first
|
||||||
|
// 1) replace all `scripts/` path literals with locale code
|
||||||
|
// 2) replace all `localStorage.getItem("lang")` with `localeName` variable
|
||||||
|
// 3) replace all `await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json())` with `localeJson` variable
|
||||||
|
estreeWalker.walk(programNode, {
|
||||||
|
enter(this: WalkerContext, node: Node) {
|
||||||
|
assertType<AstNode>(node);
|
||||||
|
|
||||||
|
if (node.type === 'Literal' && typeof node.value === 'string' && node.raw) {
|
||||||
|
if (node.raw.substring(1).startsWith(inliner.scriptsDir)) {
|
||||||
|
// we find `scripts/\w+\.js` literal and replace 'scripts' part with locale code
|
||||||
|
fileLogger.debug(`${lineCol(sourceCode, node)}: found ${inliner.scriptsDir}/ path literal ${node.raw}`);
|
||||||
|
modifications.push({
|
||||||
|
type: 'locale-name',
|
||||||
|
begin: node.start + 1,
|
||||||
|
end: node.start + 1 + inliner.scriptsDir.length,
|
||||||
|
literal: false,
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (node.raw.substring(1, node.raw.length - 1) === `${inliner.scriptsDir}/${inliner.i18nFileName}`) {
|
||||||
|
// we find `scripts/i18n.ts` literal.
|
||||||
|
// This is tipically in depmap and replace with this file name to avoid unnecessary loading i18n script
|
||||||
|
fileLogger.debug(`${lineCol(sourceCode, node)}: found ${inliner.i18nFileName} path literal ${node.raw}`);
|
||||||
|
modifications.push({
|
||||||
|
type: 'replace',
|
||||||
|
begin: node.end - 1 - inliner.i18nFileName.length,
|
||||||
|
end: node.end - 1,
|
||||||
|
text: fileName,
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalStorageGetItemLang(node)) {
|
||||||
|
fileLogger.debug(`${lineCol(sourceCode, node)}: found localStorage.getItem("lang") call`);
|
||||||
|
modifications.push({
|
||||||
|
type: 'locale-name',
|
||||||
|
begin: node.start,
|
||||||
|
end: node.end,
|
||||||
|
literal: true,
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAwaitFetchLocaleThenJson(node)) {
|
||||||
|
// await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json(), () => null)
|
||||||
|
fileLogger.debug(`${lineCol(sourceCode, node)}: found await window.fetch(\`/assets/locales/\${d}.\${x}.json\`).then(u=>u.json()) call`);
|
||||||
|
modifications.push({
|
||||||
|
type: 'locale-json',
|
||||||
|
begin: node.start,
|
||||||
|
end: node.end,
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, 'i18n');
|
||||||
|
|
||||||
|
switch (importSpecifierResult.type) {
|
||||||
|
case 'no-import':
|
||||||
|
fileLogger.debug('No import of i18n found, skipping inlining.');
|
||||||
|
return modifications;
|
||||||
|
case 'no-specifiers':
|
||||||
|
fileLogger.debug('Importing i18n without specifiers, removing the import.');
|
||||||
|
modifications.push({
|
||||||
|
type: 'delete',
|
||||||
|
begin: importSpecifierResult.importNode.start,
|
||||||
|
end: importSpecifierResult.importNode.end,
|
||||||
|
localizedOnly: false,
|
||||||
|
});
|
||||||
|
return modifications;
|
||||||
|
case 'unexpected-specifiers':
|
||||||
|
fileLogger.info(`Importing ${inliner.i18nFileName} found but with unexpected specifiers. Skipping inlining.`);
|
||||||
|
return modifications;
|
||||||
|
case 'specifier':
|
||||||
|
fileLogger.debug(`Found import i18n as ${importSpecifierResult.localI18nIdentifier}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18nImport = importSpecifierResult.importNode;
|
||||||
|
const localI18nIdentifier = importSpecifierResult.localI18nIdentifier;
|
||||||
|
|
||||||
|
// Check if the identifier is already declared in the file.
|
||||||
|
// If it is, we may overwrite it and cause issues so we skip inlining
|
||||||
|
let isSupported = true;
|
||||||
|
estreeWalker.walk(programNode, {
|
||||||
|
enter(node) {
|
||||||
|
if (node.type === 'VariableDeclaration') {
|
||||||
|
assertType<estree.VariableDeclaration>(node);
|
||||||
|
for (const id of node.declarations.flatMap(x => declsOfPattern(x.id))) {
|
||||||
|
if (id === localI18nIdentifier) {
|
||||||
|
isSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (!isSupported) {
|
||||||
|
fileLogger.error(`Duplicated identifier "${localI18nIdentifier}" in variable declaration. Skipping inlining.`);
|
||||||
|
return modifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLogger.debug(`imports i18n as ${localI18nIdentifier}`);
|
||||||
|
|
||||||
|
// In case of substitution failure, we will preserve the import statement
|
||||||
|
// otherwise we will remove it.
|
||||||
|
let preserveI18nImport = false;
|
||||||
|
|
||||||
|
const toSkip = new Set();
|
||||||
|
toSkip.add(i18nImport);
|
||||||
|
estreeWalker.walk(programNode, {
|
||||||
|
enter(this: WalkerContext, node, parent, property) {
|
||||||
|
assertType<AstNode>(node);
|
||||||
|
assertType<AstNode>(parent);
|
||||||
|
if (toSkip.has(node)) {
|
||||||
|
// This is the import specifier, skip processing it
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care original name part of the import declaration
|
||||||
|
if (node.type === 'ImportDeclaration') this.skip();
|
||||||
|
|
||||||
|
if (node.type === 'Identifier') {
|
||||||
|
assertType<estree.Identifier>(node);
|
||||||
|
assertType<estree.Property | estree.MemberExpression | estree.ExportSpecifier>(parent);
|
||||||
|
if (parent.type === 'Property' && !parent.computed && property === 'key') return; // we don't care 'id' part of { id: expr }
|
||||||
|
if (parent.type === 'MemberExpression' && !parent.computed && property === 'property') return; // we don't care 'id' part of { id: expr }
|
||||||
|
if (parent.type === 'ExportSpecifier' && property === 'exported') return; // we don't care 'id' part of { id: expr }
|
||||||
|
if (node.name === localI18nIdentifier) {
|
||||||
|
fileLogger.error(`${lineCol(sourceCode, node)}: Using i18n identifier "${localI18nIdentifier}" directly. Skipping inlining.`);
|
||||||
|
preserveI18nImport = true;
|
||||||
|
}
|
||||||
|
} else if (node.type === 'MemberExpression') {
|
||||||
|
assertType<estree.MemberExpression>(node);
|
||||||
|
const i18nPath = parseI18nPropertyAccess(node);
|
||||||
|
if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] === 'ts') {
|
||||||
|
if (parent.type === 'CallExpression' && property === 'callee') return; // we don't want to process `i18n.ts.property.stringBuiltinMethod()`
|
||||||
|
if (i18nPath.at(-1)?.startsWith('_')) fileLogger.debug(`found i18n grouped property access ${i18nPath.join('.')}`);
|
||||||
|
else fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n property access ${i18nPath.join('.')}`);
|
||||||
|
// it's i18n.ts.propertyAccess
|
||||||
|
// i18n.ts.* will always be resolved to string or object containing strings
|
||||||
|
modifications.push({
|
||||||
|
type: 'localized',
|
||||||
|
begin: node.start,
|
||||||
|
end: node.end,
|
||||||
|
localizationKey: i18nPath.slice(1), // remove 'ts' prefix
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
this.skip();
|
||||||
|
} else if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] === 'tsx') {
|
||||||
|
// it's parameterized locale substitution (`i18n.tsx.property(parameters)`)
|
||||||
|
// we expect the parameter to be an object literal
|
||||||
|
fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n function access (object) ${i18nPath.join('.')}`);
|
||||||
|
modifications.push({
|
||||||
|
type: 'parameterized-function',
|
||||||
|
begin: node.start,
|
||||||
|
end: node.end,
|
||||||
|
localizationKey: i18nPath.slice(1), // remove 'tsx' prefix
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
} else if (node.type === 'ArrowFunctionExpression') {
|
||||||
|
assertType<estree.ArrowFunctionExpression>(node);
|
||||||
|
// If there is 'i18n' in the parameters, we care interior of the function
|
||||||
|
if (node.params.flatMap(param => declsOfPattern(param)).includes(localI18nIdentifier)) this.skip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (!preserveI18nImport) {
|
||||||
|
fileLogger.debug('removing i18n import statement');
|
||||||
|
modifications.push({
|
||||||
|
type: 'delete',
|
||||||
|
begin: i18nImport.start,
|
||||||
|
end: i18nImport.end,
|
||||||
|
localizedOnly: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseI18nPropertyAccess(node: estree.Expression | estree.Super): string[] | null {
|
||||||
|
if (node.type === 'Identifier' && node.name === localI18nIdentifier) return []; // i18n itself
|
||||||
|
if (node.type !== 'MemberExpression') return null;
|
||||||
|
// super.*
|
||||||
|
if (node.object.type === 'Super') return null;
|
||||||
|
|
||||||
|
// i18n?.property is not supported
|
||||||
|
if (node.optional) return null;
|
||||||
|
|
||||||
|
let id: string | null = null;
|
||||||
|
if (node.computed) {
|
||||||
|
if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
|
||||||
|
id = node.property.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node.property.type === 'Identifier') {
|
||||||
|
id = node.property.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// non-constant property access
|
||||||
|
if (id == null) return null;
|
||||||
|
|
||||||
|
const parentAccess = parseI18nPropertyAccess(node.object);
|
||||||
|
if (parentAccess == null) return null;
|
||||||
|
return [...parentAccess, id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
function declsOfPattern(pattern: estree.Pattern | null): string[] {
|
||||||
|
if (pattern == null) return [];
|
||||||
|
switch (pattern.type) {
|
||||||
|
case 'Identifier':
|
||||||
|
return [pattern.name];
|
||||||
|
case 'ObjectPattern':
|
||||||
|
return pattern.properties.flatMap(prop => {
|
||||||
|
switch (prop.type) {
|
||||||
|
case 'Property':
|
||||||
|
return declsOfPattern(prop.value);
|
||||||
|
case 'RestElement':
|
||||||
|
return declsOfPattern(prop.argument);
|
||||||
|
default:
|
||||||
|
assertNever(prop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case 'ArrayPattern':
|
||||||
|
return pattern.elements.flatMap(p => declsOfPattern(p));
|
||||||
|
case 'RestElement':
|
||||||
|
return declsOfPattern(pattern.argument);
|
||||||
|
case 'AssignmentPattern':
|
||||||
|
return declsOfPattern(pattern.left);
|
||||||
|
case 'MemberExpression':
|
||||||
|
// assignment pattern so no new variable is declared
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
assertNever(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineCol(sourceCode: string, node: estree.Node): string {
|
||||||
|
assertType<AstNode>(node);
|
||||||
|
const leading = sourceCode.slice(0, node.start);
|
||||||
|
const lines = leading.split('\n');
|
||||||
|
const line = lines.length;
|
||||||
|
const col = lines[lines.length - 1].length + 1; // +1 for 1-based index
|
||||||
|
return `(${line}:${col})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//region checker functions
|
||||||
|
|
||||||
|
type Node =
|
||||||
|
| estree.AssignmentProperty
|
||||||
|
| estree.CatchClause
|
||||||
|
| estree.Class
|
||||||
|
| estree.ClassBody
|
||||||
|
| estree.Expression
|
||||||
|
| estree.Function
|
||||||
|
| estree.Identifier
|
||||||
|
| estree.Literal
|
||||||
|
| estree.MethodDefinition
|
||||||
|
| estree.ModuleDeclaration
|
||||||
|
| estree.ModuleSpecifier
|
||||||
|
| estree.Pattern
|
||||||
|
| estree.PrivateIdentifier
|
||||||
|
| estree.Program
|
||||||
|
| estree.Property
|
||||||
|
| estree.PropertyDefinition
|
||||||
|
| estree.SpreadElement
|
||||||
|
| estree.Statement
|
||||||
|
| estree.Super
|
||||||
|
| estree.SwitchCase
|
||||||
|
| estree.TemplateElement
|
||||||
|
| estree.VariableDeclarator
|
||||||
|
;
|
||||||
|
|
||||||
|
// localStorage.getItem("lang")
|
||||||
|
function isLocalStorageGetItemLang(getItemCall: Node): boolean {
|
||||||
|
if (getItemCall.type !== 'CallExpression') return false;
|
||||||
|
if (getItemCall.arguments.length !== 1) return false;
|
||||||
|
|
||||||
|
const langLiteral = getItemCall.arguments[0];
|
||||||
|
if (!isStringLiteral(langLiteral, 'lang')) return false;
|
||||||
|
|
||||||
|
const getItemFunction = getItemCall.callee;
|
||||||
|
if (!isMemberExpression(getItemFunction, 'getItem')) return false;
|
||||||
|
|
||||||
|
const localStorageObject = getItemFunction.object;
|
||||||
|
if (!isIdentifier(localStorageObject, 'localStorage')) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// await window.fetch(`/assets/locales/${d}.${x}.json`).then(u => u.json(), ....)
|
||||||
|
function isAwaitFetchLocaleThenJson(awaitNode: Node): boolean {
|
||||||
|
if (awaitNode.type !== 'AwaitExpression') return false;
|
||||||
|
|
||||||
|
const thenCall = awaitNode.argument;
|
||||||
|
if (thenCall.type !== 'CallExpression') return false;
|
||||||
|
if (thenCall.arguments.length < 1) return false;
|
||||||
|
|
||||||
|
const arrowFunction = thenCall.arguments[0];
|
||||||
|
if (arrowFunction.type !== 'ArrowFunctionExpression') return false;
|
||||||
|
if (arrowFunction.params.length !== 1) return false;
|
||||||
|
|
||||||
|
const arrowBodyCall = arrowFunction.body;
|
||||||
|
if (arrowBodyCall.type !== 'CallExpression') return false;
|
||||||
|
|
||||||
|
const jsonFunction = arrowBodyCall.callee;
|
||||||
|
if (!isMemberExpression(jsonFunction, 'json')) return false;
|
||||||
|
|
||||||
|
const thenFunction = thenCall.callee;
|
||||||
|
if (!isMemberExpression(thenFunction, 'then')) return false;
|
||||||
|
|
||||||
|
const fetchCall = thenFunction.object;
|
||||||
|
if (fetchCall.type !== 'CallExpression') return false;
|
||||||
|
if (fetchCall.arguments.length !== 1) return false;
|
||||||
|
|
||||||
|
// `/assets/locales/${d}.${x}.json`
|
||||||
|
const assetLocaleTemplate = fetchCall.arguments[0];
|
||||||
|
if (assetLocaleTemplate.type !== 'TemplateLiteral') return false;
|
||||||
|
if (assetLocaleTemplate.quasis.length !== 3) return false;
|
||||||
|
if (assetLocaleTemplate.expressions.length !== 2) return false;
|
||||||
|
if (assetLocaleTemplate.quasis[0].value.cooked !== '/assets/locales/') return false;
|
||||||
|
if (assetLocaleTemplate.quasis[1].value.cooked !== '.') return false;
|
||||||
|
if (assetLocaleTemplate.quasis[2].value.cooked !== '.json') return false;
|
||||||
|
|
||||||
|
const fetchFunction = fetchCall.callee;
|
||||||
|
if (!isMemberExpression(fetchFunction, 'fetch')) return false;
|
||||||
|
const windowObject = fetchFunction.object;
|
||||||
|
if (!isIdentifier(windowObject, 'window')) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecifierResult =
|
||||||
|
| { type: 'no-import' }
|
||||||
|
| { type: 'no-specifiers', importNode: estree.ImportDeclaration & AstNode }
|
||||||
|
| { type: 'unexpected-specifiers', importNode: estree.ImportDeclaration & AstNode }
|
||||||
|
| { type: 'specifier', localI18nIdentifier: string, importNode: estree.ImportDeclaration & AstNode }
|
||||||
|
;
|
||||||
|
|
||||||
|
function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult {
|
||||||
|
const imports = programNode.body.filter(x => x.type === 'ImportDeclaration');
|
||||||
|
const importNode = imports.find(x => x.source.value === `./${i18nFileName}`) as estree.ImportDeclaration | undefined;
|
||||||
|
if (!importNode) return { type: 'no-import' };
|
||||||
|
assertType<AstNode>(importNode);
|
||||||
|
|
||||||
|
if (importNode.specifiers.length === 0) {
|
||||||
|
return { type: 'no-specifiers', importNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importNode.specifiers.length !== 1) {
|
||||||
|
return { type: 'unexpected-specifiers', importNode };
|
||||||
|
}
|
||||||
|
const i18nImportSpecifier = importNode.specifiers[0];
|
||||||
|
if (i18nImportSpecifier.type !== 'ImportSpecifier') {
|
||||||
|
return { type: 'unexpected-specifiers', importNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i18nImportSpecifier.imported.type !== 'Identifier') {
|
||||||
|
return { type: 'unexpected-specifiers', importNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
const importingIdentifier = i18nImportSpecifier.imported.name;
|
||||||
|
if (importingIdentifier !== i18nSymbol) {
|
||||||
|
return { type: 'unexpected-specifiers', importNode };
|
||||||
|
}
|
||||||
|
const localI18nIdentifier = i18nImportSpecifier.local.name;
|
||||||
|
return { type: 'specifier', localI18nIdentifier, importNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
// checker helpers
|
||||||
|
function isMemberExpression(node: Node, property: string): node is estree.MemberExpression {
|
||||||
|
return node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier' && node.property.name === property;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringLiteral(node: Node, value: string): node is estree.Literal {
|
||||||
|
return node.type === 'Literal' && typeof node.value === 'string' && node.value === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentifier(node: Node, name: string): node is estree.Identifier {
|
||||||
|
return node.type === 'Identifier' && node.name === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as process from 'node:process';
|
||||||
|
|
||||||
|
const debug = process.env.BUILDER_DEBUG !== undefined && process.env.BUILDER_DEBUG !== '0';
|
||||||
|
|
||||||
|
export interface Logger {
|
||||||
|
debug(message: string): void;
|
||||||
|
|
||||||
|
warn(message: string): void;
|
||||||
|
|
||||||
|
error(message: string): void;
|
||||||
|
|
||||||
|
info(message: string): void;
|
||||||
|
|
||||||
|
prefixed(newPrefix: string): Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RootLogger extends Logger {
|
||||||
|
warningCount: number;
|
||||||
|
errorCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLogger(): RootLogger {
|
||||||
|
return loggerFactory('', {
|
||||||
|
warningCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogContext = {
|
||||||
|
warningCount: number;
|
||||||
|
errorCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function loggerFactory(prefix: string, context: LogContext): RootLogger {
|
||||||
|
return {
|
||||||
|
debug: (message: string) => {
|
||||||
|
if (debug) console.log(`[DBG] ${prefix}${message}`);
|
||||||
|
},
|
||||||
|
warn: (message: string) => {
|
||||||
|
context.warningCount++;
|
||||||
|
console.log(`${debug ? '[WRN]' : 'w:'} ${prefix}${message}`);
|
||||||
|
},
|
||||||
|
error: (message: string) => {
|
||||||
|
context.errorCount++;
|
||||||
|
console.error(`${debug ? '[ERR]' : 'e:'} ${prefix}${message}`);
|
||||||
|
},
|
||||||
|
info: (message: string) => {
|
||||||
|
console.error(`${debug ? '[INF]' : 'i:'} ${prefix}${message}`);
|
||||||
|
},
|
||||||
|
prefixed: (newPrefix: string) => {
|
||||||
|
return loggerFactory(`${prefix}${newPrefix}`, context);
|
||||||
|
},
|
||||||
|
get warningCount() {
|
||||||
|
return context.warningCount;
|
||||||
|
},
|
||||||
|
get errorCount() {
|
||||||
|
return context.errorCount;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blankLogger: Logger = {
|
||||||
|
debug: () => void 0,
|
||||||
|
warn: () => void 0,
|
||||||
|
error: () => void 0,
|
||||||
|
info: () => void 0,
|
||||||
|
prefixed: () => blankLogger,
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "frontend-builder",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./*": "./js/*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/estree": "1.0.8",
|
||||||
|
"@types/node": "22.17.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "8.38.0",
|
||||||
|
"@typescript-eslint/parser": "8.38.0",
|
||||||
|
"rollup": "4.46.2",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"estree-walker": "3.0.3",
|
||||||
|
"magic-string": "0.30.17",
|
||||||
|
"vite": "7.0.6"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as estreeWalker from 'estree-walker';
|
||||||
|
import MagicString from 'magic-string';
|
||||||
|
import { assertType } from './utils.js';
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
import type { CallExpression, Expression, Program } from 'estree';
|
||||||
|
import type { AstNode } from 'rollup';
|
||||||
|
|
||||||
|
// This plugin transforms `unref(i18n)` to `i18n` in the code, which is useful for removing unnecessary unref calls
|
||||||
|
// and helps locale inliner runs after vite build to inline the locale data into the final build.
|
||||||
|
//
|
||||||
|
// locale inliner cannot know minifiedSymbol(i18n) is 'unref(i18n)' or 'otherFunctionsWithEffect(i18n)' so
|
||||||
|
// it is necessary to remove unref calls before minification.
|
||||||
|
export function pluginRemoveUnrefI18n(
|
||||||
|
{
|
||||||
|
i18nSymbolName = 'i18n',
|
||||||
|
}: {
|
||||||
|
i18nSymbolName?: string
|
||||||
|
} = {}): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'UnwindCssModuleClassName',
|
||||||
|
renderChunk(code) {
|
||||||
|
if (!code.includes('unref(i18n)')) return null;
|
||||||
|
const ast = this.parse(code) as Program;
|
||||||
|
const magicString = new MagicString(code);
|
||||||
|
estreeWalker.walk(ast, {
|
||||||
|
enter(node) {
|
||||||
|
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'unref'
|
||||||
|
&& node.arguments.length === 1) {
|
||||||
|
// calls to unref with single argument
|
||||||
|
const arg = node.arguments[0];
|
||||||
|
if (arg.type === 'Identifier' && arg.name === i18nSymbolName) {
|
||||||
|
// this is unref(i18n) so replace it with i18n
|
||||||
|
// to replace, remove the 'unref(' and the trailing ')'
|
||||||
|
assertType<CallExpression & AstNode>(node);
|
||||||
|
assertType<Expression & AstNode>(arg);
|
||||||
|
magicString.remove(node.start, arg.start);
|
||||||
|
magicString.remove(arg.end, node.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: magicString.toString(),
|
||||||
|
map: magicString.generateMap({ hires: true }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"typeRoots": [
|
||||||
|
"./@types",
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function assertNever(x: never): never {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertType<T>(node: unknown): asserts node is T {
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import url from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execa } from 'execa';
|
||||||
|
import locales from '../../locales/index.js';
|
||||||
|
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
|
||||||
|
import { createLogger } from '../frontend-builder/logger';
|
||||||
|
|
||||||
|
// requires node 21 or later
|
||||||
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = __dirname + '/../../built/_frontend_embed_vite_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function viteBuild() {
|
||||||
|
await execa('vite', ['build'], {
|
||||||
|
cwd: __dirname,
|
||||||
|
stdout: process.stdout,
|
||||||
|
stderr: process.stderr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function buildAllLocale() {
|
||||||
|
const logger = createLogger()
|
||||||
|
const inliner = await LocaleInliner.create({
|
||||||
|
outputDir,
|
||||||
|
logger,
|
||||||
|
scriptsDir: 'scripts',
|
||||||
|
i18nFile: 'src/i18n.ts',
|
||||||
|
})
|
||||||
|
|
||||||
|
await inliner.loadFiles();
|
||||||
|
|
||||||
|
inliner.collectsModifications();
|
||||||
|
|
||||||
|
await inliner.saveAllLocales(locales);
|
||||||
|
|
||||||
|
if (logger.errorCount > 0) {
|
||||||
|
throw new Error(`Build failed with ${logger.errorCount} errors and ${logger.warningCount} warnings.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
await fs.rm(outputDir, { recursive: true, force: true });
|
||||||
|
await viteBuild();
|
||||||
|
await buildAllLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
await build();
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite",
|
"watch": "vite",
|
||||||
"build": "vite build",
|
"build": "tsx build.ts",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"icons-subsetter": "workspace:*",
|
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
|
"icons-subsetter": "workspace:*",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"start-server-and-test": "2.0.12",
|
"start-server-and-test": "2.0.12",
|
||||||
|
"tsx": "4.20.3",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vue-component-type-helpers": "3.0.5",
|
"vue-component-type-helpers": "3.0.5",
|
||||||
"vue-eslint-parser": "10.2.0",
|
"vue-eslint-parser": "10.2.0",
|
||||||
|
|
|
@ -17,15 +17,16 @@ import { createApp, defineAsyncComponent } from 'vue';
|
||||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||||
import defaultDarkTheme from '@@/themes/d-dark.json5';
|
import defaultDarkTheme from '@@/themes/d-dark.json5';
|
||||||
import { MediaProxy } from '@@/js/media-proxy.js';
|
import { MediaProxy } from '@@/js/media-proxy.js';
|
||||||
|
import { storeBootloaderErrors } from '@@/js/store-boot-errors';
|
||||||
import { applyTheme, assertIsTheme } from '@/theme.js';
|
import { applyTheme, assertIsTheme } from '@/theme.js';
|
||||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { serverMetadata } from '@/server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url, version, locale, lang, updateLocale } from '@@/js/config.js';
|
import { url, version, lang } from '@@/js/config.js';
|
||||||
import { parseEmbedParams } from '@@/js/embed-page.js';
|
import { parseEmbedParams } from '@@/js/embed-page.js';
|
||||||
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
||||||
import { serverContext } from '@/server-context.js';
|
import { serverContext } from '@/server-context.js';
|
||||||
import { i18n, updateI18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
import type { Theme } from '@/theme.js';
|
import type { Theme } from '@/theme.js';
|
||||||
|
|
||||||
|
@ -76,19 +77,7 @@ if (embedParams.colorMode === 'dark') {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
const localeVersion = localStorage.getItem('localeVersion');
|
storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
||||||
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
|
|
||||||
if (localeOutdated) {
|
|
||||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
|
|
||||||
if (res.status === 200) {
|
|
||||||
const newLocale = await res.text();
|
|
||||||
const parsedNewLocale = JSON.parse(newLocale);
|
|
||||||
localStorage.setItem('locale', newLocale);
|
|
||||||
localStorage.setItem('localeVersion', version);
|
|
||||||
updateLocale(parsedNewLocale);
|
|
||||||
updateI18n(parsedNewLocale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// サイズの制限
|
// サイズの制限
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import { I18n } from '@@/js/i18n.js';
|
import { I18n } from '@@/js/i18n.js';
|
||||||
|
import { locale } from '@@/js/locale.js';
|
||||||
import type { Locale } from '../../../locales/index.js';
|
import type { Locale } from '../../../locales/index.js';
|
||||||
import { locale } from '@@/js/config.js';
|
|
||||||
|
|
||||||
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
||||||
|
|
||||||
|
// test 以外では使わないこと。インライン化されてるのでだいたい意味がない
|
||||||
export function updateI18n(newLocale: Locale) {
|
export function updateI18n(newLocale: Locale) {
|
||||||
i18n.locale = newLocale;
|
i18n.locale = newLocale;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import locales from '../../locales/index.js';
|
||||||
import meta from '../../package.json';
|
import meta from '../../package.json';
|
||||||
import packageInfo from './package.json' with { type: 'json' };
|
import packageInfo from './package.json' with { type: 'json' };
|
||||||
import pluginJson5 from './vite.json5.js';
|
import pluginJson5 from './vite.json5.js';
|
||||||
|
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n';
|
||||||
|
|
||||||
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
|
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
|
||||||
const host = url ? (new URL(url)).hostname : undefined;
|
const host = url ? (new URL(url)).hostname : undefined;
|
||||||
|
@ -85,6 +86,7 @@ export function getConfig(): UserConfig {
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
pluginVue(),
|
pluginVue(),
|
||||||
|
pluginRemoveUnrefI18n(),
|
||||||
pluginJson5(),
|
pluginJson5(),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -135,15 +137,20 @@ export function getConfig(): UserConfig {
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
app: './src/boot.ts',
|
i18n: './src/i18n.ts',
|
||||||
|
entry: './src/boot.ts',
|
||||||
},
|
},
|
||||||
external: externalPackages.map(p => p.match),
|
external: externalPackages.map(p => p.match),
|
||||||
|
preserveEntrySignatures: 'allow-extension',
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
vue: ['vue'],
|
vue: ['vue'],
|
||||||
|
// dependencies of i18n.ts
|
||||||
|
'config': ['@@/js/config.js'],
|
||||||
},
|
},
|
||||||
chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js',
|
entryFileNames: 'scripts/[hash:8].js',
|
||||||
assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]',
|
chunkFileNames: 'scripts/[hash:8].js',
|
||||||
|
assetFileNames: 'assets/[hash:8][extname]',
|
||||||
paths(id) {
|
paths(id) {
|
||||||
for (const p of externalPackages) {
|
for (const p of externalPackages) {
|
||||||
if (p.match.test(id)) {
|
if (p.match.test(id)) {
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Locale } from '../../../locales/index.js';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
||||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||||
|
@ -17,14 +15,8 @@ export const apiUrl = location.origin + '/api';
|
||||||
export const wsOrigin = location.origin;
|
export const wsOrigin = location.origin;
|
||||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||||
export const langs = _LANGS_;
|
export const langs = _LANGS_;
|
||||||
const preParseLocale = localStorage.getItem('locale');
|
|
||||||
export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null;
|
|
||||||
export const version = _VERSION_;
|
export const version = _VERSION_;
|
||||||
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
|
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
|
||||||
export const ui = localStorage.getItem('ui');
|
export const ui = localStorage.getItem('ui');
|
||||||
export const debug = localStorage.getItem('debug') === 'true';
|
export const debug = localStorage.getItem('debug') === 'true';
|
||||||
export const isSafeMode = localStorage.getItem('isSafeMode') === 'true';
|
export const isSafeMode = localStorage.getItem('isSafeMode') === 'true';
|
||||||
|
|
||||||
export function updateLocale(newLocale: Locale): void {
|
|
||||||
locale = newLocale;
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedSt
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
type Tsx<T extends ILocale> = {
|
type Tsx<T extends ILocale> = {
|
||||||
readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
|
// `string extends T[K] ? never : K` part removes non-parameterized string keys from Tsx type.
|
||||||
|
readonly [K in keyof T as string extends T[K] ? never : K]: T[K] extends ParameterizedString<infer P>
|
||||||
? (arg: { readonly [_ in P]: string | number }) => string
|
? (arg: { readonly [_ in P]: string | number }) => string
|
||||||
// @ts-expect-error -- 証明省略
|
// @ts-expect-error -- 証明省略
|
||||||
: Tsx<T[K]>;
|
: Tsx<T[K]>;
|
||||||
|
@ -39,11 +40,7 @@ export class I18n<T extends ILocale> {
|
||||||
private devMode: boolean;
|
private devMode: boolean;
|
||||||
|
|
||||||
constructor(public locale: T, devMode = false) {
|
constructor(public locale: T, devMode = false) {
|
||||||
// 場合によってはバージョンアップ前の翻訳データを参照した結果存在しないプロパティにアクセスしてクライアントが起動できなくなることがある問題の応急処置として非devモードでもプロキシする
|
this.devMode = devMode;
|
||||||
// TODO: https://github.com/misskey-dev/misskey/issues/14453 が実装されたらそのようなことは発生し得なくなるため消す
|
|
||||||
const oukyuusyoti = true;
|
|
||||||
|
|
||||||
this.devMode = devMode || oukyuusyoti;
|
|
||||||
|
|
||||||
//#region BIND
|
//#region BIND
|
||||||
this.t = this.t.bind(this);
|
this.t = this.t.bind(this);
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lang, version } from '@@/js/config.js';
|
||||||
|
import type { Locale } from '../../../locales/index.js';
|
||||||
|
|
||||||
|
// ここはビルド時に const locale = JSON.parse("...") みたいな感じで置き換えられるので top-level await は消える
|
||||||
|
export let locale: Locale = await window.fetch(`/assets/locales/${lang}.${version}.json`).then(r => r.json(), () => null);
|
||||||
|
|
||||||
|
export function updateLocale(newLocale: Locale): void {
|
||||||
|
locale = newLocale;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Locale } from '../../../locales/index.js';
|
||||||
|
|
||||||
|
type BootLoaderLocaleBody = Locale['_bootErrors'] & { reload: Locale['reload'] };
|
||||||
|
|
||||||
|
export function storeBootloaderErrors(locale: BootLoaderLocaleBody) {
|
||||||
|
localStorage.setItem('bootloaderLocales', JSON.stringify(locale));
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
/storybook-static
|
/storybook-static
|
||||||
|
/build/
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { type Preview, setup } from '@storybook/vue3';
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||||
import { userDetailed } from './fakes.js';
|
import { userDetailed } from './fakes.js';
|
||||||
import locale from './locale.js';
|
|
||||||
import { commonHandlers, onUnhandledRequest } from './mocks.js';
|
import { commonHandlers, onUnhandledRequest } from './mocks.js';
|
||||||
import themes from './themes.js';
|
import themes from './themes.js';
|
||||||
import '../src/style.scss';
|
import '../src/style.scss';
|
||||||
|
@ -55,7 +54,6 @@ function initLocalStorage() {
|
||||||
...userDetailed(),
|
...userDetailed(),
|
||||||
policies: {},
|
policies: {},
|
||||||
}));
|
}));
|
||||||
localStorage.setItem('locale', JSON.stringify(locale));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import url from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execa } from 'execa';
|
||||||
|
import locales from '../../locales/index.js';
|
||||||
|
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
|
||||||
|
import { createLogger } from '../frontend-builder/logger';
|
||||||
|
|
||||||
|
// requires node 21 or later
|
||||||
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = __dirname + '/../../built/_frontend_vite_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function viteBuild() {
|
||||||
|
await execa('vite', ['build'], {
|
||||||
|
cwd: __dirname,
|
||||||
|
stdout: process.stdout,
|
||||||
|
stderr: process.stderr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function buildAllLocale() {
|
||||||
|
const logger = createLogger()
|
||||||
|
const inliner = await LocaleInliner.create({
|
||||||
|
outputDir,
|
||||||
|
logger,
|
||||||
|
scriptsDir: 'scripts',
|
||||||
|
i18nFile: 'src/i18n.ts',
|
||||||
|
})
|
||||||
|
|
||||||
|
await inliner.loadFiles();
|
||||||
|
|
||||||
|
inliner.collectsModifications();
|
||||||
|
|
||||||
|
await inliner.saveAllLocales(locales);
|
||||||
|
|
||||||
|
if (logger.errorCount > 0) {
|
||||||
|
throw new Error(`Build failed with ${logger.errorCount} errors and ${logger.warningCount} warnings.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
await fs.rm(outputDir, { recursive: true, force: true });
|
||||||
|
await viteBuild();
|
||||||
|
await buildAllLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
await build();
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite",
|
"watch": "vite",
|
||||||
"build": "vite build",
|
"build": "tsx build.ts",
|
||||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||||
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
|
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
|
||||||
|
@ -25,7 +25,8 @@
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.2.0",
|
"@rollup/pluginutils": "5.2.0",
|
||||||
"@sentry/vue": "10.0.0",
|
"@sentry/vue": "10.0.0",
|
||||||
"@syuilo/aiscript": "0.19.0",
|
"@syuilo/aiscript": "1.0.0",
|
||||||
|
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||||
"@twemoji/parser": "16.0.0",
|
"@twemoji/parser": "16.0.0",
|
||||||
"@vitejs/plugin-vue": "6.0.1",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vue/compiler-sfc": "3.5.18",
|
"@vue/compiler-sfc": "3.5.18",
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
|
"execa": "9.6.0",
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
"icons-subsetter": "workspace:*",
|
"icons-subsetter": "workspace:*",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
|
@ -136,6 +138,7 @@
|
||||||
"start-server-and-test": "2.0.12",
|
"start-server-and-test": "2.0.12",
|
||||||
"storybook": "9.1.0",
|
"storybook": "9.1.0",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
|
"tsx": "4.20.3",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.2.4",
|
||||||
"vitest-fetch-mock": "0.4.5",
|
"vitest-fetch-mock": "0.4.5",
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
|
|
||||||
import { computed, watch, version as vueVersion } from 'vue';
|
import { computed, watch, version as vueVersion } from 'vue';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { version, lang, updateLocale, locale, apiUrl, isSafeMode } from '@@/js/config.js';
|
import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js';
|
||||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||||
|
import { storeBootloaderErrors } from '@@/js/store-boot-errors';
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import widgets from '@/widgets/index.js';
|
import widgets from '@/widgets/index.js';
|
||||||
import directives from '@/directives/index.js';
|
import directives from '@/directives/index.js';
|
||||||
|
@ -79,25 +80,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
const localeVersion = miLocalStorage.getItem('localeVersion');
|
storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
||||||
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
|
|
||||||
|
|
||||||
async function fetchAndUpdateLocale({ useCache } = { useCache: true }) {
|
|
||||||
const fetchOptions: RequestInit | undefined = useCache ? undefined : { cache: 'no-store' };
|
|
||||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`, fetchOptions);
|
|
||||||
if (res.status === 200) {
|
|
||||||
const newLocale = await res.text();
|
|
||||||
const parsedNewLocale = JSON.parse(newLocale);
|
|
||||||
miLocalStorage.setItem('locale', newLocale);
|
|
||||||
miLocalStorage.setItem('localeVersion', version);
|
|
||||||
updateLocale(parsedNewLocale);
|
|
||||||
updateI18n(parsedNewLocale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localeOutdated) {
|
|
||||||
fetchAndUpdateLocale();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
import.meta.hot.on('locale-update', async (updatedLang: string) => {
|
import.meta.hot.on('locale-update', async (updatedLang: string) => {
|
||||||
|
@ -106,7 +89,8 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
window.setTimeout(resolve, 500);
|
window.setTimeout(resolve, 500);
|
||||||
});
|
});
|
||||||
await fetchAndUpdateLocale({ useCache: false });
|
// fetch with cache: 'no-store' to ensure the latest locale is fetched
|
||||||
|
await window.fetch(`/assets/locales/${lang}.${version}.json`, { cache: 'no-store' }).then(async res => res.status === 200 && await res.text());
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -585,6 +585,14 @@ defineExpose({
|
||||||
grid-template-columns: var(--columns);
|
grid-template-columns: var(--columns);
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
|
||||||
|
> .config {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
> .item {
|
> .item {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo>
|
<MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo>
|
||||||
|
|
||||||
<div v-if="isPlugin" class="_gaps_s">
|
<div v-if="extension.type === 'plugin'" class="_gaps_s">
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||||
<template #label>{{ i18n.ts.metadata }}</template>
|
<template #label>{{ i18n.ts.metadata }}</template>
|
||||||
|
@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkCode :code="extension.raw"/>
|
<MkCode :code="extension.raw"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isTheme" class="_gaps_s">
|
<div v-else-if="extension.type === 'theme'" class="_gaps_s">
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||||
<template #label>{{ i18n.ts.metadata }}</template>
|
<template #label>{{ i18n.ts.metadata }}</template>
|
||||||
|
@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
<MkKeyValue>
|
<MkKeyValue>
|
||||||
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
||||||
<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
|
<template #value>{{ { light: i18n.ts.light, dark: i18n.ts.dark, none: i18n.ts.none }[extension.meta.base ?? 'none'] }}</template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
|
@ -275,21 +275,19 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
// plugin
|
||||||
// https://github.com/aiscript-dev/aiscript/issues/937
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
//// plugin
|
if (noteViewInterruptors.length > 0) {
|
||||||
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
//if (noteViewInterruptors.length > 0) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
// let result: Misskey.entities.Note | null = deepClone(note);
|
try {
|
||||||
// for (const interruptor of noteViewInterruptors) {
|
result = interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
// try {
|
} catch (err) {
|
||||||
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
console.error(err);
|
||||||
// } catch (err) {
|
}
|
||||||
// console.error(err);
|
}
|
||||||
// }
|
note = result as Misskey.entities.Note;
|
||||||
// }
|
}
|
||||||
// note = result as Misskey.entities.Note;
|
|
||||||
//}
|
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note) ?? note;
|
const appearNote = getAppearNote(note) ?? note;
|
||||||
|
|
|
@ -297,20 +297,19 @@ const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
// plugin
|
||||||
//// plugin
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
if (noteViewInterruptors.length > 0) {
|
||||||
//if (noteViewInterruptors.length > 0) {
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
// let result: Misskey.entities.Note | null = deepClone(note);
|
for (const interruptor of noteViewInterruptors) {
|
||||||
// for (const interruptor of noteViewInterruptors) {
|
try {
|
||||||
// try {
|
result = interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
} catch (err) {
|
||||||
// } catch (err) {
|
console.error(err);
|
||||||
// console.error(err);
|
}
|
||||||
// }
|
}
|
||||||
// }
|
note = result as Misskey.entities.Note;
|
||||||
// note = result as Misskey.entities.Note;
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
|
|
|
@ -907,6 +907,11 @@ async function post(ev?: MouseEvent) {
|
||||||
|
|
||||||
if (uploader.items.value.some(x => x.uploaded == null)) {
|
if (uploader.items.value.some(x => x.uploaded == null)) {
|
||||||
await uploadFiles();
|
await uploadFiles();
|
||||||
|
|
||||||
|
// アップロード失敗したものがあったら中止
|
||||||
|
if (uploader.items.value.some(x => x.uploaded == null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let postData = {
|
let postData = {
|
||||||
|
|
|
@ -52,6 +52,7 @@ export type PageHeaderProps = {
|
||||||
actions?: PageHeaderItem[] | null;
|
actions?: PageHeaderItem[] | null;
|
||||||
thin?: boolean;
|
thin?: boolean;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
|
canOmitTitle?: boolean;
|
||||||
displayMyAvatar?: boolean;
|
displayMyAvatar?: boolean;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -77,7 +78,7 @@ const emit = defineEmits<{
|
||||||
const injectedPageMetadata = inject(DI.pageMetadata, ref(null));
|
const injectedPageMetadata = inject(DI.pageMetadata, ref(null));
|
||||||
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
|
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
|
||||||
|
|
||||||
const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle);
|
const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle || (props.canOmitTitle && props.tabs.length > 0));
|
||||||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
||||||
|
|
||||||
const el = useTemplateRef('el');
|
const el = useTemplateRef('el');
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import { I18n } from '@@/js/i18n.js';
|
import { I18n } from '@@/js/i18n.js';
|
||||||
|
import { locale } from '@@/js/locale.js';
|
||||||
import type { Locale } from '../../../locales/index.js';
|
import type { Locale } from '../../../locales/index.js';
|
||||||
import { locale } from '@@/js/config.js';
|
|
||||||
|
|
||||||
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
||||||
|
|
||||||
|
// test 以外では使わないこと。インライン化されてるのでだいたい意味がない
|
||||||
export function updateI18n(newLocale: Locale) {
|
export function updateI18n(newLocale: Locale) {
|
||||||
i18n.locale = newLocale;
|
i18n.locale = newLocale;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,7 @@ export type Keys = (
|
||||||
'fontSize' |
|
'fontSize' |
|
||||||
'ui' |
|
'ui' |
|
||||||
'ui_temp' |
|
'ui_temp' |
|
||||||
'locale' |
|
'bootloaderLocales' |
|
||||||
'localeVersion' |
|
|
||||||
'theme' |
|
'theme' |
|
||||||
'themeId' |
|
'themeId' |
|
||||||
'customCss' |
|
'customCss' |
|
||||||
|
|
|
@ -286,6 +286,9 @@ const patronsWithIcon = [{
|
||||||
}, {
|
}, {
|
||||||
name: '井上千二十四',
|
name: '井上千二十四',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/193afa1f039b4c339866039c3dcd74bf.jpg',
|
icon: 'https://assets.misskey-hub.net/patrons/193afa1f039b4c339866039c3dcd74bf.jpg',
|
||||||
|
}, {
|
||||||
|
name: 'NigN',
|
||||||
|
icon: 'https://assets.misskey-hub.net/patrons/1ccaef8e73ec4a50b59ff7cd688ceb84.jpg',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const patrons = [
|
const patrons = [
|
||||||
|
@ -399,6 +402,7 @@ const patrons = [
|
||||||
'みりめい',
|
'みりめい',
|
||||||
'東雲 琥珀',
|
'東雲 琥珀',
|
||||||
'ほとラズ',
|
'ほとラズ',
|
||||||
|
'スズカケン',
|
||||||
];
|
];
|
||||||
|
|
||||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||||
|
|
|
@ -88,7 +88,7 @@ let choices = [
|
||||||
]
|
]
|
||||||
|
|
||||||
// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
|
// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
|
||||||
let random = Math:gen_rng(\`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
|
let random = Math:gen_rng(\`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`, { algorithm: 'rc4_legacy' })
|
||||||
|
|
||||||
// ランダムに選択肢を選ぶ
|
// ランダムに選択肢を選ぶ
|
||||||
let chosen = choices[random(0, (choices.len - 1))]
|
let chosen = choices[random(0, (choices.len - 1))]
|
||||||
|
@ -127,7 +127,7 @@ var results = []
|
||||||
// どれだけ巻き戻しているか
|
// どれだけ巻き戻しているか
|
||||||
var cursor = 0
|
var cursor = 0
|
||||||
|
|
||||||
@do() {
|
@main() {
|
||||||
if (cursor != 0) {
|
if (cursor != 0) {
|
||||||
results = results.slice(0, (cursor + 1))
|
results = results.slice(0, (cursor + 1))
|
||||||
cursor = 0
|
cursor = 0
|
||||||
|
@ -175,7 +175,7 @@ var cursor = 0
|
||||||
onClick: forward
|
onClick: forward
|
||||||
}, {
|
}, {
|
||||||
text: "引き直す"
|
text: "引き直す"
|
||||||
onClick: do
|
onClick: main
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
Ui:C:postFormButton({
|
Ui:C:postFormButton({
|
||||||
|
@ -191,7 +191,7 @@ var cursor = 0
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
do()
|
main()
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
|
const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION}
|
||||||
|
|
|
@ -63,11 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onDeactivated, onUnmounted, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
|
import { computed, onDeactivated, onUnmounted, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { Interpreter, Parser, values } from '@syuilo/aiscript';
|
|
||||||
import { url } from '@@/js/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
|
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import type { Interpreter } from '@syuilo/aiscript';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -180,8 +180,6 @@ async function unlike() {
|
||||||
|
|
||||||
watch(() => props.id, fetchFlash, { immediate: true });
|
watch(() => props.id, fetchFlash, { immediate: true });
|
||||||
|
|
||||||
const parser = new Parser();
|
|
||||||
|
|
||||||
const started = ref(false);
|
const started = ref(false);
|
||||||
const aiscript = shallowRef<Interpreter | null>(null);
|
const aiscript = shallowRef<Interpreter | null>(null);
|
||||||
const root = ref<AsUiRoot>();
|
const root = ref<AsUiRoot>();
|
||||||
|
@ -196,6 +194,12 @@ async function run() {
|
||||||
if (aiscript.value) aiscript.value.abort();
|
if (aiscript.value) aiscript.value.abort();
|
||||||
if (!flash.value) return;
|
if (!flash.value) return;
|
||||||
|
|
||||||
|
const isLegacy = !flash.value.script.replaceAll(' ', '').startsWith('///@1.0.0');
|
||||||
|
|
||||||
|
const { Interpreter, Parser, values } = isLegacy ? await import('@syuilo/aiscript-0-19-0') : await import('@syuilo/aiscript');
|
||||||
|
|
||||||
|
const parser = new Parser();
|
||||||
|
|
||||||
components.value = [];
|
components.value = [];
|
||||||
|
|
||||||
aiscript.value = new Interpreter({
|
aiscript.value = new Interpreter({
|
||||||
|
|
|
@ -886,8 +886,6 @@ const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
miLocalStorage.setItem('lang', lang.value as string);
|
miLocalStorage.setItem('lang', lang.value as string);
|
||||||
miLocalStorage.removeItem('locale');
|
|
||||||
miLocalStorage.removeItem('localeVersion');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(fontSize, () => {
|
watch(fontSize, () => {
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :swipable="true" :displayMyAvatar="true">
|
<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :swipable="true" :displayMyAvatar="true" :canOmitTitle="true">
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||||
<MkTip v-if="isBasicTimeline(src)" :k="`tl.${src}`" style="margin-bottom: var(--MI-margin);">
|
<MkTip v-if="isBasicTimeline(src)" :k="`tl.${src}`" style="margin-bottom: var(--MI-margin);">
|
||||||
{{ i18n.ts._timelineDescription[src] }}
|
{{ i18n.ts._timelineDescription[src] }}
|
||||||
|
@ -45,8 +45,6 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
provide('shouldOmitHeaderTitle', true);
|
|
||||||
|
|
||||||
const tlComponent = useTemplateRef('tlComponent');
|
const tlComponent = useTemplateRef('tlComponent');
|
||||||
|
|
||||||
type TimelinePageSrc = BasicTimelineType | `list:${string}`;
|
type TimelinePageSrc = BasicTimelineType | `list:${string}`;
|
||||||
|
@ -105,9 +103,11 @@ const withSensitive = computed<boolean>({
|
||||||
set: (x) => saveTlFilter('withSensitive', x),
|
set: (x) => saveTlFilter('withSensitive', x),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showFixedPostForm = prefer.model('showFixedPostForm');
|
||||||
|
|
||||||
async function chooseList(ev: MouseEvent): Promise<void> {
|
async function chooseList(ev: MouseEvent): Promise<void> {
|
||||||
const lists = await userListsCache.fetch();
|
const lists = await userListsCache.fetch();
|
||||||
const items: MenuItem[] = [
|
const items: (MenuItem | undefined)[] = [
|
||||||
...lists.map(list => ({
|
...lists.map(list => ({
|
||||||
type: 'link' as const,
|
type: 'link' as const,
|
||||||
text: list.name,
|
text: list.name,
|
||||||
|
@ -121,12 +121,12 @@ async function chooseList(ev: MouseEvent): Promise<void> {
|
||||||
to: '/my/lists',
|
to: '/my/lists',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||||
const antennas = await antennasCache.fetch();
|
const antennas = await antennasCache.fetch();
|
||||||
const items: MenuItem[] = [
|
const items: (MenuItem | undefined)[] = [
|
||||||
...antennas.map(antenna => ({
|
...antennas.map(antenna => ({
|
||||||
type: 'link' as const,
|
type: 'link' as const,
|
||||||
text: antenna.name,
|
text: antenna.name,
|
||||||
|
@ -141,12 +141,12 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||||
to: '/my/antennas',
|
to: '/my/antennas',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||||
const channels = await favoritedChannelsCache.fetch();
|
const channels = await favoritedChannelsCache.fetch();
|
||||||
const items: MenuItem[] = [
|
const items: (MenuItem | undefined)[] = [
|
||||||
...channels.map(channel => {
|
...channels.map(channel => {
|
||||||
const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
|
const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
|
||||||
const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt);
|
const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt);
|
||||||
|
@ -166,7 +166,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||||
to: '/channels',
|
to: '/channels',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSrc(newSrc: TimelinePageSrc): void {
|
function saveSrc(newSrc: TimelinePageSrc): void {
|
||||||
|
@ -190,19 +190,6 @@ function saveTlFilter(key: keyof typeof store.s.tl.filter, newValue: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function timetravel(): Promise<void> {
|
|
||||||
const { canceled, result: date } = await os.inputDate({
|
|
||||||
title: i18n.ts.date,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
tlComponent.value.timetravel(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus(): void {
|
|
||||||
tlComponent.value.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchTlIfNeeded() {
|
function switchTlIfNeeded() {
|
||||||
if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
|
if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
|
||||||
src.value = availableBasicTimelines()[0];
|
src.value = availableBasicTimelines()[0];
|
||||||
|
@ -217,49 +204,54 @@ onActivated(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerActions = computed(() => {
|
const headerActions = computed(() => {
|
||||||
const tmp = [
|
const items = [{
|
||||||
{
|
icon: 'ti ti-dots',
|
||||||
icon: 'ti ti-dots',
|
text: i18n.ts.options,
|
||||||
text: i18n.ts.options,
|
handler: (ev) => {
|
||||||
handler: (ev) => {
|
const menuItems: MenuItem[] = [];
|
||||||
const menuItems: MenuItem[] = [];
|
|
||||||
|
|
||||||
|
menuItems.push({
|
||||||
|
type: 'switch',
|
||||||
|
icon: 'ti ti-repeat',
|
||||||
|
text: i18n.ts.showRenotes,
|
||||||
|
ref: withRenotes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-repeat',
|
icon: 'ti ti-messages',
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||||
ref: withRenotes,
|
ref: withReplies,
|
||||||
|
disabled: onlyFiles,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
|
menuItems.push({
|
||||||
menuItems.push({
|
type: 'switch',
|
||||||
type: 'switch',
|
icon: 'ti ti-eye-exclamation',
|
||||||
icon: 'ti ti-messages',
|
text: i18n.ts.withSensitive,
|
||||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
ref: withSensitive,
|
||||||
ref: withReplies,
|
}, {
|
||||||
disabled: onlyFiles,
|
type: 'switch',
|
||||||
});
|
icon: 'ti ti-photo',
|
||||||
}
|
text: i18n.ts.fileAttachedOnly,
|
||||||
|
ref: onlyFiles,
|
||||||
|
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
|
||||||
|
}, {
|
||||||
|
type: 'divider',
|
||||||
|
}, {
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.showFixedPostForm,
|
||||||
|
ref: showFixedPostForm,
|
||||||
|
});
|
||||||
|
|
||||||
menuItems.push({
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
type: 'switch',
|
|
||||||
icon: 'ti ti-eye-exclamation',
|
|
||||||
text: i18n.ts.withSensitive,
|
|
||||||
ref: withSensitive,
|
|
||||||
}, {
|
|
||||||
type: 'switch',
|
|
||||||
icon: 'ti ti-photo',
|
|
||||||
text: i18n.ts.fileAttachedOnly,
|
|
||||||
ref: onlyFiles,
|
|
||||||
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
|
|
||||||
});
|
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
}];
|
||||||
|
|
||||||
if (deviceKind === 'desktop') {
|
if (deviceKind === 'desktop') {
|
||||||
tmp.unshift({
|
items.unshift({
|
||||||
icon: 'ti ti-refresh',
|
icon: 'ti ti-refresh',
|
||||||
text: i18n.ts.reload,
|
text: i18n.ts.reload,
|
||||||
handler: (ev: Event) => {
|
handler: (ev: Event) => {
|
||||||
|
@ -267,7 +259,8 @@ const headerActions = computed(() => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return tmp;
|
|
||||||
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({
|
const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({
|
||||||
|
|
|
@ -7,15 +7,15 @@ import { ref, defineAsyncComponent } from 'vue';
|
||||||
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { isSafeMode } from '@@/js/config.js';
|
import { isSafeMode } from '@@/js/config.js';
|
||||||
import { genId } from '@/utility/id.js';
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { FormWithDefault } from '@/utility/form.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import type { FormWithDefault } from '@/utility/form.js';
|
|
||||||
|
|
||||||
export type Plugin = {
|
export type Plugin = {
|
||||||
installId: string;
|
installId: string;
|
||||||
|
@ -394,8 +394,8 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
|
||||||
'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
|
'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
|
||||||
utils.assertFunction(handler);
|
utils.assertFunction(handler);
|
||||||
addPluginHandler(id, 'note_view_interruptor', {
|
addPluginHandler(id, 'note_view_interruptor', {
|
||||||
handler: withContext(ctx => async (note) => {
|
handler: withContext(ctx => (note) => {
|
||||||
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
|
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(note)]));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -13,8 +13,6 @@ export async function clearCache() {
|
||||||
os.waiting();
|
os.waiting();
|
||||||
miLocalStorage.removeItem('instance');
|
miLocalStorage.removeItem('instance');
|
||||||
miLocalStorage.removeItem('instanceCachedAt');
|
miLocalStorage.removeItem('instanceCachedAt');
|
||||||
miLocalStorage.removeItem('locale');
|
|
||||||
miLocalStorage.removeItem('localeVersion');
|
|
||||||
miLocalStorage.removeItem('theme');
|
miLocalStorage.removeItem('theme');
|
||||||
miLocalStorage.removeItem('emojis');
|
miLocalStorage.removeItem('emojis');
|
||||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
},
|
},
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"include": [
|
"include": [
|
||||||
|
"./build.ts",
|
||||||
"./lib/**/*.ts",
|
"./lib/**/*.ts",
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
"./src/**/*.vue",
|
"./src/**/*.vue",
|
||||||
|
|
|
@ -14,6 +14,7 @@ import pluginJson5 from './vite.json5.js';
|
||||||
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
|
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
|
||||||
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
|
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
|
||||||
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
|
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
|
||||||
|
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js';
|
||||||
|
|
||||||
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
|
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
|
||||||
const host = url ? (new URL(url)).hostname : undefined;
|
const host = url ? (new URL(url)).hostname : undefined;
|
||||||
|
@ -111,6 +112,7 @@ export function getConfig(): UserConfig {
|
||||||
pluginWatchLocales(),
|
pluginWatchLocales(),
|
||||||
...searchIndexes.map(options => pluginCreateSearchIndex(options)),
|
...searchIndexes.map(options => pluginCreateSearchIndex(options)),
|
||||||
pluginVue(),
|
pluginVue(),
|
||||||
|
pluginRemoveUnrefI18n(),
|
||||||
pluginUnwindCssModuleClassName(),
|
pluginUnwindCssModuleClassName(),
|
||||||
pluginJson5(),
|
pluginJson5(),
|
||||||
...process.env.NODE_ENV === 'production'
|
...process.env.NODE_ENV === 'production'
|
||||||
|
@ -174,16 +176,21 @@ export function getConfig(): UserConfig {
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
app: './src/_boot_.ts',
|
i18n: './src/i18n.ts',
|
||||||
|
entry: './src/_boot_.ts',
|
||||||
},
|
},
|
||||||
external: externalPackages.map(p => p.match),
|
external: externalPackages.map(p => p.match),
|
||||||
|
preserveEntrySignatures: 'allow-extension',
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
vue: ['vue'],
|
vue: ['vue'],
|
||||||
photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'],
|
photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'],
|
||||||
|
// dependencies of i18n.ts
|
||||||
|
'config': ['@@/js/config.js'],
|
||||||
},
|
},
|
||||||
chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js',
|
entryFileNames: 'scripts/[hash:8].js',
|
||||||
assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]',
|
chunkFileNames: 'scripts/[hash:8].js',
|
||||||
|
assetFileNames: 'assets/[hash:8][extname]',
|
||||||
paths(id) {
|
paths(id) {
|
||||||
for (const p of externalPackages) {
|
for (const p of externalPackages) {
|
||||||
if (p.match.test(id)) {
|
if (p.match.test(id)) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2025.8.0-alpha.4",
|
"version": "2025.8.0-alpha.6",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
|
@ -11887,7 +11887,8 @@ export interface operations {
|
||||||
name: string;
|
name: string;
|
||||||
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
|
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
|
||||||
url: string;
|
url: string;
|
||||||
secret: string;
|
/** @default */
|
||||||
|
secret?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -12231,7 +12232,8 @@ export interface operations {
|
||||||
name: string;
|
name: string;
|
||||||
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
|
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
|
||||||
url: string;
|
url: string;
|
||||||
secret: string;
|
/** @default */
|
||||||
|
secret?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -731,8 +731,11 @@ importers:
|
||||||
specifier: 10.0.0
|
specifier: 10.0.0
|
||||||
version: 10.0.0(vue@3.5.18(typescript@5.9.2))
|
version: 10.0.0(vue@3.5.18(typescript@5.9.2))
|
||||||
'@syuilo/aiscript':
|
'@syuilo/aiscript':
|
||||||
specifier: 0.19.0
|
specifier: 1.0.0
|
||||||
version: 0.19.0
|
version: 1.0.0
|
||||||
|
'@syuilo/aiscript-0-19-0':
|
||||||
|
specifier: npm:@syuilo/aiscript@^0.19.0
|
||||||
|
version: '@syuilo/aiscript@0.19.0'
|
||||||
'@twemoji/parser':
|
'@twemoji/parser':
|
||||||
specifier: 16.0.0
|
specifier: 16.0.0
|
||||||
version: 16.0.0
|
version: 16.0.0
|
||||||
|
@ -793,6 +796,9 @@ importers:
|
||||||
eventemitter3:
|
eventemitter3:
|
||||||
specifier: 5.0.1
|
specifier: 5.0.1
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
|
execa:
|
||||||
|
specifier: 9.6.0
|
||||||
|
version: 9.6.0
|
||||||
frontend-shared:
|
frontend-shared:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../frontend-shared
|
version: link:../frontend-shared
|
||||||
|
@ -1058,6 +1064,9 @@ importers:
|
||||||
storybook-addon-misskey-theme:
|
storybook-addon-misskey-theme:
|
||||||
specifier: github:misskey-dev/storybook-addon-misskey-theme
|
specifier: github:misskey-dev/storybook-addon-misskey-theme
|
||||||
version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(1169897c5e200f76aeb2e7696f1450e0)
|
version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(1169897c5e200f76aeb2e7696f1450e0)
|
||||||
|
tsx:
|
||||||
|
specifier: 4.20.3
|
||||||
|
version: 4.20.3
|
||||||
vite-plugin-turbosnap:
|
vite-plugin-turbosnap:
|
||||||
specifier: 1.0.3
|
specifier: 1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
@ -1077,6 +1086,37 @@ importers:
|
||||||
specifier: 3.0.5
|
specifier: 3.0.5
|
||||||
version: 3.0.5(typescript@5.9.2)
|
version: 3.0.5(typescript@5.9.2)
|
||||||
|
|
||||||
|
packages/frontend-builder:
|
||||||
|
dependencies:
|
||||||
|
estree-walker:
|
||||||
|
specifier: 3.0.3
|
||||||
|
version: 3.0.3
|
||||||
|
magic-string:
|
||||||
|
specifier: 0.30.17
|
||||||
|
version: 0.30.17
|
||||||
|
vite:
|
||||||
|
specifier: 7.0.6
|
||||||
|
version: 7.0.6(@types/node@22.17.0)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)
|
||||||
|
devDependencies:
|
||||||
|
'@types/estree':
|
||||||
|
specifier: 1.0.8
|
||||||
|
version: 1.0.8
|
||||||
|
'@types/node':
|
||||||
|
specifier: 22.17.0
|
||||||
|
version: 22.17.0
|
||||||
|
'@typescript-eslint/eslint-plugin':
|
||||||
|
specifier: 8.38.0
|
||||||
|
version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0)(typescript@5.9.2))(eslint@9.31.0)(typescript@5.9.2)
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
specifier: 8.38.0
|
||||||
|
version: 8.38.0(eslint@9.31.0)(typescript@5.9.2)
|
||||||
|
rollup:
|
||||||
|
specifier: 4.46.2
|
||||||
|
version: 4.46.2
|
||||||
|
typescript:
|
||||||
|
specifier: 5.9.2
|
||||||
|
version: 5.9.2
|
||||||
|
|
||||||
packages/frontend-embed:
|
packages/frontend-embed:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@discordapp/twemoji':
|
'@discordapp/twemoji':
|
||||||
|
@ -1233,6 +1273,9 @@ importers:
|
||||||
start-server-and-test:
|
start-server-and-test:
|
||||||
specifier: 2.0.12
|
specifier: 2.0.12
|
||||||
version: 2.0.12
|
version: 2.0.12
|
||||||
|
tsx:
|
||||||
|
specifier: 4.20.3
|
||||||
|
version: 4.20.3
|
||||||
vite-plugin-turbosnap:
|
vite-plugin-turbosnap:
|
||||||
specifier: 1.0.3
|
specifier: 1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
@ -4302,6 +4345,9 @@ packages:
|
||||||
'@syuilo/aiscript@0.19.0':
|
'@syuilo/aiscript@0.19.0':
|
||||||
resolution: {integrity: sha512-ZWG4s1m6RrFjE7NeIMaxFz769YO1jW5ReTrOROrEO4IHheOrjxxJ/Ffe2TUNqX9/XxDloMwfWplKhfSzx8LGMA==}
|
resolution: {integrity: sha512-ZWG4s1m6RrFjE7NeIMaxFz769YO1jW5ReTrOROrEO4IHheOrjxxJ/Ffe2TUNqX9/XxDloMwfWplKhfSzx8LGMA==}
|
||||||
|
|
||||||
|
'@syuilo/aiscript@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m+Dxx0g2pDI198OCj/OJgiJnE4ajlbOFAMyh84FmbY1S8ss/MHytxY82dCnMZj5WVt7VE7a1rtW7biuRRfuyaA==}
|
||||||
|
|
||||||
'@szmarczak/http-timer@5.0.1':
|
'@szmarczak/http-timer@5.0.1':
|
||||||
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
|
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
@ -6765,14 +6811,6 @@ packages:
|
||||||
fd-slicer@1.1.0:
|
fd-slicer@1.1.0:
|
||||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||||
|
|
||||||
fdir@6.4.4:
|
|
||||||
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
|
|
||||||
peerDependencies:
|
|
||||||
picomatch: ^3 || ^4
|
|
||||||
peerDependenciesMeta:
|
|
||||||
picomatch:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
fdir@6.4.6:
|
fdir@6.4.6:
|
||||||
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
|
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -14637,7 +14675,7 @@ snapshots:
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
@ -14735,6 +14773,12 @@ snapshots:
|
||||||
stringz: 2.1.0
|
stringz: 2.1.0
|
||||||
uuid: 9.0.1
|
uuid: 9.0.1
|
||||||
|
|
||||||
|
'@syuilo/aiscript@1.0.0':
|
||||||
|
dependencies:
|
||||||
|
seedrandom: 3.0.5
|
||||||
|
stringz: 2.1.0
|
||||||
|
uuid: 11.1.0
|
||||||
|
|
||||||
'@szmarczak/http-timer@5.0.1':
|
'@szmarczak/http-timer@5.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
|
@ -15353,7 +15397,7 @@ snapshots:
|
||||||
'@typescript-eslint/project-service@8.34.0(typescript@5.8.3)':
|
'@typescript-eslint/project-service@8.34.0(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.38.0
|
||||||
debug: 4.4.1(supports-color@10.0.0)
|
debug: 4.4.1(supports-color@10.0.0)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -15361,8 +15405,8 @@ snapshots:
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.37.0(typescript@5.8.3)':
|
'@typescript-eslint/project-service@8.37.0(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.38.0
|
||||||
debug: 4.4.1(supports-color@10.0.0)
|
debug: 4.4.1(supports-color@10.0.0)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -15792,7 +15836,7 @@ snapshots:
|
||||||
alien-signals: 2.0.6
|
alien-signals: 2.0.6
|
||||||
muggle-string: 0.4.1
|
muggle-string: 0.4.1
|
||||||
path-browserify: 1.0.1
|
path-browserify: 1.0.1
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
|
|
||||||
|
@ -18060,10 +18104,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
pend: 1.2.0
|
pend: 1.2.0
|
||||||
|
|
||||||
fdir@6.4.4(picomatch@4.0.2):
|
|
||||||
optionalDependencies:
|
|
||||||
picomatch: 4.0.2
|
|
||||||
|
|
||||||
fdir@6.4.6(picomatch@4.0.3):
|
fdir@6.4.6(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
@ -22352,8 +22392,8 @@ snapshots:
|
||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.4.4(picomatch@4.0.2)
|
fdir: 6.4.6(picomatch@4.0.3)
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.3
|
||||||
|
|
||||||
tinypool@1.1.1: {}
|
tinypool@1.1.1: {}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ packages:
|
||||||
- packages/backend
|
- packages/backend
|
||||||
- packages/frontend-shared
|
- packages/frontend-shared
|
||||||
- packages/frontend
|
- packages/frontend
|
||||||
|
- packages/frontend-builder
|
||||||
- packages/frontend-embed
|
- packages/frontend-embed
|
||||||
- packages/icons-subsetter
|
- packages/icons-subsetter
|
||||||
- packages/sw
|
- packages/sw
|
||||||
|
|
|
@ -13,6 +13,8 @@ const fs = require('fs');
|
||||||
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
|
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
|
||||||
fs.rmSync(__dirname + '/../packages/frontend-shared/node_modules', { recursive: true, force: true });
|
fs.rmSync(__dirname + '/../packages/frontend-shared/node_modules', { recursive: true, force: true });
|
||||||
|
|
||||||
|
fs.rmSync(__dirname + '/../packages/frontend-builder/node_modules', { recursive: true, force: true });
|
||||||
|
|
||||||
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
|
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
|
||||||
fs.rmSync(__dirname + '/../packages/frontend/node_modules', { recursive: true, force: true });
|
fs.rmSync(__dirname + '/../packages/frontend/node_modules', { recursive: true, force: true });
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue