Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina 2023-06-16 06:14:29 +00:00
commit 3ec19e0b00
75 changed files with 650 additions and 562 deletions

View file

@ -39,8 +39,22 @@ Please include errors from the developer console and/or server log files if you
<!-- Tell us where on the platform it happens --> <!-- Tell us where on the platform it happens -->
<!-- DO NOT WRITE "latest". Please provide the specific version. --> <!-- DO NOT WRITE "latest". Please provide the specific version. -->
Misskey version: ### 💻 Frontend
PostgreSQL version: * Model and OS of the device(s):
Redis version: <!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
Your OS: * Browser:
Your browser: <!-- Example: Chrome 113.0.5672.126 -->
* Server URL:
<!-- Example: misskey.io -->
* Misskey:
13.x.x
### 🛰 Backend (for server admin)
<!-- If you are using a managed service, put that after the version. -->
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
* Misskey: 13.x.x
* Node: 18.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View file

@ -12,6 +12,27 @@
--> -->
## 13.13.2
### General
- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように
- ロールが付与されているユーザーリストを非公開にできるように
- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました
### Client
- Fix: タブがバックグラウンドでもstreamが切断されないように
### Server
- Fix: キャッシュが溜まり続けないように
## 13.13.1
### Client
- Fix: タブがアクティブな間はstreamが切断されないように
### Server
- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正
## 13.13.0 ## 13.13.0
### General ### General
@ -48,6 +69,7 @@
- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように - Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように
- Fix: お知らせの画像URLを空にできない問題を修正 - Fix: お知らせの画像URLを空にできない問題を修正
- Fix: i/notificationsのsinceIdが機能しない問題を修正 - Fix: i/notificationsのsinceIdが機能しない問題を修正
- Fix: pageのピン留めを解除することができない問題を修正
## 13.12.2 ## 13.12.2
@ -87,11 +109,12 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
## 13.12.0 ## 13.12.0
### NOTE ### NOTE
- Node.js 18.6.0以上が必要になりました - Node.js 18.16.0以上が必要になりました
### General ### General
- アカウントの引っ越し(フォロワー引き継ぎ)に対応 - アカウントの引っ越し(フォロワー引き継ぎ)に対応
- Meilisearchを全文検索に使用できるようになりました - Meilisearchを全文検索に使用できるようになりました
* 「フォロワーのみ」の投稿は検索結果に表示されません。
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加 - 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能 - ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。

View file

@ -169,25 +169,20 @@ describe('After user signed in', () => {
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
// TODO: アイコン設定テスト // TODO: アイコン設定テスト
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-continue]').click();
// プライバシー設定 // プライバシー設定
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-continue]').click();
// フォローはスキップ // フォローはスキップ
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-continue]').click();
// プッシュ通知設定はスキップ // プッシュ通知設定はスキップ
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-continue]').click();
cy.get('[data-cy-user-setup-back]').click();
cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-continue]').click();
}); });
}); });

View file

@ -21,6 +21,8 @@ import './commands'
Cypress.on('uncaught:exception', (err, runnable) => { Cypress.on('uncaught:exception', (err, runnable) => {
if ([ if ([
'The source image cannot be decoded',
// Chrome // Chrome
'ResizeObserver loop limit exceeded', 'ResizeObserver loop limit exceeded',

View file

@ -991,7 +991,7 @@ postToTheChannel: "In Kanal senden"
cannotBeChangedLater: "Kann später nicht mehr geändert werden." cannotBeChangedLater: "Kann später nicht mehr geändert werden."
reactionAcceptance: "Reaktionsannahme" reactionAcceptance: "Reaktionsannahme"
likeOnly: "Nur \"Gefällt mir\"" likeOnly: "Nur \"Gefällt mir\""
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen" likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)"
nonSensitiveOnly: "Keine Sensitiven" nonSensitiveOnly: "Keine Sensitiven"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
rolesAssignedToMe: "Mir zugewiesene Rollen" rolesAssignedToMe: "Mir zugewiesene Rollen"
@ -1062,6 +1062,7 @@ later: "Später"
goToMisskey: "Zu Misskey" goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert" installed: "Installiert"
branding: "Branding"
_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."
@ -1093,7 +1094,7 @@ _accountMigration:
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich." movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich."
postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen." postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen."
movedTo: "Umzugsziel:" movedTo: "Neues Konto:"
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@ -1347,7 +1348,7 @@ _role:
condition: "Bedingung" condition: "Bedingung"
isConditionalRole: "Dies ist eine konditionale Rolle." isConditionalRole: "Dies ist eine konditionale Rolle."
isPublic: "Öffentliche Rolle" isPublic: "Öffentliche Rolle"
descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt." descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt."
options: "Optionen" options: "Optionen"
policies: "Richtlinien" policies: "Richtlinien"
baseRole: "Rollenvorlage" baseRole: "Rollenvorlage"
@ -1356,8 +1357,8 @@ _role:
iconUrl: "Icon-URL" iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen" asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
isExplorable: "Rollenchronik veröffentlichen" isExplorable: "Benutzerliste veröffentlichen"
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht." descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich."
displayOrder: "Position" displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"

View file

@ -991,7 +991,7 @@ postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later." cannotBeChangedLater: "This cannot be changed later."
reactionAcceptance: "Reaction Acceptance" reactionAcceptance: "Reaction Acceptance"
likeOnly: "Only likes" likeOnly: "Only likes"
likeOnlyForRemote: "Only likes for remote instances" likeOnlyForRemote: "All (Only likes for remote instances)"
nonSensitiveOnly: "Non-sensitive only" nonSensitiveOnly: "Non-sensitive only"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
rolesAssignedToMe: "Roles assigned to me" rolesAssignedToMe: "Roles assigned to me"
@ -1062,6 +1062,7 @@ later: "Later"
goToMisskey: "To Misskey" goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries" additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed" installed: "Installed"
branding: "Branding"
_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."
@ -1093,7 +1094,7 @@ _accountMigration:
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed." movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account." postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
movedTo: "Account to move to:" movedTo: "New account:"
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1347,7 +1348,7 @@ _role:
condition: "Condition" condition: "Condition"
isConditionalRole: "This is a conditional role." isConditionalRole: "This is a conditional role."
isPublic: "Public role" isPublic: "Public role"
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users."
options: "Options" options: "Options"
policies: "Policies" policies: "Policies"
baseRole: "Role template" baseRole: "Role template"
@ -1356,8 +1357,8 @@ _role:
iconUrl: "Icon URL" iconUrl: "Icon URL"
asBadge: "Show as badge" asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Role timeline is public" isExplorable: "Make role explorable"
descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set." descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
displayOrder: "Position" displayOrder: "Position"
descriptionOfDisplayOrder: "The higher the number, the higher its UI position." descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"

1
locales/index.d.ts vendored
View file

@ -1065,6 +1065,7 @@ export interface Locale {
"goToMisskey": string; "goToMisskey": string;
"additionalEmojiDictionary": string; "additionalEmojiDictionary": string;
"installed": string; "installed": string;
"branding": string;
"_initialAccountSetting": { "_initialAccountSetting": {
"accountCreated": string; "accountCreated": string;
"letsStartAccountSetup": string; "letsStartAccountSetup": string;

View file

@ -1062,6 +1062,7 @@ later: "あとで"
goToMisskey: "Misskeyへ" goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書" additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み" installed: "インストール済み"
branding: "ブランディング"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"
@ -1351,8 +1352,8 @@ _role:
conditional: "コンディショナル" conditional: "コンディショナル"
condition: "条件" condition: "条件"
isConditionalRole: "これはコンディショナルロールです。" isConditionalRole: "これはコンディショナルロールです。"
isPublic: "ロールを公開" isPublic: "公開ロール"
descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。"
options: "オプション" options: "オプション"
policies: "ポリシー" policies: "ポリシー"
baseRole: "ベースロール" baseRole: "ベースロール"
@ -1361,8 +1362,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示" asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
isExplorable: "ロールタイムラインを公開" isExplorable: "ユーザーを見つけやすくする"
descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。" descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
displayOrder: "表示順" displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"

View file

@ -792,6 +792,7 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで"
noBotProtectionWarning: "Botプロテクションが設定されてへんで。" noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
configure: "設定する" configure: "設定する"
postToGallery: "ギャラリーへ投稿" postToGallery: "ギャラリーへ投稿"
postToHashtag: "このハッシュタグで投稿"
gallery: "ギャラリー" gallery: "ギャラリー"
recentPosts: "最近の投稿" recentPosts: "最近の投稿"
popularPosts: "人気の投稿" popularPosts: "人気の投稿"
@ -825,6 +826,7 @@ translatedFrom: "{x}から翻訳するで"
accountDeletionInProgress: "アカウント削除しとるで待っとってなー" accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。" usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
aiChanMode: "藍モードやで" aiChanMode: "藍モードやで"
devMode: "開発者モード"
keepCw: "CWを維持するで" keepCw: "CWを維持するで"
pubSub: "Pub/Subのアカウント" pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信" lastCommunication: "直近の通信"
@ -834,6 +836,8 @@ breakFollow: "フォロワーを解除するで"
breakFollowConfirm: "フォロワー解除してもええか?" breakFollowConfirm: "フォロワー解除してもええか?"
itsOn: "オンになっとるよ" itsOn: "オンになっとるよ"
itsOff: "オフになってるで" itsOff: "オフになってるで"
on: "オン"
off: "オフ"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
unread: "未読" unread: "未読"
filter: "フィルタ" filter: "フィルタ"
@ -988,6 +992,8 @@ cannotBeChangedLater: "後からは変えられへんで。"
reactionAcceptance: "ツッコミの受け入れ" reactionAcceptance: "ツッコミの受け入れ"
likeOnly: "いいねだけ" likeOnly: "いいねだけ"
likeOnlyForRemote: "リモートからはいいねだけな" likeOnlyForRemote: "リモートからはいいねだけな"
nonSensitiveOnly: "センシティブじゃないやつだけ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)"
rolesAssignedToMe: "自分に割り当てられたロール" rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?" resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語" sensitiveWords: "けったいな単語"
@ -1045,10 +1051,17 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション" options: "オプション"
specifyUser: "ユーザー指定" specifyUser: "ユーザー指定"
failedToPreviewUrl: "プレビューできへん"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール" rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。"
cancelReactionConfirm: "ツッコむんをやっぱやめるか?" cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
changeReactionConfirm: "ツッコミを別のに変えるか?" changeReactionConfirm: "ツッコミを別のに変えるか?"
later: "あとで"
goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1063,6 +1076,7 @@ _initialAccountSetting:
haveFun: "{name}、楽しんでな~" haveFun: "{name}、楽しんでな~"
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。" ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
skipAreYouSure: "初期設定飛ばすか?" skipAreYouSure: "初期設定飛ばすか?"
laterAreYouSure: "初期設定あとでやり直すん?"
_serverRules: _serverRules:
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。" description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
_accountMigration: _accountMigration:

View file

@ -870,7 +870,7 @@ instanceDefaultLightTheme: "서버 기본 라이트 테마"
instanceDefaultDarkTheme: "서버 기본 다크 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마"
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
mutePeriod: "뮤트할 기간" mutePeriod: "뮤트할 기간"
period: "투표 기한" period: "기간"
indefinitely: "무기한" indefinitely: "무기한"
tenMinutes: "10분" tenMinutes: "10분"
oneHour: "1시간" oneHour: "1시간"

View file

@ -1,6 +1,7 @@
--- ---
_lang_: "Türkçe" _lang_: "Türkçe"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün" monthAndDay: "{month}Ay {day}Gün"
search: "Arama" search: "Arama"
notifications: "Bildirim" notifications: "Bildirim"
@ -13,7 +14,9 @@ cancel: "İptal"
enterUsername: "Kullanıcı adınızı giriniz" enterUsername: "Kullanıcı adınızı giriniz"
noNotes: "Notlar mevcut değil." noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor" noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu"
settings: "Ayarlar" settings: "Ayarlar"
notificationSettings: "Bildirim Ayarları"
basicSettings: "Temel Ayarlar" basicSettings: "Temel Ayarlar"
otherSettings: "Diğer Ayarlar" otherSettings: "Diğer Ayarlar"
openInWindow: "Bir pencere ile aç" openInWindow: "Bir pencere ile aç"
@ -21,9 +24,11 @@ profile: "Profil"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı" noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı"
login: "Giriş Yap " login: "Giriş Yap "
loggingIn: "Oturum aç"
logout: ıkış Yap" logout: ıkış Yap"
signup: "Kayıt Ol" signup: "Kayıt Ol"
uploading: "Yükleniyor" uploading: "Yükleniyor"
save: "Kaydet"
users: "Kullanıcı" users: "Kullanıcı"
addUser: "Kullanıcı Ekle" addUser: "Kullanıcı Ekle"
favorite: "Favoriler" favorite: "Favoriler"
@ -31,6 +36,7 @@ favorites: "Favoriler"
unfavorite: "Favorilerden Kaldır" unfavorite: "Favorilerden Kaldır"
favorited: "Favorilerime eklendi." favorited: "Favorilerime eklendi."
alreadyFavorited: "Zaten favorilerinizde kayıtlı." alreadyFavorited: "Zaten favorilerinizde kayıtlı."
cantFavorite: "Favorilere kayıt yapılamadı"
pin: "Sabitlenmiş" pin: "Sabitlenmiş"
unpin: "Sabitlemeyi kaldır" unpin: "Sabitlemeyi kaldır"
copyContent: "İçeriği kopyala" copyContent: "İçeriği kopyala"
@ -40,23 +46,88 @@ deleteAndEdit: "Sil ve yeniden düzenle"
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
addToList: "Listeye ekle" addToList: "Listeye ekle"
sendMessage: "Mesaj Gönder" sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala" copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "KullanıcıyıKopyala"
copyNoteId: "Kimlik notunu kopyala"
searchUser: "Kullanıcıları ara" searchUser: "Kullanıcıları ara"
reply: "yanıt"
loadMore: "Devamını yükle"
showMore: "Devamını yükle"
lists: "Listeler"
noLists: "Liste yok"
note: "not"
notes: "notlar"
following: "takipçi"
followers: "takipçi"
followsYou: "seni takip ediyor"
createList: "Liste oluştur"
manageLists: "Yönetici Listeleri"
error: "hata"
follow: "takipçi"
followRequest: "Takip isteği"
followRequests: "Takip istekleri"
unfollow: "takip etmeyi bırak"
followRequestPending: "Bekleyen Takip Etme Talebi"
enterEmoji: "Emoji Giriniz"
renote: "vazgeçme"
unrenote: "not alma"
renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim"
quote: "alıntı"
pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş" pinned: "Sabitlenmiş"
you: "sen"
unmute: "sesi aç"
renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et"
block: "engelle"
unblock: "engellemeyi kaldır"
suspend: "askıya al"
unsuspend: "askıya alma"
blockConfirm: "Onayı engelle"
unblockConfirm: "engellemeyi kaldır onayla"
selectChannel: "Kanal seç"
flagAsBot: "Bot olarak işaretle"
instances: "Sunucu"
remove: "Sil" remove: "Sil"
pinnedNotes: "Sabitlenen"
userList: "Listeler"
smtpUser: "Kullanıcı Adı" smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre" smtpPass: "Şifre"
user: "Kullanıcı" user: "Kullanıcı"
searchByGoogle: "Arama" searchByGoogle: "Arama"
_theme:
keys:
renote: "vazgeçme"
_sfx: _sfx:
note: "notlar"
notification: "Bildirim" notification: "Bildirim"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
notifications: "Bildirim" notifications: "Bildirim"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
_cw:
show: "Devamını yükle"
_visibility:
followers: "takipçi"
_profile: _profile:
username: "Kullanıcı Adı" username: "Kullanıcı Adı"
_exportOrImport:
followingList: "takipçi"
blockingList: "engelle"
userLists: "Listeler"
_notification:
_types:
follow: "takipçi"
renote: "vazgeçme"
quote: "alıntı"
_actions:
reply: "yanıt"
renote: "vazgeçme"
_deck: _deck:
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"
list: "Listeler"

View file

@ -1060,6 +1060,7 @@ cancelReactionConfirm: "要取消回应吗?"
changeReactionConfirm: "要更改回应吗?" changeReactionConfirm: "要更改回应吗?"
later: "一会再说" later: "一会再说"
goToMisskey: "去往Misskey" goToMisskey: "去往Misskey"
additionalEmojiDictionary: "表情符号追加字典"
installed: "已安装" installed: "已安装"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "账户创建完成了!" accountCreated: "账户创建完成了!"

View file

@ -1060,6 +1060,9 @@ cancelReactionConfirm: "要取消做出的反應嗎?"
changeReactionConfirm: "要變更做出的反應嗎?" changeReactionConfirm: "要變更做出的反應嗎?"
later: "稍後再說" later: "稍後再說"
goToMisskey: "往Misskey" goToMisskey: "往Misskey"
additionalEmojiDictionary: "表情符號的附加辭典"
installed: "已安裝"
branding: "品牌宣傳"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "帳戶已建立完成!" accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsStartAccountSetup: "來進行帳戶的初始設定吧。"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.13.0-beta.7", "version": "13.13.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,17 @@
export class ErrorImageUrl1685973839966 {
name = 'ErrorImageUrl1685973839966'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`);
}
}

View file

@ -168,6 +168,17 @@ export class CacheService implements OnApplicationShutdown {
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);
this.userByIdCache.dispose();
this.localUserByNativeTokenCache.dispose();
this.localUserByIdCache.dispose();
this.uriPersonCache.dispose();
this.userProfileCache.dispose();
this.userMutingsCache.dispose();
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowingChannelsCache.dispose();
} }
@bindThis @bindThis

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm'; import { DataSource, In, IsNull } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService implements OnApplicationShutdown {
private cache: MemoryKVCache<Emoji | null>; private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>; public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
@ -349,4 +349,14 @@ export class CustomEmojiService {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji); this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
} }
} }
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FederatedInstanceService { export class FederatedInstanceService implements OnApplicationShutdown {
public federatedInstanceCache: RedisKVCache<Instance | null>; public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor( constructor(
@ -77,4 +77,14 @@ export class FederatedInstanceService {
this.federatedInstanceCache.set(result.host, result); this.federatedInstanceCache.set(result.host, result);
} }
@bindThis
public dispose(): void {
this.federatedInstanceCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import push from 'web-push'; import push from 'web-push';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
} }
@Injectable() @Injectable()
export class PushNotificationService { export class PushNotificationService implements OnApplicationShutdown {
private subscriptionsCache: RedisKVCache<SwSubscription[]>; private subscriptionsCache: RedisKVCache<SwSubscription[]>;
constructor( constructor(
@ -115,4 +115,14 @@ export class PushNotificationService {
}); });
} }
} }
@bindThis
public dispose(): void {
this.subscriptionsCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -435,6 +435,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);
this.roleAssignmentByUserIdCache.dispose();
} }
@bindThis @bindThis

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js'; import type { UserKeypairsRepository } from '@/models/index.js';
@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class UserKeypairService { export class UserKeypairService implements OnApplicationShutdown {
private cache: RedisKVCache<UserKeypair>; private cache: RedisKVCache<UserKeypair>;
constructor( constructor(
@ -31,4 +31,14 @@ export class UserKeypairService {
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> { public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId); return await this.cache.fetch(userId);
} }
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import escapeRegexp from 'escape-regexp'; import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
@ -30,7 +30,7 @@ export type UriParseResult = {
}; };
@Injectable() @Injectable()
export class ApDbResolverService { export class ApDbResolverService implements OnApplicationShutdown {
private publicKeyCache: MemoryKVCache<UserPublickey | null>; private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>; private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
@ -162,4 +162,15 @@ export class ApDbResolverService {
key, key,
}; };
} }
@bindThis
public dispose(): void {
this.publicKeyCache.dispose();
this.publicKeyByUserIdCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -83,6 +83,16 @@ export class RedisKVCache<T> {
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
} }
@bindThis
public gc() {
this.memoryCache.gc();
}
@bindThis
public dispose() {
this.memoryCache.dispose();
}
} }
export class RedisSingleCache<T> { export class RedisSingleCache<T> {
@ -174,10 +184,15 @@ export class RedisSingleCache<T> {
export class MemoryKVCache<T> { export class MemoryKVCache<T> {
public cache: Map<string, { date: number; value: T; }>; public cache: Map<string, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
private gcIntervalHandle: NodeJS.Timer;
constructor(lifetime: MemoryKVCache<never>['lifetime']) { constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
this.gcIntervalHandle = setInterval(() => {
this.gc();
}, 1000 * 60 * 3);
} }
@bindThis @bindThis
@ -200,7 +215,7 @@ export class MemoryKVCache<T> {
} }
@bindThis @bindThis
public delete(key: string) { public delete(key: string): void {
this.cache.delete(key); this.cache.delete(key);
} }
@ -255,6 +270,21 @@ export class MemoryKVCache<T> {
} }
return value; return value;
} }
@bindThis
public gc(): void {
const now = Date.now();
for (const [key, { date }] of this.cache.entries()) {
if ((now - date) > this.lifetime) {
this.cache.delete(key);
}
}
}
@bindThis
public dispose(): void {
clearInterval(this.gcIntervalHandle);
}
} }
export class MemorySingleCache<T> { export class MemorySingleCache<T> {

View file

@ -101,13 +101,25 @@ export class Meta {
length: 1024, length: 1024,
nullable: true, nullable: true,
}) })
public errorImageUrl: string | null; public iconUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
nullable: true, nullable: true,
}) })
public iconUrl: string | null; public serverErrorImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public notFoundImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public infoImageUrl: string | null;
@Column('boolean', { @Column('boolean', {
default: true, default: true,

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
@ -17,7 +17,7 @@ export class AuthenticationError extends Error {
} }
@Injectable() @Injectable()
export class AuthenticateService { export class AuthenticateService implements OnApplicationShutdown {
private appCache: MemoryKVCache<App>; private appCache: MemoryKVCache<App>;
constructor( constructor(
@ -85,4 +85,14 @@ export class AuthenticateService {
} }
} }
} }
@bindThis
public dispose(): void {
this.appCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -333,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
@ -674,7 +673,6 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas
const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
@ -1019,7 +1017,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_searchByUsernameAndHost, $users_searchByUsernameAndHost,
$users_search, $users_search,
$users_show, $users_show,
$users_stats,
$users_achievements, $users_achievements,
$users_updateMemo, $users_updateMemo,
$fetchRss, $fetchRss,
@ -1356,7 +1353,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_searchByUsernameAndHost, $users_searchByUsernameAndHost,
$users_search, $users_search,
$users_show, $users_show,
$users_stats,
$users_achievements, $users_achievements,
$users_updateMemo, $users_updateMemo,
$fetchRss, $fetchRss,

View file

@ -128,26 +128,27 @@ export class StreamingApiServerService {
ev.removeAllListeners(); ev.removeAllListeners();
stream.dispose(); stream.dispose();
this.redisForSub.off('message', onRedisMessage); this.redisForSub.off('message', onRedisMessage);
this.#connections.delete(connection);
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
}); });
connection.on('message', async (data) => { connection.on('pong', () => {
this.#connections.set(connection, Date.now()); this.#connections.set(connection, Date.now());
if (data.toString() === 'ping') {
connection.send('pong');
}
}); });
}); });
// 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする
this.#cleanConnectionsIntervalId = setInterval(() => { this.#cleanConnectionsIntervalId = setInterval(() => {
const now = Date.now(); const now = Date.now();
for (const [connection, lastActive] of this.#connections.entries()) { for (const [connection, lastActive] of this.#connections.entries()) {
if (now - lastActive > 1000 * 60 * 5) { if (now - lastActive > 1000 * 60 * 2) {
connection.terminate(); connection.terminate();
this.#connections.delete(connection); this.#connections.delete(connection);
} else {
connection.ping();
} }
} }
}, 1000 * 60 * 5); }, 1000 * 60);
} }
@bindThis @bindThis

View file

@ -42,7 +42,9 @@ export default class extends Endpoint<'admin/meta'> {
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl, mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl, bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl, serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
infoImageUrl: instance.infoImageUrl,
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl, backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl, logoImageUrl: instance.logoImageUrl,

View file

@ -56,6 +56,18 @@ export default class extends Endpoint<'admin/update-meta'> {
set.iconUrl = ps.iconUrl; set.iconUrl = ps.iconUrl;
} }
if (ps.serverErrorImageUrl !== undefined) {
set.serverErrorImageUrl = ps.serverErrorImageUrl;
}
if (ps.infoImageUrl !== undefined) {
set.infoImageUrl = ps.infoImageUrl;
}
if (ps.notFoundImageUrl !== undefined) {
set.notFoundImageUrl = ps.notFoundImageUrl;
}
if (ps.backgroundImageUrl !== undefined) { if (ps.backgroundImageUrl !== undefined) {
set.backgroundImageUrl = ps.backgroundImageUrl; set.backgroundImageUrl = ps.backgroundImageUrl;
} }
@ -188,10 +200,6 @@ export default class extends Endpoint<'admin/update-meta'> {
set.smtpPass = ps.smtpPass; set.smtpPass = ps.smtpPass;
} }
if (ps.errorImageUrl !== undefined) {
set.errorImageUrl = ps.errorImageUrl;
}
if (ps.enableServiceWorker !== undefined) { if (ps.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker; set.enableServiceWorker = ps.enableServiceWorker;
} }

View file

@ -146,7 +146,7 @@ export const paramDef = {
alwaysMarkNsfw: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' }, autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id' }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' }, mutedWords: { type: 'array' },
mutedInstances: { type: 'array', items: { mutedInstances: { type: 'array', items: {
type: 'string', type: 'string',

View file

@ -1,6 +1,6 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as JSON5 from 'json5'; import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js'; import type { AdsRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -124,10 +124,17 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
errorImageUrl: { serverErrorImageUrl: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: true,
default: 'https://xn--931a.moe/aiart/yubitun.png', },
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string',
optional: false, nullable: true,
}, },
iconUrl: { iconUrl: {
type: 'string', type: 'string',
@ -288,7 +295,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl, mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl, bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl, infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl, backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl, logoImageUrl: instance.logoImageUrl,

View file

@ -30,6 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const roles = await this.rolesRepository.findBy({ const roles = await this.rolesRepository.findBy({
isPublic: true, isPublic: true,
isExplorable: true,
}); });
return await this.roleEntityService.packMany(roles, me); return await this.roleEntityService.packMany(roles, me);
}); });

View file

@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const role = await this.rolesRepository.findOneBy({ const role = await this.rolesRepository.findOneBy({
id: ps.roleId, id: ps.roleId,
isPublic: true, isPublic: true,
isExplorable: true,
}); });
if (role == null) { if (role == null) {

View file

@ -1,228 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/index.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['users'],
requireCredential: false,
description: 'Show statistics about a user.',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819',
},
},
res: {
type: 'object',
optional: false, nullable: false,
properties: {
notesCount: {
type: 'integer',
optional: false, nullable: false,
},
repliesCount: {
type: 'integer',
optional: false, nullable: false,
},
renotesCount: {
type: 'integer',
optional: false, nullable: false,
},
repliedCount: {
type: 'integer',
optional: false, nullable: false,
},
renotedCount: {
type: 'integer',
optional: false, nullable: false,
},
pollVotesCount: {
type: 'integer',
optional: false, nullable: false,
},
pollVotedCount: {
type: 'integer',
optional: false, nullable: false,
},
localFollowingCount: {
type: 'integer',
optional: false, nullable: false,
},
remoteFollowingCount: {
type: 'integer',
optional: false, nullable: false,
},
localFollowersCount: {
type: 'integer',
optional: false, nullable: false,
},
remoteFollowersCount: {
type: 'integer',
optional: false, nullable: false,
},
followingCount: {
type: 'integer',
optional: false, nullable: false,
},
followersCount: {
type: 'integer',
optional: false, nullable: false,
},
sentReactionsCount: {
type: 'integer',
optional: false, nullable: false,
},
receivedReactionsCount: {
type: 'integer',
optional: false, nullable: false,
},
noteFavoritesCount: {
type: 'integer',
optional: false, nullable: false,
},
pageLikesCount: {
type: 'integer',
optional: false, nullable: false,
},
pageLikedCount: {
type: 'integer',
optional: false, nullable: false,
},
driveFilesCount: {
type: 'integer',
optional: false, nullable: false,
},
driveUsage: {
type: 'integer',
optional: false, nullable: false,
description: 'Drive usage in bytes',
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.pageLikesRepository)
private pageLikesRepository: PageLikesRepository,
@Inject(DI.noteFavoritesRepository)
private noteFavoritesRepository: NoteFavoritesRepository,
@Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository,
private driveFileEntityService: DriveFileEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
const result = await awaitAll({
notesCount: this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
repliesCount: this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.andWhere('note.replyId IS NOT NULL')
.getCount(),
renotesCount: this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.andWhere('note.renoteId IS NOT NULL')
.getCount(),
repliedCount: this.notesRepository.createQueryBuilder('note')
.where('note.replyUserId = :userId', { userId: user.id })
.getCount(),
renotedCount: this.notesRepository.createQueryBuilder('note')
.where('note.renoteUserId = :userId', { userId: user.id })
.getCount(),
pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote')
.where('vote.userId = :userId', { userId: user.id })
.getCount(),
pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote')
.innerJoin('vote.note', 'note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
localFollowingCount: this.followingsRepository.createQueryBuilder('following')
.where('following.followerId = :userId', { userId: user.id })
.andWhere('following.followeeHost IS NULL')
.getCount(),
remoteFollowingCount: this.followingsRepository.createQueryBuilder('following')
.where('following.followerId = :userId', { userId: user.id })
.andWhere('following.followeeHost IS NOT NULL')
.getCount(),
localFollowersCount: this.followingsRepository.createQueryBuilder('following')
.where('following.followeeId = :userId', { userId: user.id })
.andWhere('following.followerHost IS NULL')
.getCount(),
remoteFollowersCount: this.followingsRepository.createQueryBuilder('following')
.where('following.followeeId = :userId', { userId: user.id })
.andWhere('following.followerHost IS NOT NULL')
.getCount(),
sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction')
.where('reaction.userId = :userId', { userId: user.id })
.getCount(),
receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction')
.innerJoin('reaction.note', 'note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite')
.where('favorite.userId = :userId', { userId: user.id })
.getCount(),
pageLikesCount: this.pageLikesRepository.createQueryBuilder('like')
.where('like.userId = :userId', { userId: user.id })
.getCount(),
pageLikedCount: this.pageLikesRepository.createQueryBuilder('like')
.innerJoin('like.page', 'page')
.where('page.userId = :userId', { userId: user.id })
.getCount(),
driveFilesCount: this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id })
.getCount(),
driveUsage: this.driveFileEntityService.calcDriveUsageOf(user),
});
return {
...result,
followingCount: result.localFollowingCount + result.remoteFollowingCount,
followersCount: result.localFollowersCount + result.remoteFollowersCount,
};
});
}
}

View file

@ -26,7 +26,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, Meta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -117,6 +117,18 @@ export class ClientServerService {
return (res); return (res);
} }
@bindThis
private generateCommonPugData(meta: Meta) {
return {
instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
};
}
@bindThis @bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.register(fastifyCookie, {}); fastify.register(fastifyCookie, {});
@ -341,12 +353,10 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', { return await reply.view('base', {
img: meta.bannerUrl, img: meta.bannerUrl,
title: meta.name ?? 'Misskey',
instanceName: meta.name ?? 'Misskey',
url: this.config.url, url: this.config.url,
title: meta.name ?? 'Misskey',
desc: meta.description, desc: meta.description,
icon: meta.iconUrl, ...this.generateCommonPugData(meta),
themeColor: meta.themeColor,
}); });
}; };
@ -431,9 +441,7 @@ export class ClientServerService {
user, profile, me, user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub, sub: request.params.sub,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
// リモートユーザーなので // リモートユーザーなので
@ -481,9 +489,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl, avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting // TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note), summary: getNoteSummary(_note),
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -522,9 +528,7 @@ export class ClientServerService {
page: _page, page: _page,
profile, profile,
avatarUrl: _page.user.avatarUrl, avatarUrl: _page.user.avatarUrl,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -550,9 +554,7 @@ export class ClientServerService {
flash: _flash, flash: _flash,
profile, profile,
avatarUrl: _flash.user.avatarUrl, avatarUrl: _flash.user.avatarUrl,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -578,9 +580,7 @@ export class ClientServerService {
clip: _clip, clip: _clip,
profile, profile,
avatarUrl: _clip.user.avatarUrl, avatarUrl: _clip.user.avatarUrl,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -604,9 +604,7 @@ export class ClientServerService {
post: _post, post: _post,
profile, profile,
avatarUrl: _post.user.avatarUrl, avatarUrl: _post.user.avatarUrl,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -625,9 +623,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', { return await reply.view('channel', {
channel: _channel, channel: _channel,
instanceName: meta.name ?? 'Misskey', ...this.generateCommonPugData(meta),
icon: meta.iconUrl,
themeColor: meta.themeColor,
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);

View file

@ -31,9 +31,9 @@ html
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842 //- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0') link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)

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.type.isSensitive) - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive)
- const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive) - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive)
block title block title
= `${title} | ${instanceName}` = `${title} | ${instanceName}`

View file

@ -2,7 +2,7 @@
<MkPagination :pagination="pagination"> <MkPagination :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.notFound }}</div> <div>{{ i18n.ts.notFound }}</div>
</div> </div>
</template> </template>
@ -17,6 +17,7 @@
import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { infoImageUrl } from '@/instance';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
pagination: Paging; pagination: Paging;

View file

@ -2,7 +2,7 @@
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noNotes }}</div> <div>{{ i18n.ts.noNotes }}</div>
</div> </div>
</template> </template>
@ -32,6 +32,7 @@ import MkNote from '@/components/MkNote.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { infoImageUrl } from '@/instance';
const props = defineProps<{ const props = defineProps<{
pagination: Paging; pagination: Paging;

View file

@ -2,7 +2,7 @@
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noNotifications }}</div> <div>{{ i18n.ts.noNotifications }}</div>
</div> </div>
</template> </template>
@ -26,6 +26,7 @@ import { useStream } from '@/stream';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import { infoImageUrl } from '@/instance';
const props = defineProps<{ const props = defineProps<{
includeTypes?: typeof notificationTypes[number][]; includeTypes?: typeof notificationTypes[number][];

View file

@ -13,7 +13,7 @@
<div v-else-if="empty" key="_empty_" class="empty"> <div v-else-if="empty" key="_empty_" class="empty">
<slot name="empty"> <slot name="empty">
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
</slot> </slot>
@ -73,6 +73,8 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
}; };
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { infoImageUrl } from '@/instance';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
pagination: Paging; pagination: Paging;
disableAutoLoad?: boolean; disableAutoLoad?: boolean;

View file

@ -11,7 +11,7 @@
<MkSpacer :marginMin="20" :marginMax="28"> <MkSpacer :marginMin="20" :marginMax="28">
<div v-if="note" class="_gaps"> <div v-if="note" class="_gaps">
<div v-if="reactions.length === 0" class="_fullinfo"> <div v-if="reactions.length === 0" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
<template v-else> <template v-else>
@ -42,6 +42,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
import { infoImageUrl } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'closed'): void, (ev: 'closed'): void,

View file

@ -11,7 +11,7 @@
<MkSpacer :marginMin="20" :marginMax="28"> <MkSpacer :marginMin="20" :marginMax="28">
<div v-if="renotes" class="_gaps"> <div v-if="renotes" class="_gaps">
<div v-if="renotes.length === 0" class="_fullinfo"> <div v-if="renotes.length === 0" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
<template v-else> <template v-else>
@ -35,6 +35,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
import { infoImageUrl } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'closed'): void, (ev: 'closed'): void,

View file

@ -2,7 +2,7 @@
<MkPagination :pagination="pagination"> <MkPagination :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noUsers }}</div> <div>{{ i18n.ts.noUsers }}</div>
</div> </div>
</template> </template>
@ -19,6 +19,7 @@
import MkUserInfo from '@/components/MkUserInfo.vue'; import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { infoImageUrl } from '@/instance';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
pagination: Paging; pagination: Paging;

View file

@ -22,7 +22,7 @@
<div :class="$style.username"><MkAcct :user="user"/></div> <div :class="$style.username"><MkAcct :user="user"/></div>
</div> </div>
<div :class="$style.description"> <div :class="$style.description">
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/> <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div> </div>
<div :class="$style.status"> <div :class="$style.status">
@ -192,6 +192,13 @@ onMounted(() => {
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
} }
.mfm {
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
.status { .status {
padding: 16px 26px 16px 26px; padding: 16px 26px 16px 26px;
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/> <img :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]"> <div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft"> <div :class="$style.earLeft">
@ -24,7 +24,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue'; import { watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue'; import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy'; import { getStaticImageUrl } from '@/scripts/media-proxy';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';

View file

@ -13,13 +13,20 @@ interface Props {
const contentSymbol = Symbol(); const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => { const observer = new ResizeObserver((entries) => {
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
for (const entry of entries) { for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement; const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol]; const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement; const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width; const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width; const containerWidth = container.getBoundingClientRect().width;
container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`; results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` });
}
for (const result of results) {
result.container.style.transform = result.transform;
} }
}); });
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton> <MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
</div> </div>
@ -12,6 +12,7 @@
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { serverErrorImageUrl } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'retry'): void; (ev: 'retry'): void;

View file

@ -75,3 +75,7 @@ export const ROLE_POLICIES = [
//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM'); //export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM');
export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';

View file

@ -1,7 +1,8 @@
import { reactive } from 'vue'; import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { api } from './os'; import { api } from './os';
import { miLocalStorage } from './local-storage'; import { miLocalStorage } from './local-storage';
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -13,6 +14,12 @@ export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSO
// TODO: set default values // TODO: set default values
}); });
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
export async function fetchInstance() { export async function fetchInstance() {
const meta = await api('meta', { const meta = await api('meta', {
detail: false, detail: false,

View file

@ -2,7 +2,7 @@
<MkLoading v-if="!loaded"/> <MkLoading v-if="!loaded"/>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
<div v-show="loaded" :class="$style.root"> <div v-show="loaded" :class="$style.root">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost" :class="$style.img"/> <img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
<div class="_gaps"> <div class="_gaps">
<p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> <p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
@ -30,6 +30,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { serverErrorImageUrl } from '@/instance';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
error?: Error; error?: Error;

View file

@ -0,0 +1,133 @@
<template>
<div>
<MkStickyContainer>
<template #header><XHeader :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<MkInput v-model="iconUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</MkInput>
<MkInput v-model="bannerUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</MkInput>
<MkInput v-model="backgroundImageUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</MkInput>
<MkInput v-model="notFoundImageUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.notFoundDescription }}</template>
</MkInput>
<MkInput v-model="infoImageUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.nothing }}</template>
</MkInput>
<MkInput v-model="serverErrorImageUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.somethingHappened }}</template>
</MkInput>
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
</MkColorInput>
<MkTextarea v-model="defaultLightTheme">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<MkTextarea v-model="defaultDarkTheme">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</div>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
let iconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
let serverErrorImageUrl: string | null = $ref(null);
let infoImageUrl: string | null = $ref(null);
let notFoundImageUrl: string | null = $ref(null);
async function init() {
const meta = await os.api('admin/meta');
iconUrl = meta.iconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
defaultLightTheme = meta.defaultLightTheme;
defaultDarkTheme = meta.defaultDarkTheme;
serverErrorImageUrl = meta.serverErrorImageUrl;
infoImageUrl = meta.infoImageUrl;
notFoundImageUrl = meta.notFoundImageUrl;
}
function save() {
os.apiWithDialog('admin/update-meta', {
iconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
infoImageUrl,
notFoundImageUrl,
serverErrorImageUrl,
}).then(() => {
fetchInstance();
});
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.branding,
icon: 'ti ti-paint',
});
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -143,6 +143,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.general, text: i18n.ts.general,
to: '/admin/settings', to: '/admin/settings',
active: currentPage?.route.name === 'settings', active: currentPage?.route.name === 'settings',
}, {
icon: 'ti ti-paint',
text: i18n.ts.branding,
to: '/admin/branding',
active: currentPage?.route.name === 'branding',
}, { }, {
icon: 'ti ti-shield', icon: 'ti ti-shield',
text: i18n.ts.moderation, text: i18n.ts.moderation,

View file

@ -23,7 +23,7 @@
<MkPagination :pagination="usersPagination"> <MkPagination :pagination="usersPagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noUsers }}</div> <div>{{ i18n.ts.noUsers }}</div>
</div> </div>
</template> </template>
@ -69,6 +69,7 @@ import MkButton from '@/components/MkButton.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { infoImageUrl } from '@/instance';
const router = useRouter(); const router = useRouter();

View file

@ -29,41 +29,6 @@
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</MkTextarea> </MkTextarea>
<FormSection>
<template #label>{{ i18n.ts.theme }}</template>
<div class="_gaps_m">
<MkInput v-model="iconUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</MkInput>
<MkInput v-model="bannerUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</MkInput>
<MkInput v-model="backgroundImageUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</MkInput>
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
</MkColorInput>
<MkTextarea v-model="defaultLightTheme">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<MkTextarea v-model="defaultDarkTheme">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</div>
</FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.files }}</template> <template #label>{{ i18n.ts.files }}</template>
@ -145,12 +110,6 @@ let name: string | null = $ref(null);
let description: string | null = $ref(null); let description: string | null = $ref(null);
let maintainerName: string | null = $ref(null); let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null); let maintainerEmail: string | null = $ref(null);
let iconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
let pinnedUsers: string = $ref(''); let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false); let cacheRemoteFiles: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false);
@ -163,12 +122,6 @@ async function init() {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
name = meta.name; name = meta.name;
description = meta.description; description = meta.description;
iconUrl = meta.iconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
defaultLightTheme = meta.defaultLightTheme;
defaultDarkTheme = meta.defaultDarkTheme;
maintainerName = meta.maintainerName; maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail; maintainerEmail = meta.maintainerEmail;
pinnedUsers = meta.pinnedUsers.join('\n'); pinnedUsers = meta.pinnedUsers.join('\n');
@ -184,12 +137,6 @@ function save() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
name, name,
description, description,
iconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
maintainerName, maintainerName,
maintainerEmail, maintainerEmail,
pinnedUsers: pinnedUsers.split('\n'), pinnedUsers: pinnedUsers.split('\n'),

View file

@ -5,7 +5,7 @@
<MkPagination :pagination="pagination"> <MkPagination :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noNotes }}</div> <div>{{ i18n.ts.noNotes }}</div>
</div> </div>
</template> </template>
@ -26,6 +26,7 @@ import MkNote from '@/components/MkNote.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { infoImageUrl } from '@/instance';
const pagination = { const pagination = {
endpoint: 'i/favorites' as const, endpoint: 'i/favorites' as const,

View file

@ -5,7 +5,7 @@
<MkPagination ref="paginationComponent" :pagination="pagination"> <MkPagination ref="paginationComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noFollowRequests }}</div> <div>{{ i18n.ts.noFollowRequests }}</div>
</div> </div>
</template> </template>
@ -39,6 +39,7 @@ import { userPage, acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { infoImageUrl } from '@/instance';
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();

View file

@ -3,7 +3,7 @@
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
<i class="ti ti-alert-triangle"></i> <i class="ti ti-alert-triangle"></i>
{{ i18n.ts.nothing }} {{ i18n.ts.nothing }}
@ -36,6 +36,7 @@ import { i18n } from '@/i18n';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { serverErrorImageUrl } from '@/instance';
const props = defineProps<{ const props = defineProps<{
listId: string; listId: string;

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/> <img :src="notFoundImageUrl" class="_ghost"/>
<div>{{ i18n.ts.notFoundDescription }}</div> <div>{{ i18n.ts.notFoundDescription }}</div>
</div> </div>
</div> </div>
@ -10,6 +10,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { notFoundImageUrl } from '@/instance';
const headerActions = $computed(() => []); const headerActions = $computed(() => []);

View file

@ -3,7 +3,7 @@
<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
<div :class="$style.root"> <div :class="$style.root">
<img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<p :class="$style.text"> <p :class="$style.text">
<i class="ti ti-alert-triangle"></i> <i class="ti ti-alert-triangle"></i>
{{ error }} {{ error }}
@ -30,6 +30,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
import { instanceName } from '@/config'; import { instanceName } from '@/config';
import { serverErrorImageUrl } from '@/instance';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
role: string; role: string;

View file

@ -1,146 +0,0 @@
<template>
<div class="_gaps_m">
<FormSection v-if="stats" first>
<template #label>{{ i18n.ts.statistics }}</template>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.notesCount }}</template>
<template #value>{{ number(stats.notesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.repliesCount }}</template>
<template #value>{{ number(stats.repliesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.renotesCount }}</template>
<template #value>{{ number(stats.renotesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.repliedCount }}</template>
<template #value>{{ number(stats.repliedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.renotedCount }}</template>
<template #value>{{ number(stats.renotedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.pollVotesCount }}</template>
<template #value>{{ number(stats.pollVotesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.pollVotedCount }}</template>
<template #value>{{ number(stats.pollVotedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.sentReactionsCount }}</template>
<template #value>{{ number(stats.sentReactionsCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.receivedReactionsCount }}</template>
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.noteFavoritesCount }}</template>
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followingCount }}</template>
<template #value>{{ number(stats.followingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template>
<template #value>{{ number(stats.localFollowingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followersCount }}</template>
<template #value>{{ number(stats.followersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template>
<template #value>{{ number(stats.localFollowersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.pageLikesCount }}</template>
<template #value>{{ number(stats.pageLikesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.pageLikedCount }}</template>
<template #value>{{ number(stats.pageLikedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.driveFilesCount }}</template>
<template #value>{{ number(stats.driveFilesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.driveUsage }}</template>
<template #value>{{ bytes(stats.driveUsage) }}</template>
</MkKeyValue>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.other }}</template>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>emailVerified</template>
<template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>twoFactorEnabled</template>
<template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>securityKeys</template>
<template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>usePasswordLessLogin</template>
<template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>isModerator</template>
<template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>isAdmin</template>
<template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const stats = ref<any>({});
onMounted(() => {
os.api('users/stats', {
userId: $i!.id,
}).then(response => {
stats.value = response;
});
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.accountInfo,
icon: 'ti ti-info-circle',
});
</script>

View file

@ -3,7 +3,7 @@
<FormPagination ref="list" :pagination="pagination"> <FormPagination ref="list" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
</template> </template>
@ -47,6 +47,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { infoImageUrl } from '@/instance';
const list = ref<any>(null); const list = ref<any>(null);

View file

@ -10,7 +10,7 @@
<MkPagination :pagination="renoteMutingPagination"> <MkPagination :pagination="renoteMutingPagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noUsers }}</div> <div>{{ i18n.ts.noUsers }}</div>
</div> </div>
</template> </template>
@ -38,7 +38,7 @@
<MkPagination :pagination="mutingPagination"> <MkPagination :pagination="mutingPagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noUsers }}</div> <div>{{ i18n.ts.noUsers }}</div>
</div> </div>
</template> </template>
@ -68,7 +68,7 @@
<MkPagination :pagination="blockingPagination"> <MkPagination :pagination="blockingPagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noUsers }}</div> <div>{{ i18n.ts.noUsers }}</div>
</div> </div>
</template> </template>
@ -107,6 +107,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os'; import * as os from '@/os';
import { infoImageUrl } from '@/instance';
let tab = $ref('renoteMute'); let tab = $ref('renoteMute');

View file

@ -26,8 +26,6 @@
<template #key>{{ i18n.ts.registeredDate }}</template> <template #key>{{ i18n.ts.registeredDate }}</template>
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template> <template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
</MkKeyValue> </MkKeyValue>
<FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"></i></template>{{ i18n.ts.statistics }}</FormLink>
</div> </div>
</MkFolder> </MkFolder>

View file

@ -127,7 +127,6 @@ const profile = reactive({
lang: $i.lang, lang: $i.lang,
isBot: $i.isBot, isBot: $i.isBot,
isCat: $i.isCat, isCat: $i.isCat,
showTimelineReplies: $i.showTimelineReplies,
}); });
watch(() => profile, () => { watch(() => profile, () => {
@ -151,7 +150,7 @@ while (fields.value.length < 4) {
addField(); addField();
} }
function deleteField(index: number) { function deleteField(index: number) {
fields.value.splice(index, 1); fields.value.splice(index, 1);
} }
@ -176,7 +175,6 @@ function save() {
lang: profile.lang || null, lang: profile.lang || null,
isBot: !!profile.isBot, isBot: !!profile.isBot,
isCat: !!profile.isCat, isCat: !!profile.isCat,
showTimelineReplies: !!profile.showTimelineReplies,
}); });
claimAchievement('profileFilled'); claimAchievement('profileFilled');
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {

View file

@ -177,10 +177,6 @@ export const routes = [{
path: '/accounts', path: '/accounts',
name: 'profile', name: 'profile',
component: page(() => import('./pages/settings/accounts.vue')), component: page(() => import('./pages/settings/accounts.vue')),
}, {
path: '/account-stats',
name: 'other',
component: page(() => import('./pages/settings/account-stats.vue')),
}, { }, {
path: '/other', path: '/other',
name: 'other', name: 'other',
@ -392,6 +388,10 @@ export const routes = [{
path: '/settings', path: '/settings',
name: 'settings', name: 'settings',
component: page(() => import('./pages/admin/settings.vue')), component: page(() => import('./pages/admin/settings.vue')),
}, {
path: '/branding',
name: 'branding',
component: page(() => import('./pages/admin/branding.vue')),
}, { }, {
path: '/moderation', path: '/moderation',
name: 'moderation', name: 'moderation',

View file

@ -12,5 +12,14 @@ export function useStream(): Misskey.Stream {
token: $i.token, token: $i.token,
} : null)); } : null));
window.setTimeout(heartbeat, 1000 * 60);
return stream; return stream;
} }
function heartbeat(): void {
if (stream != null && document.visibilityState === 'visible') {
stream.heartbeat();
}
window.setTimeout(heartbeat, 1000 * 60);
}

View file

@ -254,6 +254,28 @@ async function deleteProfile() {
} }
</script> </script>
<style>
html,
body {
width: 100%;
height: 100%;
overflow: clip;
position: fixed;
top: 0;
left: 0;
overscroll-behavior: none;
}
#misskey_app {
width: 100%;
height: 100%;
overflow: clip;
position: absolute;
top: 0;
left: 0;
}
</style>
<style lang="scss" module> <style lang="scss" module>
.transition_menuDrawerBg_enterActive, .transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive { .transition_menuDrawerBg_leaveActive {

View file

@ -313,6 +313,7 @@ function onDrop(ev) {
> .body { > .body {
background: var(--bg) !important; background: var(--bg) !important;
overflow-y: scroll !important;
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: inherit; background: inherit;

View file

@ -215,6 +215,28 @@ watch($$(navFooter), () => {
}); });
</script> </script>
<style>
html,
body {
width: 100%;
height: 100%;
overflow: clip;
position: fixed;
top: 0;
left: 0;
overscroll-behavior: none;
}
#misskey_app {
width: 100%;
height: 100%;
overflow: clip;
position: absolute;
top: 0;
left: 0;
}
</style>
<style lang="scss" module> <style lang="scss" module>
$ui-font-size: 1em; // TODO: $ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px; $widgets-hide-threshold: 1090px;

View file

@ -7,7 +7,7 @@
<div class="ekmkgxbj"> <div class="ekmkgxbj">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo"> <div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div> <div>{{ i18n.ts.nothing }}</div>
</div> </div>
<div v-else :class="$style.feed"> <div v-else :class="$style.feed">
@ -25,6 +25,7 @@ import MkContainer from '@/components/MkContainer.vue';
import { url as base } from '@/config'; import { url as base } from '@/config';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { useInterval } from '@/scripts/use-interval'; import { useInterval } from '@/scripts/use-interval';
import { infoImageUrl } from '@/instance';
const name = 'rss'; const name = 'rss';

View file

@ -2147,10 +2147,6 @@ export type Endpoints = {
}; };
}; };
}; };
'users/stats': {
req: TODO;
res: TODO;
};
}; };
declare namespace entities { declare namespace entities {
@ -2324,7 +2320,9 @@ type LiteInstanceMetadata = {
themeColor: string | null; themeColor: string | null;
mascotImageUrl: string | null; mascotImageUrl: string | null;
bannerUrl: string | null; bannerUrl: string | null;
errorImageUrl: string | null; serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null; iconUrl: string | null;
backgroundImageUrl: string | null; backgroundImageUrl: string | null;
logoImageUrl: string | null; logoImageUrl: string | null;
@ -2606,6 +2604,10 @@ export class Stream extends EventEmitter<StreamEvents> {
// //
// (undocumented) // (undocumented)
disconnectToChannel(connection: NonSharedConnection): void; disconnectToChannel(connection: NonSharedConnection): void;
// (undocumented)
heartbeat(): void;
// (undocumented)
ping(): void;
// Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts
// //
// (undocumented) // (undocumented)

View file

@ -584,5 +584,4 @@ export type Endpoints = {
$default: UserDetailed; $default: UserDetailed;
}; };
}; }; }; };
'users/stats': { req: TODO; res: TODO; };
}; };

View file

@ -71,7 +71,9 @@ export type LiteInstanceMetadata = {
themeColor: string | null; themeColor: string | null;
mascotImageUrl: string | null; mascotImageUrl: string | null;
bannerUrl: string | null; bannerUrl: string | null;
errorImageUrl: string | null; serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null; iconUrl: string | null;
backgroundImageUrl: string | null; backgroundImageUrl: string | null;
logoImageUrl: string | null; logoImageUrl: string | null;

View file

@ -186,6 +186,14 @@ export default class Stream extends EventEmitter<StreamEvents> {
this.stream.send(JSON.stringify(typeOrPayload)); this.stream.send(JSON.stringify(typeOrPayload));
} }
public ping(): void {
this.stream.send('ping');
}
public heartbeat(): void {
this.stream.send('h');
}
/** /**
* Close this connection * Close this connection
*/ */