mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 12:20:21 +01:00
Merge branch 'develop'
This commit is contained in:
commit
929e545514
84 changed files with 962 additions and 851 deletions
4
COPYING
4
COPYING
|
@ -6,10 +6,6 @@ And is distributed under The GNU Affero General Public License Version 3, you sh
|
|||
|
||||
Misskey includes several third-party Open-Source softwares.
|
||||
|
||||
Unicode emoji regular expressions by Twitter, Inc.
|
||||
License: MIT
|
||||
https://github.com/twitter/twemoji-parser/blob/master/LICENSE.md
|
||||
|
||||
Emoji keywords for Unicode 11 and below by Mu-An Chiou
|
||||
License: MIT
|
||||
https://github.com/muan/emojilib/blob/master/LICENSE
|
||||
|
|
|
@ -99,6 +99,11 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
|||
|
||||
To receive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
|
||||
|
||||
Related projects
|
||||
----------------------------------------------------------------
|
||||
- [misskey.js](https://github.com/misskey-dev/misskey.js) - Misskey SDK for JavaScript
|
||||
- [mfm.js](https://github.com/misskey-dev/mfm.js) - MFM parser
|
||||
|
||||
:heart: Backers
|
||||
----------------------------------------------------------------
|
||||
<!-- PATREON_START -->
|
||||
|
|
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Reporting Security Issues
|
||||
|
||||
If you discover a security issue in Misskey, please report it by sending an
|
||||
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
|
||||
|
||||
This will allow us to assess the risk, and make a fix available before we add a
|
||||
bug report to the GitHub repository.
|
||||
|
||||
Thanks for helping make Misskey safe for everyone.
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
@ -4,7 +4,7 @@
|
|||
|
||||
import * as fs from 'fs';
|
||||
import * as gulp from 'gulp';
|
||||
import * as rimraf from 'rimraf';
|
||||
import rimraf from 'rimraf';
|
||||
const replace = require('gulp-replace');
|
||||
const terser = require('gulp-terser');
|
||||
const cssnano = require('gulp-cssnano');
|
||||
|
|
|
@ -259,8 +259,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "الصفحات"
|
||||
integration: "دمج"
|
||||
connectSerice: "أوصل"
|
||||
disconnectSerice: "قطع الاتصال"
|
||||
enableLocalTimeline: "تفعيل الخيط المحلي"
|
||||
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
|
||||
disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دائمًا و من الوصول إلى جميع المخططات الزمنية ، حتى إذا لم يتم تمكينها."
|
||||
|
|
|
@ -269,8 +269,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Stránky"
|
||||
integration: "Integrace"
|
||||
connectSerice: "Připojit"
|
||||
disconnectSerice: "Odpojit"
|
||||
enableLocalTimeline: "Povolit lokální čas"
|
||||
enableGlobalTimeline: "Povolit globální čas"
|
||||
registration: "Registrace"
|
||||
|
|
|
@ -279,6 +279,7 @@ emptyDrive: "Drive ist leer"
|
|||
emptyFolder: "Der Ordner ist leer"
|
||||
unableToDelete: "Nicht löschbar"
|
||||
inputNewFileName: "Gib einen neuen Dateinamen ein"
|
||||
inputNewDescription: "Gib eine neue Beschreibung ein"
|
||||
inputNewFolderName: "Gib einen neuen Ordnernamen ein"
|
||||
circularReferenceFolder: "Der Zielordner ist ein Unterorder des Ordners, den du verschieben möchtest."
|
||||
hasChildFilesOrFolders: "Dieser Ordner kann nicht gelöscht werden, da er nicht leer ist."
|
||||
|
@ -310,8 +311,8 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Seiten"
|
||||
integration: "Integration"
|
||||
connectSerice: "Verbinden"
|
||||
disconnectSerice: "Trennen"
|
||||
connectService: "Verbinden"
|
||||
disconnectService: "Trennen"
|
||||
enableLocalTimeline: "Lokale Chronik aktivieren"
|
||||
enableGlobalTimeline: "Globale Chronik aktivieren"
|
||||
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind."
|
||||
|
@ -546,6 +547,8 @@ disablePlayer: "Video-Player schließen"
|
|||
expandTweet: "Tweet ausklappen"
|
||||
themeEditor: "Farbthemen-Editor"
|
||||
description: "Beschreibung"
|
||||
describeFile: "Beschreibung hinzufügen"
|
||||
enterFileDescription: "Beschreibung eingeben"
|
||||
author: "Autor"
|
||||
leaveConfirm: "Es gibt unspeicherte Änderungen. Möchtest du diese verwerfen?"
|
||||
manage: "Verwaltung"
|
||||
|
|
|
@ -279,6 +279,7 @@ emptyDrive: "The drive is empty"
|
|||
emptyFolder: "This folder is empty"
|
||||
unableToDelete: "Unable to delete"
|
||||
inputNewFileName: "Enter a new filename"
|
||||
inputNewDescription: "Enter new caption"
|
||||
inputNewFolderName: "Enter a new folder name"
|
||||
circularReferenceFolder: "The destination folder is a subfolder of the folder you wish to move."
|
||||
hasChildFilesOrFolders: "Since this folder is not empty, it can not be deleted."
|
||||
|
@ -310,8 +311,8 @@ monthX: "{month}"
|
|||
yearX: "{year} /"
|
||||
pages: "Pages"
|
||||
integration: "Integration"
|
||||
connectSerice: "Connect"
|
||||
disconnectSerice: "Disconnect"
|
||||
connectService: "Connect"
|
||||
disconnectService: "Disconnect"
|
||||
enableLocalTimeline: "Enable local timeline"
|
||||
enableGlobalTimeline: "Enable global timeline"
|
||||
disablingTimelinesInfo: "Admins and Mods will always have access to all timelines, even if they are not enabled."
|
||||
|
@ -546,6 +547,8 @@ disablePlayer: "Close video player"
|
|||
expandTweet: "Expand tweet"
|
||||
themeEditor: "Theme editor"
|
||||
description: "Description"
|
||||
describeFile: "Add caption"
|
||||
enterFileDescription: "Enter caption"
|
||||
author: "Author"
|
||||
leaveConfirm: "There are unsaved changes. Do you want to discard them?"
|
||||
manage: "Management"
|
||||
|
|
|
@ -309,8 +309,6 @@ monthX: "Mes {month}"
|
|||
yearX: "Año {year}"
|
||||
pages: "Páginas"
|
||||
integration: "Integración"
|
||||
connectSerice: "Conectarse"
|
||||
disconnectSerice: "Desconectarse"
|
||||
enableLocalTimeline: "Habilitar linea de tiempo local"
|
||||
enableGlobalTimeline: "Habilitar linea de tiempo global"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos"
|
||||
|
|
|
@ -310,8 +310,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Pages"
|
||||
integration: "Intégrations"
|
||||
connectSerice: "Connecter"
|
||||
disconnectSerice: "Déconnecter"
|
||||
enableLocalTimeline: "Activer le fil local"
|
||||
enableGlobalTimeline: "Activer le fil global"
|
||||
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s et les modérateur·rice·s pourront toujours y accéder."
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
_lang_: "Bahasa Jepang"
|
||||
_lang_: "Bahasa Indonesia"
|
||||
headlineMisskey: "Jaringan terhubung melalui note"
|
||||
introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersifat sumber terbuka.\nMulailah menuliskan catatan, bagikan peristiwa terkini, serta ceritakan segala tentangmu.📡\nTunjukkan juga reaksimu pada catatan pengguna lain.👍\nMari jelajahi dunia baru🚀"
|
||||
monthAndDay: "{day} {month}"
|
||||
|
@ -310,8 +310,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Halaman"
|
||||
integration: "Integrasi"
|
||||
connectSerice: "Sambungkan"
|
||||
disconnectSerice: "Putuskan"
|
||||
enableLocalTimeline: "Nyalakan linimasa lokal"
|
||||
enableGlobalTimeline: "Nyalakan linimasa global"
|
||||
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa meskipun linimasa tersebut tidak diaktifkan."
|
||||
|
@ -977,9 +975,9 @@ _theme:
|
|||
infoFg: "Teks informasi"
|
||||
infoWarnBg: "Latar belakang peringatan"
|
||||
infoWarnFg: "Teks peringatan"
|
||||
cwBg: "Latar belakang tombol CW"
|
||||
cwFg: "Teks tombol CW"
|
||||
cwHoverBg: "Latar belakang tombol CW (Mengambang)"
|
||||
cwBg: "Latar belakang tombol Sembunyikan Konten"
|
||||
cwFg: "Teks tombol Sembunyikan Konten"
|
||||
cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)"
|
||||
toastBg: "Latar belakang pemberitahuan"
|
||||
toastFg: "Teks pemberitahuan"
|
||||
buttonBg: "Latar belakang tombol"
|
||||
|
@ -1122,7 +1120,7 @@ _widgets:
|
|||
aiscript: "Konsol AiScript"
|
||||
_cw:
|
||||
hide: "Sembunyikan"
|
||||
show: "Selebihnya"
|
||||
show: "Lihat konten"
|
||||
chars: "{count} karakter"
|
||||
files: "{count} berkas"
|
||||
_poll:
|
||||
|
@ -1551,7 +1549,7 @@ _pages:
|
|||
fn: "Fungsi"
|
||||
_fn:
|
||||
slots: "Slot"
|
||||
slots-info: "Pisahkan setiap slow dengan baris baru"
|
||||
slots-info: "Pisahkan setiap slot dengan baris baru"
|
||||
arg1: "Keluaran"
|
||||
for: "Ulangi"
|
||||
_for:
|
||||
|
|
|
@ -21,6 +21,7 @@ const languages = [
|
|||
'en-US',
|
||||
'es-ES',
|
||||
'fr-FR',
|
||||
'id-ID',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'kab-KAB',
|
||||
|
|
|
@ -305,8 +305,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Pagine"
|
||||
integration: "App collegate"
|
||||
connectSerice: "Connetti"
|
||||
disconnectSerice: "Disconnetti"
|
||||
enableLocalTimeline: "Abilita Timeline locale"
|
||||
enableGlobalTimeline: "Abilita Timeline federata"
|
||||
disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori e i moderatori potranno sempre accederci."
|
||||
|
|
|
@ -279,6 +279,7 @@ emptyDrive: "ドライブは空です"
|
|||
emptyFolder: "フォルダーは空です"
|
||||
unableToDelete: "削除できません"
|
||||
inputNewFileName: "新しいファイル名を入力してください"
|
||||
inputNewDescription: "新しいキャプションを入力してください"
|
||||
inputNewFolderName: "新しいフォルダ名を入力してください"
|
||||
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
|
||||
hasChildFilesOrFolders: "このフォルダは空でないため、削除できません。"
|
||||
|
@ -310,8 +311,8 @@ monthX: "{month}月"
|
|||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
connectSerice: "接続する"
|
||||
disconnectSerice: "切断する"
|
||||
connectService: "接続する"
|
||||
disconnectService: "切断する"
|
||||
enableLocalTimeline: "ローカルタイムラインを有効にする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを有効にする"
|
||||
disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。"
|
||||
|
@ -546,6 +547,8 @@ disablePlayer: "プレイヤーを閉じる"
|
|||
expandTweet: "ツイートを展開する"
|
||||
themeEditor: "テーマエディター"
|
||||
description: "説明"
|
||||
describeFile: "キャプションを付ける"
|
||||
enterFileDescription: "キャプションを入力"
|
||||
author: "作者"
|
||||
leaveConfirm: "未保存の変更があります。破棄しますか?"
|
||||
manage: "管理"
|
||||
|
|
|
@ -308,8 +308,6 @@ monthX: "{month}月"
|
|||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
connectSerice: "つなぐ"
|
||||
disconnectSerice: "切ってまう"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||
|
|
|
@ -279,6 +279,7 @@ emptyDrive: "드라이브가 비어 있습니다"
|
|||
emptyFolder: "폴더가 비어 있습니다"
|
||||
unableToDelete: "삭제할 수 없습니다"
|
||||
inputNewFileName: "바꿀 파일명을 입력해 주세요"
|
||||
inputNewDescription: "새 캡션을 입력해 주세요"
|
||||
inputNewFolderName: "바꿀 폴더명을 입력해 주세요"
|
||||
circularReferenceFolder: "지정한 폴더가 이동할 폴더의 하위 폴더입니다."
|
||||
hasChildFilesOrFolders: "이 폴더는 비어있지 않기 때문에 삭제할 수 없습니다."
|
||||
|
@ -310,8 +311,8 @@ monthX: "{month}월"
|
|||
yearX: "{year}년"
|
||||
pages: "페이지"
|
||||
integration: "연동"
|
||||
connectSerice: "접속"
|
||||
disconnectSerice: "연결 끊기"
|
||||
connectService: "계정 연동"
|
||||
disconnectService: "계정 연동 해제"
|
||||
enableLocalTimeline: "로컬 타임라인 활성화"
|
||||
enableGlobalTimeline: "글로벌 타임라인 활성화"
|
||||
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
|
||||
|
@ -546,6 +547,8 @@ disablePlayer: "플레이어 닫기"
|
|||
expandTweet: "트윗 확장하기"
|
||||
themeEditor: "테마 에디터"
|
||||
description: "설명"
|
||||
describeFile: "캡션 추가"
|
||||
enterFileDescription: "캡션 입력"
|
||||
author: "작성자"
|
||||
leaveConfirm: "저장하지 않은 변경사항이 있습니다. 취소하시겠습니까?"
|
||||
manage: "관리"
|
||||
|
|
|
@ -135,6 +135,7 @@ settingGuide: "Proponowana konfiguracja"
|
|||
cacheRemoteFiles: "Przechowuj zdalne pliki w pamięci podręcznej"
|
||||
cacheRemoteFilesDescription: "Gdy ta opcja jest wyłączona, zdalne pliki są ładowane bezpośrednio ze zdalnych instancji. Wyłączenie the opcji zmniejszy użycie powierzchni dyskowej, ale zwiększy transfer, ponieważ miniaturki nie będą generowane."
|
||||
flagAsBot: "To konto jest botem"
|
||||
flagAsBotDescription: "Jeżeli ten kanał jest kontrolowany przez jakiś program, ustaw tę opcję. Jeżeli włączona, będzie działać jako flaga informująca innych programistów, aby zapobiegać nieskończonej interakcji z różnymi botami i dostosowywać wewnętrzne systemy Misskey, traktując konto jako bota."
|
||||
flagAsCat: "To konto jest kotem"
|
||||
flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot."
|
||||
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
||||
|
@ -182,6 +183,7 @@ clearQueueConfirmTitle: "Czy na pewno chcesz wyczyścić kolejkę?"
|
|||
clearCachedFiles: "Wyczyść pamięć podręczną"
|
||||
clearCachedFilesConfirm: "Czy na pewno chcesz usunąć wszystkie zdalne pliki z pamięci podręcznej?"
|
||||
blockedInstances: "Zablokowane instancje"
|
||||
blockedInstancesDescription: "Wypisz nazwy hostów instancji, które powinny zostać zablokowane. Wypisane instancje nie będą mogły dłużej komunikować się z tą instancją."
|
||||
muteAndBlock: "Wycisz / Zablokuj"
|
||||
mutedUsers: "Wyciszeni użytkownicy"
|
||||
blockedUsers: "Zablokowani użytkownicy"
|
||||
|
@ -274,6 +276,7 @@ emptyDrive: "Dysk jest pusty"
|
|||
emptyFolder: "Ten katalog jest pusty"
|
||||
unableToDelete: "Nie można usunąć"
|
||||
inputNewFileName: "Wprowadź nową nazwę pliku"
|
||||
inputNewDescription: "Proszę wpisać nowy napis"
|
||||
inputNewFolderName: "Wprowadź nową nazwę katalogu"
|
||||
circularReferenceFolder: "Katalog docelowy jest podkatalogiem katalogu, który chcesz przenieść."
|
||||
hasChildFilesOrFolders: "Ponieważ ten katalog nie jest pusty, nie może być usunięty."
|
||||
|
@ -305,8 +308,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Strony"
|
||||
integration: "Integracja"
|
||||
connectSerice: "Połącz"
|
||||
disconnectSerice: "Rozłącz"
|
||||
enableLocalTimeline: "Włącz lokalną oś czasu"
|
||||
enableGlobalTimeline: "Włącz globalną oś czasu"
|
||||
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do wszystkich osi czasu, nawet gdy są one wyłączone."
|
||||
|
@ -532,6 +533,8 @@ disablePlayer: "Zamknij odtwarzacz wideo"
|
|||
expandTweet: "Rozwiń tweet"
|
||||
themeEditor: "Edytor motywu"
|
||||
description: "Opis"
|
||||
describeFile: "dodaj podpis"
|
||||
enterFileDescription: "Wprowadź napis"
|
||||
author: "Autor"
|
||||
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
||||
manage: "Zarządzanie"
|
||||
|
|
|
@ -309,8 +309,6 @@ monthX: "{month} месяц"
|
|||
yearX: "{year} год"
|
||||
pages: "Страницы"
|
||||
integration: "Интеграция"
|
||||
connectSerice: "Соединение"
|
||||
disconnectSerice: "Отключение"
|
||||
enableLocalTimeline: "Включить локальную ленту"
|
||||
enableGlobalTimeline: "Включить глобальную ленту"
|
||||
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, даже если они отключены."
|
||||
|
|
|
@ -307,8 +307,6 @@ monthX: "{month}"
|
|||
yearX: "{year}"
|
||||
pages: "Сторінки"
|
||||
integration: "Інтеграція"
|
||||
connectSerice: "Під’єднати"
|
||||
disconnectSerice: "Відключитися"
|
||||
enableLocalTimeline: "Увімкнути локальну стрічку"
|
||||
enableGlobalTimeline: "Увімкнути глобальну стрічку"
|
||||
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті."
|
||||
|
|
|
@ -279,6 +279,7 @@ emptyDrive: "驱动器为空"
|
|||
emptyFolder: "空文件夹"
|
||||
unableToDelete: "无法删除"
|
||||
inputNewFileName: "请输入新文件名"
|
||||
inputNewDescription: "请输入新标题"
|
||||
inputNewFolderName: "请输入新文件名"
|
||||
circularReferenceFolder: "目标文件夹是您要移动的文件夹的子文件夹。"
|
||||
hasChildFilesOrFolders: "此文件夹不为空,无法删除。"
|
||||
|
@ -310,8 +311,8 @@ monthX: "{month}月"
|
|||
yearX: "{year}年"
|
||||
pages: "页面"
|
||||
integration: "关联"
|
||||
connectSerice: "连接"
|
||||
disconnectSerice: "断开连接"
|
||||
connectService: "连接"
|
||||
disconnectService: "断开连接"
|
||||
enableLocalTimeline: "启用本地时间线功能"
|
||||
enableGlobalTimeline: "启用全局时间线"
|
||||
disablingTimelinesInfo: "即使时间线功能被禁用,出于便利性的原因,管理员和数据图表也可以继续使用。"
|
||||
|
@ -546,6 +547,8 @@ disablePlayer: "关闭播放器"
|
|||
expandTweet: "展开贴文"
|
||||
themeEditor: "主题编辑器"
|
||||
description: "描述"
|
||||
describeFile: "添加标题"
|
||||
enterFileDescription: "输入标题"
|
||||
author: "作者"
|
||||
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
|
||||
manage: "管理"
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
---
|
||||
_lang_: "繁體中文"
|
||||
headlineMisskey: "貼文連繫網絡"
|
||||
introMisskey: "歡迎! Misskey是一個開源且去中心化的社群網絡。\n通過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧!🚀"
|
||||
headlineMisskey: "貼文連繫網路"
|
||||
introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧!🚀"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜尋"
|
||||
notifications: "通知"
|
||||
username: "使用者名稱"
|
||||
password: "密碼"
|
||||
forgotPassword: "忘記密碼"
|
||||
fetchingAsApObject: "從聯邦宇宙取得中..."
|
||||
ok: "OK"
|
||||
gotIt: "知道了"
|
||||
cancel: "取消"
|
||||
enterUsername: "輸入使用者名稱"
|
||||
renotedBy: "{user} 轉發了"
|
||||
renotedBy: "{user} 轉傳了"
|
||||
noNotes: "貼文不可用。"
|
||||
noNotifications: "沒有通知"
|
||||
instance: "實例"
|
||||
|
@ -92,9 +93,9 @@ followRequestPending: "追隨許可批准中"
|
|||
enterEmoji: "輸入表情符號"
|
||||
renote: "轉發"
|
||||
unrenote: "取消轉發"
|
||||
renoted: "轉發成功"
|
||||
renoted: "轉傳成功"
|
||||
cantRenote: "無法轉發此貼文。"
|
||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
|
||||
quote: "引用"
|
||||
pinnedNote: "已置頂的貼文"
|
||||
pinned: "置頂"
|
||||
|
@ -309,8 +310,6 @@ monthX: "{month}月"
|
|||
yearX: "{year}年"
|
||||
pages: "頁面"
|
||||
integration: "整合"
|
||||
connectSerice: "連線"
|
||||
disconnectSerice: "中斷連線"
|
||||
enableLocalTimeline: "開啟本地時間軸"
|
||||
enableGlobalTimeline: "啟用公開時間軸"
|
||||
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
|
||||
|
@ -733,6 +732,7 @@ noBotProtectionWarning: "尚未設定Bot防護。"
|
|||
configure: "設定"
|
||||
expiration: "期限"
|
||||
middle: "中"
|
||||
emailNotConfiguredWarning: "沒有設定電子郵件地址"
|
||||
_ad:
|
||||
back: "返回"
|
||||
_gallery:
|
||||
|
|
63
package.json
63
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.81.2",
|
||||
"version": "12.82.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -30,6 +30,7 @@
|
|||
"format": "gulp format"
|
||||
},
|
||||
"resolutions": {
|
||||
"mfm-js/twemoji-parser": "13.1.x",
|
||||
"chokidar": "^3.3.1",
|
||||
"constantinople": "^4.0.1",
|
||||
"jsonld/rdf-canonize/node-forge": "0.10.0",
|
||||
|
@ -43,7 +44,7 @@
|
|||
"@koa/router": "9.0.1",
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/tracing": "5.29.2",
|
||||
"@sinonjs/fake-timers": "7.0.5",
|
||||
"@sinonjs/fake-timers": "7.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.1",
|
||||
|
@ -58,23 +59,23 @@
|
|||
"@types/jsdom": "16.2.10",
|
||||
"@types/jsonld": "1.5.5",
|
||||
"@types/katex": "0.11.0",
|
||||
"@types/koa": "2.13.1",
|
||||
"@types/koa": "2.13.3",
|
||||
"@types/koa-bodyparser": "4.3.0",
|
||||
"@types/koa-cors": "0.0.0",
|
||||
"@types/koa-favicon": "2.0.19",
|
||||
"@types/koa-logger": "3.1.1",
|
||||
"@types/koa-mount": "4.0.0",
|
||||
"@types/koa-send": "4.1.2",
|
||||
"@types/koa-views": "2.0.4",
|
||||
"@types/koa-views": "7.0.0",
|
||||
"@types/koa__cors": "3.0.2",
|
||||
"@types/koa__multer": "2.0.2",
|
||||
"@types/koa__router": "8.0.4",
|
||||
"@types/markdown-it": "12.0.1",
|
||||
"@types/matter-js": "0.14.12",
|
||||
"@types/mocha": "8.2.2",
|
||||
"@types/node": "15.3.1",
|
||||
"@types/node": "15.6.1",
|
||||
"@types/node-fetch": "2.5.10",
|
||||
"@types/nodemailer": "6.4.1",
|
||||
"@types/nodemailer": "6.4.2",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.0",
|
||||
|
@ -85,12 +86,12 @@
|
|||
"@types/qrcode": "1.4.0",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.1",
|
||||
"@types/redis": "2.8.28",
|
||||
"@types/redis": "2.8.29",
|
||||
"@types/rename": "1.0.3",
|
||||
"@types/request-stats": "3.0.0",
|
||||
"@types/rimraf": "3.0.0",
|
||||
"@types/seedrandom": "2.4.28",
|
||||
"@types/sharp": "0.28.1",
|
||||
"@types/sharp": "0.28.2",
|
||||
"@types/sinonjs__fake-timers": "6.0.2",
|
||||
"@types/speakeasy": "2.0.5",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
|
@ -102,14 +103,14 @@
|
|||
"@types/webpack-stream": "3.2.12",
|
||||
"@types/websocket": "1.0.2",
|
||||
"@types/ws": "7.4.4",
|
||||
"@typescript-eslint/parser": "4.24.0",
|
||||
"@typescript-eslint/parser": "4.25.0",
|
||||
"@vue/compiler-sfc": "3.0.11",
|
||||
"abort-controller": "3.0.0",
|
||||
"apexcharts": "3.26.3",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.4",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.910.0",
|
||||
"aws-sdk": "2.918.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.3",
|
||||
"broadcast-channel": "3.6.0",
|
||||
|
@ -120,20 +121,20 @@
|
|||
"chart.js": "2.9.4",
|
||||
"cli-highlight": "2.1.11",
|
||||
"commander": "7.2.0",
|
||||
"concurrently": "6.1.0",
|
||||
"concurrently": "6.2.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"core-js": "3.12.1",
|
||||
"core-js": "3.13.1",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "5.2.4",
|
||||
"cssnano": "5.0.3",
|
||||
"css-loader": "5.2.6",
|
||||
"cssnano": "5.0.5",
|
||||
"dateformat": "4.5.1",
|
||||
"diskusage": "1.1.3",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "7.26.0",
|
||||
"eslint-plugin-vue": "7.9.0",
|
||||
"eslint": "7.27.0",
|
||||
"eslint-plugin-vue": "7.10.0",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "16.4.0",
|
||||
"file-type": "16.5.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"glob": "7.1.7",
|
||||
"got": "11.8.2",
|
||||
|
@ -148,12 +149,12 @@
|
|||
"http-proxy-agent": "4.0.1",
|
||||
"http-signature": "1.3.5",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"idb-keyval": "5.0.5",
|
||||
"idb-keyval": "5.0.6",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-root": "2.1.0",
|
||||
"is-svg": "4.3.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "16.5.3",
|
||||
"jsdom": "16.6.0",
|
||||
"json5": "2.2.0",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "4.0.1",
|
||||
|
@ -174,22 +175,23 @@
|
|||
"markdown-it-anchor": "7.1.0",
|
||||
"matter-js": "0.17.1",
|
||||
"mfm-js": "0.16.4",
|
||||
"misskey-js": "0.0.2",
|
||||
"mocha": "8.4.0",
|
||||
"moji": "0.5.1",
|
||||
"ms": "2.1.3",
|
||||
"multer": "1.4.2",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"nodemailer": "6.6.0",
|
||||
"nodemailer": "6.6.1",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "6.0.1",
|
||||
"pg": "8.6.0",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss": "8.2.15",
|
||||
"postcss": "8.3.0",
|
||||
"postcss-loader": "5.3.0",
|
||||
"prismjs": "1.23.0",
|
||||
"probe-image-size": "7.1.0",
|
||||
"probe-image-size": "7.1.1",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "3.0.2",
|
||||
|
@ -210,30 +212,31 @@
|
|||
"rimraf": "3.0.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.32.13",
|
||||
"sass": "1.34.0",
|
||||
"sass-loader": "11.1.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.28.2",
|
||||
"sharp": "0.28.3",
|
||||
"speakeasy": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "2.0.0",
|
||||
"summaly": "2.4.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.6.22",
|
||||
"systeminformation": "5.7.4",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.117.1",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.2.1",
|
||||
"ts-node": "9.1.1",
|
||||
"ts-loader": "9.2.2",
|
||||
"ts-node": "10.0.0",
|
||||
"tsc-alias": "1.2.11",
|
||||
"tsconfig-paths": "3.9.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"twemoji-parser": "13.1.0",
|
||||
"typeorm": "0.2.32",
|
||||
"typescript": "4.2.4",
|
||||
"typescript": "4.3.2",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
|
@ -248,10 +251,10 @@
|
|||
"vue-svg-loader": "0.17.0-beta.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"web-push": "3.4.4",
|
||||
"webpack": "5.37.1",
|
||||
"webpack": "5.38.1",
|
||||
"webpack-cli": "4.7.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "7.4.5",
|
||||
"ws": "7.4.6",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h, TransitionGroup } from 'vue';
|
||||
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
|
||||
import MkAd from '@client/components/global/ad.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
|
||||
required: true,
|
||||
},
|
||||
direction: {
|
||||
|
|
|
@ -87,6 +87,10 @@ export default defineComponent({
|
|||
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
|
||||
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||
action: this.toggleSensitive
|
||||
}, {
|
||||
text: this.$ts.describeFile,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: this.describe
|
||||
}, null, {
|
||||
text: this.$ts.copyUrl,
|
||||
icon: 'fas fa-link',
|
||||
|
@ -150,6 +154,26 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
|
||||
describe() {
|
||||
os.popup(import('@client/components/media-caption.vue'), {
|
||||
title: this.$ts.describeFile,
|
||||
input: {
|
||||
placeholder: this.$ts.inputNewDescription,
|
||||
default: this.file.comment !== null ? this.file.comment : '',
|
||||
},
|
||||
image: this.file
|
||||
}, {
|
||||
done: result => {
|
||||
if (!result || result.canceled) return;
|
||||
let comment = result.result;
|
||||
os.api('drive/files/update', {
|
||||
fileId: this.file.id,
|
||||
comment: comment.length == 0 ? null : comment
|
||||
});
|
||||
}
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
toggleSensitive() {
|
||||
os.api('drive/files/update', {
|
||||
fileId: this.file.id,
|
||||
|
|
|
@ -139,7 +139,7 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
this.connection = os.stream.useSharedConnection('drive');
|
||||
this.connection = os.stream.useChannel('drive');
|
||||
|
||||
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
|
||||
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
|
||||
|
@ -301,7 +301,7 @@ export default defineComponent({
|
|||
}
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
os.api('drive/files/upload_from_url', {
|
||||
os.api('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: this.folder ? this.folder.id : undefined
|
||||
});
|
||||
|
|
|
@ -71,7 +71,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
|
||||
this.connection.on('follow', this.onFollowChange);
|
||||
this.connection.on('unfollow', this.onFollowChange);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div class="xubzgfga">
|
||||
<header>{{ image.name }}</header>
|
||||
<img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/>
|
||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
|
||||
<footer>
|
||||
<span>{{ image.type }}</span>
|
||||
<span>{{ bytes(image.size) }}</span>
|
||||
|
|
238
src/client/components/media-caption.vue
Normal file
238
src/client/components/media-caption.vue
Normal file
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
|
||||
<div class="container">
|
||||
<div class="fullwidth top-caption">
|
||||
<div class="mk-dialog">
|
||||
<header v-if="title"><Mfm :text="title"/></header>
|
||||
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
|
||||
<div class="buttons" v-if="(showOkButton || showCancelButton)">
|
||||
<MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton>
|
||||
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hdrwpsaf fullwidth">
|
||||
<header>{{ image.name }}</header>
|
||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
|
||||
<footer>
|
||||
<span>{{ image.type }}</span>
|
||||
<span>{{ bytes(image.size) }}</span>
|
||||
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkModal from '@client/components/ui/modal.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import bytes from '@client/filters/bytes';
|
||||
import number from '@client/filters/number';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
input: {
|
||||
required: true
|
||||
},
|
||||
showOkButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showCancelButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
cancelableByBgClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.input.default ? this.input.default : null
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
methods: {
|
||||
bytes,
|
||||
number,
|
||||
|
||||
done(canceled, result?) {
|
||||
this.$emit('done', { canceled, result });
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
|
||||
async ok() {
|
||||
if (!this.showOkButton) return;
|
||||
|
||||
const result = this.inputValue;
|
||||
this.done(false, result);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.done(true);
|
||||
},
|
||||
|
||||
onBgClick() {
|
||||
if (this.cancelableByBgClick) {
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
if (e.which === 27) { // ESC
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
onInputKeydown(e) {
|
||||
if (e.which === 13) { // Enter
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.top-caption {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
.mk-dialog {
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
margin: auto;
|
||||
|
||||
> header {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
margin-top: 16px;
|
||||
|
||||
> * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> textarea {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 0 24px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
font-family: inherit;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 90px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hdrwpsaf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> header,
|
||||
> footer {
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
padding: 6px 9px;
|
||||
font-size: 90%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
cursor: zoom-out;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 8px;
|
||||
opacity: 0.8;
|
||||
|
||||
> span + span {
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
border-left: solid 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="qjewsnkg" v-if="hide" @click="hide = false">
|
||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/>
|
||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div class="text">
|
||||
<div>
|
||||
<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
|
||||
|
@ -14,7 +14,7 @@
|
|||
:title="image.name"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
|
||||
</a>
|
||||
<i class="fas fa-eye-slash" @click="hide = true"></i>
|
||||
|
|
|
@ -109,7 +109,7 @@ export default defineComponent({
|
|||
|
||||
this.readObserver.observe(this.$el);
|
||||
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||
</XList>
|
||||
|
||||
<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<MkButton primary style="margin: var(--margin) auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
</button>
|
||||
</MkButton>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
@ -28,12 +28,14 @@ import XList from './date-separated-list.vue';
|
|||
import XNote from './note.vue';
|
||||
import { notificationTypes } from '../../types';
|
||||
import * as os from '@client/os';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotification,
|
||||
XList,
|
||||
XNote,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
|
@ -87,7 +89,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('notification', this.onNotification);
|
||||
},
|
||||
|
||||
|
|
|
@ -89,6 +89,27 @@ export default defineComponent({
|
|||
file.name = result;
|
||||
});
|
||||
},
|
||||
|
||||
async describe(file) {
|
||||
os.popup(import("@client/components/media-caption.vue"), {
|
||||
title: this.$ts.describeFile,
|
||||
input: {
|
||||
placeholder: this.$ts.inputNewDescription,
|
||||
default: file.comment !== null ? file.comment : "",
|
||||
},
|
||||
image: file
|
||||
}, {
|
||||
done: result => {
|
||||
if (!result || result.canceled) return;
|
||||
let comment = result.result;
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
comment: comment.length == 0 ? null : comment
|
||||
});
|
||||
}
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
showFileMenu(file, ev: MouseEvent) {
|
||||
if (this.menu) return;
|
||||
this.menu = os.modalMenu([{
|
||||
|
@ -99,6 +120,10 @@ export default defineComponent({
|
|||
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
|
||||
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
|
||||
action: () => { this.toggleSensitive(file) }
|
||||
}, {
|
||||
text: this.$ts.describeFile,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: () => { this.describe(file) }
|
||||
}, {
|
||||
text: this.$ts.attachCancel,
|
||||
icon: 'fas fa-times-circle',
|
||||
|
|
|
@ -92,33 +92,33 @@ export default defineComponent({
|
|||
this.query = {
|
||||
antennaId: this.antenna
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('antenna', {
|
||||
this.connection = os.stream.useChannel('antenna', {
|
||||
antennaId: this.antenna
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
this.connection = os.stream.useSharedConnection('homeTimeline');
|
||||
this.connection = os.stream.useChannel('homeTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
|
||||
this.connection2 = os.stream.useSharedConnection('main');
|
||||
this.connection2 = os.stream.useChannel('main');
|
||||
this.connection2.on('follow', onChangeFollowing);
|
||||
this.connection2.on('unfollow', onChangeFollowing);
|
||||
} else if (this.src == 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
this.connection = os.stream.useSharedConnection('localTimeline');
|
||||
this.connection = os.stream.useChannel('localTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
this.connection = os.stream.useSharedConnection('hybridTimeline');
|
||||
this.connection = os.stream.useChannel('hybridTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
this.connection = os.stream.useSharedConnection('globalTimeline');
|
||||
this.connection = os.stream.useChannel('globalTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('mention', prepend);
|
||||
} else if (this.src == 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
|
@ -130,14 +130,14 @@ export default defineComponent({
|
|||
prepend(note);
|
||||
}
|
||||
};
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('mention', onNote);
|
||||
} else if (this.src == 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
this.query = {
|
||||
listId: this.list
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('userList', {
|
||||
this.connection = os.stream.useChannel('userList', {
|
||||
listId: this.list
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
|
@ -148,7 +148,7 @@ export default defineComponent({
|
|||
this.query = {
|
||||
channelId: this.channel
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('channel', {
|
||||
this.connection = os.stream.useChannel('channel', {
|
||||
channelId: this.channel
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
|
|
|
@ -163,8 +163,6 @@ fetchInstance().then(() => {
|
|||
initializeSw();
|
||||
});
|
||||
|
||||
stream.init($i);
|
||||
|
||||
const app = createApp(await (
|
||||
window.location.search === '?zen' ? import('@client/ui/zen.vue') :
|
||||
!$i ? import('@client/ui/visitor.vue') :
|
||||
|
@ -296,7 +294,7 @@ if ($i) {
|
|||
}
|
||||
}
|
||||
|
||||
const main = stream.useSharedConnection('main', 'System');
|
||||
const main = stream.useChannel('main', null, 'System');
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
|
@ -358,10 +356,6 @@ if ($i) {
|
|||
sound.play('channel');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
|
|
@ -1,26 +1,14 @@
|
|||
import { computed, reactive } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { api } from './os';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
export type Instance = {
|
||||
emojis: {
|
||||
category: string;
|
||||
}[];
|
||||
ads: {
|
||||
id: string;
|
||||
ratio: number;
|
||||
place: string;
|
||||
url: string;
|
||||
imageUrl: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('instance');
|
||||
|
||||
// TODO: instanceをリアクティブにするかは再考の余地あり
|
||||
|
||||
export const instance: Instance = reactive(data ? JSON.parse(data) : {
|
||||
export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : {
|
||||
// TODO: set default values
|
||||
});
|
||||
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Stream from '@client/scripts/stream';
|
||||
import { apiUrl, debug } from '@client/config';
|
||||
import { apiUrl, debug, url } from '@client/config';
|
||||
import MkPostFormDialog from '@client/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@client/components/waiting-dialog.vue';
|
||||
import { resolve } from '@client/router';
|
||||
import { $i } from '@client/account';
|
||||
import { defaultStore } from '@client/store';
|
||||
|
||||
export const stream = markRaw(new Stream());
|
||||
export const stream = markRaw(new Misskey.Stream(url, $i));
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
let apiRequestsCount = 0; // for debug
|
||||
|
@ -20,7 +20,11 @@ export const apiRequests = ref([]); // for debug
|
|||
|
||||
export const windows = new Map();
|
||||
|
||||
export function api(endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) {
|
||||
const apiClient = new Misskey.api.APIClient({
|
||||
origin: url,
|
||||
});
|
||||
|
||||
export const api = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
const onFinally = () => {
|
||||
|
@ -56,7 +60,7 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
|
|||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
if (debug) {
|
||||
log!.res = markRaw(body);
|
||||
log!.res = markRaw(JSON.parse(JSON.stringify(body)));
|
||||
log!.state = 'success';
|
||||
}
|
||||
} else if (res.status === 204) {
|
||||
|
@ -90,17 +94,15 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
|
|||
promise.then(onFinally, onFinally);
|
||||
|
||||
return promise;
|
||||
}
|
||||
}) as typeof apiClient.request;
|
||||
|
||||
export function apiWithDialog(
|
||||
export const apiWithDialog = ((
|
||||
endpoint: string,
|
||||
data: Record<string, any> = {},
|
||||
token?: string | null | undefined,
|
||||
onSuccess?: (res: any) => void,
|
||||
onFailure?: (e: Error) => void,
|
||||
) {
|
||||
) => {
|
||||
const promise = api(endpoint, data, token);
|
||||
promiseDialog(promise, onSuccess, onFailure ? onFailure : (e) => {
|
||||
promiseDialog(promise, null, (e) => {
|
||||
dialog({
|
||||
type: 'error',
|
||||
text: e.message + '\n' + (e as any).id,
|
||||
|
@ -108,7 +110,7 @@ export function apiWithDialog(
|
|||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}) as typeof api;
|
||||
|
||||
export function promiseDialog<T extends Promise<any>>(
|
||||
promise: T,
|
||||
|
|
|
@ -90,7 +90,7 @@ export default defineComponent({
|
|||
stats: null,
|
||||
serverInfo: null,
|
||||
connection: null,
|
||||
queueConnection: os.stream.useSharedConnection('queueStats'),
|
||||
queueConnection: os.stream.useChannel('queueStats'),
|
||||
memUsage: 0,
|
||||
chartCpuMem: null,
|
||||
chartNet: null,
|
||||
|
@ -121,7 +121,7 @@ export default defineComponent({
|
|||
os.api('admin/server-info', {}).then(res => {
|
||||
this.serverInfo = res;
|
||||
|
||||
this.connection = os.stream.useSharedConnection('serverStats');
|
||||
this.connection = os.stream.useChannel('serverStats');
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
|
|
|
@ -92,6 +92,7 @@ export default defineComponent({
|
|||
version,
|
||||
url,
|
||||
stats: null,
|
||||
meta: null,
|
||||
fetchStats: () => os.api('stats', {}),
|
||||
fetchServerInfo: () => os.api('admin/server-info', {}),
|
||||
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
|
||||
|
|
|
@ -35,7 +35,7 @@ export default defineComponent({
|
|||
title: this.$ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
},
|
||||
connection: os.stream.useSharedConnection('queueStats'),
|
||||
connection: os.stream.useChannel('queueStats'),
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = os.stream.useSharedConnection('messagingIndex');
|
||||
this.connection = os.stream.useChannel('messagingIndex');
|
||||
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
|
|
|
@ -141,7 +141,7 @@ const Component = defineComponent({
|
|||
this.group = group;
|
||||
}
|
||||
|
||||
this.connection = os.stream.connectToChannel('messaging', {
|
||||
this.connection = os.stream.useChannel('messaging', {
|
||||
otherparty: this.user ? this.user.id : undefined,
|
||||
group: this.group ? this.group.id : undefined,
|
||||
});
|
||||
|
|
|
@ -61,7 +61,7 @@ export default defineComponent({
|
|||
if (this.connection) {
|
||||
this.connection.dispose();
|
||||
}
|
||||
this.connection = os.stream.connectToChannel('gamesReversiGame', {
|
||||
this.connection = os.stream.useChannel('gamesReversiGame', {
|
||||
gameId: this.game.id
|
||||
});
|
||||
this.connection.on('started', this.onStarted);
|
||||
|
|
|
@ -92,7 +92,7 @@ export default defineComponent({
|
|||
|
||||
mounted() {
|
||||
if (this.$i) {
|
||||
this.connection = os.stream.useSharedConnection('gamesReversi');
|
||||
this.connection = os.stream.useChannel('gamesReversi');
|
||||
|
||||
this.connection.on('invited', this.onInvited);
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<div class="_formLabel"><i class="fab fa-twitter"></i> Twitter</div>
|
||||
<div class="_formPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
|
||||
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectSerice }}</MkButton>
|
||||
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectSerice }}</MkButton>
|
||||
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -13,8 +13,8 @@
|
|||
<div class="_formLabel"><i class="fab fa-discord"></i> Discord</div>
|
||||
<div class="_formPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
|
||||
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectSerice }}</MkButton>
|
||||
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectSerice }}</MkButton>
|
||||
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -22,8 +22,8 @@
|
|||
<div class="_formLabel"><i class="fab fa-github"></i> GitHub</div>
|
||||
<div class="_formPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
|
||||
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectSerice }}</MkButton>
|
||||
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectSerice }}</MkButton>
|
||||
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormBase>
|
||||
|
|
|
@ -3,6 +3,11 @@ import * as url from '../../prelude/url';
|
|||
|
||||
export function getStaticImageUrl(baseUrl: string): string {
|
||||
const u = new URL(baseUrl);
|
||||
if (u.href.startsWith(`${instanceUrl}/proxy/`)) {
|
||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||
u.searchParams.set('static', '1');
|
||||
return u.href;
|
||||
}
|
||||
const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので
|
||||
return `${instanceUrl}/proxy/${dummy}?${url.query({
|
||||
url: u.href,
|
||||
|
|
|
@ -47,7 +47,7 @@ export function selectFile(src: any, label: string | null, multiple = false) {
|
|||
|
||||
const marker = Math.random().toString(); // TODO: UUIDとか使う
|
||||
|
||||
const connection = os.stream.useSharedConnection('main');
|
||||
const connection = os.stream.useChannel('main');
|
||||
connection.on('urlUploadFinished', data => {
|
||||
if (data.marker === marker) {
|
||||
res(multiple ? [data.file] : data.file);
|
||||
|
@ -55,7 +55,7 @@ export function selectFile(src: any, label: string | null, multiple = false) {
|
|||
}
|
||||
});
|
||||
|
||||
os.api('drive/files/upload_from_url', {
|
||||
os.api('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
marker
|
||||
});
|
||||
|
|
|
@ -1,312 +0,0 @@
|
|||
import autobind from 'autobind-decorator';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import { markRaw } from 'vue';
|
||||
import { debug, wsUrl } from '@client/config';
|
||||
import { query as urlQuery } from '../../prelude/url';
|
||||
|
||||
/**
|
||||
* Misskey stream connection
|
||||
*/
|
||||
export default class Stream extends EventEmitter {
|
||||
private stream: ReconnectingWebsocket;
|
||||
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
private nonSharedConnections: NonSharedConnection[] = [];
|
||||
|
||||
@autobind
|
||||
public init(user): void {
|
||||
const query = urlQuery({
|
||||
i: user?.token,
|
||||
_t: Date.now(),
|
||||
});
|
||||
|
||||
this.stream = new ReconnectingWebsocket(`${wsUrl}?${query}`, '', { minReconnectionDelay: 1 }); // https://github.com/pladaria/reconnecting-websocket/issues/91
|
||||
this.stream.addEventListener('open', this.onOpen);
|
||||
this.stream.addEventListener('close', this.onClose);
|
||||
this.stream.addEventListener('message', this.onMessage);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public useSharedConnection(channel: string, name?: string): SharedConnection {
|
||||
let pool = this.sharedConnectionPools.find(p => p.channel === channel);
|
||||
|
||||
if (pool == null) {
|
||||
pool = new Pool(this, channel);
|
||||
this.sharedConnectionPools.push(pool);
|
||||
}
|
||||
|
||||
const connection = markRaw(new SharedConnection(this, channel, pool, name));
|
||||
this.sharedConnections.push(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public removeSharedConnection(connection: SharedConnection) {
|
||||
this.sharedConnections = this.sharedConnections.filter(c => c !== connection);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public removeSharedConnectionPool(pool: Pool) {
|
||||
this.sharedConnectionPools = this.sharedConnectionPools.filter(p => p !== pool);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connectToChannel(channel: string, params?: any): NonSharedConnection {
|
||||
const connection = markRaw(new NonSharedConnection(this, channel, params));
|
||||
this.nonSharedConnections.push(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public disconnectToChannel(connection: NonSharedConnection) {
|
||||
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback of when open connection
|
||||
*/
|
||||
@autobind
|
||||
private onOpen() {
|
||||
const isReconnect = this.state === 'reconnecting';
|
||||
|
||||
this.state = 'connected';
|
||||
this.emit('_connected_');
|
||||
|
||||
// チャンネル再接続
|
||||
if (isReconnect) {
|
||||
for (const p of this.sharedConnectionPools)
|
||||
p.connect();
|
||||
for (const c of this.nonSharedConnections)
|
||||
c.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback of when close connection
|
||||
*/
|
||||
@autobind
|
||||
private onClose() {
|
||||
if (this.state === 'connected') {
|
||||
this.state = 'reconnecting';
|
||||
this.emit('_disconnected_');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback of when received a message from connection
|
||||
*/
|
||||
@autobind
|
||||
private onMessage(message) {
|
||||
const { type, body } = JSON.parse(message.data);
|
||||
|
||||
if (type === 'channel') {
|
||||
const id = body.id;
|
||||
|
||||
let connections: Connection[];
|
||||
|
||||
connections = this.sharedConnections.filter(c => c.id === id);
|
||||
|
||||
if (connections.length === 0) {
|
||||
connections = [this.nonSharedConnections.find(c => c.id === id)];
|
||||
}
|
||||
|
||||
for (const c of connections.filter(c => c != null)) {
|
||||
c.emit(body.type, Object.freeze(body.body));
|
||||
if (debug) c.inCount++;
|
||||
}
|
||||
} else {
|
||||
this.emit(type, Object.freeze(body));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to connection
|
||||
*/
|
||||
@autobind
|
||||
public send(typeOrPayload, payload?) {
|
||||
const data = payload === undefined ? typeOrPayload : {
|
||||
type: typeOrPayload,
|
||||
body: payload
|
||||
};
|
||||
|
||||
this.stream.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this connection
|
||||
*/
|
||||
@autobind
|
||||
public close() {
|
||||
this.stream.removeEventListener('open', this.onOpen);
|
||||
this.stream.removeEventListener('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
class Pool {
|
||||
public channel: string;
|
||||
public id: string;
|
||||
protected stream: Stream;
|
||||
public users = 0;
|
||||
private disposeTimerId: any;
|
||||
private isConnected = false;
|
||||
|
||||
constructor(stream: Stream, channel: string) {
|
||||
this.channel = channel;
|
||||
this.stream = stream;
|
||||
|
||||
this.id = (++idCounter).toString();
|
||||
|
||||
this.stream.on('_disconnected_', this.onStreamDisconnected);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onStreamDisconnected() {
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public inc() {
|
||||
if (this.users === 0 && !this.isConnected) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
this.users++;
|
||||
|
||||
// タイマー解除
|
||||
if (this.disposeTimerId) {
|
||||
clearTimeout(this.disposeTimerId);
|
||||
this.disposeTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dec() {
|
||||
this.users--;
|
||||
|
||||
// そのコネクションの利用者が誰もいなくなったら
|
||||
if (this.users === 0) {
|
||||
// また直ぐに再利用される可能性があるので、一定時間待ち、
|
||||
// 新たな利用者が現れなければコネクションを切断する
|
||||
this.disposeTimerId = setTimeout(() => {
|
||||
this.disconnect();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connect() {
|
||||
if (this.isConnected) return;
|
||||
this.isConnected = true;
|
||||
this.stream.send('connect', {
|
||||
channel: this.channel,
|
||||
id: this.id
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private disconnect() {
|
||||
this.stream.off('_disconnected_', this.onStreamDisconnected);
|
||||
this.stream.send('disconnect', { id: this.id });
|
||||
this.stream.removeSharedConnectionPool(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Connection extends EventEmitter {
|
||||
public channel: string;
|
||||
protected stream: Stream;
|
||||
public abstract id: string;
|
||||
|
||||
public name?: string; // for debug
|
||||
public inCount: number = 0; // for debug
|
||||
public outCount: number = 0; // for debug
|
||||
|
||||
constructor(stream: Stream, channel: string, name?: string) {
|
||||
super();
|
||||
|
||||
this.stream = stream;
|
||||
this.channel = channel;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public send(id: string, typeOrPayload, payload?) {
|
||||
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
|
||||
const body = payload === undefined ? typeOrPayload.body : payload;
|
||||
|
||||
this.stream.send('ch', {
|
||||
id: id,
|
||||
type: type,
|
||||
body: body
|
||||
});
|
||||
|
||||
if (debug) this.outCount++;
|
||||
}
|
||||
|
||||
public abstract dispose(): void;
|
||||
}
|
||||
|
||||
class SharedConnection extends Connection {
|
||||
private pool: Pool;
|
||||
|
||||
public get id(): string {
|
||||
return this.pool.id;
|
||||
}
|
||||
|
||||
constructor(stream: Stream, channel: string, pool: Pool, name?: string) {
|
||||
super(stream, channel, name);
|
||||
|
||||
this.pool = pool;
|
||||
this.pool.inc();
|
||||
}
|
||||
|
||||
@autobind
|
||||
public send(typeOrPayload, payload?) {
|
||||
super.send(this.pool.id, typeOrPayload, payload);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
this.pool.dec();
|
||||
this.removeAllListeners();
|
||||
this.stream.removeSharedConnection(this);
|
||||
}
|
||||
}
|
||||
|
||||
class NonSharedConnection extends Connection {
|
||||
public id: string;
|
||||
protected params: any;
|
||||
|
||||
constructor(stream: Stream, channel: string, params?: any) {
|
||||
super(stream, channel);
|
||||
|
||||
this.params = params;
|
||||
this.id = (++idCounter).toString();
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@autobind
|
||||
public connect() {
|
||||
this.stream.send('connect', {
|
||||
channel: this.channel,
|
||||
id: this.id,
|
||||
params: this.params
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public send(typeOrPayload, payload?) {
|
||||
super.send(this.id, typeOrPayload, payload);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public dispose() {
|
||||
this.removeAllListeners();
|
||||
this.stream.send('disconnect', { id: this.id });
|
||||
this.stream.disconnectToChannel(this);
|
||||
}
|
||||
}
|
|
@ -146,6 +146,7 @@ hr {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--modalBg);
|
||||
-webkit-backdrop-filter: var(--modalBgFilter);
|
||||
backdrop-filter: var(--modalBgFilter);
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
if ($i) {
|
||||
const connection = stream.useSharedConnection('main', 'UI');
|
||||
const connection = stream.useChannel('main', null, 'UI');
|
||||
connection.on('notification', onNotification);
|
||||
}
|
||||
|
||||
|
|
|
@ -121,33 +121,33 @@ export default defineComponent({
|
|||
this.query = {
|
||||
antennaId: this.antenna
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('antenna', {
|
||||
this.connection = os.stream.useChannel('antenna', {
|
||||
antennaId: this.antenna
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
this.connection = os.stream.useSharedConnection('homeTimeline');
|
||||
this.connection = os.stream.useChannel('homeTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
|
||||
this.connection2 = os.stream.useSharedConnection('main');
|
||||
this.connection2 = os.stream.useChannel('main');
|
||||
this.connection2.on('follow', onChangeFollowing);
|
||||
this.connection2.on('unfollow', onChangeFollowing);
|
||||
} else if (this.src == 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
this.connection = os.stream.useSharedConnection('localTimeline');
|
||||
this.connection = os.stream.useChannel('localTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
this.connection = os.stream.useSharedConnection('hybridTimeline');
|
||||
this.connection = os.stream.useChannel('hybridTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
this.connection = os.stream.useSharedConnection('globalTimeline');
|
||||
this.connection = os.stream.useChannel('globalTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('mention', prepend);
|
||||
} else if (this.src == 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
|
@ -159,14 +159,14 @@ export default defineComponent({
|
|||
prepend(note);
|
||||
}
|
||||
};
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
this.connection.on('mention', onNote);
|
||||
} else if (this.src == 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
this.query = {
|
||||
listId: this.list
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('userList', {
|
||||
this.connection = os.stream.useChannel('userList', {
|
||||
listId: this.list
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
|
@ -178,7 +178,7 @@ export default defineComponent({
|
|||
this.query = {
|
||||
channelId: this.channel
|
||||
};
|
||||
this.connection = os.stream.connectToChannel('channel', {
|
||||
this.connection = os.stream.useChannel('channel', {
|
||||
channelId: this.channel
|
||||
});
|
||||
this.connection.on('note', prepend);
|
||||
|
|
|
@ -241,7 +241,6 @@ export default defineComponent({
|
|||
> .text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +308,7 @@ export default defineComponent({
|
|||
> .indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
left: 0;
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
|
|
@ -65,7 +65,7 @@ export default defineComponent({
|
|||
extends: widget,
|
||||
data() {
|
||||
return {
|
||||
connection: os.stream.useSharedConnection('queueStats'),
|
||||
connection: os.stream.useChannel('queueStats'),
|
||||
inbox: {
|
||||
activeSincePrevTick: 0,
|
||||
active: 0,
|
||||
|
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useChannel('main');
|
||||
|
||||
this.connection.on('driveFileCreated', this.onDriveFileCreated);
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ export default defineComponent({
|
|||
os.api('server-info', {}).then(res => {
|
||||
this.meta = res;
|
||||
});
|
||||
this.connection = os.stream.useSharedConnection('serverStats');
|
||||
this.connection = os.stream.useChannel('serverStats');
|
||||
},
|
||||
unmounted() {
|
||||
this.connection.dispose();
|
||||
|
|
|
@ -93,6 +93,9 @@
|
|||
{ "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] },
|
||||
{ "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] },
|
||||
{ "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] },
|
||||
{ "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] },
|
||||
{ "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] },
|
||||
{ "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] },
|
||||
{ "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] },
|
||||
{ "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] },
|
||||
{ "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] },
|
||||
|
@ -1219,6 +1222,8 @@
|
|||
{ "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] },
|
||||
{ "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] },
|
||||
{ "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] },
|
||||
{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] },
|
||||
{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] },
|
||||
{ "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] },
|
||||
{ "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] },
|
||||
{ "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] },
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,8 @@ import { Note } from '../models/entities/note';
|
|||
import { Cache } from './cache';
|
||||
import { isSelfHost, toPunyNullable } from './convert-host';
|
||||
import { decodeReaction } from './reaction-lib';
|
||||
import config from '@/config';
|
||||
import { query } from '@/prelude/url';
|
||||
|
||||
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
|
||||
|
@ -59,9 +61,12 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
|
|||
|
||||
if (emoji == null) return null;
|
||||
|
||||
const isLocal = emoji.host == null;
|
||||
const url = isLocal ? emoji.url : `${config.url}/proxy/image.png?${query({url: emoji.url})}`;
|
||||
|
||||
return {
|
||||
name: emojiName,
|
||||
url: emoji.url,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userId = :id', { id: id })
|
||||
.andWhere('file.isLink = FALSE')
|
||||
.select('SUM(file.size)', 'sum')
|
||||
.getRawOne();
|
||||
|
||||
|
@ -69,6 +70,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost = :host', { host: toPuny(host) })
|
||||
.andWhere('file.isLink = FALSE')
|
||||
.select('SUM(file.size)', 'sum')
|
||||
.getRawOne();
|
||||
|
||||
|
@ -79,6 +81,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost IS NULL')
|
||||
.andWhere('file.isLink = FALSE')
|
||||
.select('SUM(file.size)', 'sum')
|
||||
.getRawOne();
|
||||
|
||||
|
@ -89,6 +92,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost IS NOT NULL')
|
||||
.andWhere('file.isLink = FALSE')
|
||||
.select('SUM(file.size)', 'sum')
|
||||
.getRawOne();
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import acceptFollow from './follow';
|
||||
import { IAccept, IFollow } from '../../type';
|
||||
import { IAccept, isFollow, getApType } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
|
||||
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
|
||||
const uri = activity.id || activity;
|
||||
|
||||
logger.info(`Accept: ${uri}`);
|
||||
|
@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
acceptFollow(actor, object as IFollow);
|
||||
break;
|
||||
if (isFollow(object)) return await acceptFollow(actor, object);
|
||||
|
||||
default:
|
||||
logger.warn(`Unknown accept type: ${object.type}`);
|
||||
break;
|
||||
}
|
||||
return `skip: Unknown Accept type: ${getApType(object)}`;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import createNote from './note';
|
||||
import { ICreate, getApId, validPost } from '../../type';
|
||||
import { ICreate, getApId, isPost, getApType } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
import { toArray, concat, unique } from '../../../../prelude/array';
|
||||
|
||||
|
@ -35,9 +35,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
if (validPost.includes(object.type)) {
|
||||
if (isPost(object)) {
|
||||
createNote(resolver, actor, object, false, activity);
|
||||
} else {
|
||||
logger.warn(`Unknown type: ${object.type}`);
|
||||
logger.warn(`Unknown type: ${getApType(object)}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import rejectFollow from './follow';
|
||||
import { IReject, IFollow } from '../../type';
|
||||
import { IReject, isFollow, getApType } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
|
||||
export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
|
||||
const uri = activity.id || activity;
|
||||
|
||||
logger.info(`Reject: ${uri}`);
|
||||
|
@ -18,13 +18,7 @@ export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
rejectFollow(actor, object as IFollow);
|
||||
break;
|
||||
if (isFollow(object)) return await rejectFollow(actor, object);
|
||||
|
||||
default:
|
||||
logger.warn(`Unknown reject type: ${object.type}`);
|
||||
break;
|
||||
}
|
||||
return `skip: Unknown Reject type: ${getApType(object)}`;
|
||||
};
|
||||
|
|
|
@ -3,14 +3,15 @@ import { IRemoteUser } from '../../../../models/entities/user';
|
|||
import { IAnnounce, getApId } from '../../type';
|
||||
import deleteNote from '../../../../services/note/delete';
|
||||
|
||||
export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
|
||||
export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
|
||||
const uri = getApId(activity);
|
||||
|
||||
const note = await Notes.findOne({
|
||||
uri
|
||||
});
|
||||
|
||||
if (!note) return;
|
||||
if (!note) return 'skip: no such Announce';
|
||||
|
||||
await deleteNote(actor, note);
|
||||
return 'ok: deleted';
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import { IUndo, IFollow, IBlock, ILike, IAnnounce } from '../../type';
|
||||
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
|
||||
import unfollow from './follow';
|
||||
import unblock from './block';
|
||||
import undoLike from './like';
|
||||
|
@ -9,7 +9,7 @@ import { apLogger } from '../../logger';
|
|||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
|
||||
export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
@ -25,20 +25,10 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
unfollow(actor, object as IFollow);
|
||||
break;
|
||||
case 'Block':
|
||||
unblock(actor, object as IBlock);
|
||||
break;
|
||||
case 'Like':
|
||||
case 'EmojiReaction':
|
||||
case 'EmojiReact':
|
||||
undoLike(actor, object as ILike);
|
||||
break;
|
||||
case 'Announce':
|
||||
undoAnnounce(actor, object as IAnnounce);
|
||||
break;
|
||||
}
|
||||
if (isFollow(object)) return await unfollow(actor, object);
|
||||
if (isBlock(object)) return await unblock(actor, object);
|
||||
if (isLike(object)) return await undoLike(actor, object);
|
||||
if (isAnnounce(object)) return await undoAnnounce(actor, object);
|
||||
|
||||
return `skip: unknown object type ${getApType(object)}`;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import { IUpdate, validActor } from '../../type';
|
||||
import { getApType, IUpdate, isActor } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
import { updateQuestion } from '../../models/question';
|
||||
import Resolver from '../../resolver';
|
||||
|
@ -22,13 +22,13 @@ export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> =>
|
|||
throw e;
|
||||
});
|
||||
|
||||
if (validActor.includes(object.type)) {
|
||||
if (isActor(object)) {
|
||||
await updatePerson(actor.uri!, resolver, object);
|
||||
return `ok: Person updated`;
|
||||
} else if (object.type === 'Question') {
|
||||
} else if (getApType(object) === 'Question') {
|
||||
await updateQuestion(object).catch(e => console.log(e));
|
||||
return `ok: Question updated`;
|
||||
} else {
|
||||
return `skip: Unknown type: ${object.type}`;
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
|
|||
const instance = await fetchMeta();
|
||||
const cache = instance.cacheRemoteFiles;
|
||||
|
||||
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
|
||||
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, image.name);
|
||||
|
||||
if (file.isLink) {
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
|
|
|
@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
|||
import { extractDbHost, toPuny } from '@/misc/convert-host';
|
||||
import { Emojis, Polls, MessagingMessages } from '../../../models';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type';
|
||||
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type';
|
||||
import { Emoji } from '../../../models/entities/emoji';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
|
@ -36,8 +36,8 @@ export function validateNote(object: any, uri: string) {
|
|||
return new Error('invalid Note: object is null');
|
||||
}
|
||||
|
||||
if (!validPost.includes(object.type)) {
|
||||
return new Error(`invalid Note: invalid object type ${object.type}`);
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||
}
|
||||
|
||||
if (object.id && extractDbHost(object.id) !== expectHost) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as promiseLimit from 'promise-limit';
|
|||
import config from '@/config';
|
||||
import Resolver from '../resolver';
|
||||
import { resolveImage } from './image';
|
||||
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue } from '../type';
|
||||
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType } from '../type';
|
||||
import { fromHtml } from '../../../mfm/from-html';
|
||||
import { htmlToMfm } from '../misc/html-to-mfm';
|
||||
import { resolveNote, extractEmojis } from './note';
|
||||
|
@ -137,7 +137,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
|||
|
||||
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||
|
||||
const isBot = object.type === 'Service';
|
||||
const isBot = getApType(object) === 'Service';
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
|
@ -337,7 +337,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
|||
emojis: emojiNames,
|
||||
name: person.name,
|
||||
tags,
|
||||
isBot: object.type === 'Service',
|
||||
isBot: getApType(object) === 'Service',
|
||||
isCat: (person as any).isCat === true,
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
isExplorable: !!person.discoverable,
|
||||
|
@ -476,7 +476,7 @@ export async function updateFeatured(userId: User['id']) {
|
|||
// Resolve and regist Notes
|
||||
const limit = promiseLimit<Note | null>(2);
|
||||
const featuredNotes = await Promise.all(items
|
||||
.filter(item => item.type === 'Note')
|
||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||
.slice(0, 5)
|
||||
.map(item => limit(() => resolveNote(item, resolver))));
|
||||
|
||||
|
|
|
@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
|
|||
export default (file: DriveFile) => ({
|
||||
type: 'Document',
|
||||
mediaType: file.type,
|
||||
url: DriveFiles.getPublicUrl(file)
|
||||
url: DriveFiles.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
});
|
||||
|
|
|
@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
|
|||
export default (file: DriveFile) => ({
|
||||
type: 'Image',
|
||||
url: DriveFiles.getPublicUrl(file),
|
||||
sensitive: file.isSensitive
|
||||
sensitive: file.isSensitive,
|
||||
name: file.comment
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ export type ApObject = IObject | string | (IObject | string)[];
|
|||
|
||||
export interface IObject {
|
||||
'@context': string | obj | obj[];
|
||||
type: string;
|
||||
type: string | unknown[];
|
||||
id?: string;
|
||||
summary?: string;
|
||||
published?: string;
|
||||
|
@ -51,6 +51,15 @@ export function getApId(value: string | IObject): string {
|
|||
throw new Error(`cannot detemine id`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityStreams Object type
|
||||
*/
|
||||
export function getApType(value: IObject): string {
|
||||
if (typeof value.type === 'string') return value.type;
|
||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error(`cannot detect type`);
|
||||
}
|
||||
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
const firstOne = Array.isArray(value) ? value[0] : value;
|
||||
return getApHrefNullable(firstOne);
|
||||
|
@ -92,6 +101,9 @@ export interface IOrderedCollection extends IObject {
|
|||
|
||||
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
|
||||
|
||||
export const isPost = (object: IObject): object is IPost =>
|
||||
validPost.includes(getApType(object));
|
||||
|
||||
export interface IPost extends IObject {
|
||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
||||
_misskey_content?: string;
|
||||
|
@ -112,7 +124,7 @@ export interface IQuestion extends IObject {
|
|||
}
|
||||
|
||||
export const isQuestion = (object: IObject): object is IQuestion =>
|
||||
object.type === 'Note' || object.type === 'Question';
|
||||
getApType(object) === 'Note' || getApType(object) === 'Question';
|
||||
|
||||
interface IQuestionChoice {
|
||||
name?: string;
|
||||
|
@ -126,10 +138,13 @@ export interface ITombstone extends IObject {
|
|||
}
|
||||
|
||||
export const isTombstone = (object: IObject): object is ITombstone =>
|
||||
object.type === 'Tombstone';
|
||||
getApType(object) === 'Tombstone';
|
||||
|
||||
export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
|
||||
|
||||
export const isActor = (object: IObject): object is IPerson =>
|
||||
validActor.includes(getApType(object));
|
||||
|
||||
export interface IPerson extends IObject {
|
||||
type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
|
||||
name?: string;
|
||||
|
@ -154,10 +169,10 @@ export interface IPerson extends IObject {
|
|||
}
|
||||
|
||||
export const isCollection = (object: IObject): object is ICollection =>
|
||||
object.type === 'Collection';
|
||||
getApType(object) === 'Collection';
|
||||
|
||||
export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
|
||||
object.type === 'OrderedCollection';
|
||||
getApType(object) === 'OrderedCollection';
|
||||
|
||||
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
|
||||
isCollection(object) || isOrderedCollection(object);
|
||||
|
@ -171,7 +186,7 @@ export interface IApPropertyValue extends IObject {
|
|||
|
||||
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||
object &&
|
||||
object.type === 'PropertyValue' &&
|
||||
getApType(object) === 'PropertyValue' &&
|
||||
typeof object.name === 'string' &&
|
||||
typeof (object as any).value === 'string';
|
||||
|
||||
|
@ -181,7 +196,7 @@ export interface IApMention extends IObject {
|
|||
}
|
||||
|
||||
export const isMention = (object: IObject): object is IApMention=>
|
||||
object.type === 'Mention' &&
|
||||
getApType(object) === 'Mention' &&
|
||||
typeof object.href === 'string';
|
||||
|
||||
export interface IApHashtag extends IObject {
|
||||
|
@ -190,7 +205,7 @@ export interface IApHashtag extends IObject {
|
|||
}
|
||||
|
||||
export const isHashtag = (object: IObject): object is IApHashtag =>
|
||||
object.type === 'Hashtag' &&
|
||||
getApType(object) === 'Hashtag' &&
|
||||
typeof object.name === 'string';
|
||||
|
||||
export interface IApEmoji extends IObject {
|
||||
|
@ -199,7 +214,7 @@ export interface IApEmoji extends IObject {
|
|||
}
|
||||
|
||||
export const isEmoji = (object: IObject): object is IApEmoji =>
|
||||
object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
|
||||
getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
|
||||
|
||||
export interface ICreate extends IActivity {
|
||||
type: 'Create';
|
||||
|
@ -258,17 +273,17 @@ export interface IFlag extends IActivity {
|
|||
type: 'Flag';
|
||||
}
|
||||
|
||||
export const isCreate = (object: IObject): object is ICreate => object.type === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete';
|
||||
export const isUpdate = (object: IObject): object is IUpdate => object.type === 'Update';
|
||||
export const isRead = (object: IObject): object is IRead => object.type === 'Read';
|
||||
export const isUndo = (object: IObject): object is IUndo => object.type === 'Undo';
|
||||
export const isFollow = (object: IObject): object is IFollow => object.type === 'Follow';
|
||||
export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept';
|
||||
export const isReject = (object: IObject): object is IReject => object.type === 'Reject';
|
||||
export const isAdd = (object: IObject): object is IAdd => object.type === 'Add';
|
||||
export const isRemove = (object: IObject): object is IRemove => object.type === 'Remove';
|
||||
export const isLike = (object: IObject): object is ILike => object.type === 'Like' || object.type === 'EmojiReaction' || object.type === 'EmojiReact';
|
||||
export const isAnnounce = (object: IObject): object is IAnnounce => object.type === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => object.type === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => object.type === 'Flag';
|
||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
|
||||
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
|
||||
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
|
||||
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
|
||||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
||||
export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
|
||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
|
|
|
@ -5,6 +5,8 @@ import { ApiError } from './error';
|
|||
import { SchemaType } from '@/misc/schema';
|
||||
import { AccessToken } from '../../models/entities/access-token';
|
||||
|
||||
type NonOptional<T> = T extends undefined ? never : T;
|
||||
|
||||
type SimpleUserInfo = {
|
||||
id: ILocalUser['id'];
|
||||
host: ILocalUser['host'];
|
||||
|
@ -17,11 +19,12 @@ type SimpleUserInfo = {
|
|||
isSilenced: ILocalUser['isSilenced'];
|
||||
};
|
||||
|
||||
// TODO: defaultが設定されている場合はその型も考慮する
|
||||
type Params<T extends IEndpointMeta> = {
|
||||
[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
|
||||
? ReturnType<NonNullable<T['params']>[P]['transform']>
|
||||
: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0];
|
||||
: NonNullable<T['params']>[P]['default'] extends null | number | string
|
||||
? NonOptional<ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]>
|
||||
: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0];
|
||||
};
|
||||
|
||||
export type Response = Record<string, any> | void;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models';
|
|||
import { Note } from '../../../../models/entities/note';
|
||||
import { User } from '../../../../models/entities/user';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { validActor, validPost } from '../../../../remote/activitypub/type';
|
||||
import { isActor, isPost, getApId } from '../../../../remote/activitypub/type';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
@ -154,16 +154,16 @@ async function fetchAny(uri: string) {
|
|||
}
|
||||
|
||||
// それでもみつからなければ新規であるため登録
|
||||
if (validActor.includes(object.type)) {
|
||||
const user = await createPerson(object.id);
|
||||
if (isActor(object)) {
|
||||
const user = await createPerson(getApId(object));
|
||||
return {
|
||||
type: 'User',
|
||||
object: await Users.pack(user, null, { detail: true })
|
||||
};
|
||||
}
|
||||
|
||||
if (validPost.includes(object.type)) {
|
||||
const note = await createNote(object.id, undefined, true);
|
||||
if (isPost(object)) {
|
||||
const note = await createNote(getApId(object), undefined, true);
|
||||
return {
|
||||
type: 'Note',
|
||||
object: await Notes.pack(note!, null, { detail: true })
|
||||
|
|
|
@ -49,6 +49,14 @@ export const meta = {
|
|||
'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
|
||||
'en-US': 'Whether this media is NSFW'
|
||||
}
|
||||
},
|
||||
|
||||
comment: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: undefined as any,
|
||||
desc: {
|
||||
'ja-JP': 'コメント'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -92,6 +100,8 @@ export default define(meta, async (ps, user) => {
|
|||
|
||||
if (ps.name) file.name = ps.name;
|
||||
|
||||
if (ps.comment !== undefined) file.comment = ps.comment;
|
||||
|
||||
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
|
||||
|
||||
if (ps.folderId !== undefined) {
|
||||
|
@ -113,6 +123,7 @@ export default define(meta, async (ps, user) => {
|
|||
|
||||
await DriveFiles.update(file.id, {
|
||||
name: file.name,
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
isSensitive: file.isSensitive
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { DriveFiles, GalleryPosts } from '../../../../../models';
|
|||
import { genId } from '../../../../../misc/gen-id';
|
||||
import { GalleryPost } from '../../../../../models/entities/gallery-post';
|
||||
import { ApiError } from '../../../error';
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
@ -55,7 +56,7 @@ export default define(meta, async (ps, user) => {
|
|||
id: fileId,
|
||||
userId: user.id
|
||||
})
|
||||
))).filter(file => file != null);
|
||||
))).filter((file): file is DriveFile => file != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ID } from '../../../../../misc/cafy-id';
|
|||
import { DriveFiles, GalleryPosts } from '../../../../../models';
|
||||
import { GalleryPost } from '../../../../../models/entities/gallery-post';
|
||||
import { ApiError } from '../../../error';
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
@ -58,7 +59,7 @@ export default define(meta, async (ps, user) => {
|
|||
id: fileId,
|
||||
userId: user.id
|
||||
})
|
||||
))).filter(file => file != null);
|
||||
))).filter((file): file is DriveFile => file != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
|
|
|
@ -5,6 +5,7 @@ import define from '../../define';
|
|||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notifications, Followings, Mutings, Users } from '../../../../models';
|
||||
import { notificationTypes } from '../../../../types';
|
||||
import read from '@/services/note/read';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -103,9 +104,9 @@ export default define(meta, async (ps, user) => {
|
|||
query.setParameters(followingQuery.getParameters());
|
||||
}
|
||||
|
||||
if (ps.includeTypes?.length > 0) {
|
||||
if (ps.includeTypes && ps.includeTypes.length > 0) {
|
||||
query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
|
||||
} else if (ps.excludeTypes?.length > 0) {
|
||||
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
|
||||
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
|
||||
}
|
||||
|
||||
|
@ -116,5 +117,11 @@ export default define(meta, async (ps, user) => {
|
|||
readNotification(user.id, notifications.map(x => x.id));
|
||||
}
|
||||
|
||||
const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
|
||||
|
||||
if (notes.length > 0) {
|
||||
read(user.id, notes);
|
||||
}
|
||||
|
||||
return await Notifications.packMany(notifications, user.id);
|
||||
});
|
||||
|
|
|
@ -104,22 +104,25 @@ export default define(meta, async (ps, me) => {
|
|||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMutedUserQuery(query, me);
|
||||
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(ps.tag)) return;
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
} else {
|
||||
let i = 0;
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(tag)) return;
|
||||
qb.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
i++;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(ps.tag)) throw 'Injection';
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
} else {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(tag)) throw 'Injection';
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === 'Injection') return [];
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (ps.reply != null) {
|
||||
|
|
|
@ -93,7 +93,7 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||
}
|
||||
|
||||
@autobind
|
||||
private static convertFlattenColumnsToObject(x: Record<string, number>) {
|
||||
private static convertFlattenColumnsToObject(x: Record<string, any>): Record<string, any> {
|
||||
const obj = {} as any;
|
||||
for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
|
||||
// now k is ___x_y_z
|
||||
|
@ -285,8 +285,7 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||
const latest = await this.getLatestLog(group);
|
||||
|
||||
if (latest != null) {
|
||||
const obj = Chart.convertFlattenColumnsToObject(
|
||||
latest as Record<string, any>);
|
||||
const obj = Chart.convertFlattenColumnsToObject(latest) as T;
|
||||
|
||||
// 空ログデータを作成
|
||||
data = this.getNewLog(obj);
|
||||
|
@ -474,13 +473,13 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
|
||||
|
||||
if (log) {
|
||||
const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
|
||||
chart.unshift(Chart.countUniqueFields(data));
|
||||
const data = Chart.convertFlattenColumnsToObject(log);
|
||||
chart.unshift(Chart.countUniqueFields(data) as T);
|
||||
} else {
|
||||
// 隙間埋め
|
||||
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
|
||||
const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
|
||||
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)));
|
||||
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
|
||||
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
|
||||
}
|
||||
}
|
||||
} else if (span === 'day') {
|
||||
|
@ -497,14 +496,14 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||
|
||||
if (log) {
|
||||
if (logsForEachDays[currentDayIndex]) {
|
||||
logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log));
|
||||
logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log) as T);
|
||||
} else {
|
||||
logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log)];
|
||||
logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log) as T];
|
||||
}
|
||||
} else {
|
||||
// 隙間埋め
|
||||
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
|
||||
const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
|
||||
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
|
||||
const newLog = this.getNewLog(data);
|
||||
if (logsForEachDays[currentDayIndex]) {
|
||||
logsForEachDays[currentDayIndex].unshift(newLog);
|
||||
|
@ -516,7 +515,7 @@ export default abstract class Chart<T extends Record<string, any>> {
|
|||
|
||||
for (const logs of logsForEachDays) {
|
||||
const log = this.aggregate(logs);
|
||||
chart.unshift(Chart.countUniqueFields(log));
|
||||
chart.unshift(Chart.countUniqueFields(log) as T);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -267,7 +267,8 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
|
|||
|
||||
async function deleteOldFile(user: IRemoteUser) {
|
||||
const q = DriveFiles.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id });
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.andWhere('file.isLink = FALSE');
|
||||
|
||||
if (user.avatarId) {
|
||||
q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });
|
||||
|
|
|
@ -79,7 +79,7 @@ async function postProcess(file: DriveFile, isExpired = false) {
|
|||
url: file.uri,
|
||||
thumbnailUrl: null,
|
||||
webpublicUrl: null,
|
||||
size: 0,
|
||||
storedInternal: false,
|
||||
// ローカルプロキシ用
|
||||
accessKey: uuid(),
|
||||
thumbnailAccessKey: 'thumbnail-' + uuid(),
|
||||
|
|
|
@ -25,6 +25,12 @@ export default async (
|
|||
name = null;
|
||||
}
|
||||
|
||||
// If the comment is same as the name, skip comment
|
||||
// (image.name is passed in when receiving attachment)
|
||||
if (comment !== null && name == comment) {
|
||||
comment = null;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
|
|
Loading…
Reference in a new issue