Merge pull request #10990 from misskey-dev/develop

Release: 13.13.2
This commit is contained in:
syuilo 2023-06-13 16:46:01 +09:00 committed by GitHub
commit 7093662ce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 578 additions and 551 deletions

View File

@ -46,8 +46,10 @@ Please include errors from the developer console and/or server log files if you
<!-- Example: Chrome 113.0.5672.126 --> <!-- Example: Chrome 113.0.5672.126 -->
* Server URL: * Server URL:
<!-- Example: misskey.io --> <!-- Example: misskey.io -->
* Misskey:
13.x.x
### 🛰 Backend (for instance admin) ### 🛰 Backend (for server admin)
<!-- If you are using a managed service, put that after the version. --> <!-- 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 --> * Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->

View File

@ -12,6 +12,19 @@
--> -->
## 13.13.2
### General
- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように
- ロールが付与されているユーザーリストを非公開にできるように
- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました
### Client
- Fix: タブがバックグラウンドでもstreamが切断されないように
### Server
- Fix: キャッシュが溜まり続けないように
## 13.13.1 ## 13.13.1
### Client ### Client
@ -96,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

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

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

@ -1062,6 +1062,7 @@ later: "稍後再說"
goToMisskey: "往Misskey" goToMisskey: "往Misskey"
additionalEmojiDictionary: "表情符號的附加辭典" additionalEmojiDictionary: "表情符號的附加辭典"
installed: "已安裝" installed: "已安裝"
branding: "品牌宣傳"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "帳戶已建立完成!" accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsStartAccountSetup: "來進行帳戶的初始設定吧。"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.13.1", "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

@ -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';
@ -672,7 +671,6 @@ const eps = [
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
['users/search', ep___users_search], ['users/search', ep___users_search],
['users/show', ep___users_show], ['users/show', ep___users_show],
['users/stats', ep___users_stats],
['users/achievements', ep___users_achievements], ['users/achievements', ep___users_achievements],
['users/update-memo', ep___users_updateMemo], ['users/update-memo', ep___users_updateMemo],
['fetch-rss', ep___fetchRss], ['fetch-rss', ep___fetchRss],

View File

@ -61,10 +61,17 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
errorImageUrl: { serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
default: 'https://xn--931a.moe/aiart/yubitun.png',
}, },
iconUrl: { iconUrl: {
type: 'string', type: 'string',
@ -305,7 +312,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, 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

@ -32,7 +32,9 @@ export const paramDef = {
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true }, mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true },
errorImageUrl: { type: 'string', nullable: true }, serverErrorImageUrl: { type: 'string', nullable: true },
infoImageUrl: { type: 'string', nullable: true },
notFoundImageUrl: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true },
backgroundImageUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true },
@ -149,6 +151,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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;
} }
@ -281,10 +295,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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

@ -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 '@/const'; import { notificationTypes } from '@/const';
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

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

@ -78,3 +78,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, () => {
@ -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

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

View File

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

View File

@ -294,7 +294,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;