summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-04-03 22:04:11 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-04-03 22:06:37 -0400
commit3eeb53ff63a5e776327a9fd067d0c74b9dc727f4 (patch)
tree9b4ee01e1aaf1ab7b4a24ab897ccf14e1aa19a40
parentrefactor bubble-timeline.ts to match global-timeline.ts and local-timeline.ts (diff)
parentNew Crowdin updates (#15740) (diff)
downloadsharkey-3eeb53ff63a5e776327a9fd067d0c74b9dc727f4.tar.gz
sharkey-3eeb53ff63a5e776327a9fd067d0c74b9dc727f4.tar.bz2
sharkey-3eeb53ff63a5e776327a9fd067d0c74b9dc727f4.zip
Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts: # package.json # packages/backend/src/core/AccountMoveService.ts # packages/frontend/src/components/MkDateSeparatedList.vue # packages/misskey-js/etc/misskey-js.api.md # pnpm-lock.yaml
-rw-r--r--CHANGELOG.md3
-rw-r--r--locales/ar-SA.yml1
-rw-r--r--locales/bn-BD.yml4
-rw-r--r--locales/ca-ES.yml7
-rw-r--r--locales/cs-CZ.yml4
-rw-r--r--locales/de-DE.yml4
-rw-r--r--locales/en-US.yml7
-rw-r--r--locales/es-ES.yml4
-rw-r--r--locales/fr-FR.yml4
-rw-r--r--locales/id-ID.yml4
-rw-r--r--locales/index.d.ts8
-rw-r--r--locales/it-IT.yml16
-rw-r--r--locales/ja-JP.yml2
-rw-r--r--locales/ja-KS.yml4
-rw-r--r--locales/ko-GS.yml1
-rw-r--r--locales/ko-KR.yml4
-rw-r--r--locales/pl-PL.yml4
-rw-r--r--locales/pt-PT.yml4
-rw-r--r--locales/ru-RU.yml4
-rw-r--r--locales/sk-SK.yml4
-rw-r--r--locales/th-TH.yml4
-rw-r--r--locales/uk-UA.yml4
-rw-r--r--locales/uz-UZ.yml2
-rw-r--r--locales/vi-VN.yml4
-rw-r--r--locales/zh-CN.yml6
-rw-r--r--locales/zh-TW.yml9
-rw-r--r--package.json2
-rw-r--r--packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js16
-rw-r--r--packages/backend/src/core/AccountMoveService.ts29
-rw-r--r--packages/backend/src/core/ChatService.ts4
-rw-r--r--packages/backend/src/core/RoleService.ts1
-rw-r--r--packages/backend/src/core/entities/ChatEntityService.ts8
-rw-r--r--packages/backend/src/core/entities/RoleEntityService.ts4
-rw-r--r--packages/backend/src/misc/json-schema.ts4
-rw-r--r--packages/backend/src/models/Role.ts5
-rw-r--r--packages/backend/src/models/json-schema/chat-message.ts112
-rw-r--r--packages/backend/src/models/json-schema/role.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/delete.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/react.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/unreact.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/bubble-timeline.ts1
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts1
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts1
-rw-r--r--packages/frontend-shared/themes/l-botanical.json58
-rw-r--r--packages/frontend-shared/themes/l-coffee.json53
-rw-r--r--packages/frontend/.storybook/preview.ts2
-rw-r--r--packages/frontend/src/components/MkContainer.vue8
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue13
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue5
-rw-r--r--packages/frontend/src/pages/chat/XMessage.vue62
-rw-r--r--packages/frontend/src/pages/chat/home.home.vue4
-rw-r--r--packages/frontend/src/pages/chat/home.invitations.vue9
-rw-r--r--packages/frontend/src/pages/chat/home.joiningRooms.vue15
-rw-r--r--packages/frontend/src/pages/chat/home.ownedRooms.vue10
-rw-r--r--packages/frontend/src/pages/chat/home.vue2
-rw-r--r--packages/frontend/src/pages/chat/message.vue10
-rw-r--r--packages/frontend/src/pages/chat/room.form.vue21
-rw-r--r--packages/frontend/src/pages/chat/room.info.vue5
-rw-r--r--packages/frontend/src/pages/chat/room.members.vue5
-rw-r--r--packages/frontend/src/pages/chat/room.search.vue3
-rw-r--r--packages/frontend/src/pages/chat/room.vue122
-rw-r--r--packages/frontend/src/use/use-mutation-observer.ts4
-rw-r--r--packages/frontend/src/utility/timeline-date-separate.ts63
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md60
-rw-r--r--packages/misskey-js/package.json2
-rw-r--r--packages/misskey-js/src/autogen/models.ts2
-rw-r--r--packages/misskey-js/src/autogen/types.ts46
-rw-r--r--packages/misskey-js/src/streaming.types.ts51
74 files changed, 622 insertions, 242 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68f3c0a800..899fb7c5e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
- 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です
- 参加中のルームをミュートして通知が来ないように設定可能です
- メッセージにはリアクションも可能です
+- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。
+ - 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
- Enhance: フロントエンドのエラートラッキングができるように
@@ -69,6 +71,7 @@
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
+- Fix: 非ログインでタイムラインのストリームに接続した際、表示にログイン必須のノートが流れる場合がある問題を修正
## 2025.3.1
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index a792a6804d..adc37c2414 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1240,7 +1240,6 @@ _theme:
shadow: "الظل"
navBg: "خلفية الشريط الجانبي"
navFg: "نص الشريط الجانبي"
- navHoverFg: "نص الشريط الجانبي (عند التمرير فوقه)"
link: "رابط"
hashtag: "وسم"
mention: "أشر الى"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index 4a55c91006..442fed6e21 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -998,7 +998,6 @@ _theme:
header: "হেডার"
navBg: "সাইডবারের পটভূমি"
navFg: "সাইডবারের পাঠ্য"
- navHoverFg: "সাইডবারের পাঠ্য (হভার)"
navActive: "সাইডবারের পাঠ্য (অ্যাকটিভ)"
navIndicator: "সাইডবারের ইনডিকেটর"
link: "লিংক"
@@ -1021,11 +1020,8 @@ _theme:
buttonHoverBg: "বাটনের পটভূমি (হভার)"
inputBorder: "ইনপুট ফিল্ডের বর্ডার"
driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
- wallpaperOverlay: "ওয়ালপেপার ওভারলে"
badge: "ব্যাজ"
messageBg: "চ্যাটের পটভূমি"
- accentDarken: "অ্যাকসেন্ট (গাঢ়)"
- accentLighten: "অ্যাকসেন্ট (হাল্কা)"
fgHighlighted: "হাইলাইট করা পাঠ্য"
_sfx:
note: "নোটগুলি"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index b0d32c4fc1..acb79061a6 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "Exclou els bots"
antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR."
notifyAntenna: "Notifica'm les publicacions noves"
withFileAntenna: "Només les publicacions amb fitxers"
+hideNotesInSensitiveChannel: "Amaga les notes a canals sensibles "
enableServiceworker: "Activar les notificacions al navegador"
antennaUsersDescription: "Llistar un nom d'usuari per línia"
caseSensitive: "Sensible a majúscules i minúscules "
@@ -1339,6 +1340,7 @@ compress: "Comprimir "
right: "Dreta"
bottom: "A baix "
top: "A dalt "
+embed: "Incrustar"
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "Mostrar sub botons a la barra de navegació "
ifOn: "Quan s'encén "
ifOff: "Quan s'apaga "
+ enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
_chat:
showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar"
@@ -2122,7 +2125,6 @@ _theme:
header: "Capçalera"
navBg: "Fons de la barra lateral"
navFg: "Text de la barra lateral"
- navHoverFg: "Text barra lateral (en passar per sobre)"
navActive: "Text barra lateral (actiu)"
navIndicator: "Indicador barra lateral"
link: "Enllaç"
@@ -2145,11 +2147,8 @@ _theme:
buttonHoverBg: "Fons botó (en passar-hi per sobre)"
inputBorder: "Contorn del cap d'introducció "
driveFolderBg: "Fons de la carpeta Disc"
- wallpaperOverlay: "Superposició del fons de pantalla "
badge: "Insígnia "
messageBg: "Fons del xat"
- accentDarken: "Accent (fosc)"
- accentLighten: "Accent (clar)"
fgHighlighted: "Text ressaltat"
_sfx:
note: "Notes"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index c5c8c0f860..87f9445409 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1626,7 +1626,6 @@ _theme:
header: "Nadpis"
navBg: "Pozadí postranního panelu"
navFg: "Text na postranním panelu"
- navHoverFg: "Text na postranním panelu (Hover)"
navActive: "Text na postranním panelu (Aktivní)"
navIndicator: "Indikátor na postranním panelu"
link: "Odkaz"
@@ -1649,11 +1648,8 @@ _theme:
buttonHoverBg: "Pozadí tlačítka (Hover)"
inputBorder: "Ohraničení vstupního pole"
driveFolderBg: "Pozadí složky disku"
- wallpaperOverlay: "Překrytí tapety"
badge: "Odznak"
messageBg: "Pozadí chatu"
- accentDarken: "Akcent (Ztmavený)"
- accentLighten: "Akcent (Zesvětlený)"
fgHighlighted: "Zvýrazněný text"
_sfx:
note: "Poznámky"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 8dea7518cb..973a561a86 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -2105,7 +2105,6 @@ _theme:
header: "Kopfzeile"
navBg: "Hintergrund der Seitenleiste"
navFg: "Text der Seitenleiste"
- navHoverFg: "Text der Seitenleiste (Mouseover)"
navActive: "Text der Seitenleiste (Aktiv)"
navIndicator: "Indikator der Seitenleiste"
link: "Link"
@@ -2128,11 +2127,8 @@ _theme:
buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)"
inputBorder: "Rahmen von Eingabefeldern"
driveFolderBg: "Hintergrund von Drive-Ordnern"
- wallpaperOverlay: "Hintergrundbild-Overlay"
badge: "Wappen"
messageBg: "Hintergrund von Chats"
- accentDarken: "Akzent (Verdunkelt)"
- accentLighten: "Akzent (Erhellt)"
fgHighlighted: "Hervorgehobener Text"
_sfx:
note: "Notizen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index b667891938..cc0c219a60 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "Exclude bot accounts"
antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
notifyAntenna: "Notify about new notes"
withFileAntenna: "Only notes with files"
+hideNotesInSensitiveChannel: "Hide notes from sensitive channels"
enableServiceworker: "Enable Push-Notifications for your Browser"
antennaUsersDescription: "List one username per line"
caseSensitive: "Case sensitive"
@@ -1339,6 +1340,7 @@ compress: "Compress"
right: "Right"
bottom: "Bottom"
top: "Top"
+embed: "Embed"
_chat:
noMessagesYet: "No messages yet"
newMessage: "New message"
@@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "Show sub-buttons on the navigation bar"
ifOn: "When turned on"
ifOff: "When turned off"
+ enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
_chat:
showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send"
@@ -2122,7 +2125,6 @@ _theme:
header: "Header"
navBg: "Sidebar background"
navFg: "Sidebar text"
- navHoverFg: "Sidebar text (Hover)"
navActive: "Sidebar text (Active)"
navIndicator: "Sidebar indicator"
link: "Link"
@@ -2145,11 +2147,8 @@ _theme:
buttonHoverBg: "Button background (Hover)"
inputBorder: "Input field border"
driveFolderBg: "Drive folder background"
- wallpaperOverlay: "Wallpaper overlay"
badge: "Badge"
messageBg: "Chat background"
- accentDarken: "Accent (Darkened)"
- accentLighten: "Accent (Lightened)"
fgHighlighted: "Highlighted Text"
_sfx:
note: "New note"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index aeb9f33cec..f60e9beabe 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1965,7 +1965,6 @@ _theme:
header: "Cabezal"
navBg: "Fondo de la barra lateral"
navFg: "Texto de la barra lateral"
- navHoverFg: "Texto de la barra lateral (hover)"
navActive: "Texto de la barra lateral (activo)"
navIndicator: "Indicador de la barra lateral"
link: "Vínculo"
@@ -1988,11 +1987,8 @@ _theme:
buttonHoverBg: "Fondo de botón (hover)"
inputBorder: "Borde de los campos de entrada"
driveFolderBg: "Fondo de capeta del drive"
- wallpaperOverlay: "Transparencia del fondo de pantalla"
badge: "Medalla"
messageBg: "Fondo de chat"
- accentDarken: "Acento (oscuro)"
- accentLighten: "Acento (claro)"
fgHighlighted: "Texto resaltado"
_sfx:
note: "Notas"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index aed6b5c570..3a6f520ae6 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1816,7 +1816,6 @@ _theme:
header: "Entête"
navBg: "Fond de la barre latérale"
navFg: "Texte de la barre latérale"
- navHoverFg: "Texte de la barre latérale (survolé)"
navActive: "Texte de la barre latérale (actif)"
navIndicator: "Indicateur de barre latérale"
link: "Lien"
@@ -1839,11 +1838,8 @@ _theme:
buttonHoverBg: "Arrière-plan du bouton (survolé)"
inputBorder: "Cadre de la zone de texte"
driveFolderBg: "Arrière-plan du dossier de disque"
- wallpaperOverlay: "Superposition de fond d'écran"
badge: "Badge"
messageBg: "Arrière plan de la discussion"
- accentDarken: "Plus sombre"
- accentLighten: "Plus clair"
fgHighlighted: "Texte mis en évidence"
_sfx:
note: "Nouvelle note"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 944d416ac1..22ccbf153a 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -1931,7 +1931,6 @@ _theme:
header: "Header"
navBg: "Latar belakang bilah samping"
navFg: "Teks bilah samping"
- navHoverFg: "Teks bilah samping (Mengambang)"
navActive: "Teks bilah samping (Aktif)"
navIndicator: "Indikator bilah samping"
link: "Tautan"
@@ -1954,11 +1953,8 @@ _theme:
buttonHoverBg: "Latar belakang tombol (Mengambang)"
inputBorder: "Batas bidang masukan"
driveFolderBg: "Latar belakang folder drive"
- wallpaperOverlay: "Lapisan wallpaper"
badge: "Lencana"
messageBg: "Latar belakang obrolan"
- accentDarken: "Aksen (Gelap)"
- accentLighten: "Aksen (Terang)"
fgHighlighted: "Teks yang disorot"
_sfx:
note: "Catatan"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 59a1e6dccc..fa340119d3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -7386,6 +7386,14 @@ export interface Locale extends ILocale {
*/
"descriptionOfDisplayOrder": string;
/**
+ * アサイン状態を移行先アカウントにも引き継ぐ
+ */
+ "preserveAssignmentOnMoveAccount": string;
+ /**
+ * オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。
+ */
+ "preserveAssignmentOnMoveAccount_description": string;
+ /**
* モデレーターのメンバー編集を許可
*/
"canEditMembersByModerator": string;
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 3ec8414ded..403f9c7718 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "Escludere i Bot"
antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)."
notifyAntenna: "Invia notifiche delle nuove note"
withFileAntenna: "Solo note con file in allegato"
+hideNotesInSensitiveChannel: "Nascondere le Note dai canali espliciti"
enableServiceworker: "Abilita ServiceWorker"
antennaUsersDescription: "Elenca un nome utente per riga"
caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole"
@@ -1336,6 +1337,10 @@ 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"
+right: "Destra"
+bottom: "Sotto"
+top: "Sopra"
+embed: "Incorporare"
_chat:
noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio"
@@ -1406,9 +1411,11 @@ _settings:
timelineAndNote: "Note e Timeline"
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
+ useStickyIcons: "Fissa le icone durante lo scorrimento"
showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione"
ifOn: "Quando attivato"
ifOff: "Quando disattivato"
+ enableSyncThemesBetweenDevices: "Sincronizzare il tema tra i dispositivi"
_chat:
showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce"
@@ -2118,14 +2125,13 @@ _theme:
header: "Intestazione"
navBg: "Sfondo della barra laterale"
navFg: "Testo della barra laterale"
- navHoverFg: "Testo della barra laterale (al passaggio del mouse)"
navActive: "Testo della barra laterale (attivo)"
navIndicator: "Indicatore di barra laterale"
link: "Link"
hashtag: "Hashtag"
mention: "Menzioni"
mentionMe: "Menzioni (di me)"
- renote: "Rinota"
+ renote: "Renota"
modalBg: "Sfondo modale."
divider: "Interruzione di linea"
scrollbarHandle: "Maniglie della barra di scorrimento"
@@ -2141,11 +2147,8 @@ _theme:
buttonHoverBg: "Sfondo del pulsante (sorvolato)"
inputBorder: "Inquadra casella di testo"
driveFolderBg: "Sfondo della cartella di disco"
- wallpaperOverlay: "Sovrapposizione dello sfondo"
badge: "Distintivo"
messageBg: "Sfondo della chat"
- accentDarken: "Temi (scuri)"
- accentLighten: "Temi (luminosi)"
fgHighlighted: "Testo in evidenza."
_sfx:
note: "Nota"
@@ -2592,6 +2595,9 @@ _notification:
_deck:
alwaysShowMainColumn: "Mostra sempre la colonna principale"
columnAlign: "Allineare colonne"
+ columnGap: "Margine tra le colonne"
+ deckMenuPosition: "Posizione del menu Deck"
+ navbarPosition: "Posizione barra di navigazione"
addColumn: "Aggiungi colonna"
newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note"
configureColumn: "Impostazioni colonna"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 947b7b4fa5..323dc3a38a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1907,6 +1907,8 @@ _role:
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
+ preserveAssignmentOnMoveAccount: "アサイン状態を移行先アカウントにも引き継ぐ"
+ preserveAssignmentOnMoveAccount_description: "オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index ec11cd8df5..378eaf2ad5 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -2007,7 +2007,6 @@ _theme:
header: "ヘッダー"
navBg: "サイドバーの背景"
navFg: "サイドバーの文字"
- navHoverFg: "サイドバー文字(ホバー)"
navActive: "サイドバー文字(アクティブ)"
navIndicator: "サイドバーのインジケーター"
link: "リンク"
@@ -2030,11 +2029,8 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
driveFolderBg: "ドライブフォルダーの背景"
- wallpaperOverlay: "壁紙のオーバーレイ"
badge: "バッジ"
messageBg: "チャットの背景"
- accentDarken: "アクセント (暗め)"
- accentLighten: "アクセント (明るめ)"
fgHighlighted: "強調されとる文字"
_sfx:
note: "ノート"
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index 6e0ed8ce81..fb21b47fac 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -747,6 +747,7 @@ _theme:
description: "설멩"
keys:
mention: "멘션"
+ renote: "리노트"
_sfx:
note: "새 노트"
notification: "알림"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 9ee5f19513..f957eb7da3 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -2067,7 +2067,6 @@ _theme:
header: "헤더"
navBg: "사이드바 배경"
navFg: "사이드바 텍스트"
- navHoverFg: "사이드바 텍스트 (호버)"
navActive: "사이드바 텍스트 (활성)"
navIndicator: "사이드바 인디케이터"
link: "링크"
@@ -2090,11 +2089,8 @@ _theme:
buttonHoverBg: "버튼 배경 (호버)"
inputBorder: "입력 필드 테두리"
driveFolderBg: "드라이브 폴더 배경"
- wallpaperOverlay: "배경화면 오버레이"
badge: "배지"
messageBg: "대화 배경"
- accentDarken: "강조 색상 (어두움)"
- accentLighten: "강조 색상 (밝음)"
fgHighlighted: "강조된 텍스트"
_sfx:
note: "새 노트"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index d52473b8c1..46f8939137 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -1212,7 +1212,6 @@ _theme:
header: "Nagłówek"
navBg: "Tło paska bocznego"
navFg: "Tekst paska bocznego"
- navHoverFg: "Tekst paska bocznego (zbliżenie)"
navActive: "Tekst paska bocznego (aktywny)"
navIndicator: "Wskaźnik paska bocznego"
link: "Odnośnik"
@@ -1235,11 +1234,8 @@ _theme:
buttonHoverBg: "Tło przycisku (po najechaniu)"
inputBorder: "Obramowanie pola wejścia"
driveFolderBg: "Tło folderu na dysku"
- wallpaperOverlay: "Nakładka tapety"
badge: "Odznaka"
messageBg: "Tło czatu"
- accentDarken: "Akcent (ciemniejszy)"
- accentLighten: "Akcent (jaśniejszy)"
fgHighlighted: "Wyróżniony tekst"
_sfx:
note: "Wpisy"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index 1660969e15..e899685d9f 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -1997,7 +1997,6 @@ _theme:
header: "Cabeçalho"
navBg: "Plano de fundo da barra lateral"
navFg: "Texto da barra lateral"
- navHoverFg: "Texto da coluna lateral (Selecionado)"
navActive: "Texto da coluna lateral (Ativa)"
navIndicator: "Indicador da coluna lateral"
link: "Link"
@@ -2020,11 +2019,8 @@ _theme:
buttonHoverBg: "Plano de fundo de botão (Selecionado)"
inputBorder: "Borda de campo digitável"
driveFolderBg: "Plano de fundo da pasta no Drive"
- wallpaperOverlay: "Sobreposição do papel de parede."
badge: "Emblema"
messageBg: "Plano de fundo do chat"
- accentDarken: "Cor de destaque (Escurecida)"
- accentLighten: "Cor de destaque (Esclarecida)"
fgHighlighted: "Texto Destacado"
_sfx:
note: "Posts"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 496bd147ae..e81af534e7 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1689,7 +1689,6 @@ _theme:
header: "Заголовок"
navBg: "Фон боковой панели"
navFg: "Текст на боковой панели"
- navHoverFg: "Текст на боковой панели (под указателем)"
navActive: "Текст на боковой панели (активирован)"
navIndicator: "Индикатор на боковой панели"
link: "Ссылка"
@@ -1712,11 +1711,8 @@ _theme:
buttonHoverBg: "Текст кнопки"
inputBorder: "Рамка поля ввода"
driveFolderBg: "Фон папки «Диска»"
- wallpaperOverlay: "Слой обоев"
badge: "Значок"
messageBg: "Фон беседы"
- accentDarken: "Фон (затемнённый)"
- accentLighten: "Фон (осветлённый)"
fgHighlighted: "Подсвеченный текст"
_sfx:
note: "Заметки"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 7f7827202f..1638fd293e 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1089,7 +1089,6 @@ _theme:
header: "Hlavička"
navBg: "Pozadie bočného panela"
navFg: "Text bočného panela"
- navHoverFg: "Text bočného panela (pod kurzorom)"
navActive: "Text bočného panela (aktívny)"
navIndicator: "Indikátor bočného panela"
link: "Odkaz"
@@ -1112,11 +1111,8 @@ _theme:
buttonHoverBg: "Pozadie tlačidla (pod kurzorom)"
inputBorder: "Okraj vstupného poľa"
driveFolderBg: "Pozadie priečinu disku"
- wallpaperOverlay: "Vrstvenie pozadia"
badge: "Odznak"
messageBg: "Pozadie chatu"
- accentDarken: "Akcent (stmavené)"
- accentLighten: "Akcent (zosvetlené)"
fgHighlighted: "Zvýraznený text"
_sfx:
note: "Poznámky"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index e93d65b5c6..06f68c85fe 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -1974,7 +1974,6 @@ _theme:
header: "ส่วนหัว"
navBg: "พื้นหลังแถบด้านข้าง"
navFg: "ข้อความแถบด้านข้าง"
- navHoverFg: "ข้อความแถบด้านข้าง (โฮเวอร์)"
navActive: "ข้อความแถบด้านข้าง (ใช้งานอยู่)"
navIndicator: "ตัวระบุแถบด้านข้าง"
link: "ลิงก์"
@@ -1997,11 +1996,8 @@ _theme:
buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
- wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
badge: "ตรา"
messageBg: "พื้นหลังแชท"
- accentDarken: "สีหลัก (มืด)"
- accentLighten: "สีหลัก (สว่าง)"
fgHighlighted: "ข้อความที่ไฮไลต์"
_sfx:
note: "โน้ต"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 01849dc484..6a970fadfb 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -1283,7 +1283,6 @@ _theme:
header: "Заголовок"
navBg: "Фон бокової панелі"
navFg: "Текст бокової панелі"
- navHoverFg: "Текст бокової панелі (під курсором)"
navActive: "Текст бокової панелі (активне)"
navIndicator: "Індикатор бокової панелі"
link: "Посилання"
@@ -1306,11 +1305,8 @@ _theme:
buttonHoverBg: "Фон кнопки (при наведенні)"
inputBorder: "Край поля вводу"
driveFolderBg: "Фон папки на диску"
- wallpaperOverlay: "Накладання шпалер"
badge: "Значок"
messageBg: "Фон переписки"
- accentDarken: "Акцент (Затемлений)"
- accentLighten: "Акцент (Освітлений)"
fgHighlighted: "Виділений текст"
_sfx:
note: "Нотатки"
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index daa268f1c4..8289a6d60c 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -907,8 +907,6 @@ _theme:
mention: "Murojat"
renote: "Qayta qayd etish"
divider: "Ajratrmoq"
- accentDarken: "Urg'u (Qoraytirilgan)"
- accentLighten: "Urg'u (Yoritilgan)"
fgHighlighted: "Belgilangan matn"
_sfx:
note: "Qaydlar"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index 3a2df8d83e..16917ebf06 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1530,7 +1530,6 @@ _theme:
header: "Ảnh bìa"
navBg: "Nền thanh bên"
navFg: "Chữ thanh bên"
- navHoverFg: "Chữ thanh bên (Khi chạm)"
navActive: "Chữ thanh bên (Khi chọn)"
navIndicator: "Chỉ báo thanh bên"
link: "Đường dẫn"
@@ -1553,11 +1552,8 @@ _theme:
buttonHoverBg: "Nền nút (Chạm)"
inputBorder: "Đường viền khung soạn thảo"
driveFolderBg: "Nền thư mục Ổ đĩa"
- wallpaperOverlay: "Lớp phủ hình nền"
badge: "Huy hiệu"
messageBg: "Nền chat"
- accentDarken: "Màu phụ (Tối)"
- accentLighten: "Màu phụ (Sáng)"
fgHighlighted: "Chữ nổi bật"
_sfx:
note: "Tút"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 8371473c94..67f2692f03 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "排除机器人账户"
antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
notifyAntenna: "开启通知"
withFileAntenna: "仅带有附件的帖子"
+hideNotesInSensitiveChannel: "隐藏敏感频道内的帖子"
enableServiceworker: "启用 ServiceWorker"
antennaUsersDescription: "指定用户名,一行一个"
caseSensitive: "区分大小写"
@@ -1339,6 +1340,7 @@ compress: "压缩"
right: "右"
bottom: "下"
top: "上"
+embed: "嵌入"
_chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
@@ -2122,7 +2124,6 @@ _theme:
header: "顶栏"
navBg: "侧边栏背景"
navFg: "侧栏文本"
- navHoverFg: "侧栏文本(悬停)"
navActive: "侧栏文本(活动)"
navIndicator: "侧栏标记"
link: "链接"
@@ -2145,11 +2146,8 @@ _theme:
buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框"
driveFolderBg: "网盘的文件夹背景"
- wallpaperOverlay: "壁纸叠加层"
badge: "徽章"
messageBg: "聊天背景"
- accentDarken: "强调色(深)"
- accentLighten: "强调色(浅)"
fgHighlighted: "高亮显示文本"
_sfx:
note: "帖子"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index b04e1daab6..8f707c8824 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "排除機器人帳戶"
antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)"
notifyAntenna: "通知有新貼文"
withFileAntenna: "僅帶有附件的貼文"
+hideNotesInSensitiveChannel: "隱藏敏感頻道的貼文"
enableServiceworker: "啟用瀏覽器的推播通知"
antennaUsersDescription: "填寫使用者名稱,以換行分隔"
caseSensitive: "區分大小寫"
@@ -1339,6 +1340,7 @@ compress: "壓縮"
right: "右"
bottom: "下"
top: "上"
+embed: "嵌入"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "在導覽列顯示輔助按鈕"
ifOn: "開啟時"
ifOff: "關閉時"
+ enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
_chat:
showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息"
@@ -1885,6 +1888,8 @@ _role:
descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。"
displayOrder: "顯示順序"
descriptionOfDisplayOrder: "數字越大,顯示在UI上的越上面。"
+ preserveAssignmentOnMoveAccount: "將指派狀態承接至轉移後的帳戶"
+ preserveAssignmentOnMoveAccount_description: "開啟此選項後,當具備此角色的帳戶被移轉時,該角色也會承接至轉移後的帳戶。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"
@@ -2122,7 +2127,6 @@ _theme:
header: "標題"
navBg: "側邊欄的背景 "
navFg: "側邊欄的文字"
- navHoverFg: "側邊欄文字(懸浮) "
navActive: "側邊欄文字(活動)"
navIndicator: "側邊欄指示符"
link: "連結"
@@ -2145,11 +2149,8 @@ _theme:
buttonHoverBg: "按鈕背景 (漂浮)"
inputBorder: "輸入框邊框"
driveFolderBg: "雲端硬碟文件夾背景"
- wallpaperOverlay: "壁紙覆蓋層"
badge: "徽章"
messageBg: "私訊背景"
- accentDarken: "強調色(黑暗)"
- accentLighten: "強調色(明亮)"
fgHighlighted: "突顯文字"
_sfx:
note: "貼文"
diff --git a/package.json b/package.json
index c5163f8f4e..cc6bea1d11 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharkey",
- "version": "2025.4.0-beta.1",
+ "version": "2025.4.0-rc.0",
"codename": "shonk",
"repository": {
"type": "git",
diff --git a/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js
new file mode 100644
index 0000000000..ff4f7a051b
--- /dev/null
+++ b/packages/backend/migration/1743558299182-RoleCopyOnMoveAccount.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RoleCopyOnMoveAccount1743558299182 {
+ name = 'RoleCopyOnMoveAccount1743558299182'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" ADD "preserveAssignmentOnMoveAccount" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "preserveAssignmentOnMoveAccount"`);
+ }
+}
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 2dbd16b5fe..5128caff60 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -24,6 +24,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
+import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class AccountMoveService {
@@ -64,6 +65,7 @@ export class AccountMoveService {
private relayService: RelayService,
private queueService: QueueService,
private systemAccountService: SystemAccountService,
+ private roleService: RoleService,
) {
}
@@ -123,6 +125,7 @@ export class AccountMoveService {
this.copyBlocking(src, dst),
this.copyMutings(src, dst),
this.deleteScheduledNotes(src),
+ this.copyRoles(src, dst),
this.updateLists(src, dst),
]);
} catch {
@@ -220,6 +223,32 @@ export class AccountMoveService {
});
}
+ @bindThis
+ public async copyRoles(src: ThinUser, dst: ThinUser): Promise<void> {
+ // Insert new roles with the same values except userId
+ // role service may have cache for roles so retrieve roles from service
+ const [oldRoleAssignments, roles] = await Promise.all([
+ this.roleService.getUserAssigns(src.id),
+ this.roleService.getRoles(),
+ ]);
+
+ if (oldRoleAssignments.length === 0) return;
+
+ // No promise all since the only async operation is writing to the database
+ for (const oldRoleAssignment of oldRoleAssignments) {
+ const role = roles.find(x => x.id === oldRoleAssignment.roleId);
+ if (role == null) continue; // Very unlikely however removing role may cause this case
+ if (!role.preserveAssignmentOnMoveAccount) continue;
+
+ try {
+ await this.roleService.assign(dst.id, role.id, oldRoleAssignment.expiresAt);
+ } catch (e) {
+ if (e instanceof RoleService.AlreadyAssignedError) continue;
+ throw e;
+ }
+ }
+ }
+
/**
* Update lists while moving accounts.
* - No removal of the old account from the lists
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts
index 6194f624b1..3984cefc80 100644
--- a/packages/backend/src/core/ChatService.ts
+++ b/packages/backend/src/core/ChatService.ts
@@ -99,7 +99,7 @@ export class ChatService {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
- }): Promise<Packed<'ChatMessageLite'>> {
+ }): Promise<Packed<'ChatMessageLiteFor1on1'>> {
if (fromUser.id === toUser.id) {
throw new Error('yourself');
}
@@ -210,7 +210,7 @@ export class ChatService {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
- }): Promise<Packed<'ChatMessageLite'>> {
+ }): Promise<Packed<'ChatMessageLiteForRoom'>> {
const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({
userId: m.userId,
isMuted: m.isMuted,
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 6e4346e22d..d4b5b07b38 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -639,6 +639,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
isModerator: values.isModerator,
isExplorable: values.isExplorable,
asBadge: values.asBadge,
+ preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts
index 099a9e3ad2..da112d5444 100644
--- a/packages/backend/src/core/entities/ChatEntityService.ts
+++ b/packages/backend/src/core/entities/ChatEntityService.ts
@@ -128,7 +128,7 @@ export class ChatEntityService {
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
};
},
- ): Promise<Packed<'ChatMessageLite'>> {
+ ): Promise<Packed<'ChatMessageLiteFor1on1'>> {
const packedFiles = options?._hint_?.packedFiles;
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
@@ -147,7 +147,7 @@ export class ChatEntityService {
createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text,
fromUserId: message.fromUserId,
- toUserId: message.toUserId,
+ toUserId: message.toUserId!,
fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
reactions,
@@ -177,7 +177,7 @@ export class ChatEntityService {
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
};
},
- ): Promise<Packed<'ChatMessageLite'>> {
+ ): Promise<Packed<'ChatMessageLiteForRoom'>> {
const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers;
@@ -199,7 +199,7 @@ export class ChatEntityService {
text: message.text,
fromUserId: message.fromUserId,
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
- toRoomId: message.toRoomId,
+ toRoomId: message.toRoomId!,
fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
reactions,
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
index 2a7dc37bce..3fa38c9521 100644
--- a/packages/backend/src/core/entities/RoleEntityService.ts
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -13,6 +13,7 @@ import type { MiRole } from '@/models/Role.js';
import { bindThis } from '@/decorators.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
+import { Packed } from '@/misc/json-schema.js';
@Injectable()
export class RoleEntityService {
@@ -31,7 +32,7 @@ export class RoleEntityService {
public async pack(
src: MiRole['id'] | MiRole,
me?: { id: MiUser['id'] } | null | undefined,
- ) {
+ ): Promise<Packed<'Role'>> {
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
@@ -67,6 +68,7 @@ export class RoleEntityService {
isModerator: role.isModerator,
isExplorable: role.isExplorable,
asBadge: role.asBadge,
+ preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder,
policies: policies,
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index bc9308ca9b..27aa3d89de 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -63,7 +63,7 @@ import {
} from '@/models/json-schema/meta.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
-import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
+import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
@@ -126,6 +126,8 @@ export const refs = {
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema,
ChatMessageLite: packedChatMessageLiteSchema,
+ ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema,
+ ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema,
ChatRoom: packedChatRoomSchema,
ChatRoomInvitation: packedChatRoomInvitationSchema,
ChatRoomMembership: packedChatRoomMembershipSchema,
diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts
index a173971b2c..4c7da252bd 100644
--- a/packages/backend/src/models/Role.ts
+++ b/packages/backend/src/models/Role.ts
@@ -251,6 +251,11 @@ export class MiRole {
@Column('boolean', {
default: false,
})
+ public preserveAssignmentOnMoveAccount: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
public canEditMembersByModerator: boolean;
// UIに表示する際の並び順用(大きいほど先頭)
diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts
index 44b7298702..3b5e85ab69 100644
--- a/packages/backend/src/models/json-schema/chat-message.ts
+++ b/packages/backend/src/models/json-schema/chat-message.ts
@@ -72,7 +72,7 @@ export const packedChatMessageSchema = {
},
user: {
type: 'object',
- optional: true, nullable: true,
+ optional: false, nullable: false,
ref: 'UserLite',
},
},
@@ -144,3 +144,113 @@ export const packedChatMessageLiteSchema = {
},
},
} as const;
+
+export const packedChatMessageLiteFor1on1Schema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ toUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ },
+ },
+} as const;
+
+export const packedChatMessageLiteForRoomSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ fromUserId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ fromUser: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ toRoomId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ text: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ fileId: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ file: {
+ type: 'object',
+ optional: true, nullable: true,
+ ref: 'DriveFile',
+ },
+ reactions: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ reaction: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 2537351dc0..285c10c4bd 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -397,6 +397,11 @@ export const packedRoleSchema = {
optional: false, nullable: false,
example: false,
},
+ preserveAssignmentOnMoveAccount: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ example: false,
+ },
canEditMembersByModerator: {
type: 'boolean',
optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
index e0c02f7a5d..f92f7ebaeb 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
@@ -36,6 +36,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' },
+ preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 465ad7aaaf..175adcb63f 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -41,6 +41,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' },
+ preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
@@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isAdministrator: ps.isAdministrator,
isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
+ preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
index 1f334d5750..a988dc60b9 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts
@@ -30,7 +30,7 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
- ref: 'ChatMessageLite',
+ ref: 'ChatMessageLiteForRoom',
},
errors: {
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
index 6b77a026fb..bbaab8a6c3 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts
@@ -30,7 +30,7 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
- ref: 'ChatMessageLite',
+ ref: 'ChatMessageLiteFor1on1',
},
errors: {
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
index 959599ddcf..25fc774d4f 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts
@@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
+ requiredRolePolicy: 'canChat',
kind: 'write:chat',
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
index 561e36ed19..0145e380be 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts
@@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
+ requiredRolePolicy: 'canChat',
kind: 'write:chat',
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
index 7aef35db04..b6d3356196 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts
@@ -23,7 +23,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
- ref: 'ChatMessageLite',
+ ref: 'ChatMessageLiteForRoom',
},
},
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
index 4eb25259fb..b97bad8a9c 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts
@@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
+ requiredRolePolicy: 'canChat',
kind: 'write:chat',
diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
index 9d308d79b0..a35f121bb1 100644
--- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts
@@ -24,7 +24,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
- ref: 'ChatMessageLite',
+ ref: 'ChatMessageLiteFor1on1',
},
},
diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 08a7858e08..d29101cbc5 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -57,6 +57,7 @@ class BubbleTimelineChannel extends Channel {
if (note.channelId != null) return;
if (note.user.host == null) return;
if (!this.instance.bubbleInstances.includes(note.user.host)) return;
+ if (note.user.requireSigninToViewContents && this.user == null) return;
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 1b4dca3089..c90e99d930 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -53,6 +53,7 @@ class GlobalTimelineChannel extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
+ if (note.user.requireSigninToViewContents && this.user == null) return;
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 2c31aab66d..630bcce8cf 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -56,6 +56,7 @@ class LocalTimelineChannel extends Channel {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
+ if (note.user.requireSigninToViewContents && this.user == null) return;
// 関係ない返信は除外
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {
diff --git a/packages/frontend-shared/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5
index 17e9ca246f..4ad539e8a0 100644
--- a/packages/frontend-shared/themes/l-botanical.json5
+++ b/packages/frontend-shared/themes/l-botanical.json5
@@ -13,18 +13,18 @@
fgHighlighted: '#6bc9a0',
fgOnWhite: '@accent',
divider: '#cfcfcf',
- panel: '@X14',
+ panel: '#ebe7e5',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
header: ':alpha<0.7<@panel',
- navBg: '@X14',
+ navBg: '#ebe7e5',
renote: '#229e92',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
buttonGradateB: ':hue<-70<@accent',
- success: '#86b300',
- X14: '#ebe7e5'
+ success: '@accent',
+ error: '#da5635',
},
}
diff --git a/packages/frontend-shared/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5
index b64cc73583..df3a12a37b 100644
--- a/packages/frontend-shared/themes/l-coffee.json5
+++ b/packages/frontend-shared/themes/l-coffee.json5
@@ -18,5 +18,8 @@
mention: '@accent',
mentionMe: 'rgb(170, 149, 98)',
hashtag: '@accent',
+ error: '#db9184',
+ warn: '#dbc184',
+ success: '#a3c975',
},
}
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 269bc4fb9a..fb855c1410 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -83,7 +83,7 @@ queueMicrotask(() => {
widgets(app);
misskeyOS = os;
if (isChromatic()) {
- prefer.set('animation', false);
+ prefer.commit('animation', false);
}
});
});
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 5b79ac6699..3362139c3d 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -216,6 +216,14 @@ onUnmounted(() => {
.content {
--MI-stickyTop: 0px;
+ /*
+ 理屈は知らないけど、ここでbackgroundを設定しておかないと
+ スクロールコンテナーが少なくともChromeにおいて
+ main thread scrolling になってしまい、パフォーマンスが(多分)落ちる。
+ backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない
+ */
+ background: var(--MI_THEME-panel);
+
&.omitted {
position: relative;
max-height: var(--maxHeight);
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 857b7e3f92..63e6b74154 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -3,16 +3,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
+<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) -->
+
<script lang="ts">
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
import type { PropType } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js';
import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
-import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js';
+import { getDateText } from '@/utility/timeline-date-separate.js';
import { $i } from '@/i.js';
export default defineComponent({
@@ -46,15 +48,6 @@ export default defineComponent({
setup(props, { slots, expose }) {
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
- function getDateText(dateInstance: Date) {
- const date = dateInstance.getDate();
- const month = dateInstance.getMonth() + 1;
- return i18n.tsx.monthAndDay({
- month: month.toString(),
- day: date.toString(),
- });
- }
-
if (props.items.length === 0) return;
const renderChildrenImpl = (shouldHideAds: boolean) => props.items.map((item, i) => {
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index f6c24c13da..d04996dac0 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -52,6 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
+ <MkSwitch v-model="role.preserveAssignmentOnMoveAccount" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.preserveAssignmentOnMoveAccount }}</template>
+ <template #caption>{{ i18n.ts._role.preserveAssignmentOnMoveAccount_description }}</template>
+ </MkSwitch>
+
<MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index 8f7e9dd986..7524283641 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -5,33 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { [$style.isMe]: isMe }]">
- <MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/>
+ <MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/>
<div :class="$style.body" @contextmenu.stop="onContextmenu">
- <div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName']" :user="message.fromUser"/></div>
+ <div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div>
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
- <div v-if="!message.isDeleted" :class="$style.content">
- <Mfm
- v-if="message.text"
- ref="text"
- class="_selectable"
- :text="message.text"
- :i="$i"
- :nyaize="'respect'"
- :enableEmojiMenu="true"
- :enableEmojiMenuReaction="true"
- />
- <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
- </div>
- <div v-else :class="$style.content">
- <p>{{ i18n.ts.deleted }}</p>
- </div>
+ <Mfm
+ v-if="message.text"
+ ref="text"
+ class="_selectable"
+ :text="message.text"
+ :i="$i"
+ :nyaize="'respect'"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ />
+ <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
</MkFukidashi>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
<div :class="$style.footer">
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
<MkTime :class="$style.time" :time="message.createdAt"/>
- <MkA v-if="isSearchResult && message.toRoomId" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA>
- <MkA v-if="isSearchResult && message.toUserId && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
+ <MkA v-if="isSearchResult && 'toRoom' in message && message.toRoom != null" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA>
+ <MkA v-if="isSearchResult && 'toUser' in message && message.toUser != null && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
</div>
<TransitionGroup
:enterActiveClass="prefer.s.animation ? $style.transition_reaction_enterActive : ''"
@@ -62,6 +57,7 @@ import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import { isLink } from '@@/js/is-link.js';
import type { MenuItem } from '@/types/menu.js';
+import type { NormalizedChatMessage } from './room.vue';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { ensureSignin } from '@/i.js';
@@ -76,11 +72,12 @@ import * as sound from '@/utility/sound.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
const $i = ensureSignin();
const props = defineProps<{
- message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage;
+ message: NormalizedChatMessage | Misskey.entities.ChatMessage;
isSearchResult?: boolean;
}>();
@@ -88,6 +85,8 @@ const isMe = computed(() => props.message.fromUserId === $i.id);
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
provide(DI.mfmEmojiReactCallback, (reaction) => {
+ if (!$i.policies.canChat) return;
+
sound.playMisskeySfx('reaction');
misskeyApi('chat/messages/react', {
messageId: props.message.id,
@@ -96,7 +95,12 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
});
function react(ev: MouseEvent) {
- reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => {
+ if (!$i.policies.canChat) return;
+
+ const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
+ if (!targetEl) return;
+
+ reactionPicker.show(targetEl, null, async (reaction) => {
sound.playMisskeySfx('reaction');
misskeyApi('chat/messages/react', {
messageId: props.message.id,
@@ -106,6 +110,8 @@ function react(ev: MouseEvent) {
}
function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
+ if (!$i.policies.canChat) return;
+
if (record.user.id === $i.id) {
misskeyApi('chat/messages/unreact', {
messageId: props.message.id,
@@ -132,7 +138,7 @@ function onContextmenu(ev: MouseEvent) {
function showMenu(ev: MouseEvent, contextmenu = false) {
const menu: MenuItem[] = [];
- if (!isMe.value) {
+ if (!isMe.value && $i.policies.canChat) {
menu.push({
text: i18n.ts.reaction,
icon: 'ti ti-mood-plus',
@@ -150,7 +156,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
text: i18n.ts.copyContent,
icon: 'ti ti-copy',
action: () => {
- copyToClipboard(props.message.text);
+ copyToClipboard(props.message.text ?? '');
},
});
@@ -158,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
type: 'divider',
});
- if (isMe.value) {
+ if (isMe.value && $i.policies.canChat) {
menu.push({
text: i18n.ts.delete,
icon: 'ti ti-trash',
@@ -169,14 +175,16 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
});
},
});
- } else {
+ }
+
+ if (!isMe.value && props.message.fromUser != null) {
menu.push({
text: i18n.ts.reportAbuse,
icon: 'ti ti-exclamation-circle',
action: () => {
const localUrl = `${url}/chat/messages/${props.message.id}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
- user: props.message.fromUser,
+ user: props.message.fromUser!,
initialComment: `${localUrl}\n-----\n`,
}, {
closed: () => dispose(),
diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue
index 105f5f7989..17f0e0fbcd 100644
--- a/packages/frontend/src/pages/chat/home.home.vue
+++ b/packages/frontend/src/pages/chat/home.home.vue
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
+import { onActivated, onDeactivated, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import XMessage from './XMessage.vue';
@@ -163,7 +163,7 @@ async function fetchHistory() {
.map(m => ({
id: m.id,
message: m,
- other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
+ other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
isMe: m.fromUserId === $i.id,
}));
diff --git a/packages/frontend/src/pages/chat/home.invitations.vue b/packages/frontend/src/pages/chat/home.invitations.vue
index 4c3c0b282e..82b22ea9dd 100644
--- a/packages/frontend/src/pages/chat/home.invitations.vue
+++ b/packages/frontend/src/pages/chat/home.invitations.vue
@@ -35,18 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import { ensureSignin } from '@/i.js';
import { useRouter } from '@/router.js';
-import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue';
-const $i = ensureSignin();
-
const router = useRouter();
const fetching = ref(true);
@@ -55,8 +51,7 @@ const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]);
async function fetchInvitations() {
fetching.value = true;
- const res = await misskeyApi('chat/rooms/invitations/inbox', {
- });
+ const res = await misskeyApi('chat/rooms/invitations/inbox');
invitations.value = res;
diff --git a/packages/frontend/src/pages/chat/home.joiningRooms.vue b/packages/frontend/src/pages/chat/home.joiningRooms.vue
index 63e4d2adf8..f9fd6bfd55 100644
--- a/packages/frontend/src/pages/chat/home.joiningRooms.vue
+++ b/packages/frontend/src/pages/chat/home.joiningRooms.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div v-if="memberships.length > 0" class="_gaps_s">
- <XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room"/>
+ <XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room!"/>
</div>
<div v-if="!fetching && memberships.length == 0" class="_fullinfo">
<div>{{ i18n.ts._chat.noRooms }}</div>
@@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XRoom from './XRoom.vue';
-import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import { ensureSignin } from '@/i.js';
-import { useRouter } from '@/router.js';
-import * as os from '@/os.js';
-
-const $i = ensureSignin();
-
-const router = useRouter();
const fetching = ref(true);
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
@@ -36,8 +28,7 @@ const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
async function fetchRooms() {
fetching.value = true;
- const res = await misskeyApi('chat/rooms/joining', {
- });
+ const res = await misskeyApi('chat/rooms/joining');
memberships.value = res;
diff --git a/packages/frontend/src/pages/chat/home.ownedRooms.vue b/packages/frontend/src/pages/chat/home.ownedRooms.vue
index b0449fb373..ce7da15563 100644
--- a/packages/frontend/src/pages/chat/home.ownedRooms.vue
+++ b/packages/frontend/src/pages/chat/home.ownedRooms.vue
@@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XRoom from './XRoom.vue';
-import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import { ensureSignin } from '@/i.js';
-import { useRouter } from '@/router.js';
-import * as os from '@/os.js';
-
-const $i = ensureSignin();
-
-const router = useRouter();
const fetching = ref(true);
const rooms = ref<Misskey.entities.ChatRoom[]>([]);
diff --git a/packages/frontend/src/pages/chat/home.vue b/packages/frontend/src/pages/chat/home.vue
index 9bb7235a64..e29ab28f2d 100644
--- a/packages/frontend/src/pages/chat/home.vue
+++ b/packages/frontend/src/pages/chat/home.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { computed, ref } from 'vue';
import XHome from './home.home.vue';
import XInvitations from './home.invitations.vue';
import XJoiningRooms from './home.joiningRooms.vue';
diff --git a/packages/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue
index 975d1a2be9..3ac90a93fd 100644
--- a/packages/frontend/src/pages/chat/message.vue
+++ b/packages/frontend/src/pages/chat/message.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader>
<MkSpacer :contentMax="700">
- <div v-if="initializing">
+ <div v-if="initializing || message == null">
<MkLoading/>
</div>
<div v-else>
@@ -17,23 +17,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
+import { ref, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XMessage from './XMessage.vue';
-import * as os from '@/os.js';
-import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
-import { ensureSignin } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
-import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
messageId?: string;
}>();
const initializing = ref(true);
-const message = ref<Misskey.entities.ChatMessage>();
+const message = ref<Misskey.entities.ChatMessage | null>();
async function initialize() {
initializing.value = true;
diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue
index 27ddbeb565..9389b16ce7 100644
--- a/packages/frontend/src/pages/chat/room.form.vue
+++ b/packages/frontend/src/pages/chat/room.form.vue
@@ -34,14 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly } from 'vue';
+import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly, onBeforeUnmount } from 'vue';
import * as Misskey from 'misskey-js';
//import insertTextAtCursor from 'insert-text-at-cursor';
-import { throttle } from 'throttle-debounce';
import { formatTimeString } from '@/utility/format-time-string.js';
import { selectFile } from '@/utility/select-file.js';
import * as os from '@/os.js';
-import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { uploadFile } from '@/utility/upload.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -62,6 +60,7 @@ const text = ref<string>('');
const file = ref<Misskey.entities.DriveFile | null>(null);
const sending = ref(false);
const textareaReadOnly = ref(false);
+let autocompleteInstance: Autocomplete | null = null;
const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null);
@@ -171,7 +170,9 @@ function chooseFile(ev: MouseEvent) {
}
function onChangeFile() {
- if (fileEl.value.files![0]) upload(fileEl.value.files[0]);
+ if (fileEl.value == null || fileEl.value.files == null) return;
+
+ if (fileEl.value.files[0]) upload(fileEl.value.files[0]);
}
function upload(fileToUpload: File, name?: string) {
@@ -270,8 +271,9 @@ async function insertEmoji(ev: MouseEvent) {
}
onMounted(() => {
- // TODO: detach when unmount
- new Autocomplete(textareaEl.value, text);
+ if (textareaEl.value != null) {
+ autocompleteInstance = new Autocomplete(textareaEl.value, text);
+ }
// 書きかけの投稿を復元
const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()];
@@ -280,6 +282,13 @@ onMounted(() => {
file.value = draft.data.file;
}
});
+
+onBeforeUnmount(() => {
+ if (autocompleteInstance) {
+ autocompleteInstance.detach();
+ autocompleteInstance = null;
+ }
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/chat/room.info.vue b/packages/frontend/src/pages/chat/room.info.vue
index 8439e5f772..2f091388a0 100644
--- a/packages/frontend/src/pages/chat/room.info.vue
+++ b/packages/frontend/src/pages/chat/room.info.vue
@@ -26,11 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
import MkInput from '@/components/MkInput.vue';
@@ -73,7 +72,7 @@ async function del() {
router.push('/chat');
}
-const isMuted = ref(props.room.isMuted);
+const isMuted = ref(props.room.isMuted ?? false);
watch(isMuted, async () => {
await os.apiWithDialog('chat/rooms/mute', {
diff --git a/packages/frontend/src/pages/chat/room.members.vue b/packages/frontend/src/pages/chat/room.members.vue
index bff038570f..5a574068cb 100644
--- a/packages/frontend/src/pages/chat/room.members.vue
+++ b/packages/frontend/src/pages/chat/room.members.vue
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr v-if="memberships.length > 0">
<div v-for="membership in memberships" :key="membership.id" :class="$style.membership">
- <MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`">
- <MkUserCardMini :user="membership.user"/>
+ <MkA :class="$style.membershipBody" :to="`${userPage(membership.user!)}`">
+ <MkUserCardMini :user="membership.user!"/>
</MkA>
</div>
@@ -39,7 +39,6 @@ import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import * as os from '@/os.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user.js';
import { ensureSignin } from '@/i.js';
diff --git a/packages/frontend/src/pages/chat/room.search.vue b/packages/frontend/src/pages/chat/room.search.vue
index e382834578..20b6e22a46 100644
--- a/packages/frontend/src/pages/chat/room.search.vue
+++ b/packages/frontend/src/pages/chat/room.search.vue
@@ -33,14 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import XMessage from './XMessage.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import { misskeyApi } from '@/utility/misskey-api.js';
-import * as os from '@/os.js';
import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index ce823968f7..9942dbeee9 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -38,7 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
tag="div" class="_gaps"
>
- <XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message"/>
+ <template v-for="item in timeline.toReversed()" :key="item.id">
+ <XMessage v-if="item.type === 'item'" :message="item.data"/>
+ <div v-else-if="item.type === 'date'" :class="$style.dateDivider">
+ <span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
+ <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
+ <span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
+ </div>
+ </template>
</TransitionGroup>
</div>
@@ -79,15 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
+import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
import * as Misskey from 'misskey-js';
-import { getScrollContainer, isTailVisible } from '@@/js/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
import XMessage from './XMessage.vue';
import XForm from './room.form.vue';
import XSearch from './room.search.vue';
import XMembers from './room.members.vue';
import XInfo from './room.info.vue';
import type { MenuItem } from '@/types/menu.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js';
@@ -100,6 +108,7 @@ import MkButton from '@/components/MkButton.vue';
import { useRouter } from '@/router.js';
import { useMutationObserver } from '@/use/use-mutation-observer.js';
import MkInfo from '@/components/MkInfo.vue';
+import { makeDateSeparatedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
const $i = ensureSignin();
const router = useRouter();
@@ -109,15 +118,23 @@ const props = defineProps<{
roomId?: string;
}>();
+export type NormalizedChatMessage = Omit<Misskey.entities.ChatMessageLite, 'fromUser' | 'reactions'> & {
+ fromUser: Misskey.entities.UserLite;
+ reactions: (Misskey.entities.ChatMessageLite['reactions'][number] & {
+ user: Misskey.entities.UserLite;
+ })[];
+};
+
const initializing = ref(true);
const moreFetching = ref(false);
-const messages = ref<Misskey.entities.ChatMessage[]>([]);
+const messages = ref<NormalizedChatMessage[]>([]);
const canFetchMore = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
const room = ref<Misskey.entities.ChatRoom | null>(null);
-const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null);
+const connection = ref<Misskey.IChannelConnection<Misskey.Channels['chatUser']> | Misskey.IChannelConnection<Misskey.Channels['chatRoom']> | null>(null);
const showIndicator = ref(false);
const timelineEl = useTemplateRef('timelineEl');
+const timeline = makeDateSeparatedTimelineComputedRef(messages);
const SCROLL_HEAD_THRESHOLD = 200;
@@ -138,18 +155,14 @@ useMutationObserver(timelineEl, {
}
});
-function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) {
- const reactions = [...message.reactions];
- for (const record of reactions) {
- if (room.value == null && record.user == null) { // 1on1の時はuserは省略される
- record.user = message.fromUserId === $i.id ? user.value : $i;
- }
- }
-
+function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage): NormalizedChatMessage {
return {
...message,
- fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user),
- reactions,
+ fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user.value!),
+ reactions: message.reactions.map(record => ({
+ ...record,
+ user: record.user ?? (message.fromUserId === $i.id ? user.value! : $i),
+ })),
};
}
@@ -184,8 +197,8 @@ async function initialize() {
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
]);
- room.value = r;
- messages.value = m.map(x => normalizeMessage(x));
+ room.value = r as Misskey.entities.ChatRoomsShowResponse;
+ messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x));
if (messages.value.length === LIMIT) {
canFetchMore.value = true;
@@ -221,11 +234,11 @@ async function fetchMore() {
moreFetching.value = true;
const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', {
- userId: user.value.id,
+ userId: user.value!.id,
limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id,
}) : await misskeyApi('chat/messages/room-timeline', {
- roomId: room.value.id,
+ roomId: room.value!.id,
limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id,
});
@@ -236,7 +249,7 @@ async function fetchMore() {
moreFetching.value = false;
}
-function onMessage(message: Misskey.entities.ChatMessage) {
+function onMessage(message: Misskey.entities.ChatMessageLite) {
sound.playMisskeySfx('chatMessage');
messages.value.unshift(normalizeMessage(message));
@@ -253,34 +266,34 @@ function onMessage(message: Misskey.entities.ChatMessage) {
}
}
-function onDeleted(id) {
+function onDeleted(id: string) {
const index = messages.value.findIndex(m => m.id === id);
if (index !== -1) {
messages.value.splice(index, 1);
}
}
-function onReact(ctx) {
+function onReact(ctx: Parameters<Misskey.Channels['chatUser']['events']['react']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['react']>[0]) {
const message = messages.value.find(m => m.id === ctx.messageId);
if (message) {
if (room.value == null) { // 1on1の時はuserは省略される
message.reactions.push({
reaction: ctx.reaction,
- user: message.fromUserId === $i.id ? user : $i,
+ user: message.fromUserId === $i.id ? user.value! : $i,
});
} else {
message.reactions.push({
reaction: ctx.reaction,
- user: ctx.user,
+ user: ctx.user!,
});
}
}
}
-function onUnreact(ctx) {
+function onUnreact(ctx: Parameters<Misskey.Channels['chatUser']['events']['unreact']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['unreact']>[0]) {
const message = messages.value.find(m => m.id === ctx.messageId);
if (message) {
- const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id);
+ const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user!.id);
if (index !== -1) {
message.reactions.splice(index, 1);
}
@@ -310,14 +323,18 @@ onBeforeUnmount(() => {
});
async function inviteUser() {
+ if (room.value == null) return;
+
const invitee = await os.selectUser({ includeSelf: false, localOnly: true });
os.apiWithDialog('chat/rooms/invitations/create', {
- roomId: room.value?.id,
+ roomId: room.value.id,
userId: invitee.id,
});
}
async function leaveRoom() {
+ if (room.value == null) return;
+
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.areYouSure,
@@ -325,7 +342,7 @@ async function leaveRoom() {
if (canceled) return;
misskeyApi('chat/rooms/leave', {
- roomId: room.value?.id,
+ roomId: room.value.id,
});
router.push('/chat');
}
@@ -384,19 +401,36 @@ const headerTabs = computed(() => room.value ? [{
icon: 'ti ti-search',
}]);
-const headerActions = computed(() => [{
+const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-dots',
+ text: '',
handler: showMenu,
}]);
-definePage(computed(() => !initializing.value ? user.value ? {
- userName: user,
- title: user.value.name ?? user.value.username,
- avatar: user,
-} : {
- title: room.value?.name,
- icon: 'ti ti-users',
-} : null));
+definePage(computed(() => {
+ if (!initializing.value) {
+ if (user.value) {
+ return {
+ userName: user.value,
+ title: user.value.name ?? user.value.username,
+ avatar: user.value,
+ };
+ } else if (room.value) {
+ return {
+ title: room.value.name,
+ icon: 'ti ti-users',
+ };
+ } else {
+ return {
+ title: i18n.ts.chat,
+ };
+ }
+ } else {
+ return {
+ title: i18n.ts.chat,
+ };
+ }
+}));
</script>
<style lang="scss" module>
@@ -464,4 +498,18 @@ definePage(computed(() => !initializing.value ? user.value ? {
transition: opacity 0.5s;
opacity: 0;
}
+
+.dateDivider {
+ display: flex;
+ font-size: 85%;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5em;
+ opacity: 0.75;
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: 999px;
+ width: fit-content;
+ padding: 0.5em 1em;
+ margin: 0 auto;
+}
</style>
diff --git a/packages/frontend/src/use/use-mutation-observer.ts b/packages/frontend/src/use/use-mutation-observer.ts
index b35dbcd7a8..7b774022dc 100644
--- a/packages/frontend/src/use/use-mutation-observer.ts
+++ b/packages/frontend/src/use/use-mutation-observer.ts
@@ -4,9 +4,9 @@
*/
import { onUnmounted, watch } from 'vue';
-import type { Ref, ShallowRef } from 'vue';
+import type { Ref } from 'vue';
-export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
+export function useMutationObserver(targetNodeRef: Ref<HTMLElement | null | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
const observer = new MutationObserver(callback);
watch(targetNodeRef, (targetNode) => {
diff --git a/packages/frontend/src/utility/timeline-date-separate.ts b/packages/frontend/src/utility/timeline-date-separate.ts
new file mode 100644
index 0000000000..e1bc9790b9
--- /dev/null
+++ b/packages/frontend/src/utility/timeline-date-separate.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed } from 'vue';
+import type { Ref } from 'vue';
+
+export function getDateText(dateInstance: Date) {
+ const date = dateInstance.getDate();
+ const month = dateInstance.getMonth() + 1;
+ return `${month.toString()}/${date.toString()}`;
+}
+
+export type DateSeparetedTimelineItem<T> = {
+ id: string;
+ type: 'item';
+ data: T;
+} | {
+ id: string;
+ type: 'date';
+ prev: Date;
+ prevText: string;
+ next: Date;
+ nextText: string;
+};
+
+export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]>) {
+ return computed<DateSeparetedTimelineItem<T>[]>(() => {
+ const tl: DateSeparetedTimelineItem<T>[] = [];
+ for (let i = 0; i < items.value.length; i++) {
+ const item = items.value[i];
+
+ const date = new Date(item.createdAt);
+ const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
+
+ tl.push({
+ id: item.id,
+ type: 'item',
+ data: item,
+ });
+
+ if (
+ i !== items.value.length - 1 &&
+ nextDate != null && (
+ date.getFullYear() !== nextDate.getFullYear() ||
+ date.getMonth() !== nextDate.getMonth() ||
+ date.getDate() !== nextDate.getDate()
+ )
+ ) {
+ tl.push({
+ id: `date-${item.id}`,
+ type: 'date',
+ prev: date,
+ prevText: getDateText(date),
+ next: nextDate,
+ nextText: getDateText(nextDate),
+ });
+ }
+ }
+ return tl;
+ });
+}
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index d94e73a1d6..30c4273dbd 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -853,6 +853,54 @@ export type Channels = {
claimTimeIsUp: null | Record<string, never>;
};
};
+ chatUser: {
+ params: {
+ otherId: string;
+ };
+ events: {
+ message: (payload: ChatMessageLite) => void;
+ deleted: (payload: ChatMessageLite['id']) => void;
+ react: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ unreact: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ };
+ receives: {
+ read: {
+ id: ChatMessageLite['id'];
+ };
+ };
+ };
+ chatRoom: {
+ params: {
+ roomId: string;
+ };
+ events: {
+ message: (payload: ChatMessageLite) => void;
+ deleted: (payload: ChatMessageLite['id']) => void;
+ react: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ unreact: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ };
+ receives: {
+ read: {
+ id: ChatMessageLite['id'];
+ };
+ };
+ };
};
// @public (undocumented)
@@ -1000,6 +1048,12 @@ type ChatMessage = components['schemas']['ChatMessage'];
type ChatMessageLite = components['schemas']['ChatMessageLite'];
// @public (undocumented)
+type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
+
+// @public (undocumented)
+type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
+
+// @public (undocumented)
type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -2152,6 +2206,8 @@ declare namespace entities {
AbuseReportNotificationRecipient,
ChatMessage,
ChatMessageLite,
+ ChatMessageLiteFor1on1,
+ ChatMessageLiteForRoom,
ChatRoom,
ChatRoomInvitation,
ChatRoomMembership
@@ -3853,8 +3909,8 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
-// src/streaming.types.ts:233:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
-// src/streaming.types.ts:243:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:234:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
+// src/streaming.types.ts:244:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index fd641b1e66..da20986288 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.4.0-beta.1",
+ "version": "2025.4.0-rc.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 0ff9749602..15c3ee7e55 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -56,6 +56,8 @@ export type SystemWebhook = components['schemas']['SystemWebhook'];
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
export type ChatMessage = components['schemas']['ChatMessage'];
export type ChatMessageLite = components['schemas']['ChatMessageLite'];
+export type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
+export type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
export type ChatRoom = components['schemas']['ChatRoom'];
export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation'];
export type ChatRoomMembership = components['schemas']['ChatRoomMembership'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 8677f64b71..449d901a24 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5405,6 +5405,8 @@ export type components = {
/** @example false */
asBadge: boolean;
/** @example false */
+ preserveAssignmentOnMoveAccount: boolean;
+ /** @example false */
canEditMembersByModerator: boolean;
policies: {
[key: string]: {
@@ -5693,10 +5695,10 @@ export type components = {
fileId?: string | null;
file?: components['schemas']['DriveFile'] | null;
isRead?: boolean;
- reactions: ({
+ reactions: {
reaction: string;
- user?: components['schemas']['UserLite'] | null;
- })[];
+ user: components['schemas']['UserLite'];
+ }[];
};
ChatMessageLite: {
id: string;
@@ -5714,6 +5716,34 @@ export type components = {
user?: components['schemas']['UserLite'] | null;
})[];
};
+ ChatMessageLiteFor1on1: {
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ fromUserId: string;
+ toUserId: string;
+ text?: string | null;
+ fileId?: string | null;
+ file?: components['schemas']['DriveFile'] | null;
+ reactions: {
+ reaction: string;
+ }[];
+ };
+ ChatMessageLiteForRoom: {
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ fromUserId: string;
+ fromUser: components['schemas']['UserLite'];
+ toRoomId: string;
+ text?: string | null;
+ fileId?: string | null;
+ file?: components['schemas']['DriveFile'] | null;
+ reactions: {
+ reaction: string;
+ user: components['schemas']['UserLite'];
+ }[];
+ };
ChatRoom: {
id: string;
/** Format: date-time */
@@ -9983,6 +10013,7 @@ export type operations = {
/** @default false */
isExplorable?: boolean;
asBadge: boolean;
+ preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator: boolean;
displayOrder: number;
policies: Record<string, never>;
@@ -10258,6 +10289,7 @@ export type operations = {
isAdministrator?: boolean;
isExplorable?: boolean;
asBadge?: boolean;
+ preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator?: boolean;
displayOrder?: number;
policies?: Record<string, never>;
@@ -15108,7 +15140,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['ChatMessageLite'];
+ 'application/json': components['schemas']['ChatMessageLiteForRoom'];
};
};
/** @description Client error */
@@ -15171,7 +15203,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['ChatMessageLite'];
+ 'application/json': components['schemas']['ChatMessageLiteFor1on1'];
};
};
/** @description Client error */
@@ -15346,7 +15378,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['ChatMessageLite'][];
+ 'application/json': components['schemas']['ChatMessageLiteForRoom'][];
};
};
/** @description Client error */
@@ -15574,7 +15606,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['ChatMessageLite'][];
+ 'application/json': components['schemas']['ChatMessageLiteFor1on1'][];
};
};
/** @description Client error */
diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts
index 005c90b702..d13a54b1c2 100644
--- a/packages/misskey-js/src/streaming.types.ts
+++ b/packages/misskey-js/src/streaming.types.ts
@@ -1,6 +1,7 @@
import {
Antenna,
ChatMessage,
+ ChatMessageLite,
DriveFile,
DriveFolder,
Note,
@@ -243,7 +244,55 @@ export type Channels = {
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
claimTimeIsUp: null | Record<string, never>;
}
- }
+ };
+ chatUser: {
+ params: {
+ otherId: string;
+ };
+ events: {
+ message: (payload: ChatMessageLite) => void;
+ deleted: (payload: ChatMessageLite['id']) => void;
+ react: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ unreact: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ };
+ receives: {
+ read: {
+ id: ChatMessageLite['id'];
+ };
+ };
+ };
+ chatRoom: {
+ params: {
+ roomId: string;
+ };
+ events: {
+ message: (payload: ChatMessageLite) => void;
+ deleted: (payload: ChatMessageLite['id']) => void;
+ react: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ unreact: (payload: {
+ reaction: string;
+ user?: UserLite;
+ messageId: ChatMessageLite['id'];
+ }) => void;
+ };
+ receives: {
+ read: {
+ id: ChatMessageLite['id'];
+ };
+ };
+ };
};
export type NoteUpdatedEvent = { id: Note['id'] } & ({