Merge remote-tracking branch 'upstream/develop' into abuse-report-resolver

This commit is contained in:
Chocolate Pie 2023-07-22 10:32:33 +09:00
commit 4b86cb2aa9
98 changed files with 2545 additions and 2235 deletions

View File

@ -82,6 +82,8 @@ redis:
#pass: example-pass #pass: example-pass
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
# You can specify more ioredis options...
#username: example-username
#redisForPubsub: #redisForPubsub:
# host: localhost # host: localhost
@ -90,6 +92,8 @@ redis:
# #pass: example-pass # #pass: example-pass
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# # You can specify more ioredis options...
# #username: example-username
#redisForJobQueue: #redisForJobQueue:
# host: localhost # host: localhost
@ -98,6 +102,8 @@ redis:
# #pass: example-pass # #pass: example-pass
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -15,18 +15,24 @@
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### General ### General
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました - 通報の即時解決機能の追加
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
## 13.14.1
### General
- 招待機能を改善しました - 招待機能を改善しました
* 過去に発行した招待コードを確認できるようになりました * 過去に発行した招待コードを確認できるようになりました
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
- 通報の即時解決機能の追加 - ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client ### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように - ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように - 画像を動画と同様に簡単に隠せるように
- Enhance: ノートの埋め込みが複数画像と動画を表示されるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外) - オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように - 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
@ -37,9 +43,14 @@
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
- オフライン時の画面にリロードボタンを追加 - オフライン時の画面にリロードボタンを追加
- Renote時に公開範囲のデフォルト設定が適用されるように
- Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加 - Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加
- ロール設定画面でロールIDを確認できるように - ロール設定画面でロールIDを確認できるように
- コンテキストメニュー表示時のパフォーマンスを改善 - コンテキストメニュー表示時のパフォーマンスを改善
- フォロー/フォロワー非公開時の表示を改善
- 本文にMFMが含まれている場合に自動でたたまれる機能が、返信先や引用RNにも適用されるように
- position は対象外になりました
- AiScriptを0.15.0に更新
- Fix: サーバーメトリクスが90度傾いている - Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
@ -56,14 +67,19 @@
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように - nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用 - 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- featuredートのsignedGet回数を減らしました - featuredートのsignedGet回数を減らしました
- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加 - ActivityPubの署名用鍵長を2048bitに変更しパフォーマンスを向上(新規アカウントのみ)
- リモートサーバーのセンシティブなファイルのキャッシュだけを無効化できるオプションを追加
- MeilisearchにIndexするートの範囲を設定できるように - MeilisearchにIndexするートの範囲を設定できるように
- Export notes with file detail - Export notes with file detail
- Add unix socket support - Add unix socket support
- 設定ファイルでioredisの全てのオプションを指定可能に
- Fix: エクスポートしたカスタム絵文字のzipが大きいと読み込めない問題を修正
- Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正 - Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正
- Fix: Remove Meilisearch index when notes are deleted - Fix: Remove Meilisearch index when notes are deleted
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正 - Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正 - Fix: インスタンスのアイコンがbase64の場合の挙動を修正
- Fix: ローカルの `Person` を指す `acct` URI を解析するときのバグを修正しました
- Fix: 無効化されたアンテナが再度有効化されないことがある問題を修正
## 13.13.2 ## 13.13.2

View File

@ -214,30 +214,13 @@ Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run ### Setup & Run
#### Universal #### Setup
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash ```bash
pnpm --filter misskey-js build pnpm --filter misskey-js build
``` ```
##### Run #### Run
```bash ```bash
pnpm --filter frontend storybook-dev pnpm --filter frontend storybook-dev

View File

@ -54,6 +54,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();
@ -78,6 +79,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();

View File

@ -1091,6 +1091,9 @@ usedAt: "Benutzt am"
unused: "Unbenutzt" unused: "Unbenutzt"
used: "Benutzt" used: "Benutzt"
expired: "Abgelaufen" expired: "Abgelaufen"
doYouAgree: "Zustimmen?"
beSureToReadThisAsItIsImportant: "Lies bitte diese wichtige Informationen."
iHaveReadXCarefullyAndAgree: "Ich habe den Text \"{x}\" gelesen und stimme zu."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten." letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@ -1996,6 +1999,7 @@ _deck:
introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!" introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!"
introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen." introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen."
widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu." widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu."
useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden"
_columns: _columns:
main: "Hauptspalte" main: "Hauptspalte"
widgets: "Widgets" widgets: "Widgets"

View File

@ -1091,6 +1091,9 @@ usedAt: "Used at"
unused: "Unused" unused: "Unused"
used: "Used" used: "Used"
expired: "Expired" expired: "Expired"
doYouAgree: "Agree?"
beSureToReadThisAsItIsImportant: "Please read this important information."
iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Your account was successfully created!" accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile." letsStartAccountSetup: "For starters, let's set up your profile."
@ -1996,6 +1999,7 @@ _deck:
introduction: "Create the perfect interface for you by arranging columns freely!" introduction: "Create the perfect interface for you by arranging columns freely!"
introduction2: "Click on the + on the right of the screen to add new colums whenever you want." introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget." widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
_columns: _columns:
main: "Main" main: "Main"
widgets: "Widgets" widgets: "Widgets"

3
locales/index.d.ts vendored
View File

@ -1094,6 +1094,9 @@ export interface Locale {
"unused": string; "unused": string;
"used": string; "used": string;
"expired": string; "expired": string;
"doYouAgree": string;
"beSureToReadThisAsItIsImportant": string;
"iHaveReadXCarefullyAndAgree": string;
"_initialAccountSetting": { "_initialAccountSetting": {
"accountCreated": string; "accountCreated": string;
"letsStartAccountSetup": string; "letsStartAccountSetup": string;

View File

@ -779,10 +779,10 @@ info: "Informazioni"
userInfo: "Informazioni utente" userInfo: "Informazioni utente"
unknown: "Sconosciuto" unknown: "Sconosciuto"
onlineStatus: "Stato di connessione" onlineStatus: "Stato di connessione"
hideOnlineStatus: "Stato invisibile" hideOnlineStatus: "Modalità invisibile"
hideOnlineStatusDescription: "Abilitare l'opzione di stato invisibile può guastare la praticità di singole funzioni, come la ricerca." hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca."
online: "Online" online: "Online"
active: "Attiv@" active: "Attività"
offline: "Offline" offline: "Offline"
notRecommended: "Sconsigliato" notRecommended: "Sconsigliato"
botProtection: "Protezione contro i bot" botProtection: "Protezione contro i bot"
@ -856,8 +856,8 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
classic: "Classico" classic: "Classico"
muteThread: "Silenzia la conversazione" muteThread: "Silenzia la conversazione"
unmuteThread: "Riattiva la conversazione" unmuteThread: "Riattiva la conversazione"
ffVisibility: "Ambito pubblico del collegamento" ffVisibility: "Visibilità delle connessioni"
ffVisibilityDescription: "È possibile impostare la portata pubblica delle informazioni sui propri follower/seguaci." ffVisibilityDescription: "Puoi scegliere a chi mostrare le tue relazioni con altri profili nel fediverso."
continueThread: "Altri thread." continueThread: "Altri thread."
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
incorrectPassword: "La password è errata." incorrectPassword: "La password è errata."
@ -1996,6 +1996,7 @@ _deck:
introduction: "Combinate le colonne per creare la vostra interfaccia!" introduction: "Combinate le colonne per creare la vostra interfaccia!"
introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo."
widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità"
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Riquadri" widgets: "Riquadri"

View File

@ -1042,7 +1042,7 @@ vertical: "縦"
horizontal: "横" horizontal: "横"
position: "位置" position: "位置"
serverRules: "サーバールール" serverRules: "サーバールール"
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。" pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。"
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
continue: "続ける" continue: "続ける"
preservedUsernames: "予約ユーザー名" preservedUsernames: "予約ユーザー名"
@ -1091,6 +1091,9 @@ usedAt: "使用日時"
unused: "未使用" unused: "未使用"
used: "使用済み" used: "使用済み"
expired: "期限切れ" expired: "期限切れ"
doYouAgree: "同意しますか?"
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"

View File

@ -1067,6 +1067,9 @@ branding: "あ"
enableServerMachineStats: "サーバーのマシン情報見せびらかすで" enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。" turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。"
inviteCodeCreated: "招待コード作ったで"
inviteLimitExceeded: "招待コード作りすぎやで。"
createLimitRemaining: "作成できる招待コード: 残り {limit} 個やで"
unused: "つこてへん" unused: "つこてへん"
used: "もうつこてる" used: "もうつこてる"
_initialAccountSetting: _initialAccountSetting:

View File

@ -40,7 +40,7 @@ favorites: "즐겨찾기"
unfavorite: "즐겨찾기에서 제거" unfavorite: "즐겨찾기에서 제거"
favorited: "즐겨찾기에 등록했습니다" favorited: "즐겨찾기에 등록했습니다"
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다" alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다"
cantFavorite: "즐겨찾기에 등록하지 못했습니다." cantFavorite: "즐겨찾기에 등록하지 못했습니다"
pin: "프로필에 고정" pin: "프로필에 고정"
unpin: "프로필에서 고정 해제" unpin: "프로필에서 고정 해제"
copyContent: "내용 복사" copyContent: "내용 복사"
@ -49,11 +49,15 @@ delete: "삭제"
deleteAndEdit: "삭제 후 편집" deleteAndEdit: "삭제 후 편집"
deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다." deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다."
addToList: "리스트에 추가" addToList: "리스트에 추가"
addToAntenna: "안테나에 추가"
sendMessage: "메시지 보내기" sendMessage: "메시지 보내기"
copyRSS: "RSS 복사" copyRSS: "RSS 복사"
copyUsername: "유저명 복사" copyUsername: "유저명 복사"
copyUserId: "유저 ID 복사" copyUserId: "유저 ID 복사"
copyNoteId: "노트 ID 복사" copyNoteId: "노트 ID 복사"
copyFileId: "파일 ID 복사"
copyFolderId: "폴더 ID 복사"
copyProfileUrl: "프로필 URL 복사"
searchUser: "사용자 검색" searchUser: "사용자 검색"
reply: "답글" reply: "답글"
loadMore: "더 보기" loadMore: "더 보기"
@ -104,7 +108,7 @@ renote: "리노트"
unrenote: "리노트 취소" unrenote: "리노트 취소"
renoted: "리노트했습니다" renoted: "리노트했습니다"
cantRenote: "이 게시물은 리노트 할 수 없습니다." cantRenote: "이 게시물은 리노트 할 수 없습니다."
cantReRenote: "리노트를 리노트 할 수 없습니다." cantReRenote: "리노트를 리노트할 수 없습니다."
quote: "인용" quote: "인용"
inChannelRenote: "채널 내 리노트" inChannelRenote: "채널 내 리노트"
inChannelQuote: "채널 내 인용" inChannelQuote: "채널 내 인용"
@ -112,7 +116,7 @@ pinnedNote: "고정해놓은 노트"
pinned: "프로필에 고정" pinned: "프로필에 고정"
you: "당신" you: "당신"
clickToShow: "클릭하여 보기" clickToShow: "클릭하여 보기"
sensitive: "열람주의" sensitive: "열람 주의"
add: "추가" add: "추가"
reaction: "리액션" reaction: "리액션"
reactions: "리액션" reactions: "리액션"
@ -152,10 +156,12 @@ addEmoji: "이모지 추가"
settingGuide: "추천 설정" settingGuide: "추천 설정"
cacheRemoteFiles: "리모트 파일을 캐시" cacheRemoteFiles: "리모트 파일을 캐시"
cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시"
cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다."
flagAsBot: "나는 봇입니다" flagAsBot: "나는 봇입니다"
flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다." flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다."
flagAsCat: "나는 고양이다냥" flagAsCat: "나는 고양이다냥"
flagAsCatDescription: "이 계정이 고양이라면 활성화 해주세요." flagAsCatDescription: "이 계정이 고양이라면 활성화 주세요."
flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기" flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
@ -201,7 +207,7 @@ instanceInfo: "서버 정보"
statistics: "통계" statistics: "통계"
clearQueue: "대기열 비우기" clearQueue: "대기열 비우기"
clearQueueConfirmTitle: "대기열을 비우시겠습니까?" clearQueueConfirmTitle: "대기열을 비우시겠습니까?"
clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearQueueConfirmText: "대기열에 남아 있는 노트는 더 이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다."
clearCachedFiles: "캐시 비우기" clearCachedFiles: "캐시 비우기"
clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?"
blockedInstances: "차단된 서버" blockedInstances: "차단된 서버"
@ -313,6 +319,7 @@ copyUrl: "URL 복사"
rename: "이름 변경" rename: "이름 변경"
avatar: "아바타" avatar: "아바타"
banner: "배너" banner: "배너"
displayOfSensitiveMedia: "민감한 미디어 표시"
whenServerDisconnected: "서버와의 접속이 끊겼을 때" whenServerDisconnected: "서버와의 접속이 끊겼을 때"
disconnectedFromServer: "서버와의 연결이 끊어졌습니다" disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
reload: "새로고침" reload: "새로고침"
@ -1066,6 +1073,24 @@ installed: "설치됨"
branding: "브랜딩" branding: "브랜딩"
enableServerMachineStats: "서버의 머신 사양을 공개하기" enableServerMachineStats: "서버의 머신 사양을 공개하기"
enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화" enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화"
turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다."
createInviteCode: "초대 코드 생성"
createWithOptions: "옵션을 지정하여 생성"
createCount: "초대 수"
inviteCodeCreated: "초대 코드 생성됨"
inviteLimitExceeded: "초대 코드 생성 한도를 초과했습니다."
createLimitRemaining: "초대 한도: {limit}회 남음"
inviteLimitResetCycle: " {time}시간 이내에 최대 {limit}개의 초대 코드를 생성할 수 있습니다."
expirationDate: "만료 날짜"
noExpirationDate: "만료기간 없음"
inviteCodeUsedAt: "다음에 사용된 초대 코드"
registeredUserUsingInviteCode: "초대 코드 사용 대상"
waitingForMailAuth: "이메일 인증 보류 중"
inviteCodeCreator: "초대 코드 생성자"
usedAt: "사용 시각"
unused: "사용되지 않음"
used: "사용됨"
expired: "만료됨"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "계정 생성이 완료되었습니다!" accountCreated: "계정 생성이 완료되었습니다!"
letsStartAccountSetup: "계정의 초기 설정을 진행합니다." letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
@ -1376,6 +1401,9 @@ _role:
ltlAvailable: "로컬 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용" canPublicNote: "공개 노트 허용"
canInvite: "서버 초대 코드 발행" canInvite: "서버 초대 코드 발행"
inviteLimit: "초대 한도"
inviteLimitCycle: "초대 발급 간격"
inviteExpirationTime: "초대 만료 기간"
canManageCustomEmojis: "커스텀 이모지 관리" canManageCustomEmojis: "커스텀 이모지 관리"
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
alwaysMarkNsfw: "파일을 항상 NSFW로 지정" alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
@ -1438,6 +1466,7 @@ _ad:
back: "뒤로" back: "뒤로"
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"
hide: "보이지 않음" hide: "보이지 않음"
timezoneinfo: "요일은 서버의 표준 시간대에 따라 결정됩니다."
_forgotPassword: _forgotPassword:
enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다."
ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오."
@ -1489,6 +1518,10 @@ _aboutMisskey:
donate: "Misskey에 기부하기" donate: "Misskey에 기부하기"
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
patrons: "후원자" patrons: "후원자"
_displayOfSensitiveMedia:
respect: "민감한 콘텐츠로 표시된 미디어 숨기기"
ignore: "민감한 콘텐츠로 표시된 미디어 보이기"
force: "미디어 항상 숨기기"
_instanceTicker: _instanceTicker:
none: "보이지 않음" none: "보이지 않음"
remote: "리모트 유저에게만 보이기" remote: "리모트 유저에게만 보이기"
@ -1963,6 +1996,7 @@ _deck:
introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!" introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다." introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요" widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
_columns: _columns:
main: "메인" main: "메인"
widgets: "위젯" widgets: "위젯"

View File

@ -20,6 +20,7 @@ noNotes: "ບໍ່ມີຫມາຍເຫດ"
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
instance: "ອີນສະແຕນ" instance: "ອີນສະແຕນ"
settings: "ກຳນົດຄ່າ" settings: "ກຳນົດຄ່າ"
notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ"
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ"
@ -48,9 +49,15 @@ delete: "ລຶບ"
deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​"
deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ"
addToList: "ເພີ່ມໃສ່ລາຍຊື່" addToList: "ເພີ່ມໃສ່ລາຍຊື່"
addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ"
sendMessage: "ສົ່ງຂໍ້ຄວາມ" sendMessage: "ສົ່ງຂໍ້ຄວາມ"
copyRSS: "ສຳເນົາ RSS" copyRSS: "ສຳເນົາ RSS"
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້"
copyNoteId: "ສຳເນົາ ID ບັນທຶກ"
copyFileId: "ສຳເນົາ ID ໄຟລ໌"
copyFolderId: "ສຳເນົາ ID ໂຟນເດີ"
copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌"
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
reply: "ຕອບ​ໄປ​ທີ" reply: "ຕອບ​ໄປ​ທີ"
loadMore: "ໂຫຼດເພີ່ມເຕີມ" loadMore: "ໂຫຼດເພີ່ມເຕີມ"
@ -109,6 +116,7 @@ sensitive: "NSFW"
add: "ເພີ່ມ" add: "ເພີ່ມ"
reaction: "ປະຕິກິລິຍາ" reaction: "ປະຕິກິລິຍາ"
reactions: "ປະຕິກິລິຍາ" reactions: "ປະຕິກິລິຍາ"
attachCancel: "ເອົາໄຟລ໌ແນບ"
mute: "ປີດສຽງ" mute: "ປີດສຽງ"
unmute: "ເປີດສຽງ" unmute: "ເປີດສຽງ"
block: "ບ໋ອກ" block: "ບ໋ອກ"
@ -116,6 +124,10 @@ unblock: "ຍົກເລີກກາຮົບລັອກ"
suspend: "ລະງັບ" suspend: "ລະງັບ"
unsuspend: "ເຊົາ​ລະ​ງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ"
selectList: "ເລືອກບັນຊີລາຍການ" selectList: "ເລືອກບັນຊີລາຍການ"
editList: "ແກ້ໄຂລາຍຊື່"
selectChannel: "ເລືອກຊ່ອງ"
selectAntenna: "ເລືອກເສົາອາກາດ"
editAntenna: "ແກ້ໄຂເສົາອາກາດ"
selectWidget: "ເລືອກວິກເຈັດ" selectWidget: "ເລືອກວິກເຈັດ"
editWidgets: "ແກ້ໄຂ Widget" editWidgets: "ແກ້ໄຂ Widget"
editWidgetsExit: "ສຳເລັດແລ້ວ" editWidgetsExit: "ສຳເລັດແລ້ວ"
@ -125,6 +137,7 @@ emojis: "ອີໂມຈິ"
emojiName: "ຊື່ Emoji" emojiName: "ຊື່ Emoji"
emojiUrl: "URL ອີໂມຈິ" emojiUrl: "URL ອີໂມຈິ"
addEmoji: "ຕື່ມອີໂມຈິ" addEmoji: "ຕື່ມອີໂມຈິ"
settingGuide: "ການຕັ້ງຄ່າທີ່ແນະນໍາ"
flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ" flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ"
flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ"
flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ"
@ -133,10 +146,13 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
addAccount: "ເພີ່ມບັນຊີ" addAccount: "ເພີ່ມບັນຊີ"
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ"
general: "ທົ່ວໄປ" general: "ທົ່ວໄປ"
wallpaper: "ພາບພື້ນຫລັງ" wallpaper: "ພາບພື້ນຫລັງ"
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ"
searchWith: "ຊອກຫາ: {q}" searchWith: "ຊອກຫາ: {q}"
youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
proxyAccount: "ບັນຊີພຣັອກຊີ" proxyAccount: "ບັນຊີພຣັອກຊີ"
host: "ໂຮດສ" host: "ໂຮດສ"
selectUser: "ເລືອກຜູ້ໃຊ້" selectUser: "ເລືອກຜູ້ໃຊ້"
@ -155,7 +171,9 @@ operations: "ການດຳເນີນງານ"
software: "ຊອບແວ" software: "ຊອບແວ"
version: "ສະບັບ" version: "ສະບັບ"
metadata: "Metadata" metadata: "Metadata"
withNFiles: "{n} ໄຟລ໌(s)"
monitor: "ຈໍພາບ" monitor: "ຈໍພາບ"
jobQueue: "ຄິວວຽກ"
cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ" cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ"
network: "ເຄືອຂ່າຍ" network: "ເຄືອຂ່າຍ"
disk: "ດິສກ໌" disk: "ດິສກ໌"
@ -343,6 +361,7 @@ _widgets:
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
activity: "ກິດຈະກຳ" activity: "ກິດຈະກຳ"
federation: "ສະຫະພັນ" federation: "ສະຫະພັນ"
jobQueue: "ຄິວວຽກ"
_userList: _userList:
chooseList: "ເລືອກບັນຊີລາຍການ" chooseList: "ເລືອກບັນຊີລາຍການ"
_cw: _cw:

View File

@ -389,10 +389,13 @@ help: "Hjälp"
close: "Stäng" close: "Stäng"
invites: "Inbjudan" invites: "Inbjudan"
members: "Medlemmar" members: "Medlemmar"
transfer: "Överför"
text: "Text" text: "Text"
enable: "Aktivera" enable: "Aktivera"
next: "Nästa" next: "Nästa"
invitations: "Inbjudan" invitations: "Inbjudan"
invitationCode: "Inbjudningskod"
available: "Tillgängligt"
weakPassword: "Svagt Lösenord" weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord" normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord" strongPassword: "Starkt Lösenord"
@ -481,6 +484,7 @@ windowMinimize: "Minimera"
windowRestore: "Återställ" windowRestore: "Återställ"
pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!" pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?" resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
dataSaver: "Databesparing"
_achievements: _achievements:
_types: _types:
_open3windows: _open3windows:

View File

@ -1,7 +1,7 @@
--- ---
_lang_: "ภาษาไทย" _lang_: "ภาษาไทย"
headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" headlineMisskey: "เชื่อมต่อระบบ Network ด้วย Note"
introMisskey: "ยินดีต้อนรับจ้าาา! Misskey เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀" introMisskey: "ยินดีต้อนรับทุกคนจ้า! Misskey คือ บริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน \"โน้ต (Note)\" เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ \"React\" กับเรื่องราวของคนอื่น ๆ ด้วย! 👍\n\nมุ่งสู่โลกใบใหม่กันเถอะ🚀"
poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b> (เรียกว่า \"อินสแตนซ์ Misskey\")" poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>Misskey</b> (เรียกว่า \"อินสแตนซ์ Misskey\")"
monthAndDay: "{month}/{day}" monthAndDay: "{month}/{day}"
search: "ค้นหา" search: "ค้นหา"
@ -339,7 +339,7 @@ thisYear: "ปีนี้"
thisMonth: "เดือนนี้" thisMonth: "เดือนนี้"
today: "วันนี้" today: "วันนี้"
dayX: "{day}" dayX: "{day}"
monthX: "{เดือน}" monthX: "เดือน {month}"
yearX: "{year}" yearX: "{year}"
pages: "หน้า" pages: "หน้า"
integration: "รวบรวม" integration: "รวบรวม"
@ -1996,6 +1996,7 @@ _deck:
introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!" introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!"
introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ" introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ"
widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต" widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต"
useSimpleUiForNonRootPages: "แสดง UI ของ Root Page อย่างง่าย "
_columns: _columns:
main: "หลัก" main: "หลัก"
widgets: "วิดเจ็ต" widgets: "วิดเจ็ต"

View File

@ -156,6 +156,8 @@ addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
flagAsBot: "这是一个机器人账号" flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。" flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫" flagAsCat: "将这个账户设定为一只猫"
@ -1072,7 +1074,20 @@ branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息" enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon" enableIdenticonGeneration: "启用生成用户 Identicon"
turnOffToImprovePerformance: "关闭该选项可以提高性能。" turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "发行邀请码"
createWithOptions: "使用选项来创建"
createCount: "发行数"
inviteCodeCreated: "已创建邀请码" inviteCodeCreated: "已创建邀请码"
inviteLimitExceeded: "可供发行的邀请码已达上限。"
createLimitRemaining: "可供发行的邀请码:剩余{limit}个"
inviteLimitResetCycle: "可以在{time}内发行最多{limit}个邀请码。"
expirationDate: "有效日期"
noExpirationDate: "不设置有效日期"
inviteCodeUsedAt: "邀请码被使用的日期和时间"
registeredUserUsingInviteCode: "使用了邀请码的用户"
waitingForMailAuth: "等待验证电子邮件"
inviteCodeCreator: "发行邀请码的用户"
usedAt: "使用时间"
unused: "未使用" unused: "未使用"
used: "已使用" used: "已使用"
expired: "已过期" expired: "已过期"
@ -1386,6 +1401,9 @@ _role:
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
canInvite: "发放服务器邀请码" canInvite: "发放服务器邀请码"
inviteLimit: "可发行邀请码的数量"
inviteLimitCycle: "邀请码的发行间隔"
inviteExpirationTime: "邀请码的有效日期"
canManageCustomEmojis: "管理自定义表情符号" canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量" driveCapacity: "网盘容量"
alwaysMarkNsfw: "总是将文件标记为 NSFW" alwaysMarkNsfw: "总是将文件标记为 NSFW"
@ -1978,6 +1996,7 @@ _deck:
introduction: "将各列进行组合以创建您自己的界面!" introduction: "将各列进行组合以创建您自己的界面!"
introduction2: "您可以随时通过屏幕右侧的 + 来添加列" introduction2: "您可以随时通过屏幕右侧的 + 来添加列"
widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具"
useSimpleUiForNonRootPages: "用简易UI表示非根页面"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.14.0-beta.5", "version": "13.14.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@8.6.0", "packageManager": "pnpm@8.6.9",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",

View File

@ -2,14 +2,7 @@ import Redis from 'ioredis';
import { loadConfig } from './built/config.js'; import { loadConfig } from './built/config.js';
const config = loadConfig(); const config = loadConfig();
const redis = new Redis({ const redis = new Redis(config.redis);
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
redis.on('connect', () => redis.disconnect()); redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => { redis.on('error', (e) => {

View File

@ -57,32 +57,31 @@
"@aws-sdk/client-s3": "3.367.0", "@aws-sdk/client-s3": "3.367.0",
"@aws-sdk/lib-storage": "3.367.0", "@aws-sdk/lib-storage": "3.367.0",
"@aws-sdk/node-http-handler": "3.360.0", "@aws-sdk/node-http-handler": "3.360.0",
"@bull-board/api": "5.6.0", "@bull-board/api": "5.6.1",
"@bull-board/fastify": "5.6.0", "@bull-board/fastify": "5.6.1",
"@bull-board/ui": "5.6.0", "@bull-board/ui": "5.6.1",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0", "@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0", "@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.2.1", "@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.0", "@fastify/multipart": "7.7.1",
"@fastify/static": "6.10.2", "@fastify/static": "6.10.2",
"@fastify/view": "8.0.0", "@fastify/view": "8.0.0",
"@nestjs/common": "10.0.5", "@nestjs/common": "10.1.0",
"@nestjs/core": "10.0.5", "@nestjs/core": "10.1.0",
"@nestjs/testing": "10.0.5", "@nestjs/testing": "10.1.0",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0", "@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.69", "@swc/core": "1.3.70",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "4.3.0", "bullmq": "4.4.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.0", "cbor": "9.0.0",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -93,8 +92,7 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "fastify": "4.20.0",
"fastify": "4.19.2",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -139,7 +137,6 @@
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"s-age": "1.1.2",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"semver": "7.5.4", "semver": "7.5.4",
"sharp": "0.32.3", "sharp": "0.32.3",
@ -157,7 +154,6 @@
"typeorm": "0.3.17", "typeorm": "0.3.17",
"typescript": "5.1.6", "typescript": "5.1.6",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.14",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.3", "web-push": "3.6.3",
"ws": "8.13.0", "ws": "8.13.0",
@ -172,7 +168,6 @@
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.0", "@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.3", "@types/jest": "29.5.3",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
@ -191,7 +186,6 @@
"@types/qrcode": "1.5.1", "@types/qrcode": "1.5.1",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.5.0", "@types/semver": "7.5.0",
@ -199,10 +193,8 @@
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6",
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.5", "@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.61.0",

View File

@ -41,14 +41,7 @@ const $meilisearch: Provider = {
const $redis: Provider = { const $redis: Provider = {
provide: DI.redis, provide: DI.redis,
useFactory: (config: Config) => { useFactory: (config: Config) => {
return new Redis.Redis({ return new Redis.Redis(config.redis);
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
}, },
inject: [DI.config], inject: [DI.config],
}; };
@ -56,14 +49,7 @@ const $redis: Provider = {
const $redisForPub: Provider = { const $redisForPub: Provider = {
provide: DI.redisForPub, provide: DI.redisForPub,
useFactory: (config: Config) => { useFactory: (config: Config) => {
const redis = new Redis.Redis({ const redis = new Redis.Redis(config.redisForPubsub);
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
return redis; return redis;
}, },
inject: [DI.config], inject: [DI.config],
@ -72,14 +58,7 @@ const $redisForPub: Provider = {
const $redisForSub: Provider = { const $redisForSub: Provider = {
provide: DI.redisForSub, provide: DI.redisForSub,
useFactory: (config: Config) => { useFactory: (config: Config) => {
const redis = new Redis.Redis({ const redis = new Redis.Redis(config.redisForPubsub);
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
redis.subscribe(config.host); redis.subscribe(config.host);
return redis; return redis;
}, },

View File

@ -6,6 +6,16 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
/** /**
* *
@ -35,30 +45,9 @@ export type Source = {
user: string; user: string;
pass: string; pass: string;
}[]; }[];
redis: { redis: RedisOptionsSource;
host: string; redisForPubsub?: RedisOptionsSource;
port: number; redisForJobQueue?: RedisOptionsSource;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForPubsub?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForJobQueue?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
meilisearch?: { meilisearch?: {
host: string; host: string;
port: string; port: string;
@ -119,8 +108,9 @@ export type Mixin = {
mediaProxy: string; mediaProxy: string;
externalMediaProxyEnabled: boolean; externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null; videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>; redis: RedisOptions & RedisOptionsSource;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;
@ -182,9 +172,9 @@ export function loadConfig() {
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
: null; : null;
if (!config.redis.prefix) config.redis.prefix = mixin.host; mixin.redis = convertRedisOptions(config.redis, mixin.host);
if (config.redisForPubsub == null) config.redisForPubsub = config.redis; mixin.redisForPubsub = config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, mixin.host) : mixin.redis;
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis; mixin.redisForJobQueue = config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, mixin.host) : mixin.redis;
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }
@ -196,3 +186,14 @@ function tryCreateUrl(url: string) {
throw new Error(`url="${url}" is not a valid URL.`); throw new Error(`url="${url}" is not a valid URL.`);
} }
} }
function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOptions & RedisOptionsSource {
return {
...options,
password: options.pass,
prefix: options.prefix ?? host,
family: options.family == null ? 0 : options.family,
keyPrefix: `${options.prefix ?? host}:`,
db: options.db ?? 0,
};
}

View File

@ -4,10 +4,9 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import type { User } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';

View File

@ -33,7 +33,7 @@ export class CreateSystemUserService {
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair();
let account!: User; let account!: User;

View File

@ -2,6 +2,7 @@ import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as Redis from 'ioredis';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -10,7 +11,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom'; import type { DOMWindow } from 'jsdom';
import * as Redis from 'ioredis';
type NodeInfo = { type NodeInfo = {
openRegistrations?: unknown; openRegistrations?: unknown;

View File

@ -574,7 +574,7 @@ export class NoteCreateService implements OnApplicationShutdown {
where: { where: {
userId: data.reply.userId, userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id, threadId: data.reply.threadId ?? data.reply.id,
} },
}); });
if (!isThreadMuted) { if (!isThreadMuted) {

View File

@ -109,7 +109,7 @@ export class QueueService {
removeOnFail: true, removeOnFail: true,
}; };
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({ await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({
name: d[0], name: d[0],
data: { data: {
user, user,

View File

@ -8,8 +8,9 @@ import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { WebfingerService } from '@/core/WebfingerService.js'; import { ILink, WebfingerService } from '@/core/WebfingerService.js';
import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -27,6 +28,7 @@ export class RemoteUserResolveService {
private utilityService: UtilityService, private utilityService: UtilityService,
private webfingerService: WebfingerService, private webfingerService: WebfingerService,
private remoteLoggerService: RemoteLoggerService, private remoteLoggerService: RemoteLoggerService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
) { ) {
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
@ -67,6 +69,22 @@ export class RemoteUserResolveService {
if (user == null) { if (user == null) {
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
if (self.href.startsWith(this.config.url)) {
const local = this.apDbResolverService.parseUri(self.href);
if (local.local && local.type === 'users') {
// the LR points to local
return (await this.apDbResolverService
.getUserFromApId(self.href)
.then((u) => {
if (u == null) {
throw new Error('local user not found');
} else {
return u;
}
})) as LocalUser;
}
}
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
@ -119,7 +137,7 @@ export class RemoteUserResolveService {
} }
@bindThis @bindThis
private async resolveSelf(acctLower: string) { private async resolveSelf(acctLower: string): Promise<ILink> {
this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
const finger = await this.webfingerService.webfinger(acctLower).catch(err => { const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`);

View File

@ -220,14 +220,19 @@ export class RoleService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async getUserRoles(userId: User['id']) { public async getUserAssigns(userId: User['id']) {
const now = Date.now(); const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); return assigns;
}
@bindThis
public async getUserRoles(userId: User['id']) {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const assigns = await this.getUserAssigns(userId);
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];

View File

@ -92,7 +92,7 @@ export class SignupService {
const keyPair = await new Promise<string[]>((res, rej) => const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', { generateKeyPair('rsa', {
modulusLength: 4096, modulusLength: 2048,
publicKeyEncoding: { publicKeyEncoding: {
type: 'spki', type: 'spki',
format: 'pem', format: 'pem',

View File

@ -1,5 +1,6 @@
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { IsNull } from 'typeorm';
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
@ -21,9 +22,8 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '../logger.js';
import { IsNull } from 'typeorm';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -322,7 +322,7 @@ export class UserFollowingService implements OnModuleInit {
where: { where: {
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
} },
}); });
if (following === null || !following.follower || !following.followee) { if (following === null || !following.follower || !following.followee) {
@ -412,8 +412,8 @@ export class UserFollowingService implements OnModuleInit {
followerId: user.id, followerId: user.id,
followee: { followee: {
movedToUri: IsNull(), movedToUri: IsNull(),
} },
} },
}); });
const nonMovedFollowers = await this.followingsRepository.count({ const nonMovedFollowers = await this.followingsRepository.count({
relations: { relations: {
@ -423,8 +423,8 @@ export class UserFollowingService implements OnModuleInit {
followeeId: user.id, followeeId: user.id,
follower: { follower: {
movedToUri: IsNull(), movedToUri: IsNull(),
} },
} },
}); });
await this.usersRepository.update( await this.usersRepository.update(
{ id: user.id }, { id: user.id },
@ -646,7 +646,7 @@ export class UserFollowingService implements OnModuleInit {
where: { where: {
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
} },
}); });
if (!following || !following.followee || !following.follower) return; if (!following || !following.followee || !following.follower) return;

View File

@ -52,7 +52,7 @@ export class VideoProcessingService {
query({ query({
thumbnail: '1', thumbnail: '1',
url, url,
}) }),
); );
} }
} }

View File

@ -6,12 +6,12 @@ import { query as urlQuery } from '@/misc/prelude/url.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
type ILink = { export type ILink = {
href: string; href: string;
rel?: string; rel?: string;
}; };
type IWebFinger = { export type IWebFinger = {
links: ILink[]; links: ILink[];
subject: string; subject: string;
}; };

View File

@ -220,6 +220,23 @@ export class ApPersonService implements OnModuleInit {
return null; return null;
} }
private async resolveAvatarAndBanner(user: RemoteUser, icon: any, image: any): Promise<Pick<RemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
const [avatar, banner] = await Promise.all([icon, image].map(img => {
if (img == null) return null;
if (user == null) throw new Error('failed to create user: user is null');
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
return {
avatarId: avatar?.id ?? null,
bannerId: banner?.id ?? null,
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
avatarBlurhash: avatar?.blurhash ?? null,
bannerBlurhash: banner?.blurhash ?? null,
};
}
/** /**
* Personを作成します * Personを作成します
*/ */
@ -259,6 +276,16 @@ export class ApPersonService implements OnModuleInit {
// Create user // Create user
let user: RemoteUser | null = null; let user: RemoteUser | null = null;
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host)
.then(_emojis => _emojis.map(emoji => emoji.name))
.catch(err => {
this.logger.error(`error occured while fetching user emojis`, { stack: err });
return [];
});
//#endregion
try { try {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
@ -285,6 +312,7 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
emojis,
})) as RemoteUser; })) as RemoteUser;
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
@ -321,6 +349,9 @@ export class ApPersonService implements OnModuleInit {
if (user == null) throw new Error('failed to create user: user is null'); if (user == null) throw new Error('failed to create user: user is null');
// Register to the cache
this.cacheService.uriPersonCache.set(user.uri, user);
// Register host // Register host
this.federatedInstanceService.fetch(host).then(async i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
@ -336,45 +367,16 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(user, tags); this.hashtagService.updateUsertags(user, tags);
//#region アバターとヘッダー画像をフェッチ //#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => { try {
if (img == null) return null; const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image);
if (user == null) throw new Error('failed to create user: user is null'); await this.usersRepository.update(user.id, updates);
return this.apImageService.resolveImage(user, img).catch(() => null); user = { ...user, ...updates };
}));
const avatarId = avatar?.id ?? null; // Register to the cache
const bannerId = banner?.id ?? null; this.cacheService.uriPersonCache.set(user.uri, user);
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; } catch (err) {
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; this.logger.error('error occured while fetching user avatar/banner', { stack: err });
const avatarBlurhash = avatar?.blurhash ?? null; }
const bannerBlurhash = banner?.blurhash ?? null;
await this.usersRepository.update(user.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarBlurhash,
bannerBlurhash,
});
user.avatarId = avatarId;
user.bannerId = bannerId;
user.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl;
user.avatarBlurhash = avatarBlurhash;
user.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [];
});
const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user.id, { emojis: emojiNames });
//#endregion //#endregion
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
@ -400,7 +402,7 @@ export class ApPersonService implements OnModuleInit {
if (uri.startsWith(`${this.config.url}/`)) return; if (uri.startsWith(`${this.config.url}/`)) return;
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; const exist = await this.fetchPerson(uri) as RemoteUser | null;
if (exist === null) return; if (exist === null) return;
//#endregion //#endregion
@ -413,12 +415,6 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`); this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
if (img == null) return null;
return this.apImageService.resolveImage(exist, img).catch(() => null);
}));
// カスタム絵文字取得 // カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
@ -454,6 +450,7 @@ export class ApPersonService implements OnModuleInit {
movedToUri: person.movedTo ?? null, movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null, alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable, isExplorable: person.discoverable,
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; } as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => { const moving = ((): boolean => {
@ -476,18 +473,6 @@ export class ApPersonService implements OnModuleInit {
if (moving) updates.movedAt = new Date(); if (moving) updates.movedAt = new Date();
if (avatar) {
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
}
if (banner) {
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
}
// Update user // Update user
await this.usersRepository.update(exist.id, updates); await this.usersRepository.update(exist.id, updates);

View File

@ -3,8 +3,8 @@ import { DI } from '@/di-symbols.js';
import type { AbuseUserReportsRepository } from '@/models/index.js'; import type { AbuseUserReportsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class AbuseUserReportEntityService { export class AbuseUserReportEntityService {

View File

@ -4,8 +4,8 @@ import type { AuthSessionsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { AuthSession } from '@/models/entities/AuthSession.js'; import type { AuthSession } from '@/models/entities/AuthSession.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { AppEntityService } from './AppEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { AppEntityService } from './AppEntityService.js';
@Injectable() @Injectable()
export class AuthSessionEntityService { export class AuthSessionEntityService {

View File

@ -50,7 +50,7 @@ export class ChannelEntityService {
const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({ const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
where: { where: {
noteChannelId: channel.id, noteChannelId: channel.id,
userId: meId userId: meId,
}, },
}) : undefined; }) : undefined;

View File

@ -4,8 +4,8 @@ import type { FollowRequestsRepository } from '@/models/index.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { FollowRequest } from '@/models/entities/FollowRequest.js'; import type { FollowRequest } from '@/models/entities/FollowRequest.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class FollowRequestEntityService { export class FollowRequestEntityService {

View File

@ -3,8 +3,8 @@ import { DI } from '@/di-symbols.js';
import type { GalleryLikesRepository } from '@/models/index.js'; import type { GalleryLikesRepository } from '@/models/index.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { GalleryLike } from '@/models/entities/GalleryLike.js'; import type { GalleryLike } from '@/models/entities/GalleryLike.js';
import { GalleryPostEntityService } from './GalleryPostEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GalleryPostEntityService } from './GalleryPostEntityService.js';
@Injectable() @Injectable()
export class GalleryLikeEntityService { export class GalleryLikeEntityService {

View File

@ -4,8 +4,8 @@ import type { ModerationLogsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { ModerationLog } from '@/models/entities/ModerationLog.js'; import type { ModerationLog } from '@/models/entities/ModerationLog.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class ModerationLogEntityService { export class ModerationLogEntityService {

View File

@ -4,8 +4,8 @@ import type { NoteFavoritesRepository } from '@/models/index.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import type { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteEntityService } from './NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { NoteEntityService } from './NoteEntityService.js';
@Injectable() @Injectable()
export class NoteFavoriteEntityService { export class NoteFavoriteEntityService {

View File

@ -4,8 +4,8 @@ import type { PageLikesRepository } from '@/models/index.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { PageLike } from '@/models/entities/PageLike.js'; import type { PageLike } from '@/models/entities/PageLike.js';
import { PageEntityService } from './PageEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { PageEntityService } from './PageEntityService.js';
@Injectable() @Injectable()
export class PageLikeEntityService { export class PageLikeEntityService {

View File

@ -3,8 +3,8 @@ import { DI } from '@/di-symbols.js';
import type { SigninsRepository } from '@/models/index.js'; import type { SigninsRepository } from '@/models/index.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { Signin } from '@/models/entities/Signin.js'; import type { Signin } from '@/models/entities/Signin.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class SigninEntityService { export class SigninEntityService {

View File

@ -1,11 +1,11 @@
import { Writable, WritableOptions } from "node:stream"; import { Writable, WritableOptions } from 'node:stream';
export class DevNull extends Writable implements NodeJS.WritableStream { export class DevNull extends Writable implements NodeJS.WritableStream {
constructor(opts?: WritableOptions) { constructor(opts?: WritableOptions) {
super(opts); super(opts);
} }
_write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) {
setImmediate(cb); setImmediate(cb);
} }
} }

View File

@ -5,10 +5,10 @@ const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
export function parseUlid(id: string): { date: Date; } { export function parseUlid(id: string): { date: Date; } {
const timestamp = id.slice(0, 10); const timestamp = id.slice(0, 10);
let time = 0; let time = 0;
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
time = time * 32 + CHARS.indexOf(timestamp[i]); time = time * 32 + CHARS.indexOf(timestamp[i]);
} }
return { date: new Date(time) }; return { date: new Date(time) };
} }

View File

@ -10,7 +10,7 @@ export async function awaitAll<T>(obj: Promiseable<T>): Promise<T> {
const resolvedValues = await Promise.all(values.map(value => const resolvedValues = await Promise.all(values.map(value =>
(!value || !value.constructor || value.constructor.name !== 'Object') (!value || !value.constructor || value.constructor.name !== 'Object')
? value ? value
: awaitAll(value) : awaitAll(value),
)); ));
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {

View File

@ -50,7 +50,6 @@ import { User } from '@/models/entities/User.js';
import { UserIp } from '@/models/entities/UserIp.js'; import { UserIp } from '@/models/entities/UserIp.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js'; import { UserList } from '@/models/entities/UserList.js';
import { UserListFavorite } from './entities/UserListFavorite.js';
import { UserListJoining } from '@/models/entities/UserListJoining.js'; import { UserListJoining } from '@/models/entities/UserListJoining.js';
import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { UserPending } from '@/models/entities/UserPending.js'; import { UserPending } from '@/models/entities/UserPending.js';
@ -65,6 +64,7 @@ import { Role } from '@/models/entities/Role.js';
import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; import { RoleAssignment } from '@/models/entities/RoleAssignment.js';
import { Flash } from '@/models/entities/Flash.js'; import { Flash } from '@/models/entities/Flash.js';
import { FlashLike } from '@/models/entities/FlashLike.js'; import { FlashLike } from '@/models/entities/FlashLike.js';
import { UserListFavorite } from './entities/UserListFavorite.js';
import type { Repository } from 'typeorm'; import type { Repository } from 'typeorm';
export { export {

View File

@ -15,11 +15,8 @@ export const QUEUE = {
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
return { return {
connection: { connection: {
port: config.redisForJobQueue.port, ...config.redisForJobQueue,
host: config.redisForJobQueue.host, keyPrefix: undefined
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
password: config.redisForJobQueue.pass,
db: config.redisForJobQueue.db ?? 0,
}, },
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
}; };

View File

@ -9,10 +9,10 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { EmailService } from '@/core/EmailService.js'; import { EmailService } from '@/core/EmailService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { SearchService } from '@/core/SearchService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbUserDeleteJobData } from '../types.js'; import type { DbUserDeleteJobData } from '../types.js';
import { SearchService } from "@/core/SearchService.js";
@Injectable() @Injectable()
export class DeleteAccountProcessorService { export class DeleteAccountProcessorService {

View File

@ -1,7 +1,7 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { ZipReader } from 'slacc';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import unzipper from 'unzipper';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -72,9 +72,9 @@ export class ImportCustomEmojisProcessorService {
} }
const outputPath = path + '/emojis'; const outputPath = path + '/emojis';
const unzipStream = fs.createReadStream(destPath); try {
const extractor = unzipper.Extract({ path: outputPath }); this.logger.succ(`Unzipping to ${outputPath}`);
extractor.on('close', async () => { ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
const meta = JSON.parse(metaRaw); const meta = JSON.parse(metaRaw);
@ -115,8 +115,12 @@ export class ImportCustomEmojisProcessorService {
cleanup(); cleanup();
this.logger.succ('Imported'); this.logger.succ('Imported');
}); } catch (e) {
unzipStream.pipe(extractor); if (e instanceof Error || typeof e === 'string') {
this.logger.succ(`Unzipping to ${outputPath}`); this.logger.error(e);
}
cleanup();
throw e;
}
} }
} }

View File

@ -1,16 +1,16 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type * as Bull from 'bullmq';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import { RelationshipJobData } from '../types.js';
import type { UsersRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { LocalUser, RemoteUser } from '@/models/entities/User.js'; import { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { RelationshipJobData } from '../types.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@Injectable() @Injectable()
export class RelationshipProcessorService { export class RelationshipProcessorService {

View File

@ -3,6 +3,8 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import rename from 'rename'; import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -18,11 +20,9 @@ import { contentDisposition } from '@/misc/content-disposition.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -180,8 +180,8 @@ export class FileServerService {
reply.header('Content-Disposition', reply.header('Content-Disposition',
contentDisposition( contentDisposition(
'inline', 'inline',
correctFilename(file.filename, image.ext) correctFilename(file.filename, image.ext),
) ),
); );
return image.data; return image.data;
} }
@ -278,11 +278,11 @@ export class FileServerService {
}; };
} else { } else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({ .resize({
height: 'emoji' in request.query ? 128 : 320, height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.webp(webpDefault); .webp(webpDefault);
image = { image = {
data, data,
@ -355,8 +355,8 @@ export class FileServerService {
reply.header('Content-Disposition', reply.header('Content-Disposition',
contentDisposition( contentDisposition(
'inline', 'inline',
correctFilename(file.filename, image.ext) correctFilename(file.filename, image.ext),
) ),
); );
return image.data; return image.data;
} catch (e) { } catch (e) {

View File

@ -1,18 +1,18 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import vary from 'vary'; import vary from 'vary';
import fastifyAccepts from '@fastify/accepts';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { FindOptionsWhere } from 'typeorm';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import type { FindOptionsWhere } from 'typeorm';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
import fastifyAccepts from '@fastify/accepts';
@Injectable() @Injectable()
export class WellKnownServerService { export class WellKnownServerService {

View File

@ -13,9 +13,9 @@ import { EmailService } from '@/core/EmailService.js';
import { LocalUser } from '@/models/entities/User.js'; import { LocalUser } from '@/models/entities/User.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { FastifyRequest, FastifyReply } from 'fastify';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
@Injectable() @Injectable()
export class SignupApiService { export class SignupApiService {

View File

@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -77,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
emojiId: emoji.id, emojiId: emoji.id,
}); });
return { return this.emojiEntityService.packDetailed(emoji);
id: emoji.id,
};
}); });
} }
} }

View File

@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (queryarry) { if (queryarry) {
emojis = emojis.filter(emoji => emojis = emojis.filter(emoji =>
queryarry.includes(`:${emoji.name}:`) queryarry.includes(`:${emoji.name}:`),
); );
} else { } else {
emojis = emojis.filter(emoji => emojis = emojis.filter(emoji =>

View File

@ -61,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const signins = await this.signinsRepository.findBy({ userId: user.id }); const signins = await this.signinsRepository.findBy({ userId: user.id });
const roleAssigns = await this.roleService.getUserAssigns(user.id);
const roles = await this.roleService.getUserRoles(user.id); const roles = await this.roleService.getUserRoles(user.id);
return { return {
@ -85,6 +86,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
signins, signins,
policies: await this.roleService.getUserPolicies(user.id), policies: await this.roleService.getUserPolicies(user.id),
roles: await this.roleEntityService.packMany(roles, me), roles: await this.roleEntityService.packMany(roles, me),
roleAssigns: roleAssigns.map(a => ({
createdAt: a.createdAt.toISOString(),
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
roleId: a.roleId,
})),
}; };
}); });
} }

View File

@ -76,6 +76,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchAntenna); throw new ApiError(meta.errors.noSuchAntenna);
} }
this.antennasRepository.update(antenna.id, {
isActive: true,
lastUsedAt: new Date(),
});
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const noteIdsRes = await this.redisClient.xrevrange( const noteIdsRes = await this.redisClient.xrevrange(
`antennaTimeline:${antenna.id}`, `antennaTimeline:${antenna.id}`,
@ -112,11 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.noteReadService.read(me.id, notes); this.noteReadService.read(me.id, notes);
} }
this.antennasRepository.update(antenna.id, {
isActive: true,
lastUsedAt: new Date(),
});
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });
} }

View File

@ -112,6 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,
notify: ps.notify, notify: ps.notify,
isActive: true,
lastUsedAt: new Date(),
}); });
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id })); this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));

View File

@ -5,8 +5,8 @@ import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
where: { where: {
blockerId: blocker.id, blockerId: blocker.id,
blockeeId: blockee.id, blockeeId: blockee.id,
} },
}); });
if (!exist) { if (!exist) {

View File

@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['account', 'notes', 'clips'], tags: ['account', 'notes', 'clips'],

View File

@ -5,8 +5,8 @@ import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['following', 'users'], tags: ['following', 'users'],

View File

@ -5,8 +5,8 @@ import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['following', 'users'], tags: ['following', 'users'],

View File

@ -23,7 +23,7 @@ export const meta = {
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
kind: 'permission', kind: 'permission',
}, },
} },
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const procedures = this.twoFactorAuthenticationService.getProcedures(); const procedures = this.twoFactorAuthenticationService.getProcedures();
if (!(procedures as any)[attestation.fmt]) { if (!(procedures as any)[attestation.fmt]) {
throw new Error('unsupported fmt'); throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`);
} }
const verificationData = (procedures as any)[attestation.fmt].verify({ const verificationData = (procedures as any)[attestation.fmt].verify({

View File

@ -267,7 +267,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
const ads = await this.adsRepository.createQueryBuilder("ads") const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() }) .where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() }) .andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => { .andWhere(new Brackets(qb => {

View File

@ -4,8 +4,8 @@ import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],

View File

@ -3,8 +3,8 @@ import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],

View File

@ -3,8 +3,8 @@ import type { PromoReadsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],

View File

@ -35,7 +35,7 @@ export const meta = {
code: 'NO_SUCH_REGISTRATION', code: 'NO_SUCH_REGISTRATION',
id: ' b09d8066-8064-5613-efb6-0e963b21d012', id: ' b09d8066-8064-5613-efb6-0e963b21d012',
}, },
} },
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -5,8 +5,8 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],

View File

@ -3,9 +3,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js'; import Channel from '../channel.js';
import { StreamMessages } from '../types.js'; import { StreamMessages } from '../types.js';
import { RoleService } from '@/core/RoleService.js';
class RoleTimelineChannel extends Channel { class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline'; public readonly chName = 'roleTimeline';

View File

@ -5,8 +5,8 @@ block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = `${config.url}/notes/${note.id}`; - const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
- const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive) - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
- const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive) - const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
block title block title
= `${title} | ${instanceName}` = `${title} | ${instanceName}`
@ -19,15 +19,17 @@ block og
meta(property='og:title' content= title) meta(property='og:title' content= title)
meta(property='og:description' content= summary) meta(property='og:description' content= summary)
meta(property='og:url' content= url) meta(property='og:url' content= url)
if video if videos.length
meta(property='og:video:url' content= video.url) each video in videos
meta(property='og:video:secure_url' content= video.url) meta(property='og:video:url' content= video.url)
meta(property='og:video:type' content= video.type) meta(property='og:video:secure_url' content= video.url)
// FIXME: add width and height meta(property='og:video:type' content= video.type)
// FIXME: add embed player for Twitter // FIXME: add width and height
if image // FIXME: add embed player for Twitter
if images.length
meta(property='twitter:card' content='summary_large_image') meta(property='twitter:card' content='summary_large_image')
meta(property='og:image' content= image.url) each image in images
meta(property='og:image' content= image.url)
else else
meta(property='twitter:card' content='summary') meta(property='twitter:card' content='summary')
meta(property='og:image' content= avatarUrl) meta(property='og:image' content= avatarUrl)

View File

@ -4,8 +4,9 @@
"scripts": { "scripts": {
"watch": "vite", "watch": "vite",
"build": "vite build", "build": "vite build",
"storybook-dev": "chokidar 'src/**/*.{mdx,ts,vue}' -d 1000 -t 1000 --initial -i '**/*.stories.ts' -c 'pkill -f node_modules/storybook/index.js; node_modules/.bin/tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && node_modules/.bin/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": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && storybook build", "build-storybook-pre": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build",
"chromatic": "chromatic", "chromatic": "chromatic",
"test": "vitest --run", "test": "vitest --run",
"test-and-coverage": "vitest --run --coverage", "test-and-coverage": "vitest --run --coverage",
@ -19,10 +20,10 @@
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.3", "@syuilo/aiscript": "0.15.0",
"@tabler/icons-webfont": "2.25.0", "@tabler/icons-webfont": "2.25.0",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.14", "@vue-macros/reactivity-transform": "0.3.15",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6", "astring": "1.8.6",
"autosize": "6.0.1", "autosize": "6.0.1",
@ -54,7 +55,7 @@
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.3.0", "punycode": "2.3.0",
"querystring": "0.2.1", "querystring": "0.2.1",
"rollup": "3.26.2", "rollup": "3.26.3",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"sass": "1.63.6", "sass": "1.63.6",
@ -116,7 +117,6 @@
"@vitest/coverage-v8": "0.33.0", "@vitest/coverage-v8": "0.33.0",
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.4",
"acorn": "8.10.0", "acorn": "8.10.0",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.17.1", "cypress": "12.17.1",
"eslint": "8.45.0", "eslint": "8.45.0",
@ -127,6 +127,7 @@
"micromatch": "4.0.5", "micromatch": "4.0.5",
"msw": "1.2.2", "msw": "1.2.2",
"msw-storybook-addon": "1.8.0", "msw-storybook-addon": "1.8.0",
"nodemon": "3.0.1",
"prettier": "3.0.0", "prettier": "3.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",

View File

@ -19,7 +19,7 @@
</div> </div>
<div v-if="file.isSensitive" :class="[$style.label, $style.red]"> <div v-if="file.isSensitive" :class="[$style.label, $style.red]">
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/> <img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
<p :class="$style.labelText">NSFW</p> <p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
</div> </div>
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/> <MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>

View File

@ -20,7 +20,7 @@
<template v-if="hide"> <template v-if="hide">
<div :class="$style.hiddenText"> <div :class="$style.hiddenText">
<div :class="$style.hiddenTextWrapper"> <div :class="$style.hiddenTextWrapper">
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b> <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
<span style="display: block;">{{ i18n.ts.clickToShow }}</span> <span style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div> </div>
@ -30,7 +30,7 @@
<div :class="$style.indicators"> <div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div> <div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div> <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div> </div>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i> <i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>

View File

@ -165,6 +165,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog'; import { showMovedDialog } from '@/scripts/show-moved-dialog';
import { shouldCollapsed } from '@/scripts/collapsed';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -204,17 +205,7 @@ let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && ( const isLong = shouldCollapsed(appearNote);
(appearNote.text.includes('$[x2')) ||
(appearNote.text.includes('$[x3')) ||
(appearNote.text.includes('$[x4')) ||
(appearNote.text.includes('$[scale')) ||
(appearNote.text.includes('$[position')) ||
(appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) ||
(appearNote.files.length >= 5) ||
(urls && urls.length >= 4)
));
const collapsed = ref(appearNote.cw == null && isLong); const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
@ -259,6 +250,17 @@ useTooltip(renoteButton, async (showing) => {
}, {}, 'closed'); }, {}, 'closed');
}); });
type Visibility = 'public' | 'home' | 'followers' | 'specified';
// defaultStore.state.visibilitystringstring
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
if (a === 'specified' || b === 'specified') return 'specified';
if (a === 'followers' || b === 'followers') return 'followers';
if (a === 'home' || b === 'home') return 'home';
// if (a === 'public' || b === 'public')
return 'public';
}
function renote(viaKeyboard = false) { function renote(viaKeyboard = false) {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
@ -309,7 +311,12 @@ function renote(viaKeyboard = false) {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
os.api('notes/create', { os.api('notes/create', {
localOnly,
visibility: smallerVisibility(appearNote.visibility, configuredVisibility),
renoteId: appearNote.id, renoteId: appearNote.id,
}).then(() => { }).then(() => {
os.toast(i18n.ts.renoted); os.toast(i18n.ts.renoted);

View File

@ -907,6 +907,7 @@ defineExpose({
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 4px; gap: 4px;
margin-bottom: -10px;
} }
.headerLeft { .headerLeft {
@ -1024,7 +1025,7 @@ defineExpose({
} }
.targetNote { .targetNote {
padding: 0 20px 16px 20px; padding: 10px 20px 16px 20px;
} }
.withQuote { .withQuote {

View File

@ -5,7 +5,7 @@
<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> <div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/> <MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
<div v-if="element.isSensitive" :class="$style.sensitive"> <div v-if="element.isSensitive" :class="$style.sensitive">
<i class="ti ti-alert-triangle" style="margin: auto;"></i> <i class="ti ti-eye-exclamation" style="margin: auto;"></i>
</div> </div>
</div> </div>
</template> </template>

View File

@ -9,7 +9,10 @@
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
</div> </div>
<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div> <div style="text-align: center;">
<div>{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
<div style="font-weight: bold; margin-top: 0.5em;">{{ i18n.ts.beSureToReadThisAsItIsImportant }}</div>
</div>
<MkFolder v-if="availableServerRules" :defaultOpen="true"> <MkFolder v-if="availableServerRules" :defaultOpen="true">
<template #label>{{ i18n.ts.serverRules }}</template> <template #label>{{ i18n.ts.serverRules }}</template>
@ -19,7 +22,7 @@
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol> </ol>
<MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
<MkFolder v-if="availableTos" :defaultOpen="true"> <MkFolder v-if="availableTos" :defaultOpen="true">
@ -28,7 +31,7 @@
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a> <a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a>
<MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
@ -37,7 +40,7 @@
<a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> <a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
<MkSwitch v-model="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree>{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
<div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div> <div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div>
@ -52,13 +55,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os';
const availableServerRules = instance.serverRules.length > 0; const availableServerRules = instance.serverRules.length > 0;
const availableTos = instance.tosUrl != null; const availableTos = instance.tosUrl != null;
@ -75,6 +79,48 @@ const emit = defineEmits<{
(ev: 'cancel'): void; (ev: 'cancel'): void;
(ev: 'done'): void; (ev: 'done'): void;
}>(); }>();
async function updateAgreeServerRules(v: boolean) {
if (v) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
});
if (confirm.canceled) return;
agreeServerRules.value = true;
} else {
agreeServerRules.value = false;
}
}
async function updateAgreeTos(v: boolean) {
if (v) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }),
});
if (confirm.canceled) return;
agreeTos.value = true;
} else {
agreeTos.value = false;
}
}
async function updateAgreeNote(v: boolean) {
if (v) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
});
if (confirm.canceled) return;
agreeNote.value = true;
} else {
agreeNote.value = false;
}
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -31,16 +31,13 @@ import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue'; import MkPoll from '@/components/MkPoll.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { $i } from '@/account'; import { $i } from '@/account';
import { shouldCollapsed } from '@/scripts/collapsed';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
}>(); }>();
const isLong = const isLong = shouldCollapsed(props.note);
props.note.cw == null && props.note.text != null && (
(props.note.text.split('\n').length > 9) ||
(props.note.text.length > 500)
);
const collapsed = $ref(isLong); const collapsed = $ref(isLong);
</script> </script>

View File

@ -52,19 +52,21 @@
</footer> </footer>
</article> </article>
</component> </component>
<div v-if="tweetId" :class="$style.action"> <template v-if="showActions">
<MkButton :small="true" inline @click="tweetExpanded = true"> <div v-if="tweetId" :class="$style.action">
<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }} <MkButton :small="true" inline @click="tweetExpanded = true">
</MkButton> <i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }}
</div> </MkButton>
<div v-if="!playerEnabled && player.url" :class="$style.action"> </div>
<MkButton :small="true" inline @click="playerEnabled = true"> <div v-if="!playerEnabled && player.url" :class="$style.action">
<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }} <MkButton :small="true" inline @click="playerEnabled = true">
</MkButton> <i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
<MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()"> </MkButton>
<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }} <MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()">
</MkButton> <i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }}
</div> </MkButton>
</div>
</template>
</div> </div>
</template> </template>
@ -85,9 +87,11 @@ const props = withDefaults(defineProps<{
url: string; url: string;
detail?: boolean; detail?: boolean;
compact?: boolean; compact?: boolean;
showActions?: boolean;
}>(), { }>(), {
detail: false, detail: false,
compact: false, compact: false,
showActions: true,
}); });
const MOBILE_THRESHOLD = 500; const MOBILE_THRESHOLD = 500;

View File

@ -1,7 +1,7 @@
<template> <template>
<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/>
</Transition> </Transition>
</div> </div>
</template> </template>

View File

@ -15,13 +15,13 @@
</div> </div>
<div :class="$style.status"> <div :class="$style.status">
<div :class="$style.statusItem"> <div :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ user.notesCount }}</span> <p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
</div> </div>
<div :class="$style.statusItem"> <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ user.followingCount }}</span> <p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span>
</div> </div>
<div :class="$style.statusItem"> <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ user.followersCount }}</span> <p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
</div> </div>
</div> </div>
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/> <MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
@ -31,9 +31,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';
import number from '@/filters/number';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { $i } from '@/account'; import { $i } from '@/account';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe';
defineProps<{ defineProps<{
user: misskey.entities.UserDetailed; user: misskey.entities.UserDetailed;

View File

@ -30,11 +30,11 @@
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div> <div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
<div>{{ number(user.notesCount) }}</div> <div>{{ number(user.notesCount) }}</div>
</div> </div>
<div :class="$style.statusItem"> <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div> <div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div>
<div>{{ number(user.followingCount) }}</div> <div>{{ number(user.followingCount) }}</div>
</div> </div>
<div :class="$style.statusItem"> <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div> <div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div>
<div>{{ number(user.followersCount) }}</div> <div>{{ number(user.followersCount) }}</div>
</div> </div>
@ -61,6 +61,7 @@ import number from '@/filters/number';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { $i } from '@/account'; import { $i } from '@/account';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe';
const props = defineProps<{ const props = defineProps<{
showing: boolean; showing: boolean;

View File

@ -4,6 +4,9 @@ import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
let lock: Promise<undefined> | undefined;
const common = { const common = {
render(args) { render(args) {
return { return {
@ -25,41 +28,57 @@ const common = {
template: '<MkAd v-bind="props" />', template: '<MkAd v-bind="props" />',
}; };
}, },
/* FIXME: disabled because it still didnt pass after applying #11267
async play({ canvasElement, args }) { async play({ canvasElement, args }) {
const canvas = within(canvasElement); if (lock) {
const a = canvas.getByRole<HTMLAnchorElement>('link'); console.warn('This test is unexpectedly running twice in parallel, fix it!');
await expect(a.href).toMatch(/^https?:\/\/.*#test$/); console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267');
const img = within(a).getByRole('img'); await lock;
await expect(img).toBeInTheDocument();
let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(1);
const i = buttons[0];
await expect(i).toBeInTheDocument();
await userEvent.click(i);
await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back));
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1);
const reduce = args.__hasReduce ? buttons[0] : null;
const back = buttons[args.__hasReduce ? 1 : 0];
if (reduce) {
await expect(reduce).toBeInTheDocument();
await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
} }
await expect(back).toBeInTheDocument();
await expect(back).toHaveTextContent(i18n.ts._ad.back); let resolve: (value?: any) => void;
await userEvent.click(back); lock = new Promise(r => resolve = r);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
if (reduce) { try {
await expect(reduce).not.toBeInTheDocument(); const canvas = within(canvasElement);
const a = canvas.getByRole<HTMLAnchorElement>('link');
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
const img = within(a).getByRole('img');
await expect(img).toBeInTheDocument();
let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(1);
const i = buttons[0];
await expect(i).toBeInTheDocument();
await userEvent.click(i);
await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back));
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1);
const reduce = args.__hasReduce ? buttons[0] : null;
const back = buttons[args.__hasReduce ? 1 : 0];
if (reduce) {
await expect(reduce).toBeInTheDocument();
await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
}
await expect(back).toBeInTheDocument();
await expect(back).toHaveTextContent(i18n.ts._ad.back);
await userEvent.click(back);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
if (reduce) {
await expect(reduce).not.toBeInTheDocument();
}
await expect(back).not.toBeInTheDocument();
const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
await expect(aAgain).toBeInTheDocument();
const imgAgain = within(aAgain).getByRole('img');
await expect(imgAgain).toBeInTheDocument();
} finally {
resolve!();
lock = undefined;
} }
await expect(back).not.toBeInTheDocument();
const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
await expect(aAgain).toBeInTheDocument();
const imgAgain = within(aAgain).getByRole('img');
await expect(imgAgain).toBeInTheDocument();
}, },
*/
args: { args: {
prefer: [], prefer: [],
specify: { specify: {

View File

@ -179,6 +179,9 @@ const patronsWithIcon = [{
}, { }, {
name: 'カガミ', name: 'カガミ',
icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg', icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
}, {
name: 'フランギ・シュウ',
icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg',
}]; }];
const patrons = [ const patrons = [
@ -276,6 +279,7 @@ const patrons = [
'ぷーざ', 'ぷーざ',
'越貝鯛丸', '越貝鯛丸',
'Nick / pprmint.', 'Nick / pprmint.',
'kino3277',
]; ];
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));

View File

@ -32,7 +32,7 @@
<MkUserCardMini :user="file.user"/> <MkUserCardMini :user="file.user"/>
</MkA> </MkA>
<div> <div>
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> <MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div> </div>
<div> <div>

View File

@ -40,7 +40,7 @@
</div> </div>
<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub"> <div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div> <div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div> </div>
</div> </div>

View File

@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton> <MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
<MkInput v-model="name"> <MkInput v-model="name" pattern="[a-z0-9_]">
<template #label>{{ i18n.ts.name }}</template> <template #label>{{ i18n.ts.name }}</template>
</MkInput> </MkInput>
<MkInput v-model="category" :datalist="customEmojiCategories"> <MkInput v-model="category" :datalist="customEmojiCategories">
@ -70,6 +70,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import * as misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -95,7 +96,7 @@ let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
let file = $ref(); let file = $ref<misskey.entities.DriveFile>();
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
@ -110,6 +111,10 @@ const emit = defineEmits<{
async function changeImage(ev) { async function changeImage(ev) {
file = await selectFile(ev.currentTarget ?? ev.target, null); file = await selectFile(ev.currentTarget ?? ev.target, null);
const candidate = file.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
name = candidate;
}
} }
async function addRole() { async function addRole() {

View File

@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
const PRESET_DEFAULT = `/// @ 0.13.3 const PRESET_DEFAULT = `/// @ 0.15.0
var name = "" var name = ""
@ -51,7 +51,7 @@ Ui:render([
]) ])
`; `;
const PRESET_OMIKUJI = `/// @ 0.13.3 const PRESET_OMIKUJI = `/// @ 0.15.0
// //
// //
@ -94,7 +94,7 @@ Ui:render([
]) ])
`; `;
const PRESET_SHUFFLE = `/// @ 0.13.3 const PRESET_SHUFFLE = `/// @ 0.15.0
// //
let string = "ペペロンチーノ" let string = "ペペロンチーノ"
@ -173,7 +173,7 @@ var cursor = 0
do() do()
`; `;
const PRESET_QUIZ = `/// @ 0.13.3 const PRESET_QUIZ = `/// @ 0.15.0
let title = '地理クイズ' let title = '地理クイズ'
let qas = [{ let qas = [{
@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls) Ui:render(qaEls)
`; `;
const PRESET_TIMELINE = `/// @ 0.13.3 const PRESET_TIMELINE = `/// @ 0.15.0
// API // API
@fetch() { @fetch() {

View File

@ -236,6 +236,7 @@ definePageMetadata(computed(() => post ? {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
> .avatar { > .avatar {
width: 52px; width: 52px;

View File

@ -55,7 +55,7 @@
</div> </div>
<div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div> </div>
</div> </div>
@ -85,7 +85,7 @@
</div> </div>
<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div> </div>
</div> </div>

View File

@ -112,9 +112,17 @@
<MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> <div :class="$style.roleItemMain">
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> <button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
</div> </div>
</div> </div>
</MkFolder> </MkFolder>
@ -220,6 +228,7 @@ const filesPagination = {
userId: props.userId, userId: props.userId,
})), })),
}; };
let expandedRoles = $ref([]);
function createFetcher() { function createFetcher() {
if (iAmModerator) { if (iAmModerator) {
@ -384,6 +393,14 @@ async function unassignRole(role, ev) {
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
} }
function toggleRoleItem(role) {
if (expandedRoles.includes(role.id)) {
expandedRoles = expandedRoles.filter(x => x !== role.id);
} else {
expandedRoles.push(role.id);
}
}
watch(() => props.userId, () => { watch(() => props.userId, () => {
init = createFetcher(); init = createFetcher();
}, { }, {
@ -523,11 +540,22 @@ definePageMetadata(computed(() => ({
} }
.roleItem { .roleItem {
}
.roleItemMain {
display: flex; display: flex;
} }
.role { .role {
flex: 1; flex: 1;
min-width: 0;
margin-right: 8px;
}
.roleItemSub {
padding: 6px 12px;
font-size: 85%;
color: var(--fgTransparentWeak);
} }
.roleUnassign { .roleUnassign {

View File

@ -100,15 +100,15 @@
</dl> </dl>
</div> </div>
<div class="status"> <div class="status">
<MkA v-click-anime :to="userPage(user)"> <MkA :to="userPage(user)">
<b>{{ number(user.notesCount) }}</b> <b>{{ number(user.notesCount) }}</b>
<span>{{ i18n.ts.notes }}</span> <span>{{ i18n.ts.notes }}</span>
</MkA> </MkA>
<MkA v-click-anime :to="userPage(user, 'following')"> <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')">
<b>{{ number(user.followingCount) }}</b> <b>{{ number(user.followingCount) }}</b>
<span>{{ i18n.ts.following }}</span> <span>{{ i18n.ts.following }}</span>
</MkA> </MkA>
<MkA v-click-anime :to="userPage(user, 'followers')"> <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')">
<b>{{ number(user.followersCount) }}</b> <b>{{ number(user.followersCount) }}</b>
<span>{{ i18n.ts.followers }}</span> <span>{{ i18n.ts.followers }}</span>
</MkA> </MkA>
@ -160,6 +160,7 @@ import { dateString } from '@/filters/date';
import { confetti } from '@/scripts/confetti'; import { confetti } from '@/scripts/confetti';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import { api } from '@/os'; import { api } from '@/os';
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe';
const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));

View File

@ -0,0 +1,19 @@
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import { extractUrlFromMfm } from './extract-url-from-mfm';
export function shouldCollapsed(note: misskey.entities.Note): boolean {
const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null;
const collapsed = note.cw == null && note.text != null && (
(note.text.includes('$[x2')) ||
(note.text.includes('$[x3')) ||
(note.text.includes('$[x4')) ||
(note.text.includes('$[scale')) ||
(note.text.split('\n').length > 9) ||
(note.text.length > 500) ||
(note.files.length >= 5) ||
(!!urls && urls.length >= 4)
);
return collapsed;
}

View File

@ -1,3 +1,20 @@
const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => {
const start = performance.now();
const timeoutId = setTimeout(() => {
callback({
didTimeout: false, // polyfill でタイムアウト発火することはない
timeRemaining() {
const diff = performance.now() - start;
return Math.max(0, 50 - diff); // <https://www.w3.org/TR/requestidlecallback/#idle-periods>
},
});
});
return timeoutId;
});
const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => {
clearTimeout(timeoutId);
});
class IdlingRenderScheduler { class IdlingRenderScheduler {
#renderers: Set<FrameRequestCallback>; #renderers: Set<FrameRequestCallback>;
#rafId: number; #rafId: number;

View File

@ -0,0 +1,11 @@
import * as misskey from 'misskey-js';
import { $i } from '@/account';
export function isFfVisibleForMe(user: misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
if (user.ffVisibility === 'private') return false;
if (user.ffVisibility === 'followers' && !user.isFollowing) return false;
return true;
}

File diff suppressed because it is too large Load Diff