Compare commits
42 Commits
13.0.0-bet
...
13.0.0-rc.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
34a7b52105 | ||
![]() |
30fc166c08 | ||
![]() |
c84d86b368 | ||
![]() |
1e5d4db0a1 | ||
![]() |
5e02f0d325 | ||
![]() |
ce5506f331 | ||
![]() |
91105845d8 | ||
![]() |
2bedc084a3 | ||
![]() |
027ef1ea4a | ||
![]() |
668aa17eef | ||
![]() |
ebf8ef22e4 | ||
![]() |
bcb5182e86 | ||
![]() |
f45059b7b1 | ||
![]() |
d0aee58599 | ||
![]() |
68e65ed5df | ||
![]() |
367ccb9971 | ||
![]() |
4151087d3c | ||
![]() |
39c058a4bb | ||
![]() |
d1807ee5dc | ||
![]() |
e6a76b31be | ||
![]() |
98469117bf | ||
![]() |
a5becfc042 | ||
![]() |
d2204fd5c8 | ||
![]() |
519a08f8b5 | ||
![]() |
303519a1bd | ||
![]() |
161da24841 | ||
![]() |
6e40024660 | ||
![]() |
73c78d4c38 | ||
![]() |
2654936c17 | ||
![]() |
23810e3e1e | ||
![]() |
d6c89bf003 | ||
![]() |
49ab2a5f93 | ||
![]() |
bc0b8afb1f | ||
![]() |
b250456814 | ||
![]() |
0a6e237d09 | ||
![]() |
54ff4e53cb | ||
![]() |
002ccbb5f0 | ||
![]() |
7b7faf1e84 | ||
![]() |
9936088200 | ||
![]() |
990f4b52bd | ||
![]() |
4c21d83639 | ||
![]() |
d43a4a2d46 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -18,6 +18,12 @@ You should also include the user name that made the change.
|
||||
- Various usability improvements
|
||||
- Various UI tweaks
|
||||
|
||||
### Notable features
|
||||
- ロール機能
|
||||
- 従来より柔軟にユーザーの権限を管理できます。例えば、「インスタンスのパトロンはアンテナを30個まで作れる」「基本的にLTLは見れないが、許可した人だけ見れる」「招待制インスタンスだけどユーザーなら誰でも他者を招待できる」のような運用はもちろん、「ローカルユーザーかつアカウント作成から1日未満のユーザーはパブリックな投稿を行えない」のように複数条件を組み合わせて、自動でロールを付与する設定も可能です。
|
||||
- Misskey Play
|
||||
- 従来の動的なPagesに代わる、新しいプラットフォームです。動的なコンテンツ(アプリケーション)に特化していて、Pagesに比べてはるかに柔軟なアプリケーションを作成可能です。
|
||||
|
||||
### Changes
|
||||
#### For server admins
|
||||
- Node.js 18.x or later is required
|
||||
@@ -27,12 +33,13 @@ You should also include the user name that made the change.
|
||||
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
|
||||
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
|
||||
- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
||||
- 従来のモデレーターフラグは廃止され、より高度なロール機能が導入されました
|
||||
- これに伴い、アップデートを行うと全てのモデレーターフラグは失われます。そのため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
|
||||
- インスタンスブロックはサブドメインにも適用されるようになります
|
||||
- ロールの導入に伴い、いくつかの機能がロールと統合されました
|
||||
- モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
|
||||
- サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。
|
||||
- ユーザーごとのドライブ容量設定はロールに統合されました
|
||||
- ユーザーごとのドライブ容量設定はロールに統合されました。
|
||||
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールのドライブ容量を編集してください。
|
||||
- LTL/GTLの解放状態はロールに統合されました
|
||||
- LTL/GTLの解放状態はロールに統合されました。
|
||||
|
||||
#### For users
|
||||
- ノートのウォッチ機能が削除されました
|
||||
@@ -44,7 +51,7 @@ You should also include the user name that made the change.
|
||||
- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
||||
- 0.12.x未満のプラグインは読み込むことはできません
|
||||
- iOS15以下のデバイスはサポートされなくなりました
|
||||
- Firefox109以下はサポートされなくなりました
|
||||
- Firefox110以下はサポートされなくなりました
|
||||
|
||||
#### For app developers
|
||||
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
|
||||
@@ -66,9 +73,14 @@ You should also include the user name that made the change.
|
||||
- Push notification of Antenna note @tamaina
|
||||
- AVIF support @tamaina
|
||||
- Add Cloudflare Turnstile CAPTCHA support @CyberRex0
|
||||
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
|
||||
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
|
||||
- ハードワードミュートの最大文字数を設定可能に @syuilo
|
||||
- Webhookの作成可能数を設定可能に @syuilo
|
||||
- Server: signToActivityPubGet is set to true by default @syuilo
|
||||
- Server: improve syslog performance @syuilo
|
||||
- Server: Use undici instead of node-fetch and got @tamaina
|
||||
- Server: Judge instance block by endsWith @tamaina
|
||||
- Server: improve note scoring for featured notes @CyberRex0
|
||||
- Server: アンケート選択肢の文字数制限を緩和 @syuilo
|
||||
- Server: improve stats api performance @syuilo
|
||||
@@ -100,6 +112,7 @@ You should also include the user name that made the change.
|
||||
- Client: add heatmap of daily active users to about page @syuilo
|
||||
- Client: introduce fluent emoji @syuilo
|
||||
- Client: add new theme @syuilo
|
||||
- Client: add new mfm function (position, fg, bg) @syuilo
|
||||
- Client: show fireworks when visit user who today is birthday @syuilo
|
||||
- Client: show bot warning on screen when logged in as bot account @syuilo
|
||||
- Client: improve overall performance of client @syuilo
|
||||
@@ -119,6 +132,7 @@ You should also include the user name that made the change.
|
||||
- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu
|
||||
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
|
||||
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
|
||||
- Server: follow request list api pagination @sim1222
|
||||
- Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit
|
||||
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
|
||||
- Client: case insensitive emoji search @saschanaz
|
||||
|
@@ -817,6 +817,7 @@ account: "الحسابات"
|
||||
cannotLoad: "تعذر التحميل"
|
||||
like: "أعجبني"
|
||||
show: "المظهر"
|
||||
color: "اللون"
|
||||
_emailUnavailable:
|
||||
used: "هذا البريد الإلكتروني مستخدم"
|
||||
format: "صيغة البريد الإلكتروني غير صالحة"
|
||||
|
@@ -853,6 +853,7 @@ localOnly: "শুধুমাত্র লোকাল"
|
||||
account: "অ্যাকাউন্টগুলি"
|
||||
like: "পছন্দ করা"
|
||||
show: "প্রদর্শন"
|
||||
color: "রং"
|
||||
_emailUnavailable:
|
||||
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
|
||||
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"
|
||||
|
@@ -611,6 +611,7 @@ slow: "Pomalá"
|
||||
fast: "Rychlá"
|
||||
account: "Účty"
|
||||
show: "Zobrazit"
|
||||
color: "Barva"
|
||||
_ad:
|
||||
back: "Zpět"
|
||||
_gallery:
|
||||
|
@@ -924,6 +924,33 @@ neverShow: "Nicht wieder anzeigen"
|
||||
remindMeLater: "Vielleicht später"
|
||||
didYouLikeMisskey: "Gefällt dir Misskey?"
|
||||
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
|
||||
roles: "Rollen"
|
||||
role: "Rolle"
|
||||
normalUser: "Standardbenutzer"
|
||||
undefined: "Undefiniert"
|
||||
assign: "Zuweisen"
|
||||
unassign: "Entfernen"
|
||||
color: "Farbe"
|
||||
_role:
|
||||
new: "Rolle erstellen"
|
||||
edit: "Rolle bearbeiten"
|
||||
name: "Rollenname"
|
||||
description: "Rollenbeschreibung"
|
||||
permission: "Rollenberechtigungen"
|
||||
isPublic: "Öffentliche Rolle"
|
||||
descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt."
|
||||
options: "Optionen"
|
||||
baseRole: "Rollenvorlage"
|
||||
useBaseValue: "Wert der Rollenvorlage verwenden"
|
||||
chooseRoleToAssign: "Zuzuweisende Rolle auswählen"
|
||||
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
|
||||
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
|
||||
_options:
|
||||
gtlAvailable: "Kann auf die globale Chronik zugreifen"
|
||||
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
|
||||
canPublicNote: "Kann öffentliche Notizen erstellen"
|
||||
driveCapacity: "Drive-Kapazität"
|
||||
antennaMax: "Maximale Anzahl an Antennen"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||
sensitivity: "Erkennungssensitivität"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
_lang_: "Ελληνικά"
|
||||
monthAndDay: "{μήνας}/{ημέρα}"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Αναζήτηση"
|
||||
notifications: "Ειδοποιήσεις"
|
||||
username: "Όνομα μέλους"
|
||||
|
@@ -924,6 +924,33 @@ neverShow: "Don't show again"
|
||||
remindMeLater: "Maybe later"
|
||||
didYouLikeMisskey: "Have you taken a liking to Misskey?"
|
||||
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
|
||||
roles: "Roles"
|
||||
role: "Role"
|
||||
normalUser: "Normal user"
|
||||
undefined: "Undefined"
|
||||
assign: "Assign"
|
||||
unassign: "Unassign"
|
||||
color: "Color"
|
||||
_role:
|
||||
new: "New role"
|
||||
edit: "Edit role"
|
||||
name: "Role name"
|
||||
description: "Role description"
|
||||
permission: "Role permissions"
|
||||
isPublic: "Public role"
|
||||
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
|
||||
options: "Role options"
|
||||
baseRole: "Base role"
|
||||
useBaseValue: "Use base role value"
|
||||
chooseRoleToAssign: "Select the role to assign"
|
||||
canEditMembersByModerator: "Allow moderators to edit the list members of this role"
|
||||
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
|
||||
_options:
|
||||
gtlAvailable: "Viewing the global timeline"
|
||||
ltlAvailable: "Viewing the local timeline"
|
||||
canPublicNote: "Can send public notes"
|
||||
driveCapacity: "Drive capacity"
|
||||
antennaMax: "Maximum number of antennas"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
sensitivity: "Detection sensitivity"
|
||||
|
@@ -918,6 +918,7 @@ cannotLoad: "No se puede cargar."
|
||||
numberOfProfileView: "Número de vistas de perfil"
|
||||
like: "¡Muy bien!"
|
||||
show: "Apariencia"
|
||||
color: "Color"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
|
||||
sensitivity: "Sensibilidad de detección"
|
||||
|
@@ -911,7 +911,11 @@ loggedInAsBot: "Connecté actuellement en tant que bot"
|
||||
tools: "Outils"
|
||||
cannotLoad: "Chargement impossible"
|
||||
like: "J'aime"
|
||||
numberOfLikes: "Favoris"
|
||||
show: "Affichage"
|
||||
neverShow: "Ne plus afficher"
|
||||
remindMeLater: "Peut-être plus tard"
|
||||
color: "Couleur"
|
||||
_sensitiveMediaDetection:
|
||||
description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
|
||||
sensitivity: "Sensibilité de la détection"
|
||||
|
@@ -859,6 +859,7 @@ like: "Suka"
|
||||
unlike: "Tidak Suka"
|
||||
numberOfLikes: "Jumlah yang disukai"
|
||||
show: "Tampilkan"
|
||||
color: "Warna"
|
||||
_emailUnavailable:
|
||||
used: "Alamat surel ini telah digunakan"
|
||||
format: "Format tidak valid."
|
||||
|
@@ -109,7 +109,7 @@ you: "Tu"
|
||||
clickToShow: "Clicca per visualizzare"
|
||||
sensitive: "Contenuto sensibile"
|
||||
add: "Aggiungi"
|
||||
reaction: "Reazione"
|
||||
reaction: "Reazioni"
|
||||
reactionSetting: "Reazioni visualizzate sul pannello"
|
||||
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
|
||||
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
|
||||
@@ -226,7 +226,7 @@ currentPassword: "Password attuale"
|
||||
newPassword: "Nuova Password"
|
||||
newPasswordRetype: "Conferma password"
|
||||
attachFile: "Allega file"
|
||||
more: "Altri!"
|
||||
more: "Di più!"
|
||||
featured: "Tendenze"
|
||||
usernameOrUserId: "Nome utente o ID utente"
|
||||
noSuchUser: "Nessun utente trovato"
|
||||
@@ -512,7 +512,7 @@ newNoteRecived: "Vedi le nuove note"
|
||||
sounds: "Impostazioni suoni"
|
||||
sound: "Impostazioni suoni"
|
||||
listen: "Ascolta"
|
||||
none: "Niente"
|
||||
none: "Nessuno"
|
||||
showInPage: "Visualizza in pagina"
|
||||
popout: "Finestra pop-out"
|
||||
volume: "Volume"
|
||||
@@ -578,7 +578,7 @@ useFullReactionPicker: "Usa la totalità del pannello di reazioni"
|
||||
width: "Larghezza"
|
||||
height: "Altezza"
|
||||
large: "Grande"
|
||||
medium: "Predefinito"
|
||||
medium: "Medio"
|
||||
small: "Piccolo"
|
||||
generateAccessToken: "Genera token di accesso"
|
||||
permission: "Autorizzazioni "
|
||||
@@ -649,7 +649,7 @@ instanceTicker: "Informazioni sull'istanza da cui vengono le note"
|
||||
waitingFor: "Aspettando {x}"
|
||||
random: "Casuale"
|
||||
system: "Sistema"
|
||||
switchUi: "Cambiare interfaccia utente"
|
||||
switchUi: "Cambiare interfaccia"
|
||||
desktop: "Desktop"
|
||||
clip: "Nota"
|
||||
createNew: "Crea"
|
||||
@@ -799,7 +799,7 @@ received: "Ricevuto"
|
||||
searchResult: "Risultati della Ricerca"
|
||||
hashtags: "Hashtag"
|
||||
troubleshooting: "Risoluzione problemi"
|
||||
useBlurEffect: "Utilizza effetto sfocatura per l'interfaccia utente"
|
||||
useBlurEffect: "Utilizza effetto sfocatura nell'interfaccia"
|
||||
learnMore: "Più dettagli"
|
||||
misskeyUpdated: "Misskey è stato aggiornato!"
|
||||
whatIsNew: "Visualizza le informazioni sull'aggiornamento"
|
||||
@@ -917,7 +917,14 @@ tools: "Strumenti"
|
||||
cannotLoad: "Caricamento impossibile"
|
||||
numberOfProfileView: "Visualizzazioni profilo"
|
||||
like: "Mi piace!"
|
||||
unlike: "Non mi piace"
|
||||
numberOfLikes: "Numero di Like"
|
||||
show: "Visualizza"
|
||||
neverShow: "Non mostrare più"
|
||||
remindMeLater: "Rimanda"
|
||||
didYouLikeMisskey: "Ti piace Misskey?"
|
||||
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
|
||||
color: "Colore"
|
||||
_sensitiveMediaDetection:
|
||||
description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente."
|
||||
sensitivity: "Sensibilità di rilevamento"
|
||||
@@ -1090,9 +1097,9 @@ _channel:
|
||||
usersCount: "{n} partecipanti"
|
||||
notesCount: "{n} note"
|
||||
_menuDisplay:
|
||||
sideFull: "laro"
|
||||
sideIcon: "Orizzontale (icona)"
|
||||
top: "superficie"
|
||||
sideFull: "Laterale"
|
||||
sideIcon: "Laterale (solo icone)"
|
||||
top: "In alto"
|
||||
hide: "Nascondere"
|
||||
_wordMute:
|
||||
muteWords: "Parole da filtrare"
|
||||
@@ -1194,10 +1201,10 @@ _ago:
|
||||
future: "Futuro"
|
||||
justNow: "Ora"
|
||||
secondsAgo: "{n}s fa"
|
||||
minutesAgo: "{n}min fa"
|
||||
minutesAgo: "{n} min fa"
|
||||
hoursAgo: "{n} ore fa"
|
||||
daysAgo: "{n} giorni fa"
|
||||
weeksAgo: "{n} settimane fa"
|
||||
daysAgo: "{n} gg fa"
|
||||
weeksAgo: "{n} sett. fa"
|
||||
monthsAgo: "{n} mesi fa"
|
||||
yearsAgo: "{n} anni fa"
|
||||
_time:
|
||||
@@ -1319,10 +1326,12 @@ _widgets:
|
||||
jobQueue: "Coda di lavoro"
|
||||
serverMetric: "Statistiche server"
|
||||
aiscript: "Console AiScript"
|
||||
aiscriptApp: "App AiScript"
|
||||
aichan: "Mascotte Ai"
|
||||
userList: "Elenco utenti"
|
||||
_userList:
|
||||
chooseList: "Seleziona una lista"
|
||||
clicker: "Cliccaggio"
|
||||
_cw:
|
||||
hide: "Nascondere"
|
||||
show: "Mostra di più"
|
||||
@@ -1425,7 +1434,16 @@ _timelines:
|
||||
social: "Sociale"
|
||||
global: "Federata"
|
||||
_play:
|
||||
new: "Crea un Play"
|
||||
edit: "Modifica i Play"
|
||||
created: "Il Play è stato creato"
|
||||
updated: "Il Play è stato aggiornato"
|
||||
deleted: "Il Play è stato eliminato"
|
||||
pageSetting: "Impostazioni di Play"
|
||||
editThisPage: "Modifica il Play"
|
||||
viewSource: "Visualizza sorgente"
|
||||
my: "I miei Play"
|
||||
liked: "Play piaciuti"
|
||||
featured: "Popolari"
|
||||
title: "Titolo"
|
||||
script: "Script"
|
||||
|
@@ -193,7 +193,7 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。
|
||||
clearCachedFiles: "キャッシュをクリア"
|
||||
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
||||
blockedInstances: "ブロックしたインスタンス"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
@@ -931,6 +931,7 @@ undefined: "未定義"
|
||||
assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
manageCustomEmojis: "カスタム絵文字の管理"
|
||||
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
@@ -957,13 +958,21 @@ _role:
|
||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
canInvite: "インスタンス招待コードの発行"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
wordMuteMax: "ワードミュートの最大文字数"
|
||||
webhookMax: "Webhookの作成可能数"
|
||||
_condition:
|
||||
isLocal: "ローカルユーザー"
|
||||
isRemote: "リモートユーザー"
|
||||
createdLessThan: "アカウント作成から~以内"
|
||||
createdMoreThan: "アカウント作成から~経過"
|
||||
followersLessThanOrEq: "フォロワー数が~以下"
|
||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||
followingLessThanOrEq: "フォロー数が~以下"
|
||||
followingMoreThanOrEq: "フォロー数が~以上"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
|
@@ -915,8 +915,40 @@ caption: "キャプション"
|
||||
loggedInAsBot: "Botアカウントでログイン中やで"
|
||||
tools: "ツール"
|
||||
cannotLoad: "読み込めへんで"
|
||||
numberOfProfileView: "プロフィール表示回数"
|
||||
like: "ええやん!"
|
||||
unlike: "いいねを解除"
|
||||
numberOfLikes: "いいね数"
|
||||
show: "表示"
|
||||
neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
didYouLikeMisskey: "Misskeyを気に入っとっただけましたん?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
||||
roles: "ロール"
|
||||
role: "ロール"
|
||||
undefined: "未定義"
|
||||
assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
edit: "ロールの編集"
|
||||
name: "ロール名"
|
||||
description: "ロールの説明"
|
||||
isPublic: "ロールを公開"
|
||||
descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができるで。そんで、ユーザーのプロフィールでこのロールが表示されるで。"
|
||||
options: "オプション"
|
||||
baseRole: "ベースロール"
|
||||
useBaseValue: "ベースロールの値を使用"
|
||||
chooseRoleToAssign: "アサインするロールを選択"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。"
|
||||
_options:
|
||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
driveCapacity: "ドライブ容量"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出して、モデレーションに役立てることができるで。サーバーの負荷が少し増えてまうなあ。"
|
||||
sensitivity: "検出感度やで"
|
||||
@@ -1318,10 +1350,12 @@ _widgets:
|
||||
jobQueue: "ジョブキュー"
|
||||
serverMetric: "サーバーメトリクス"
|
||||
aiscript: "AiScriptコンソール"
|
||||
aiscriptApp: "AiScript App"
|
||||
aichan: "藍"
|
||||
userList: "ユーザーリスト"
|
||||
_userList:
|
||||
chooseList: "リストを選ぶ"
|
||||
clicker: "クリッカー"
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
show: "続き見して!"
|
||||
@@ -1385,6 +1419,7 @@ _profile:
|
||||
changeBanner: "バナー画像を変更するで"
|
||||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
favoritedNotes: "お気に入りにしたノート"
|
||||
followingList: "フォロー"
|
||||
muteList: "ミュート"
|
||||
blockingList: "ブロック"
|
||||
@@ -1423,7 +1458,16 @@ _timelines:
|
||||
social: "ソーシャル"
|
||||
global: "グローバル"
|
||||
_play:
|
||||
new: "Playの作成"
|
||||
edit: "Playの編集"
|
||||
created: "Playを作ったで"
|
||||
updated: "Playを更新したで"
|
||||
deleted: "Playを消したで"
|
||||
pageSetting: "Play設定"
|
||||
editThisPage: "このPlayを編集"
|
||||
viewSource: "ソースを表示"
|
||||
my: "自分のPlay"
|
||||
liked: "いいねしたPlay"
|
||||
featured: "人気"
|
||||
title: "タイトル"
|
||||
script: "スクリプト"
|
||||
|
@@ -15,7 +15,7 @@ gotIt: "알겠어요"
|
||||
cancel: "취소"
|
||||
noThankYou: "나중에"
|
||||
enterUsername: "유저명 입력"
|
||||
renotedBy: "{user}님의 리노트"
|
||||
renotedBy: "{user}님이 리노트"
|
||||
noNotes: "노트가 없습니다"
|
||||
noNotifications: "표시할 알림이 없습니다"
|
||||
instance: "인스턴스"
|
||||
@@ -924,6 +924,31 @@ neverShow: "다시 보지 않기"
|
||||
remindMeLater: "나중에 알림"
|
||||
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
|
||||
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
|
||||
roles: "역할"
|
||||
role: "역할"
|
||||
undefined: "정의되지 않음"
|
||||
assign: "할당"
|
||||
unassign: "할당 취소"
|
||||
color: "색"
|
||||
_role:
|
||||
new: "새 역할 생성"
|
||||
edit: "역할 수정"
|
||||
name: "역할 이름"
|
||||
description: "역할 설명"
|
||||
isPublic: "공개 역할"
|
||||
descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다."
|
||||
options: "옵션"
|
||||
baseRole: "기본 역할"
|
||||
useBaseValue: "기본값 사용"
|
||||
chooseRoleToAssign: "할당할 역할 선택"
|
||||
canEditMembersByModerator: "모더레이터의 역할 수정 허용"
|
||||
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 추가하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 가능합니다."
|
||||
_options:
|
||||
gtlAvailable: "글로벌 타임라인 보이기"
|
||||
ltlAvailable: "로컬 타임라인 보이기"
|
||||
canPublicNote: "공개 노트 허용"
|
||||
driveCapacity: "드라이브 용량"
|
||||
antennaMax: "최대 안테나 생성 허용 수"
|
||||
_sensitiveMediaDetection:
|
||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||
sensitivity: "탐지 민감도"
|
||||
|
@@ -868,6 +868,7 @@ sendPushNotificationReadMessageCaption: "Chwilowo pojawi się powiadomienie \"{e
|
||||
loggedInAsBot: "Jesteś obecnie zalogowany/a jako bot"
|
||||
like: "Polub"
|
||||
show: "Wyświetlanie"
|
||||
color: "Kolor"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Zmniejsza wysiłek związany z moderacją serwera dzięki automatycznemu rozpoznawaniu zawartości NSFW za pomocą uczenia maszynowego. To nieznacznie zwiększy obciążenie serwera."
|
||||
setSensitiveFlagAutomatically: "Oznacz jako NSFW"
|
||||
|
@@ -866,6 +866,7 @@ windowMaximize: "Развернуть"
|
||||
windowRestore: "Восстановить"
|
||||
like: "Нравится!"
|
||||
show: "Отображение"
|
||||
color: "Цвет"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
||||
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
||||
|
@@ -917,6 +917,7 @@ neverShow: "Nabudúce nezobrazovať"
|
||||
remindMeLater: "Pripomenúť neskôr"
|
||||
didYouLikeMisskey: "Páči sa vám Misskey?"
|
||||
pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!"
|
||||
color: "Farba"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
||||
sensitivity: "Citlivosť detekcie"
|
||||
|
@@ -924,6 +924,31 @@ neverShow: "ไม่ต้องแสดงข้อความนี้อ
|
||||
remindMeLater: "ไว้ครั้งหน้าแล้วกัน"
|
||||
didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?"
|
||||
pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!"
|
||||
roles: "บทบาท"
|
||||
role: "บทบาท"
|
||||
undefined: "ไม่ได้กำหนด"
|
||||
assign: "กำหนด"
|
||||
unassign: "ยังไม่มอบหมาย"
|
||||
color: "สี"
|
||||
_role:
|
||||
new: "บทบาทใหม่"
|
||||
edit: "แก้ไขบทบาท"
|
||||
name: "ชื่อบทบาท"
|
||||
description: "คำอธิบายบทบาท"
|
||||
isPublic: "บทบาทสาธารณะ"
|
||||
descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
baseRole: "บทบาทพื้นฐาน"
|
||||
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
|
||||
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
|
||||
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
||||
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
||||
_options:
|
||||
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
|
||||
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
|
||||
canPublicNote: "สามารถส่งโน้ตสาธารณะ"
|
||||
driveCapacity: "ความจุของไดรฟ์"
|
||||
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
|
||||
_sensitiveMediaDetection:
|
||||
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||
sensitivity: "การตรวจจับความไว"
|
||||
|
@@ -894,6 +894,7 @@ windowRestore: "Відновити"
|
||||
caption: "Підпис"
|
||||
like: "Вподобати"
|
||||
show: "Відображення"
|
||||
color: "Колір"
|
||||
_sensitiveMediaDetection:
|
||||
sensitivity: "Чутливість детектування"
|
||||
setSensitiveFlagAutomatically: "Позначити як NSFW"
|
||||
|
@@ -896,6 +896,7 @@ account: "Tài khoản của bạn"
|
||||
move: "Di chuyển"
|
||||
like: "Thích"
|
||||
show: "Hiển thị"
|
||||
color: "Màu sắc"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
|
||||
sensitivity: "Phát hiện nhạy cảm"
|
||||
|
@@ -13,7 +13,7 @@ fetchingAsApObject: "在联邦宇宙查询中..."
|
||||
ok: "OK"
|
||||
gotIt: "我明白了"
|
||||
cancel: "取消"
|
||||
noThankYou: "不用"
|
||||
noThankYou: "不用,谢谢"
|
||||
enterUsername: "输入用户名"
|
||||
renotedBy: "由 {user} 转贴"
|
||||
noNotes: "没有帖子"
|
||||
@@ -924,6 +924,48 @@ neverShow: "不再显示"
|
||||
remindMeLater: "稍后提醒我"
|
||||
didYouLikeMisskey: "您喜欢Misskey吗?"
|
||||
pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发,请在有余力的情况下进行捐助!"
|
||||
roles: "角色"
|
||||
role: "角色"
|
||||
normalUser: "普通用户"
|
||||
undefined: "未定义"
|
||||
assign: "分配"
|
||||
unassign: "取消分配"
|
||||
color: "颜色"
|
||||
_role:
|
||||
new: "创建角色"
|
||||
edit: "编辑角色"
|
||||
name: "用户组名称"
|
||||
description: "用户组的描述"
|
||||
permission: "用户组的权限"
|
||||
descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。"
|
||||
assignTarget: "授权对象"
|
||||
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个用户组中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
|
||||
manual: "手动"
|
||||
conditional: "符合条件"
|
||||
condition: "条件"
|
||||
isConditionalRole: "这是一个条件控制的用户组。"
|
||||
isPublic: "公开用户组"
|
||||
descriptionOfIsPublic: "任何人都可以看到分配该用户组的用户,用户的个人资料也将显示该用户组。"
|
||||
options: "选项"
|
||||
baseRole: "基本角色"
|
||||
useBaseValue: "使用基本角色的值"
|
||||
chooseRoleToAssign: "选择要分配的角色"
|
||||
canEditMembersByModerator: "允许版主编辑成员"
|
||||
descriptionOfCanEditMembersByModerator: "如果选中,版主和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||
_options:
|
||||
gtlAvailable: "查看全局时间线"
|
||||
ltlAvailable: "查看本地时间线"
|
||||
canPublicNote: "允许公开发帖"
|
||||
driveCapacity: "网盘容量"
|
||||
antennaMax: "可创建的最大天线数量"
|
||||
_condition:
|
||||
isLocal: "是本地用户"
|
||||
isRemote: "是远程用户"
|
||||
createdLessThan: "账户创建时间少于"
|
||||
createdMoreThan: "账户创建时间超过"
|
||||
and: "全部符合"
|
||||
or: "任一符合"
|
||||
not: "不符合"
|
||||
_sensitiveMediaDetection:
|
||||
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
||||
sensitivity: "检测敏感度"
|
||||
|
@@ -325,7 +325,7 @@ connectService: "己連結"
|
||||
disconnectService: "己斷開 "
|
||||
enableLocalTimeline: "開啟本地時間軸"
|
||||
enableGlobalTimeline: "啟用公開時間軸"
|
||||
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
|
||||
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。"
|
||||
registration: "註冊"
|
||||
enableRegistration: "開啟新使用者註冊"
|
||||
invite: "邀請"
|
||||
@@ -388,7 +388,7 @@ aboutMisskey: "關於 Misskey"
|
||||
administrator: "管理員"
|
||||
token: "權杖"
|
||||
twoStepAuthentication: "兩階段驗證"
|
||||
moderator: "板主"
|
||||
moderator: "審核員"
|
||||
moderation: "言論調節"
|
||||
nUsersMentioned: "提到了{n}"
|
||||
securityKey: "安全金鑰"
|
||||
@@ -924,6 +924,36 @@ neverShow: "不再顯示"
|
||||
remindMeLater: "以後再說"
|
||||
didYouLikeMisskey: "您是否喜愛Misskey呢?"
|
||||
pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!"
|
||||
roles: "角色"
|
||||
role: "角色"
|
||||
normalUser: "一般使用者"
|
||||
undefined: "未定義"
|
||||
assign: "指派"
|
||||
unassign: "取消指派"
|
||||
color: "顏色"
|
||||
_role:
|
||||
new: "建立角色"
|
||||
edit: "編輯角色"
|
||||
name: "角色名稱"
|
||||
description: "角色描述 "
|
||||
permission: "角色的權限"
|
||||
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理者</b>能變更實例的全部設定。"
|
||||
assignTarget: "指派目標"
|
||||
manual: "手動"
|
||||
condition: "條件"
|
||||
isConditionalRole: "這是條件角色。"
|
||||
isPublic: "角色為公開"
|
||||
options: "選項"
|
||||
baseRole: "基本角色"
|
||||
useBaseValue: "使用基本角色的值"
|
||||
chooseRoleToAssign: "選擇要指派的角色"
|
||||
_options:
|
||||
driveCapacity: "雲端硬碟容量"
|
||||
_condition:
|
||||
isLocal: "本地使用者"
|
||||
isRemote: "遠端使用者"
|
||||
createdLessThan: "自建立帳戶開始~以內"
|
||||
createdMoreThan: "自建立帳戶開始~經過"
|
||||
_sensitiveMediaDetection:
|
||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||
sensitivity: "檢測敏感度"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.0.0-beta.41",
|
||||
"version": "13.0.0-rc.1",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -72,7 +72,7 @@
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "8.1.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"mfm-js": "0.23.1",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.14",
|
||||
"ms": "3.0.0-canary.1",
|
||||
|
@@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@@ -73,7 +74,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'antennaCreated':
|
||||
this.antennas.push(body);
|
||||
|
@@ -4,8 +4,9 @@ import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Meta } from '@/models/entities/Meta.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class MetaService implements OnApplicationShutdown {
|
||||
@@ -40,7 +41,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
this.cache = body;
|
||||
|
@@ -10,22 +10,31 @@ import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export type RoleOptions = {
|
||||
gtlAvailable: boolean;
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canInvite: boolean;
|
||||
canManageCustomEmojis: boolean;
|
||||
driveCapacityMb: number;
|
||||
antennaLimit: number;
|
||||
wordMuteLimit: number;
|
||||
webhookLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_ROLE: RoleOptions = {
|
||||
gtlAvailable: true,
|
||||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
canInvite: false,
|
||||
canManageCustomEmojis: false,
|
||||
driveCapacityMb: 100,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
webhookLimit: 3,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -63,7 +72,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'roleCreated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
@@ -141,6 +150,18 @@ export class RoleService implements OnApplicationShutdown {
|
||||
case 'createdMoreThan': {
|
||||
return user.createdAt.getTime() < (Date.now() - (value.sec * 1000));
|
||||
}
|
||||
case 'followersLessThanOrEq': {
|
||||
return user.followersCount <= value.value;
|
||||
}
|
||||
case 'followersMoreThanOrEq': {
|
||||
return user.followersCount >= value.value;
|
||||
}
|
||||
case 'followingLessThanOrEq': {
|
||||
return user.followingCount <= value.value;
|
||||
}
|
||||
case 'followingMoreThanOrEq': {
|
||||
return user.followingCount >= value.value;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -179,8 +200,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true),
|
||||
ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true),
|
||||
canPublicNote: getOptionValues('canPublicNote').some(x => x === true),
|
||||
canInvite: getOptionValues('canInvite').some(x => x === true),
|
||||
canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true),
|
||||
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
|
||||
antennaLimit: Math.max(...getOptionValues('antennaLimit')),
|
||||
wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
|
||||
webhookLimit: Math.max(...getOptionValues('webhookLimit')),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/mode
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@@ -39,7 +40,7 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'userChangeSuspendedState':
|
||||
case 'remoteUserUpdated': {
|
||||
@@ -62,6 +63,13 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
this.localUserByNativeTokenCache.set(body.newToken, user);
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
const follower = this.userByIdCache.get(body.followerId);
|
||||
if (follower) follower.followingCount++;
|
||||
const followee = this.userByIdCache.get(body.followeeId);
|
||||
if (followee) followee.followersCount++;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@@ -62,6 +62,7 @@ export class UserFollowingService {
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {
|
||||
@@ -195,6 +196,8 @@ export class UserFollowingService {
|
||||
}
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
//#region Increment counts
|
||||
await Promise.all([
|
||||
@@ -314,6 +317,8 @@ export class UserFollowingService {
|
||||
follower: {id: User['id']; host: User['host']; },
|
||||
followee: { id: User['id']; host: User['host']; },
|
||||
): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
//#region Decrement following / followers counts
|
||||
await Promise.all([
|
||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||
|
@@ -24,6 +24,12 @@ export class UtilityService {
|
||||
return this.toPuny(this.config.host) === this.toPuny(host);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
|
||||
if (host == null) return false;
|
||||
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public extractDbHost(uri: string): string {
|
||||
const url = new URL(uri);
|
||||
|
@@ -3,8 +3,9 @@ import Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/index.js';
|
||||
import type { Webhook } from '@/models/entities/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService implements OnApplicationShutdown {
|
||||
@@ -39,7 +40,7 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
|
@@ -291,7 +291,7 @@ export class ApInboxService {
|
||||
|
||||
// アナウンス先をブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return;
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
@@ -96,7 +96,7 @@ export class Resolver {
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
||||
throw new Error('Instance is blocked');
|
||||
}
|
||||
|
||||
|
@@ -324,7 +324,7 @@ export class ApNoteService {
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) throw { statusCode: 451 };
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 };
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
@@ -61,21 +61,21 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followerHost)')
|
||||
.where('following.followerHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||
.setParameters(pubsubSubQuery.getParameters())
|
||||
@@ -84,7 +84,7 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.isSuspended = false')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
@@ -92,7 +92,7 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.isSuspended = false')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
@@ -7,8 +7,8 @@ import type { } from '@/models/entities/Blocking.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UtilityService } from '../UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class InstanceEntityService {
|
||||
@@ -17,6 +17,8 @@ export class InstanceEntityService {
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,7 +37,7 @@ export class InstanceEntityService {
|
||||
followersCount: instance.followersCount,
|
||||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.isSuspended,
|
||||
isBlocked: meta.blockedHosts.includes(instance.host),
|
||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
openRegistrations: instance.openRegistrations,
|
||||
|
@@ -62,6 +62,7 @@ export class RoleEntityService {
|
||||
isModerator: role.isModerator,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
options: roleOptions,
|
||||
usersCount: assigns.length,
|
||||
...(opts.detail ? {
|
||||
users: this.userEntityService.packMany(assigns.map(x => x.userId), me),
|
||||
} : {}),
|
||||
|
@@ -448,6 +448,14 @@ export class UserEntityService implements OnModuleInit {
|
||||
userId: user.id,
|
||||
}).then(result => result >= 1)
|
||||
: false,
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
}))),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail && isMe ? {
|
||||
|
@@ -34,6 +34,26 @@ type CondFormulaValueCreatedMoreThan = {
|
||||
sec: number;
|
||||
};
|
||||
|
||||
type CondFormulaValueFollowersLessThanOrEq = {
|
||||
type: 'followersLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
type CondFormulaValueFollowersMoreThanOrEq = {
|
||||
type: 'followersMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
type CondFormulaValueFollowingLessThanOrEq = {
|
||||
type: 'followingLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
type CondFormulaValueFollowingMoreThanOrEq = {
|
||||
type: 'followingMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type RoleCondFormulaValue =
|
||||
CondFormulaValueAnd |
|
||||
CondFormulaValueOr |
|
||||
@@ -41,7 +61,11 @@ export type RoleCondFormulaValue =
|
||||
CondFormulaValueIsLocal |
|
||||
CondFormulaValueIsRemote |
|
||||
CondFormulaValueCreatedLessThan |
|
||||
CondFormulaValueCreatedMoreThan;
|
||||
CondFormulaValueCreatedMoreThan |
|
||||
CondFormulaValueFollowersLessThanOrEq |
|
||||
CondFormulaValueFollowersMoreThanOrEq |
|
||||
CondFormulaValueFollowingLessThanOrEq |
|
||||
CondFormulaValueFollowingMoreThanOrEq;
|
||||
|
||||
@Entity()
|
||||
export class Role {
|
||||
|
@@ -56,7 +56,7 @@ export class DeliverProcessorService {
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.blockedHosts.includes(this.utilityService.toPuny(host))) {
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
|
||||
return 'skip (blocked)';
|
||||
}
|
||||
|
||||
|
@@ -76,7 +76,7 @@ export class InboxProcessorService {
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
||||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export class InboxProcessorService {
|
||||
|
||||
// ブロックしてたら中断
|
||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||
if (meta.blockedHosts.includes(ldHost)) {
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
||||
return `Blocked request: ${ldHost}`;
|
||||
}
|
||||
} else {
|
||||
|
@@ -271,6 +271,17 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
if (ep.meta.requireRoleOption != null && !user!.isRoot) {
|
||||
const myRole = await this.roleService.getUserRoleOptions(user!.id);
|
||||
if (!myRole[ep.meta.requireRoleOption]) {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to a required role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
|
||||
throw new ApiError({
|
||||
message: 'Your app does not have the necessary permissions to use this endpoint.',
|
||||
|
@@ -36,8 +36,8 @@ export class ApiServerService {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apiCallService: ApiCallService,
|
||||
private signupApiServiceService: SignupApiService,
|
||||
private signinApiServiceService: SigninApiService,
|
||||
private signupApiService: SignupApiService,
|
||||
private signinApiService: SigninApiService,
|
||||
private githubServerService: GithubServerService,
|
||||
private discordServerService: DiscordServerService,
|
||||
private twitterServerService: TwitterServerService,
|
||||
@@ -116,7 +116,7 @@ export class ApiServerService {
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
}
|
||||
}>('/signup', (request, reply) => this.signupApiServiceService.signup(request, reply));
|
||||
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
|
||||
|
||||
fastify.post<{
|
||||
Body: {
|
||||
@@ -129,9 +129,9 @@ export class ApiServerService {
|
||||
credentialId?: string;
|
||||
challengeId?: string;
|
||||
};
|
||||
}>('/signin', (request, reply) => this.signinApiServiceService.signin(request, reply));
|
||||
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
|
||||
|
||||
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiServiceService.signupPending(request, reply));
|
||||
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
|
||||
|
||||
fastify.register(this.discordServerService.create);
|
||||
fastify.register(this.githubServerService.create);
|
||||
|
@@ -37,7 +37,7 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___admin_invite from './endpoints/admin/invite.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -371,7 +371,7 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati
|
||||
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
|
||||
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
|
||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||
const $admin_invite: Provider = { provide: 'ep:admin/invite', useClass: ep___admin_invite.default };
|
||||
const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default };
|
||||
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
||||
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
||||
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
||||
@@ -709,7 +709,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$admin_invite,
|
||||
$invite,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
@@ -1041,7 +1041,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$admin_invite,
|
||||
$invite,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
|
@@ -12,8 +12,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { ILocalUser } from '@/models/entities/User.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
@@ -193,7 +193,7 @@ export class SignupApiService {
|
||||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
this.signinService.signin(request, reply, account as ILocalUser);
|
||||
return this.signinService.signin(request, reply, account as ILocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___admin_invite from './endpoints/admin/invite.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -368,7 +368,7 @@ const eps = [
|
||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||
['admin/get-table-stats', ep___admin_getTableStats],
|
||||
['admin/get-user-ips', ep___admin_getUserIps],
|
||||
['admin/invite', ep___admin_invite],
|
||||
['invite', ep___invite],
|
||||
['admin/promo/create', ep___admin_promo_create],
|
||||
['admin/queue/clear', ep___admin_queue_clear],
|
||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||
@@ -695,6 +695,8 @@ export interface IEndpointMeta {
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
readonly requireRoleOption?: string;
|
||||
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||
|
@@ -38,7 +38,7 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -14,7 +14,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
|
@@ -14,7 +14,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
|
@@ -9,7 +9,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -10,7 +10,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
|
@@ -5,7 +5,7 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
@@ -11,7 +11,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -8,7 +8,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@@ -9,7 +9,7 @@ export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canManageCustomEmojis',
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
|
@@ -139,7 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.blockedHosts)) {
|
||||
set.blockedHosts = ps.blockedHosts.filter(Boolean);
|
||||
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
|
||||
}
|
||||
|
||||
if (ps.themeColor !== undefined) {
|
||||
|
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
// ブロックしてたら中断
|
||||
const fetchedMeta = await this.metaService.fetch();
|
||||
if (fetchedMeta.blockedHosts.includes(this.utilityService.extractDbHost(uri))) return null;
|
||||
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
|
||||
|
||||
let local = await this.mergePack(me, ...await Promise.all([
|
||||
this.apDbResolverService.getUserFromApId(uri),
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import type { FollowRequestsRepository } from '@/models/index.js';
|
||||
import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -40,7 +41,11 @@ export const meta = {
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@@ -52,13 +57,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private followRequestEntityService: FollowRequestEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const reqs = await this.followRequestsRepository.findBy({
|
||||
followeeId: me.id,
|
||||
});
|
||||
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId);
|
||||
|
||||
return await Promise.all(reqs.map(req => this.followRequestEntityService.pack(req)));
|
||||
const requests = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -62,6 +63,12 @@ export const meta = {
|
||||
code: 'INVALID_REGEXP',
|
||||
id: '0d786918-10df-41cd-8f33-8dec7d9a89a5',
|
||||
},
|
||||
|
||||
tooManyMutedWords: {
|
||||
message: 'Too many muted words.',
|
||||
code: 'TOO_MANY_MUTED_WORDS',
|
||||
id: '010665b1-a211-42d2-bc64-8f6609d79785',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
@@ -144,6 +151,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private userFollowingService: UserFollowingService,
|
||||
private accountUpdateService: AccountUpdateService,
|
||||
private hashtagService: HashtagService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _user, token) => {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
|
||||
@@ -163,6 +171,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
||||
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
||||
if (ps.mutedWords !== undefined) {
|
||||
// TODO: ちゃんと数える
|
||||
const length = JSON.stringify(ps.mutedWords).length;
|
||||
if (length > (await this.roleService.getUserRoleOptions(user.id)).wordMuteLimit) {
|
||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||
}
|
||||
|
||||
// validate regular expression syntax
|
||||
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
||||
|
@@ -5,6 +5,7 @@ import type { WebhooksRepository } from '@/models/index.js';
|
||||
import { webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
@@ -12,6 +13,14 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
tooManyWebhooks: {
|
||||
message: 'You cannot create webhook any more.',
|
||||
code: 'TOO_MANY_WEBHOOKS',
|
||||
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
@@ -38,8 +47,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const currentWebhooksCount = await this.webhooksRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentWebhooksCount > (await this.roleService.getUserRoleOptions(me.id)).webhookLimit) {
|
||||
throw new ApiError(meta.errors.tooManyWebhooks);
|
||||
}
|
||||
|
||||
const webhook = await this.webhooksRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
|
@@ -67,7 +67,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
active: ps.active,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('webhookUpdated', webhook);
|
||||
const updated = await this.webhooksRepository.findOneByOrFail({
|
||||
id: ps.webhookId,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('webhookUpdated', updated);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -6,10 +6,10 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRoleOption: 'canInvite',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async () => {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const code = rndstr({
|
||||
length: 8,
|
||||
chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
|
@@ -14,7 +14,7 @@ import type { Page } from '@/models/entities/Page.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { Webhook } from '@/models/entities/Webhook.js';
|
||||
import type { Meta } from '@/models/entities/Meta.js';
|
||||
import { Role, RoleAssignment } from '@/models';
|
||||
import { Following, Role, RoleAssignment } from '@/models';
|
||||
import type Emitter from 'strict-event-emitter-types';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface InternalStreamTypes {
|
||||
userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
|
||||
userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
|
||||
remoteUserUpdated: Serialized<{ id: User['id']; }>;
|
||||
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
||||
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
||||
defaultRoleOverrideUpdated: Serialized<Role['options']>;
|
||||
roleCreated: Serialized<Role>;
|
||||
roleDeleted: Serialized<Role>;
|
||||
|
@@ -37,7 +37,7 @@
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.1",
|
||||
"mfm-js": "0.23.3",
|
||||
"misskey-js": "0.0.14",
|
||||
"photoswipe": "5.3.4",
|
||||
"prismjs": "1.29.0",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none">
|
||||
<svg :class="$style.root" viewBox="0 0 10 10" preserveAspectRatio="none">
|
||||
<template v-if="props.graduations === 'dots'">
|
||||
<circle
|
||||
v-for="(angle, i) in graduationsMajor"
|
||||
@@ -39,8 +39,7 @@
|
||||
-->
|
||||
|
||||
<line
|
||||
class="s"
|
||||
:class="{ animate: !disableSAnimate && sAnimation !== 'none', elastic: sAnimation === 'elastic', easeOut: sAnimation === 'easeOut' }"
|
||||
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
|
||||
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
|
||||
:x2="5 + (0 * ((sHandLengthRatio * 5) - handsPadding))"
|
||||
@@ -205,21 +204,21 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mbcofsoe {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .s {
|
||||
will-change: transform;
|
||||
transform-origin: 50% 50%;
|
||||
.s {
|
||||
will-change: transform;
|
||||
transform-origin: 50% 50%;
|
||||
|
||||
&.animate.elastic {
|
||||
transition: transform .2s cubic-bezier(.4,2.08,.55,.44);
|
||||
}
|
||||
&.animate.elastic {
|
||||
transition: transform .2s cubic-bezier(.4,2.08,.55,.44);
|
||||
}
|
||||
|
||||
&.animate.easeOut {
|
||||
transition: transform .7s cubic-bezier(0,.7,.3,1);
|
||||
}
|
||||
&.animate.easeOut {
|
||||
transition: transform .7s cubic-bezier(0,.7,.3,1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button class="nrvgflfu _button" @click="toggle">
|
||||
<button class="_button" :class="$style.root" @click="toggle">
|
||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||
<span v-if="!modelValue">{{ label }}</span>
|
||||
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -34,8 +34,8 @@ const toggle = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nrvgflfu {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.7em;
|
||||
@@ -46,17 +46,17 @@ const toggle = () => {
|
||||
&:hover {
|
||||
background: var(--cwHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 4px;
|
||||
.label {
|
||||
margin-left: 4px;
|
||||
|
||||
&:before {
|
||||
content: '(';
|
||||
}
|
||||
&:before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ')';
|
||||
}
|
||||
&:after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
|
||||
import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
|
||||
import MkAd from '@/components/global/MkAd.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
|
||||
type: Array as PropType<MisskeyEntity[]>,
|
||||
required: true,
|
||||
},
|
||||
direction: {
|
||||
@@ -33,6 +34,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule();
|
||||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
@@ -57,21 +59,25 @@ export default defineComponent({
|
||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: 'separator',
|
||||
class: $style['separator'],
|
||||
key: item.id + ':separator',
|
||||
}, h('p', {
|
||||
class: 'date',
|
||||
class: $style['date'],
|
||||
}, [
|
||||
h('span', [
|
||||
h('span', {
|
||||
class: $style['date-1'],
|
||||
}, [
|
||||
h('i', {
|
||||
class: 'ti ti-chevron-up icon',
|
||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||
}),
|
||||
getDateText(item.createdAt),
|
||||
]),
|
||||
h('span', [
|
||||
h('span', {
|
||||
class: $style['date-2'],
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
h('i', {
|
||||
class: 'ti ti-chevron-down icon',
|
||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||
}),
|
||||
]),
|
||||
]));
|
||||
@@ -89,26 +95,62 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
function onBeforeLeave(el: HTMLElement) {
|
||||
el.style.top = `${el.offsetTop}px`;
|
||||
el.style.left = `${el.offsetLeft}px`;
|
||||
}
|
||||
function onLeaveCanceled(el: HTMLElement) {
|
||||
el.style.top = '';
|
||||
el.style.left = '';
|
||||
}
|
||||
|
||||
return () => h(
|
||||
defaultStore.state.animation ? TransitionGroup : 'div',
|
||||
defaultStore.state.animation ? {
|
||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
||||
name: 'list',
|
||||
tag: 'div',
|
||||
'data-direction': props.direction,
|
||||
'data-reversed': props.reversed ? 'true' : 'false',
|
||||
} : {
|
||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
||||
{
|
||||
class: {
|
||||
[$style['date-separated-list']]: true,
|
||||
[$style['date-separated-list-nogap']]: props.noGap,
|
||||
[$style['reversed']]: props.reversed,
|
||||
[$style['direction-down']]: props.direction === 'down',
|
||||
[$style['direction-up']]: props.direction === 'up',
|
||||
},
|
||||
...(defaultStore.state.animation ? {
|
||||
name: 'list',
|
||||
tag: 'div',
|
||||
onBeforeLeave,
|
||||
onLeaveCanceled,
|
||||
} : {}),
|
||||
},
|
||||
{ default: renderChildren });
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sqadhkmv {
|
||||
<style lang="scss" module>
|
||||
.date-separated-list {
|
||||
container-type: inline-size;
|
||||
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
> .list-leave-active,
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> .list-leave-from,
|
||||
> .list-leave-to,
|
||||
> .list-leave-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -116,73 +158,75 @@ export default defineComponent({
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
}
|
||||
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.date-separated-list-nogap {
|
||||
> * {
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
&[data-direction="up"] {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-direction="down"] {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
}
|
||||
|
||||
> .separator {
|
||||
text-align: center;
|
||||
|
||||
> .date {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--dateLabelFg);
|
||||
|
||||
> span {
|
||||
&:first-child {
|
||||
margin-right: 8px;
|
||||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: 8px;
|
||||
|
||||
> .icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.noGap {
|
||||
> * {
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.direction-up {
|
||||
&:global {
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
.direction-down {
|
||||
&:global {
|
||||
> .list-enter-from,
|
||||
> .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reversed {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.separator {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.date {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--dateLabelFg);
|
||||
}
|
||||
|
||||
.date-1 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-1-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.date-2-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
|
||||
<div class="header _button" @click="toggle">
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
<span class="text"><slot name="label"></slot></span>
|
||||
<span class="right">
|
||||
<span class="text"><slot name="suffix"></slot></span>
|
||||
<div ref="rootEl" :class="[$style.root, { [$style.opened]: opened }]">
|
||||
<div :class="$style.header" class="_button" @click="toggle">
|
||||
<span :class="$style.headerIcon"><slot name="icon"></slot></span>
|
||||
<span :class="$style.headerText"><slot name="label"></slot></span>
|
||||
<span :class="$style.headerRight">
|
||||
<span :class="$style.headerRightText"><slot name="suffix"></slot></span>
|
||||
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||
<i v-else class="ti ti-chevron-down icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
|
||||
<Transition
|
||||
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||
:enter-active-class="$store.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="$store.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enter-from-class="$store.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leave-to-class="$store.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@leave="leave"
|
||||
@@ -94,85 +97,88 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
overflow-y: clip;
|
||||
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.folder-toggle-enter-from, .folder-toggle-leave-to {
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dwzlatin {
|
||||
.root {
|
||||
display: block;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px 10px 14px;
|
||||
background: var(--buttonBg);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 0.75em;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
||||
& + .text {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
> .right {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
|
||||
> .text:not(:empty) {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: 0 0 6px 6px;
|
||||
container-type: inline-size;
|
||||
overflow: auto;
|
||||
|
||||
&.bgSame {
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
&.opened {
|
||||
> .header {
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 12px 9px 12px;
|
||||
background: var(--buttonBg);
|
||||
border-radius: 6px;
|
||||
transition: border-radius 0.3s;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
margin-right: 0.75em;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
||||
& + .headerText {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.headerText {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.headerRightText:not(:empty) {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.body {
|
||||
background: var(--panel);
|
||||
border-radius: 0 0 6px 6px;
|
||||
container-type: inline-size;
|
||||
overflow: auto;
|
||||
|
||||
&.bgSame {
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -9,7 +9,16 @@
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap }]">
|
||||
<MkDateSeparatedList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" :class="$style.notes">
|
||||
<MkDateSeparatedList
|
||||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:no-gap="noGap"
|
||||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
|
@@ -35,26 +35,26 @@
|
||||
<div v-once :class="$style.content">
|
||||
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :author="notification.note.renote.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'mention'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
<span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
||||
|
@@ -15,14 +15,14 @@
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
<slot :items="items"></slot>
|
||||
<slot :items="items" :fetching="fetching || moreFetching"></slot>
|
||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
@@ -31,15 +31,18 @@
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue';
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
const TOLERANCE = 16;
|
||||
|
||||
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
||||
endpoint: E;
|
||||
@@ -58,8 +61,11 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
|
||||
reversed?: boolean;
|
||||
|
||||
offsetMode?: boolean;
|
||||
};
|
||||
|
||||
pageEl?: HTMLElement;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
disableAutoLoad?: boolean;
|
||||
@@ -72,21 +78,73 @@ const emit = defineEmits<{
|
||||
(ev: 'queue', count: number): void;
|
||||
}>();
|
||||
|
||||
type Item = { id: string; [another: string]: unknown; };
|
||||
let rootEl = $shallowRef<HTMLElement>();
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const items = ref<Item[]>([]);
|
||||
const queue = ref<Item[]>([]);
|
||||
// 遡り中かどうか
|
||||
let backed = $ref(false);
|
||||
|
||||
let scrollRemove = $ref<(() => void) | null>(null);
|
||||
|
||||
const items = ref<MisskeyEntity[]>([]);
|
||||
const queue = ref<MisskeyEntity[]>([]);
|
||||
const offset = ref(0);
|
||||
const fetching = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const more = ref(false);
|
||||
const backed = ref(false); // 遡り中か否か
|
||||
const isBackTop = ref(false);
|
||||
const empty = computed(() => items.value.length === 0);
|
||||
const error = ref(false);
|
||||
const {
|
||||
enableInfiniteScroll
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const init = async (): Promise<void> => {
|
||||
const contentEl = $computed(() => props.pagination.pageEl || rootEl);
|
||||
const scrollableElement = $computed(() => getScrollContainer(contentEl));
|
||||
|
||||
// 先頭が表示されているかどうかを検出
|
||||
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
|
||||
let scrollObserver = $ref<IntersectionObserver>();
|
||||
|
||||
watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
|
||||
if (scrollObserver) scrollObserver.disconnect();
|
||||
|
||||
scrollObserver = new IntersectionObserver(entries => {
|
||||
backed = entries[0].isIntersecting;
|
||||
}, {
|
||||
root: scrollableElement,
|
||||
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
||||
threshold: 0.01,
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
watch($$(rootEl), () => {
|
||||
scrollObserver.disconnect();
|
||||
nextTick(() => {
|
||||
if (rootEl) scrollObserver.observe(rootEl);
|
||||
});
|
||||
});
|
||||
|
||||
watch([$$(backed), $$(contentEl)], () => {
|
||||
if (!backed) {
|
||||
if (!contentEl) return;
|
||||
|
||||
scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
|
||||
} else {
|
||||
if (scrollRemove) scrollRemove();
|
||||
scrollRemove = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
emit('queue', queue.value.length);
|
||||
}, { deep: true });
|
||||
|
||||
async function init(): Promise<void> {
|
||||
queue.value = [];
|
||||
fetching.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
@@ -96,18 +154,15 @@ const init = async (): Promise<void> => {
|
||||
}).then(res => {
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
if (props.pagination.reversed) {
|
||||
if (i === res.length - 2) item._shouldInsertAd_ = true;
|
||||
} else {
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
|
||||
res.pop();
|
||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||
if (props.pagination.reversed) moreFetching.value = true;
|
||||
items.value = res;
|
||||
more.value = true;
|
||||
} else {
|
||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||
items.value = res;
|
||||
more.value = false;
|
||||
}
|
||||
offset.value = res.length;
|
||||
@@ -117,17 +172,16 @@ const init = async (): Promise<void> => {
|
||||
error.value = true;
|
||||
fetching.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const reload = (): void => {
|
||||
const reload = (): Promise<void> => {
|
||||
items.value = [];
|
||||
init();
|
||||
return init();
|
||||
};
|
||||
|
||||
const fetchMore = async (): Promise<void> => {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||
moreFetching.value = true;
|
||||
backed.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
await os.api(props.pagination.endpoint, {
|
||||
...params,
|
||||
@@ -142,22 +196,52 @@ const fetchMore = async (): Promise<void> => {
|
||||
}).then(res => {
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
if (props.pagination.reversed) {
|
||||
if (i === res.length - 9) item._shouldInsertAd_ = true;
|
||||
} else {
|
||||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
const reverseConcat = _res => {
|
||||
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
|
||||
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
|
||||
|
||||
items.value = items.value.concat(_res);
|
||||
|
||||
return nextTick(() => {
|
||||
if (scrollableElement) {
|
||||
scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
};
|
||||
|
||||
if (res.length > SECOND_FETCH_LIMIT) {
|
||||
res.pop();
|
||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||
more.value = true;
|
||||
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
items.value = items.value.concat(res);
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
} else {
|
||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||
more.value = false;
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
items.value = items.value.concat(res);
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
}
|
||||
offset.value += res.length;
|
||||
moreFetching.value = false;
|
||||
}, err => {
|
||||
moreFetching.value = false;
|
||||
});
|
||||
@@ -180,10 +264,10 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||
}).then(res => {
|
||||
if (res.length > SECOND_FETCH_LIMIT) {
|
||||
res.pop();
|
||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||
items.value = items.value.concat(res);
|
||||
more.value = true;
|
||||
} else {
|
||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
||||
items.value = items.value.concat(res);
|
||||
more.value = false;
|
||||
}
|
||||
offset.value += res.length;
|
||||
@@ -193,106 +277,96 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||
});
|
||||
};
|
||||
|
||||
const prepend = (item: Item): void => {
|
||||
if (props.pagination.reversed) {
|
||||
if (rootEl.value) {
|
||||
const container = getScrollContainer(rootEl.value);
|
||||
if (container == null) {
|
||||
// TODO?
|
||||
} else {
|
||||
const pos = getScrollPosition(rootEl.value);
|
||||
const viewHeight = container.clientHeight;
|
||||
const height = container.scrollHeight;
|
||||
const isBottom = (pos + viewHeight > height - 32);
|
||||
if (isBottom) {
|
||||
// オーバーフローしたら古いアイテムは捨てる
|
||||
if (items.value.length >= props.displayLimit) {
|
||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
||||
//items.value = items.value.slice(-props.displayLimit);
|
||||
while (items.value.length >= props.displayLimit) {
|
||||
items.value.shift();
|
||||
}
|
||||
more.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items.value.push(item);
|
||||
// TODO
|
||||
} else {
|
||||
// 初回表示時はunshiftだけでOK
|
||||
if (!rootEl.value) {
|
||||
items.value.unshift(item);
|
||||
return;
|
||||
}
|
||||
|
||||
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
||||
|
||||
if (isTop) {
|
||||
// Prepend the item
|
||||
items.value.unshift(item);
|
||||
|
||||
// オーバーフローしたら古いアイテムは捨てる
|
||||
if (items.value.length >= props.displayLimit) {
|
||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
||||
//this.items = items.value.slice(0, props.displayLimit);
|
||||
while (items.value.length >= props.displayLimit) {
|
||||
items.value.pop();
|
||||
}
|
||||
more.value = true;
|
||||
}
|
||||
} else {
|
||||
queue.value.push(item);
|
||||
onScrollTop(rootEl.value, () => {
|
||||
for (const item of queue.value) {
|
||||
prepend(item);
|
||||
}
|
||||
queue.value = [];
|
||||
});
|
||||
}
|
||||
const prepend = (item: MisskeyEntity): void => {
|
||||
// 初回表示時はunshiftだけでOK
|
||||
if (!rootEl) {
|
||||
items.value.unshift(item);
|
||||
return;
|
||||
}
|
||||
|
||||
const isTop = isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
|
||||
|
||||
if (isTop) unshiftItems([item]);
|
||||
else prependQueue(item);
|
||||
};
|
||||
|
||||
const append = (item: Item): void => {
|
||||
function unshiftItems(newItems: MisskeyEntity[]) {
|
||||
const length = newItems.length + items.value.length;
|
||||
items.value = [ ...newItems, ...items.value ].slice(0, props.displayLimit);
|
||||
|
||||
if (length >= props.displayLimit) more.value = true;
|
||||
}
|
||||
|
||||
function executeQueue() {
|
||||
if (queue.value.length === 0) return;
|
||||
unshiftItems(queue.value);
|
||||
queue.value = [];
|
||||
}
|
||||
|
||||
function prependQueue(newItem: MisskeyEntity) {
|
||||
queue.value.unshift(newItem);
|
||||
if (queue.value.length >= props.displayLimit) {
|
||||
queue.value.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const appendItem = (item: MisskeyEntity): void => {
|
||||
items.value.push(item);
|
||||
};
|
||||
|
||||
const removeItem = (finder: (item: Item) => boolean) => {
|
||||
const removeItem = (finder: (item: MisskeyEntity) => boolean) => {
|
||||
const i = items.value.findIndex(finder);
|
||||
items.value.splice(i, 1);
|
||||
};
|
||||
|
||||
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
|
||||
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
||||
const i = items.value.findIndex(item => item.id === id);
|
||||
items.value[i] = replacer(items.value[i]);
|
||||
};
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
emit('queue', queue.value.length);
|
||||
}, { deep: true });
|
||||
|
||||
init();
|
||||
const inited = init();
|
||||
|
||||
onActivated(() => {
|
||||
isBackTop.value = false;
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isBackTop.value = window.scrollY === 0;
|
||||
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
||||
});
|
||||
|
||||
function toBottom() {
|
||||
scrollToBottom(contentEl);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
inited.then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
|
||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||
// more = trueを遅らせる
|
||||
setTimeout(() => {
|
||||
moreFetching.value = false;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
scrollObserver.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
items,
|
||||
queue,
|
||||
backed,
|
||||
more,
|
||||
inited,
|
||||
reload,
|
||||
prepend,
|
||||
append,
|
||||
append: appendItem,
|
||||
removeItem,
|
||||
updateItem,
|
||||
});
|
||||
|
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<MkA v-adaptive-bg :to="`/admin/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||
<div :class="$style.title">{{ role.name }}</div>
|
||||
<div :class="$style.title">
|
||||
<span :class="$style.name">{{ role.name }}</span>
|
||||
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
|
||||
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
|
||||
</div>
|
||||
<div :class="$style.description">{{ role.description }}</div>
|
||||
</MkA>
|
||||
</template>
|
||||
@@ -9,6 +13,7 @@
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
role: any;
|
||||
@@ -23,9 +28,18 @@ const props = defineProps<{
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.users {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="mk-toast">
|
||||
<Transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')">
|
||||
<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
|
||||
<div class="message">
|
||||
<div>
|
||||
<Transition
|
||||
:enter-active-class="$store.state.animation ? $style.transition_toast_enterActive : ''"
|
||||
:leave-active-class="$store.state.animation ? $style.transition_toast_leaveActive : ''"
|
||||
:enter-from-class="$store.state.animation ? $style.transition_toast_enterFrom : ''"
|
||||
:leave-to-class="$store.state.animation ? $style.transition_toast_leaveTo : ''"
|
||||
appear @after-leave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
|
||||
<div style="padding: 16px 24px;">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,35 +38,31 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toast-enter-active, .toast-leave-active {
|
||||
<style lang="scss" module>
|
||||
.transition_toast_enterActive,
|
||||
.transition_toast_leaveActive {
|
||||
transition: opacity 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.toast-enter-from, .toast-leave-to {
|
||||
.transition_toast_enterFrom,
|
||||
.transition_toast_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.mk-toast {
|
||||
> .body {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 0 auto;
|
||||
margin-top: 16px;
|
||||
min-width: 300px;
|
||||
max-width: calc(100% - 32px);
|
||||
width: min-content;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
|
||||
> .message {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
> .root {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 0 auto;
|
||||
margin-top: 16px;
|
||||
min-width: 300px;
|
||||
max-width: calc(100% - 32px);
|
||||
width: min-content;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div class="ewlycnyt">
|
||||
<div class="title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
|
||||
<div class="version">✨{{ version }}🚀</div>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
|
||||
<div :class="$style.version">✨{{ version }}🚀</div>
|
||||
<MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton>
|
||||
<MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton>
|
||||
<MkButton :class="$style.gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
@@ -32,8 +32,8 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ewlycnyt {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
@@ -42,17 +42,17 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .version {
|
||||
margin: 1em 0;
|
||||
}
|
||||
.version {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
> .gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
.gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
|
||||
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
<div v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
|
||||
<iframe :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
</div>
|
||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
|
||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" :class="$style.twitter">
|
||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||
</div>
|
||||
<div v-else class="mk-url-preview">
|
||||
<component :is="self ? 'MkA' : 'a'" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
<div v-else :class="$style.urlPreview">
|
||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
</div>
|
||||
<article>
|
||||
<header>
|
||||
<h1 v-if="unknownUrl">{{ url }}</h1>
|
||||
<h1 v-else-if="fetching"><MkEllipsis/></h1>
|
||||
<h1 v-else :title="title">{{ title }}</h1>
|
||||
<article :class="$style.body">
|
||||
<header :class="$style.header">
|
||||
<h1 v-if="unknownUrl" :class="$style.title">{{ url }}</h1>
|
||||
<h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1>
|
||||
<h1 v-else :class="$style.title" :title="title">{{ title }}</h1>
|
||||
</header>
|
||||
<p v-if="unknownUrl">{{ i18n.ts.cannotLoad }}</p>
|
||||
<p v-else-if="fetching"><MkEllipsis/></p>
|
||||
<p v-else-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
|
||||
<footer>
|
||||
<img v-if="icon" class="icon" :src="icon"/>
|
||||
<p v-if="unknownUrl">?</p>
|
||||
<p v-else-if="fetching"><MkEllipsis/></p>
|
||||
<p v-else :title="sitename">{{ sitename }}</p>
|
||||
<p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p>
|
||||
<p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p>
|
||||
<p v-else-if="description" :class="$style.text" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
|
||||
<footer :class="$style.footer">
|
||||
<img v-if="icon" :class="$style.siteIcon" :src="icon"/>
|
||||
<p v-if="unknownUrl" :class="$style.siteName">?</p>
|
||||
<p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p>
|
||||
<p v-else :class="$style.siteName" :title="sitename">{{ sitename }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</component>
|
||||
<div v-if="tweetId" class="action">
|
||||
<div v-if="tweetId" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = true">
|
||||
<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="!playerEnabled && player.url" class="action">
|
||||
<div v-if="!playerEnabled && player.url" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="playerEnabled = true">
|
||||
<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
|
||||
</MkButton>
|
||||
@@ -136,197 +136,198 @@ onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.player {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button {
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
right: 0;
|
||||
font-size: 1em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--fg);
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
opacity: 0.7;
|
||||
.disablePlayer {
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
right: 0;
|
||||
font-size: 1em;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--fg);
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
> iframe {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.mk-url-preview {
|
||||
> .link {
|
||||
position: relative;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 0 1px var(--divider);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
.playerIframe {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
.twitter {
|
||||
|
||||
> article > header > h1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.urlPreview {
|
||||
}
|
||||
|
||||
& + article {
|
||||
left: 100px;
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
.link {
|
||||
position: relative;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 0 1px var(--divider);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
|
||||
> article {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 8px;
|
||||
height: 16px;
|
||||
|
||||
> img {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
> p {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: var(--urlPreviewInfo);
|
||||
font-size: 0.8em;
|
||||
line-height: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
> article {
|
||||
> header h1, p, footer {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
> .body > .header > .title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
> .action {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
&.compact {
|
||||
> .body {
|
||||
> .header .title, .text, .footer {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& + .body {
|
||||
left: 100px;
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 8px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.siteIcon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.siteName {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: var(--urlPreviewInfo);
|
||||
font-size: 0.8em;
|
||||
line-height: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@container (max-width: 400px) {
|
||||
.mk-url-preview {
|
||||
> .link {
|
||||
font-size: 12px;
|
||||
.link {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
height: 80px;
|
||||
}
|
||||
.thumbnail {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
.body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 350px) {
|
||||
.mk-url-preview {
|
||||
> .link {
|
||||
font-size: 10px;
|
||||
.link {
|
||||
font-size: 10px;
|
||||
|
||||
&.compact {
|
||||
> .thumbnail {
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
width: 56px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 8px;
|
||||
> .body {
|
||||
left: 56px;
|
||||
width: calc(100% - 56px);
|
||||
padding: 4px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 4px;
|
||||
> .header {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 4px;
|
||||
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
> .thumbnail {
|
||||
position: absolute;
|
||||
width: 56px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> article {
|
||||
left: 56px;
|
||||
width: calc(100% - 56px);
|
||||
padding: 4px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 2px;
|
||||
}
|
||||
> .footer {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.siteIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
|
||||
<div class="beaffaef">
|
||||
<div v-for="u in users" :key="u.id" class="user">
|
||||
<MkAvatar class="avatar" :user="u"/>
|
||||
<MkUserName class="name" :user="u" :nowrap="true"/>
|
||||
<div :class="$style.root">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
|
||||
</div>
|
||||
<div v-if="users.length < count" class="omitted">+{{ count - users.length }}</div>
|
||||
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
@@ -26,26 +26,34 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.beaffaef {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
font-size: 0.9em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
> .user {
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.user {
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
|
||||
}
|
||||
|
||||
.omitted {
|
||||
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,34 +1,41 @@
|
||||
<template>
|
||||
<Transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
||||
<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
|
||||
<div class="body _shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<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>
|
||||
<Transition
|
||||
:enter-active-class="$store.state.animation ? $style.transition_window_enterActive : ''"
|
||||
:leave-active-class="$store.state.animation ? $style.transition_window_leaveActive : ''"
|
||||
:enter-from-class="$store.state.animation ? $style.transition_window_enterFrom : ''"
|
||||
:leave-to-class="$store.state.animation ? $style.transition_window_leaveTo : ''"
|
||||
appear
|
||||
@after-leave="$emit('closed')"
|
||||
>
|
||||
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
|
||||
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div :class="[$style.header, { [$style.mini]: mini }]" @contextmenu.prevent.stop="onContextmenu">
|
||||
<span :class="$style.headerLeft">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</span>
|
||||
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<span :class="$style.headerTitle" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<span class="right">
|
||||
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="button _button" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
|
||||
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="button _button" @click="maximize()"><i class="ti ti-rectangle"></i></button>
|
||||
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="button _button" @click="close()"><i class="ti ti-x"></i></button>
|
||||
<span :class="$style.headerRight">
|
||||
<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
|
||||
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
|
||||
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="canResize">
|
||||
<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
|
||||
<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
|
||||
<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
|
||||
<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
|
||||
<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
|
||||
<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
|
||||
<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
|
||||
<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTop" @mousedown.prevent="onTopHandleMousedown"></div>
|
||||
<div :class="$style.handleRight" @mousedown.prevent="onRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottom" @mousedown.prevent="onBottomHandleMousedown"></div>
|
||||
<div :class="$style.handleLeft" @mousedown.prevent="onLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTopLeft" @mousedown.prevent="onTopLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTopRight" @mousedown.prevent="onTopRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottomRight" @mousedown.prevent="onBottomRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottomLeft" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
|
||||
</template>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -407,166 +414,174 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.window-enter-active, .window-leave-active {
|
||||
<style lang="scss" module>
|
||||
.transition_window_enterActive,
|
||||
.transition_window_leaveActive {
|
||||
transition: opacity 0.2s, transform 0.2s !important;
|
||||
}
|
||||
.window-enter-from, .window-leave-to {
|
||||
.transition_window_enterFrom,
|
||||
.transition_window_leaveTo {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.ebkgocck {
|
||||
.root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
> .body {
|
||||
overflow: clip;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .header {
|
||||
--height: 39px;
|
||||
|
||||
&.mini {
|
||||
--height: 32px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
height: var(--height);
|
||||
background: var(--windowHeader);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
//border-bottom: solid 1px var(--divider);
|
||||
font-size: 95%;
|
||||
font-weight: bold;
|
||||
|
||||
> .left, > .right {
|
||||
> .button {
|
||||
height: var(--height);
|
||||
width: var(--height);
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .left {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> .right {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
line-height: var(--height);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--panel);
|
||||
container-type: inline-size;
|
||||
}
|
||||
}
|
||||
|
||||
> .handle {
|
||||
$size: 8px;
|
||||
|
||||
position: absolute;
|
||||
|
||||
&.top {
|
||||
top: -($size);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $size;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
&.right {
|
||||
top: 0;
|
||||
right: -($size);
|
||||
width: $size;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: -($size);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $size;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 0;
|
||||
left: -($size);
|
||||
width: $size;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
&.top-left {
|
||||
top: -($size);
|
||||
left: -($size);
|
||||
width: $size * 2;
|
||||
height: $size * 2;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
&.top-right {
|
||||
top: -($size);
|
||||
right: -($size);
|
||||
width: $size * 2;
|
||||
height: $size * 2;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
bottom: -($size);
|
||||
right: -($size);
|
||||
width: $size * 2;
|
||||
height: $size * 2;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
&.bottom-left {
|
||||
bottom: -($size);
|
||||
left: -($size);
|
||||
width: $size * 2;
|
||||
height: $size * 2;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
}
|
||||
|
||||
&.maximized {
|
||||
> .body {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: clip;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.header {
|
||||
--height: 39px;
|
||||
|
||||
&.mini {
|
||||
--height: 32px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
height: var(--height);
|
||||
background: var(--windowHeader);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
//border-bottom: solid 1px var(--divider);
|
||||
font-size: 95%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
height: var(--height);
|
||||
width: var(--height);
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
line-height: var(--height);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--panel);
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
$handleSize: 8px;
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.handleTop {
|
||||
composes: handle;
|
||||
top: -($handleSize);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $handleSize;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.handleRight {
|
||||
composes: handle;
|
||||
top: 0;
|
||||
right: -($handleSize);
|
||||
width: $handleSize;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.handleBottom {
|
||||
composes: handle;
|
||||
bottom: -($handleSize);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $handleSize;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.handleLeft {
|
||||
composes: handle;
|
||||
top: 0;
|
||||
left: -($handleSize);
|
||||
width: $handleSize;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.handleTopLeft {
|
||||
composes: handle;
|
||||
top: -($handleSize);
|
||||
left: -($handleSize);
|
||||
width: $handleSize * 2;
|
||||
height: $handleSize * 2;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.handleTopRight {
|
||||
composes: handle;
|
||||
top: -($handleSize);
|
||||
right: -($handleSize);
|
||||
width: $handleSize * 2;
|
||||
height: $handleSize * 2;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.handleBottomRight {
|
||||
composes: handle;
|
||||
bottom: -($handleSize);
|
||||
right: -($handleSize);
|
||||
width: $handleSize * 2;
|
||||
height: $handleSize * 2;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.handleBottomLeft {
|
||||
composes: handle;
|
||||
bottom: -($handleSize);
|
||||
left: -($handleSize);
|
||||
width: $handleSize * 2;
|
||||
height: $handleSize * 2;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,28 +1,10 @@
|
||||
<template>
|
||||
<span class="mk-ellipsis">
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</span>
|
||||
<span :class="$style.root">
|
||||
<span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-ellipsis {
|
||||
> span {
|
||||
animation: ellipsis 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.32s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<style lang="scss" module>
|
||||
@keyframes ellipsis {
|
||||
0%, 80%, 100% {
|
||||
opacity: 1;
|
||||
@@ -31,4 +13,24 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.dot {
|
||||
animation: ellipsis 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.32s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Transition :name="$store.state.animation ? '_transition_zoom' : ''" appear>
|
||||
<div class="mjndxjcg">
|
||||
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
||||
<p><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
|
||||
<MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
||||
<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
|
||||
<MkButton :class="$style.button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -13,24 +13,24 @@ import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mjndxjcg {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.text {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
> .button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
> img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -43,7 +43,7 @@ export default defineComponent({
|
||||
render() {
|
||||
if (this.text == null || this.text === '') return;
|
||||
|
||||
const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS });
|
||||
const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text);
|
||||
|
||||
const validTime = (t: string | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
@@ -87,22 +87,22 @@ export default defineComponent({
|
||||
let style;
|
||||
switch (token.props.name) {
|
||||
case 'tada': {
|
||||
const speed = validTime(token.props.args.speed) || '1s';
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : '');
|
||||
break;
|
||||
}
|
||||
case 'jelly': {
|
||||
const speed = validTime(token.props.args.speed) || '1s';
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
||||
break;
|
||||
}
|
||||
case 'twitch': {
|
||||
const speed = validTime(token.props.args.speed) || '0.5s';
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'shake': {
|
||||
const speed = validTime(token.props.args.speed) || '0.5s';
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
@@ -115,17 +115,17 @@ export default defineComponent({
|
||||
token.props.args.x ? 'mfm-spinX' :
|
||||
token.props.args.y ? 'mfm-spinY' :
|
||||
'mfm-spin';
|
||||
const speed = validTime(token.props.args.speed) || '1.5s';
|
||||
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||
break;
|
||||
}
|
||||
case 'jump': {
|
||||
const speed = validTime(token.props.args.speed) || '0.75s';
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'bounce': {
|
||||
const speed = validTime(token.props.args.speed) || '0.75s';
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
|
||||
break;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ export default defineComponent({
|
||||
}, genEl(token.children));
|
||||
}
|
||||
case 'rainbow': {
|
||||
const speed = validTime(token.props.args.speed) || '1s';
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
|
||||
break;
|
||||
}
|
||||
@@ -181,16 +181,34 @@ export default defineComponent({
|
||||
return h(MkSparkle, {}, genEl(token.children));
|
||||
}
|
||||
case 'rotate': {
|
||||
const degrees = parseInt(token.props.args.deg) || '90';
|
||||
const degrees = parseInt(token.props.args.deg) ?? '90';
|
||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||
break;
|
||||
}
|
||||
case 'position': {
|
||||
const x = parseInt(token.props.args.x ?? '0');
|
||||
const y = parseInt(token.props.args.y ?? '0');
|
||||
style = `transform: translateX(${x}em) translateY(${y}em);`;
|
||||
break;
|
||||
}
|
||||
case 'fg': {
|
||||
let color = token.props.args.color;
|
||||
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
|
||||
style = `color: #${color};`;
|
||||
break;
|
||||
}
|
||||
case 'bg': {
|
||||
let color = token.props.args.color;
|
||||
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
|
||||
style = `background-color: #${color};`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (style == null) {
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
|
||||
} else {
|
||||
return h('span', {
|
||||
style: 'display: inline-block;' + style,
|
||||
style: 'display: inline-block; ' + style,
|
||||
}, genEl(token.children));
|
||||
}
|
||||
}
|
||||
@@ -285,11 +303,11 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
case 'mathInline': {
|
||||
return [h('code', genEl(token.props.formula))];
|
||||
return [h('code', token.props.formula)];
|
||||
}
|
||||
|
||||
case 'mathBlock': {
|
||||
return [h('code', genEl(token.props.formula))];
|
||||
return [h('code', token.props.formula)];
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
|
@@ -6,6 +6,10 @@
|
||||
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
||||
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
|
||||
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
|
||||
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
|
||||
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
|
||||
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
|
||||
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
|
||||
<option value="and">{{ i18n.ts._role._condition.and }}</option>
|
||||
<option value="or">{{ i18n.ts._role._condition.or }}</option>
|
||||
<option value="not">{{ i18n.ts._role._condition.not }}</option>
|
||||
@@ -13,6 +17,9 @@
|
||||
<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
|
||||
<i class="ti ti-menu-2"></i>
|
||||
</button>
|
||||
<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
|
||||
<i class="ti ti-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps">
|
||||
@@ -20,7 +27,7 @@
|
||||
<template #item="{element}">
|
||||
<div :class="$style.item">
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
<RolesEditorFormula :model-value="element" draggable @update:model-value="updated => valuesItemUpdated(updated)"/>
|
||||
<RolesEditorFormula :model-value="element" draggable @update:model-value="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
@@ -34,6 +41,9 @@
|
||||
<MkInput v-else-if="type === 'createdLessThan' || type === 'createdMoreThan'" v-model="v.sec" type="number">
|
||||
<template #suffix>sec</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||
</MkInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -55,6 +65,7 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -81,6 +92,10 @@ const type = computed({
|
||||
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
||||
if (t === 'createdLessThan') v.value.sec = 86400;
|
||||
if (t === 'createdMoreThan') v.value.sec = 86400;
|
||||
if (t === 'followersLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'followersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingMoreThanOrEq') v.value.value = 10;
|
||||
v.value.type = t;
|
||||
},
|
||||
});
|
||||
@@ -93,6 +108,14 @@ function valuesItemUpdated(item) {
|
||||
const i = v.value.values.findIndex(_item => _item.id === item.id);
|
||||
v.value.values[i] = item;
|
||||
}
|
||||
|
||||
function removeItem(item) {
|
||||
v.value.values = v.value.values.filter(_item => _item.id !== item.id);
|
||||
}
|
||||
|
||||
function removeSelf() {
|
||||
emit('remove');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -113,6 +136,10 @@ function valuesItemUpdated(item) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
border: solid 2px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
|
@@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
|
||||
action: lookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user',
|
||||
icon: 'ti ti-user-plus',
|
||||
text: i18n.ts.invite,
|
||||
action: invite,
|
||||
}] : [])],
|
||||
@@ -223,7 +223,7 @@ provideMetadataReceiver((info) => {
|
||||
});
|
||||
|
||||
const invite = () => {
|
||||
os.api('admin/invite').then(x => {
|
||||
os.api('invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code,
|
||||
|
@@ -77,6 +77,32 @@
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ options_canInvite_useDefault ? i18n.ts._role.useBaseValue : (options_canInvite_value ? i18n.ts.yes : i18n.ts.no) }}</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="options_canInvite_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="options_canInvite_value" :disabled="options_canInvite_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ options_canManageCustomEmojis_useDefault ? i18n.ts._role.useBaseValue : (options_canManageCustomEmojis_value ? i18n.ts.yes : i18n.ts.no) }}</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="options_canManageCustomEmojis_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="options_canManageCustomEmojis_value" :disabled="options_canManageCustomEmojis_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ options_driveCapacityMb_useDefault ? i18n.ts._role.useBaseValue : (options_driveCapacityMb_value + 'MB') }}</template>
|
||||
@@ -101,6 +127,31 @@
|
||||
</MkInput>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #suffix>{{ options_wordMuteLimit_useDefault ? i18n.ts._role.useBaseValue : (options_wordMuteLimit_value) }}</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="options_wordMuteLimit_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="options_wordMuteLimit_value" :disabled="options_wordMuteLimit_useDefault" type="number" :readonly="readonly">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #suffix>{{ options_webhookLimit_useDefault ? i18n.ts._role.useBaseValue : (options_webhookLimit_value) }}</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="options_webhookLimit_useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="options_webhookLimit_value" :disabled="options_webhookLimit_useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSlot>
|
||||
|
||||
@@ -160,10 +211,18 @@ let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefau
|
||||
let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? false);
|
||||
let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true);
|
||||
let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? false);
|
||||
let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true);
|
||||
let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? false);
|
||||
let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true);
|
||||
let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? false);
|
||||
let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
|
||||
let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0);
|
||||
let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
|
||||
let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0);
|
||||
let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
|
||||
let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0);
|
||||
let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
|
||||
let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0);
|
||||
|
||||
if (_DEV_) {
|
||||
watch($$(condFormula), () => {
|
||||
@@ -176,8 +235,12 @@ function getOptions() {
|
||||
gtlAvailable: { useDefault: options_gtlAvailable_useDefault, value: options_gtlAvailable_value },
|
||||
ltlAvailable: { useDefault: options_ltlAvailable_useDefault, value: options_ltlAvailable_value },
|
||||
canPublicNote: { useDefault: options_canPublicNote_useDefault, value: options_canPublicNote_value },
|
||||
canInvite: { useDefault: options_canInvite_useDefault, value: options_canInvite_value },
|
||||
canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value },
|
||||
driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
|
||||
antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
|
||||
wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
|
||||
webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -32,6 +32,22 @@
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ options_canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="options_canInvite">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ options_canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="options_canManageCustomEmojis">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ options_driveCapacityMb }}MB</template>
|
||||
@@ -46,6 +62,22 @@
|
||||
<MkInput v-model="options_antennaLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #suffix>{{ options_wordMuteLimit }}</template>
|
||||
<MkInput v-model="options_wordMuteLimit" type="number">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #suffix>{{ options_webhookLimit }}</template>
|
||||
<MkInput v-model="options_webhookLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
@@ -81,8 +113,12 @@ const roles = await os.api('admin/roles/list');
|
||||
let options_gtlAvailable = $ref(instance.baseRole.gtlAvailable);
|
||||
let options_ltlAvailable = $ref(instance.baseRole.ltlAvailable);
|
||||
let options_canPublicNote = $ref(instance.baseRole.canPublicNote);
|
||||
let options_canInvite = $ref(instance.baseRole.canInvite);
|
||||
let options_canManageCustomEmojis = $ref(instance.baseRole.canManageCustomEmojis);
|
||||
let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
|
||||
let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
|
||||
let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
|
||||
let options_webhookLimit = $ref(instance.baseRole.webhookLimit);
|
||||
|
||||
async function updateBaseRole() {
|
||||
await os.apiWithDialog('admin/roles/update-default-role-override', {
|
||||
@@ -90,8 +126,12 @@ async function updateBaseRole() {
|
||||
gtlAvailable: options_gtlAvailable,
|
||||
ltlAvailable: options_ltlAvailable,
|
||||
canPublicNote: options_canPublicNote,
|
||||
canInvite: options_canInvite,
|
||||
canManageCustomEmojis: options_canManageCustomEmojis,
|
||||
driveCapacityMb: options_driveCapacityMb,
|
||||
antennaLimit: options_antennaLimit,
|
||||
wordMuteLimit: options_wordMuteLimit,
|
||||
webhookLimit: options_webhookLimit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
@@ -69,7 +69,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, defineComponent, ref, shallowRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
@@ -10,7 +10,7 @@
|
||||
v-for="(message, i) in messages"
|
||||
:key="message.id"
|
||||
v-anim="i"
|
||||
class="message"
|
||||
class="message _panel"
|
||||
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
|
||||
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:data-index="i"
|
||||
|
@@ -256,9 +256,10 @@ defineExpose({
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
color: var(--fg);
|
||||
background: rgba(12, 18, 16, 0.85);
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@@ -1,51 +1,48 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
class=""
|
||||
class="root"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
<div class="mk-messaging-room">
|
||||
<div class="body">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noMessagesYet }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: messages, fetching: pFetching }">
|
||||
<MkDateSeparatedList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ messages: true, 'deny-move-transition': pFetching }"
|
||||
:items="messages"
|
||||
direction="up"
|
||||
reversed
|
||||
>
|
||||
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="typers.length > 0" class="typers">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
||||
<template #users>
|
||||
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</div>
|
||||
<Transition :name="animation ? 'fade' : ''">
|
||||
<div v-show="showIndicator" class="new-message">
|
||||
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
|
||||
<div class="body">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noMessagesYet }}</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
|
||||
</footer>
|
||||
</template>
|
||||
<template #default="{ items: messages, fetching: pFetching }">
|
||||
<MkDateSeparatedList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ messages: true, 'deny-move-transition': pFetching }"
|
||||
:items="messages"
|
||||
direction="up"
|
||||
reversed
|
||||
>
|
||||
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<footer>
|
||||
<div v-if="typers.length > 0" class="typers">
|
||||
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
||||
<template #users>
|
||||
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</div>
|
||||
<Transition :name="animation ? 'fade' : ''">
|
||||
<div v-show="showIndicator" class="new-message">
|
||||
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
|
||||
</div>
|
||||
</Transition>
|
||||
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -140,7 +137,9 @@ async function fetch() {
|
||||
document.addEventListener('visibilitychange', onVisibilitychange);
|
||||
|
||||
nextTick(() => {
|
||||
thisScrollToBottom();
|
||||
pagingComponent.inited.then(() => {
|
||||
thisScrollToBottom();
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
fetching = false;
|
||||
}, 300);
|
||||
@@ -305,11 +304,12 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-messaging-room {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
.root {
|
||||
display: content;
|
||||
|
||||
> .body {
|
||||
min-height: 80%;
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
@@ -349,8 +349,9 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
bottom: 0;
|
||||
padding-top: 8px;
|
||||
bottom: 0;
|
||||
bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
> .new-message {
|
||||
width: 100%;
|
||||
@@ -395,6 +396,8 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||
max-height: 12em;
|
||||
overflow-y: scroll;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,9 @@
|
||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user.roles.length > 0" class="roles">
|
||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">{{ role.name }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i"/>
|
||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||
@@ -337,6 +340,18 @@ onUnmounted(() => {
|
||||
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
||||
}
|
||||
|
||||
> .roles {
|
||||
padding: 24px 24px 0 154px;
|
||||
font-size: 0.95em;
|
||||
|
||||
> .role {
|
||||
border: solid 1px var(--color, var(--divider));
|
||||
border-radius: 999px;
|
||||
margin-right: 4px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 24px 24px 24px 154px;
|
||||
font-size: 0.95em;
|
||||
@@ -467,6 +482,11 @@ onUnmounted(() => {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> .roles {
|
||||
padding: 16px 16px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
@@ -305,6 +305,9 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/channels',
|
||||
component: page(() => import('./pages/channels.vue')),
|
||||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/registry/keys/system/:path(*)?',
|
||||
component: page(() => import('./pages/registry.keys.vue')),
|
||||
@@ -331,7 +334,7 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/emojis',
|
||||
name: 'emojis',
|
||||
component: page(() => import('./pages/admin/emojis.vue')),
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
|
@@ -1 +1 @@
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
|
||||
|
@@ -10,53 +10,67 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function getScrollPosition(el: Element | null): number {
|
||||
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) {
|
||||
if (!el.parentElement) return top;
|
||||
const data = el.dataset.stickyContainerHeaderHeight;
|
||||
const newTop = data ? Number(data) + top : top;
|
||||
if (el === container) return newTop;
|
||||
return getStickyTop(el.parentElement, container, newTop);
|
||||
}
|
||||
|
||||
export function getScrollPosition(el: HTMLElement | null): number {
|
||||
const container = getScrollContainer(el);
|
||||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
export function isTopVisible(el: Element | null): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
|
||||
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||
// とりあえず評価してみる
|
||||
if (isTopVisible(el)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
|
||||
return scrollTop <= topPosition;
|
||||
}
|
||||
|
||||
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
|
||||
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
|
||||
return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
|
||||
}
|
||||
|
||||
export function onScrollTop(el: Element, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (isTopVisible(el)) {
|
||||
if (isTopVisible(el, tolerance)) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
if (once) removeListener();
|
||||
}
|
||||
};
|
||||
|
||||
function removeListener() { container.removeEventListener('scroll', onScroll); }
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
||||
export function onScrollBottom(el: Element, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||
const container = getScrollContainer(el);
|
||||
|
||||
// とりあえず評価してみる
|
||||
if (isBottomVisible(el, tolerance, container)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
|
||||
const containerOrWindow = container || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
const pos = getScrollPosition(el);
|
||||
if (pos + el.clientHeight > el.scrollHeight - 1) {
|
||||
if (isBottomVisible(el, 1, container)) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
if (once) removeListener();
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
function removeListener() {
|
||||
containerOrWindow.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
||||
export function scroll(el: Element, options: {
|
||||
top?: number;
|
||||
left?: number;
|
||||
behavior?: ScrollBehavior;
|
||||
}) {
|
||||
export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
|
||||
const container = getScrollContainer(el);
|
||||
if (container == null) {
|
||||
window.scroll(options);
|
||||
@@ -65,21 +79,51 @@ export function scroll(el: Element, options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
|
||||
/**
|
||||
* Scroll to Top
|
||||
* @param el Scroll container element
|
||||
* @param options Scroll options
|
||||
*/
|
||||
export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
|
||||
scroll(el, { top: 0, ...options });
|
||||
}
|
||||
|
||||
export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
|
||||
scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
|
||||
/**
|
||||
* Scroll to Bottom
|
||||
* @param el Content element
|
||||
* @param options Scroll options
|
||||
* @param container Scroll container element
|
||||
*/
|
||||
export function scrollToBottom(
|
||||
el: HTMLElement,
|
||||
options: ScrollToOptions = {},
|
||||
container = getScrollContainer(el),
|
||||
) {
|
||||
if (container) {
|
||||
container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options });
|
||||
} else {
|
||||
window.scroll({
|
||||
top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isBottom(el: Element, asobi = 0) {
|
||||
const container = getScrollContainer(el);
|
||||
const current = container
|
||||
? el.scrollTop + el.offsetHeight
|
||||
: window.scrollY + window.innerHeight;
|
||||
const max = container
|
||||
? el.scrollHeight
|
||||
: document.body.offsetHeight;
|
||||
return current >= (max - asobi);
|
||||
export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
return scrollTop <= tolerance;
|
||||
}
|
||||
|
||||
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
|
||||
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
|
||||
return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
|
||||
}
|
||||
|
||||
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
||||
export function getBodyScrollHeight() {
|
||||
return Math.max(
|
||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight
|
||||
);
|
||||
}
|
||||
|
6
packages/frontend/src/types/date-separated-list.ts
Normal file
6
packages/frontend/src/types/date-separated-list.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type MisskeyEntity = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
_shouldInsertAd_?: boolean;
|
||||
[x: string]: any;
|
||||
};
|
@@ -2,6 +2,7 @@ import * as os from '@/os';
|
||||
import { instance } from '@/instance';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export function openInstanceMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
@@ -46,7 +47,28 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
to: '/clicker',
|
||||
text: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}],
|
||||
}, ($i && ($i.isAdmin || $i.role.canInvite) && instance.disableRegistration) ? {
|
||||
text: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
action: () => {
|
||||
os.api('invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
},
|
||||
} : undefined, ($i && ($i.isAdmin || $i.role.canManageCustomEmojis)) ? {
|
||||
type: 'link',
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
} : undefined],
|
||||
}, null, {
|
||||
text: i18n.ts.help,
|
||||
icon: 'ti ti-question-circle',
|
||||
|
@@ -424,6 +424,7 @@ async function deleteProfile() {
|
||||
|
||||
.navButtonIcon {
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navButtonIndicator {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user