Compare commits

...

58 Commits

Author SHA1 Message Date
syuilo
6bf1d7e398 13.0.0-rc.4 2023-01-14 18:50:25 +09:00
syuilo
e46e7f5252 ノートをピン留めできる数を設定可能に
Resolve #9555
2023-01-14 18:04:56 +09:00
syuilo
5952f1ac24 Update roles.editor.vue 2023-01-14 17:49:02 +09:00
syuilo
a08369fe36 enhance(client): 分かりやすいエラーメッセージを表示するように 2023-01-14 17:46:45 +09:00
syuilo
6cb9612943 fix import 2023-01-14 17:40:51 +09:00
syuilo
76c049522e enhance: ユーザーリストおよびユーザーリスト内のユーザーの作成可能数を設定可能に 2023-01-14 17:38:16 +09:00
syuilo
c41879c542 refactor(client): use css modules 2023-01-14 17:23:49 +09:00
syuilo
99bdb11d24 Update CHANGELOG.md 2023-01-14 17:02:49 +09:00
syuilo
c2009acb2d enhance: クリップおよびクリップ内のノートの作成可能数を設定可能に 2023-01-14 16:14:24 +09:00
syuilo
46d2a8726e fix missing import 2023-01-14 16:04:13 +09:00
syuilo
7df3ca7388 enhance(server): add rate limits for some endpoints 2023-01-14 15:59:15 +09:00
syuilo
51b8d4ae3e 13.0.0-rc.3 2023-01-14 15:08:30 +09:00
syuilo
ab1124abba refactor(client): use css modules 2023-01-14 15:05:23 +09:00
syuilo
3db84a2e8f refactor(client): use css modules 2023-01-14 15:02:14 +09:00
syuilo
9a78bbf0f1 refactor(client): use css modules 2023-01-14 15:01:28 +09:00
syuilo
efbec444e8 refactor(client): use css modules 2023-01-14 14:57:48 +09:00
syuilo
2f06f2a6da New Crowdin updates (#9544)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)
2023-01-14 14:20:09 +09:00
tamaina
b8da51e08c fix css module syntax error 2023-01-14 04:52:42 +00:00
syuilo
af6a578fa6 13.0.0-rc.2 2023-01-14 13:49:13 +09:00
tamaina
73d735a1f7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-14 04:48:17 +00:00
tamaina
b8b1899a9f chore: fix ref name (pages/timeline.vue) 2023-01-14 04:47:59 +00:00
syuilo
d52f0617a1 fix(server): ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正
Fix #9550
2023-01-14 13:41:53 +09:00
syuilo
c730973294 多分 fix #9551 2023-01-14 13:36:18 +09:00
syuilo
2c2e064871 refactor(client): use css modules 2023-01-14 13:29:41 +09:00
syuilo
e3c39d4b52 refactor(client): use css modules 2023-01-14 12:45:20 +09:00
syuilo
5da74897ae refactor(client): use css modules 2023-01-14 12:43:54 +09:00
syuilo
4b1009b34e refactor(client): use css modules 2023-01-14 12:30:32 +09:00
syuilo
203a7ad073 refactor(client): use css modules 2023-01-14 12:15:02 +09:00
syuilo
34a7b52105 13.0.0-rc.1 2023-01-14 12:00:07 +09:00
syuilo
30fc166c08 refactor(client): use css modules 2023-01-14 11:59:08 +09:00
syuilo
c84d86b368 refactor(client): use css modules 2023-01-14 11:48:30 +09:00
syuilo
1e5d4db0a1 refactor(client): use css modules 2023-01-14 11:46:22 +09:00
syuilo
5e02f0d325 refactor(client): use css modules 2023-01-14 11:39:35 +09:00
syuilo
ce5506f331 refactor(client): use css modules 2023-01-14 11:23:02 +09:00
syuilo
91105845d8 refactor(client): use css modules 2023-01-14 11:18:12 +09:00
syuilo
2bedc084a3 tweak MkRolePreview 2023-01-14 11:14:14 +09:00
syuilo
027ef1ea4a Update vite.config.ts 2023-01-14 11:10:41 +09:00
syuilo
668aa17eef refactor(client): use css modules 2023-01-14 10:57:34 +09:00
syuilo
ebf8ef22e4 🎨 2023-01-14 10:49:53 +09:00
syuilo
bcb5182e86 Webhookの作成可能数を設定可能に 2023-01-14 10:48:11 +09:00
syuilo
f45059b7b1 fix 2023-01-14 10:46:40 +09:00
syuilo
d0aee58599 13.0.0-beta.43 2023-01-14 09:02:48 +09:00
syuilo
68e65ed5df update mfm-js 2023-01-14 09:02:33 +09:00
syuilo
367ccb9971 enhance(client): add new mfm function (position, fg, bg)
Resolve #9527
2023-01-14 08:52:32 +09:00
syuilo
4151087d3c フォロワー数、フォロー数もConditional roleで利用できるように 2023-01-14 08:27:23 +09:00
syuilo
39c058a4bb Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-14 08:04:41 +09:00
syuilo
d1807ee5dc enhance: ハードワードミュートの最大文字数を設定可能に
Resolve #7574
2023-01-14 08:04:38 +09:00
syuilo
e6a76b31be Update CHANGELOG.md 2023-01-14 06:26:06 +09:00
こけっち
98469117bf fix: follow request list api pagination (#9548)
* fix: follow request list api pagination

* fix: improve alias

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-01-14 06:18:58 +09:00
syuilo
a5becfc042 Update CHANGELOG.md 2023-01-14 06:00:58 +09:00
tamaina
d2204fd5c8 refactor: pagination/date-separated-list系処理を良い感じに? (#8209)
* pages/messaging/messaging-room.vue

* wip

* wip

* wip???

* wip?

* ✌️

* messaaging-room.form.vue rewrite to compositon api

* refactor

* 関心事でないのでとりあえず置いておく

* 🎨

* 🎨

* i18n.ts

* fix scroll container find function

* fix

* FIX

* ✌️

* Fix scroll bottom detect

* wip

* aaaaaaaaaaa

* rename

* fix

* fix?

* ✌️

* ✌️

* clean up

* clena up

* refactor

* scroll event once or not

* fix

* fix once

* add safe-area-inset-bottom to spacer

* fix

* ✌️

* 🎨

* fix

* fix

* wip

* ✌️

* clean up

* fix lint

* Update packages/client/src/components/global/sticky-container.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* Update packages/client/src/components/ui/pagination.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* Update packages/client/src/pages/messaging/messaging-room.form.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* clean up: single line comment

* https://github.com/misskey-dev/misskey/pull/8209#discussion_r867386077

* fix

* asobi → tolerance

* pick form

* pick message

* pick room

* fix lint

* fix scroll?

* fix scroll.ts

* fix directives/sticky-container

* update global/sticky-container.vue

* fix, 🎨

* revert merge

* ✌️

* fix lint errors

* 🎨

* Update packages/client/src/types/date-separated-list.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* https://github.com/misskey-dev/misskey/pull/8209#discussion_r917225080

* use '

* Update packages/client/src/scripts/scroll.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* use Number.EPSILON

Co-authored-by: acid-chicken <root@acid-chicken.com>

* revert

* fix

* fix

* Use % instead of vh

* 🎨

* 🎨

* 🎨

* wip

* wip

* css modules

Co-authored-by: Johann150 <johann.galle@protonmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-01-13 18:25:40 +09:00
syuilo
519a08f8b5 Update CHANGELOG.md 2023-01-13 18:21:47 +09:00
tamaina
303519a1bd enhance: Judge instance block by endsWith (#9263)
* TypeScriptでendsWith

* fix

* SQL?

* バ〜カアホ

* Update packages/backend/src/core/UtilityService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* add comment

* add description

* Update packages/backend/src/core/UtilityService.ts

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* Update packages/backend/src/core/chart/charts/federation.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* remove comment

* fix

* fix?

* add changelog

* ILIKE, ARRAY

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-01-13 18:21:07 +09:00
syuilo
161da24841 fix(server): signup-complete Broken
Fix #9538
2023-01-13 17:59:40 +09:00
syuilo
6e40024660 typo 2023-01-13 17:49:05 +09:00
syuilo
73c78d4c38 fix(client): 数式を含む投稿が表示されないのを修正
Fix #9535
2023-01-13 17:45:20 +09:00
syuilo
2654936c17 fix 2023-01-13 17:42:12 +09:00
syuilo
23810e3e1e Update CHANGELOG.md 2023-01-13 15:29:55 +09:00
99 changed files with 2496 additions and 1716 deletions

View File

@@ -20,7 +20,7 @@ You should also include the user name that made the change.
### Notable features
- ロール機能
- 従来より柔軟にユーザーの権限を管理できます。例えば、「インスタンスのパトロンはアンテナを30個まで作れる」「基本的にLTLは見れないが、許可した人だけ見れる」のような運用はもちろん、「ローカルユーザーかつアカウント作成から1日未満のユーザーはパブリックな投稿を行えない」のように複数条件を組み合わせて、自動でロールを付与する設定も可能です。
- 従来より柔軟にユーザーの権限を管理できます。例えば、「インスタンスのパトロンはアンテナを30個まで作れる」「基本的にLTLは見れないが、許可した人だけ見れる」「招待制インスタンスだけどユーザーなら誰でも他者を招待できる」のような運用はもちろん、「ローカルユーザーかつアカウント作成から1日未満のユーザーはパブリックな投稿を行えない」のように複数条件を組み合わせて、自動でロールを付与する設定も可能です。
- Misskey Play
- 従来の動的なPagesに代わる、新しいプラットフォームです。動的なコンテンツ(アプリケーション)に特化していて、Pagesに比べてはるかに柔軟なアプリケーションを作成可能です。
@@ -33,6 +33,7 @@ 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
- インスタンスブロックはサブドメインにも適用されるようになります
- ロールの導入に伴い、いくつかの機能がロールと統合されました
- モデレーターはロールに統合されました。今までのモデレーター情報は失われるため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
- サイレンスはロールに統合されました。今までのユーザーは恩赦されるため、予めサイレンス一覧を記録しておくのをおすすめします。
@@ -50,7 +51,8 @@ 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以下はサポートされなくなりました
- 109でもContainerQueriesのフラグを有効にする事で問題なく使用できます
#### For app developers
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
@@ -72,13 +74,20 @@ 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
- ユーザーリストおよびユーザーリスト内のユーザーの作成可能数を設定可能に @syuilo
- ハードワードミュートの最大文字数を設定可能に @syuilo
- Webhookの作成可能数を設定可能に @syuilo
- ノートをピン留めできる数を設定可能に @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: add rate limits for some endpoints @syuilo
- Server: improve stats api performance @syuilo
- Server: improve nodeinfo performance @syuilo
- Server: delete outdated notifications regularly to improve db performance @syuilo
@@ -108,6 +117,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
@@ -127,6 +137,8 @@ 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
- Server: ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正 @syuilo
- Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
- Client: case insensitive emoji search @saschanaz

View File

@@ -931,12 +931,20 @@ undefined: "Undefiniert"
assign: "Zuweisen"
unassign: "Entfernen"
color: "Farbe"
manageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
_role:
new: "Rolle erstellen"
edit: "Rolle bearbeiten"
name: "Rollenname"
description: "Rollenbeschreibung"
permission: "Rollenberechtigungen"
descriptionOfPermission: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten."
assignTarget: "Zuweisungsart"
descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditionell</b> bedeutet, dass die Liste der Benutzer einer Rolle durch eine Liste an Konditionen automatisch verwaltet wird."
manual: "Manuell"
conditional: "Konditional"
condition: "Konditionen"
isConditionalRole: "Dies ist eine konditionale Rolle."
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"
@@ -949,8 +957,16 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Einladungscodes für diese Instanz erstellen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität"
antennaMax: "Maximale Anzahl an Antennen"
_condition:
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
and: "UND"
or: "ODER"
not: "NICHT"
_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"

View File

@@ -931,12 +931,20 @@ undefined: "Undefined"
assign: "Assign"
unassign: "Unassign"
color: "Color"
manageCustomEmojis: "Manage Custom Emojis"
_role:
new: "New role"
edit: "Edit role"
name: "Role name"
description: "Role description"
permission: "Role permissions"
descriptionOfPermission: "<b>Moderators</b> can perform basic moderation operations.\n<b>Administrators</b> can change all settings of the instance."
assignTarget: "Assign target"
descriptionOfAssignTarget: "<b>Manual</b> to manually change who is part of this role and who is not.\n<b>Conditional</b> to have users be automatically assigned and removed from this role based on a set of conditions."
manual: "Manual"
conditional: "Conditional"
condition: "Conditions"
isConditionalRole: "This is a conditional role."
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"
@@ -949,8 +957,23 @@ _role:
gtlAvailable: "Viewing the global timeline"
ltlAvailable: "Viewing the local timeline"
canPublicNote: "Can send public notes"
canInvite: "Create instance invite codes"
canManageCustomEmojis: "Manage Custom Emojis"
driveCapacity: "Drive capacity"
antennaMax: "Maximum number of antennas"
wordMuteMax: "Maximum number of characters allowed in the word mute string"
_condition:
isLocal: "Local user"
isRemote: "Remote user"
createdLessThan: "Created less than"
createdMoreThan: "Created more than"
followersLessThanOrEq: "The number of followers is less than or equal to"
followersMoreThanOrEq: "The number of followers is greater than or equal to"
followingLessThanOrEq: "The number of accounts following is less than or equal to"
followingMoreThanOrEq: "The number of accounts following is greater than or equal to"
and: "AND"
or: "OR"
not: "NOT"
_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"

View File

@@ -924,7 +924,51 @@ 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!"
roles: "Ruoli"
role: "Ruolo"
normalUser: "Profilo standard"
undefined: "Indefinito"
assign: "Assegna"
unassign: "Disassegna"
color: "Colore"
manageCustomEmojis: "Gestisci le emoji personalizzate"
_role:
new: "Nuovo ruolo"
edit: "Modifica ruolo"
name: "Nome del ruolo"
description: "Descrizione del ruolo"
permission: "Permessi del ruolo"
descriptionOfPermission: "<b>Moderatori</b> possono svolgere le attività di moderazione basilari.\n<b>Amministratori</b> possono modificare la configurazione dell'istanza."
assignTarget: "Assegna il target"
descriptionOfAssignTarget: "<b>Manuale</b> per assegnare manualmente questo ruolo ai profili.\n<b>Condizionale</b> per assegnare o rimuovere automaticamente questo ruolo ai profili, secondo determinate condizioni."
manual: "Manuale"
conditional: "Condizionale"
condition: "Condizioni"
isConditionalRole: "Questo è un ruolo condizionato"
isPublic: "Ruolo pubblico"
descriptionOfIsPublic: "La lista di profili assegnati a questo ruolo è visibile a chiunque. Inoltre, il ruolo verrà mostrato nei relativi profili."
options: "Opzioni del ruolo"
baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base"
chooseRoleToAssign: "Seleziona il ruolo da assegnare"
canEditMembersByModerator: "Consenti ai Moderatori di modificare i membri di questo ruolo"
descriptionOfCanEditMembersByModerator: "Se attivo, anche i Moderatori potranno assegnare o togliere questo ruolo. Altrimenti, se disattivo, potranno solo gli Amministratori."
_options:
gtlAvailable: "Disponibilità della Timeline Federata"
ltlAvailable: "Disponibilità della Timeline Locale"
canPublicNote: "Può scrivere Note con Visibilità Pubblica"
canInvite: "Genera codici di invito all'istanza"
canManageCustomEmojis: "Gestire le emoji personalizzate"
driveCapacity: "Capienza del Drive"
antennaMax: "Numero massimo di Antenne"
_condition:
isLocal: "Profilo locale"
isRemote: "Profilo remoto"
createdLessThan: "Creato meno di"
createdMoreThan: "Creato più di"
and: "E"
or: "O"
not: "NON"
_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"

View File

@@ -193,7 +193,7 @@ clearQueueConfirmText: "未配達の投稿は配送されなくなります。
clearCachedFiles: "キャッシュをクリア"
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
blockedInstances: "ブロックしたインスタンス"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
@@ -932,6 +932,7 @@ assign: "アサイン"
unassign: "アサインを解除"
color: "色"
manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作成することはできません。"
_role:
new: "ロールの作成"
@@ -961,12 +962,23 @@ _role:
canInvite: "インスタンス招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
webhookMax: "Webhookの作成可能数"
clipMax: "クリップの作成可能数"
noteEachClipsMax: "クリップ内のノートの最大数"
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
_condition:
isLocal: "ローカルユーザー"
isRemote: "リモートユーザー"
createdLessThan: "アカウント作成から~以内"
createdMoreThan: "アカウント作成から~経過"
followersLessThanOrEq: "フォロワー数が~以下"
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
and: "~かつ~"
or: "~または~"
not: "~ではない"

View File

@@ -926,15 +926,25 @@ didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?"
pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!"
roles: "บทบาท"
role: "บทบาท"
normalUser: "ผู้ใช้มาตรฐาน"
undefined: "ไม่ได้กำหนด"
assign: "กำหนด"
unassign: "ยังไม่มอบหมาย"
color: "สี"
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
_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: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
options: "ตัวเลือกบทบาท"
@@ -947,8 +957,18 @@ _role:
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
driveCapacity: "ความจุของไดรฟ์"
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
_condition:
isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล"
createdLessThan: "สร้างน้อยกว่า"
createdMoreThan: "สร้างมากกว่า"
and: "และ"
or: "หรือ"
not: "ไม่"
_sensitiveMediaDetection:
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
sensitivity: "การตรวจจับความไว"

View File

@@ -931,41 +931,50 @@ undefined: "未定义"
assign: "分配"
unassign: "取消分配"
color: "颜色"
manageCustomEmojis: "管理自定义表情符号"
_role:
new: "创建角色"
edit: "编辑角色"
name: "用户组名称"
description: "用户组的描述"
permission: "用户组的权限"
name: "角色名称"
description: "角色描述"
permission: "角色权限"
descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。"
assignTarget: "授权对象"
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个用户组中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
manual: "手动"
conditional: "符合条件"
condition: "条件"
isConditionalRole: "这是一个条件控制的用户组。"
isPublic: "公开用户组"
descriptionOfIsPublic: "任何人都可以看到分配该用户组的用户用户的个人资料也将显示该用户组。"
isConditionalRole: "这是一个条件控制的角色。"
isPublic: "角色公开"
descriptionOfIsPublic: "任何人都可以看到分配该角色的用户。而用户的个人资料也将显示该角色。"
options: "选项"
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色"
canEditMembersByModerator: "允许版主编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,版主和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
_options:
gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖"
canInvite: "发放实例邀请码"
canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量"
antennaMax: "可创建的最大天线数量"
wordMuteMax: "屏蔽词的字数限制"
webhookMax: "Webhook 创建数量限制"
_condition:
isLocal: "是本地用户"
isRemote: "是远程用户"
createdLessThan: "账户创建时间少于"
createdMoreThan: "账户创建时间超过"
and: "全部符合"
or: "任一符合"
not: "不符合"
followersLessThanOrEq: "关注者不多于"
followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于"
and: "符合以下全部条件"
or: "符合以下任一条件"
not: "不符合以下任何条件"
_sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度"

View File

@@ -324,7 +324,7 @@ integration: "整合"
connectService: "己連結"
disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
@@ -388,7 +388,7 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "審核員"
moderator: "監察員"
moderation: "言論調節"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
@@ -869,7 +869,7 @@ recommended: "推薦"
check: "檢查"
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理帳號登入才可以檢視。"
requireAdminForView: "必須以管理帳號登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳號。"
typeToConfirm: "要執行這項操作,請輸入 {x} "
deleteAccount: "刪除帳號"
@@ -931,29 +931,49 @@ undefined: "未定義"
assign: "指派"
unassign: "取消指派"
color: "顏色"
manageCustomEmojis: "管理自訂表情符號"
_role:
new: "建立角色"
edit: "編輯角色"
name: "角色名稱"
description: "角色描述 "
permission: "角色的權限"
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理</b>能變更實例的全部設定。"
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: "允許公開貼文"
canInvite: "發行實例邀請碼"
canManageCustomEmojis: "管理自訂表情符號"
driveCapacity: "雲端硬碟容量"
antennaMax: "可建立的天線數量"
webhookMax: "可建立的Webhook數"
_condition:
isLocal: "本地使用者"
isRemote: "遠端使用者"
createdLessThan: "自建立帳戶開始~以內"
createdMoreThan: "自建立帳戶開始~經過"
followersLessThanOrEq: "追隨者人數在~以下"
followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上"
and: "~和~"
or: "~或~"
not: "~否"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.0.0-beta.42",
"version": "13.0.0-rc.4",
"codename": "indigo",
"repository": {
"type": "git",

View File

@@ -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",

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class NotePiningService {
@@ -30,6 +31,7 @@ export class NotePiningService {
private userEntityService: UserEntityService,
private idService: IdService,
private roleService: RoleService,
private relayService: RelayService,
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
@@ -55,7 +57,7 @@ export class NotePiningService {
const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id });
if (pinings.length >= 5) {
if (pinings.length >= (await this.roleService.getUserRoleOptions(user.id)).pinLimit) {
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
}

View File

@@ -10,6 +10,7 @@ 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 = {
@@ -19,7 +20,14 @@ export type RoleOptions = {
canInvite: boolean;
canManageCustomEmojis: boolean;
driveCapacityMb: number;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
webhookLimit: number;
clipLimit: number;
noteEachClipsLimit: number;
userListLimit: number;
userEachUserListsLimit: number;
};
export const DEFAULT_ROLE: RoleOptions = {
@@ -29,7 +37,14 @@ export const DEFAULT_ROLE: RoleOptions = {
canInvite: false,
canManageCustomEmojis: false,
driveCapacityMb: 100,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
webhookLimit: 3,
clipLimit: 10,
noteEachClipsLimit: 200,
userListLimit: 10,
userEachUserListsLimit: 50,
};
@Injectable()
@@ -67,7 +82,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);
@@ -145,6 +160,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;
}
@@ -186,7 +213,14 @@ export class RoleService implements OnApplicationShutdown {
canInvite: getOptionValues('canInvite').some(x => x === true),
canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true),
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
pinLimit: Math.max(...getOptionValues('pinLimit')),
antennaLimit: Math.max(...getOptionValues('antennaLimit')),
wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
webhookLimit: Math.max(...getOptionValues('webhookLimit')),
clipLimit: Math.max(...getOptionValues('clipLimit')),
noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')),
userListLimit: Math.max(...getOptionValues('userListLimit')),
userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')),
};
}

View File

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

View File

@@ -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),

View File

@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class UserListService {
@@ -23,13 +24,21 @@ export class UserListService {
private userEntityService: UserEntityService,
private idService: IdService,
private userFollowingService: UserFollowingService,
private roleService: RoleService,
private globalEventServie: GlobalEventService,
private proxyAccountService: ProxyAccountService,
) {
}
@bindThis
public async push(target: User, list: UserList) {
public async push(target: User, list: UserList, me: User) {
const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userEachUserListsLimit) {
throw new Error('Too many users');
}
await this.userListJoiningsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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),
} : {}),

View File

@@ -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 {

View File

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

View File

@@ -10,10 +10,10 @@ import { DownloadService } from '@/core/DownloadService.js';
import { UserListService } from '@/core/UserListService.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import type { DbUserImportJobData } from '../types.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ImportUserListsProcessorService {
@@ -102,7 +102,7 @@ export class ImportUserListsProcessorService {
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.push(target, list!);
this.userListService.push(target, list!, user);
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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),

View File

@@ -5,15 +5,15 @@ import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
limit: {
duration: ms('1hour'),
max: 100,
max: 20,
},
requireCredential: true,

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js';
import type { Channel } from '@/models/entities/Channel.js';
@@ -14,6 +15,11 @@ export const meta = {
kind: 'write:channels',
limit: {
duration: ms('1hour'),
max: 10,
},
res: {
type: 'object',
optional: false, nullable: false,

View File

@@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account', 'notes', 'clips'],
@@ -13,6 +15,11 @@ export const meta = {
kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: {
noSuchClip: {
message: 'No such clip.',
@@ -31,6 +38,12 @@ export const meta = {
code: 'ALREADY_CLIPPED',
id: '734806c4-542c-463a-9311-15c512803965',
},
tooManyClipNotes: {
message: 'You cannot add notes to the clip any more.',
code: 'TOO_MANY_CLIP_NOTES',
id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118',
},
},
} as const;
@@ -54,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private clipNotesRepository: ClipNotesRepository,
private idService: IdService,
private roleService: RoleService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -80,6 +94,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.alreadyClipped);
}
const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).noteEachClipsLimit) {
throw new ApiError(meta.errors.tooManyClipNotes);
}
await this.clipNotesRepository.insert({
id: this.idService.genId(),
noteId: note.id,

View File

@@ -4,6 +4,8 @@ import { IdService } from '@/core/IdService.js';
import type { ClipsRepository } from '@/models/index.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['clips'],
@@ -17,6 +19,14 @@ export const meta = {
optional: false, nullable: false,
ref: 'Clip',
},
errors: {
tooManyClips: {
message: 'You cannot create clip any more.',
code: 'TOO_MANY_CLIPS',
id: '920f7c2d-6208-4b76-8082-e632020f5883',
},
},
} as const;
export const paramDef = {
@@ -37,9 +47,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private clipsRepository: ClipsRepository,
private clipEntityService: ClipEntityService,
private roleService: RoleService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).clipLimit) {
throw new ApiError(meta.errors.tooManyClips);
}
const clip = await this.clipsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),

View File

@@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
}
const meta = await this.metaService.fetch();
const instance = await this.metaService.fetch();
try {
// Create file
@@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
requestIp: meta.enableIpLogging ? ip : null,
requestHeaders: meta.enableIpLogging ? headers : null,
requestIp: instance.enableIpLogging ? ip : null,
requestHeaders: instance.enableIpLogging ? headers : null,
});
return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) {
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
throw new ApiError();
} finally {
cleanup!();
cleanup!();
}
});
}

View File

@@ -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,17 @@ 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)
.andWhere('request.followeeId = :meId', { meId: me.id });
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)));
});
}
}

View File

@@ -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(/^\/(.+)\/(.*)$/);

View File

@@ -5,6 +5,8 @@ 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';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['webhooks'],
@@ -12,6 +14,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 +48,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(),

View File

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

View File

@@ -1,12 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { MutingsRepository } from '@/models/index.js';
import type { Muting } from '@/models/entities/Muting.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
@@ -15,6 +16,11 @@ export const meta = {
kind: 'write:mutes',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: {
noSuchUser: {
message: 'No such user.',

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { NoteFavoritesRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -13,6 +14,11 @@ export const meta = {
kind: 'write:favorites',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: {
noSuchNote: {
message: 'No such note.',

View File

@@ -5,6 +5,8 @@ import type { UserList } from '@/models/entities/UserList.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['lists'],
@@ -20,6 +22,14 @@ export const meta = {
optional: false, nullable: false,
ref: 'UserList',
},
errors: {
tooManyUserLists: {
message: 'You cannot create user list any more.',
code: 'TOO_MANY_USERLISTS',
id: '0cf21a28-7715-4f39-a20d-777bfdb8d138',
},
},
} as const;
export const paramDef = {
@@ -39,8 +49,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListEntityService: UserListEntityService,
private idService: IdService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
@@ -15,6 +16,11 @@ export const meta = {
description: 'Add a user to an existing list.',
limit: {
duration: ms('1hour'),
max: 30,
},
errors: {
noSuchList: {
message: 'No such list.',
@@ -105,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Push the user
await this.userListService.push(user, userList);
await this.userListService.push(user, userList, me);
});
}
}

View File

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

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,11 +1,11 @@
<template>
<span class="zjobosdg">
<span>
<span v-text="hh"></span>
<span class="colon" :class="{ showColon }">:</span>
<span :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-text="mm"></span>
<span v-if="showS" class="colon" :class="{ showColon }">:</span>
<span v-if="showS" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showS" v-text="ss"></span>
<span v-if="showMs" class="colon" :class="{ showColon }">:</span>
<span v-if="showMs" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showMs" v-text="ms"></span>
</span>
</template>
@@ -62,16 +62,14 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.zjobosdg {
> .colon {
opacity: 0;
transition: opacity 1s ease;
<style lang="scss" module>
.colon {
opacity: 0;
transition: opacity 1s ease;
&.showColon {
opacity: 1;
transition: opacity 0s;
}
&.showColon {
opacity: 1;
transition: opacity 0s;
}
}
</style>

View File

@@ -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>

View File

@@ -1,8 +1,15 @@
<template>
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<Transition
:name="transitionName"
:enter-active-class="$style['transition_' + transitionName + '_enterActive']"
:leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
:duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog' || type === 'dialog:top', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: transparentBg && (type === 'popup') }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed, [$style.top]: type === 'dialog:top' }]" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot>
</div>
</div>
@@ -280,8 +287,9 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.send-enter-active, .send-leave-active {
<style lang="scss" module>
.transition_send_enterActive,
.transition_send_leaveActive {
> .bg {
transition: opacity 0.3s !important;
}
@@ -291,7 +299,8 @@ defineExpose({
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
}
}
.send-enter-from, .send-leave-to {
.transition_send_enterFrom,
.transition_send_leaveTo {
> .bg {
opacity: 0;
}
@@ -303,7 +312,8 @@ defineExpose({
}
}
.modal-enter-active, .modal-leave-active {
.transition_modal_enterActive,
.transition_modal_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -313,7 +323,8 @@ defineExpose({
transition: opacity 0.2s, transform 0.2s !important;
}
}
.modal-enter-from, .modal-leave-to {
.transition_modal_enterFrom,
.transition_modal_leaveTo {
> .bg {
opacity: 0;
}
@@ -326,7 +337,8 @@ defineExpose({
}
}
.modal-popup-enter-active, .modal-popup-leave-active {
.transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
> .bg {
transition: opacity 0.1s !important;
}
@@ -336,7 +348,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
}
}
.modal-popup-enter-from, .modal-popup-leave-to {
.transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
> .bg {
opacity: 0;
}
@@ -349,7 +362,7 @@ defineExpose({
}
}
.modal-drawer-enter-active {
.transition_modal-drawer_enterActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -358,7 +371,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-leave-active {
.transition_modal-drawer_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -367,7 +380,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-enter-from, .modal-drawer-leave-to {
.transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
> .bg {
opacity: 0;
}
@@ -378,15 +392,7 @@ defineExpose({
}
}
.qzhlnise {
> .bg {
&.transparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
.root {
&.dialog {
> .content {
position: fixed;
@@ -408,12 +414,12 @@ defineExpose({
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
}
> ::v-deep(*) {
&:global > * {
margin: auto;
}
&.top {
> ::v-deep(*) {
&:global > * {
margin-top: 0;
}
}
@@ -445,11 +451,18 @@ defineExpose({
right: 0;
margin: auto;
> ::v-deep(*) {
&:global > * {
margin: auto;
}
}
}
}
.bg {
&.bgTransparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
</style>

View File

@@ -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>

View File

@@ -8,7 +8,7 @@
</template>
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</MkDateSeparatedList>
@@ -97,8 +97,8 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.elsfgstc {
<style lang="scss" module>
.list {
background: var(--panel);
}
</style>

View File

@@ -17,7 +17,7 @@
</template>
</template>
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
<div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
<RouterView :router="router"/>
</div>
</MkWindow>
@@ -133,8 +133,8 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.yrolvcoq {
<style lang="scss" module>
.root {
min-height: 100%;
background: var(--bg);

View File

@@ -1,5 +1,11 @@
<template>
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<Transition
:enter-active-class="$store.state.animation ? $style.transition_fade_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_fade_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_fade_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_fade_leaveTo : ''"
mode="out-in"
>
<MkLoading v-if="fetching"/>
<MkError v-else-if="error" @retry="init()"/>
@@ -14,15 +20,15 @@
</div>
<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">
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
</div>
<slot :items="items"></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">
<slot :items="items" :fetching="fetching || moreFetching"></slot>
<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
@@ -31,15 +37,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 +67,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 +84,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 +160,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 +178,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 +202,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 +270,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,125 +283,113 @@ 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,
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
<style lang="scss" module>
.transition_fade_enterActive,
.transition_fade_leaveActive {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
.transition_fade_enterFrom,
.transition_fade_leaveTo {
opacity: 0;
}
.cxiknjgy {
> .button {
margin-left: auto;
margin-right: auto;
}
.more {
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
</MkModal>
</template>
@@ -26,12 +26,10 @@ const emit = defineEmits<{
let modal = $shallowRef<InstanceType<typeof MkModal>>();
</script>
<style lang="scss" scoped>
.sfhdhdhq {
&.drawer {
border-radius: 24px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
<style lang="scss" module>
.drawer {
border-radius: 24px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
</style>

View File

@@ -1,66 +1,65 @@
<template>
<div
class="gafaadew"
:class="{ modal, _popup: modal }"
:class="[$style.root, { [$style.modal]: modal, _popup: modal }]"
@dragover.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.stop="onDrop"
>
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/>
<header :class="$style.header">
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
</button>
<div class="right">
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="ti ti-world-off"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<div :class="$style.headerRight">
<span :class="[$style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" :class="$style.localOnly"><i class="ti ti-world-off"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button" :class="$style.visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
</button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<button v-click-anime class="submit _button" :class="{ posting }" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div class="inner">
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.previewButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner">
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template>
<i :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
</div>
</button>
</div>
</header>
<div class="form" :class="{ fixed }">
<MkNoteSimple v-if="reply" class="preview" :note="reply"/>
<MkNoteSimple v-if="renote" class="preview" :note="renote"/>
<div v-if="quoteId" class="with-quote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified">
<div :class="[$style.form]">
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
<div v-if="visibility === 'specified'" :class="$style.toSpecified">
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id">
<div :class="$style.visibleUsers">
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
<MkAcct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
<button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
</span>
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" class="attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" :class="[$style.text, { [$style.withCw]: useCw }]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer>
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="ti ti-plug"></i></button>
<XNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<footer :class="$style.footer">
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.footerButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
</footer>
<datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -742,306 +741,275 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.gafaadew {
<style lang="scss" module>
.root {
position: relative;
&.modal {
width: 100%;
max-width: 520px;
}
}
> header {
z-index: 1000;
height: 66px;
.header {
z-index: 1000;
height: 66px;
}
> .cancel {
padding: 0;
font-size: 1em;
width: 64px;
line-height: 66px;
}
.cancel {
padding: 0;
font-size: 1em;
width: 64px;
line-height: 66px;
}
> .account {
height: 100%;
aspect-ratio: 1/1;
display: inline-flex;
vertical-align: bottom;
.account {
height: 100%;
aspect-ratio: 1/1;
display: inline-flex;
vertical-align: bottom;
}
> .avatar {
width: 28px;
height: 28px;
margin: auto;
}
}
.avatar {
width: 28px;
height: 28px;
margin: auto;
}
> .right {
position: absolute;
top: 0;
right: 0;
.headerRight {
position: absolute;
top: 0;
right: 0;
}
> .text-count {
opacity: 0.7;
line-height: 66px;
}
.textCount {
opacity: 0.7;
line-height: 66px;
}
> .visibility {
height: 34px;
width: 34px;
margin: 0 0 0 8px;
.visibility {
height: 34px;
width: 34px;
margin: 0 0 0 8px;
& + .localOnly {
margin-left: 0 !important;
}
}
> .local-only {
margin: 0 0 0 12px;
opacity: 0.7;
}
& + .localOnly {
margin-left: 0 !important;
}
}
> .preview {
display: inline-block;
padding: 0;
margin: 0 8px 0 0;
font-size: 16px;
width: 34px;
height: 34px;
border-radius: 6px;
.localOnly {
margin: 0 0 0 12px;
opacity: 0.7;
}
&:hover {
background: var(--X5);
}
.previewButton {
display: inline-block;
padding: 0;
margin: 0 8px 0 0;
font-size: 16px;
width: 34px;
height: 34px;
border-radius: 6px;
&.active {
color: var(--accent);
}
}
&:hover {
background: var(--X5);
}
> .submit {
margin: 16px 16px 16px 0;
vertical-align: bottom;
&.previewButtonActive {
color: var(--accent);
}
}
&:disabled {
opacity: 0.7;
}
.submit {
margin: 16px 16px 16px 0;
vertical-align: bottom;
&.posting {
cursor: wait;
}
&:disabled {
opacity: 0.7;
}
&:not(:disabled):hover {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
&.posting {
cursor: wait;
}
&:not(:disabled):active {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
> .inner {
padding: 0 12px;
line-height: 34px;
font-weight: bold;
border-radius: 4px;
font-size: 0.9em;
min-width: 90px;
box-sizing: border-box;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
> i {
margin-left: 6px;
}
}
}
&:not(:disabled):hover {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
> .form {
> .preview {
padding: 16px;
&:not(:disabled):active {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
}
> .with-quote {
margin: 0 0 8px 0;
color: var(--accent);
.submitInner {
padding: 0 12px;
line-height: 34px;
font-weight: bold;
border-radius: 4px;
font-size: 0.9em;
min-width: 90px;
box-sizing: border-box;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
> button {
padding: 4px 8px;
color: var(--accentAlpha04);
.form {
}
&:hover {
color: var(--accentAlpha06);
}
.preview {
padding: 16px 20px 0 20px;
}
&:active {
color: var(--accentDarken30);
}
}
}
.targetNote {
padding: 0 20px 16px 20px;
}
> .to-specified {
padding: 6px 24px;
margin-bottom: 8px;
overflow: auto;
white-space: nowrap;
.withQuote {
margin: 0 0 8px 0;
color: var(--accent);
}
> .visibleUsers {
display: inline;
top: -1px;
font-size: 14px;
.toSpecified {
padding: 6px 24px;
margin-bottom: 8px;
overflow: auto;
white-space: nowrap;
}
> button {
padding: 4px;
border-radius: 8px;
}
.visibleUsers {
display: inline;
top: -1px;
font-size: 14px;
}
> span {
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: 8px;
background: var(--X4);
.visibleUser {
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: 8px;
background: var(--X4);
}
> button {
padding: 4px 8px;
}
}
}
}
.hasNotSpecifiedMentions {
margin: 0 20px 16px 20px;
}
> .hasNotSpecifiedMentions {
margin: 0 20px 16px 20px;
}
.cw,
.hashtags,
.text {
display: block;
box-sizing: border-box;
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: inherit;
> .cw,
> .hashtags,
> .text {
display: block;
box-sizing: border-box;
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: inherit;
&:focus {
outline: none;
}
&:focus {
outline: none;
}
&:disabled {
opacity: 0.5;
}
}
&:disabled {
opacity: 0.5;
}
}
.cw {
z-index: 1;
padding-bottom: 8px;
border-bottom: solid 0.5px var(--divider);
}
> .cw {
z-index: 1;
padding-bottom: 8px;
border-bottom: solid 0.5px var(--divider);
}
.hashtags {
z-index: 1;
padding-top: 8px;
padding-bottom: 8px;
border-top: solid 0.5px var(--divider);
}
> .hashtags {
z-index: 1;
padding-top: 8px;
padding-bottom: 8px;
border-top: solid 0.5px var(--divider);
}
.text {
max-width: 100%;
min-width: 100%;
min-height: 90px;
> .text {
max-width: 100%;
min-width: 100%;
min-height: 90px;
&.withCw {
padding-top: 8px;
}
}
&.withCw {
padding-top: 8px;
}
}
.footer {
padding: 0 16px 16px 16px;
}
> footer {
padding: 0 16px 16px 16px;
.footerButton {
display: inline-block;
padding: 0;
margin: 0;
font-size: 1em;
width: 46px;
height: 46px;
border-radius: 6px;
> button {
display: inline-block;
padding: 0;
margin: 0;
font-size: 1em;
width: 46px;
height: 46px;
border-radius: 6px;
&:hover {
background: var(--X5);
}
&:hover {
background: var(--X5);
}
&.active {
color: var(--accent);
}
}
}
&.footerButtonActive {
color: var(--accent);
}
}
@container (max-width: 500px) {
.gafaadew {
> header {
height: 50px;
.header {
height: 50px;
> .cancel {
width: 50px;
> .cancel {
width: 50px;
line-height: 50px;
}
> .headerRight {
> .textCount {
line-height: 50px;
}
> .right {
> .text-count {
line-height: 50px;
}
> .submit {
margin: 8px;
}
> .submit {
margin: 8px;
}
}
}
> .form {
> .to-specified {
padding: 6px 16px;
}
.toSpecified {
padding: 6px 16px;
}
> .cw,
> .hashtags,
> .text {
padding: 0 16px;
}
.cw,
.hashtags,
.text {
padding: 0 16px;
}
> .text {
min-height: 80px;
}
.text {
min-height: 80px;
}
> footer {
padding: 0 8px 8px 8px;
}
}
.footer {
padding: 0 8px 8px 8px;
}
}
@container (max-width: 310px) {
.gafaadew {
> .form {
> footer {
> button {
font-size: 14px;
width: 44px;
height: 44px;
}
}
}
.footerButton {
font-size: 14px;
width: 44px;
height: 44px;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }">
<span :class="[$style.text, { [$style.up]: up }]">
<MkReactionIcon class="icon" :reaction="reaction"/>
</span>
</div>
@@ -43,30 +43,28 @@ onMounted(() => {
position: fixed;
width: 128px;
height: 128px;
}
&:global {
> .text {
display: block;
height: 1em;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform;
.text {
display: block;
height: 1em;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform;
&.up {
opacity: 0;
transform: translateY(-50px) rotateZ(v-bind(angle));
}
}
&.up {
opacity: 0;
transform: translateY(-50px) rotateZ(v-bind(angle));
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb">
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div>
<div :class="$style.root">
<MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/>
<div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
</div>
</MkTooltip>
</template>
@@ -23,20 +23,20 @@ const emit = defineEmits<{
}>();
</script>
<style lang="scss" scoped>
.beeadbfb {
<style lang="scss" module>
.root {
text-align: center;
}
> .icon {
display: block;
width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
margin: 0 auto;
object-fit: contain;
}
.icon {
display: block;
width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
margin: 0 auto;
object-fit: contain;
}
> .name {
font-size: 0.9em;
}
.name {
font-size: 0.9em;
}
</style>

View File

@@ -1,16 +1,16 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey">
<div class="reaction">
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ getReactionName(reaction) }}</div>
<div :class="$style.root">
<div :class="$style.reaction">
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/>
<div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
</div>
<div class="users">
<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.users">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :user="u" :nowrap="true"/>
</div>
<div v-if="users.length > 10" class="omitted">+{{ count - 10 }}</div>
<div v-if="users.length > 10">+{{ count - 10 }}</div>
</div>
</div>
</MkTooltip>
@@ -43,53 +43,53 @@ function getReactionName(reaction: string): string {
}
</script>
<style lang="scss" scoped>
.bqxuuuey {
<style lang="scss" module>
.root {
display: flex;
}
> .reaction {
max-width: 100px;
text-align: center;
.reaction {
max-width: 100px;
text-align: center;
}
> .icon {
display: block;
width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
object-fit: contain;
margin: 0 auto;
}
.reactionIcon {
display: block;
width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
object-fit: contain;
margin: 0 auto;
}
> .name {
font-size: 1em;
}
}
.reactionName {
font-size: 1em;
}
> .users {
flex: 1;
min-width: 0;
font-size: 0.95em;
border-left: solid 0.5px var(--divider);
padding-left: 10px;
margin-left: 10px;
margin-right: 14px;
text-align: left;
.users {
flex: 1;
min-width: 0;
font-size: 0.95em;
border-left: solid 0.5px var(--divider);
padding-left: 10px;
margin-left: 10px;
margin-right: 14px;
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;
}
}
.avatar {
width: 24px;
height: 24px;
margin-right: 3px;
}
</style>

View File

@@ -2,12 +2,12 @@
<button
ref="buttonEl"
v-ripple="canToggle"
class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }"
class="_button"
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle }]"
@click="toggleReaction()"
>
<MkReactionIcon class="icon" :reaction="reaction"/>
<span class="count">{{ count }}</span>
<MkReactionIcon :class="$style.icon" :reaction="reaction"/>
<span :class="$style.count">{{ count }}</span>
</button>
</template>
@@ -92,8 +92,8 @@ useTooltip(buttonEl, async (showing) => {
}, 100);
</script>
<style lang="scss" scoped>
.hkzvhatu {
<style lang="scss" module>
.root {
display: inline-block;
height: 32px;
margin: 2px;
@@ -127,11 +127,11 @@ useTooltip(buttonEl, async (showing) => {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
}
}
}
> .count {
font-size: 0.9em;
line-height: 32px;
margin: 0 0 0 4px;
}
.count {
font-size: 0.9em;
line-height: 32px;
margin: 0 0 0 4px;
}
</style>

View File

@@ -1,5 +1,12 @@
<template>
<TransitionGroup :name="$store.state.animation ? 'x' : ''" tag="div" class="tdflqwzn" :class="{ isMe }">
<TransitionGroup
:enter-active-class="$store.state.animation ? $style.transition_x_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_x_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_x_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_x_leaveTo : ''"
:move-class="$store.state.animation ? $style.transition_x_move : ''"
tag="div" :class="$style.root"
>
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
</TransitionGroup>
</template>
@@ -19,29 +26,26 @@ const initialReactions = new Set(Object.keys(props.note.reactions));
const isMe = computed(() => $i && $i.id === props.note.userId);
</script>
<style lang="scss" scoped>
.x-move, .x-enter-active, .x-leave-active {
<style lang="scss" module>
.transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
}
.x-enter-from, .x-leave-to {
.transition_x_enterFrom,
.transition_x_leaveTo {
opacity: 0;
transform: scale(0.7);
}
.x-leave-active {
position: absolute;
.transition_x_leaveActive {
position: absolute;
}
.tdflqwzn {
.root {
margin: 4px -2px 0 -2px;
&:empty {
display: none;
}
&.isMe {
> span {
cursor: default !important;
}
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="jmgmzlwq"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
<div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
</template>
<script lang="ts" setup>
@@ -10,18 +10,18 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.jmgmzlwq {
<style lang="scss" module>
.root {
font-size: 0.8em;
padding: 16px;
background: var(--infoWarnBg);
color: var(--infoWarnFg);
border-radius: var(--radius);
overflow: clip;
}
> .link {
margin-left: 4px;
color: var(--accent);
}
.link {
margin-left: 4px;
color: var(--accent);
}
</style>

View File

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

View File

@@ -1,6 +1,6 @@
<template>
<span class="mk-sparkle">
<span ref="el">
<span :class="$style.root">
<span ref="el" style="display: inline-block;">
<slot></slot>
</span>
<!-- なぜか path に対する key が機能しないため
@@ -32,7 +32,7 @@
</path>
</svg>
-->
<svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg">
<svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: -32px; left: -32px;">
<path
style="transform-origin: center; transform-box: fill-box;"
:transform="`translate(${particle.x} ${particle.y})`"
@@ -111,20 +111,10 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.mk-sparkle {
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
> span {
display: inline-block;
}
> svg {
position: absolute;
top: -32px;
left: -32px;
pointer-events: none;
}
pointer-events: none;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<div class="wrmlmaau" :class="{ collapsed }">
<div class="body">
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div :class="$style.body">
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files.length > 0">
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
@@ -15,8 +15,8 @@
<summary>{{ i18n.ts.poll }}</summary>
<MkPoll :note="note"/>
</details>
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
<span>{{ i18n.ts.showMore }}</span>
<button v-if="collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
</button>
</div>
</template>
@@ -39,23 +39,10 @@ const collapsed = $ref(
));
</script>
<style lang="scss" scoped>
.wrmlmaau {
<style lang="scss" module>
.root {
overflow-wrap: break-word;
> .body {
> .reply {
margin-right: 6px;
color: var(--accent);
}
> .rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
}
&.collapsed {
position: relative;
max-height: 9em;
@@ -70,7 +57,7 @@ const collapsed = $ref(
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
> span {
> .fadeLabel {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
@@ -80,11 +67,26 @@ const collapsed = $ref(
}
&:hover {
> span {
> .fadeLabel {
background: var(--panelHighlight);
}
}
}
}
}
.body {
}
.reply {
margin-right: 6px;
color: var(--accent);
}
.rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;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>

View File

@@ -1,5 +1,5 @@
<template>
<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div>
</template>
<script lang="ts" setup>
@@ -21,24 +21,24 @@ const text = $computed(() => {
});
</script>
<style lang="scss" scoped>
.fzgwjkgc {
<style lang="scss" module>
.root {
box-shadow: 0 0 0 3px var(--panel);
border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
&.online {
&.status_online {
background: #58d4c9;
}
&.active {
&.status_active {
background: #e4bc48;
}
&.offline {
&.status_offline {
background: #ea5353;
}
&.unknown {
&.status_unknown {
background: #888;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,16 +1,16 @@
<template>
<div v-if="chosen" class="qiivuoyo">
<div v-if="!showMenu" class="main" :class="chosen.place">
<a :href="chosen.url" target="_blank">
<img :src="chosen.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="ti ti-info-circle info-circle"></span></button>
<div v-if="chosen" :class="$style.root">
<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
</a>
</div>
<div v-else class="menu">
<div class="body">
<div v-else :class="$style.menu">
<div :class="$style.menuContainer">
<div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
<MkButton v-if="chosen.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
</div>
</div>
@@ -92,95 +92,99 @@ function reduceFrequency(): void {
}
</script>
<style lang="scss" scoped>
.qiivuoyo {
<style lang="scss" module>
.root {
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
}
> .main {
text-align: center;
.main {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
&.form_square {
> .link,
> .link > .img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
> .menu {
&.form_horizontal {
padding: 8px;
text-align: center;
> .body {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
> .link,
> .link > .img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
> .button {
margin: 8px auto;
}
&.form_horizontal-big {
padding: 8px;
> .link,
> .link > .img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.form_vertical {
> .link,
> .link > .img {
max-width: min(100px, 100%);
}
}
}
.link {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> .img {
filter: contrast(120%);
}
}
}
.img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
.i {
position: absolute;
top: 1px;
right: 1px;
display: grid;
place-content: center;
background: var(--panel);
border-radius: 100%;
padding: 2px;
}
.iIcon {
font-size: 14px;
line-height: 17px;
}
.menu {
padding: 8px;
text-align: center;
}
.menuContainer {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
}
.menuButton {
margin: 8px auto;
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<template>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
</template>
<script lang="ts" setup>
@@ -157,8 +157,8 @@ const props = withDefaults(defineProps<{
}
</style>
<style lang="scss" scoped>
.havbbuyv {
<style lang="scss" module>
.root {
white-space: pre-wrap;
&.nowrap {
@@ -167,24 +167,5 @@ const props = withDefaults(defineProps<{
overflow: hidden;
text-overflow: ellipsis;
}
::v-deep(.quote) {
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
}
::v-deep(pre) {
font-size: 0.8em;
}
> ::v-deep(code) {
font-size: 0.8em;
word-break: break-all;
padding: 4px 6px;
}
}
</style>

View File

@@ -1,36 +1,36 @@
<template>
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
<div v-if="narrow" class="buttons left">
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/>
<div v-if="show" ref="el" :class="[$style.root, { [$style.slim]: narrow, [$style.thin]: thin_ }]" :style="{ background: bg }" @click="onClick">
<div v-if="narrow" :class="$style.buttonsLeft">
<MkAvatar v-if="props.displayMyAvatar && $i" :class="$style.avatar" :user="$i" :disable-preview="true"/>
</div>
<template v-if="metadata">
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i>
<div v-if="!hideTitle" :class="$style.titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" :class="$style.titleAvatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
<div class="title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" class="subtitle">
<div :class="$style.title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
<div v-else-if="metadata.title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" :class="$style.subtitle">
{{ metadata.subtitle }}
</div>
<div v-if="narrow && hasTabs" class="subtitle activeTab">
<div v-if="narrow && hasTabs" :class="[$style.subtitle, $style.activeTab]">
{{ tabs.find(tab => tab.key === props.tab)?.title }}
<i class="chevron ti ti-chevron-down"></i>
<i class="ti ti-chevron-down" :class="$style.chevron"></i>
</div>
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
<div v-if="!narrow || hideTitle" :class="$style.tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="_button" :class="[$style.tab, { [$style.active]: tab.key != null && tab.key === props.tab }]" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" :class="[$style.tabIcon, tab.icon]"></i>
<span v-if="!tab.iconOnly" :class="$style.tabTitle">{{ tab.title }}</span>
</button>
<div ref="tabHighlightEl" class="highlight"></div>
<div ref="tabHighlightEl" :class="$style.tabHighlight"></div>
</div>
</template>
<div class="buttons right">
<div :class="$style.buttonsRight">
<template v-for="action in actions">
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>
@@ -178,8 +178,8 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.fdidabkb {
<style lang="scss" module>
.root {
--height: 50px;
display: flex;
width: 100%;
@@ -215,154 +215,156 @@ onUnmounted(() => {
}
}
}
}
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
min-width: var(--height);
height: var(--height);
margin: 0 var(--margin);
.buttons {
--margin: 8px;
display: flex;
align-items: center;
min-width: var(--height);
height: var(--height);
margin: 0 var(--margin);
&.left {
margin-right: auto;
&:empty {
width: var(--height);
}
}
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
}
.buttonsLeft {
composes: buttons;
margin-right: auto;
}
&.right {
margin-left: auto;
}
.buttonsRight {
composes: buttons;
margin-left: auto;
}
&:empty {
width: var(--height);
}
.avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
.button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
&:hover {
background: rgba(0, 0, 0, 0.05);
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
width: 16px;
text-align: center;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
&.highlighted {
color: var(--accent);
}
}
> .tabs {
position: relative;
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
.fullButton {
& + .fullButton {
margin-left: 12px;
}
}
> .tab {
.titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
}
.titleAvatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
.titleIcon {
margin-right: 8px;
width: 16px;
text-align: center;
}
.title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
}
.subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
> .icon + .title {
margin-left: 8px;
}
}
> .highlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
margin-left: 6px;
}
}
}
.tabs {
position: relative;
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
}
.tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
}
.tabIcon + .tabTitle {
margin-left: 8px;
}
.tabHighlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
}
</style>

View File

@@ -12,6 +12,15 @@ import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { MFM_TAGS } from '@/scripts/mfm-tags';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default defineComponent({
props: {
text: {
@@ -43,7 +52,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 +96,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 +124,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 +179,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 +190,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));
}
}
@@ -258,11 +285,11 @@ export default defineComponent({
case 'quote': {
if (!this.nowrap) {
return [h('div', {
class: 'quote',
style: QUOTE_STYLE,
}, genEl(token.children))];
} else {
return [h('span', {
class: 'quote',
style: QUOTE_STYLE,
}, genEl(token.children))];
}
}
@@ -285,11 +312,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': {

View File

@@ -4,6 +4,7 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
import { i18n } from './i18n';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
import { MenuItem } from '@/types/menu';
@@ -17,9 +18,16 @@ export const apiWithDialog = ((
) => {
const promise = api(endpoint, data, token);
promiseDialog(promise, null, (err) => {
let title = null;
let text = err.message + '\n' + (err as any).id;
if (err.code.startsWith('TOO_MANY')) {
title = i18n.ts.youCannotCreateAnymore;
text = `${i18n.ts.error}: ${err.id}`;
}
alert({
type: 'error',
text: err.message + '\n' + (err as any).id,
title,
text,
});
});

View File

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

View File

@@ -116,6 +116,18 @@
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ options_pinLimit_useDefault ? i18n.ts._role.useBaseValue : (options_pinLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_pinLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_pinLimit_value" :disabled="options_pinLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template>
@@ -127,6 +139,79 @@
</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>
<MkFolder>
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
<template #suffix>{{ options_clipLimit_useDefault ? i18n.ts._role.useBaseValue : (options_clipLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_clipLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_clipLimit_value" :disabled="options_clipLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<template #suffix>{{ options_noteEachClipsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_noteEachClipsLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_noteEachClipsLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_noteEachClipsLimit_value" :disabled="options_noteEachClipsLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
<template #suffix>{{ options_userListLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userListLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_userListLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_userListLimit_value" :disabled="options_userListLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<template #suffix>{{ options_userEachUserListsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userEachUserListsLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_userEachUserListsLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_userEachUserListsLimit_value" :disabled="options_userEachUserListsLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
</div>
</FormSlot>
@@ -192,8 +277,22 @@ let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCust
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_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true);
let options_pinLimit_value = $ref(role?.options?.pinLimit?.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);
let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true);
let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? 0);
let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true);
let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? 0);
let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true);
let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? 0);
let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true);
let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? 0);
if (_DEV_) {
watch($$(condFormula), () => {
@@ -209,7 +308,14 @@ function getOptions() {
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 },
pinLimit: { useDefault: options_pinLimit_useDefault, value: options_pinLimit_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 },
clipLimit: { useDefault: options_clipLimit_useDefault, value: options_clipLimit_value },
noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
};
}

View File

@@ -56,12 +56,63 @@
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ options_pinLimit }}</template>
<MkInput v-model="options_pinLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<template #suffix>{{ options_antennaLimit }}</template>
<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>
<MkFolder>
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
<template #suffix>{{ options_clipLimit }}</template>
<MkInput v-model="options_clipLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<template #suffix>{{ options_noteEachClipsLimit }}</template>
<MkInput v-model="options_noteEachClipsLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
<template #suffix>{{ options_userListLimit }}</template>
<MkInput v-model="options_userListLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<template #suffix>{{ options_userEachUserListsLimit }}</template>
<MkInput v-model="options_userEachUserListsLimit" type="number">
</MkInput>
</MkFolder>
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
@@ -100,7 +151,14 @@ 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_pinLimit = $ref(instance.baseRole.pinLimit);
let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
let options_webhookLimit = $ref(instance.baseRole.webhookLimit);
let options_clipLimit = $ref(instance.baseRole.clipLimit);
let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit);
let options_userListLimit = $ref(instance.baseRole.userListLimit);
let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit);
async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-role-override', {
@@ -111,7 +169,14 @@ async function updateBaseRole() {
canInvite: options_canInvite,
canManageCustomEmojis: options_canManageCustomEmojis,
driveCapacityMb: options_driveCapacityMb,
pinLimit: options_pinLimit,
antennaLimit: options_antennaLimit,
wordMuteLimit: options_wordMuteLimit,
webhookLimit: options_webhookLimit,
clipLimit: options_clipLimit,
noteEachClipsLimit: options_noteEachClipsLimit,
userListLimit: options_userListLimit,
userEachUserListsLimit: options_userEachUserListsLimit,
},
});
}

View File

@@ -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"

View File

@@ -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 {

View File

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

View File

@@ -9,7 +9,8 @@
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<XTimeline
ref="tl" :key="src"
ref="tlComponent"
:key="src"
:src="src"
:sound="true"
@queue="queueUpdated"

View File

@@ -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'];

View File

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

View File

@@ -0,0 +1,6 @@
export type MisskeyEntity = {
id: string;
createdAt: string;
_shouldInsertAd_?: boolean;
[x: string]: any;
};

View File

@@ -65,6 +65,3 @@ defineExpose({
});
*/
</script>
<style lang="scss" scoped>
</style>

View File

@@ -53,6 +53,3 @@ const menu = [{
action: setList,
}];
</script>
<style lang="scss" scoped>
</style>

View File

@@ -8,12 +8,12 @@
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<div v-if="disabled" class="iwaalbte">
<p>
<div v-if="disabled" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-minus-circle"></i>
{{ $t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
<p :class="$style.disabledDescription">{{ $t('disabled-timeline.description') }}</p>
</div>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/>
</XColumn>
@@ -80,16 +80,16 @@ const menu = [{
}];
</script>
<style lang="scss" scoped>
.iwaalbte {
<style lang="scss" module>
.disabled {
text-align: center;
}
> p {
margin: 16px;
.disabledTitle {
margin: 16px;
}
&.desc {
font-size: 14px;
}
}
.disabledDescription {
font-size: 90%;
}
</style>

View File

@@ -2,8 +2,8 @@
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
<div class="wtdtxvec">
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
<div :class="$style.root">
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
<XWidgets :edit="edit" :widgets="column.widgets ?? []" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
</div>
</XColumn>
@@ -54,16 +54,16 @@ const menu = [{
}];
</script>
<style lang="scss" scoped>
.wtdtxvec {
<style lang="scss" module>
.root {
--margin: 8px;
--panelBorder: none;
padding: 0 var(--margin);
}
> .intro {
padding: 16px;
text-align: center;
}
.intro {
padding: 16px;
text-align: center;
}
</style>

View File

@@ -37,12 +37,11 @@
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent } from 'vue';
import XHeader from './header.vue';
import { host, instanceName } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { ColdDeviceStorage } from '@/store';
import { mainRouter } from '@/router';
@@ -52,7 +51,6 @@ const DESKTOP_THRESHOLD = 1100;
export default defineComponent({
components: {
XHeader,
MkPagination,
MkButton,
},

View File

@@ -1,3 +1,4 @@
import path from 'path';
import pluginVue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
@@ -62,7 +63,8 @@ export default defineConfig(({ command, mode }) => {
if (process.env.NODE_ENV === 'production') {
return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4);
} else {
return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4) + '-' + name;
//return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4) + '-' + name;
return (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
}
},
},

View File

@@ -4225,7 +4225,7 @@ __metadata:
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
@@ -8077,7 +8077,7 @@ __metadata:
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
@@ -11566,12 +11566,12 @@ __metadata:
languageName: node
linkType: hard
"mfm-js@npm:0.23.1":
version: 0.23.1
resolution: "mfm-js@npm:0.23.1"
"mfm-js@npm:0.23.3":
version: 0.23.3
resolution: "mfm-js@npm:0.23.3"
dependencies:
twemoji-parser: 14.0.0
checksum: 1c489ee30db7b4a2abd979f5e6453edb0c2e5ebb45e303cf4275215bb606620cf7df4b22442dfc560f6d2724986ed03153a301684a19a5decd72e8f8b5b3b81b
checksum: 7079f80a53a9afc8599333f3256fb18a6bf7c01102a2f8f2be657843726a34835e2af34e26bc5b27e45b217fb2f120c0d3006e9fab2a972c845e9f7361e3cc1b
languageName: node
linkType: hard