Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
ebc1b09298 | |||
52cbda91b9 | |||
ef77f281a6 | |||
010a65dd92 | |||
ed515cc24c | |||
e44be45c90 | |||
8fcfdc88d8 | |||
a903263e00 | |||
ac1072fd52 | |||
ba331f771d | |||
24a10acd34 | |||
98a15fd080 | |||
7e635d23d3 | |||
427e36e7ec | |||
fd477f0ad2 | |||
1258a588c7 | |||
b541ecb099 | |||
8840724a7c | |||
191b2692d2 | |||
b74c924f66 | |||
fb42e40958 | |||
49d861c6e6 | |||
93d81bd695 | |||
f1775debb5 | |||
58002cac58 | |||
6aa52ecbfa | |||
045c9bf088 | |||
f53ff2089b | |||
92bfbc0bb6 | |||
5bb2d0f284 | |||
7205021d62 | |||
b1d133c3d5 | |||
56f577ddb8 | |||
12dd7deac5 | |||
f0b0e46e45 | |||
71f339b23a | |||
0120dee999 | |||
9f3b85527d | |||
79c71bf22a | |||
957a69779a | |||
80a2bc401a | |||
f17eb379ac | |||
4568fba7a9 | |||
40998587b3 | |||
d1924e875a | |||
655f7a8dfc |
138 changed files with 709 additions and 4345 deletions
|
@ -324,9 +324,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "الصفحات"
|
||||
integration: "التكامل"
|
||||
connectService: "اتصل"
|
||||
disconnectService: "اقطع الاتصال"
|
||||
enableLocalTimeline: "تفعيل الخيط المحلي"
|
||||
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
|
||||
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية\
|
||||
|
@ -711,7 +708,6 @@ apply: "تطبيق"
|
|||
receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل"
|
||||
emailNotification: "إشعارات البريد الكتروني"
|
||||
inChannelSearch: "ابحث عن قناة"
|
||||
useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن"
|
||||
typingUsers: "{users} يكتب(ون)..."
|
||||
jumpToSpecifiedDate: "انتقل إلى تاريخ محدد"
|
||||
showingPastTimeline: "أنت تستعرض حاليًا خيطًا زمنيًا قديمًا"
|
||||
|
|
|
@ -339,9 +339,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "পৃষ্ঠা"
|
||||
integration: "ইন্টিগ্রেশন"
|
||||
connectService: "সংযুক্ত করুন"
|
||||
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
|
||||
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
|
||||
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
|
||||
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই\
|
||||
|
@ -773,7 +770,6 @@ receiveAnnouncementFromInstance: "এই ইন্সট্যান্স থ
|
|||
emailNotification: "ইমেইল বিজ্ঞপ্তি"
|
||||
publish: "প্রকাশ"
|
||||
inChannelSearch: "চ্যানেলে খুঁজুন"
|
||||
useReactionPickerForContextMenu: "রাইট ক্লিকের মাধ্যমে রিঅ্যাকশন পিকার খুলুন"
|
||||
typingUsers: "{users} লেখছে"
|
||||
jumpToSpecifiedDate: "একটি নির্দিষ্ট তারিখে যান"
|
||||
showingPastTimeline: "অতীতের টাইমলাইন দেখানো হচ্ছে"
|
||||
|
|
|
@ -307,9 +307,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Stránky"
|
||||
integration: "Integrace"
|
||||
connectService: "Připojit"
|
||||
disconnectService: "Odpojit"
|
||||
enableLocalTimeline: "Povolit lokální čas"
|
||||
enableGlobalTimeline: "Povolit globální čas"
|
||||
registration: "Registrace"
|
||||
|
|
|
@ -350,9 +350,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Seiten"
|
||||
integration: "Integration"
|
||||
connectService: "Verbinden"
|
||||
disconnectService: "Trennen"
|
||||
enableLocalTimeline: "Lokale Chronik aktivieren"
|
||||
enableGlobalTimeline: "Globale Chronik aktivieren"
|
||||
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle\
|
||||
|
@ -793,7 +790,6 @@ receiveAnnouncementFromInstance: "Benachrichtigungen von dieser Instanz empfange
|
|||
emailNotification: "Email-Benachrichtigungen"
|
||||
publish: "Veröffentlichen"
|
||||
inChannelSearch: "In Kanal suchen"
|
||||
useReactionPickerForContextMenu: "Reaktionsauswahl durch Rechtsklick öffnen"
|
||||
typingUsers: "{users} ist/sind am schreiben …"
|
||||
jumpToSpecifiedDate: "Zu bestimmtem Datum springen"
|
||||
showingPastTimeline: "Es wird eine alte Chronik angezeigt"
|
||||
|
|
|
@ -340,9 +340,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pages"
|
||||
integration: "Integration"
|
||||
connectService: "Connect"
|
||||
disconnectService: "Disconnect"
|
||||
enableLocalTimeline: "Enable local timeline"
|
||||
enableGlobalTimeline: "Enable global timeline"
|
||||
disablingTimelinesInfo: "Adminstrators and Moderators will always have access to all\
|
||||
|
@ -775,7 +772,6 @@ receiveAnnouncementFromInstance: "Receive notifications from this instance"
|
|||
emailNotification: "Email notifications"
|
||||
publish: "Publish"
|
||||
inChannelSearch: "Search in channel"
|
||||
useReactionPickerForContextMenu: "Open reaction picker on right-click"
|
||||
typingUsers: "{users} is/are typing..."
|
||||
jumpToSpecifiedDate: "Jump to specific date"
|
||||
showingPastTimeline: "Currently displaying an old timeline"
|
||||
|
@ -1512,13 +1508,3 @@ _deck:
|
|||
list: "List"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct notes"
|
||||
_services:
|
||||
_discord:
|
||||
connected: "Discord: @{username}#{discriminator} connected to FoundKey: @{mkUsername}!"
|
||||
disconnected: "Discord linkage has been removed."
|
||||
_twitter:
|
||||
connected: "Twitter: @{twitterUserName} connected to FoundKey: @{userName}!"
|
||||
disconnected: "Twitter linkage has been removed."
|
||||
_github:
|
||||
connected: "GitHub: @{login} connected to FoundKey: @{userName}!"
|
||||
disconnected: "GitHub linkage has been removed."
|
||||
|
|
|
@ -342,9 +342,6 @@ dayX: "Día {day}"
|
|||
monthX: "Mes {month}"
|
||||
yearX: "Año {year}"
|
||||
pages: "Páginas"
|
||||
integration: "Integración"
|
||||
connectService: "Conectar"
|
||||
disconnectService: "Desconectar"
|
||||
enableLocalTimeline: "Habilitar linea de tiempo local"
|
||||
enableGlobalTimeline: "Habilitar linea de tiempo global"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia\
|
||||
|
@ -776,8 +773,6 @@ receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
|
|||
emailNotification: "Notificaciones por correo electrónico"
|
||||
publish: "Publicar"
|
||||
inChannelSearch: "Buscar en el canal"
|
||||
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu\
|
||||
\ de reacciones"
|
||||
typingUsers: "{users} está escribiendo"
|
||||
jumpToSpecifiedDate: "Saltar a una fecha específica"
|
||||
showingPastTimeline: "Mostrar líneas de tiempo antiguas"
|
||||
|
|
|
@ -342,9 +342,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pages"
|
||||
integration: "Intégrations"
|
||||
connectService: "Connexion"
|
||||
disconnectService: "Déconnexion"
|
||||
enableLocalTimeline: "Activer le fil local"
|
||||
enableGlobalTimeline: "Activer le fil global"
|
||||
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s\
|
||||
|
@ -785,7 +782,6 @@ receiveAnnouncementFromInstance: "Recevoir les messages d'information de l'insta
|
|||
emailNotification: "Notifications par mail"
|
||||
publish: "Public"
|
||||
inChannelSearch: "Chercher dans le canal"
|
||||
useReactionPickerForContextMenu: "Clic-droit pour ouvrir le panneau de réactions"
|
||||
typingUsers: "{users} est en train d'écrire"
|
||||
jumpToSpecifiedDate: "Se rendre à la date"
|
||||
showingPastTimeline: "Un fil ancien est affiché"
|
||||
|
|
|
@ -341,9 +341,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Halaman"
|
||||
integration: "Integrasi"
|
||||
connectService: "Sambungkan"
|
||||
disconnectService: "Putuskan"
|
||||
enableLocalTimeline: "Nyalakan linimasa lokal"
|
||||
enableGlobalTimeline: "Nyalakan linimasa global"
|
||||
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa\
|
||||
|
@ -780,7 +777,6 @@ receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini"
|
|||
emailNotification: "Pemberitahuan surel"
|
||||
publish: "Terbitkan"
|
||||
inChannelSearch: "Cari di kanal"
|
||||
useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan"
|
||||
typingUsers: "{users} sedang mengetik..."
|
||||
jumpToSpecifiedDate: "Loncat ke tanggal spesifik"
|
||||
showingPastTimeline: "Sedang menampilkan linimasa lama"
|
||||
|
|
|
@ -335,9 +335,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pagine"
|
||||
integration: "App collegate"
|
||||
connectService: "Connessione"
|
||||
disconnectService: "Disconnessione "
|
||||
enableLocalTimeline: "Abilita Timeline locale"
|
||||
enableGlobalTimeline: "Abilita Timeline federata"
|
||||
disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori e\
|
||||
|
@ -762,8 +759,6 @@ receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza"
|
|||
emailNotification: "Eventi per notifiche via mail"
|
||||
publish: "Pubblico"
|
||||
inChannelSearch: "Cerca in canale"
|
||||
useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello\
|
||||
\ di reazioni"
|
||||
typingUsers: "{users} sta(nno) scrivendo"
|
||||
jumpToSpecifiedDate: "Vai alla data "
|
||||
showingPastTimeline: "Stai visualizzando una vecchia timeline"
|
||||
|
|
|
@ -317,9 +317,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
connectService: "接続する"
|
||||
disconnectService: "切断する"
|
||||
enableLocalTimeline: "ローカルタイムラインを有効にする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを有効にする"
|
||||
disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。"
|
||||
|
@ -721,7 +718,6 @@ receiveAnnouncementFromInstance: "インスタンスからのお知らせを受
|
|||
emailNotification: "メール通知"
|
||||
publish: "公開"
|
||||
inChannelSearch: "チャンネル内検索"
|
||||
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開く"
|
||||
typingUsers: "{users}が入力中"
|
||||
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||
showingPastTimeline: "過去のタイムラインを表示しています"
|
||||
|
@ -1450,13 +1446,3 @@ _deck:
|
|||
list: "リスト"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
_services:
|
||||
_discord:
|
||||
connected: "Discord: @{username}#{discriminator} を、FoundKey: @{mkUsername} に接続しました!"
|
||||
disconnected: "Discordの連携を解除しました :v:"
|
||||
_twitter:
|
||||
connected: "Twitter: @{twitterUserName} を、FoundKey: @{userName} に接続しました!"
|
||||
disconnected: "Twitterの連携を解除しました :v:"
|
||||
_github:
|
||||
connected: "GitHub: @{login} を、FoundKey: @{userName} に接続しました!"
|
||||
disconnected: "GitHubの連携を解除しました :v:"
|
||||
|
|
|
@ -319,7 +319,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
integration: "連携"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||
|
@ -633,7 +632,6 @@ apply: "適用"
|
|||
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
||||
emailNotification: "メール通知"
|
||||
inChannelSearch: "チャンネル内検索"
|
||||
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開くようにする"
|
||||
typingUsers: "{users}が今書きよるで"
|
||||
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||
showingPastTimeline: "過去のタイムラインを表示してるで"
|
||||
|
|
|
@ -315,9 +315,6 @@ dayX: "{day}일"
|
|||
monthX: "{month}월"
|
||||
yearX: "{year}년"
|
||||
pages: "페이지"
|
||||
integration: "연동"
|
||||
connectService: "계정 연동"
|
||||
disconnectService: "계정 연동 해제"
|
||||
enableLocalTimeline: "로컬 타임라인 활성화"
|
||||
enableGlobalTimeline: "글로벌 타임라인 활성화"
|
||||
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
|
||||
|
@ -716,7 +713,6 @@ receiveAnnouncementFromInstance: "이 인스턴스의 알림을 이메일로 수
|
|||
emailNotification: "메일 알림"
|
||||
publish: "게시"
|
||||
inChannelSearch: "채널에서 검색"
|
||||
useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
|
||||
typingUsers: "{users} 님이 입력하고 있어요.."
|
||||
jumpToSpecifiedDate: "특정 날짜로 이동"
|
||||
showingPastTimeline: "과거의 타임라인을 표시하고 있어요"
|
||||
|
|
|
@ -329,9 +329,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Strony"
|
||||
integration: "Integracja"
|
||||
connectService: "Połącz"
|
||||
disconnectService: "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\
|
||||
|
@ -737,7 +734,6 @@ receiveAnnouncementFromInstance: "Otrzymuj powiadomienia e-mail z tej instancji"
|
|||
emailNotification: "Powiadomienia e-mail"
|
||||
publish: "Publikuj"
|
||||
inChannelSearch: "Szukaj na kanale"
|
||||
useReactionPickerForContextMenu: "Otwórz wybornik reakcji prawym kliknięciem"
|
||||
typingUsers: "{users} pisze(-ą)..."
|
||||
jumpToSpecifiedDate: "Przejdź do określonej daty"
|
||||
showingPastTimeline: "Obecnie wyświetla starą oś czasu"
|
||||
|
|
|
@ -342,9 +342,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Pagini"
|
||||
integration: "Integrare"
|
||||
connectService: "Conectează"
|
||||
disconnectService: "Deconectează"
|
||||
enableLocalTimeline: "Activează cronologia locală"
|
||||
enableGlobalTimeline: "Activeaza cronologia globală"
|
||||
disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate\
|
||||
|
|
|
@ -335,9 +335,6 @@ dayX: "{day} день"
|
|||
monthX: "{month} месяц"
|
||||
yearX: "{year} год"
|
||||
pages: "Страницы"
|
||||
integration: "Интеграция"
|
||||
connectService: "Подключиться"
|
||||
disconnectService: "Отключиться"
|
||||
enableLocalTimeline: "Включить локальную ленту"
|
||||
enableGlobalTimeline: "Включить глобальную ленту"
|
||||
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам,\
|
||||
|
@ -770,7 +767,6 @@ receiveAnnouncementFromInstance: "Получать оповещения с ин
|
|||
emailNotification: "Уведомления по электронной почте"
|
||||
publish: "Опубликовать"
|
||||
inChannelSearch: "Поиск по каналу"
|
||||
useReactionPickerForContextMenu: "Открывать палитру реакций правой кнопкой"
|
||||
typingUsers: "Стук клавиш. Это {users}…"
|
||||
jumpToSpecifiedDate: "Перейти к заданной дате"
|
||||
showingPastTimeline: "Отображается старая лента"
|
||||
|
|
|
@ -336,9 +336,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Stránky"
|
||||
integration: "Integrácia"
|
||||
connectService: "Pripojiť"
|
||||
disconnectService: "Odpojiť"
|
||||
enableLocalTimeline: "Povoliť lokálnu časovú os"
|
||||
enableGlobalTimeline: "Povoliť globálnu časovú os"
|
||||
disablingTimelinesInfo: "Administrátori a moderátori majú vždy prístup ku všetkým\
|
||||
|
@ -761,7 +758,6 @@ receiveAnnouncementFromInstance: "Prijať notifikácie z tohoto servera"
|
|||
emailNotification: "Emailové upozornenia"
|
||||
publish: "Zverejniť"
|
||||
inChannelSearch: "Hľadať v kanáli"
|
||||
useReactionPickerForContextMenu: "Otvoriť výber reakcií na pravý klik"
|
||||
typingUsers: "{users} píše/u"
|
||||
jumpToSpecifiedDate: "Skočiť na konkrétny dátum"
|
||||
showingPastTimeline: "Práve vidíte starú časovú os"
|
||||
|
|
|
@ -336,9 +336,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Сторінки"
|
||||
integration: "Інтеграція"
|
||||
connectService: "Під’єднати"
|
||||
disconnectService: "Відключитися"
|
||||
enableLocalTimeline: "Увімкнути локальну стрічку"
|
||||
enableGlobalTimeline: "Увімкнути глобальну стрічку"
|
||||
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх\
|
||||
|
@ -768,7 +765,6 @@ receiveAnnouncementFromInstance: "Отримувати оповіщення з
|
|||
emailNotification: "Сповіщення електронною поштою"
|
||||
publish: "Опублікувати"
|
||||
inChannelSearch: "Пошук за каналом"
|
||||
useReactionPickerForContextMenu: "Відкривати палітру реакцій правою кнопкою"
|
||||
typingUsers: "Стук клавіш. Це {users}…"
|
||||
goBack: "Назад"
|
||||
info: "Інформація"
|
||||
|
|
|
@ -336,9 +336,6 @@ dayX: "{day}"
|
|||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Trang"
|
||||
integration: "Tương tác"
|
||||
connectService: "Kết nối"
|
||||
disconnectService: "Ngắt kết nối"
|
||||
enableLocalTimeline: "Bật bảng tin máy chủ"
|
||||
enableGlobalTimeline: "Bật bảng tin liên hợp"
|
||||
disablingTimelinesInfo: "Quản trị viên và Kiểm duyệt viên luôn có quyền truy cập mọi\
|
||||
|
@ -767,7 +764,6 @@ receiveAnnouncementFromInstance: "Nhận thông báo từ máy chủ này"
|
|||
emailNotification: "Thông báo email"
|
||||
publish: "Đăng"
|
||||
inChannelSearch: "Tìm trong kênh"
|
||||
useReactionPickerForContextMenu: "Nhấn chuột phải để mở bộ chọn biểu cảm"
|
||||
typingUsers: "{users} đang nhập…"
|
||||
jumpToSpecifiedDate: "Đến một ngày cụ thể"
|
||||
showingPastTimeline: "Hiện đang hiển thị dòng thời gian cũ"
|
||||
|
|
|
@ -315,9 +315,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "页面"
|
||||
integration: "关联"
|
||||
connectService: "连接"
|
||||
disconnectService: "断开连接"
|
||||
enableLocalTimeline: "启用本地时间线功能"
|
||||
enableGlobalTimeline: "启用全局时间线"
|
||||
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
|
||||
|
@ -717,7 +714,6 @@ receiveAnnouncementFromInstance: "从实例接收通知"
|
|||
emailNotification: "邮件通知"
|
||||
publish: "发布"
|
||||
inChannelSearch: "频道内搜索"
|
||||
useReactionPickerForContextMenu: "单击右键打开回应工具栏"
|
||||
typingUsers: "{users}正在输入"
|
||||
jumpToSpecifiedDate: "跳转到特定日期"
|
||||
showingPastTimeline: "显示过去的时间线"
|
||||
|
|
|
@ -315,9 +315,6 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "頁面"
|
||||
integration: "整合"
|
||||
connectService: "己連結"
|
||||
disconnectService: "己斷開 "
|
||||
enableLocalTimeline: "開啟本地時間軸"
|
||||
enableGlobalTimeline: "啟用公開時間軸"
|
||||
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
|
||||
|
@ -716,7 +713,6 @@ receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
|
|||
emailNotification: "郵件通知"
|
||||
publish: "發佈"
|
||||
inChannelSearch: "頻道内搜尋"
|
||||
useReactionPickerForContextMenu: "點擊右鍵開啟回應工具欄"
|
||||
typingUsers: "{users}輸入中..."
|
||||
jumpToSpecifiedDate: "跳轉到特定日期"
|
||||
showingPastTimeline: "顯示過往的時間線"
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"@types/node": "^18.7.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "5.1.1",
|
||||
|
@ -48,7 +49,6 @@
|
|||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.8.3"
|
||||
},
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 200 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 118 KiB |
|
@ -15,8 +15,8 @@
|
|||
"test": "npm run mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.2.2",
|
||||
"@bull-board/koa": "4.0.0",
|
||||
"@bull-board/api": "^4.3.1",
|
||||
"@bull-board/koa": "^4.3.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@koa/cors": "3.1.0",
|
||||
|
@ -29,7 +29,6 @@
|
|||
"ajv": "8.11.0",
|
||||
"archiver": "5.3.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1165.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
|
@ -121,13 +120,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.97",
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.8",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "^2.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/is-url": "1.2.30",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsbn": "^1.2.30",
|
||||
"@types/jsdom": "16.2.14",
|
||||
"@types/jsonld": "1.5.6",
|
||||
"@types/jsrsasign": "10.5.1",
|
||||
|
@ -142,11 +144,12 @@
|
|||
"@types/koa__cors": "3.1.1",
|
||||
"@types/koa__multer": "2.0.4",
|
||||
"@types/koa__router": "8.0.11",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/node": "18.7.16",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.5",
|
||||
"@types/oauth": "^0.9.1",
|
||||
"@types/opentype.js": "^1.3.4",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
|
@ -154,18 +157,22 @@
|
|||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.3",
|
||||
"@types/redis": "4.0.11",
|
||||
"@types/redis-info": "^3.0.0",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/sharp": "0.30.5",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/speakeasy": "2.0.7",
|
||||
"@types/syslog-pro": "^1.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/unzipper": "^0.10.5",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"cross-env": "7.0.3",
|
||||
|
|
11
packages/backend/src/@types/redis-lock.d.ts
vendored
Normal file
11
packages/backend/src/@types/redis-lock.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
declare module 'redis-lock' {
|
||||
import { promisify } from 'util';
|
||||
import { Redis } from 'ioredis';
|
||||
export type unlockFunction = (done?: () => void) => void;
|
||||
interface lockFactory {
|
||||
(lockName: string, timeout: number, taskToPerform: (unlock: unlockFunction) => void): void;
|
||||
[promisify.custom](lockName: string, timeout: number): Promise<unlockFunction>;
|
||||
}
|
||||
function lock (client: Redis, retryDelay: number): lockFactory;
|
||||
export default lock;
|
||||
}
|
4
packages/backend/src/@types/twemoji-parser__dist__lib__regex.d.ts
vendored
Normal file
4
packages/backend/src/@types/twemoji-parser__dist__lib__regex.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module 'twemoji-parser/dist/lib/regex.js' {
|
||||
const a: {'default': RegExp};
|
||||
export default a;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { promisify } from 'node:util';
|
||||
import redisLock from 'redis-lock';
|
||||
import redisLock, { unlockFunction } from 'redis-lock';
|
||||
import { redisClient } from '@/db/redis.js';
|
||||
|
||||
/**
|
||||
|
@ -18,14 +18,14 @@ const lock: (key: string, timeout?: number) => Promise<() => void>
|
|||
* @param timeout Lock timeout (ms), The timeout releases previous lock.
|
||||
* @returns Unlock function
|
||||
*/
|
||||
export function getApLock(uri: string, timeout = 30 * 1000) {
|
||||
return lock(`ap-object:${uri}`, timeout);
|
||||
export async function getApLock(uri: string, timeout: number = 30 * 1000): Promise<unlockFunction> {
|
||||
return await lock(`ap-object:${uri}`, timeout);
|
||||
}
|
||||
|
||||
export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
|
||||
return lock(`instance:${host}`, timeout);
|
||||
export async function getFetchInstanceMetadataLock(host: string, timeout: number = 30 * 1000): Promise<unlockFunction> {
|
||||
return await lock(`instance:${host}`, timeout);
|
||||
}
|
||||
|
||||
export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
|
||||
return lock(`chart-insert:${lockKey}`, timeout);
|
||||
export async function getChartInsertLock(lockKey: string, timeout: number = 30 * 1000): Promise<unlockFunction> {
|
||||
return await lock(`chart-insert:${lockKey}`, timeout);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class I18n<T extends Record<string, any>> {
|
|||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, any>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||
let str = key.split('.').reduce((o, i) => o[i] as Record<string, any> | string, this.locale) as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Note } from '@/models/entities/note.js';
|
||||
|
||||
export function isPureRenote(note: Note): boolean {
|
||||
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
|
||||
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
|
|||
* @param user http-signature user
|
||||
* @param url URL to fetch
|
||||
*/
|
||||
export async function signedGet(url: string, user: { id: User['id'] }) {
|
||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedGet({
|
||||
|
|
|
@ -15,8 +15,8 @@ export class AuthenticationError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
let token: string | null = null;
|
||||
export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
let maybeToken: string | null = null;
|
||||
|
||||
// check if there is an authorization header set
|
||||
if (authorization != null) {
|
||||
|
@ -27,15 +27,16 @@ export default async (authorization: string | null | undefined, bodyToken: strin
|
|||
// check if OAuth 2.0 Bearer tokens are being used
|
||||
// Authorization schemes are case insensitive
|
||||
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
|
||||
token = authorization.substring(7);
|
||||
maybeToken = authorization.substring(7);
|
||||
} else {
|
||||
throw new AuthenticationError('unsupported authentication scheme');
|
||||
}
|
||||
} else if (bodyToken != null) {
|
||||
token = bodyToken;
|
||||
maybeToken = bodyToken;
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
const token: string = maybeToken;
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await localUserByNativeTokenCache.fetch(token,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null) {
|
||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null, followingQuery: SelectQueryBuilder<any> | null) {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
|
@ -14,6 +14,7 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
|
|||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
||||
.orWhere('note.mentions && array[:meId]::varchar[]', { meId: me.id })
|
||||
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.userId = :meId', { meId: me.id });
|
||||
|
@ -22,6 +23,12 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
|
|||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}));
|
||||
if (followingQuery !== null) {
|
||||
qb.orWhere(new Brackets(qb => { qb
|
||||
.where(`note.mentions && array(${ followingQuery.getQuery() })`)
|
||||
.setParameters(followingQuery.getParameters())
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,18 +101,6 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableTwitterIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableServiceWorker: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -166,30 +154,6 @@ export const meta = {
|
|||
optional: true, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
twitterConsumerKey: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
twitterConsumerSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
githubClientId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
githubClientSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
discordClientId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
discordClientSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
summaryProxy: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
|
@ -314,9 +278,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
pinnedPages: instance.pinnedPages,
|
||||
|
@ -330,12 +291,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
twitterConsumerKey: instance.twitterConsumerKey,
|
||||
twitterConsumerSecret: instance.twitterConsumerSecret,
|
||||
githubClientId: instance.githubClientId,
|
||||
githubClientSecret: instance.githubClientSecret,
|
||||
discordClientId: instance.discordClientId,
|
||||
discordClientSecret: instance.discordClientSecret,
|
||||
summalyProxy: instance.summalyProxy,
|
||||
email: instance.email,
|
||||
smtpSecure: instance.smtpSecure,
|
||||
|
|
|
@ -58,15 +58,6 @@ export const paramDef = {
|
|||
summalyProxy: { type: 'string', nullable: true },
|
||||
deeplAuthKey: { type: 'string', nullable: true },
|
||||
deeplIsPro: { type: 'boolean' },
|
||||
enableTwitterIntegration: { type: 'boolean' },
|
||||
twitterConsumerKey: { type: 'string', nullable: true },
|
||||
twitterConsumerSecret: { type: 'string', nullable: true },
|
||||
enableGithubIntegration: { type: 'boolean' },
|
||||
githubClientId: { type: 'string', nullable: true },
|
||||
githubClientSecret: { type: 'string', nullable: true },
|
||||
enableDiscordIntegration: { type: 'boolean' },
|
||||
discordClientId: { type: 'string', nullable: true },
|
||||
discordClientSecret: { type: 'string', nullable: true },
|
||||
enableEmail: { type: 'boolean' },
|
||||
email: { type: 'string', nullable: true },
|
||||
smtpSecure: { type: 'boolean' },
|
||||
|
@ -231,42 +222,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.summalyProxy = ps.summalyProxy;
|
||||
}
|
||||
|
||||
if (ps.enableTwitterIntegration !== undefined) {
|
||||
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerKey !== undefined) {
|
||||
set.twitterConsumerKey = ps.twitterConsumerKey;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerSecret !== undefined) {
|
||||
set.twitterConsumerSecret = ps.twitterConsumerSecret;
|
||||
}
|
||||
|
||||
if (ps.enableGithubIntegration !== undefined) {
|
||||
set.enableGithubIntegration = ps.enableGithubIntegration;
|
||||
}
|
||||
|
||||
if (ps.githubClientId !== undefined) {
|
||||
set.githubClientId = ps.githubClientId;
|
||||
}
|
||||
|
||||
if (ps.githubClientSecret !== undefined) {
|
||||
set.githubClientSecret = ps.githubClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableDiscordIntegration !== undefined) {
|
||||
set.enableDiscordIntegration = ps.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
if (ps.discordClientId !== undefined) {
|
||||
set.discordClientId = ps.discordClientId;
|
||||
}
|
||||
|
||||
if (ps.discordClientSecret !== undefined) {
|
||||
set.discordClientSecret = ps.discordClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableEmail !== undefined) {
|
||||
set.enableEmail = ps.enableEmail;
|
||||
}
|
||||
|
|
|
@ -170,18 +170,6 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableTwitterIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableServiceWorker: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -226,18 +214,6 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
twitter: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
github: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
discord: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
serviceWorker: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -317,10 +293,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
|
@ -343,9 +315,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
hcaptcha: instance.enableHcaptcha,
|
||||
recaptcha: instance.enableRecaptcha,
|
||||
objectStorage: instance.useObjectStorage,
|
||||
twitter: instance.enableTwitterIntegration,
|
||||
github: instance.enableGithubIntegration,
|
||||
discord: instance.enableDiscordIntegration,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
miauth: true,
|
||||
},
|
||||
|
|
|
@ -75,7 +75,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, null);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -89,7 +89,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, followingQuery);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -82,7 +82,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, null);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -81,7 +81,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, followingQuery);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -15,9 +15,6 @@ import handler from './api-handler.js';
|
|||
import signup from './private/signup.js';
|
||||
import signin from './private/signin.js';
|
||||
import signupPending from './private/signup-pending.js';
|
||||
import discord from './service/discord.js';
|
||||
import github from './service/github.js';
|
||||
import twitter from './service/twitter.js';
|
||||
|
||||
// Init app
|
||||
const app = new Koa();
|
||||
|
@ -81,10 +78,6 @@ router.post('/signup', signup);
|
|||
router.post('/signin', signin);
|
||||
router.post('/signup-pending', signupPending);
|
||||
|
||||
router.use(discord.routes());
|
||||
router.use(github.routes());
|
||||
router.use(twitter.routes());
|
||||
|
||||
router.get('/v1/instance/peers', async ctx => {
|
||||
const instances = await Instances.find({
|
||||
select: ['host'],
|
||||
|
|
|
@ -1,298 +0,0 @@
|
|||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { redisClient } from '@/db/redis.js';
|
||||
import { I18n } from '@/misc/i18n.js';
|
||||
import signin from '../common/signin.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(config.url));
|
||||
}
|
||||
|
||||
const locales = await import('../../../../../../locales/index.js').then(mod => mod.default);
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/discord', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
delete profile.integrations.discord;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._discord.disconnected');
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
});
|
||||
|
||||
async function getOAuth2() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (meta.enableDiscordIntegration) {
|
||||
return new OAuth2(
|
||||
meta.discordClientId!,
|
||||
meta.discordClientSecret!,
|
||||
'https://discord.com/',
|
||||
'api/oauth2/authorize',
|
||||
'api/oauth2/token');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/connect/discord', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/dc/cb`,
|
||||
scope: ['identify'],
|
||||
state: uuid(),
|
||||
response_type: 'code',
|
||||
};
|
||||
|
||||
redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/signin/discord', async ctx => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/dc/cb`,
|
||||
scope: ['identify'],
|
||||
state: uuid(),
|
||||
response_type: 'code',
|
||||
};
|
||||
|
||||
ctx.cookies.set('signin_with_discord_sid', sessid, {
|
||||
path: '/',
|
||||
secure: config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/dc/cb', async ctx => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const oauth2 = await getOAuth2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get('signin_with_discord_sid');
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(code, {
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri,
|
||||
}, (err, accessToken, refreshToken, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.createQueryBuilder()
|
||||
.where('"integrations"->\'discord\'->>\'id\' = :id', { id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (profile == null) {
|
||||
ctx.throw(404, `There were no FoundKey accounts linked to @${username}#${discriminator}...`);
|
||||
return;
|
||||
}
|
||||
|
||||
await UserProfiles.update(profile.userId, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
discord: {
|
||||
id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate,
|
||||
username,
|
||||
discriminator,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
signin(ctx, await Users.findOneBy({ id: profile.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(code, {
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri,
|
||||
}, (err, accessToken, refreshToken, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
discord: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresDate,
|
||||
id,
|
||||
username,
|
||||
discriminator,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._discord.connected', {
|
||||
username,
|
||||
discriminator,
|
||||
mkUsername: user.username,
|
||||
});
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,269 +0,0 @@
|
|||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { redisClient } from '@/db/redis.js';
|
||||
import signin from '../common/signin.js';
|
||||
import { I18n } from '@/misc/i18n.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(config.url));
|
||||
}
|
||||
|
||||
const locales = await import('../../../../../../locales/index.js').then(mod => mod.default);
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/github', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
delete profile.integrations.github;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._github.disconnected');
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
});
|
||||
|
||||
async function getOath2() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
|
||||
return new OAuth2(
|
||||
meta.githubClientId,
|
||||
meta.githubClientSecret,
|
||||
'https://github.com/',
|
||||
'login/oauth/authorize',
|
||||
'login/oauth/access_token');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/connect/github', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (!userToken) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/gh/cb`,
|
||||
scope: ['read:user'],
|
||||
state: uuid(),
|
||||
};
|
||||
|
||||
redisClient.set(userToken, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/signin/github', async ctx => {
|
||||
const sessid = uuid();
|
||||
|
||||
const params = {
|
||||
redirect_uri: `${config.url}/api/gh/cb`,
|
||||
scope: ['read:user'],
|
||||
state: uuid(),
|
||||
};
|
||||
|
||||
ctx.cookies.set('signin_with_github_sid', sessid, {
|
||||
path: '/',
|
||||
secure: config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(params));
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
ctx.redirect(oauth2!.getAuthorizeUrl(params));
|
||||
});
|
||||
|
||||
router.get('/gh/cb', async ctx => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const oauth2 = await getOath2();
|
||||
|
||||
if (!userToken) {
|
||||
const sessid = ctx.cookies.get('signin_with_github_sid');
|
||||
|
||||
if (!sessid) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(code, {
|
||||
redirect_uri,
|
||||
}, (err, accessToken, refresh, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({ accessToken });
|
||||
}
|
||||
}));
|
||||
|
||||
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
'Authorization': `bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where('"integrations"->\'github\'->>\'id\' = :id', { id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(404, `There were no FoundKey accounts linked to @${login}...`);
|
||||
return;
|
||||
}
|
||||
|
||||
signin(ctx, await Users.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, state) => {
|
||||
res(JSON.parse(state));
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.query.state !== state) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||
oauth2!.getOAuthAccessToken(
|
||||
code,
|
||||
{ redirect_uri },
|
||||
(err, accessToken, refresh, result) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (result.error) {
|
||||
rej(result.error);
|
||||
} else {
|
||||
res({ accessToken });
|
||||
}
|
||||
}));
|
||||
|
||||
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
'Authorization': `bearer ${accessToken}`,
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
github: {
|
||||
accessToken,
|
||||
id,
|
||||
login,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._github.connected', {
|
||||
login,
|
||||
userName: user.username,
|
||||
});
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,211 +0,0 @@
|
|||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import autwh from 'autwh';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import config from '@/config/index.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { redisClient } from '@/db/redis.js';
|
||||
import signin from '../common/signin.js';
|
||||
import { I18n } from '@/misc/i18n.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext): boolean {
|
||||
function normalizeUrl(url?: string): string {
|
||||
return url == null ? '' : url.endsWith('/') ? url.substr(0, url.length - 1) : url;
|
||||
}
|
||||
|
||||
const referer = ctx.headers['referer'];
|
||||
|
||||
return (normalizeUrl(referer) === normalizeUrl(config.url));
|
||||
}
|
||||
|
||||
const locales = await import('../../../../../../locales/index.js').then(mod => mod.default);
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get('/disconnect/twitter', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
delete profile.integrations.twitter;
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._twitter.disconnected');
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
});
|
||||
|
||||
async function getTwAuth() {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
|
||||
return autwh({
|
||||
consumerKey: meta.twitterConsumerKey,
|
||||
consumerSecret: meta.twitterConsumerSecret,
|
||||
callbackUrl: `${config.url}/api/tw/cb`,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/connect/twitter', async ctx => {
|
||||
if (!compareOrigin(ctx)) {
|
||||
ctx.throw(400, 'invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = getUserToken(ctx);
|
||||
if (userToken == null) {
|
||||
ctx.throw(400, 'signin required');
|
||||
return;
|
||||
}
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
redisClient.set(userToken, JSON.stringify(twCtx));
|
||||
ctx.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get('/signin/twitter', async ctx => {
|
||||
const twAuth = await getTwAuth();
|
||||
const twCtx = await twAuth!.begin();
|
||||
|
||||
const sessid = uuid();
|
||||
|
||||
redisClient.set(sessid, JSON.stringify(twCtx));
|
||||
|
||||
ctx.cookies.set('signin_with_twitter_sid', sessid, {
|
||||
path: '/',
|
||||
secure: config.url.startsWith('https'),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
ctx.redirect(twCtx.url);
|
||||
});
|
||||
|
||||
router.get('/tw/cb', async ctx => {
|
||||
const userToken = getUserToken(ctx);
|
||||
|
||||
const twAuth = await getTwAuth();
|
||||
|
||||
if (userToken == null) {
|
||||
const sessid = ctx.cookies.get('signin_with_twitter_sid');
|
||||
|
||||
if (sessid == null) {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
redisClient.get(sessid, async (_, twCtx) => {
|
||||
res(twCtx);
|
||||
});
|
||||
});
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
if (link == null) {
|
||||
ctx.throw(404, `There were no FoundKey accounts linked to @${result.screenName}...`);
|
||||
return;
|
||||
}
|
||||
|
||||
signin(ctx, await Users.findOneBy({ id: link.userId }) as ILocalUser, true);
|
||||
} else {
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const get = new Promise<any>((res, rej) => {
|
||||
redisClient.get(userToken, async (_, twCtx) => {
|
||||
res(twCtx);
|
||||
});
|
||||
});
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
|
||||
const user = await Users.findOneByOrFail({
|
||||
host: IsNull(),
|
||||
token: userToken,
|
||||
});
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
const locale = locales[profile.lang || 'en-US'];
|
||||
const i18n = new I18n(locale);
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
integrations: {
|
||||
...profile.integrations,
|
||||
twitter: {
|
||||
accessToken: result.accessToken,
|
||||
accessTokenSecret: result.accessTokenSecret,
|
||||
userId: result.userId,
|
||||
screenName: result.screenName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = i18n.t('_services._twitter.connected', {
|
||||
twitterUserName: result.screenName,
|
||||
userName: user.username,
|
||||
});
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -62,21 +62,21 @@ export default abstract class Channel {
|
|||
});
|
||||
}
|
||||
|
||||
protected withPackedNote(callback: (note: Packed<'Note'>) => void): (Note) => void {
|
||||
protected withPackedNote(callback: (note: Packed<'Note'>) => Promise<void>): (note: Note) => Promise<void> {
|
||||
return async (note: Note) => {
|
||||
try {
|
||||
// because `note` was previously JSON.stringify'ed, the fields that
|
||||
// were objects before are now strings and have to be restored or
|
||||
// removed from the object
|
||||
note.createdAt = new Date(note.createdAt);
|
||||
delete note.reply;
|
||||
delete note.renote;
|
||||
delete note.user;
|
||||
delete note.channel;
|
||||
note.reply = null;
|
||||
note.renote = null;
|
||||
note.user = null;
|
||||
note.channel = null;
|
||||
|
||||
const packed = await Notes.pack(note, this.user, { detail: true });
|
||||
|
||||
callback(packed);
|
||||
await callback(packed);
|
||||
} catch (err) {
|
||||
if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
|
||||
// skip: note not visible to user
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Users } from '@/models/index.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
|
@ -12,10 +13,11 @@ export default class extends Channel {
|
|||
private channelId: string;
|
||||
private typers: Record<User['id'], Date> = {};
|
||||
private emitTypersIntervalId: ReturnType<typeof setInterval>;
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -27,7 +29,7 @@ export default class extends Channel {
|
|||
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
if (note.channelId !== this.channelId) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
|
|||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -25,7 +27,7 @@ export default class extends Channel {
|
|||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
|
@ -8,10 +9,11 @@ export default class extends Channel {
|
|||
public static shouldShare = false;
|
||||
public static requireCredential = false;
|
||||
private q: string[][];
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -23,7 +25,7 @@ export default class extends Channel {
|
|||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
|
||||
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
|
||||
if (!matched) return;
|
||||
|
|
|
@ -2,16 +2,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -19,7 +21,7 @@ export default class extends Channel {
|
|||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
if (note.channelId) {
|
||||
if (!this.followingChannels.has(note.channelId)) return;
|
||||
} else {
|
||||
|
|
|
@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -23,7 +25,7 @@ export default class extends Channel {
|
|||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
// チャンネルの投稿ではなく、自分自身の投稿 または
|
||||
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
|
||||
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
|
||||
|
@ -38,11 +40,8 @@ export default class extends Channel {
|
|||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (note.reply && note.mentions && !this.user!.showTimelineReplies) {
|
||||
if (!note.mentions.includes(this.user!.id) && !note.mentions.some((user: string) => this.following.has(user))) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -2,16 +2,18 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
|||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -24,7 +26,7 @@ export default class extends Channel {
|
|||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
if (note.user.host !== null) return;
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { UserListJoinings, UserLists } from '@/models/index.js';
|
|||
import { User } from '@/models/entities/user.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
export default class extends Channel {
|
||||
|
@ -11,11 +12,12 @@ export default class extends Channel {
|
|||
private listId: string;
|
||||
public listUsers: User['id'][] = [];
|
||||
private listUsersClock: NodeJS.Timer;
|
||||
private onNote: (note: Note) => Promise<void>;
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.updateListUsers = this.updateListUsers.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
|
@ -48,7 +50,7 @@ export default class extends Channel {
|
|||
this.listUsers = users.map(x => x.userId);
|
||||
}
|
||||
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
|
||||
if (!this.listUsers.includes(note.userId)) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import Emitter from 'strict-event-emitter-types';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
|
@ -15,6 +14,7 @@ import { Signin } from '@/models/entities/signin.js';
|
|||
import { Page } from '@/models/entities/page.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import type { StrictEventEmitter as Emitter } from 'strict-event-emitter-types';
|
||||
|
||||
//#region Stream type-body definitions
|
||||
export interface InternalStreamTypes {
|
||||
|
|
|
@ -16,6 +16,10 @@ export const initializeStreamingServer = (server: http.Server): void => {
|
|||
|
||||
ws.on('request', async (request): Promise<void> => {
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
if (q.i instanceof Array) {
|
||||
request.reject(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const [user, app] = await authenticate(request.httpRequest.headers.authorization, q.i)
|
||||
.catch(err => {
|
||||
|
|
|
@ -138,13 +138,13 @@ export const startServer = () => {
|
|||
return server;
|
||||
};
|
||||
|
||||
export default () => new Promise(resolve => {
|
||||
export default (): Promise<void> => new Promise(resolve => {
|
||||
const server = createServer();
|
||||
|
||||
initializeStreamingServer(server);
|
||||
|
||||
server.on('error', e => {
|
||||
switch ((e as any).code) {
|
||||
switch ((e as NodeJS.ErrnoException).code) {
|
||||
case 'EACCES':
|
||||
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
|
||||
break;
|
||||
|
@ -164,5 +164,5 @@ export default () => new Promise(resolve => {
|
|||
}
|
||||
});
|
||||
|
||||
server.listen(config.port, resolve);
|
||||
server.listen(config.port, () => resolve());
|
||||
});
|
||||
|
|
|
@ -72,9 +72,6 @@ const nodeinfo2 = async () => {
|
|||
enableHcaptcha: meta.enableHcaptcha,
|
||||
enableRecaptcha: meta.enableRecaptcha,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
enableTwitterIntegration: meta.enableTwitterIntegration,
|
||||
enableGithubIntegration: meta.enableGithubIntegration,
|
||||
enableDiscordIntegration: meta.enableDiscordIntegration,
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
|
|
|
@ -13,13 +13,6 @@
|
|||
|
||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
||||
(async () => {
|
||||
window.onerror = (e) => {
|
||||
renderError('SOMETHING_HAPPENED', e);
|
||||
};
|
||||
window.onunhandledrejection = (e) => {
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||
};
|
||||
|
||||
const v = localStorage.getItem('v') || VERSION;
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
|
@ -102,54 +95,6 @@
|
|||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function renderError(code, details) {
|
||||
let errorsElement = document.getElementById('errors');
|
||||
if (!errorsElement) {
|
||||
document.getElementsByTagName("head")[0].insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<link rel="stylesheet" href="../error.css" />`);
|
||||
document.documentElement.innerHTML = `
|
||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
</svg>
|
||||
<h1>An error has occurred!</h1>
|
||||
<button class="button-big" onclick="location.reload(true);">
|
||||
<span class="button-label-big">Refresh</span>
|
||||
</button>
|
||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Flush preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Attempt to repair in Repair Tool</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<div id="errors"></div>
|
||||
`;
|
||||
|
||||
errorsElement = document.getElementById('errors');
|
||||
}
|
||||
|
||||
const detailsElement = document.createElement('details');
|
||||
detailsElement.innerHTML = `<br><summary><code>ERROR CODE: ${code}</code></summary>${JSON.stringify(details)}`;
|
||||
errorsElement.appendChild(detailsElement);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function checkUpdate() {
|
||||
// TODO: サーバーが落ちている場合などのエラーハンドリング
|
||||
|
|
|
@ -31,7 +31,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
|||
const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({
|
||||
url,
|
||||
lang: lang ?? 'ja-JP',
|
||||
})}`) : await summaly.default(url, {
|
||||
})}`) as Awaited<ReturnType<typeof summaly.default>> : await summaly.default(url, {
|
||||
followRedirects: false,
|
||||
lang: lang ?? 'ja-JP',
|
||||
});
|
||||
|
@ -53,8 +53,8 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
|||
}
|
||||
};
|
||||
|
||||
function wrap(url?: string): string | null {
|
||||
return url != null
|
||||
function wrap(url: string|null): string|null {
|
||||
return url !== null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `${config.url}/proxy/preview.webp?${query({
|
||||
url,
|
||||
|
|
|
@ -104,7 +104,7 @@ async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
|
|||
throw new Error('No wellknown links');
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
const links = wellknown.links;
|
||||
|
||||
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
|
|
|
@ -6,9 +6,10 @@ import * as SyslogPro from 'syslog-pro';
|
|||
import config from '@/config/index.js';
|
||||
import { envOption } from '@/env.js';
|
||||
|
||||
type KeywordColor = Parameters<typeof convertColor.keyword.rgb>[0];
|
||||
type Domain = {
|
||||
name: string;
|
||||
color?: string;
|
||||
color?: KeywordColor;
|
||||
};
|
||||
|
||||
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
|
||||
|
@ -17,9 +18,9 @@ export default class Logger {
|
|||
private domain: Domain;
|
||||
private parentLogger: Logger | null = null;
|
||||
private store: boolean;
|
||||
private syslogClient: any | null = null;
|
||||
private syslogClient: SyslogPro.RFC5424 | null = null;
|
||||
|
||||
constructor(domain: string, color?: string, store = true) {
|
||||
constructor(domain: string, color?: KeywordColor, store = true) {
|
||||
this.domain = {
|
||||
name: domain,
|
||||
color,
|
||||
|
@ -28,9 +29,9 @@ export default class Logger {
|
|||
|
||||
if (config.syslog) {
|
||||
this.syslogClient = new SyslogPro.RFC5424({
|
||||
applacationName: 'FoundKey',
|
||||
applicationName: 'FoundKey',
|
||||
timestamp: true,
|
||||
encludeStructuredData: true,
|
||||
includeStructuredData: true,
|
||||
color: true,
|
||||
extendedColor: true,
|
||||
server: {
|
||||
|
@ -41,7 +42,7 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
public createSubLogger(domain: string, color?: string, store = true): Logger {
|
||||
public createSubLogger(domain: string, color?: KeywordColor, store = true): Logger {
|
||||
const logger = new Logger(domain, color, store);
|
||||
logger.parentLogger = this;
|
||||
return logger;
|
||||
|
@ -57,7 +58,7 @@ export default class Logger {
|
|||
}
|
||||
|
||||
const time = dateFormat(new Date(), 'HH:mm:ss');
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
|
||||
const l =
|
||||
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
|
||||
level === 'warning' ? chalk.yellow('WARN') :
|
||||
|
@ -85,7 +86,6 @@ export default class Logger {
|
|||
level === 'error' ? this.syslogClient.error :
|
||||
level === 'warning' ? this.syslogClient.warning :
|
||||
level === 'success' ? this.syslogClient.info :
|
||||
level === 'debug' ? this.syslogClient.info :
|
||||
level === 'info' ? this.syslogClient.info :
|
||||
null as never;
|
||||
|
||||
|
@ -94,7 +94,7 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
public error(x: string | Error, data?: Record<string, any> = {}, important = false): void { // 実行を継続できない状況で使う
|
||||
public error(x: string | Error, data: Record<string, any> = {}, important = false): void { // 実行を継続できない状況で使う
|
||||
if (x instanceof Error) {
|
||||
data.e = x;
|
||||
this.log('error', x.toString(), data, important);
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function createMessage(user: { id: User['id']; host: User['host'];
|
|||
text: text ? text.trim() : null,
|
||||
userId: user.id,
|
||||
isRead: false,
|
||||
reads: [] as any[],
|
||||
reads: [] as string[],
|
||||
uri,
|
||||
} as MessagingMessage;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||
import { Brackets, FindOptionsWhere, In, IsNull, Not } from 'typeorm';
|
||||
import { publishNoteStream } from '@/services/stream.js';
|
||||
import renderDelete from '@/remote/activitypub/renderer/delete.js';
|
||||
import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
|
||||
|
@ -106,7 +106,7 @@ async function findCascadingNotes(note: Note): Promise<Note[]> {
|
|||
}
|
||||
|
||||
async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
|
||||
const where = [] as any[];
|
||||
const where: FindOptionsWhere<User>[] = [];
|
||||
|
||||
// mention / reply / dm
|
||||
if (note.mentions.length > 0) {
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vueuse/core": "9.1.0",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.13.0",
|
||||
"browser-image-resizer": "2.4.1",
|
||||
|
@ -86,7 +86,6 @@
|
|||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.17.7",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/seedrandom": "3.0.2",
|
||||
|
@ -98,10 +97,10 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^9.1.1",
|
||||
"rollup-plugin-sizes": "^1.0.4",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }">
|
||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1, supercompact }">
|
||||
<div class="main">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="body">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<XNoteHeader :supercompact="supercompact" class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
<MkNoteSubNoteContent class="text" :note="note"/>
|
||||
<MkNoteSubNoteContent class="text" :note="note" :supercompact="supercompact && note.cw == null" :compact-heads="compactHeads"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,11 +38,15 @@ const props = withDefaults(defineProps<{
|
|||
note: foundkey.entities.Note;
|
||||
conversation?: foundkey.entities.Note[] | null;
|
||||
|
||||
supercompact?: boolean;
|
||||
compactHeads?: boolean;
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
depth?: number;
|
||||
}>(), {
|
||||
conversation: null,
|
||||
depth: 1,
|
||||
supercompact: false,
|
||||
compactHeads: false,
|
||||
});
|
||||
|
||||
let showContent = $ref(false);
|
||||
|
@ -67,6 +71,13 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
|
|||
}
|
||||
}
|
||||
|
||||
&.supercompact {
|
||||
> .main > .avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
display: flex;
|
||||
|
||||
|
@ -76,7 +87,7 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
|
|||
margin: 0 8px 0 0;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
|
28
packages/client/src/components/NoteTooltip.vue
Normal file
28
packages/client/src/components/NoteTooltip.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<MkTooltip :showing="true" :target-element="targetElement" :max-width="400" @closed="emit('closed')">
|
||||
<div class="tooltip-note">
|
||||
<MkNoteSub v-for="note in notes" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'foundkey-js';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
import MkNoteSub from './MkNoteSub.vue';
|
||||
|
||||
defineProps<{
|
||||
notes: misskey.entities.Note[];
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-note {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
|
||||
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }">
|
||||
<ol v-if="type === 'user'" ref="suggests" class="users">
|
||||
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
|
||||
<img class="avatar" :src="user.avatarUrl"/>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
>
|
||||
|
@ -101,10 +100,6 @@ function onClick(ev: MouseEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
function onDragstart(ev: DragEvent): void {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@mouseover="onMouseover"
|
||||
@mouseout="onMouseout"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
|
@ -218,27 +217,6 @@ function deleteFolder() {
|
|||
});
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu([{
|
||||
text: i18n.ts.openInWindow,
|
||||
icon: 'fas fa-window-restore',
|
||||
action: () => {
|
||||
os.popup(defineAsyncComponent(() => import('./drive-window.vue')), {
|
||||
initialFolder: props.folder,
|
||||
}, {
|
||||
}, 'closed');
|
||||
},
|
||||
}, null, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: rename,
|
||||
}, null, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'fas fa-trash-alt',
|
||||
danger: true,
|
||||
action: deleteFolder,
|
||||
}], ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="yfudmmck">
|
||||
<nav>
|
||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||
<div class="path">
|
||||
<XNavFolder
|
||||
:class="{ current: folder == null }"
|
||||
:parent-folder="folder"
|
||||
|
@ -33,7 +33,6 @@
|
|||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents" class="contents">
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
||||
|
@ -601,10 +600,6 @@ function showMenu(ev: MouseEvent) {
|
|||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -32,39 +32,6 @@ const active = $computed(() => {
|
|||
return resolved.route.name === router.currentRoute.value.name;
|
||||
});
|
||||
|
||||
function onContextmenu(ev) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString() !== '') return;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: props.to,
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(props.to);
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(props.to);
|
||||
},
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(props.to, '_blank');
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${props.to}`);
|
||||
},
|
||||
}], ev);
|
||||
}
|
||||
|
||||
function nav() {
|
||||
if (props.behavior === 'browser') {
|
||||
location.href = props.to;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: defaultStore.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect handleHash" :class="{ cat: user.isCat, square: defaultStore.state.squareAvatars }" :style="{ color, background: handleHash }" :title="acct(user)" @click="onClick">
|
||||
<img class="inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
|
||||
</span>
|
||||
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: defaultStore.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
|
||||
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect handleHash" :class="{ cat: user.isCat, square: defaultStore.state.squareAvatars }" :style="{ color, background: handleHash }" :to="userPage(user)" :title="acct(user)" :target="target">
|
||||
<img class="inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
|
||||
</MkA>
|
||||
|
@ -39,6 +39,67 @@ const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
|
|||
? getStaticImageUrl(props.user.avatarUrl)
|
||||
: props.user.avatarUrl);
|
||||
|
||||
// colors from Nature Methods column by Bang Wong
|
||||
// https://www.nature.com/articles/nmeth.1618
|
||||
const hashlette = ['rgb(0, 0, 0)', 'rgb(230, 159, 0)', 'rgb(86, 180, 233)', 'rgb(0, 158, 115)', 'rgb(240, 228, 66)', 'rgb(0, 114, 178)', 'rgb(213, 94, 0)', 'rgb(204, 121, 167)'] as const;
|
||||
|
||||
/**
|
||||
* FNV-1a Hash implementation (32, 64, 128, 256, 512, and 1024 bit)
|
||||
*
|
||||
* stolen under the terms of MIT license which i actually had to make up myself cause all i had was the readme
|
||||
*
|
||||
* Copyright © 2017 Travis Webb and desudesutalk
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @author Travis Webb <me@traviswebb.com>
|
||||
* @see http://tools.ietf.org/html/draft-eastlake-fnv-06
|
||||
*/
|
||||
function fnv1a32(str: string): number {
|
||||
let i: number, l = str.length - 3, t0 = 0, v0 = 0x9dc5, t1 = 0, v1 = 0x811c;
|
||||
|
||||
for (i = 0; i < l;) {
|
||||
v0 ^= str.charCodeAt(i++);
|
||||
t0 = v0 * 403; t1 = v1 * 403;
|
||||
t1 += v0 << 8;
|
||||
v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535;
|
||||
v0 ^= str.charCodeAt(i++);
|
||||
t0 = v0 * 403; t1 = v1 * 403;
|
||||
t1 += v0 << 8;
|
||||
v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535;
|
||||
v0 ^= str.charCodeAt(i++);
|
||||
t0 = v0 * 403; t1 = v1 * 403;
|
||||
t1 += v0 << 8;
|
||||
v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535;
|
||||
v0 ^= str.charCodeAt(i++);
|
||||
t0 = v0 * 403; t1 = v1 * 403;
|
||||
t1 += v0 << 8;
|
||||
v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535;
|
||||
}
|
||||
|
||||
while (i < l + 3) {
|
||||
v0 ^= str.charCodeAt(i++);
|
||||
t0 = v0 * 403; t1 = v1 * 403;
|
||||
t1 += v0 << 8;
|
||||
v1 = (t1 + (t0 >>> 16)) & 65535; v0 = t0 & 65535;
|
||||
}
|
||||
|
||||
return ((v1 << 16) >>> 0) + v0;
|
||||
}
|
||||
|
||||
const handleHash = $computed(() => {
|
||||
const stripColours = fnv1a32(acct(props.user)).toString(8).split('').map(digitStr => hashlette[parseInt(digitStr)]);
|
||||
const fraction = 1 / stripColours.length;
|
||||
const positions = stripColours.flatMap((colour, idx) =>
|
||||
[`${colour} ${fraction * idx}turn`, `${colour} ${fraction * (idx + 1)}turn`]
|
||||
);
|
||||
return `conic-gradient(${positions})`;
|
||||
});
|
||||
|
||||
function onClick(ev: MouseEvent) {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
@ -91,6 +152,14 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
&.handleHash {
|
||||
> .inner {
|
||||
padding: 10%;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> .indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }" :compact-heads="compactHeads"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -12,6 +12,7 @@ withDefaults(defineProps<{
|
|||
author?: any;
|
||||
customEmojis?: any;
|
||||
isNote?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>(), {
|
||||
plain: false,
|
||||
nowrap: false,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.slice(local.length) : url" :rel="rel" :target="target"
|
||||
@contextmenu.stop="() => {}"
|
||||
>
|
||||
<template v-if="!self">
|
||||
<span class="schema">{{ schema }}//</span>
|
||||
|
|
|
@ -4,14 +4,12 @@
|
|||
<div class="main">
|
||||
<template v-for="item in items">
|
||||
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<i class="icon" :class="[ item.icon, { indicated: item.indicated } ]"></i>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" @click.passive="close()">
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<i class="icon" :class="[ item.icon, { indicated: item.indicated } ]"></i>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -66,7 +64,7 @@ const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuD
|
|||
icon: def.icon,
|
||||
to: def.to,
|
||||
action: def.action,
|
||||
indicate: def.indicated,
|
||||
indicated: def.indicated,
|
||||
}));
|
||||
|
||||
function close() {
|
||||
|
@ -146,20 +144,6 @@ function help(ev: MouseEvent) {
|
|||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
> .indicator {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 32px;
|
||||
color: var(--indicator);
|
||||
font-size: 8px;
|
||||
animation: blink 1s infinite;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
:alt="video.comment"
|
||||
preload="none"
|
||||
controls
|
||||
@contextmenu.stop
|
||||
>
|
||||
<source
|
||||
:src="video.url"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span class="main">
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe, [$style.compact]: compact }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="[$style.icon, { [$style.compact]: compact }]" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span v-if="!compact" class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
|
@ -25,6 +25,7 @@ import { defaultStore } from '@/store';
|
|||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
|
||||
|
@ -49,6 +50,10 @@ useCssModule();
|
|||
border-radius: 999px;
|
||||
color: var(--mention);
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.isMe {
|
||||
color: var(--mentionMe);
|
||||
}
|
||||
|
@ -61,6 +66,14 @@ useCssModule();
|
|||
margin: 0 0.2em 0 0;
|
||||
vertical-align: bottom;
|
||||
border-radius: 100%;
|
||||
|
||||
.compact > & {
|
||||
margin: 0;
|
||||
border: 0.1em solid var(--mention);
|
||||
}
|
||||
.compact.isMe > & {
|
||||
border: 0.1em solid var(--mentionMe);
|
||||
}
|
||||
}
|
||||
|
||||
.mainHost {
|
||||
|
|
|
@ -42,6 +42,11 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
compactHeads: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -232,6 +237,7 @@ export default defineComponent({
|
|||
key: Math.random(),
|
||||
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
|
||||
username: token.props.username,
|
||||
compact: this.compactHeads,
|
||||
})];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<div class="header">
|
||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageMetadata?.value" class="title">
|
||||
|
@ -59,33 +59,6 @@ provide('shouldOmitHeaderTitle', true);
|
|||
provide('shouldHeaderThin', true);
|
||||
|
||||
const pageUrl = $computed(() => url + path);
|
||||
const contextmenu = $computed(() => {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(pageUrl, '_blank');
|
||||
modal.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(pageUrl);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
function navigate(path, record = true) {
|
||||
if (record) history.push(router.getCurrentPath());
|
||||
|
@ -105,10 +78,6 @@ function popout() {
|
|||
_popout(path, rootEl);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(contextmenu, ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<article class="article">
|
||||
<header class="header">
|
||||
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
|
@ -230,24 +230,6 @@ function undoReact(note): void {
|
|||
});
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
|
||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
||||
viaKeyboard,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
<MkUserName v-if="!supercompact" :user="note.user"/>
|
||||
<div v-if="supercompact" class="username"><MkAcct :user="note.user"/></div>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
||||
<div class="username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="!supercompact" class="username"><MkAcct :user="note.user"/></div>
|
||||
<div class="info">
|
||||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
|
@ -23,6 +24,7 @@ import { userPage } from '@/filters/user';
|
|||
defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
pinned?: boolean;
|
||||
supercompact?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :supercompact="compactParent" :compact-heads="compactHeads" class="reply-to"/>
|
||||
<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
|
||||
<div v-if="isRenote" class="renote">
|
||||
|
@ -30,7 +30,7 @@
|
|||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<article class="article">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
|
@ -42,8 +42,8 @@
|
|||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
||||
<div class="text">
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" :compact-heads="compactHeads"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
<footer class="footer">
|
||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="button _button" @click="reply()">
|
||||
<button ref="replyButton" class="button _button" @click="reply()">
|
||||
<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
|
||||
<template v-else><i class="fas fa-reply"></i></template>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
|
@ -107,6 +107,7 @@ import * as foundkey from 'foundkey-js';
|
|||
import MkNoteSub from './MkNoteSub.vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XNoteSimple from './note-simple.vue';
|
||||
import NoteTooltip from './NoteTooltip.vue';
|
||||
import XReactionsViewer from './reactions-viewer.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
|
@ -127,10 +128,13 @@ import { $i } from '@/account';
|
|||
import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
pinned?: boolean;
|
||||
compactParent?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
@ -157,9 +161,11 @@ const isRenote = (
|
|||
|
||||
const el = ref<HTMLElement>();
|
||||
const menuButton = ref<HTMLElement>();
|
||||
const replyButton = ref<HTMLElement>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const renoteTime = ref<HTMLElement>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
const parentReply = ref<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as foundkey.entities.Note : note);
|
||||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
|
@ -192,6 +198,30 @@ useNoteCapture({
|
|||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(parentReply, async (showing) => {
|
||||
const parentNote = await os.api('notes/show', {
|
||||
noteId: appearNote.replyId,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: [parentNote],
|
||||
targetElement: parentReply.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
useTooltip(replyButton, async (showing) => {
|
||||
const replies = await os.api('notes/replies', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: replies,
|
||||
targetElement: replyButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
@ -224,24 +254,6 @@ function undoReact(): void {
|
|||
|
||||
const currentClipPage = inject<Ref<foundkey.entities.Clip> | null>('currentClipPage', null);
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
|
||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
||||
viaKeyboard,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<template #default="{ items: notes }">
|
||||
<div class="giivymft" :class="{ noGap }">
|
||||
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" class="notes">
|
||||
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note"/>
|
||||
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note" :compact-parent="true"/>
|
||||
</XList>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<template #default="{ items: notifications }">
|
||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :compact-parent="true" :compact-heads="true" />
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||
</XList>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
:close-button="true"
|
||||
:buttons-left="buttonsLeft"
|
||||
:buttons-right="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
|
@ -84,28 +83,6 @@ provideMetadataReceiver((info) => {
|
|||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
|
||||
const contextmenu = $computed(() => ([{
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url + router.getCurrentPath(), '_blank');
|
||||
windowEl.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
},
|
||||
}]));
|
||||
|
||||
function back() {
|
||||
history.pop();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div v-show="files.length != 0" class="skeikyzd">
|
||||
<XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
|
||||
<template #item="{element}">
|
||||
<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<div @click="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" class="sensitive">
|
||||
<i class="fas fa-exclamation-triangle icon"></i>
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
</button>
|
||||
<div>
|
||||
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
|
||||
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
|
||||
<span v-if="localOnly" class="local-only"><i class="fas fa-tree-city"></i></span>
|
||||
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
|
||||
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
|
||||
<span v-else-if="visibility === 'home'"><i class="fas fa-home"></i></span>
|
||||
<span v-else-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
|
||||
<span v-if="visibility === 'public'"><i class="fas fa-bullhorn"></i></span>
|
||||
<span v-else-if="visibility === 'home'"><i class="fas fa-lock-open"></i></span>
|
||||
<span v-else-if="visibility === 'followers'"><i class="fas fa-lock"></i></span>
|
||||
<span v-else-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
|
||||
</button>
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
@click="toggleReaction()"
|
||||
>
|
||||
<MkEmoji class="icon" :emoji="reaction" :custom-emojis="note.emojis" :is-reaction="true" :normal="true"/>
|
||||
<span class="count">{{ count }}</span>
|
||||
<span v-if="users === undefined" class="count">{{ count }}</span>
|
||||
<span v-if="users !== undefined">
|
||||
<MkAvatar v-for="u in users" class="user" :style="{ height: '2em', width: '2em' }" :key="u.id" :user="u" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
@ -25,13 +28,14 @@ const props = defineProps<{
|
|||
count: number;
|
||||
isInitial: boolean;
|
||||
note: foundkey.entities.Note;
|
||||
users?: foundkey.entities.UserLite[];
|
||||
}>();
|
||||
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
const toggleReaction = () => {
|
||||
const toggleReaction = (): void => {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<template>
|
||||
<div class="tdflqwzn" :class="{ isMe }">
|
||||
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
|
||||
<div ref="targetEl" class="tdflqwzn" :class="{ isMe }">
|
||||
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :users="usersMap[reaction]" :note="note"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useIntersectionObserver } from '@vueuse/core';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import XReaction from './reactions-viewer.reaction.vue';
|
||||
import { $i } from '@/account';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
|
@ -17,6 +19,39 @@ const props = defineProps<{
|
|||
const initialReactions = new Set(Object.keys(props.note.reactions));
|
||||
|
||||
const isMe = computed(() => $i && $i.id === props.note.userId);
|
||||
|
||||
const usersMap = ref({});
|
||||
const superloaded = ref(false);
|
||||
const targetEl = ref();
|
||||
|
||||
async function updateReactions(currentValue): Promise<void> {
|
||||
const reactions = await os.api('notes/reactions', {
|
||||
noteId: currentValue.id,
|
||||
limit: 100,
|
||||
});
|
||||
const users = {};
|
||||
for (const reaction of reactions) {
|
||||
if (users[reaction.type] === undefined) {
|
||||
users[reaction.type] = [];
|
||||
}
|
||||
users[reaction.type].push(reaction.user);
|
||||
}
|
||||
usersMap.value = users;
|
||||
superloaded.value = true;
|
||||
}
|
||||
|
||||
async function prepareFetch(note: foundkey.entities.Note): Promise<void> {
|
||||
superloaded.value = false;
|
||||
const { stop } = useIntersectionObserver(targetEl, ([{ isIntersecting }]) => {
|
||||
if (isIntersecting) {
|
||||
superloaded.value = true;
|
||||
stop();
|
||||
updateReactions(note);
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => prepareFetch(props.note));
|
||||
watch(props.note, prepareFetch, { deep: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -40,11 +40,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social _section">
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="wrmlmaau" :class="{ collapsed, isLong }">
|
||||
<div class="wrmlmaau" :class="{ collapsed, isLong, supercompact }">
|
||||
<div class="body">
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis" :compact-heads="compactHeads"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@ -15,7 +15,7 @@
|
|||
<XPoll :note="note"/>
|
||||
</details>
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
|
||||
<span>{{ i18n.ts.showMore }}</span>
|
||||
<span v-if="!supercompact">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
<button v-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true">
|
||||
<span>{{ i18n.ts.showLess }}</span>
|
||||
|
@ -24,13 +24,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import XPoll from './poll.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import NoteTooltip from './NoteTooltip.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
supercompact?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>();
|
||||
|
||||
const isLong = (
|
||||
|
@ -39,7 +45,20 @@ const isLong = (
|
|||
(props.note.text.length > 500)
|
||||
)
|
||||
);
|
||||
const collapsed = $ref(props.note.cw == null && isLong);
|
||||
const collapsed = $ref(props.supercompact || (props.note.cw == null && isLong));
|
||||
|
||||
const parentReply = ref<HTMLElement>();
|
||||
useTooltip(parentReply, async (showing): Promise<void> => {
|
||||
const parentNote = await os.api('notes/show', {
|
||||
noteId: props.note.replyId,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: [parentNote],
|
||||
targetElement: parentReply.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -64,6 +83,10 @@ const collapsed = $ref(props.note.cw == null && isLong);
|
|||
max-height: 9em;
|
||||
overflow: hidden;
|
||||
|
||||
&.supercompact {
|
||||
max-height: 4.5em;
|
||||
}
|
||||
|
||||
> .fade {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" appear>
|
||||
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
import MkMenu from './menu.vue';
|
||||
import { MenuItem } from './types/menu.vue';
|
||||
import contains from '@/scripts/contains';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
ev: MouseEvent;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $ref<HTMLDivElement>();
|
||||
|
||||
let zIndex = $ref<number>(os.claimZIndex('high'));
|
||||
|
||||
onMounted(() => {
|
||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
|
||||
const width = rootEl.offsetWidth;
|
||||
const height = rootEl.offsetHeight;
|
||||
|
||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - width + window.pageXOffset;
|
||||
}
|
||||
|
||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
}
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
rootEl.style.top = `${top}px`;
|
||||
rootEl.style.left = `${left}px`;
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
function onMousedown(evt: Event) {
|
||||
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nvlagfpb {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,6 @@
|
|||
class="rrevdjwt"
|
||||
:class="{ center: align === 'center', asDrawer }"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
>
|
||||
<template v-for="(item, i) in items2">
|
||||
<div v-if="item === null" class="divider"></div>
|
||||
|
@ -15,28 +14,24 @@
|
|||
<span><MkEllipsis/></span>
|
||||
</span>
|
||||
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close()">
|
||||
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
|
||||
<i v-if="item.icon" class="fa-fw" :class="[ item.icon, { indicated: item.indicate }]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
<span>{{ item.text }}</span>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</MkA>
|
||||
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close()">
|
||||
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
|
||||
<i v-if="item.icon" class="fa-fw" :class="[ item.icon, { indicated: item.indicate }]"></i>
|
||||
<span>{{ item.text }}</span>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
|
||||
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active, indicated: item.indicate }" :disabled="item.active" @click="clicked(item.action, $event)">
|
||||
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
|
||||
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
|
||||
</span>
|
||||
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
|
||||
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
|
||||
<i v-if="item.icon" class="fa-fw" :class="[ item.icon, { indicated: item.indicate }]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
<span>{{ item.text }}</span>
|
||||
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
|
||||
</button>
|
||||
</template>
|
||||
<span v-if="items2.length === 0" class="none item">
|
||||
|
@ -236,15 +231,6 @@ function focusDown() {
|
|||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
> .indicator {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 13px;
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick"></div>
|
||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||
<slot :max-height="maxHeight" :type="type"></slot>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="emit('closed')">
|
||||
<div v-if="showing" ref="main" class="ebkgocck">
|
||||
<div class="body _shadow _narrow_" @mousedown="moveToTop" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<div class="header" :class="{ mini }">
|
||||
<span class="left">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</span>
|
||||
|
@ -70,7 +70,6 @@ const props = withDefaults(defineProps<{
|
|||
closeButton?: boolean;
|
||||
mini?: boolean;
|
||||
front?: boolean;
|
||||
contextmenu?: MenuItem[];
|
||||
buttonsLeft?: any[];
|
||||
buttonsRight?: any[];
|
||||
}>(), {
|
||||
|
@ -79,7 +78,6 @@ const props = withDefaults(defineProps<{
|
|||
closeButton: true,
|
||||
mini: false,
|
||||
front: false,
|
||||
contextmenu: () => [] as MenuItem[],
|
||||
buttonsLeft: () => [],
|
||||
buttonsRight: () => [],
|
||||
});
|
||||
|
@ -121,12 +119,6 @@ function onKeydown(evt: KeyboardEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
if (props.contextmenu) {
|
||||
os.contextMenu(props.contextmenu, ev);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToTop(): void {
|
||||
main.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low').toString();
|
||||
}
|
||||
|
@ -139,9 +131,6 @@ function getClickPos(evt: MouseEvent | TouchEvent): [number, number] {
|
|||
}
|
||||
|
||||
function onHeaderMousedown(evt: MouseEvent | TouchEvent): void {
|
||||
// Right-click ignored as it is likely to have attempted to open a context menu
|
||||
if (evt instanceof MouseEvent && evt.button === 2) return;
|
||||
|
||||
if (!contains(main, document.activeElement)) main.focus();
|
||||
|
||||
const position = main.getBoundingClientRect();
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="gqyayizv _popup">
|
||||
<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
|
||||
<div><i class="fas fa-globe"></i></div>
|
||||
<div><i class="fas fa-bullhorn"></i></div>
|
||||
<div>
|
||||
<span>{{ i18n.ts._visibility.public }}</span>
|
||||
<span>{{ i18n.ts._visibility.publicDescription }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
|
||||
<div><i class="fas fa-home"></i></div>
|
||||
<div><i class="fas fa-lock-open"></i></div>
|
||||
<div>
|
||||
<span>{{ i18n.ts._visibility.home }}</span>
|
||||
<span>{{ i18n.ts._visibility.homeDescription }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
||||
<div><i class="fas fa-unlock"></i></div>
|
||||
<div><i class="fas fa-lock"></i></div>
|
||||
<div>
|
||||
<span>{{ i18n.ts._visibility.followers }}</span>
|
||||
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
||||
|
@ -31,7 +31,7 @@
|
|||
</button>
|
||||
<div class="divider"></div>
|
||||
<button key="localOnly" class="_button localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly">
|
||||
<div><i class="fas fa-biohazard"></i></div>
|
||||
<div><i class="fas fa-tree-city"></i></div>
|
||||
<div>
|
||||
<span>{{ i18n.ts._visibility.localOnly }}</span>
|
||||
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<span v-if="note.visibility !== 'public'" :class="$style.visibility">
|
||||
<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
|
||||
<span :class="$style.visibility">
|
||||
<i v-if="note.visibility === 'public'" class="fas fa-bullhorn"></i>
|
||||
<i v-else-if="note.visibility === 'home'" class="fas fa-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="fas fa-lock"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly"><i class="fas fa-biohazard"></i></span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly"><i class="fas fa-tree-city"></i></span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -171,7 +171,6 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
const app = createApp(
|
||||
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||
);
|
||||
|
|
|
@ -169,13 +169,6 @@ export const menuDef = reactive({
|
|||
localStorage.setItem('ui', 'default');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.deck,
|
||||
active: ui === 'deck',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'deck');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.classic,
|
||||
active: ui === 'classic',
|
||||
|
|
|
@ -527,24 +527,6 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
|||
});
|
||||
}
|
||||
|
||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
return new Promise((resolve) => {
|
||||
let dispose;
|
||||
popup(defineAsyncComponent(() => import('@/components/ui/context-menu.vue')), {
|
||||
items,
|
||||
ev,
|
||||
}, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}) {
|
||||
return new Promise((resolve) => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
|
@ -564,8 +546,6 @@ export function post(props: Record<string, any> = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export const deckGlobalEvents = new EventEmitter();
|
||||
|
||||
/*
|
||||
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
|
|
|
@ -157,11 +157,6 @@ const menuDef = $computed(() => [{
|
|||
text: i18n.ts.relays,
|
||||
to: '/admin/relays',
|
||||
active: props.initialPage === 'relays',
|
||||
}, {
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.integration,
|
||||
to: '/admin/integrations',
|
||||
active: props.initialPage === 'integrations',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.ts.instanceBlocking,
|
||||
|
@ -199,7 +194,6 @@ const component = $computed(() => {
|
|||
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
|
||||
default: return null;
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="discordClientId" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Client ID</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="discordClientSecret" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Client Secret</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let uri: string = $ref('');
|
||||
let enableDiscordIntegration: boolean = $ref(false);
|
||||
let discordClientId: string | null = $ref(null);
|
||||
let discordClientSecret: string | null = $ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
uri = meta.uri;
|
||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
discordClientId = meta.discordClientId;
|
||||
discordClientSecret = meta.discordClientSecret;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableDiscordIntegration,
|
||||
discordClientId,
|
||||
discordClientSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue