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
This commit is contained in:
Hazelnoot 2025-04-03 22:04:11 -04:00
commit 3eeb53ff63
74 changed files with 622 additions and 242 deletions

View file

@ -11,6 +11,8 @@
- 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です
- 参加中のルームをミュートして通知が来ないように設定可能です - 参加中のルームをミュートして通知が来ないように設定可能です
- メッセージにはリアクションも可能です - メッセージにはリアクションも可能です
- Feat: アカウントのマイグレーション時に古いアカウントからロールをコピーできるようになりました。
- 管理者がロールの設定でマイグレーション時にコピーするかを指定できるようになります。
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です - Misskeyネイティブでダッシュボードを実装予定です
- Enhance: フロントエンドのエラートラッキングができるように - Enhance: フロントエンドのエラートラッキングができるように
@ -69,6 +71,7 @@
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
- Fix: 連合無しモードでも外部から照会可能だった問題を修正 - Fix: 連合無しモードでも外部から照会可能だった問題を修正
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正 - Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
- Fix: 非ログインでタイムラインのストリームに接続した際、表示にログイン必須のノートが流れる場合がある問題を修正
## 2025.3.1 ## 2025.3.1

View file

@ -1240,7 +1240,6 @@ _theme:
shadow: "الظل" shadow: "الظل"
navBg: "خلفية الشريط الجانبي" navBg: "خلفية الشريط الجانبي"
navFg: "نص الشريط الجانبي" navFg: "نص الشريط الجانبي"
navHoverFg: "نص الشريط الجانبي (عند التمرير فوقه)"
link: "رابط" link: "رابط"
hashtag: "وسم" hashtag: "وسم"
mention: "أشر الى" mention: "أشر الى"

View file

@ -998,7 +998,6 @@ _theme:
header: "হেডার" header: "হেডার"
navBg: "সাইডবারের পটভূমি" navBg: "সাইডবারের পটভূমি"
navFg: "সাইডবারের পাঠ্য" navFg: "সাইডবারের পাঠ্য"
navHoverFg: "সাইডবারের পাঠ্য (হভার)"
navActive: "সাইডবারের পাঠ্য (অ্যাকটিভ)" navActive: "সাইডবারের পাঠ্য (অ্যাকটিভ)"
navIndicator: "সাইডবারের ইনডিকেটর" navIndicator: "সাইডবারের ইনডিকেটর"
link: "লিংক" link: "লিংক"
@ -1021,11 +1020,8 @@ _theme:
buttonHoverBg: "বাটনের পটভূমি (হভার)" buttonHoverBg: "বাটনের পটভূমি (হভার)"
inputBorder: "ইনপুট ফিল্ডের বর্ডার" inputBorder: "ইনপুট ফিল্ডের বর্ডার"
driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি" driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
wallpaperOverlay: "ওয়ালপেপার ওভারলে"
badge: "ব্যাজ" badge: "ব্যাজ"
messageBg: "চ্যাটের পটভূমি" messageBg: "চ্যাটের পটভূমি"
accentDarken: "অ্যাকসেন্ট (গাঢ়)"
accentLighten: "অ্যাকসেন্ট (হাল্কা)"
fgHighlighted: "হাইলাইট করা পাঠ্য" fgHighlighted: "হাইলাইট করা পাঠ্য"
_sfx: _sfx:
note: "নোটগুলি" note: "নোটগুলি"

View file

@ -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." antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR."
notifyAntenna: "Notifica'm les publicacions noves" notifyAntenna: "Notifica'm les publicacions noves"
withFileAntenna: "Només les publicacions amb fitxers" withFileAntenna: "Només les publicacions amb fitxers"
hideNotesInSensitiveChannel: "Amaga les notes a canals sensibles "
enableServiceworker: "Activar les notificacions al navegador" enableServiceworker: "Activar les notificacions al navegador"
antennaUsersDescription: "Llistar un nom d'usuari per línia" antennaUsersDescription: "Llistar un nom d'usuari per línia"
caseSensitive: "Sensible a majúscules i minúscules " caseSensitive: "Sensible a majúscules i minúscules "
@ -1339,6 +1340,7 @@ compress: "Comprimir "
right: "Dreta" right: "Dreta"
bottom: "A baix " bottom: "A baix "
top: "A dalt " top: "A dalt "
embed: "Incrustar"
_chat: _chat:
noMessagesYet: "Encara no tens missatges " noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou" newMessage: "Missatge nou"
@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "Mostrar sub botons a la barra de navegació " showNavbarSubButtons: "Mostrar sub botons a la barra de navegació "
ifOn: "Quan s'encén " ifOn: "Quan s'encén "
ifOff: "Quan s'apaga " ifOff: "Quan s'apaga "
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
_chat: _chat:
showSenderName: "Mostrar el nom del remitent" showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar" sendOnEnter: "Introdueix per enviar"
@ -2122,7 +2125,6 @@ _theme:
header: "Capçalera" header: "Capçalera"
navBg: "Fons de la barra lateral" navBg: "Fons de la barra lateral"
navFg: "Text de la barra lateral" navFg: "Text de la barra lateral"
navHoverFg: "Text barra lateral (en passar per sobre)"
navActive: "Text barra lateral (actiu)" navActive: "Text barra lateral (actiu)"
navIndicator: "Indicador barra lateral" navIndicator: "Indicador barra lateral"
link: "Enllaç" link: "Enllaç"
@ -2145,11 +2147,8 @@ _theme:
buttonHoverBg: "Fons botó (en passar-hi per sobre)" buttonHoverBg: "Fons botó (en passar-hi per sobre)"
inputBorder: "Contorn del cap d'introducció " inputBorder: "Contorn del cap d'introducció "
driveFolderBg: "Fons de la carpeta Disc" driveFolderBg: "Fons de la carpeta Disc"
wallpaperOverlay: "Superposició del fons de pantalla "
badge: "Insígnia " badge: "Insígnia "
messageBg: "Fons del xat" messageBg: "Fons del xat"
accentDarken: "Accent (fosc)"
accentLighten: "Accent (clar)"
fgHighlighted: "Text ressaltat" fgHighlighted: "Text ressaltat"
_sfx: _sfx:
note: "Notes" note: "Notes"

View file

@ -1626,7 +1626,6 @@ _theme:
header: "Nadpis" header: "Nadpis"
navBg: "Pozadí postranního panelu" navBg: "Pozadí postranního panelu"
navFg: "Text na postranním panelu" navFg: "Text na postranním panelu"
navHoverFg: "Text na postranním panelu (Hover)"
navActive: "Text na postranním panelu (Aktivní)" navActive: "Text na postranním panelu (Aktivní)"
navIndicator: "Indikátor na postranním panelu" navIndicator: "Indikátor na postranním panelu"
link: "Odkaz" link: "Odkaz"
@ -1649,11 +1648,8 @@ _theme:
buttonHoverBg: "Pozadí tlačítka (Hover)" buttonHoverBg: "Pozadí tlačítka (Hover)"
inputBorder: "Ohraničení vstupního pole" inputBorder: "Ohraničení vstupního pole"
driveFolderBg: "Pozadí složky disku" driveFolderBg: "Pozadí složky disku"
wallpaperOverlay: "Překrytí tapety"
badge: "Odznak" badge: "Odznak"
messageBg: "Pozadí chatu" messageBg: "Pozadí chatu"
accentDarken: "Akcent (Ztmavený)"
accentLighten: "Akcent (Zesvětlený)"
fgHighlighted: "Zvýrazněný text" fgHighlighted: "Zvýrazněný text"
_sfx: _sfx:
note: "Poznámky" note: "Poznámky"

View file

@ -2105,7 +2105,6 @@ _theme:
header: "Kopfzeile" header: "Kopfzeile"
navBg: "Hintergrund der Seitenleiste" navBg: "Hintergrund der Seitenleiste"
navFg: "Text der Seitenleiste" navFg: "Text der Seitenleiste"
navHoverFg: "Text der Seitenleiste (Mouseover)"
navActive: "Text der Seitenleiste (Aktiv)" navActive: "Text der Seitenleiste (Aktiv)"
navIndicator: "Indikator der Seitenleiste" navIndicator: "Indikator der Seitenleiste"
link: "Link" link: "Link"
@ -2128,11 +2127,8 @@ _theme:
buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)"
inputBorder: "Rahmen von Eingabefeldern" inputBorder: "Rahmen von Eingabefeldern"
driveFolderBg: "Hintergrund von Drive-Ordnern" driveFolderBg: "Hintergrund von Drive-Ordnern"
wallpaperOverlay: "Hintergrundbild-Overlay"
badge: "Wappen" badge: "Wappen"
messageBg: "Hintergrund von Chats" messageBg: "Hintergrund von Chats"
accentDarken: "Akzent (Verdunkelt)"
accentLighten: "Akzent (Erhellt)"
fgHighlighted: "Hervorgehobener Text" fgHighlighted: "Hervorgehobener Text"
_sfx: _sfx:
note: "Notizen" note: "Notizen"

View file

@ -424,6 +424,7 @@ antennaExcludeBots: "Exclude bot accounts"
antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
notifyAntenna: "Notify about new notes" notifyAntenna: "Notify about new notes"
withFileAntenna: "Only notes with files" withFileAntenna: "Only notes with files"
hideNotesInSensitiveChannel: "Hide notes from sensitive channels"
enableServiceworker: "Enable Push-Notifications for your Browser" enableServiceworker: "Enable Push-Notifications for your Browser"
antennaUsersDescription: "List one username per line" antennaUsersDescription: "List one username per line"
caseSensitive: "Case sensitive" caseSensitive: "Case sensitive"
@ -1339,6 +1340,7 @@ compress: "Compress"
right: "Right" right: "Right"
bottom: "Bottom" bottom: "Bottom"
top: "Top" top: "Top"
embed: "Embed"
_chat: _chat:
noMessagesYet: "No messages yet" noMessagesYet: "No messages yet"
newMessage: "New message" newMessage: "New message"
@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "Show sub-buttons on the navigation bar" showNavbarSubButtons: "Show sub-buttons on the navigation bar"
ifOn: "When turned on" ifOn: "When turned on"
ifOff: "When turned off" ifOff: "When turned off"
enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
_chat: _chat:
showSenderName: "Show sender's name" showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send" sendOnEnter: "Press Enter to send"
@ -2122,7 +2125,6 @@ _theme:
header: "Header" header: "Header"
navBg: "Sidebar background" navBg: "Sidebar background"
navFg: "Sidebar text" navFg: "Sidebar text"
navHoverFg: "Sidebar text (Hover)"
navActive: "Sidebar text (Active)" navActive: "Sidebar text (Active)"
navIndicator: "Sidebar indicator" navIndicator: "Sidebar indicator"
link: "Link" link: "Link"
@ -2145,11 +2147,8 @@ _theme:
buttonHoverBg: "Button background (Hover)" buttonHoverBg: "Button background (Hover)"
inputBorder: "Input field border" inputBorder: "Input field border"
driveFolderBg: "Drive folder background" driveFolderBg: "Drive folder background"
wallpaperOverlay: "Wallpaper overlay"
badge: "Badge" badge: "Badge"
messageBg: "Chat background" messageBg: "Chat background"
accentDarken: "Accent (Darkened)"
accentLighten: "Accent (Lightened)"
fgHighlighted: "Highlighted Text" fgHighlighted: "Highlighted Text"
_sfx: _sfx:
note: "New note" note: "New note"

View file

@ -1965,7 +1965,6 @@ _theme:
header: "Cabezal" header: "Cabezal"
navBg: "Fondo de la barra lateral" navBg: "Fondo de la barra lateral"
navFg: "Texto de la barra lateral" navFg: "Texto de la barra lateral"
navHoverFg: "Texto de la barra lateral (hover)"
navActive: "Texto de la barra lateral (activo)" navActive: "Texto de la barra lateral (activo)"
navIndicator: "Indicador de la barra lateral" navIndicator: "Indicador de la barra lateral"
link: "Vínculo" link: "Vínculo"
@ -1988,11 +1987,8 @@ _theme:
buttonHoverBg: "Fondo de botón (hover)" buttonHoverBg: "Fondo de botón (hover)"
inputBorder: "Borde de los campos de entrada" inputBorder: "Borde de los campos de entrada"
driveFolderBg: "Fondo de capeta del drive" driveFolderBg: "Fondo de capeta del drive"
wallpaperOverlay: "Transparencia del fondo de pantalla"
badge: "Medalla" badge: "Medalla"
messageBg: "Fondo de chat" messageBg: "Fondo de chat"
accentDarken: "Acento (oscuro)"
accentLighten: "Acento (claro)"
fgHighlighted: "Texto resaltado" fgHighlighted: "Texto resaltado"
_sfx: _sfx:
note: "Notas" note: "Notas"

View file

@ -1816,7 +1816,6 @@ _theme:
header: "Entête" header: "Entête"
navBg: "Fond de la barre latérale" navBg: "Fond de la barre latérale"
navFg: "Texte 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)" navActive: "Texte de la barre latérale (actif)"
navIndicator: "Indicateur de barre latérale" navIndicator: "Indicateur de barre latérale"
link: "Lien" link: "Lien"
@ -1839,11 +1838,8 @@ _theme:
buttonHoverBg: "Arrière-plan du bouton (survolé)" buttonHoverBg: "Arrière-plan du bouton (survolé)"
inputBorder: "Cadre de la zone de texte" inputBorder: "Cadre de la zone de texte"
driveFolderBg: "Arrière-plan du dossier de disque" driveFolderBg: "Arrière-plan du dossier de disque"
wallpaperOverlay: "Superposition de fond d'écran"
badge: "Badge" badge: "Badge"
messageBg: "Arrière plan de la discussion" messageBg: "Arrière plan de la discussion"
accentDarken: "Plus sombre"
accentLighten: "Plus clair"
fgHighlighted: "Texte mis en évidence" fgHighlighted: "Texte mis en évidence"
_sfx: _sfx:
note: "Nouvelle note" note: "Nouvelle note"

View file

@ -1931,7 +1931,6 @@ _theme:
header: "Header" header: "Header"
navBg: "Latar belakang bilah samping" navBg: "Latar belakang bilah samping"
navFg: "Teks bilah samping" navFg: "Teks bilah samping"
navHoverFg: "Teks bilah samping (Mengambang)"
navActive: "Teks bilah samping (Aktif)" navActive: "Teks bilah samping (Aktif)"
navIndicator: "Indikator bilah samping" navIndicator: "Indikator bilah samping"
link: "Tautan" link: "Tautan"
@ -1954,11 +1953,8 @@ _theme:
buttonHoverBg: "Latar belakang tombol (Mengambang)" buttonHoverBg: "Latar belakang tombol (Mengambang)"
inputBorder: "Batas bidang masukan" inputBorder: "Batas bidang masukan"
driveFolderBg: "Latar belakang folder drive" driveFolderBg: "Latar belakang folder drive"
wallpaperOverlay: "Lapisan wallpaper"
badge: "Lencana" badge: "Lencana"
messageBg: "Latar belakang obrolan" messageBg: "Latar belakang obrolan"
accentDarken: "Aksen (Gelap)"
accentLighten: "Aksen (Terang)"
fgHighlighted: "Teks yang disorot" fgHighlighted: "Teks yang disorot"
_sfx: _sfx:
note: "Catatan" note: "Catatan"

8
locales/index.d.ts vendored
View file

@ -7385,6 +7385,14 @@ export interface Locale extends ILocale {
* UI上で先頭に表示されます * UI上で先頭に表示されます
*/ */
"descriptionOfDisplayOrder": string; "descriptionOfDisplayOrder": string;
/**
*
*/
"preserveAssignmentOnMoveAccount": string;
/**
*
*/
"preserveAssignmentOnMoveAccount_description": string;
/** /**
* *
*/ */

View file

@ -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)." 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" notifyAntenna: "Invia notifiche delle nuove note"
withFileAntenna: "Solo note con file in allegato" withFileAntenna: "Solo note con file in allegato"
hideNotesInSensitiveChannel: "Nascondere le Note dai canali espliciti"
enableServiceworker: "Abilita ServiceWorker" enableServiceworker: "Abilita ServiceWorker"
antennaUsersDescription: "Elenca un nome utente per riga" antennaUsersDescription: "Elenca un nome utente per riga"
caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole" caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole"
@ -1336,6 +1337,10 @@ chat: "Chat"
migrateOldSettings: "Migrare le vecchie impostazioni" 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." 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" compress: "Comprimi"
right: "Destra"
bottom: "Sotto"
top: "Sopra"
embed: "Incorporare"
_chat: _chat:
noMessagesYet: "Ancora nessun messaggio" noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio" newMessage: "Nuovo messaggio"
@ -1406,9 +1411,11 @@ _settings:
timelineAndNote: "Note e Timeline" timelineAndNote: "Note e Timeline"
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile" makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni." 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" showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione"
ifOn: "Quando attivato" ifOn: "Quando attivato"
ifOff: "Quando disattivato" ifOff: "Quando disattivato"
enableSyncThemesBetweenDevices: "Sincronizzare il tema tra i dispositivi"
_chat: _chat:
showSenderName: "Mostra il nome del mittente" showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce" sendOnEnter: "Invio spedisce"
@ -2118,14 +2125,13 @@ _theme:
header: "Intestazione" header: "Intestazione"
navBg: "Sfondo della barra laterale" navBg: "Sfondo della barra laterale"
navFg: "Testo della barra laterale" navFg: "Testo della barra laterale"
navHoverFg: "Testo della barra laterale (al passaggio del mouse)"
navActive: "Testo della barra laterale (attivo)" navActive: "Testo della barra laterale (attivo)"
navIndicator: "Indicatore di barra laterale" navIndicator: "Indicatore di barra laterale"
link: "Link" link: "Link"
hashtag: "Hashtag" hashtag: "Hashtag"
mention: "Menzioni" mention: "Menzioni"
mentionMe: "Menzioni (di me)" mentionMe: "Menzioni (di me)"
renote: "Rinota" renote: "Renota"
modalBg: "Sfondo modale." modalBg: "Sfondo modale."
divider: "Interruzione di linea" divider: "Interruzione di linea"
scrollbarHandle: "Maniglie della barra di scorrimento" scrollbarHandle: "Maniglie della barra di scorrimento"
@ -2141,11 +2147,8 @@ _theme:
buttonHoverBg: "Sfondo del pulsante (sorvolato)" buttonHoverBg: "Sfondo del pulsante (sorvolato)"
inputBorder: "Inquadra casella di testo" inputBorder: "Inquadra casella di testo"
driveFolderBg: "Sfondo della cartella di disco" driveFolderBg: "Sfondo della cartella di disco"
wallpaperOverlay: "Sovrapposizione dello sfondo"
badge: "Distintivo" badge: "Distintivo"
messageBg: "Sfondo della chat" messageBg: "Sfondo della chat"
accentDarken: "Temi (scuri)"
accentLighten: "Temi (luminosi)"
fgHighlighted: "Testo in evidenza." fgHighlighted: "Testo in evidenza."
_sfx: _sfx:
note: "Nota" note: "Nota"
@ -2592,6 +2595,9 @@ _notification:
_deck: _deck:
alwaysShowMainColumn: "Mostra sempre la colonna principale" alwaysShowMainColumn: "Mostra sempre la colonna principale"
columnAlign: "Allineare colonne" columnAlign: "Allineare colonne"
columnGap: "Margine tra le colonne"
deckMenuPosition: "Posizione del menu Deck"
navbarPosition: "Posizione barra di navigazione"
addColumn: "Aggiungi colonna" addColumn: "Aggiungi colonna"
newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note" newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note"
configureColumn: "Impostazioni colonna" configureColumn: "Impostazioni colonna"

View file

@ -1907,6 +1907,8 @@ _role:
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
displayOrder: "表示順" displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
preserveAssignmentOnMoveAccount: "アサイン状態を移行先アカウントにも引き継ぐ"
preserveAssignmentOnMoveAccount_description: "オンにすると、このロールが付与されたアカウントが移行された際に、移行先アカウントにもこのロールが引き継がれるようになります。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"

View file

@ -2007,7 +2007,6 @@ _theme:
header: "ヘッダー" header: "ヘッダー"
navBg: "サイドバーの背景" navBg: "サイドバーの背景"
navFg: "サイドバーの文字" navFg: "サイドバーの文字"
navHoverFg: "サイドバー文字(ホバー)"
navActive: "サイドバー文字(アクティブ)" navActive: "サイドバー文字(アクティブ)"
navIndicator: "サイドバーのインジケーター" navIndicator: "サイドバーのインジケーター"
link: "リンク" link: "リンク"
@ -2030,11 +2029,8 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)" buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り" inputBorder: "入力ボックスの縁取り"
driveFolderBg: "ドライブフォルダーの背景" driveFolderBg: "ドライブフォルダーの背景"
wallpaperOverlay: "壁紙のオーバーレイ"
badge: "バッジ" badge: "バッジ"
messageBg: "チャットの背景" messageBg: "チャットの背景"
accentDarken: "アクセント (暗め)"
accentLighten: "アクセント (明るめ)"
fgHighlighted: "強調されとる文字" fgHighlighted: "強調されとる文字"
_sfx: _sfx:
note: "ノート" note: "ノート"

View file

@ -747,6 +747,7 @@ _theme:
description: "설멩" description: "설멩"
keys: keys:
mention: "멘션" mention: "멘션"
renote: "리노트"
_sfx: _sfx:
note: "새 노트" note: "새 노트"
notification: "알림" notification: "알림"

View file

@ -2067,7 +2067,6 @@ _theme:
header: "헤더" header: "헤더"
navBg: "사이드바 배경" navBg: "사이드바 배경"
navFg: "사이드바 텍스트" navFg: "사이드바 텍스트"
navHoverFg: "사이드바 텍스트 (호버)"
navActive: "사이드바 텍스트 (활성)" navActive: "사이드바 텍스트 (활성)"
navIndicator: "사이드바 인디케이터" navIndicator: "사이드바 인디케이터"
link: "링크" link: "링크"
@ -2090,11 +2089,8 @@ _theme:
buttonHoverBg: "버튼 배경 (호버)" buttonHoverBg: "버튼 배경 (호버)"
inputBorder: "입력 필드 테두리" inputBorder: "입력 필드 테두리"
driveFolderBg: "드라이브 폴더 배경" driveFolderBg: "드라이브 폴더 배경"
wallpaperOverlay: "배경화면 오버레이"
badge: "배지" badge: "배지"
messageBg: "대화 배경" messageBg: "대화 배경"
accentDarken: "강조 색상 (어두움)"
accentLighten: "강조 색상 (밝음)"
fgHighlighted: "강조된 텍스트" fgHighlighted: "강조된 텍스트"
_sfx: _sfx:
note: "새 노트" note: "새 노트"

View file

@ -1212,7 +1212,6 @@ _theme:
header: "Nagłówek" header: "Nagłówek"
navBg: "Tło paska bocznego" navBg: "Tło paska bocznego"
navFg: "Tekst paska bocznego" navFg: "Tekst paska bocznego"
navHoverFg: "Tekst paska bocznego (zbliżenie)"
navActive: "Tekst paska bocznego (aktywny)" navActive: "Tekst paska bocznego (aktywny)"
navIndicator: "Wskaźnik paska bocznego" navIndicator: "Wskaźnik paska bocznego"
link: "Odnośnik" link: "Odnośnik"
@ -1235,11 +1234,8 @@ _theme:
buttonHoverBg: "Tło przycisku (po najechaniu)" buttonHoverBg: "Tło przycisku (po najechaniu)"
inputBorder: "Obramowanie pola wejścia" inputBorder: "Obramowanie pola wejścia"
driveFolderBg: "Tło folderu na dysku" driveFolderBg: "Tło folderu na dysku"
wallpaperOverlay: "Nakładka tapety"
badge: "Odznaka" badge: "Odznaka"
messageBg: "Tło czatu" messageBg: "Tło czatu"
accentDarken: "Akcent (ciemniejszy)"
accentLighten: "Akcent (jaśniejszy)"
fgHighlighted: "Wyróżniony tekst" fgHighlighted: "Wyróżniony tekst"
_sfx: _sfx:
note: "Wpisy" note: "Wpisy"

View file

@ -1997,7 +1997,6 @@ _theme:
header: "Cabeçalho" header: "Cabeçalho"
navBg: "Plano de fundo da barra lateral" navBg: "Plano de fundo da barra lateral"
navFg: "Texto da barra lateral" navFg: "Texto da barra lateral"
navHoverFg: "Texto da coluna lateral (Selecionado)"
navActive: "Texto da coluna lateral (Ativa)" navActive: "Texto da coluna lateral (Ativa)"
navIndicator: "Indicador da coluna lateral" navIndicator: "Indicador da coluna lateral"
link: "Link" link: "Link"
@ -2020,11 +2019,8 @@ _theme:
buttonHoverBg: "Plano de fundo de botão (Selecionado)" buttonHoverBg: "Plano de fundo de botão (Selecionado)"
inputBorder: "Borda de campo digitável" inputBorder: "Borda de campo digitável"
driveFolderBg: "Plano de fundo da pasta no Drive" driveFolderBg: "Plano de fundo da pasta no Drive"
wallpaperOverlay: "Sobreposição do papel de parede."
badge: "Emblema" badge: "Emblema"
messageBg: "Plano de fundo do chat" messageBg: "Plano de fundo do chat"
accentDarken: "Cor de destaque (Escurecida)"
accentLighten: "Cor de destaque (Esclarecida)"
fgHighlighted: "Texto Destacado" fgHighlighted: "Texto Destacado"
_sfx: _sfx:
note: "Posts" note: "Posts"

View file

@ -1689,7 +1689,6 @@ _theme:
header: "Заголовок" header: "Заголовок"
navBg: "Фон боковой панели" navBg: "Фон боковой панели"
navFg: "Текст на боковой панели" navFg: "Текст на боковой панели"
navHoverFg: "Текст на боковой панели (под указателем)"
navActive: "Текст на боковой панели (активирован)" navActive: "Текст на боковой панели (активирован)"
navIndicator: "Индикатор на боковой панели" navIndicator: "Индикатор на боковой панели"
link: "Ссылка" link: "Ссылка"
@ -1712,11 +1711,8 @@ _theme:
buttonHoverBg: "Текст кнопки" buttonHoverBg: "Текст кнопки"
inputBorder: "Рамка поля ввода" inputBorder: "Рамка поля ввода"
driveFolderBg: "Фон папки «Диска»" driveFolderBg: "Фон папки «Диска»"
wallpaperOverlay: "Слой обоев"
badge: "Значок" badge: "Значок"
messageBg: "Фон беседы" messageBg: "Фон беседы"
accentDarken: "Фон (затемнённый)"
accentLighten: "Фон (осветлённый)"
fgHighlighted: "Подсвеченный текст" fgHighlighted: "Подсвеченный текст"
_sfx: _sfx:
note: "Заметки" note: "Заметки"

View file

@ -1089,7 +1089,6 @@ _theme:
header: "Hlavička" header: "Hlavička"
navBg: "Pozadie bočného panela" navBg: "Pozadie bočného panela"
navFg: "Text 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)" navActive: "Text bočného panela (aktívny)"
navIndicator: "Indikátor bočného panela" navIndicator: "Indikátor bočného panela"
link: "Odkaz" link: "Odkaz"
@ -1112,11 +1111,8 @@ _theme:
buttonHoverBg: "Pozadie tlačidla (pod kurzorom)" buttonHoverBg: "Pozadie tlačidla (pod kurzorom)"
inputBorder: "Okraj vstupného poľa" inputBorder: "Okraj vstupného poľa"
driveFolderBg: "Pozadie priečinu disku" driveFolderBg: "Pozadie priečinu disku"
wallpaperOverlay: "Vrstvenie pozadia"
badge: "Odznak" badge: "Odznak"
messageBg: "Pozadie chatu" messageBg: "Pozadie chatu"
accentDarken: "Akcent (stmavené)"
accentLighten: "Akcent (zosvetlené)"
fgHighlighted: "Zvýraznený text" fgHighlighted: "Zvýraznený text"
_sfx: _sfx:
note: "Poznámky" note: "Poznámky"

View file

@ -1974,7 +1974,6 @@ _theme:
header: "ส่วนหัว" header: "ส่วนหัว"
navBg: "พื้นหลังแถบด้านข้าง" navBg: "พื้นหลังแถบด้านข้าง"
navFg: "ข้อความแถบด้านข้าง" navFg: "ข้อความแถบด้านข้าง"
navHoverFg: "ข้อความแถบด้านข้าง (โฮเวอร์)"
navActive: "ข้อความแถบด้านข้าง (ใช้งานอยู่)" navActive: "ข้อความแถบด้านข้าง (ใช้งานอยู่)"
navIndicator: "ตัวระบุแถบด้านข้าง" navIndicator: "ตัวระบุแถบด้านข้าง"
link: "ลิงก์" link: "ลิงก์"
@ -1997,11 +1996,8 @@ _theme:
buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)" buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
inputBorder: "เส้นขอบของช่องป้อนข้อมูล" inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์" driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
badge: "ตรา" badge: "ตรา"
messageBg: "พื้นหลังแชท" messageBg: "พื้นหลังแชท"
accentDarken: "สีหลัก (มืด)"
accentLighten: "สีหลัก (สว่าง)"
fgHighlighted: "ข้อความที่ไฮไลต์" fgHighlighted: "ข้อความที่ไฮไลต์"
_sfx: _sfx:
note: "โน้ต" note: "โน้ต"

View file

@ -1283,7 +1283,6 @@ _theme:
header: "Заголовок" header: "Заголовок"
navBg: "Фон бокової панелі" navBg: "Фон бокової панелі"
navFg: "Текст бокової панелі" navFg: "Текст бокової панелі"
navHoverFg: "Текст бокової панелі (під курсором)"
navActive: "Текст бокової панелі (активне)" navActive: "Текст бокової панелі (активне)"
navIndicator: "Індикатор бокової панелі" navIndicator: "Індикатор бокової панелі"
link: "Посилання" link: "Посилання"
@ -1306,11 +1305,8 @@ _theme:
buttonHoverBg: "Фон кнопки (при наведенні)" buttonHoverBg: "Фон кнопки (при наведенні)"
inputBorder: "Край поля вводу" inputBorder: "Край поля вводу"
driveFolderBg: "Фон папки на диску" driveFolderBg: "Фон папки на диску"
wallpaperOverlay: "Накладання шпалер"
badge: "Значок" badge: "Значок"
messageBg: "Фон переписки" messageBg: "Фон переписки"
accentDarken: "Акцент (Затемлений)"
accentLighten: "Акцент (Освітлений)"
fgHighlighted: "Виділений текст" fgHighlighted: "Виділений текст"
_sfx: _sfx:
note: "Нотатки" note: "Нотатки"

View file

@ -907,8 +907,6 @@ _theme:
mention: "Murojat" mention: "Murojat"
renote: "Qayta qayd etish" renote: "Qayta qayd etish"
divider: "Ajratrmoq" divider: "Ajratrmoq"
accentDarken: "Urg'u (Qoraytirilgan)"
accentLighten: "Urg'u (Yoritilgan)"
fgHighlighted: "Belgilangan matn" fgHighlighted: "Belgilangan matn"
_sfx: _sfx:
note: "Qaydlar" note: "Qaydlar"

View file

@ -1530,7 +1530,6 @@ _theme:
header: "Ảnh bìa" header: "Ảnh bìa"
navBg: "Nền thanh bên" navBg: "Nền thanh bên"
navFg: "Chữ thanh bên" navFg: "Chữ thanh bên"
navHoverFg: "Chữ thanh bên (Khi chạm)"
navActive: "Chữ thanh bên (Khi chọn)" navActive: "Chữ thanh bên (Khi chọn)"
navIndicator: "Chỉ báo thanh bên" navIndicator: "Chỉ báo thanh bên"
link: "Đường dẫn" link: "Đường dẫn"
@ -1553,11 +1552,8 @@ _theme:
buttonHoverBg: "Nền nút (Chạm)" buttonHoverBg: "Nền nút (Chạm)"
inputBorder: "Đường viền khung soạn thảo" inputBorder: "Đường viền khung soạn thảo"
driveFolderBg: "Nền thư mục Ổ đĩa" driveFolderBg: "Nền thư mục Ổ đĩa"
wallpaperOverlay: "Lớp phủ hình nền"
badge: "Huy hiệu" badge: "Huy hiệu"
messageBg: "Nền chat" messageBg: "Nền chat"
accentDarken: "Màu phụ (Tối)"
accentLighten: "Màu phụ (Sáng)"
fgHighlighted: "Chữ nổi bật" fgHighlighted: "Chữ nổi bật"
_sfx: _sfx:
note: "Tút" note: "Tút"

View file

@ -424,6 +424,7 @@ antennaExcludeBots: "排除机器人账户"
antennaKeywordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。" antennaKeywordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
notifyAntenna: "开启通知" notifyAntenna: "开启通知"
withFileAntenna: "仅带有附件的帖子" withFileAntenna: "仅带有附件的帖子"
hideNotesInSensitiveChannel: "隐藏敏感频道内的帖子"
enableServiceworker: "启用 ServiceWorker" enableServiceworker: "启用 ServiceWorker"
antennaUsersDescription: "指定用户名,一行一个" antennaUsersDescription: "指定用户名,一行一个"
caseSensitive: "区分大小写" caseSensitive: "区分大小写"
@ -1339,6 +1340,7 @@ compress: "压缩"
right: "右" right: "右"
bottom: "下" bottom: "下"
top: "上" top: "上"
embed: "嵌入"
_chat: _chat:
noMessagesYet: "还没有消息" noMessagesYet: "还没有消息"
newMessage: "新消息" newMessage: "新消息"
@ -2122,7 +2124,6 @@ _theme:
header: "顶栏" header: "顶栏"
navBg: "侧边栏背景" navBg: "侧边栏背景"
navFg: "侧栏文本" navFg: "侧栏文本"
navHoverFg: "侧栏文本(悬停)"
navActive: "侧栏文本(活动)" navActive: "侧栏文本(活动)"
navIndicator: "侧栏标记" navIndicator: "侧栏标记"
link: "链接" link: "链接"
@ -2145,11 +2146,8 @@ _theme:
buttonHoverBg: "按钮背景(悬停)" buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框" inputBorder: "输入框边框"
driveFolderBg: "网盘的文件夹背景" driveFolderBg: "网盘的文件夹背景"
wallpaperOverlay: "壁纸叠加层"
badge: "徽章" badge: "徽章"
messageBg: "聊天背景" messageBg: "聊天背景"
accentDarken: "强调色(深)"
accentLighten: "强调色(浅)"
fgHighlighted: "高亮显示文本" fgHighlighted: "高亮显示文本"
_sfx: _sfx:
note: "帖子" note: "帖子"

View file

@ -424,6 +424,7 @@ antennaExcludeBots: "排除機器人帳戶"
antennaKeywordsDescription: "空格代表「以及」AND換行代表「或者」OR" antennaKeywordsDescription: "空格代表「以及」AND換行代表「或者」OR"
notifyAntenna: "通知有新貼文" notifyAntenna: "通知有新貼文"
withFileAntenna: "僅帶有附件的貼文" withFileAntenna: "僅帶有附件的貼文"
hideNotesInSensitiveChannel: "隱藏敏感頻道的貼文"
enableServiceworker: "啟用瀏覽器的推播通知" enableServiceworker: "啟用瀏覽器的推播通知"
antennaUsersDescription: "填寫使用者名稱,以換行分隔" antennaUsersDescription: "填寫使用者名稱,以換行分隔"
caseSensitive: "區分大小寫" caseSensitive: "區分大小寫"
@ -1339,6 +1340,7 @@ compress: "壓縮"
right: "右" right: "右"
bottom: "下" bottom: "下"
top: "上" top: "上"
embed: "嵌入"
_chat: _chat:
noMessagesYet: "尚無訊息" noMessagesYet: "尚無訊息"
newMessage: "新訊息" newMessage: "新訊息"
@ -1413,6 +1415,7 @@ _settings:
showNavbarSubButtons: "在導覽列顯示輔助按鈕" showNavbarSubButtons: "在導覽列顯示輔助按鈕"
ifOn: "開啟時" ifOn: "開啟時"
ifOff: "關閉時" ifOff: "關閉時"
enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
_chat: _chat:
showSenderName: "顯示發送者的名稱" showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息" sendOnEnter: "按下 Enter 發送訊息"
@ -1885,6 +1888,8 @@ _role:
descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。" descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。"
displayOrder: "顯示順序" displayOrder: "顯示順序"
descriptionOfDisplayOrder: "數字越大顯示在UI上的越上面。" descriptionOfDisplayOrder: "數字越大顯示在UI上的越上面。"
preserveAssignmentOnMoveAccount: "將指派狀態承接至轉移後的帳戶"
preserveAssignmentOnMoveAccount_description: "開啟此選項後,當具備此角色的帳戶被移轉時,該角色也會承接至轉移後的帳戶。"
canEditMembersByModerator: "允許編輯審查員的成員" canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"
@ -2122,7 +2127,6 @@ _theme:
header: "標題" header: "標題"
navBg: "側邊欄的背景 " navBg: "側邊欄的背景 "
navFg: "側邊欄的文字" navFg: "側邊欄的文字"
navHoverFg: "側邊欄文字(懸浮) "
navActive: "側邊欄文字(活動)" navActive: "側邊欄文字(活動)"
navIndicator: "側邊欄指示符" navIndicator: "側邊欄指示符"
link: "連結" link: "連結"
@ -2145,11 +2149,8 @@ _theme:
buttonHoverBg: "按鈕背景 (漂浮)" buttonHoverBg: "按鈕背景 (漂浮)"
inputBorder: "輸入框邊框" inputBorder: "輸入框邊框"
driveFolderBg: "雲端硬碟文件夾背景" driveFolderBg: "雲端硬碟文件夾背景"
wallpaperOverlay: "壁紙覆蓋層"
badge: "徽章" badge: "徽章"
messageBg: "私訊背景" messageBg: "私訊背景"
accentDarken: "強調色(黑暗)"
accentLighten: "強調色(明亮)"
fgHighlighted: "突顯文字" fgHighlighted: "突顯文字"
_sfx: _sfx:
note: "貼文" note: "貼文"

View file

@ -1,6 +1,6 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2025.4.0-beta.1", "version": "2025.4.0-rc.0",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -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"`);
}
}

View file

@ -24,6 +24,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class AccountMoveService { export class AccountMoveService {
@ -64,6 +65,7 @@ export class AccountMoveService {
private relayService: RelayService, private relayService: RelayService,
private queueService: QueueService, private queueService: QueueService,
private systemAccountService: SystemAccountService, private systemAccountService: SystemAccountService,
private roleService: RoleService,
) { ) {
} }
@ -123,6 +125,7 @@ export class AccountMoveService {
this.copyBlocking(src, dst), this.copyBlocking(src, dst),
this.copyMutings(src, dst), this.copyMutings(src, dst),
this.deleteScheduledNotes(src), this.deleteScheduledNotes(src),
this.copyRoles(src, dst),
this.updateLists(src, dst), this.updateLists(src, dst),
]); ]);
} catch { } 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. * Update lists while moving accounts.
* - No removal of the old account from the lists * - No removal of the old account from the lists

View file

@ -99,7 +99,7 @@ export class ChatService {
text?: string | null; text?: string | null;
file?: MiDriveFile | null; file?: MiDriveFile | null;
uri?: string | null; uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> { }): Promise<Packed<'ChatMessageLiteFor1on1'>> {
if (fromUser.id === toUser.id) { if (fromUser.id === toUser.id) {
throw new Error('yourself'); throw new Error('yourself');
} }
@ -210,7 +210,7 @@ export class ChatService {
text?: string | null; text?: string | null;
file?: MiDriveFile | null; file?: MiDriveFile | null;
uri?: string | null; uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> { }): Promise<Packed<'ChatMessageLiteForRoom'>> {
const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({ const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({
userId: m.userId, userId: m.userId,
isMuted: m.isMuted, isMuted: m.isMuted,

View file

@ -639,6 +639,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
isModerator: values.isModerator, isModerator: values.isModerator,
isExplorable: values.isExplorable, isExplorable: values.isExplorable,
asBadge: values.asBadge, asBadge: values.asBadge,
preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: values.canEditMembersByModerator, canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder, displayOrder: values.displayOrder,
policies: values.policies, policies: values.policies,

View file

@ -128,7 +128,7 @@ export class ChatEntityService {
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>; packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
}; };
}, },
): Promise<Packed<'ChatMessageLite'>> { ): Promise<Packed<'ChatMessageLiteFor1on1'>> {
const packedFiles = options?._hint_?.packedFiles; const packedFiles = options?._hint_?.packedFiles;
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); 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(), createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text, text: message.text,
fromUserId: message.fromUserId, fromUserId: message.fromUserId,
toUserId: message.toUserId, toUserId: message.toUserId!,
fileId: message.fileId, fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
reactions, reactions,
@ -177,7 +177,7 @@ export class ChatEntityService {
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
}; };
}, },
): Promise<Packed<'ChatMessageLite'>> { ): Promise<Packed<'ChatMessageLiteForRoom'>> {
const packedFiles = options?._hint_?.packedFiles; const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers; const packedUsers = options?._hint_?.packedUsers;
@ -199,7 +199,7 @@ export class ChatEntityService {
text: message.text, text: message.text,
fromUserId: message.fromUserId, fromUserId: message.fromUserId,
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId), fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
toRoomId: message.toRoomId, toRoomId: message.toRoomId!,
fileId: message.fileId, fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
reactions, reactions,

View file

@ -13,6 +13,7 @@ import type { MiRole } from '@/models/Role.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Packed } from '@/misc/json-schema.js';
@Injectable() @Injectable()
export class RoleEntityService { export class RoleEntityService {
@ -31,7 +32,7 @@ export class RoleEntityService {
public async pack( public async pack(
src: MiRole['id'] | MiRole, src: MiRole['id'] | MiRole,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ): Promise<Packed<'Role'>> {
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
@ -67,6 +68,7 @@ export class RoleEntityService {
isModerator: role.isModerator, isModerator: role.isModerator,
isExplorable: role.isExplorable, isExplorable: role.isExplorable,
asBadge: role.asBadge, asBadge: role.asBadge,
preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder, displayOrder: role.displayOrder,
policies: policies, policies: policies,

View file

@ -63,7 +63,7 @@ import {
} from '@/models/json-schema/meta.js'; } from '@/models/json-schema/meta.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.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 { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
@ -126,6 +126,8 @@ export const refs = {
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema, ChatMessage: packedChatMessageSchema,
ChatMessageLite: packedChatMessageLiteSchema, ChatMessageLite: packedChatMessageLiteSchema,
ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema,
ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema,
ChatRoom: packedChatRoomSchema, ChatRoom: packedChatRoomSchema,
ChatRoomInvitation: packedChatRoomInvitationSchema, ChatRoomInvitation: packedChatRoomInvitationSchema,
ChatRoomMembership: packedChatRoomMembershipSchema, ChatRoomMembership: packedChatRoomMembershipSchema,

View file

@ -248,6 +248,11 @@ export class MiRole {
}) })
public isExplorable: boolean; public isExplorable: boolean;
@Column('boolean', {
default: false,
})
public preserveAssignmentOnMoveAccount: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -72,7 +72,7 @@ export const packedChatMessageSchema = {
}, },
user: { user: {
type: 'object', type: 'object',
optional: true, nullable: true, optional: false, nullable: false,
ref: 'UserLite', ref: 'UserLite',
}, },
}, },
@ -144,3 +144,113 @@ export const packedChatMessageLiteSchema = {
}, },
}, },
} as const; } 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;

View file

@ -397,6 +397,11 @@ export const packedRoleSchema = {
optional: false, nullable: false, optional: false, nullable: false,
example: false, example: false,
}, },
preserveAssignmentOnMoveAccount: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
canEditMembersByModerator: { canEditMembersByModerator: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -36,6 +36,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' }, asBadge: { type: 'boolean' },
preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' }, displayOrder: { type: 'number' },
policies: { policies: {

View file

@ -41,6 +41,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean' }, isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' }, asBadge: { type: 'boolean' },
preserveAssignmentOnMoveAccount: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' }, displayOrder: { type: 'number' },
policies: { policies: {
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
isExplorable: ps.isExplorable, isExplorable: ps.isExplorable,
asBadge: ps.asBadge, asBadge: ps.asBadge,
preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder, displayOrder: ps.displayOrder,
policies: ps.policies, policies: ps.policies,

View file

@ -30,7 +30,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'ChatMessageLite', ref: 'ChatMessageLiteForRoom',
}, },
errors: { errors: {

View file

@ -30,7 +30,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'ChatMessageLite', ref: 'ChatMessageLiteFor1on1',
}, },
errors: { errors: {

View file

@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'], tags: ['chat'],
requireCredential: true, requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat', kind: 'write:chat',

View file

@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'], tags: ['chat'],
requireCredential: true, requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat', kind: 'write:chat',

View file

@ -23,7 +23,7 @@ export const meta = {
items: { items: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'ChatMessageLite', ref: 'ChatMessageLiteForRoom',
}, },
}, },

View file

@ -13,6 +13,7 @@ export const meta = {
tags: ['chat'], tags: ['chat'],
requireCredential: true, requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat', kind: 'write:chat',

View file

@ -24,7 +24,7 @@ export const meta = {
items: { items: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'ChatMessageLite', ref: 'ChatMessageLiteFor1on1',
}, },
}, },

View file

@ -57,6 +57,7 @@ class BubbleTimelineChannel extends Channel {
if (note.channelId != null) return; if (note.channelId != null) return;
if (note.user.host == null) return; if (note.user.host == null) return;
if (!this.instance.bubbleInstances.includes(note.user.host)) 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; if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;

View file

@ -53,6 +53,7 @@ class GlobalTimelineChannel extends Channel {
if (note.visibility !== 'public') return; if (note.visibility !== 'public') return;
if (note.channelId != null) return; if (note.channelId != null) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;

View file

@ -56,6 +56,7 @@ class LocalTimelineChannel extends Channel {
if (note.user.host !== null) return; if (note.user.host !== null) return;
if (note.visibility !== 'public') return; if (note.visibility !== 'public') return;
if (note.channelId != null) 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) { if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {

View file

@ -13,18 +13,18 @@
fgHighlighted: '#6bc9a0', fgHighlighted: '#6bc9a0',
fgOnWhite: '@accent', fgOnWhite: '@accent',
divider: '#cfcfcf', divider: '#cfcfcf',
panel: '@X14', panel: '#ebe7e5',
panelHeaderBg: '@panel', panelHeaderBg: '@panel',
panelHeaderDivider: '@divider', panelHeaderDivider: '@divider',
header: ':alpha<0.7<@panel', header: ':alpha<0.7<@panel',
navBg: '@X14', navBg: '#ebe7e5',
renote: '#229e92', renote: '#229e92',
mention: '#da6d35', mention: '#da6d35',
mentionMe: '#d44c4c', mentionMe: '#d44c4c',
hashtag: '#4cb8d4', hashtag: '#4cb8d4',
link: '@accent', link: '@accent',
buttonGradateB: ':hue<-70<@accent', buttonGradateB: ':hue<-70<@accent',
success: '#86b300', success: '@accent',
X14: '#ebe7e5' error: '#da5635',
}, },
} }

View file

@ -18,5 +18,8 @@
mention: '@accent', mention: '@accent',
mentionMe: 'rgb(170, 149, 98)', mentionMe: 'rgb(170, 149, 98)',
hashtag: '@accent', hashtag: '@accent',
error: '#db9184',
warn: '#dbc184',
success: '#a3c975',
}, },
} }

View file

@ -83,7 +83,7 @@ queueMicrotask(() => {
widgets(app); widgets(app);
misskeyOS = os; misskeyOS = os;
if (isChromatic()) { if (isChromatic()) {
prefer.set('animation', false); prefer.commit('animation', false);
} }
}); });
}); });

View file

@ -216,6 +216,14 @@ onUnmounted(() => {
.content { .content {
--MI-stickyTop: 0px; --MI-stickyTop: 0px;
/*
理屈は知らないけどここでbackgroundを設定しておかないと
スクロールコンテナーが少なくともChromeにおいて
main thread scrolling になってしまいパフォーマンスが(多分)落ちる
backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない
*/
background: var(--MI_THEME-panel);
&.omitted { &.omitted {
position: relative; position: relative;
max-height: var(--maxHeight); max-height: var(--maxHeight);

View file

@ -3,16 +3,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->
<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) -->
<script lang="ts"> <script lang="ts">
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js'; import type { MisskeyEntity } from '@/types/date-separated-list.js';
import MkAd from '@/components/global/MkAd.vue'; import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { getDateText } from '@/utility/timeline-date-separate.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
export default defineComponent({ export default defineComponent({
@ -46,15 +48,6 @@ export default defineComponent({
setup(props, { slots, expose }) { setup(props, { slots, expose }) {
const $style = useCssModule(); // 使 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; if (props.items.length === 0) return;
const renderChildrenImpl = (shouldHideAds: boolean) => props.items.map((item, i) => { const renderChildrenImpl = (shouldHideAds: boolean) => props.items.map((item, i) => {

View file

@ -52,6 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </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"> <MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template> <template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template> <template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>

View file

@ -5,33 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="[$style.root, { [$style.isMe]: isMe }]"> <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.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"> <MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
<div v-if="!message.isDeleted" :class="$style.content"> <Mfm
<Mfm v-if="message.text"
v-if="message.text" ref="text"
ref="text" class="_selectable"
class="_selectable" :text="message.text"
:text="message.text" :i="$i"
:i="$i" :nyaize="'respect'"
:nyaize="'respect'" :enableEmojiMenu="true"
:enableEmojiMenu="true" :enableEmojiMenuReaction="true"
:enableEmojiMenuReaction="true" />
/> <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
</div>
<div v-else :class="$style.content">
<p>{{ i18n.ts.deleted }}</p>
</div>
</MkFukidashi> </MkFukidashi>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
<div :class="$style.footer"> <div :class="$style.footer">
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button> <button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
<MkTime :class="$style.time" :time="message.createdAt"/> <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 && 'toRoom' in message && message.toRoom != null" :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 && 'toUser' in message && message.toUser != null && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
</div> </div>
<TransitionGroup <TransitionGroup
:enterActiveClass="prefer.s.animation ? $style.transition_reaction_enterActive : ''" :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 { url } from '@@/js/config.js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { NormalizedChatMessage } from './room.vue';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
@ -76,11 +72,12 @@ import * as sound from '@/utility/sound.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
const $i = ensureSignin(); const $i = ensureSignin();
const props = defineProps<{ const props = defineProps<{
message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage; message: NormalizedChatMessage | Misskey.entities.ChatMessage;
isSearchResult?: boolean; 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)) : []); const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
provide(DI.mfmEmojiReactCallback, (reaction) => { provide(DI.mfmEmojiReactCallback, (reaction) => {
if (!$i.policies.canChat) return;
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
misskeyApi('chat/messages/react', { misskeyApi('chat/messages/react', {
messageId: props.message.id, messageId: props.message.id,
@ -96,7 +95,12 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
}); });
function react(ev: MouseEvent) { 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'); sound.playMisskeySfx('reaction');
misskeyApi('chat/messages/react', { misskeyApi('chat/messages/react', {
messageId: props.message.id, messageId: props.message.id,
@ -106,6 +110,8 @@ function react(ev: MouseEvent) {
} }
function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) { function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
if (!$i.policies.canChat) return;
if (record.user.id === $i.id) { if (record.user.id === $i.id) {
misskeyApi('chat/messages/unreact', { misskeyApi('chat/messages/unreact', {
messageId: props.message.id, messageId: props.message.id,
@ -132,7 +138,7 @@ function onContextmenu(ev: MouseEvent) {
function showMenu(ev: MouseEvent, contextmenu = false) { function showMenu(ev: MouseEvent, contextmenu = false) {
const menu: MenuItem[] = []; const menu: MenuItem[] = [];
if (!isMe.value) { if (!isMe.value && $i.policies.canChat) {
menu.push({ menu.push({
text: i18n.ts.reaction, text: i18n.ts.reaction,
icon: 'ti ti-mood-plus', icon: 'ti ti-mood-plus',
@ -150,7 +156,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
text: i18n.ts.copyContent, text: i18n.ts.copyContent,
icon: 'ti ti-copy', icon: 'ti ti-copy',
action: () => { action: () => {
copyToClipboard(props.message.text); copyToClipboard(props.message.text ?? '');
}, },
}); });
@ -158,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
type: 'divider', type: 'divider',
}); });
if (isMe.value) { if (isMe.value && $i.policies.canChat) {
menu.push({ menu.push({
text: i18n.ts.delete, text: i18n.ts.delete,
icon: 'ti ti-trash', icon: 'ti ti-trash',
@ -169,14 +175,16 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
}); });
}, },
}); });
} else { }
if (!isMe.value && props.message.fromUser != null) {
menu.push({ menu.push({
text: i18n.ts.reportAbuse, text: i18n.ts.reportAbuse,
icon: 'ti ti-exclamation-circle', icon: 'ti ti-exclamation-circle',
action: () => { action: () => {
const localUrl = `${url}/chat/messages/${props.message.id}`; const localUrl = `${url}/chat/messages/${props.message.id}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: props.message.fromUser, user: props.message.fromUser!,
initialComment: `${localUrl}\n-----\n`, initialComment: `${localUrl}\n-----\n`,
}, { }, {
closed: () => dispose(), closed: () => dispose(),

View file

@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <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 * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import XMessage from './XMessage.vue'; import XMessage from './XMessage.vue';
@ -163,7 +163,7 @@ async function fetchHistory() {
.map(m => ({ .map(m => ({
id: m.id, id: m.id,
message: m, 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, isMe: m.fromUserId === $i.id,
})); }));

View file

@ -35,18 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { ensureSignin } from '@/i.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
const $i = ensureSignin();
const router = useRouter(); const router = useRouter();
const fetching = ref(true); const fetching = ref(true);
@ -55,8 +51,7 @@ const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]);
async function fetchInvitations() { async function fetchInvitations() {
fetching.value = true; fetching.value = true;
const res = await misskeyApi('chat/rooms/invitations/inbox', { const res = await misskeyApi('chat/rooms/invitations/inbox');
});
invitations.value = res; invitations.value = res;

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_gaps"> <div class="_gaps">
<div v-if="memberships.length > 0" class="_gaps_s"> <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>
<div v-if="!fetching && memberships.length == 0" class="_fullinfo"> <div v-if="!fetching && memberships.length == 0" class="_fullinfo">
<div>{{ i18n.ts._chat.noRooms }}</div> <div>{{ i18n.ts._chat.noRooms }}</div>
@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XRoom from './XRoom.vue'; import XRoom from './XRoom.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.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 fetching = ref(true);
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]); const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
@ -36,8 +28,7 @@ const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
async function fetchRooms() { async function fetchRooms() {
fetching.value = true; fetching.value = true;
const res = await misskeyApi('chat/rooms/joining', { const res = await misskeyApi('chat/rooms/joining');
});
memberships.value = res; memberships.value = res;

View file

@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XRoom from './XRoom.vue'; import XRoom from './XRoom.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.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 fetching = ref(true);
const rooms = ref<Misskey.entities.ChatRoom[]>([]); const rooms = ref<Misskey.entities.ChatRoom[]>([]);

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { computed, ref } from 'vue';
import XHome from './home.home.vue'; import XHome from './home.home.vue';
import XInvitations from './home.invitations.vue'; import XInvitations from './home.invitations.vue';
import XJoiningRooms from './home.joiningRooms.vue'; import XJoiningRooms from './home.joiningRooms.vue';

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<PageWithHeader> <PageWithHeader>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div v-if="initializing"> <div v-if="initializing || message == null">
<MkLoading/> <MkLoading/>
</div> </div>
<div v-else> <div v-else>
@ -17,23 +17,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <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 * as Misskey from 'misskey-js';
import XMessage from './XMessage.vue'; import XMessage from './XMessage.vue';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const props = defineProps<{ const props = defineProps<{
messageId?: string; messageId?: string;
}>(); }>();
const initializing = ref(true); const initializing = ref(true);
const message = ref<Misskey.entities.ChatMessage>(); const message = ref<Misskey.entities.ChatMessage | null>();
async function initialize() { async function initialize() {
initializing.value = true; initializing.value = true;

View file

@ -34,14 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <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 * as Misskey from 'misskey-js';
//import insertTextAtCursor from 'insert-text-at-cursor'; //import insertTextAtCursor from 'insert-text-at-cursor';
import { throttle } from 'throttle-debounce';
import { formatTimeString } from '@/utility/format-time-string.js'; import { formatTimeString } from '@/utility/format-time-string.js';
import { selectFile } from '@/utility/select-file.js'; import { selectFile } from '@/utility/select-file.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { uploadFile } from '@/utility/upload.js'; import { uploadFile } from '@/utility/upload.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
@ -62,6 +60,7 @@ const text = ref<string>('');
const file = ref<Misskey.entities.DriveFile | null>(null); const file = ref<Misskey.entities.DriveFile | null>(null);
const sending = ref(false); const sending = ref(false);
const textareaReadOnly = ref(false); const textareaReadOnly = ref(false);
let autocompleteInstance: Autocomplete | null = null;
const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null); const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null);
@ -171,7 +170,9 @@ function chooseFile(ev: MouseEvent) {
} }
function onChangeFile() { 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) { function upload(fileToUpload: File, name?: string) {
@ -270,8 +271,9 @@ async function insertEmoji(ev: MouseEvent) {
} }
onMounted(() => { onMounted(() => {
// TODO: detach when unmount if (textareaEl.value != null) {
new Autocomplete(textareaEl.value, text); autocompleteInstance = new Autocomplete(textareaEl.value, text);
}
// 稿 // 稿
const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()]; const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()];
@ -280,6 +282,13 @@ onMounted(() => {
file.value = draft.data.file; file.value = draft.data.file;
} }
}); });
onBeforeUnmount(() => {
if (autocompleteInstance) {
autocompleteInstance.detach();
autocompleteInstance = null;
}
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -26,11 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -73,7 +72,7 @@ async function del() {
router.push('/chat'); router.push('/chat');
} }
const isMuted = ref(props.room.isMuted); const isMuted = ref(props.room.isMuted ?? false);
watch(isMuted, async () => { watch(isMuted, async () => {
await os.apiWithDialog('chat/rooms/mute', { await os.apiWithDialog('chat/rooms/mute', {

View file

@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr v-if="memberships.length > 0"> <hr v-if="memberships.length > 0">
<div v-for="membership in memberships" :key="membership.id" :class="$style.membership"> <div v-for="membership in memberships" :key="membership.id" :class="$style.membership">
<MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`"> <MkA :class="$style.membershipBody" :to="`${userPage(membership.user!)}`">
<MkUserCardMini :user="membership.user"/> <MkUserCardMini :user="membership.user!"/>
</MkA> </MkA>
</div> </div>
@ -39,7 +39,6 @@ import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import * as os from '@/os.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';

View file

@ -33,14 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XMessage from './XMessage.vue'; import XMessage from './XMessage.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import * as os from '@/os.js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';

View file

@ -38,7 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="prefer.s.animation ? $style.transition_x_move : ''" :moveClass="prefer.s.animation ? $style.transition_x_move : ''"
tag="div" class="_gaps" 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> </TransitionGroup>
</div> </div>
@ -79,15 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <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 * as Misskey from 'misskey-js';
import { getScrollContainer, isTailVisible } from '@@/js/scroll.js'; import { getScrollContainer } from '@@/js/scroll.js';
import XMessage from './XMessage.vue'; import XMessage from './XMessage.vue';
import XForm from './room.form.vue'; import XForm from './room.form.vue';
import XSearch from './room.search.vue'; import XSearch from './room.search.vue';
import XMembers from './room.members.vue'; import XMembers from './room.members.vue';
import XInfo from './room.info.vue'; import XInfo from './room.info.vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
@ -100,6 +108,7 @@ import MkButton from '@/components/MkButton.vue';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import { useMutationObserver } from '@/use/use-mutation-observer.js'; import { useMutationObserver } from '@/use/use-mutation-observer.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { makeDateSeparatedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
const $i = ensureSignin(); const $i = ensureSignin();
const router = useRouter(); const router = useRouter();
@ -109,15 +118,23 @@ const props = defineProps<{
roomId?: string; 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 initializing = ref(true);
const moreFetching = ref(false); const moreFetching = ref(false);
const messages = ref<Misskey.entities.ChatMessage[]>([]); const messages = ref<NormalizedChatMessage[]>([]);
const canFetchMore = ref(false); const canFetchMore = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null); const user = ref<Misskey.entities.UserDetailed | null>(null);
const room = ref<Misskey.entities.ChatRoom | 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 showIndicator = ref(false);
const timelineEl = useTemplateRef('timelineEl'); const timelineEl = useTemplateRef('timelineEl');
const timeline = makeDateSeparatedTimelineComputedRef(messages);
const SCROLL_HEAD_THRESHOLD = 200; const SCROLL_HEAD_THRESHOLD = 200;
@ -138,18 +155,14 @@ useMutationObserver(timelineEl, {
} }
}); });
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) { function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage): NormalizedChatMessage {
const reactions = [...message.reactions];
for (const record of reactions) {
if (room.value == null && record.user == null) { // 1on1user
record.user = message.fromUserId === $i.id ? user.value : $i;
}
}
return { return {
...message, ...message,
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user), fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user.value!),
reactions, 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 }), misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
]); ]);
room.value = r; room.value = r as Misskey.entities.ChatRoomsShowResponse;
messages.value = m.map(x => normalizeMessage(x)); messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x));
if (messages.value.length === LIMIT) { if (messages.value.length === LIMIT) {
canFetchMore.value = true; canFetchMore.value = true;
@ -221,11 +234,11 @@ async function fetchMore() {
moreFetching.value = true; moreFetching.value = true;
const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', { const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', {
userId: user.value.id, userId: user.value!.id,
limit: LIMIT, limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id, untilId: messages.value[messages.value.length - 1].id,
}) : await misskeyApi('chat/messages/room-timeline', { }) : await misskeyApi('chat/messages/room-timeline', {
roomId: room.value.id, roomId: room.value!.id,
limit: LIMIT, limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id, untilId: messages.value[messages.value.length - 1].id,
}); });
@ -236,7 +249,7 @@ async function fetchMore() {
moreFetching.value = false; moreFetching.value = false;
} }
function onMessage(message: Misskey.entities.ChatMessage) { function onMessage(message: Misskey.entities.ChatMessageLite) {
sound.playMisskeySfx('chatMessage'); sound.playMisskeySfx('chatMessage');
messages.value.unshift(normalizeMessage(message)); 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); const index = messages.value.findIndex(m => m.id === id);
if (index !== -1) { if (index !== -1) {
messages.value.splice(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); const message = messages.value.find(m => m.id === ctx.messageId);
if (message) { if (message) {
if (room.value == null) { // 1on1user if (room.value == null) { // 1on1user
message.reactions.push({ message.reactions.push({
reaction: ctx.reaction, reaction: ctx.reaction,
user: message.fromUserId === $i.id ? user : $i, user: message.fromUserId === $i.id ? user.value! : $i,
}); });
} else { } else {
message.reactions.push({ message.reactions.push({
reaction: ctx.reaction, 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); const message = messages.value.find(m => m.id === ctx.messageId);
if (message) { 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) { if (index !== -1) {
message.reactions.splice(index, 1); message.reactions.splice(index, 1);
} }
@ -310,14 +323,18 @@ onBeforeUnmount(() => {
}); });
async function inviteUser() { async function inviteUser() {
if (room.value == null) return;
const invitee = await os.selectUser({ includeSelf: false, localOnly: true }); const invitee = await os.selectUser({ includeSelf: false, localOnly: true });
os.apiWithDialog('chat/rooms/invitations/create', { os.apiWithDialog('chat/rooms/invitations/create', {
roomId: room.value?.id, roomId: room.value.id,
userId: invitee.id, userId: invitee.id,
}); });
} }
async function leaveRoom() { async function leaveRoom() {
if (room.value == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.ts.areYouSure, text: i18n.ts.areYouSure,
@ -325,7 +342,7 @@ async function leaveRoom() {
if (canceled) return; if (canceled) return;
misskeyApi('chat/rooms/leave', { misskeyApi('chat/rooms/leave', {
roomId: room.value?.id, roomId: room.value.id,
}); });
router.push('/chat'); router.push('/chat');
} }
@ -384,19 +401,36 @@ const headerTabs = computed(() => room.value ? [{
icon: 'ti ti-search', icon: 'ti ti-search',
}]); }]);
const headerActions = computed(() => [{ const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-dots', icon: 'ti ti-dots',
text: '',
handler: showMenu, handler: showMenu,
}]); }]);
definePage(computed(() => !initializing.value ? user.value ? { definePage(computed(() => {
userName: user, if (!initializing.value) {
title: user.value.name ?? user.value.username, if (user.value) {
avatar: user, return {
} : { userName: user.value,
title: room.value?.name, title: user.value.name ?? user.value.username,
icon: 'ti ti-users', avatar: user.value,
} : null)); };
} 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> </script>
<style lang="scss" module> <style lang="scss" module>
@ -464,4 +498,18 @@ definePage(computed(() => !initializing.value ? user.value ? {
transition: opacity 0.5s; transition: opacity 0.5s;
opacity: 0; 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> </style>

View file

@ -4,9 +4,9 @@
*/ */
import { onUnmounted, watch } from 'vue'; 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); const observer = new MutationObserver(callback);
watch(targetNodeRef, (targetNode) => { watch(targetNodeRef, (targetNode) => {

View file

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

View file

@ -853,6 +853,54 @@ export type Channels = {
claimTimeIsUp: null | Record<string, never>; 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) // @public (undocumented)
@ -999,6 +1047,12 @@ type ChatMessage = components['schemas']['ChatMessage'];
// @public (undocumented) // @public (undocumented)
type ChatMessageLite = components['schemas']['ChatMessageLite']; type ChatMessageLite = components['schemas']['ChatMessageLite'];
// @public (undocumented)
type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
// @public (undocumented)
type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
// @public (undocumented) // @public (undocumented)
type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json']; type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json'];
@ -2152,6 +2206,8 @@ declare namespace entities {
AbuseReportNotificationRecipient, AbuseReportNotificationRecipient,
ChatMessage, ChatMessage,
ChatMessageLite, ChatMessageLite,
ChatMessageLiteFor1on1,
ChatMessageLiteForRoom,
ChatRoom, ChatRoom,
ChatRoomInvitation, ChatRoomInvitation,
ChatRoomMembership 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/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.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:234: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: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) // (No @packageDocumentation comment for this package)

View file

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.4.0-beta.1", "version": "2025.4.0-rc.0",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View file

@ -56,6 +56,8 @@ export type SystemWebhook = components['schemas']['SystemWebhook'];
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
export type ChatMessage = components['schemas']['ChatMessage']; export type ChatMessage = components['schemas']['ChatMessage'];
export type ChatMessageLite = components['schemas']['ChatMessageLite']; 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 ChatRoom = components['schemas']['ChatRoom'];
export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation']; export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation'];
export type ChatRoomMembership = components['schemas']['ChatRoomMembership']; export type ChatRoomMembership = components['schemas']['ChatRoomMembership'];

View file

@ -5405,6 +5405,8 @@ export type components = {
/** @example false */ /** @example false */
asBadge: boolean; asBadge: boolean;
/** @example false */ /** @example false */
preserveAssignmentOnMoveAccount: boolean;
/** @example false */
canEditMembersByModerator: boolean; canEditMembersByModerator: boolean;
policies: { policies: {
[key: string]: { [key: string]: {
@ -5693,10 +5695,10 @@ export type components = {
fileId?: string | null; fileId?: string | null;
file?: components['schemas']['DriveFile'] | null; file?: components['schemas']['DriveFile'] | null;
isRead?: boolean; isRead?: boolean;
reactions: ({ reactions: {
reaction: string; reaction: string;
user?: components['schemas']['UserLite'] | null; user: components['schemas']['UserLite'];
})[]; }[];
}; };
ChatMessageLite: { ChatMessageLite: {
id: string; id: string;
@ -5714,6 +5716,34 @@ export type components = {
user?: components['schemas']['UserLite'] | null; 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: { ChatRoom: {
id: string; id: string;
/** Format: date-time */ /** Format: date-time */
@ -9983,6 +10013,7 @@ export type operations = {
/** @default false */ /** @default false */
isExplorable?: boolean; isExplorable?: boolean;
asBadge: boolean; asBadge: boolean;
preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator: boolean; canEditMembersByModerator: boolean;
displayOrder: number; displayOrder: number;
policies: Record<string, never>; policies: Record<string, never>;
@ -10258,6 +10289,7 @@ export type operations = {
isAdministrator?: boolean; isAdministrator?: boolean;
isExplorable?: boolean; isExplorable?: boolean;
asBadge?: boolean; asBadge?: boolean;
preserveAssignmentOnMoveAccount?: boolean;
canEditMembersByModerator?: boolean; canEditMembersByModerator?: boolean;
displayOrder?: number; displayOrder?: number;
policies?: Record<string, never>; policies?: Record<string, never>;
@ -15108,7 +15140,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ChatMessageLite']; 'application/json': components['schemas']['ChatMessageLiteForRoom'];
}; };
}; };
/** @description Client error */ /** @description Client error */
@ -15171,7 +15203,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ChatMessageLite']; 'application/json': components['schemas']['ChatMessageLiteFor1on1'];
}; };
}; };
/** @description Client error */ /** @description Client error */
@ -15346,7 +15378,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ChatMessageLite'][]; 'application/json': components['schemas']['ChatMessageLiteForRoom'][];
}; };
}; };
/** @description Client error */ /** @description Client error */
@ -15574,7 +15606,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': components['schemas']['ChatMessageLite'][]; 'application/json': components['schemas']['ChatMessageLiteFor1on1'][];
}; };
}; };
/** @description Client error */ /** @description Client error */

View file

@ -1,6 +1,7 @@
import { import {
Antenna, Antenna,
ChatMessage, ChatMessage,
ChatMessageLite,
DriveFile, DriveFile,
DriveFolder, DriveFolder,
Note, Note,
@ -243,7 +244,55 @@ export type Channels = {
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>; updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
claimTimeIsUp: null | Record<string, never>; 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'] } & ({ export type NoteUpdatedEvent = { id: Note['id'] } & ({