Compare commits

...

89 Commits

Author SHA1 Message Date
renovate[bot] 6e8538db85
fix(deps): update dependency vite to v6.2.4 [security] 2025-03-31 18:40:34 +00:00
syuilo 5aca91251a fix missing import 2025-03-31 20:48:16 +09:00
syuilo 1c683c3fcc fix(frontend): インストールしたテーマがテーマ一覧にすぐ反映されない 2025-03-31 20:36:49 +09:00
syuilo 08072e294b 🎨 2025-03-31 20:17:48 +09:00
github-actions[bot] f637d0dff9 Bump version to 2025.3.2-beta.20 2025-03-31 08:34:36 +00:00
かっこかり 15a5bb17e3
fix(frontend): チャットのデザイン調整 (#15708)
* fix(frontend): チャットのデザイン調整

* remove unused locales

* 🎨

* Update XMessage.vue

* Update XMessage.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-31 17:33:00 +09:00
syuilo 888e04ce82 fix(frontend): tweak deck onboarding 2025-03-31 16:40:44 +09:00
github-actions[bot] 3a028abea5 Bump version to 2025.3.2-beta.19 2025-03-31 04:26:53 +00:00
syuilo ff59089ad9 🎨 2025-03-31 13:24:20 +09:00
syuilo 93214862b1 🎨 2025-03-31 13:18:50 +09:00
syuilo ea722b8360 🎨 2025-03-31 13:00:34 +09:00
syuilo 49f1f7194d 🎨 2025-03-31 11:13:02 +09:00
syuilo 8baf54e629 fix(frontend): fix ad rendering of timeline 2025-03-31 11:01:32 +09:00
syuilo 393f893a2c 🎨 2025-03-30 20:51:27 +09:00
syuilo 87a7238976 enhance(frontend): デッキのオプションを追加 2025-03-30 20:44:00 +09:00
syuilo e0d8702839 perf(frontend): tweak MkRange 2025-03-30 18:13:39 +09:00
syuilo 6e929ece6f fix(frontend): suppress inject warn 2025-03-30 18:13:08 +09:00
syuilo 303b62aff3
New Crowdin updates (#15721)
* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)
2025-03-30 14:34:32 +09:00
syuilo 45b8423429 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-30 14:34:14 +09:00
syuilo 0655c8a29b clean up 2025-03-30 14:33:52 +09:00
syuilo 17f3113b92 🎨 2025-03-30 14:30:04 +09:00
syuilo 4f4cb6738c fix(frontend): drop classic ui 2025-03-30 14:25:56 +09:00
github-actions[bot] 591175b42e Bump version to 2025.3.2-beta.18 2025-03-30 02:54:21 +00:00
syuilo c03f9bff0a 🎨 2025-03-30 11:52:51 +09:00
syuilo 88c743aa33 chore(frontend): remove unused style 2025-03-30 11:45:41 +09:00
syuilo c06d0b9b42 enhance(frontend): organize settings page 2025-03-30 11:27:35 +09:00
syuilo 4af49e8385 enhance(frontend): organize settings page 2025-03-30 11:16:38 +09:00
syuilo aeda34e5e7 fix(frontend): 広告が無い場合の表示を修正 2025-03-30 09:11:59 +09:00
syuilo 7d842c1a95 fix(frontend): avoid naming confliction of MkAd 2025-03-30 09:07:15 +09:00
syuilo 93fc2456b3 refactor(frontend): refactor base styles 2025-03-30 08:59:18 +09:00
syuilo a420a95fae perf(frontend): アニメーション無効時のパフォーマンスを向上 2025-03-30 08:49:14 +09:00
renovate[bot] 2618585a25
fix(deps): update dependency vite to v6.2.3 [security] (#15710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 08:39:40 +09:00
syuilo 61846a04b2
Update CHANGELOG.md 2025-03-29 23:07:46 +09:00
syuilo cce88c904b
Update CHANGELOG.md 2025-03-29 22:16:22 +09:00
syuilo d866ab12e9
perf(frontend): reduce stacking context in deck 2025-03-29 22:00:01 +09:00
github-actions[bot] df75715d29 Bump version to 2025.3.2-beta.17 2025-03-29 12:23:35 +00:00
syuilo 02da241ec9 Revert "(test)"
This reverts commit eb4062cf63.
2025-03-29 21:16:25 +09:00
syuilo eb4062cf63 (test) 2025-03-29 21:02:31 +09:00
syuilo d9012740a1 enhance(frontend): アイコンのスクロール追従を無効化してパフォーマンス向上できるように 2025-03-29 20:56:59 +09:00
syuilo 1b776a7e7e perf(frontend): reduce stack contexts 2025-03-29 20:02:51 +09:00
syuilo e0b7c56458 Revert "test"
This reverts commit 2787158a04.
2025-03-29 19:23:30 +09:00
syuilo 2787158a04 test 2025-03-29 18:41:52 +09:00
syuilo 2bbc0878e7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-29 18:03:34 +09:00
syuilo fb1542429f 🎨 2025-03-29 18:03:31 +09:00
github-actions[bot] 05b23eda59 Bump version to 2025.3.2-beta.16 2025-03-29 09:01:05 +00:00
syuilo ddd6d72dd7
New Crowdin updates (#15716)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)
2025-03-29 17:59:40 +09:00
syuilo 25db8c2fa9 🎨 2025-03-29 17:59:09 +09:00
syuilo 2ad7b010e4 🎨 2025-03-29 17:57:03 +09:00
syuilo 7c06ffc422 refactor 2025-03-29 17:28:20 +09:00
syuilo df3ed93f84 clean up 2025-03-29 17:15:47 +09:00
syuilo b030e33856 perf(frontend): improve performance of timeline page 2025-03-29 17:15:31 +09:00
syuilo 7fd3adedee fix tests 2025-03-29 17:00:01 +09:00
syuilo ae59578115 refactor 2025-03-29 16:55:12 +09:00
syuilo 609a37742c clean up 2025-03-29 16:11:15 +09:00
syuilo d9d796b204 lint fixes 2025-03-29 16:09:27 +09:00
syuilo 6c2c3f08be refactor(frontend): use symbol for di 2025-03-29 16:04:01 +09:00
syuilo e5e4390494 fix(frontend): suppress inject warn 2025-03-29 16:01:51 +09:00
syuilo 5a09e7a8b4 lint 2025-03-29 15:57:34 +09:00
syuilo 88e6bd1533 Update eslint.config.js 2025-03-29 15:55:22 +09:00
syuilo 7d8c98767a lint 2025-03-29 15:53:44 +09:00
syuilo 490222fb78 perf(frontend): avoid needless AsyncComponentWrapper 2025-03-29 15:33:19 +09:00
syuilo 1af4081090 enhance(frontend): disable horizontal swipe for timeline/notifications to improve ux 2025-03-29 15:00:29 +09:00
syuilo 33e76f9dfc Revert "🎨"
This reverts commit 3451c9a0de.
2025-03-29 13:55:05 +09:00
syuilo 8dd8f636dc 🎨 2025-03-29 13:52:15 +09:00
syuilo 3451c9a0de 🎨 2025-03-29 13:39:44 +09:00
syuilo fc88410c0d refactor(frontend): tweak MkNotes and MkNotifications 2025-03-29 13:34:53 +09:00
syuilo 3682c0069c Revert "test"
This reverts commit 2b42e8f171.
2025-03-29 12:27:13 +09:00
syuilo 2b42e8f171 test 2025-03-29 11:18:49 +09:00
github-actions[bot] e990831a09 Bump version to 2025.3.2-beta.15 2025-03-28 07:39:15 +00:00
syuilo 18355a0838 perf(frontend): avoid main thread scroll repaint 2025-03-28 16:34:21 +09:00
syuilo 811077ca66 perf(frontend): avoid main thread scroll repaint 2025-03-28 15:26:15 +09:00
syuilo 1c26dae39f enhance(frontend): リモートアカウントでチャットが使えるかどうか知る術がないため表示を改善 2025-03-28 11:00:45 +09:00
syuilo c37f9d38a3 enhance(frontend): チャットが開放されていない場合のUIを改善 2025-03-28 10:48:13 +09:00
syuilo dec3e86e5e enhance(backend): アカウントでチャットが有効になっているかどうかをユーザーのレスポンスに含めるように 2025-03-28 10:48:03 +09:00
syuilo 0c14250678 enhance(frontend): チャットが開放されていない場合のUIを改善 2025-03-28 10:34:25 +09:00
github-actions[bot] 30e0259062 Bump version to 2025.3.2-beta.14 2025-03-28 00:53:31 +00:00
syuilo 26aa7c6ca1
New Crowdin updates (#15707)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)
2025-03-28 09:49:59 +09:00
syuilo 29f5e5ca32 fix(frontend): classic uiが表示できない
Fix #15715
2025-03-28 09:49:47 +09:00
syuilo a25fa62d64 enhance(frontend): ファイルアップロード処理のリファクタと設定の簡略化 2025-03-28 09:47:34 +09:00
syuilo 61e09d483e refactor 2025-03-28 09:22:37 +09:00
syuilo e40846c46b fix e2e test 2025-03-27 19:50:03 +09:00
syuilo a78db27a3c Update CHANGELOG.md 2025-03-27 17:30:06 +09:00
syuilo f7e901deb2 test fixes 2025-03-27 17:30:04 +09:00
syuilo b95da9c9a4 enhance(backend): ミュートしているユーザーをユーザー検索の結果から除外するように 2025-03-27 17:12:23 +09:00
syuilo c29a5764d3 refactor(backend): better method name 2025-03-27 16:51:08 +09:00
Acid Chicken ed86b1706d
ci(storybook): prevent running for bots 2025-03-26 12:17:56 +09:00
syuilo 36865a5771 enhance(frontend): improve chat ux 2025-03-26 10:49:36 +09:00
syuilo 5e90679916 lint 2025-03-26 10:26:48 +09:00
syuilo ac49a3e992 typo 2025-03-26 09:06:34 +09:00
161 changed files with 1729 additions and 1731 deletions

View File

@ -5,13 +5,15 @@ on:
branches:
- master
- develop
- dev/storybook8 # for testing
pull_request_target:
branches-ignore:
# Since pull requests targets master mostly is the "develop" branch.
# Storybook CI is checked on the "push" event of "develop" branch so it would cause a duplicate build.
# This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master.
- master
# Neither Dependabot nor Renovate will change the actual behavior for components.
- dependabot/**
- renovate/**
jobs:
build:

View File

@ -13,6 +13,7 @@
- メッセージにはリアクションも可能です
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
### Client
- Feat: 設定の管理が強化されました
@ -32,11 +33,16 @@
- ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
- 再度ログインすればサーバーのバックアップから設定データを復元可能です
- エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
- 設定情報の移行は自動で行われますが、何らかの理由で失敗した場合、設定→その他→旧設定情報を移行 で再試行可能です
- Feat: 画面を重ねて表示するオプションを実装(実験的)
- 設定 → その他 → 実験的機能 → Enable stacking router view
- Enhance: プラグインの管理が強化されました
- インストール/アンインストール/設定の変更時にリロード不要になりました
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
- Enhance: デッキUIでカラム間のマージンを設定できるように
- Enhance: デッキUIでデッキメニューの位置を設定できるように
- Enhance: デッキUIでナビゲーションバーの位置を設定できるように
- Enhance: アイコンのスクロール追従を無効化してパフォーマンス向上できるように
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
- Enhance: テーマ設定画面のデザインを改善
@ -44,7 +50,12 @@
- 投稿フォームをリセットできるように
- 文字数カウントを復活
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
- Enhance: 全体的なブラッシュアップ
- Enhance 全体的なパフォーマンス向上
- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます
### Server
- Enhance 全体的なパフォーマンス向上

View File

@ -1335,6 +1335,7 @@ information: "Informació"
chat: "Xat"
migrateOldSettings: "Migració de la configuració antiga "
migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual."
compress: "Comprimir "
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@ -1363,6 +1364,8 @@ _chat:
newline: "Línia nova "
muteThisRoom: "Silenciar aquesta sala"
deleteRoom: "Esborrar la sala"
chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte."
chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari."
cannotChatWithTheUser: "No pots xatejar amb aquest usuari"
cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert."
chatWithThisUser: "Xateja amb aquest usuari"
@ -1403,6 +1406,7 @@ _settings:
timelineAndNote: "Línia de temps i nota"
makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables"
makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions."
useStickyIcons: "Utilitza icones fixes"
showNavbarSubButtons: "Mostrar sub botons a la barra de navegació "
ifOn: "Quan s'encén "
ifOff: "Quan s'apaga "
@ -2546,6 +2550,7 @@ _notification:
newNote: "Nota nova"
unreadAntennaNote: "Antena {name}"
roleAssigned: "Rol assignat "
chatRoomInvitationReceived: "T'han invitat a una sala de xat"
emptyPushNotificationMessage: "Les notificacions han sigut actualitzades"
achievementEarned: "Aconseguiment desblocat"
testNotification: "Notificació de prova"
@ -2574,6 +2579,7 @@ _notification:
receiveFollowRequest: "Rebuda una petició de seguiment"
followRequestAccepted: "Petició de seguiment acceptada"
roleAssigned: "Rol donat"
chatRoomInvitationReceived: "Invitat a la sala de xat"
achievementEarned: "Assoliment desbloquejat"
exportCompleted: "Exportació completada"
login: "Iniciar sessió"
@ -2713,6 +2719,7 @@ _moderationLogTypes:
deletePage: "Esborrar la pàgina"
deleteFlash: "Esborrar el guió"
deleteGalleryPost: "Esborrar la publicació de la galeria"
deleteChatRoom: "Esborra la sala de xat"
updateProxyAccountDescription: "Actualitzar descripció del compte proxy"
_fileViewer:
title: "Detall del fitxer"

View File

@ -11,6 +11,7 @@ username: "Uživatelské jméno"
password: "Heslo"
initialPasswordForSetup: "Počáteční heslo pro nastavení"
initialPasswordIsIncorrect: "Počáteční heslo pro nastavení je nesprávné"
initialPasswordForSetupDescription: "Použijte heslo, které jste nastavili v konfiguračním souboru, pokud jste Misskey instalovali ručně.\nPokud užíváte Misskey hostovací službu, použijte poskytnuté heslo.\nPokud jste heslo nenastavovali, zanechte prázdné."
forgotPassword: "Zapomenuté heslo"
fetchingAsApObject: "Načítám data z Fediversu..."
ok: "Potvrdit"
@ -48,6 +49,8 @@ pin: "Připnout"
unpin: "Odepnout"
copyContent: "Zkopírovat obsah"
copyLink: "Kopírovat odkaz"
copyRemoteLink: "Zkoprírovat vzdálený odkaz"
copyLinkRenote: "Zkopírovat odkaz renotu"
delete: "Smazat"
deleteAndEdit: "Smazat a upravit"
deleteAndEditConfirm: "Jste si jistí že chcete smazat tuto poznámku a editovat ji? Ztratíte tím všechny reakce, sdílení a odpovědi na ni."
@ -540,6 +543,7 @@ showInPage: "Zobrazit na stránce"
popout: "Pop-out"
volume: "Hlasitost"
masterVolume: "Celková hlasitost"
notUseSound: "Zakázat zvuk"
details: "Detaily"
chooseEmoji: "Vybrat emotikon"
unableToProcess: "Operace nebyla dokončena."

View File

@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschl
explore: "Erkunden"
messageRead: "Gelesen"
noMoreHistory: "Kein weiterer Verlauf vorhanden"
startChat: "Chat starten"
nUsersRead: "Von {n} Benutzern gelesen"
agreeTo: "Ich stimme {0} zu"
agree: "Zustimmen"
@ -1256,7 +1257,7 @@ replaying: "Aufzeichnung"
endReplay: "Aufzeichnung verlassen"
copyReplayData: "Aufzeichnung kopieren"
ranking: "Rangliste"
lastNDays: "Letzten {n} Tage"
lastNDays: "Letzte {n} Tage"
backToTitle: "Zurück zum Startbildschirm"
hemisphere: "Hemisphäre"
withSensitive: "Zeige \"sensitive Inhalte\" an"
@ -1318,25 +1319,93 @@ noName: "Kein Name"
skip: "Überspringen"
restore: "Wiederherstellen"
syncBetweenDevices: "Zwischen Geräten synchronisieren"
preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden."
preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?"
preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server"
preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät"
preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen"
paste: "Einfügen"
emojiPalette: "Emoji-Palette"
postForm: "Notizfenster"
textCount: "Zeichenanzahl"
information: "Über"
chat: "Chat"
migrateOldSettings: "Alte Client-Einstellungen migrieren"
migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben."
compress: "Komprimieren"
_chat:
noMessagesYet: "Noch keine Nachrichten"
newMessage: "Neue Nachricht"
individualChat: "Privater Chat"
individualChat_description: "Führe einen privaten Chat mit einer anderen Person."
roomChat_description: "Ein Chat-Raum, an dem mehrere Personen teilnehmen können.\nDu kannst auch Personen einladen, die keine privaten Chats zulassen, wenn sie die Einladung annehmen."
createRoom: "Raum erstellen"
inviteUserToChat: "Lade Benutzer ein, um mit dem Chatten zu beginnen"
yourRooms: "Erstellte Räume"
invitations: "Einladen"
noInvitations: "Keine Einladungen"
history: "Verlauf"
noHistory: "Kein Verlauf gefunden"
noRooms: "Keine Räume gefunden"
inviteUser: "Benutzer einladen"
sentInvitations: "Verschickte Einladungen"
join: "Beitreten"
ignore: "Ignorieren"
leave: "Raum verlassen"
members: "Mitglieder"
searchMessages: "Nachrichten suchen"
home: "Startseite"
send: "Senden"
newline: "Neue Zeile"
muteThisRoom: "Raum stummschalten"
deleteRoom: "Raum löschen"
chatNotAvailableForThisAccountOrServer: "Der Chat ist auf diesem Server oder für dieses Konto nicht aktiviert."
chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert."
cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich"
cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert."
chatWithThisUser: "Mit dem Benutzer chatten"
thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an."
thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt."
thisUserAllowsChatOnlyFromMutualFollowing: "Dieser Benutzer akzeptiert nur Chats von Benutzern, die sich gegenseitig folgen."
thisUserNotAllowedChatAnyone: "Dieser Benutzer nimmt keine Chats von anderen Benutzern an."
chatAllowedUsers: "Wem das Chatten erlaubt werden soll"
chatAllowedUsers_note: "Du kannst unabhängig von dieser Einstellung mit allen Personen chatten, denen du eine Chat-Nachricht gesendet hast."
_chatAllowedUsers:
everyone: "Jeder"
followers: "Nur deine Follower"
following: "Nur Benutzer, denen du folgst"
mutual: "Nur Benutzer, die sich gegenseitig folgen"
none: "Niemand"
_emojiPalette:
palettes: "Palette"
enableSyncBetweenDevicesForPalettes: "Synchronisierung der Paletten zwischen Geräten aktivieren"
paletteForMain: "Hauptpalette"
paletteForReaction: "Reaktions-Palette"
_settings:
driveBanner: "Du kannst den Drive verwalten und konfigurieren, die Auslastung überprüfen und Einstellungen für das Hochladen von Dateien vornehmen."
pluginBanner: "Du kannst die Funktionen des Clients mit Plugins erweitern. Plugins können installiert, individuell konfiguriert und verwaltet werden."
api: "API"
webhook: "Webhook"
serviceConnectionBanner: "Du kannst Zugriffstoken und Webhooks für die Integration mit externen Anwendungen und Diensten verwalten und konfigurieren."
accountData: "Kontodaten"
accountDataBanner: "Export/Import und Verwaltung von Kontodatenarchiven."
muteAndBlockBanner: "Du kannst Einstellungen konfigurieren und verwalten, um Inhalte auszublenden und Aktionen für bestimmte Benutzer zu beschränken."
accessibilityBanner: "Die Clients können personalisiert und für eine optimale Nutzung im Hinblick auf ihre Darstellung und ihr Verhalten eingerichtet werden."
privacyBanner: "Du kannst Einstellungen für die Privatsphäre deines Kontos vornehmen, z. B. inwieweit Inhalte veröffentlicht werden, wie leicht sie zu finden sind und ob Follower genehmigt werden müssen."
securityBanner: "Du kannst Einstellungen für die Kontosicherheit konfigurieren, z. B. Passwörter, Anmeldemethoden, Authentifizierungs-Apps und Passkeys."
appearanceBanner: "Du kannst das Erscheinungsbild und die Anzeigeeinstellungen für den Client nach deinen Wünschen konfigurieren."
soundsBanner: "Du kannst die Einstellungen für die Wiedergabe von Klängen im Client konfigurieren."
timelineAndNote: "Chroniken und Notizen"
makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen"
makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen."
ifOn: "Wenn eingeschaltet"
ifOff: "Wenn ausgeschaltet"
_chat:
showSenderName: "Name des Absenders anzeigen"
sendOnEnter: "Eingabetaste sendet Nachricht"
_preferencesProfile:
profileName: "Profilname"
profileNameDescription: "Lege einen Namen fest, der dieses Gerät identifiziert."
profileNameDescription2: "Beispiel: \"Haupt-PC\", \"Smartphone\""
_preferencesBackup:
autoBackup: "Automatische Sicherung"
@ -1353,9 +1422,11 @@ _accountSettings:
requireSigninToViewContentsDescription2: "Der Inhalt wird nicht in URL-Vorschauen (OGP), eingebettet in Webseiten oder auf Servern, die keine Zitate unterstützen, angezeigt."
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
makeNotesFollowersOnlyBeforeDescription: "Solange diese Funktion aktiviert ist, sind Notizen, die nach dem eingestellten Datum und der eingestellten Zeit liegen oder die eingestellte Zeit abgelaufen ist, nur für Follower sichtbar. Bei Deaktivierung wird auch der öffentliche Status der Notiz wiederhergestellt."
makeNotesHiddenBefore: "Frühere Notizen privat machen"
makeNotesHiddenBeforeDescription: ""
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
mayNotEffectSomeSituations: "Diese Einschränkungen sind vereinfacht. Sie gelten möglicherweise nicht in allen Situationen, z. B. bei der Anzeige auf einem fremden Server oder während der Moderation."
notesOlderThanSpecifiedDateAndTime: "Notizen vor einem bestimmtem Datum und Uhrzeit"
_abuseUserReport:
forward: "Weiterleiten"
@ -1363,11 +1434,15 @@ _abuseUserReport:
resolve: "lösen"
accept: "Akzeptieren"
reject: "Ablehnen"
resolveTutorial: "Wenn der Inhalt der Meldung rechtmäßig ist, wähle „Akzeptieren“, um sie als gelöst zu markieren.\nWenn der Inhalt der Meldung unzulässig ist, wähle „Ablehnen“, um sie zu ignorieren."
_delivery:
status: "Auslieferungsstatus"
stop: "Gesperrt"
_type:
none: "Wird veröffentlicht"
manuallySuspended: "Manuell gesperrt"
goneSuspended: "Gesperrt wegen Löschung des Servers"
autoSuspendedForNotResponding: "Gesperrt, weil der Server nicht antwortet"
_bubbleGame:
howToPlay: "Wie man spielt"
hold: "Halten"
@ -1377,6 +1452,8 @@ _bubbleGame:
highScore: "Höchstpunktzahl"
maxChain: "Maximale Anzahl an Verkettungen"
yen: "{yen} Yen"
estimatedQty: "{qty} Stück"
scoreSweets: "{onigiriQtyWithUnit} Onigiri"
_howToPlay:
section1: "Passe die Position an und lasse das Objekt in das Spielfeld fallen."
section2: "Wenn sich zwei Objekte der gleichen Art berühren, verwandeln sie sich in ein anderes Objekt und du bekommst Punkte."
@ -1434,15 +1511,20 @@ _initialTutorial:
reactDone: "Du kannst eine Reaktion zurücknehmen, indem du auf den '-' Button drückst."
_timeline:
title: "So funktionieren die Chroniken"
description1: "Misskey stellt mehrere Chroniken bereit (einige können je nach den Richtlinien des Servers nicht verfügbar sein)."
home: "Du kannst Beiträge von den Konten sehen, denen du folgst."
local: "Du kannst Beiträge aller Benutzer auf diesem Server sehen."
social: "Notizen von der Startseite und der lokalen Chronik werden angezeigt."
global: "Du kannst Notizen von allen föderierten Servern sehen."
description2: "Du kannst jederzeit am oberen Rand des Bildschirms zwischen den jeweiligen Chroniken wechseln."
description3: "Darüber hinaus gibt es Listen-Chroniken und Kanal-Chroniken. Weitere Einzelheiten findest du unter {link}."
_postNote:
description1: "Wenn du eine Notiz auf Misskey veröffentlichst, stehen dir verschiedene Optionen zur Verfügung. Die Oberfläche sieht folgendermaßen aus."
_visibility:
description: "Du kannst einschränken, wer deine Notiz sehen kann."
public: "Deine Notiz wird für alle Nutzer sichtbar sein."
home: "Nur auf der Startseite sichtbar. Kann von Followern, Profilbesuchern und durch Renotes gesehen werden."
followers: "Nur für Follower sichtbar. Nur Follower können es sehen und niemand sonst, und es kann nicht von anderen gerenoted werden."
direct: "Die Notiz wird nur für den angegebenen Benutzer veröffentlicht und der Empfänger wird benachrichtigt. Kann anstelle von Direktnachrichten verwendet werden."
doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!"
doNotSendConfidencialOnDirect2: "Die Administratoren des Servers können den Inhalt der Notiz sehen. Sei vorsichtig mit sensiblen Informationen, wenn du Direktnachrichten an Benutzer auf nicht vertrauenswürdigen Servern sendest."
@ -1453,8 +1535,10 @@ _initialTutorial:
_exampleNote:
cw: "Das wird dich bestimmt hungrig machen!"
note: "Ich hatte gerade einen Donut mit Schokoladenüberzug 🍩😋"
useCases: "Dient zur Kennzeichnung von Notizen, wie sie in den Serverrichtlinien vorgeschrieben sind, oder zur eigenen Festlegung von Spoiler-Beiträgen oder sensiblem Text."
_howToMakeAttachmentsSensitive:
title: "Wie markiert man Anhänge als sensibel?"
description: "Markiere Anhänge als sensibel, die aufgrund von den Serverregeln nicht sichtbar sein sollen."
tryThisFile: "Versuche, das angehängte Bild als sensibel zu markieren!"
_exampleNote:
note: "Ups, ich habe es vergeigt, den Natto-Deckel zu öffnen..."
@ -1465,7 +1549,9 @@ _initialTutorial:
title: "Du hast das Tutorial abgeschlossen! 🎉"
description: "Die hier beschriebenen Funktionen sind nur ein kleiner Teil dessen, was Misskey zu bieten hat; um mehr darüber zu erfahren, wie du Misskey benutzen kannst, besuche bitte {link}."
_timelineDescription:
home: "In der Startseiten-Chronik kannst du Notizen von Konten sehen, denen du folgst."
local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server."
social: "Die soziale Chronik zeigt Notizen von der Startseite und der lokalen Chronik."
global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern."
_serverRules:
description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen."
@ -1483,6 +1569,8 @@ _serverSettings:
fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
reactionsBufferingDescription: "Wenn diese Option aktiviert ist, kann sie die Leistung beim Erstellen von Reaktionen erheblich verbessern und die Belastung der Datenbank verringern. Allerdings steigt die Speichernutzung von Redis."
inquiryUrl: "Kontakt-URL"
inquiryUrlDescription: "Gib eine URL für das Kontaktformular der Serverbetreiber oder eine Webseite an, die Kontaktinformationen enthält."
openRegistration: "Registrierung von Konten aktivieren"
openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
_accountMigration:
@ -1800,6 +1888,7 @@ _role:
canManageAvatarDecorations: "Profilbilddekorationen verwalten"
driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren"
canUpdateBioMedia: "Kann ein Profil- oder ein Bannerbild bearbeiten"
pinMax: "Maximale Anzahl an angehefteten Notizen"
antennaMax: "Maximale Anzahl an Antennen"
wordMuteMax: "Maximale Zeichenlänge für Wortstummschaltungen"
@ -1815,12 +1904,20 @@ _role:
canUseTranslator: "Verwendung des Übersetzers"
avatarDecorationLimit: "Maximale Anzahl an Profilbilddekorationen, die angebracht werden können"
canImportAntennas: "Importieren von Antennen erlauben"
canImportBlocking: "Importieren von Blockierungen zulassen"
canImportFollowing: "Importieren von Gefolgten zulassen"
canImportMuting: "Importieren von Stummgeschalteten zulassen"
canImportUserLists: "Importieren von Listen erlauben"
canChat: "Chatten erlauben"
_condition:
roleAssignedTo: "Manuellen Rollen zugewiesen"
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
isCat: "Katzen-Benutzer"
isBot: "Bot-Benutzer"
isSuspended: "Gesperrter Benutzer"
isLocked: "Private Konten"
isExplorable: "Benutzer, die ihr Konto im \"Erkunden\"-Bereich sichtbar machen"
createdLessThan: "Kontoerstellung liegt weniger als X zurück"
createdMoreThan: "Kontoerstellung liegt mehr als X zurück"
followersLessThanOrEq: "Hat X oder weniger Follower"
@ -1975,6 +2072,7 @@ _theme:
installed: "{name} wurde installiert"
installedThemes: "Installierte Farbschemata"
builtinThemes: "Eingebaute Farbschemata"
instanceTheme: "Server-Thema"
alreadyInstalled: "Dieses Farbschema ist bereits installiert"
invalid: "Der Code dieses Farbschemas ist ungültig"
make: "Farbschema erstellen"
@ -2041,6 +2139,7 @@ _sfx:
noteMy: "Meine Notizen"
notification: "Benachrichtigungen"
reaction: "Auswählen einer Reaktion"
chatMessage: "Chat-Nachrichten"
_soundSettings:
driveFile: "Audiodatei aus dem Drive verwenden"
driveFileWarn: "Wähle eine Audiodatei aus dem Drive"
@ -2064,6 +2163,10 @@ _timeIn:
seconds: "In {n}s"
minutes: "In {n} Min."
hours: "In {n} Std."
days: "In {n} Tagen"
weeks: "In {n} Wochen"
months: "In {n} Monaten"
years: "In {n} Jahren"
_time:
second: "Sekunde(n)"
minute: "Minute(n)"
@ -2097,6 +2200,7 @@ _2fa:
backupCodesDescription: "Verwende diese Codes, falls du nicht mehr auf deine App zur Zweifaktorauthentifizierung zugreifen kannst. Jeder Code kann nur einmal verwendet werden. Bewahre sie an einem sicheren Ort auf."
backupCodeUsedWarning: "Ein Backup-Code wurde verwendet. Falls du den Zugriff zu deiner Zweifaktorauthentifizierungsapp verloren hast, konfiguriere diese bitte möglichst bald erneut."
backupCodesExhaustedWarning: "Alle Backup-Codes wurden verwendet. Falls du den Zugang zu deiner Zweifaktorauthentifizierungsapp verlierst, wirst du dich nicht mehr in dieses Konto einloggen können. Bitte konfiguriere diese App erneut."
moreDetailedGuideHere: "Hier ist eine ausführliche Anleitung"
_permissions:
"read:account": "Deine Benutzerkontoinformationen lesen"
"write:account": "Deine Benutzerkontoinformationen bearbeiten"
@ -2147,8 +2251,12 @@ _permissions:
"read:admin:server-info": "Serverinformationen anzeigen"
"read:admin:show-moderation-log": "Moderationsprotokoll einsehen"
"read:admin:show-user": "Private Benutzerinformationen einsehen"
"write:admin:suspend-user": "Benutzer sperren"
"write:admin:unset-user-avatar": "Benutzer-Profilbild entfernen"
"write:admin:unset-user-banner": "Benutzer-Banner entfernen"
"write:admin:unsuspend-user": "Benutzer entsperren"
"write:admin:meta": "Metadaten der Instanz verwalten"
"write:admin:user-note": "Moderationsvermerke verwalten"
"write:admin:roles": "Rollen verwalten"
"read:admin:roles": "Rollen anzeigen"
"write:admin:relays": "Relays verwalten"
@ -2172,7 +2280,10 @@ _permissions:
"read:admin:ad": "Werbung ansehen"
"write:invite-codes": "Einladungscodes erstellen"
"read:invite-codes": "Einladungscodes anzeigen"
"read:federation": "Informationen zur Föderation einsehen"
"write:report-abuse": "Verstöße melden"
"write:chat": "Chats bedienen"
"read:chat": "Chats durchsuchen"
_auth:
shareAccessTitle: "Verteilung von App-Berechtigungen"
shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?"
@ -2295,6 +2406,7 @@ _profile:
avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen."
followedMessage: "Nachricht, wenn dir jemand folgt"
followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt."
followedMessageDescriptionForLockedAccount: "Wenn Folgeanfragen deine Genehmigung brauchen, wird dies beim Genehmigen einer Anfrage angezeigt."
_exportOrImport:
allNotes: "Alle Notizen"
favoritedNotes: "Als Favorit markierte Notizen"
@ -2352,6 +2464,7 @@ _play:
title: "Titel"
script: "Skript"
summary: "Beschreibung"
visibilityDescription: "Wenn du die Sichtbarkeit auf Privat stellst, wird der Play nicht auf deinem Profil sichtbar sein, aber jeder, der die URL hat, kann ihn trotzdem aufrufen."
_pages:
newPage: "Seite erstellen"
editPage: "Seite bearbeiten"
@ -2383,6 +2496,7 @@ _pages:
eyeCatchingImageSet: "Vorschaubild festlegen"
eyeCatchingImageRemove: "Vorschaubild entfernen"
chooseBlock: "Block hinzufügen"
enterSectionTitle: "Titel des Abschnitts eingeben"
selectType: "Typ auswählen"
contentBlocks: "Inhalt"
inputBlocks: "Eingabe"
@ -2393,6 +2507,8 @@ _pages:
section: "Abschnitt"
image: "Bild"
button: "Knopf"
dynamic: "Dynamische Bausteine"
dynamicDescription: "Dieser Baustein wurde abgeschafft. Bitte verwende von nun an {play}."
note: "Eingebettete Notiz"
_note:
id: "Notiz-ID"
@ -2415,6 +2531,7 @@ _notification:
newNote: "Neue Notiz"
unreadAntennaNote: "Antenne {name}"
roleAssigned: "Rolle zugewiesen"
chatRoomInvitationReceived: "Du wurdest in einen Chatraum eingeladen"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
achievementEarned: "Errungenschaft freigeschaltet"
testNotification: "Testbenachrichtigung"
@ -2442,9 +2559,11 @@ _notification:
receiveFollowRequest: "Erhaltene Follow-Anfragen"
followRequestAccepted: "Akzeptierte Follow-Anfragen"
roleAssigned: "Rolle zugewiesen"
chatRoomInvitationReceived: "Einladungen zum Chatraum"
achievementEarned: "Errungenschaft freigeschaltet"
exportCompleted: "Der Export ist abgeschlossen"
login: "Anmeldung"
createToken: "Erstellung von Zugriffstokens"
test: "Test-Benachrichtigungen"
app: "Benachrichtigungen von Apps"
_actions:
@ -2560,6 +2679,7 @@ _moderationLogTypes:
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"
resolveAbuseReport: "Meldung bearbeitet"
forwardAbuseReport: "Meldung weitergeleitet"
updateAbuseReportNote: "Moderationsnotiz einer Meldung aktualisiert"
createInvitation: "Einladung erstellt"
createAd: "Werbung erstellt"
deleteAd: "Werbung gelöscht"
@ -2579,6 +2699,8 @@ _moderationLogTypes:
deletePage: "Seite gelöscht"
deleteFlash: "Play gelöscht"
deleteGalleryPost: "Galeriebeitrag gelöscht"
deleteChatRoom: "Chatraum gelöscht"
updateProxyAccountDescription: "Beschreibung des Proxy-Benutzerkontos aktualisiert"
_fileViewer:
title: "Dateiinformationen"
type: "Dateityp"
@ -2644,6 +2766,7 @@ _hemisphere:
S: "Südliche Erdhalbkugel"
caption: "Wird in einigen Client-Einstellungen zur Bestimmung der Jahreszeit verwendet."
_reversi:
reversi: "Reversi"
gameSettings: "Spieleinstellungen"
chooseBoard: "Spielbrett auswählen"
blackOrWhite: "Schwarz/Weiß"
@ -2661,6 +2784,7 @@ _reversi:
pastTurnOf: "Zug von {name}"
surrender: "Aufgeben"
surrendered: "Aufgegeben"
timeout: "Zeit abgelaufen"
drawn: "Unentschieden"
won: "{name} hat gewonnen"
black: "Schwarz"
@ -2675,6 +2799,7 @@ _reversi:
freeMatch: "Freies Spiel"
lookingForPlayer: "Gegner werden gesucht..."
gameCanceled: "Das Spiel wurde abgesagt."
shareToTlTheGameWhenStart: "Spiel in der Chronik teilen, wenn es gestartet wurde"
iStartedAGame: "Das Spiel hat begonnen! #MisskeyReversi"
opponentHasSettingsChanged: "Der Gegner hat seine Einstellungen geändert."
allowIrregularRules: "Irreguläre Regeln (völlig frei)"
@ -2695,7 +2820,9 @@ _urlPreviewSetting:
requireContentLengthDescription: "Wenn der Server keine Content-Length zurückgibt, wird keine Vorschau erzeugt."
userAgent: "User-Agent"
userAgentDescription: "Legt den User-Agent fest, der beim Abrufen der Vorschau verwendet werden soll. Bleibt er leer, wird der Standard-User-Agent verwendet."
summaryProxy: "Proxy-Endpunkte, die Vorschaubilder erzeugen"
summaryProxyDescription: "Generierung von Vorschaubildern mit Summaly Proxy anstelle von Misskey selbst."
summaryProxyDescription2: "Die folgenden Parameter werden als Abfrage-Strings mit dem Proxy verknüpft. Wenn der Proxy sie nicht unterstützt, werden die Werte ignoriert."
_mediaControls:
pip: "Bild-in-Bild"
playbackRate: "Wiedergabegeschwindigkeit"
@ -2703,15 +2830,57 @@ _mediaControls:
_contextMenu:
title: "Kontextmenü"
app: "Anwendung"
appWithShift: "Anwendung per Umschalttaste"
native: "Natives Browsermenü"
_gridComponent:
_error:
requiredValue: "Dieser Wert ist ein Pflichtfeld"
columnTypeNotSupport: "Die Validierung regulärer Ausdrücke wird nur für Spalten vom Typ \"Text\" unterstützt."
patternNotMatch: "Dieser Wert stimmt nicht mit dem Schema in {pattern} überein"
notUnique: "Dieser Wert muss eindeutig sein"
_roleSelectDialog:
notSelected: "Nicht ausgewählt"
_customEmojisManager:
_gridCommon:
copySelectionRows: "Ausgewählte Zeilen kopieren"
copySelectionRanges: "Auswahl kopieren"
deleteSelectionRows: "Ausgewählte Zeilen löschen"
searchSettings: "Sucheinstellungen"
searchSettingCaption: "Detaillierte Suchkriterien festlegen."
searchLimit: "Anzahl der Ergebnisse"
sortOrder: "Sortierung"
registrationLogs: "Registrierungsprotokoll"
registrationLogsCaption: "Protokolle werden beim Aktualisieren oder Löschen von Emojis angezeigt. Sie verschwinden nach dem Aktualisieren oder Löschen, dem Wechsel zu einer neuen Seite oder dem Neuladen."
alertEmojisRegisterFailedDescription: "Emoji konnte nicht aktualisiert oder gelöscht werden. Bitte prüfe das Registrierungsprotokoll für Details."
_logs:
failureLogNothing: "Es gibt kein Fehlerprotokoll."
logNothing: "Keine Protokoll-Einträge."
_remote:
selectionRowDetail: "Details der ausgewählten Zeile"
importSelectionRangesRows: "Zeilen in der Auswahl importieren"
importEmojisButton: "Ausgewählte Emojis importieren"
confirmImportEmojisTitle: "Emojis importieren"
confirmImportEmojisDescription: "Importiere {count} Emoji(s), die von entfernten Server empfangen wurden. Bitte achte genau auf die Lizenz der Emojis. Bist du sicher, dass du fortfahren möchtest?"
_local:
tabTitleList: "Hinzugefügte Emojis"
tabTitleRegister: "Emojis hinzufügen"
_list:
emojisNothing: "Es wurden keine Emojis hinzugefügt."
alertUpdateEmojisNothingDescription: "Es wurden keine Emojis geändert."
alertDeleteEmojisNothingDescription: "Es gibt keine zu löschenden Emojis."
confirmUpdateEmojisDescription: "Aktualisiere {count} Emoji(s). Willst du fortfahren?"
confirmDeleteEmojisDescription: "Lösche {count} ausgewählte Emoji(s). Willst du fortfahren?"
confirmMovePageDesciption: "An den Emojis auf dieser Seite wurden Änderungen vorgenommen.\nWenn du die Seite verlässt, ohne zu speichern, werden alle auf dieser Seite vorgenommenen Änderungen verworfen."
_register:
uploadSettingDescription: "Hier kannst du das Verhalten beim Hochladen von Emojis konfigurieren."
directoryToCategoryLabel: "Gib den Namen des Verzeichnisses in das Feld „Kategorie“ ein"
directoryToCategoryCaption: "Wenn du ein Verzeichnis ziehst und ablegst, gib den Verzeichnisnamen in das Feld „Kategorie“ ein."
emojiInputAreaList1: "Ziehe Bilddateien oder Verzeichnisse per Drag-and-drop in diesen Rahmen"
emojiInputAreaList2: "Klicke auf diesen Link, um von deinem PC aus zu wählen"
emojiInputAreaList3: "Klicke auf diesen Link, um vom Drive aus zu wählen"
confirmRegisterEmojisDescription: "Füge die in der Liste aufgeführten Emojis als neue benutzerdefinierte Emojis hinzu. Bist du sicher? (Um eine Überlastung zu vermeiden, können nur {count} Emoji(s) in einem Vorgang hinzugefügt werden)"
confirmClearEmojisDescription: "Verwerfe die Bearbeitungen und lösche die Emojis aus der Liste. Bist du sicher, dass du fortfahren möchtest?"
confirmUploadEmojisDescription: "Lade die {count} abgelegte(n) Datei(en) in das Drive hoch. Bist du sicher, dass du fortfahren möchtest?"
_embedCodeGen:
title: "Einbettungscode anpassen"
header: "Kopfzeile anzeigen"
@ -2719,6 +2888,9 @@ _embedCodeGen:
maxHeight: "Maximale Höhe"
maxHeightDescription: "Der Wert 0 deaktiviert die Einstellung der maximalen Höhe. Gib einen Wert an, um zu verhindern, dass das Widget weiterhin vertikal vergrößert wird."
maxHeightWarn: "Die Begrenzung der maximalen Höhe ist deaktiviert (0). Wenn dies nicht beabsichtigt war, setze die maximale Höhe auf einen Wert fest."
previewIsNotActual: "Die Anzeige weicht von der tatsächlichen Einbettung ab, da sie den auf dem Vorschaufenster angezeigten Bereich überschreitet."
rounded: "Ecken abrunden"
border: "Dem äußeren Rand einen Rahmen hinzufügen"
applyToPreview: "Auf die Vorschau anwenden"
generateCode: "Einbettungscode generieren"
codeGenerated: "Der Code wurde generiert"
@ -2749,8 +2921,17 @@ _remoteLookupErrors:
title: "Nicht gefunden"
description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut."
_captcha:
verify: "Bitte beantworte das CAPTCHA"
testSiteKeyMessage: "Du kannst die Vorschau prüfen, indem du die Testwerte für den Site- und Secret-Key eingibst. Weitere Informationen findest du auf der folgenden Seite."
_error:
_requestFailed:
title: "CAPTCHA-Anfrage fehlgeschlagen."
text: "Bitte probiere es später noch einmal oder überprüfe die Einstellungen erneut."
_verificationFailed:
title: "CAPTCHA-Prüfung fehlgeschlagen"
text: "Bitte überprüfe nochmals, ob die Einstellungen korrekt sind."
_unknown:
title: "CAPTCHA-Fehler"
text: "Es ist ein unerwarteter Fehler aufgetreten."
_bootErrors:
title: "Laden fehlgeschlagen"

View File

@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
explore: "Explore"
messageRead: "Read"
noMoreHistory: "There is no further history"
startChat: "Start chat"
nUsersRead: "read by {n}"
agreeTo: "I agree to {0}"
agree: "Agree"
@ -1331,12 +1332,55 @@ emojiPalette: "Emoji palette"
postForm: "Posting form"
textCount: "Character count"
information: "About"
chat: "Chat"
migrateOldSettings: "Migrate old client settings"
migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten."
compress: "Compress"
_chat:
noMessagesYet: "No messages yet"
newMessage: "New message"
individualChat: "Private Chat"
individualChat_description: "Have a private chat with another person."
roomChat: "Room Chat"
roomChat_description: "A chat room which can have multiple people.\nYou can also invite people who don't allow private chats if they accept the invite."
createRoom: "Create Room"
inviteUserToChat: "Invite users to start chatting"
yourRooms: "Created rooms"
joiningRooms: "Joined rooms"
invitations: "Invite"
noInvitations: "No invitations"
history: "History"
noHistory: "No history available"
noRooms: "No rooms found"
inviteUser: "Invite Users"
sentInvitations: "Sent Invites"
join: "Join"
ignore: "Ignore"
leave: "Leave room"
members: "Members"
searchMessages: "Search messages"
home: "Home"
send: "Send"
newline: "New line"
muteThisRoom: "Mute room"
deleteRoom: "Delete room"
chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account."
chatNotAvailableInOtherAccount: "The chat function is disabled for the other user."
cannotChatWithTheUser: "Cannot start a chat with this user"
cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat."
chatWithThisUser: "Chat with user"
thisUserAllowsChatOnlyFromFollowers: "This user accepts chats from followers only."
thisUserAllowsChatOnlyFromFollowing: "This user accepts chats only from users they follow."
thisUserAllowsChatOnlyFromMutualFollowing: "This user only accepts chats from users who are mutual followers."
thisUserNotAllowedChatAnyone: "This user is not accepting chats from anyone."
chatAllowedUsers: "Who to allow chatting with"
chatAllowedUsers_note: "You can chat with anyone to whom you have sent a chat message regardless of this setting."
_chatAllowedUsers:
everyone: "Everyone"
followers: "Only your followers"
following: "Only users you are following"
mutual: "Mutual followers only"
none: "Nobody"
_emojiPalette:
palettes: "Palette"
enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
@ -1362,6 +1406,13 @@ _settings:
timelineAndNote: "Timeline and note"
makeEveryTextElementsSelectable: "Make all text elements selectable"
makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations."
useStickyIcons: "Make icons follow while scrolling"
showNavbarSubButtons: "Show sub-buttons on the navigation bar"
ifOn: "When turned on"
ifOff: "When turned off"
_chat:
showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send"
_preferencesProfile:
profileName: "Profile name"
profileNameDescription: "Set a name that identifies this device."
@ -1871,6 +1922,7 @@ _role:
canImportFollowing: "Allow importing following"
canImportMuting: "Allow importing muting"
canImportUserLists: "Allow importing lists"
canChat: "Allow Chat"
_condition:
roleAssignedTo: "Assigned to manual roles"
isLocal: "Local user"
@ -2101,6 +2153,7 @@ _sfx:
noteMy: "Own note"
notification: "Notifications"
reaction: "On choosing a reaction"
chatMessage: "Chat Messages"
_soundSettings:
driveFile: "Use an audio file in Drive."
driveFileWarn: "Select an audio file from Drive."
@ -2248,6 +2301,7 @@ _permissions:
"read:federation": "Get federation data"
"write:report-abuse": "Report violation"
"write:chat": "Compose or delete chat messages"
"read:chat": "Browse Chat"
_auth:
shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
@ -2496,6 +2550,7 @@ _notification:
newNote: "New note"
unreadAntennaNote: "Antenna {name}"
roleAssigned: "Role given"
chatRoomInvitationReceived: "You have been invited to a chat room"
emptyPushNotificationMessage: "Push notifications have been updated"
achievementEarned: "Achievement unlocked"
testNotification: "Test notification"
@ -2524,6 +2579,7 @@ _notification:
receiveFollowRequest: "Received follow requests"
followRequestAccepted: "Accepted follow requests"
roleAssigned: "Role given"
chatRoomInvitationReceived: "Invited to chat room"
achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed"
login: "Sign In"
@ -2663,6 +2719,7 @@ _moderationLogTypes:
deletePage: "Page deleted"
deleteFlash: "Play deleted"
deleteGalleryPost: "Gallery post deleted"
deleteChatRoom: "Deleted Chat Room"
updateProxyAccountDescription: "Update the description of the proxy account"
_fileViewer:
title: "File details"

44
locales/index.d.ts vendored
View File

@ -5358,6 +5358,22 @@ export interface Locale extends ILocale {
*
*/
"migrateOldSettings_description": string;
/**
*
*/
"compress": string;
/**
*
*/
"right": string;
/**
*
*/
"bottom": string;
/**
*
*/
"top": string;
"_chat": {
/**
*
@ -5468,6 +5484,14 @@ export interface Locale extends ILocale {
*
*/
"deleteRoom": string;
/**
*
*/
"chatNotAvailableForThisAccountOrServer": string;
/**
* 使
*/
"chatNotAvailableInOtherAccount": string;
/**
*
*/
@ -5485,7 +5509,7 @@ export interface Locale extends ILocale {
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
@ -5622,6 +5646,10 @@ export interface Locale extends ILocale {
*
*/
"makeEveryTextElementsSelectable_description": string;
/**
*
*/
"useStickyIcons": string;
/**
*
*/
@ -10049,6 +10077,18 @@ export interface Locale extends ILocale {
*
*/
"columnAlign": string;
/**
*
*/
"columnGap": string;
/**
*
*/
"deckMenuPosition": string;
/**
*
*/
"navbarPosition": string;
/**
*
*/
@ -10102,7 +10142,7 @@ export interface Locale extends ILocale {
*/
"introduction": string;
/**
* +
* +
*/
"introduction2": string;
/**

View File

@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
explore: "Esplora"
messageRead: "Visualizzato"
noMoreHistory: "Non c'è più cronologia da visualizzare"
startChat: "Inizia a chattare"
nUsersRead: "Letto da {n} persone"
agreeTo: "Sono d'accordo con {0}"
agree: "Accetto"
@ -1331,12 +1332,55 @@ emojiPalette: "Tavolozza emoji"
postForm: "Finestra di pubblicazione"
textCount: "Il numero di caratteri"
information: "Informazioni"
chat: "Chat"
migrateOldSettings: "Migrare le vecchie impostazioni"
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
compress: "Comprimi"
_chat:
noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio"
individualChat: "Chat individuale"
individualChat_description: "Puoi chattare con una persona specifica."
roomChat: "Stanza di chat"
roomChat_description: "Puoi chattare con più persone.\nInoltre, anche le persone che non consentono chat personalizzate possono chattare se gli altri accettano."
createRoom: "Crea stanza"
inviteUserToChat: "Invita a chattare altre persone"
yourRooms: "Le tue stanze"
joiningRooms: "Stanze a cui partecipi"
invitations: "Invita"
noInvitations: "Nessun invito"
history: "Cronologia"
noHistory: "Nessuna cronologia"
noRooms: "Nessuna stanza"
inviteUser: "Invita"
sentInvitations: "Inviti spediti"
join: "Entra"
ignore: "Ignora"
leave: "Esci"
members: "Membri"
searchMessages: "Cerca messaggi"
home: "Home"
send: "Inviare"
newline: "Nuova riga"
muteThisRoom: "Silenzia stanza"
deleteRoom: "Elimina stanza"
chatNotAvailableForThisAccountOrServer: "Questo server, o questo profilo ha disabilitato la chat."
chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona."
cannotChatWithTheUser: "Impossibile chattare con questa persona"
cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo."
chatWithThisUser: "Chatta con questa persona"
thisUserAllowsChatOnlyFromFollowers: "Questa persona permette di chattare soltanto i propri Follower."
thisUserAllowsChatOnlyFromFollowing: "Questa persona permette di chattare soltanto ai suoi Follow."
thisUserAllowsChatOnlyFromMutualFollowing: "Questa persona permette di chattare solo a relazioni reciproche."
thisUserNotAllowedChatAnyone: "Questa persona non permette di chattare a nessuno."
chatAllowedUsers: "Persone ammesse alla chat"
chatAllowedUsers_note: "Puoi chattare con le persone a cui hai già inviato un messaggio, indipendentemente da questa impostazione."
_chatAllowedUsers:
everyone: "Chiunque"
followers: "Solo i tuoi Follower"
following: "Solo i tuoi Follow"
mutual: "Solo relazioni reciproche"
none: "Nessuno"
_emojiPalette:
palettes: "Tavolozza"
enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
@ -1362,6 +1406,12 @@ _settings:
timelineAndNote: "Note e Timeline"
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione"
ifOn: "Quando attivato"
ifOff: "Quando disattivato"
_chat:
showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce"
_preferencesProfile:
profileName: "Nome del profilo"
profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
@ -1871,6 +1921,7 @@ _role:
canImportFollowing: "Può importare Following"
canImportMuting: "Può importare Silenziati"
canImportUserLists: "Può importare liste di Profili"
canChat: "Chat consentita"
_condition:
roleAssignedTo: "Assegnato a ruoli manualmente"
isLocal: "Profilo locale"
@ -2101,6 +2152,7 @@ _sfx:
noteMy: "Mia nota"
notification: "Notifiche"
reaction: "Quando seleziono una reazione"
chatMessage: "Messaggio di chat"
_soundSettings:
driveFile: "Suoni del Drive"
driveFileWarn: "Seleziona file dal dispositivo"
@ -2248,6 +2300,7 @@ _permissions:
"read:federation": "Vedere la federazione"
"write:report-abuse": "Inviare segnalazioni"
"write:chat": "Gestire la chat"
"read:chat": "Visualizzare le chat"
_auth:
shareAccessTitle: "Permessi dell'applicazione"
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"
@ -2496,6 +2549,7 @@ _notification:
newNote: "Nuove Note"
unreadAntennaNote: "Antenna {name}"
roleAssigned: "Ruolo assegnato"
chatRoomInvitationReceived: "Invito in una stanza di chat"
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
achievementEarned: "Obiettivo raggiunto"
testNotification: "Provare la notifica"
@ -2524,6 +2578,7 @@ _notification:
receiveFollowRequest: "Richieste di follow in arrivo"
followRequestAccepted: "Richieste di follow accettate"
roleAssigned: "Ruolo concesso"
chatRoomInvitationReceived: "Invito in una stanza di chat"
achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata"
login: "Accessi"
@ -2663,6 +2718,7 @@ _moderationLogTypes:
deletePage: "Pagina eliminata"
deleteFlash: "Play eliminato"
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
deleteChatRoom: "Elimina chat"
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
_fileViewer:
title: "Dettagli del file"

View File

@ -1335,6 +1335,10 @@ information: "情報"
chat: "チャット"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮"
right: "右"
bottom: "下"
top: "上"
_chat:
noMessagesYet: "まだメッセージはありません"
@ -1364,11 +1368,13 @@ _chat:
newline: "改行"
muteThisRoom: "このルームをミュート"
deleteRoom: "ルームを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
@ -1406,6 +1412,7 @@ _settings:
timelineAndNote: "タイムラインとノート"
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
useStickyIcons: "アイコンをスクロールに追従させる"
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
ifOn: "オンのとき"
ifOff: "オフのとき"
@ -2658,6 +2665,9 @@ _notification:
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
columnGap: "カラム間のマージン"
deckMenuPosition: "デッキメニューの位置"
navbarPosition: "ナビゲーションバーの位置"
addColumn: "カラムを追加"
newNoteNotificationSettings: "新着ノート通知の設定"
configureColumn: "カラムの設定"
@ -2671,7 +2681,7 @@ _deck:
newProfile: "新規プロファイル"
deleteProfile: "プロファイルを削除"
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
introduction2: "カラムを追加するには、画面の + をクリックします。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"

View File

@ -1335,6 +1335,7 @@ information: "关于"
chat: "聊天"
migrateOldSettings: "迁移旧设置信息"
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
compress: "压缩"
_chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
@ -1363,6 +1364,8 @@ _chat:
newline: "换行"
muteThisRoom: "静音此房间"
deleteRoom: "删除房间"
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。"
cannotChatWithTheUser: "无法与此用户聊天"
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
chatWithThisUser: "聊天"
@ -1403,6 +1406,7 @@ _settings:
timelineAndNote: "时间线和帖子"
makeEveryTextElementsSelectable: "使所有的文字均可选择"
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
useStickyIcons: "使图标跟随滚动"
showNavbarSubButtons: "在导航栏中显示副按钮"
ifOn: "启用时"
ifOff: "关闭时"
@ -2546,6 +2550,7 @@ _notification:
newNote: "新的帖子"
unreadAntennaNote: "天线 {name}"
roleAssigned: "授予的角色"
chatRoomInvitationReceived: "受邀加入聊天室"
emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "获得成就"
testNotification: "测试通知"
@ -2574,6 +2579,7 @@ _notification:
receiveFollowRequest: "收到关注请求"
followRequestAccepted: "关注请求已通过"
roleAssigned: "授予的角色"
chatRoomInvitationReceived: "受邀加入聊天室"
achievementEarned: "取得的成就"
exportCompleted: "已完成导出"
login: "登录"
@ -2713,6 +2719,7 @@ _moderationLogTypes:
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件"
deleteChatRoom: "删除聊天室"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
title: "文件信息"

View File

@ -1335,6 +1335,7 @@ information: "關於"
chat: "聊天"
migrateOldSettings: "遷移舊設定資訊"
migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。"
compress: "壓縮"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@ -1350,7 +1351,7 @@ _chat:
noInvitations: "沒有邀請"
history: "歷史紀錄"
noHistory: "沒有歷史紀錄"
noRooms: "此聊天室不存在"
noRooms: "沒有可用的聊天室"
inviteUser: "邀請使用者"
sentInvitations: "已傳送的邀請"
join: "加入"
@ -1363,6 +1364,8 @@ _chat:
newline: "換行"
muteThisRoom: "此聊天室已靜音"
deleteRoom: "刪除聊天室"
chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。"
chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。"
cannotChatWithTheUser: "無法與此使用者聊天"
cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。"
chatWithThisUser: "聊天"
@ -1403,6 +1406,7 @@ _settings:
timelineAndNote: "時間軸及貼文"
makeEveryTextElementsSelectable: "允許選取所有文字"
makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。"
useStickyIcons: "使大頭貼跟隨捲動"
showNavbarSubButtons: "在導覽列顯示輔助按鈕"
ifOn: "開啟時"
ifOff: "關閉時"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.3.2-beta.13",
"version": "2025.3.2-beta.20",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -306,7 +306,7 @@ export type GlobalEvents = {
name: 'notesStream';
payload: Serialized<Packed<'Note'>>;
};
chat: {
chatUser: {
name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};

View File

@ -69,7 +69,7 @@ export class QueryService {
// ここでいうBlockedは被Blockedの意
@bindThis
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockerId')
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
@ -127,7 +127,7 @@ export class QueryService {
}
@bindThis
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });

View File

@ -234,8 +234,8 @@ export class SearchService {
}
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
return query.limit(pagination.limit).getMany();
}

View File

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
import { type FollowingsRepository, MiUser, type MutingsRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import type { Config } from '@/config.js';
@ -22,10 +22,19 @@ export class UserSearchService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userEntityService: UserEntityService,
) {
}
@ -58,7 +67,7 @@ export class UserSearchService {
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
*/
@bindThis
public async search(
public async searchByUsernameAndHost(
params: {
username?: string | null,
host?: string | null,
@ -202,4 +211,91 @@ export class UserSearchService {
return userQuery;
}
@bindThis
public async search(query: string, meId: MiUser['id'] | null, options: Partial<{
limit: number;
offset: number;
origin: 'local' | 'remote' | 'combined';
}> = {}) {
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
const isUsername = query.startsWith('@') && !query.includes(' ') && query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
const mutingQuery = meId == null ? null : this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: meId });
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (mutingQuery) {
nameQuery.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`);
nameQuery.setParameters(mutingQuery.getParameters());
}
if (options.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (options.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(options.limit)
.offset(options.offset)
.getMany();
if (users.length < (options.limit ?? 30)) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' });
if (mutingQuery) {
profQuery.andWhere(`prof.userId NOT IN (${mutingQuery.getQuery()})`);
profQuery.setParameters(mutingQuery.getParameters());
}
if (options.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (options.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const userQuery = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await userQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(options.limit)
.offset(options.offset)
.getMany(),
);
}
return users;
}
}

View File

@ -463,6 +463,7 @@ export class WebhookTestService {
followersVisibility: 'public',
followingVisibility: 'public',
chatScope: 'mutual',
canChat: true,
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,

View File

@ -557,6 +557,7 @@ export class UserEntityService implements OnModuleInit {
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
chatScope: user.chatScope,
canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat),
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,

View File

@ -363,6 +363,10 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
},
canChat: {
type: 'boolean',
nullable: false, optional: false,
},
roles: {
type: 'array',
nullable: false, optional: false,

View File

@ -109,8 +109,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {

View File

@ -122,8 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
//#endregion

View File

@ -87,8 +87,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) {
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const notes = await query

View File

@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const notes = await query.limit(ps.limit).getMany();

View File

@ -79,8 +79,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}

View File

@ -243,8 +243,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -156,8 +156,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {

View File

@ -72,9 +72,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });

View File

@ -72,8 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const renotes = await query.limit(ps.limit).getMany();

View File

@ -56,8 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const timeline = await query.limit(ps.limit).getMany();

View File

@ -81,8 +81,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
try {
if (ps.tag) {

View File

@ -199,8 +199,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -184,8 +184,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);

View File

@ -185,8 +185,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
if (ps.withFiles) {

View File

@ -63,7 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQueryForUsers(query, me);
this.queryService.generateBlockQueryForUsers(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')

View File

@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userSearchService: UserSearchService,
) {
super(meta, paramDef, (ps, me) => {
return this.userSearchService.search({
return this.userSearchService.searchByUsernameAndHost({
username: ps.username,
host: ps.host,
}, {

View File

@ -3,14 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { UserSearchService } from '@/core/UserSearchService.js';
export const meta = {
tags: ['users'],
@ -45,79 +42,15 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private userSearchService: UserSearchService,
) {
super(meta, paramDef, async (ps, me) => {
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
}
const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, {
offset: ps.offset,
limit: ps.limit,
origin: ps.origin,
});
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});

View File

@ -35,7 +35,7 @@ class ChatRoomChannel extends Channel {
}
@bindThis
private async onEvent(data: GlobalEvents['chat']['payload']) {
private async onEvent(data: GlobalEvents['chatRoom']['payload']) {
this.send(data.type, data.body);
}

View File

@ -35,7 +35,7 @@ class ChatUserChannel extends Channel {
}
@bindThis
private async onEvent(data: GlobalEvents['chat']['payload']) {
private async onEvent(data: GlobalEvents['chatUser']['payload']) {
this.send(data.type, data.body);
}

View File

@ -84,6 +84,7 @@ describe('ユーザー', () => {
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
chatScope: user.chatScope,
canChat: user.canChat,
roles: user.roles,
memo: user.memo,
});
@ -346,6 +347,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
assert.strictEqual(response.chatScope, 'mutual');
assert.strictEqual(response.canChat, true);
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@ -732,7 +734,7 @@ describe('ユーザー', () => {
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれ', user: () => userMutedByAlice },
{ label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },

View File

@ -134,13 +134,13 @@ describe('UserSearchService', () => {
await app.close();
});
describe('search', () => {
describe('searchByUsernameAndHost', () => {
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alycia, alysha, alyson]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@ -154,7 +154,7 @@ describe('UserSearchService', () => {
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@ -168,7 +168,7 @@ describe('UserSearchService', () => {
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@ -181,7 +181,7 @@ describe('UserSearchService', () => {
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,
@ -195,7 +195,7 @@ describe('UserSearchService', () => {
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
await setInactive([alyson, alice, alysha, bobbie, bobby]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ },
{ limit: 100 },
root,
@ -216,7 +216,7 @@ describe('UserSearchService', () => {
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setInactive([alice, alyce, alycia]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
);
@ -228,7 +228,7 @@ describe('UserSearchService', () => {
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
);
@ -240,7 +240,7 @@ describe('UserSearchService', () => {
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al', host: 'exam' },
{ limit: 100 },
root,
@ -253,7 +253,7 @@ describe('UserSearchService', () => {
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
await setSuspended([alice, alyce, alycia]);
const result = await service.search(
const result = await service.searchByUsernameAndHost(
{ username: 'al' },
{ limit: 100 },
root,

View File

@ -34,7 +34,7 @@
"typescript": "5.8.2",
"uuid": "11.1.0",
"json5": "2.2.3",
"vite": "6.2.2",
"vite": "6.2.4",
"vue": "3.5.13"
},
"devDependencies": {

View File

@ -95,7 +95,7 @@ async function onclick(ev: MouseEvent) {
position: absolute;
border-radius: 6px;
background-color: var(--MI_THEME-fg);
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@ -153,7 +153,7 @@ html[data-color-scheme=light] .visible {
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
display: inline-block;
font-weight: bold;
font-size: 0.8em;

View File

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible, isHeadVisible } from '@@/js/scroll.js';
import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible, isHeadVisible } from '@@/js/scroll.js';
import type { ComputedRef } from 'vue';
import { misskeyApi } from '@/misskey-api.js';
import { i18n } from '@/i18n.js';
@ -252,7 +252,7 @@ const fetchMore = async (): Promise<void> => {
return nextTick(() => {
if (scrollableElement.value) {
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}

View File

@ -278,7 +278,7 @@ rt {
}
._acrylic {
background: var(--MI_THEME-acrylicPanel);
background: color(from var(--MI_THEME-panel) srgb r g b / 0.5);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}

View File

@ -93,7 +93,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
return removeListener;
}
export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
export function scrollInContainer(el: HTMLElement, options: ScrollToOptions | undefined) {
const container = getScrollContainer(el);
if (container == null) {
window.scroll(options);
@ -108,7 +108,7 @@ export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
* @param options Scroll options
*/
export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
scroll(el, { top: 0, ...options });
scrollInContainer(el, { top: 0, ...options });
}
/**

View File

@ -10,13 +10,10 @@
props: {
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentedBg: ':alpha<0.15<@accent',
love: '#dd2e44',
focus: ':alpha<0.3<@accent',
bg: '#000',
acrylicBg: ':alpha<0.5<@bg',
fg: '#dadada',
fgTransparentWeak: ':alpha<0.75<@fg',
fgTransparent: ':alpha<0.5<@fg',
@ -31,7 +28,6 @@
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
shadow: 'rgba(0, 0, 0, 0.3)',

View File

@ -10,13 +10,10 @@
props: {
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentedBg: ':alpha<0.15<@accent',
love: '#dd2e44',
focus: ':alpha<0.3<@accent',
bg: '#fff',
acrylicBg: ':alpha<0.5<@bg',
fg: '#5f5f5f',
fgTransparentWeak: ':alpha<0.75<@fg',
fgTransparent: ':alpha<0.5<@fg',
@ -31,7 +28,6 @@
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
shadow: 'rgba(0, 0, 0, 0.1)',

View File

@ -25,7 +25,6 @@
mention: '#ffd152',
modalBg: 'rgba(0, 0, 0, 0.5)',
success: '#86b300',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '#fb5d38',
messageBg: '@bg',
@ -37,10 +36,7 @@
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
buttonGradateA: '@accent',
buttonGradateB: ':hue<-20<@accent',
driveFolderBg: ':alpha<0.3<@accent',

View File

@ -31,7 +31,6 @@
modalBg: 'rgba(0, 0, 0, 0.5)',
success: '#86b300',
switchBg: 'rgba(255, 255, 255, 0.15)',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '@mention',
messageBg: '@bg',
@ -48,10 +47,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
accentLighten: ':lighten<10<@accent',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',
fgTransparent: ':alpha<0.5<@fg',

View File

@ -32,7 +32,6 @@
success: '#86b300',
buttonBg: '#0000000d',
switchBg: 'rgba(255, 255, 255, 0.15)',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '@mention',
messageBg: '@bg',
@ -49,10 +48,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: '#0000001a',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',

View File

@ -28,7 +28,6 @@
mention: '@accent',
modalBg: 'rgba(0, 0, 0, 0.3)',
success: '#86b300',
acrylicBg: ':alpha<0.5<@bg',
indicator: '@accent',
mentionMe: '@mention',
messageBg: '@bg',
@ -40,10 +39,7 @@
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':darken<3<@fg',
fgTransparent: ':alpha<0.5<@fg',

View File

@ -58,7 +58,12 @@ export default [
// location ... window.locationと衝突 or 紛らわしい
// document ... window.documentと衝突 or 紛らわしい
// history ... window.historyと衝突 or 紛らわしい
'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history'],
// scroll ... window.scrollと衝突 or 紛らわしい
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
// setInterval ... window.setIntervalと衝突 or 紛らわしい
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
'no-restricted-globals': [
'error',
{
@ -85,6 +90,26 @@ export default [
'name': 'history',
'message': 'Use `window.history`.',
},
{
'name': 'scroll',
'message': 'Use `window.scroll`.',
},
{
'name': 'setTimeout',
'message': 'Use `window.setTimeout`.',
},
{
'name': 'setInterval',
'message': 'Use `window.setInterval`.',
},
{
'name': 'clearTimeout',
'message': 'Use `window.clearTimeout`.',
},
{
'name': 'clearInterval',
'message': 'Use `window.clearInterval`.',
},
{
'name': 'name',
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',

View File

@ -74,7 +74,7 @@
"typescript": "5.8.2",
"uuid": "11.1.0",
"v-code-diff": "1.13.1",
"vite": "6.2.2",
"vite": "6.2.4",
"vue": "3.5.13",
"vuedraggable": "next",
"wanakana": "5.3.1"

View File

@ -29,7 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
export async function common(createVue: () => App<Element>) {
export async function common(createVue: () => Promise<App<Element>>) {
console.info(`Misskey v${version}`);
if (_DEV_) {
@ -263,7 +263,7 @@ export async function common(createVue: () => App<Element>) {
});
});
const app = createVue();
const app = await createVue();
if (_DEV_) {
app.config.performance = true;

View File

@ -32,7 +32,7 @@ import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
export async function mainBoot() {
const { isClientUpdated, lastVersion } = await common(() => {
const { isClientUpdated, lastVersion } = await common(async () => {
let uiStyle = ui;
const searchParams = new URLSearchParams(window.location.search);
@ -46,19 +46,16 @@ export async function mainBoot() {
let rootComponent: Component;
switch (uiStyle) {
case 'zen':
rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue'));
rootComponent = await import('@/ui/zen.vue').then(x => x.default);
break;
case 'deck':
rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue'));
rootComponent = await import('@/ui/deck.vue').then(x => x.default);
break;
case 'visitor':
rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue'));
break;
case 'classic':
rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue'));
rootComponent = await import('@/ui/visitor.vue').then(x => x.default);
break;
default:
rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue'));
rootComponent = await import('@/ui/universal.vue').then(x => x.default);
break;
}

View File

@ -6,11 +6,10 @@
import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js';
import { emojiPicker } from '@/utility/emoji-picker.js';
import UiMinimum from '@/ui/minimum.vue';
export async function subBoot() {
const { isClientUpdated } = await common(() => createApp(
defineAsyncComponent(() => import('@/ui/minimum.vue')),
));
const { isClientUpdated } = await common(async () => createApp(UiMinimum));
emojiPicker.init();
}

View File

@ -420,7 +420,7 @@ onBeforeUnmount(() => {
}
&:active {
background: var(--MI_THEME-accentDarken);
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
color: #fff !important;
}
}

View File

@ -2,20 +2,18 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelFollowButton from './MkChannelFollowButton.vue';
import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js';
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {

View File

@ -103,13 +103,13 @@ async function onClick() {
background: var(--MI_THEME-accent);
&:hover {
background: var(--MI_THEME-accentLighten);
border-color: var(--MI_THEME-accentLighten);
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
&:active {
background: var(--MI_THEME-accentDarken);
border-color: var(--MI_THEME-accentDarken);
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
}
}

View File

@ -2,18 +2,16 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue';
import type { StoryObj } from '@storybook/vue3';
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {

View File

@ -151,11 +151,11 @@ function onDragend() {
background: var(--MI_THEME-accent);
&:hover {
background: var(--MI_THEME-accentLighten);
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
&:active {
background: var(--MI_THEME-accentDarken);
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
}
> .label {

View File

@ -4,34 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<nav :class="$style.nav">
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
:parentFolder="folder"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
<template v-for="f in hierarchyFolders">
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
<MkStickyContainer>
<template #header>
<nav :class="$style.nav">
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder
:folder="f"
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
:parentFolder="folder"
:class="[$style.navPathItem]"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
</template>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
</div>
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
</nav>
<template v-for="f in hierarchyFolders">
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
<XNavFolder
:folder="f"
:parentFolder="folder"
:class="[$style.navPathItem]"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
</template>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
</div>
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
</nav>
</template>
<div
ref="main"
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
@ -91,8 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="fetching"/>
</div>
<div v-if="draghover" :class="$style.dropzone"></div>
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
@ -110,6 +112,7 @@ import { i18n } from '@/i18n.js';
import { uploadFile, uploads } from '@/utility/upload.js';
import { claimAchievement } from '@/utility/achievements.js';
import { prefer } from '@/preferences.js';
import { chooseFileFromPc } from '@/utility/select-file.js';
const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder;
@ -130,7 +133,6 @@ const emit = defineEmits<{
}>();
const loadMoreFiles = useTemplateRef('loadMoreFiles');
const fileInput = useTemplateRef('fileInput');
const folder = ref<Misskey.entities.DriveFolder | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]);
@ -142,7 +144,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads;
const connection = useStream().useChannel('drive');
const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // $ref使
//
const draghover = ref(false);
@ -304,10 +305,6 @@ function onDrop(ev: DragEvent) {
//#endregion
}
function selectLocalFile() {
fileInput.value?.click();
}
function urlUpload() {
os.inputText({
title: i18n.ts.uploadFromUrl,
@ -383,15 +380,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
});
}
function onChangeFileInput() {
if (!fileInput.value?.files) return;
for (const file of Array.from(fileInput.value.files)) {
upload(file, folder.value);
}
}
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) {
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => {
addFile(res, true);
});
}
@ -630,16 +620,20 @@ function getMenu() {
const menu: MenuItem[] = [];
menu.push({
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
}, { type: 'divider' }, {
text: i18n.ts.addFile,
type: 'label',
}, {
text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
icon: 'ti ti-upload',
action: () => {
chooseFileFromPc(true, { keepOriginal: false });
},
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => { selectLocalFile(); },
action: () => {
chooseFileFromPc(true, { keepOriginal: true });
},
}, {
text: i18n.ts.fromUrl,
icon: 'ti ti-link',
@ -751,22 +745,17 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" module>
.root {
display: flex;
flex-direction: column;
height: 100%;
}
.nav {
display: flex;
z-index: 2;
width: 100%;
padding: 0 8px;
box-sizing: border-box;
overflow: auto;
font-size: 0.9em;
box-shadow: 0 1px 0 var(--MI_THEME-divider);
user-select: none;
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
.navPath {

View File

@ -79,7 +79,7 @@ function opening() {
picker.value?.focus();
//
setTimeout(() => {
window.setTimeout(() => {
picker.value?.focus();
}, 10);
}

View File

@ -239,7 +239,7 @@ onMounted(() => {
bottom: var(--MI-stickyBottom, 0px);
left: 0;
padding: 12px;
background: var(--MI_THEME-acrylicBg);
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
background-size: auto auto;

View File

@ -211,13 +211,13 @@ onBeforeUnmount(() => {
background: var(--MI_THEME-accent);
&:hover {
background: var(--MI_THEME-accentLighten);
border-color: var(--MI_THEME-accentLighten);
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
&:active {
background: var(--MI_THEME-accentDarken);
border-color: var(--MI_THEME-accentDarken);
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
}
}

View File

@ -51,7 +51,7 @@ withDefaults(defineProps<{
padding-top: calc(var(--fukidashi-radius) * .13);
&.accented {
--fukidashi-bg: var(--MI_THEME-accent);
--fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%);
}
&.shadow {
@ -87,6 +87,12 @@ withDefaults(defineProps<{
padding: 10px 14px;
}
@container (max-width: 450px) {
.content {
padding: 8px 12px;
}
}
.tail {
position: absolute;
top: 0;

View File

@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) {
pullDistance.value = 0;
isSwiping.value = false;
setTimeout(() => {
window.setTimeout(() => {
isSwipingForClass.value = false;
}, 400);

View File

@ -220,7 +220,7 @@ function showMenu(ev: MouseEvent) {
position: absolute;
border-radius: 6px;
background-color: var(--MI_THEME-fg);
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@ -294,7 +294,7 @@ html[data-color-scheme=light] .visible {
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
display: inline-block;
font-weight: bold;
font-size: 0.8em;

View File

@ -339,7 +339,7 @@ const bufferedDataRatio = computed(() => {
// MediaControl Events
function onMouseOver() {
if (controlStateTimer) {
clearTimeout(controlStateTimer);
window.clearTimeout(controlStateTimer);
}
isHoverring.value = true;
}
@ -553,7 +553,7 @@ onDeactivated(() => {
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
display: inline-block;
font-weight: bold;
font-size: 0.8em;
@ -565,7 +565,7 @@ onDeactivated(() => {
position: absolute;
border-radius: 6px;
background-color: var(--MI_THEME-fg);
color: var(--MI_THEME-accentLighten);
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
font-size: 12px;
opacity: .5;
padding: 5px 8px;

View File

@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js';
import { focusTrap } from '@/utility/focus-trap.js';
import { focusParent } from '@/utility/focus.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null;
@ -94,7 +95,7 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
provide('modal', true);
provide(DI.inModal, true);
const maxHeight = ref<number>();
const fixed = ref(false);

View File

@ -14,8 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
<div v-if="isRenote" :class="$style.renote">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@ -47,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="$style.main">
<MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
@ -660,7 +658,6 @@ function emitUpdReaction(emoji: string, delta: number) {
<style lang="scss" module>
.root {
position: relative;
transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: clip;
contain: content;
@ -726,6 +723,8 @@ function emitUpdReaction(emoji: string, delta: number) {
}
.skipRender {
// TODO: TransitionGroupnote
// TransitionskipRender
content-visibility: auto;
contain-intrinsic-size: 0 150px;
}
@ -852,9 +851,12 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 14px 0 0;
width: 58px;
height: 58px;
position: sticky !important;
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
&.useSticky {
position: sticky !important;
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
}
.main {
@ -1041,7 +1043,10 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 10px 0 0;
width: 46px;
height: 46px;
top: calc(14px + var(--MI-stickyTop, 0px));
&.useSticky {
top: calc(14px + var(--MI-stickyTop, 0px));
}
}
}

View File

@ -13,32 +13,37 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap }]">
<MkDateSeparatedList
ref="notes"
v-slot="{ item: note }"
:items="notes"
:direction="pagination.reversed ? 'up' : 'down'"
:reversed="pagination.reversed"
:noGap="noGap"
:ad="true"
:class="$style.notes"
>
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
</MkDateSeparatedList>
</div>
<component
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
:moveClass=" $style.transition_x_move"
tag="div"
>
<template v-for="(note, i) in notes" :key="note.id">
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]">
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
<div :class="$style.ad">
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
</div>
</div>
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true"/>
</template>
</component>
</template>
</MkPagination>
</template>
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { useTemplateRef, TransitionGroup } from 'vue';
import type { Paging } from '@/components/MkPagination.vue';
import MkNote from '@/components/MkNote.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import { prefer } from '@/preferences.js';
const props = defineProps<{
pagination: Paging;
@ -54,22 +59,49 @@ defineExpose({
</script>
<style lang="scss" module>
.transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
}
.transition_x_enterFrom,
.transition_x_leaveTo {
opacity: 0;
transform: translateY(-50%);
}
.transition_x_leaveActive {
position: absolute;
}
.root {
container-type: inline-size;
&.noGap {
> .notes {
background: var(--MI_THEME-panel);
background: var(--MI_THEME-panel);
.note {
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
.ad {
padding: 8px;
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
}
&:not(.noGap) {
> .notes {
background: var(--MI_THEME-bg);
background: var(--MI_THEME-bg);
.note {
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
.note {
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
}
}
.ad:empty {
display: none;
}
</style>

View File

@ -14,22 +14,31 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
<component
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
:moveClass=" $style.transition_x_move"
tag="div"
>
<template v-for="(notification, i) in notifications" :key="notification.id">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
</template>
</component>
</template>
</MkPagination>
</MkPullToRefresh>
</template>
<script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue';
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js';
import type { notificationTypes } from '@@/js/const.js';
import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
@ -84,28 +93,36 @@ onMounted(() => {
connection.on('notificationFlushed', reload);
});
onActivated(() => {
pagingComponent.value?.reload();
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
connection.on('notificationFlushed', reload);
});
onUnmounted(() => {
if (connection) connection.dispose();
});
onDeactivated(() => {
if (connection) connection.dispose();
});
defineExpose({
reload,
});
</script>
<style lang="scss" module>
.list {
.transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
}
.transition_x_enterFrom,
.transition_x_leaveTo {
opacity: 0;
transform: translateY(-50%);
}
.transition_x_leaveActive {
position: absolute;
}
.notifications {
container-type: inline-size;
background: var(--MI_THEME-panel);
}
.item {
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
</style>

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="error" @retry="init()"/>
<div v-else-if="empty" key="_empty_" class="empty">
<div v-else-if="empty" key="_empty_">
<slot name="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/>
@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
<MkLoading v-else/>
</div>
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
<div v-show="!pagination.reversed && more" key="_more_">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
<MkLoading v-else/>
</div>
</div>
</Transition>
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js';
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js';
import type { ComputedRef } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -258,7 +258,7 @@ const fetchMore = async (): Promise<void> => {
return nextTick(() => {
if (scrollableElement.value) {
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}
@ -357,7 +357,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
clearTimeout(timerForSetPause);
window.clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@ -453,11 +453,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
setTimeout(toBottom, 800);
window.setTimeout(toBottom, 800);
// scrollToBottommoreFetching
// more = true
setTimeout(() => {
window.setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@ -467,11 +467,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
if (timerForSetPause) {
clearTimeout(timerForSetPause);
window.clearTimeout(timerForSetPause);
timerForSetPause = null;
}
if (preventAppearFetchMoreTimer.value) {
clearTimeout(preventAppearFetchMoreTimer.value);
window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
scrollObserver.value?.disconnect();

View File

@ -140,7 +140,7 @@ import { DI } from '@/di.js';
const $i = ensureSignin();
const modal = inject('modal');
const modal = inject(DI.inModal, false);
const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
@ -1314,7 +1314,7 @@ html[data-color-scheme=light] .preview {
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
font-size: 110%;
border: none;
border-radius: 0;
background: transparent;

View File

@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div :class="{ [$style.slotClip]: isPullStart }">
<slot/>
</div>
<slot/>
</div>
</template>
@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> {
return;
}
const startTime = Date.now();
let intervalId = setInterval(() => {
let intervalId = window.setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
pullDistance.value = to;
clearInterval(intervalId);
window.clearInterval(intervalId);
r();
return;
}
@ -261,8 +260,4 @@ defineExpose({
margin: 5px 0;
}
}
.slotClip {
overflow-y: clip;
}
</style>

View File

@ -159,12 +159,13 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
const onDrag = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault();
let beforeValue = finalValue.value;
const containerRect = containerEl.value!.getBoundingClientRect();
const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
if (props.continuousUpdate) {
if (props.continuousUpdate && beforeValue !== finalValue.value) {
emit('update:modelValue', finalValue.value);
}
};
@ -286,7 +287,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
border-radius: 999px;
&:hover {
background: var(--MI_THEME-accentLighten);
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
}
}

View File

@ -4,22 +4,24 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<TransitionGroup
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
<component
:is="prefer.s.animation ? TransitionGroup : 'div'"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
:moveClass="$style.transition_x_move"
tag="div" :class="$style.root"
>
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
<slot v-if="hasMoreReactions" name="more"/>
</TransitionGroup>
</component>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { inject, watch, ref } from 'vue';
import { TransitionGroup } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';

View File

@ -125,7 +125,7 @@ async function done() {
left: 0;
padding: 12px;
border-top: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-acrylicBg);
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}

View File

@ -144,7 +144,7 @@ fetchRoles();
}
.roleItemArea {
background-color: var(--MI_THEME-acrylicBg);
background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
border-radius: var(--MI-radius);
padding: 12px;
overflow-y: auto;

View File

@ -84,7 +84,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
align-items: center;
font-weight: bold;
backdrop-filter: var(--MI-blur, blur(15px));
background: var(--MI_THEME-acrylicBg);
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
z-index: 1;
}

View File

@ -280,7 +280,7 @@ onMounted(async () => {
left: 0;
padding: 12px;
border-top: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-acrylicBg);
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}

View File

@ -4,50 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<svg
version="1.1"
viewBox="0 0 203.2 152.4"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<svg viewBox="0 0 200 150">
<g fill-rule="evenodd">
<rect width="203.2" height="152.4" :fill="themeVariables.bg" stroke-width=".26458" />
<rect width="65.498" height="152.4" :fill="themeVariables.panel" stroke-width=".26458" />
<rect x="65.498" width="137.7" height="40.892" :fill="themeVariables.acrylicBg" stroke-width=".265" />
<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel" />
<rect width="200" height="150" :fill="themeVariables.bg"/>
<rect width="64" height="150" :fill="themeVariables.navBg"/>
<rect x="64" width="136" height="41" :fill="themeVariables.bg"/>
<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel"/>
</g>
<circle cx="32.749" cy="83.054" r="21.132" :fill="themeVariables.accentedBg" stroke-dasharray="0.319256, 0.319256" stroke-width=".15963" style="paint-order:stroke fill markers" />
<circle cx="136.67" cy="106.76" r="23.876" :fill="themeVariables.fg" fill-opacity="0.5" stroke-dasharray="0.352425, 0.352425" stroke-width=".17621" style="paint-order:stroke fill markers" />
<g :fill="themeVariables.fg" fill-rule="evenodd" stroke-width=".26458">
<rect x="171.27" y="87.815" width="48.576" height="6.8747" ry="3.4373"/>
<rect x="171.27" y="105.09" width="48.576" height="6.875" ry="3.4375"/>
<rect x="171.27" y="121.28" width="48.576" height="6.875" ry="3.4375"/>
<rect x="171.27" y="137.47" width="48.576" height="6.875" ry="3.4375"/>
<circle cx="32" cy="83" r="21" :fill="themeVariables.accentedBg"/>
<circle cx="136" cy="106" r="23" :fill="themeVariables.fg" fill-opacity="0.5"/>
<g :fill="themeVariables.fg" fill-rule="evenodd">
<rect x="171" y="88" width="48" height="6" ry="3"/>
<rect x="171" y="108" width="48" height="6" ry="3"/>
<rect x="171" y="128" width="48" height="6" ry="3"/>
</g>
<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75" />
<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75"/>
<g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
<path d="m5 12h-2l9-9 9 9h-2" />
<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7" />
<path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6" />
<path d="m0 0h24v24h-24z" fill="none" stroke="none"/>
<path d="m5 12h-2l9-9 9 9h-2"/>
<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"/>
<path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6"/>
</g>
<g transform="matrix(.61621 0 0 .61621 25.354 117.92)" fill="none" :stroke="themeVariables.fg" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6" />
<path d="m9 17v1a3 3 0 0 0 6 0v-1" />
<path d="m0 0h24v24h-24z" fill="none" stroke="none"/>
<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6"/>
<path d="m9 17v1a3 3 0 0 0 6 0v-1"/>
</g>
<image x="20.948" y="18.388" width="23.602" height="23.602" image-rendering="optimizeSpeed" preserveAspectRatio="xMidYMid meet" v-bind="{ 'xlink:href': instance.iconUrl || '/favicon.ico' }" />
<circle cx="32" cy="32" r="16" :fill="themeVariables.accent"/>
<circle cx="140" cy="20" r="6" :fill="themeVariables.success"/>
<circle cx="160" cy="20" r="6" :fill="themeVariables.warn"/>
<circle cx="180" cy="20" r="6" :fill="themeVariables.error"/>
</svg>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { compile } from '@/theme.js';
import type { Theme } from '@/theme.js';
import { deepClone } from '@/utility/clone.js';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import type { Theme } from '@/theme.js';
import { compile } from '@/theme.js';
import { deepClone } from '@/utility/clone.js';
const props = defineProps<{
theme: Theme;
@ -55,20 +51,23 @@ const props = defineProps<{
const themeVariables = ref<{
bg: string;
acrylicBg: string;
panel: string;
fg: string;
divider: string;
accent: string;
accentedBg: string;
navBg: string;
}>({
bg: 'var(--MI_THEME-bg)',
acrylicBg: 'var(--MI_THEME-acrylicBg)',
panel: 'var(--MI_THEME-panel)',
fg: 'var(--MI_THEME-fg)',
divider: 'var(--MI_THEME-divider)',
accent: 'var(--MI_THEME-accent)',
accentedBg: 'var(--MI_THEME-accentedBg)',
navBg: 'var(--MI_THEME-navBg)',
success: 'var(--MI_THEME-success)',
warn: 'var(--MI_THEME-warn)',
error: 'var(--MI_THEME-error)',
});
watch(() => props.theme, (theme) => {
@ -76,7 +75,7 @@ watch(() => props.theme, (theme) => {
const _theme = deepClone(theme);
if (_theme?.base != null) {
if (_theme.base != null) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
@ -85,12 +84,15 @@ watch(() => props.theme, (theme) => {
themeVariables.value = {
bg: compiled.bg ?? 'var(--MI_THEME-bg)',
acrylicBg: compiled.acrylicBg ?? 'var(--MI_THEME-acrylicBg)',
panel: compiled.panel ?? 'var(--MI_THEME-panel)',
fg: compiled.fg ?? 'var(--MI_THEME-fg)',
divider: compiled.divider ?? 'var(--MI_THEME-divider)',
accent: compiled.accent ?? 'var(--MI_THEME-accent)',
accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)',
navBg: compiled.navBg ?? 'var(--MI_THEME-navBg)',
success: compiled.success ?? 'var(--MI_THEME-success)',
warn: compiled.warn ?? 'var(--MI_THEME-warn)',
error: compiled.error ?? 'var(--MI_THEME-error)',
};
}, { immediate: true });
</script>

View File

@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''"
appear @afterLeave="emit('closed')"
appear :css="prefer.s.animation"
@afterLeave="emit('closed')"
>
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>

View File

@ -36,7 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
</div>
</div>
<div v-else></div>
</template>
<script lang="ts" setup>
@ -53,7 +52,7 @@ import { prefer } from '@/preferences.js';
type Ad = (typeof instance)['ads'][number];
const props = defineProps<{
prefer: string[];
preferForms: string[];
specify?: Ad;
}>();
@ -72,7 +71,7 @@ const choseAd = (): Ad | null => {
ratio: 0,
} : ad);
let ads = allAds.filter(ad => props.prefer.includes(ad.place));
let ads = allAds.filter(ad => props.preferForms.includes(ad.place));
if (ads.length === 0) {
ads = allAds.filter(ad => ad.place === 'square');

View File

@ -25,7 +25,7 @@ const props = defineProps<{
menuReaction?: boolean;
}>();
const react = inject(DI.mfmEmojiReactCallback);
const react = inject(DI.mfmEmojiReactCallback, null);
const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;

View File

@ -2,11 +2,10 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { waitFor } from '@storybook/test';
import type { StoryObj } from '@storybook/vue3';
import MkPageHeader from './MkPageHeader.vue';
import type { StoryObj } from '@storybook/vue3';
export const Empty = {
render(args) {
return {
@ -29,7 +28,7 @@ export const Empty = {
};
},
async play() {
const wait = new Promise((resolve) => setTimeout(resolve, 800));
const wait = new Promise((resolve) => window.setTimeout(resolve, 800));
await waitFor(async () => await wait);
},
args: {

View File

@ -133,7 +133,7 @@ async function enter(el: Element) {
entering = false;
});
setTimeout(renderTab, 170);
window.setTimeout(renderTab, 170);
}
function afterEnter(el: Element) {

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
<div v-if="show" ref="el" :class="[$style.root]">
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
<MkAvatar :class="$style.avatar" :user="$i"/>
@ -42,7 +42,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import { scrollToTop } from '@@/js/scroll.js';
import XTabs from './MkPageHeader.tabs.vue';
import type { Tab } from './MkPageHeader.tabs.vue';
@ -69,8 +68,7 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string);
}>();
const viewId = inject(DI.viewId);
const viewTransitionName = computed(() => `${viewId}---pageHeader`);
//const viewId = inject(DI.viewId);
const injectedPageMetadata = inject(DI.pageMetadata);
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
@ -78,7 +76,6 @@ const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = useTemplateRef('el');
const bg = ref<string | undefined>(undefined);
const narrow = ref(false);
const hasTabs = computed(() => props.tabs.length > 0);
const hasActions = computed(() => props.actions && props.actions.length > 0);
@ -106,19 +103,9 @@ function onTabClick(): void {
top();
}
const calcBg = () => {
const rawBg = 'var(--MI_THEME-bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
let ro: ResizeObserver | null;
onMounted(() => {
calcBg();
globalEvents.on('themeChanging', calcBg);
if (el.value && el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
@ -131,18 +118,17 @@ onMounted(() => {
});
onUnmounted(() => {
globalEvents.off('themeChanging', calcBg);
if (ro) ro.disconnect();
});
</script>
<style lang="scss" module>
.root {
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
width: 100%;
view-transition-name: v-bind(viewTransitionName);
}
.upper,

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template>
<div :class="$style.body">
@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { scrollInContainer } from '@@/js/scroll.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import type { Tab } from './MkPageHeader.tabs.vue';
@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{
});
const tab = defineModel<string>('tab');
const rootEl = useTemplateRef('rootEl');
defineExpose({
scrollToTop: () => {
if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
},
});
</script>
<style lang="scss" module>

View File

@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1);
const rootEl = useTemplateRef('rootEl');
onMounted(() => {
rootEl.value.style.viewTransitionName = viewId; // view-transition-namecss var使
if (prefer.s.animation) {
rootEl.value.style.viewTransitionName = viewId; // view-transition-namecss var使
}
});
// view-transition-new<pt-name-selector>css var使v-bind

View File

@ -345,7 +345,7 @@ $cellHeight: 28px;
border: solid 0.5px transparent;
&.selected {
border: solid 0.5px var(--MI_THEME-accentLighten);
border: solid 0.5px hsl(from var(--MI_THEME-accent) h s calc(l + 10));
}
&.ranged {

View File

@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
import type { GridColumn } from '@/components/grid/column.js';
import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
import type { MenuItem } from '@/types/menu.js';
import { GridEventEmitter } from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js';
import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
import { handleKeyEvent } from '@/utility/key-event.js';
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
import type { GridColumn } from '@/components/grid/column.js';
import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
import type { MenuItem } from '@/types/menu.js';
type RowHolder = {
row: GridRow,
cells: GridCell[],
@ -130,7 +129,7 @@ const bus = new GridEventEmitter();
*
* @see {@link onResize}
*/
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
/**

View File

@ -15,4 +15,5 @@ export const DI = {
currentStickyTop: Symbol() as InjectionKey<Ref<number>>,
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
inModal: Symbol() as InjectionKey<boolean>,
};

View File

@ -145,13 +145,6 @@ export const navbarItemDef = reactive({
miLocalStorage.setItem('ui', 'deck');
unisonReload();
},
}, {
text: i18n.ts.classic,
active: ui === 'classic',
action: () => {
miLocalStorage.setItem('ui', 'classic');
unisonReload();
},
}], ev.currentTarget ?? ev.target);
},
},

View File

@ -13,6 +13,8 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
import { i18n } from '@/i18n.js';
@ -23,8 +25,6 @@ import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { pleaseLogin } from '@/utility/please-login.js';
import { showMovedDialog } from '@/utility/show-moved-dialog.js';

View File

@ -296,7 +296,7 @@ onMounted(async () => {
left: 0;
padding: 12px;
border-top: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-acrylicBg);
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}

View File

@ -18,11 +18,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</option>
</MkSelect>
<MkSwitch v-model="keepOriginalUploading">
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch>
<MkSwitch v-model="directoryToCategory">
<template #label>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}</template>
<template #caption>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}</template>
@ -245,7 +240,6 @@ function setupGrid(): GridSetting {
const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<GridItem[]>([]);
const selectedFolderId = ref(prefer.s.uploadFolder);
const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
const directoryToCategory = ref<boolean>(false);
const registerButtonDisabled = ref<boolean>(false);
const requestLogs = ref<RequestLogItem[]>([]);
@ -338,7 +332,7 @@ async function onDrop(ev: DragEvent) {
it.file,
selectedFolderId.value,
it.file.name.replace(/\.[^.]+$/, ''),
keepOriginalUploading.value,
true,
),
}),
),
@ -373,7 +367,7 @@ async function onFileSelectClicked() {
true,
{
uploadFolder: selectedFolderId.value,
keepOriginal: keepOriginalUploading.value,
keepOriginal: true,
//
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
},

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { scroll } from '@@/js/scroll.js';
import { scrollInContainer } from '@@/js/scroll.js';
import MkTimeline from '@/components/MkTimeline.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -49,7 +49,7 @@ function queueUpdated(q) {
}
function top() {
scroll(rootEl.value, { top: 0 });
scrollInContainer(rootEl.value, { top: 0 });
}
async function timetravel() {

Some files were not shown because too many files have changed in this diff Show More