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)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
- Enhance: フロントエンドのエラートラッキングができるように
@ -69,6 +71,7 @@
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
- Fix: 非ログインでタイムラインのストリームに接続した際、表示にログイン必須のノートが流れる場合がある問題を修正
## 2025.3.1

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

8
locales/index.d.ts vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "sharkey",
"version": "2025.4.0-beta.1",
"version": "2025.4.0-rc.0",
"codename": "shonk",
"repository": {
"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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class AccountMoveService {
@ -64,6 +65,7 @@ export class AccountMoveService {
private relayService: RelayService,
private queueService: QueueService,
private systemAccountService: SystemAccountService,
private roleService: RoleService,
) {
}
@ -123,6 +125,7 @@ export class AccountMoveService {
this.copyBlocking(src, dst),
this.copyMutings(src, dst),
this.deleteScheduledNotes(src),
this.copyRoles(src, dst),
this.updateLists(src, dst),
]);
} catch {
@ -220,6 +223,32 @@ export class AccountMoveService {
});
}
@bindThis
public async copyRoles(src: ThinUser, dst: ThinUser): Promise<void> {
// Insert new roles with the same values except userId
// role service may have cache for roles so retrieve roles from service
const [oldRoleAssignments, roles] = await Promise.all([
this.roleService.getUserAssigns(src.id),
this.roleService.getRoles(),
]);
if (oldRoleAssignments.length === 0) return;
// No promise all since the only async operation is writing to the database
for (const oldRoleAssignment of oldRoleAssignments) {
const role = roles.find(x => x.id === oldRoleAssignment.roleId);
if (role == null) continue; // Very unlikely however removing role may cause this case
if (!role.preserveAssignmentOnMoveAccount) continue;
try {
await this.roleService.assign(dst.id, role.id, oldRoleAssignment.expiresAt);
} catch (e) {
if (e instanceof RoleService.AlreadyAssignedError) continue;
throw e;
}
}
}
/**
* Update lists while moving accounts.
* - No removal of the old account from the lists

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -72,7 +72,7 @@ export const packedChatMessageSchema = {
},
user: {
type: 'object',
optional: true, nullable: true,
optional: false, nullable: false,
ref: 'UserLite',
},
},
@ -144,3 +144,113 @@ export const packedChatMessageLiteSchema = {
},
},
} as const;
export const packedChatMessageLiteFor1on1Schema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
fromUserId: {
type: 'string',
optional: false, nullable: false,
},
toUserId: {
type: 'string',
optional: false, nullable: false,
},
text: {
type: 'string',
optional: true, nullable: true,
},
fileId: {
type: 'string',
optional: true, nullable: true,
},
file: {
type: 'object',
optional: true, nullable: true,
ref: 'DriveFile',
},
reactions: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
reaction: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
},
} as const;
export const packedChatMessageLiteForRoomSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
fromUserId: {
type: 'string',
optional: false, nullable: false,
},
fromUser: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
toRoomId: {
type: 'string',
optional: false, nullable: false,
},
text: {
type: 'string',
optional: true, nullable: true,
},
fileId: {
type: 'string',
optional: true, nullable: true,
},
file: {
type: 'object',
optional: true, nullable: true,
ref: 'DriveFile',
},
reactions: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
reaction: {
type: 'string',
optional: false, nullable: false,
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
},
},
},
},
} as const;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ export const meta = {
items: {
type: 'object',
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.user.host == null) return;
if (!this.instance.bubbleInstances.includes(note.user.host)) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;

View file

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

View file

@ -56,6 +56,7 @@ class LocalTimelineChannel extends Channel {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
// 関係ない返信は除外
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import XMessage from './XMessage.vue';
@ -163,7 +163,7 @@ async function fetchHistory() {
.map(m => ({
id: m.id,
message: m,
other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
isMe: m.fromUserId === $i.id,
}));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,7 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
tag="div" class="_gaps"
>
<XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message"/>
<template v-for="item in timeline.toReversed()" :key="item.id">
<XMessage v-if="item.type === 'item'" :message="item.data"/>
<div v-else-if="item.type === 'date'" :class="$style.dateDivider">
<span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
<span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
</div>
</template>
</TransitionGroup>
</div>
@ -79,15 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
import * as Misskey from 'misskey-js';
import { getScrollContainer, isTailVisible } from '@@/js/scroll.js';
import { getScrollContainer } from '@@/js/scroll.js';
import XMessage from './XMessage.vue';
import XForm from './room.form.vue';
import XSearch from './room.search.vue';
import XMembers from './room.members.vue';
import XInfo from './room.info.vue';
import type { MenuItem } from '@/types/menu.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js';
@ -100,6 +108,7 @@ import MkButton from '@/components/MkButton.vue';
import { useRouter } from '@/router.js';
import { useMutationObserver } from '@/use/use-mutation-observer.js';
import MkInfo from '@/components/MkInfo.vue';
import { makeDateSeparatedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
const $i = ensureSignin();
const router = useRouter();
@ -109,15 +118,23 @@ const props = defineProps<{
roomId?: string;
}>();
export type NormalizedChatMessage = Omit<Misskey.entities.ChatMessageLite, 'fromUser' | 'reactions'> & {
fromUser: Misskey.entities.UserLite;
reactions: (Misskey.entities.ChatMessageLite['reactions'][number] & {
user: Misskey.entities.UserLite;
})[];
};
const initializing = ref(true);
const moreFetching = ref(false);
const messages = ref<Misskey.entities.ChatMessage[]>([]);
const messages = ref<NormalizedChatMessage[]>([]);
const canFetchMore = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
const room = ref<Misskey.entities.ChatRoom | null>(null);
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null);
const connection = ref<Misskey.IChannelConnection<Misskey.Channels['chatUser']> | Misskey.IChannelConnection<Misskey.Channels['chatRoom']> | null>(null);
const showIndicator = ref(false);
const timelineEl = useTemplateRef('timelineEl');
const timeline = makeDateSeparatedTimelineComputedRef(messages);
const SCROLL_HEAD_THRESHOLD = 200;
@ -138,18 +155,14 @@ useMutationObserver(timelineEl, {
}
});
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) {
const reactions = [...message.reactions];
for (const record of reactions) {
if (room.value == null && record.user == null) { // 1on1user
record.user = message.fromUserId === $i.id ? user.value : $i;
}
}
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage): NormalizedChatMessage {
return {
...message,
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user),
reactions,
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user.value!),
reactions: message.reactions.map(record => ({
...record,
user: record.user ?? (message.fromUserId === $i.id ? user.value! : $i),
})),
};
}
@ -184,8 +197,8 @@ async function initialize() {
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
]);
room.value = r;
messages.value = m.map(x => normalizeMessage(x));
room.value = r as Misskey.entities.ChatRoomsShowResponse;
messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x));
if (messages.value.length === LIMIT) {
canFetchMore.value = true;
@ -221,11 +234,11 @@ async function fetchMore() {
moreFetching.value = true;
const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', {
userId: user.value.id,
userId: user.value!.id,
limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id,
}) : await misskeyApi('chat/messages/room-timeline', {
roomId: room.value.id,
roomId: room.value!.id,
limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id,
});
@ -236,7 +249,7 @@ async function fetchMore() {
moreFetching.value = false;
}
function onMessage(message: Misskey.entities.ChatMessage) {
function onMessage(message: Misskey.entities.ChatMessageLite) {
sound.playMisskeySfx('chatMessage');
messages.value.unshift(normalizeMessage(message));
@ -253,34 +266,34 @@ function onMessage(message: Misskey.entities.ChatMessage) {
}
}
function onDeleted(id) {
function onDeleted(id: string) {
const index = messages.value.findIndex(m => m.id === id);
if (index !== -1) {
messages.value.splice(index, 1);
}
}
function onReact(ctx) {
function onReact(ctx: Parameters<Misskey.Channels['chatUser']['events']['react']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['react']>[0]) {
const message = messages.value.find(m => m.id === ctx.messageId);
if (message) {
if (room.value == null) { // 1on1user
message.reactions.push({
reaction: ctx.reaction,
user: message.fromUserId === $i.id ? user : $i,
user: message.fromUserId === $i.id ? user.value! : $i,
});
} else {
message.reactions.push({
reaction: ctx.reaction,
user: ctx.user,
user: ctx.user!,
});
}
}
}
function onUnreact(ctx) {
function onUnreact(ctx: Parameters<Misskey.Channels['chatUser']['events']['unreact']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['unreact']>[0]) {
const message = messages.value.find(m => m.id === ctx.messageId);
if (message) {
const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id);
const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user!.id);
if (index !== -1) {
message.reactions.splice(index, 1);
}
@ -310,14 +323,18 @@ onBeforeUnmount(() => {
});
async function inviteUser() {
if (room.value == null) return;
const invitee = await os.selectUser({ includeSelf: false, localOnly: true });
os.apiWithDialog('chat/rooms/invitations/create', {
roomId: room.value?.id,
roomId: room.value.id,
userId: invitee.id,
});
}
async function leaveRoom() {
if (room.value == null) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.areYouSure,
@ -325,7 +342,7 @@ async function leaveRoom() {
if (canceled) return;
misskeyApi('chat/rooms/leave', {
roomId: room.value?.id,
roomId: room.value.id,
});
router.push('/chat');
}
@ -384,19 +401,36 @@ const headerTabs = computed(() => room.value ? [{
icon: 'ti ti-search',
}]);
const headerActions = computed(() => [{
const headerActions = computed<PageHeaderItem[]>(() => [{
icon: 'ti ti-dots',
text: '',
handler: showMenu,
}]);
definePage(computed(() => !initializing.value ? user.value ? {
userName: user,
title: user.value.name ?? user.value.username,
avatar: user,
} : {
title: room.value?.name,
icon: 'ti ti-users',
} : null));
definePage(computed(() => {
if (!initializing.value) {
if (user.value) {
return {
userName: user.value,
title: user.value.name ?? user.value.username,
avatar: user.value,
};
} else if (room.value) {
return {
title: room.value.name,
icon: 'ti ti-users',
};
} else {
return {
title: i18n.ts.chat,
};
}
} else {
return {
title: i18n.ts.chat,
};
}
}));
</script>
<style lang="scss" module>
@ -464,4 +498,18 @@ definePage(computed(() => !initializing.value ? user.value ? {
transition: opacity 0.5s;
opacity: 0;
}
.dateDivider {
display: flex;
font-size: 85%;
align-items: center;
justify-content: center;
gap: 0.5em;
opacity: 0.75;
border: solid 0.5px var(--MI_THEME-divider);
border-radius: 999px;
width: fit-content;
padding: 0.5em 1em;
margin: 0 auto;
}
</style>

View file

@ -4,9 +4,9 @@
*/
import { onUnmounted, watch } from 'vue';
import type { Ref, ShallowRef } from 'vue';
import type { Ref } from 'vue';
export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
export function useMutationObserver(targetNodeRef: Ref<HTMLElement | null | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
const observer = new MutationObserver(callback);
watch(targetNodeRef, (targetNode) => {

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>;
};
};
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)
@ -999,6 +1047,12 @@ type ChatMessage = components['schemas']['ChatMessage'];
// @public (undocumented)
type ChatMessageLite = components['schemas']['ChatMessageLite'];
// @public (undocumented)
type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
// @public (undocumented)
type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
// @public (undocumented)
type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json'];
@ -2152,6 +2206,8 @@ declare namespace entities {
AbuseReportNotificationRecipient,
ChatMessage,
ChatMessageLite,
ChatMessageLiteFor1on1,
ChatMessageLiteForRoom,
ChatRoom,
ChatRoomInvitation,
ChatRoomMembership
@ -3853,8 +3909,8 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:233:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:243:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:234:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:244:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

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

View file

@ -56,6 +56,8 @@ export type SystemWebhook = components['schemas']['SystemWebhook'];
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
export type ChatMessage = components['schemas']['ChatMessage'];
export type ChatMessageLite = components['schemas']['ChatMessageLite'];
export type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
export type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
export type ChatRoom = components['schemas']['ChatRoom'];
export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation'];
export type ChatRoomMembership = components['schemas']['ChatRoomMembership'];

View file

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

View file

@ -1,6 +1,7 @@
import {
Antenna,
ChatMessage,
ChatMessageLite,
DriveFile,
DriveFolder,
Note,
@ -243,7 +244,55 @@ export type Channels = {
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
claimTimeIsUp: null | Record<string, never>;
}
}
};
chatUser: {
params: {
otherId: string;
};
events: {
message: (payload: ChatMessageLite) => void;
deleted: (payload: ChatMessageLite['id']) => void;
react: (payload: {
reaction: string;
user?: UserLite;
messageId: ChatMessageLite['id'];
}) => void;
unreact: (payload: {
reaction: string;
user?: UserLite;
messageId: ChatMessageLite['id'];
}) => void;
};
receives: {
read: {
id: ChatMessageLite['id'];
};
};
};
chatRoom: {
params: {
roomId: string;
};
events: {
message: (payload: ChatMessageLite) => void;
deleted: (payload: ChatMessageLite['id']) => void;
react: (payload: {
reaction: string;
user?: UserLite;
messageId: ChatMessageLite['id'];
}) => void;
unreact: (payload: {
reaction: string;
user?: UserLite;
messageId: ChatMessageLite['id'];
}) => void;
};
receives: {
read: {
id: ChatMessageLite['id'];
};
};
};
};
export type NoteUpdatedEvent = { id: Note['id'] } & ({