Compare commits

...

40 commits

Author SHA1 Message Date
syuilo
c165749a29 chore(frontend): fix type errors 2025-07-06 20:54:02 +09:00
syuilo
c4fdf5a47c chore(frontend): fix type errors 2025-07-06 20:47:31 +09:00
syuilo
288f0abeac chore(frontend): fix type errors 2025-07-06 20:37:09 +09:00
かっこかり
89ed8be8ff
fix(frontend): MkRange/MkSelectでdisabledが効かなくなっている問題を修正 (#16263)
* fix(frontend): MkRange/MkSelectでdisabledが効かなくなっている問題を修正

* Update Changelog

* 誤字
2025-07-06 19:38:09 +09:00
かっこかり
a8abb03d17
refactor(frontend): Formまわりの型強化 (#16260)
* refactor(frontend): Formまわりの型強化

* fix

* avoid non-null assertion and add null check for safety

* refactor

* avoid non-null assertion and add null check for safety

* Update clip.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-07-06 19:36:11 +09:00
syuilo
c2a01551a7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-07-06 19:32:33 +09:00
syuilo
553ccff77c chore(frontend): tweak selector to improve rendering performance 2025-07-06 19:32:31 +09:00
かっこかり
9dddc84750
refactor(frontend): menuの型定義の可読性向上 (#16261) 2025-07-06 17:24:34 +09:00
syuilo
004cfd5e4b clean up 2025-07-06 15:57:21 +09:00
syuilo
40a35968f0 clean up 2025-07-06 15:54:33 +09:00
syuilo
e6ec15e397 feat: 特定のドライブファイルを添付しているチャットメッセージを一覧できるように 2025-07-06 09:54:49 +09:00
syuilo
8430256f22 clean up 2025-07-05 19:29:18 +09:00
Souma
abde15979b
enhance(backend): Add display name to email (#16256)
* feat(backend): Add display name to email

Make it clear who sent emails.

* docs(changelog): Add a description about this change

Users can notice what's changed by this PR.
2025-07-05 18:22:08 +09:00
syuilo
f128682200 fix type errors 2025-07-05 17:13:29 +09:00
syuilo
cc4cdd1ec0 clean up 2025-07-05 12:13:08 +09:00
syuilo
075df75afa Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-07-05 11:56:22 +09:00
syuilo
64eb338d65 🎨 2025-07-05 11:56:20 +09:00
tamaina
d986da745b
ノートのサーバー情報(InstanceTicker)のデザイン/パフォーマンス改善(-webkit-text-stroke ver) (#16225)
* Revert "enhance(frontend): Instance Tickerのデザイン改善 (#15946)"

This reverts commit 04928ba7d1.

* enhance(frontend): Instance Tickerのデザイン改善(-webkit-text-stroke)

* 🎨

* use theme fg/bg

* use panel
2025-07-05 09:59:48 +09:00
syuilo
50f5b29290
New Crowdin updates (#16237)
* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Portuguese)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

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

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

* New translations ja-jp.yml (Indonesian)
2025-07-05 09:07:25 +09:00
syuilo
a460bb7913 perf(frontend): improve rendering performance 2025-07-05 09:05:47 +09:00
syuilo
7cf1eccd04 clean up 2025-07-05 08:31:20 +09:00
syuilo
73397e1b7e Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-07-05 08:18:56 +09:00
tamaina
bf17092b41
test: VS Code上で複数のjestテストを表示できるように (#16251) 2025-07-05 08:18:15 +09:00
github-actions[bot]
6d1018f42b Bump version to 2025.7.0-beta.0 2025-07-04 09:52:01 +00:00
かっこかり
7667011266
fix(frontend): ウェルカムタイムラインのメディア表示がCWを考慮していない問題を修正 (#16247)
* fix(frontend): ウェルカムタイムラインのメディア表示がCWを考慮していない問題を修正

* Update Changelog
2025-07-04 18:49:21 +09:00
syuilo
a45ccc18b4 refactor 2025-07-04 18:33:41 +09:00
syuilo
c29a4d9503 fix(test): Play検索機能でBackend Unit Testが壊れている 2025-07-04 18:31:34 +09:00
syuilo
5caf2b27cf fix(test): Play検索機能でBackend Unit Testが壊れている
Fix #16248
2025-07-04 16:32:56 +09:00
syuilo
dd87d26bdc feat: Playを検索できるように
#13115
2025-07-04 10:20:00 +09:00
かっこかり
b7a6301c2e
fix(frontend): プラグインのアンインストール時にローカルのセーブデータを削除するように (#16246)
* fix(frontend): プラグインのアンインストール時にローカルのセーブデータを削除するように

* Update Changelog

* remove unused import

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-07-04 07:40:00 +09:00
syuilo
73e8d950df enhance(frontend): 投稿フォームにファイルをペースト/ドロップした際のUXを改善
Resolve #16205
2025-07-03 19:11:46 +09:00
syuilo
45033974f7
Update CHANGELOG.md 2025-07-03 19:03:34 +09:00
github-actions[bot]
7acfbc23d6 Bump version to 2025.7.0-alpha.0 2025-07-03 09:57:13 +00:00
github-actions[bot]
a9a746edce Bump version to 2025.6.4-alpha.4 2025-07-03 09:55:33 +00:00
かっこかり
179d990c39
fix(frontend): タブが不可視なあいだのpaginationのアップデートを停止するように (#16243)
* fix(frontend): タブが不可視なあいだのpaginationのアップデートを停止するように

* fix lint

* 待たない
2025-07-03 18:52:16 +09:00
4ster1sk
7c44881ca8
enhance(backend): avatarUrlの上限文字数の引き上げ (#16235) 2025-07-03 18:03:02 +09:00
tamaina
ccbc4cffaa
enhance(frontend): 共有ページで、titleとtextに同じ内容が入っていた際の削除ロジックを強化 (#16226)
* enhance(frontend): 共有ページで、titleとtextに同じ内容が入っていた際の削除ロジックを強化
Fix #16224

* fix

* +→*

* fix

* use RegExp.test

* Update packages/frontend/src/pages/share.vue

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-07-03 18:00:43 +09:00
tamaina
706244925d
fix(frontend): 条件により保存できない場合のメッセージを汎用的なものへ (#16238)
Fix #16228
2025-07-03 17:59:55 +09:00
かっこかり
09a5e4b10a
fix(frontend): Paginatorの型エラー解消 (#16230)
* fix(frontend): fix paginator type error

* fix

* refactor

* fix

* fix

* fix(paginator): remove readonly type

* fix

* typo

* fix: R -> E

* remove any

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-07-03 11:20:26 +09:00
syuilo
c48acad04b 🎨 2025-06-29 17:21:43 +09:00
128 changed files with 1485 additions and 878 deletions

View file

@ -6,8 +6,12 @@
"files.associations": { "files.associations": {
"*.test.ts": "typescript" "*.test.ts": "typescript"
}, },
"jest.jestCommandLine": "pnpm run jest",
"jest.runMode": "on-demand", "jest.runMode": "on-demand",
"jest.virtualFolders": [
{ "name": "backend unit", "jestCommandLine": "pnpm -F backend run test" },
{ "name": "backend e2e", "jestCommandLine": "pnpm -F backend run test:e2e"},
{ "name": "misskey-js", "jestCommandLine": "pnpm -F misskey-js run jest" }
],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": "explicit"
}, },

View file

@ -1,20 +1,29 @@
## 2025.6.4 ## 2025.7.0
### General ### General
- Feat: ノートの下書き機能 - Feat: ノートの下書き機能
- Feat: クリップ内でノートを検索できるように - Feat: クリップ内でノートを検索できるように
- Feat: Playを検索できるように
- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように
### Client ### Client
- Feat: モデログを検索できるように - Feat: モデログを検索できるように
- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように - Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように
- Enhance: ファイルアップロード前にキャプション設定を行えるように - Enhance: ファイルアップロード前にキャプション設定を行えるように
- Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように - Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように
- Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように - Enhance: ページネーション(一覧表示)の並び順を逆にできるように
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように - Enhance: ページネーション(一覧表示)の基準日時を指定できるように
- Enhance: レンダリングパフォーマンスの向上
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正 - Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
- Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正
- Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正
- Fix: 非ログイン時のハイライトートの画像がCWの有無を考慮せず表示される問題を修正
- Fix: レンジ選択・ドロップダウンにて、操作を無効にすべきところで無効にならない問題を修正
### Server ### Server
- Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に - Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に
- Enhance: メールの送信者としてサーバー名を表示するように (サーバー名が設定されている場合)
- Fix: ジョブキューのProgressの値を正しく計算する - Fix: ジョブキューのProgressの値を正しく計算する

View file

@ -3182,7 +3182,7 @@ drafts: "Esborrany "
_drafts: _drafts:
select: "Seleccionar esborrany" select: "Seleccionar esborrany"
cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear." cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear."
cannotCreateDraftOfRenote: "No es poden crear esborranys de remotes." cannotCreateDraft: "Amb aquest contingut no es poden crear esborranys."
delete: "Esborrar esborranys" delete: "Esborrar esborranys"
deleteAreYouSure: "Vols esborrar els esborranys?" deleteAreYouSure: "Vols esborrar els esborranys?"
noDrafts: "No hi ha esborranys" noDrafts: "No hi ha esborranys"

View file

@ -3182,7 +3182,6 @@ drafts: "Drafts"
_drafts: _drafts:
select: "Select Draft" select: "Select Draft"
cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded." cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded."
cannotCreateDraftOfRenote: "You cannot create a draft of a renote."
delete: "Delete Draft" delete: "Delete Draft"
deleteAreYouSure: "Delete draft?" deleteAreYouSure: "Delete draft?"
noDrafts: "No drafts" noDrafts: "No drafts"

View file

@ -3182,7 +3182,7 @@ drafts: "Borrador"
_drafts: _drafts:
select: "Seleccionar borradores" select: "Seleccionar borradores"
cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear." cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear."
cannotCreateDraftOfRenote: "No se pueden crear borradores de renotas." cannotCreateDraft: "No se pueden crear borradores con este contenido."
delete: "Eliminar borrador" delete: "Eliminar borrador"
deleteAreYouSure: "¿Quieres borrar el borrador?" deleteAreYouSure: "¿Quieres borrar el borrador?"
noDrafts: "No hay borradores disponibles." noDrafts: "No hay borradores disponibles."

View file

@ -5,9 +5,13 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>." poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
monthAndDay: "{day} {month}" monthAndDay: "{day} {month}"
search: "Penelusuran" search: "Penelusuran"
reset: "Reset"
notifications: "Notifikasi" notifications: "Notifikasi"
username: "Nama Pengguna" username: "Nama Pengguna"
password: "Kata sandi" password: "Kata sandi"
initialPasswordForSetup: "Kata sandi untuk memulai konfigurasi awal"
initialPasswordIsIncorrect: "Kata sandi untuk memulai konfigurasi awal salah."
initialPasswordForSetupDescription: "Jika Anda menginstal Misskey sendiri, gunakan kata sandi yang Anda masukkan di berkas konfigurasi.\nJika Anda menggunakan layanan hosting Misskey, gunakan kata sandi yang diberikan.\nJika Anda belum mengatur kata sandi, biarkan kosong dan lanjutkan."
forgotPassword: "Lupa Kata Sandi" forgotPassword: "Lupa Kata Sandi"
fetchingAsApObject: "Mengambil data dari Fediverse..." fetchingAsApObject: "Mengambil data dari Fediverse..."
ok: "OK" ok: "OK"
@ -45,6 +49,7 @@ pin: "Sematkan ke profil"
unpin: "Lepas sematan dari profil" unpin: "Lepas sematan dari profil"
copyContent: "Salin konten" copyContent: "Salin konten"
copyLink: "Salin tautan" copyLink: "Salin tautan"
copyRemoteLink: "Salin tautan jarak jauh"
copyLinkRenote: "Salin tautan renote" copyLinkRenote: "Salin tautan renote"
delete: "Hapus" delete: "Hapus"
deleteAndEdit: "Hapus dan sunting" deleteAndEdit: "Hapus dan sunting"
@ -212,8 +217,10 @@ perDay: "per Hari"
stopActivityDelivery: "Berhenti mengirim aktivitas" stopActivityDelivery: "Berhenti mengirim aktivitas"
blockThisInstance: "Blokir instansi ini" blockThisInstance: "Blokir instansi ini"
silenceThisInstance: "Senyapkan instansi ini" silenceThisInstance: "Senyapkan instansi ini"
mediaSilenceThisInstance: "Server media senyap"
operations: "Tindakan" operations: "Tindakan"
software: "Perangkat lunak" software: "Perangkat lunak"
softwareName: "Nama Perangkat Lunak"
version: "Versi" version: "Versi"
metadata: "Metadata" metadata: "Metadata"
withNFiles: "{n} berkas" withNFiles: "{n} berkas"
@ -1040,7 +1047,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi
disableFederationOk: "Matikan federasi" disableFederationOk: "Matikan federasi"
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar." invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
emailNotSupported: "Instansi ini tidak mendukung mengirim surel" emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
postToTheChannel: "Catat ke kanal" postToTheChannel: "Buat Catatan ke Kanal"
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi." cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
reactionAcceptance: "Penerimaan reaksi" reactionAcceptance: "Penerimaan reaksi"
likeOnly: "Hanya suka" likeOnly: "Hanya suka"
@ -2400,7 +2407,7 @@ _deck:
main: "Utama" main: "Utama"
widgets: "Widget" widgets: "Widget"
notifications: "Notifikasi" notifications: "Notifikasi"
tl: "Lini masa" tl: "Beranda"
antenna: "Antena" antenna: "Antena"
list: "Daftar" list: "Daftar"
channel: "Kanal" channel: "Kanal"

8
locales/index.d.ts vendored
View file

@ -10890,6 +10890,10 @@ export interface Locale extends ILocale {
* *
*/ */
"attachedNotes": string; "attachedNotes": string;
/**
*
*/
"usage": string;
/** /**
* *
*/ */
@ -12270,9 +12274,9 @@ export interface Locale extends ILocale {
*/ */
"cannotCreateDraftAnymore": string; "cannotCreateDraftAnymore": string;
/** /**
* *
*/ */
"cannotCreateDraftOfRenote": string; "cannotCreateDraft": string;
/** /**
* *
*/ */

View file

@ -2885,6 +2885,7 @@ _fileViewer:
url: "URL" url: "URL"
uploadedAt: "追加日" uploadedAt: "追加日"
attachedNotes: "添付されているノート" attachedNotes: "添付されているノート"
usage: "利用"
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
_externalResourceInstaller: _externalResourceInstaller:
@ -3288,7 +3289,7 @@ drafts: "下書き"
_drafts: _drafts:
select: "下書きを選択" select: "下書きを選択"
cannotCreateDraftAnymore: "下書きの作成可能数を超えています。" cannotCreateDraftAnymore: "下書きの作成可能数を超えています。"
cannotCreateDraftOfRenote: "リノートの下書きは作成できません。" cannotCreateDraft: "この内容では下書きを作成できません。"
delete: "下書きを削除" delete: "下書きを削除"
deleteAreYouSure: "下書きを削除しますか?" deleteAreYouSure: "下書きを削除しますか?"
noDrafts: "下書きはありません" noDrafts: "下書きはありません"

View file

@ -3182,7 +3182,6 @@ drafts: "초안"
_drafts: _drafts:
select: "초안 선택" select: "초안 선택"
cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다." cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다."
cannotCreateDraftOfRenote: "리노트 초안은 작성할 수 없습니다."
delete: "초안 삭제\n" delete: "초안 삭제\n"
deleteAreYouSure: "초안을 삭제하시겠습니까?" deleteAreYouSure: "초안을 삭제하시겠습니까?"
noDrafts: "초안 없음\n" noDrafts: "초안 없음\n"

View file

@ -3182,6 +3182,5 @@ drafts: "Rascunhos"
_drafts: _drafts:
select: "Selecionar Rascunho" select: "Selecionar Rascunho"
cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido." cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido."
cannotCreateDraftOfRenote: "Você não pode criar o rascunho de uma repostagem."
delete: "Excluir Rascunho" delete: "Excluir Rascunho"
restore: "Redefinir" restore: "Redefinir"

View file

@ -3182,7 +3182,7 @@ drafts: "草稿"
_drafts: _drafts:
select: "选择草稿" select: "选择草稿"
cannotCreateDraftAnymore: "已超过可创建的草稿数量。" cannotCreateDraftAnymore: "已超过可创建的草稿数量。"
cannotCreateDraftOfRenote: "无法创建转帖草稿。" cannotCreateDraft: "此内容无法创建草稿。"
delete: "删除草稿" delete: "删除草稿"
deleteAreYouSure: "要删除草稿吗?" deleteAreYouSure: "要删除草稿吗?"
noDrafts: "没有草稿" noDrafts: "没有草稿"

View file

@ -3182,7 +3182,7 @@ drafts: "草稿\n"
_drafts: _drafts:
select: "選擇草槁" select: "選擇草槁"
cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n" cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n"
cannotCreateDraftOfRenote: "無法建立轉發的草稿。\n" cannotCreateDraft: "無法以此內容建立草稿。\n"
delete: "刪除草稿" delete: "刪除草稿"
deleteAreYouSure: "確定要刪除草稿嗎?\n" deleteAreYouSure: "確定要刪除草稿嗎?\n"
noDrafts: "沒有草稿。\n" noDrafts: "沒有草稿。\n"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.6.4-alpha.3", "version": "2025.7.0-beta.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FixAvatarUrl1750729939704 {
name = 'FixAvatarUrl1750729939704'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "avatarUrl" TYPE character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "avatarUrl" TYPE character varying(512)`);
}
}

View file

@ -145,7 +145,10 @@ export class EmailService {
try { try {
// TODO: htmlサニタイズ // TODO: htmlサニタイズ
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: this.meta.email!, from: this.meta.name ? {
name: this.meta.name,
address: this.meta.email!,
} : this.meta.email!,
to: to, to: to,
subject: subject, subject: subject,
text: text, text: text,

View file

@ -4,8 +4,11 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { type FlashsRepository } from '@/models/_.js'; import { type FlashLikesRepository, MiUser, type FlashsRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
/** /**
* MisskeyPlay関係のService * MisskeyPlay関係のService
@ -15,6 +18,11 @@ export class FlashService {
constructor( constructor(
@Inject(DI.flashsRepository) @Inject(DI.flashsRepository)
private flashRepository: FlashsRepository, private flashRepository: FlashsRepository,
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
private queryService: QueryService,
) { ) {
} }
@ -37,4 +45,43 @@ export class FlashService {
return await builder.getMany(); return await builder.getMany();
} }
public async myLikes(meId: MiUser['id'], opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number, search?: string | null }) {
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate)
.andWhere('like.userId = :meId', { meId })
.leftJoinAndSelect('like.flash', 'flash');
if (opts.search != null) {
for (const word of opts.search.trim().split(' ')) {
query.andWhere(new Brackets(qb => {
qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
}));
}
}
const likes = await query
.limit(opts.limit)
.getMany();
return likes;
}
public async search(searchQuery: string, opts: { sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number, limit?: number }) {
const query = this.queryService.makePaginationQuery(this.flashRepository.createQueryBuilder('flash'), opts.sinceId, opts.untilId, opts.sinceDate, opts.untilDate)
.andWhere('flash.visibility = \'public\'');
for (const word of searchQuery.trim().split(' ')) {
query.andWhere(new Brackets(qb => {
qb.orWhere('flash.title ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
qb.orWhere('flash.summary ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
}));
}
const result = await query
.limit(opts.limit)
.getMany();
return result;
}
} }

View file

@ -120,7 +120,7 @@ export class MiUser {
// avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
@Column('varchar', { @Column('varchar', {
length: 512, nullable: true, length: 1024, nullable: true,
}) })
public avatarUrl: string | null; public avatarUrl: string | null;

View file

@ -51,7 +51,7 @@ export const packedFlashSchema = {
}, },
likedCount: { likedCount: {
type: 'number', type: 'number',
optional: false, nullable: true, optional: false, nullable: false,
}, },
isLiked: { isLiked: {
type: 'boolean', type: 'boolean',

View file

@ -168,6 +168,7 @@ export * as 'clips/update' from './endpoints/clips/update.js';
export * as 'drive' from './endpoints/drive.js'; export * as 'drive' from './endpoints/drive.js';
export * as 'drive/files' from './endpoints/drive/files.js'; export * as 'drive/files' from './endpoints/drive/files.js';
export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js';
export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
export * as 'drive/files/create' from './endpoints/drive/files/create.js'; export * as 'drive/files/create' from './endpoints/drive/files/create.js';
export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
@ -208,6 +209,7 @@ export * as 'flash/my-likes' from './endpoints/flash/my-likes.js';
export * as 'flash/show' from './endpoints/flash/show.js'; export * as 'flash/show' from './endpoints/flash/show.js';
export * as 'flash/unlike' from './endpoints/flash/unlike.js'; export * as 'flash/unlike' from './endpoints/flash/unlike.js';
export * as 'flash/update' from './endpoints/flash/update.js'; export * as 'flash/update' from './endpoints/flash/update.js';
export * as 'flash/search' from './endpoints/flash/search.js';
export * as 'following/create' from './endpoints/following/create.js'; export * as 'following/create' from './endpoints/following/create.js';
export * as 'following/delete' from './endpoints/following/delete.js'; export * as 'following/delete' from './endpoints/following/delete.js';
export * as 'following/invalidate' from './endpoints/following/invalidate.js'; export * as 'following/invalidate' from './endpoints/following/invalidate.js';

View file

@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.search != null) { if (ps.search != null) {
for (const word of ps.search!.trim().split(' ')) { for (const word of ps.search.trim().split(' ')) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` }); qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });

View file

@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive', 'chat'],
requireCredential: true,
kind: 'read:drive',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'ChatMessage',
},
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '485ce26d-f5d2-4313-9783-e689d131eafb',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository,
private chatEntityService: ChatEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
userId: await this.roleService.isModerator(me) ? undefined : me.id,
});
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
query.andWhere('message.fileId = :fileId', { fileId: file.id });
const messages = await query.limit(ps.limit).getMany();
return await this.chatEntityService.packMessagesDetailed(messages, me);
});
}
}

View file

@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FlashLikesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js'; import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = { export const meta = {
tags: ['account', 'flash'], tags: ['account', 'flash'],
@ -46,6 +45,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' }, sinceDate: { type: 'integer' },
untilDate: { type: 'integer' }, untilDate: { type: 'integer' },
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
}, },
required: [], required: [],
} as const; } as const;
@ -53,20 +53,18 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
private flashLikeEntityService: FlashLikeEntityService, private flashLikeEntityService: FlashLikeEntityService,
private queryService: QueryService, private flashService: FlashService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) const likes = await this.flashService.myLikes(me.id, {
.andWhere('like.userId = :meId', { meId: me.id }) sinceId: ps.sinceId,
.leftJoinAndSelect('like.flash', 'flash'); untilId: ps.untilId,
sinceDate: ps.sinceDate,
const likes = await query untilDate: ps.untilDate,
.limit(ps.limit) limit: ps.limit,
.getMany(); search: ps.search,
});
return this.flashLikeEntityService.packMany(likes, me); return this.flashLikeEntityService.packMany(likes, me);
}); });

View file

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
requireCredential: false,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Flash',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
query: { type: 'string', minLength: 1, maxLength: 100 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
},
required: ['query'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.flashService.search(ps.query, {
sinceId: ps.sinceId,
untilId: ps.untilId,
sinceDate: ps.sinceDate,
untilDate: ps.untilDate,
limit: ps.limit,
});
return await this.flashEntityService.packMany(result, me);
});
}
}

View file

@ -7,9 +7,10 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { FlashService } from '@/core/FlashService.js'; import { FlashService } from '@/core/FlashService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { FlashLikesRepository, FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
describe('FlashService', () => { describe('FlashService', () => {
let app: TestingModule; let app: TestingModule;
@ -18,6 +19,7 @@ describe('FlashService', () => {
// -------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------
let flashsRepository: FlashsRepository; let flashsRepository: FlashsRepository;
let flashLikesRepository: FlashLikesRepository;
let usersRepository: UsersRepository; let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository; let userProfilesRepository: UserProfilesRepository;
let idService: IdService; let idService: IdService;
@ -65,6 +67,7 @@ describe('FlashService', () => {
app = await Test.createTestingModule({ app = await Test.createTestingModule({
imports: [ imports: [
GlobalModule, GlobalModule,
CoreModule,
], ],
providers: [ providers: [
FlashService, FlashService,
@ -75,6 +78,7 @@ describe('FlashService', () => {
service = app.get(FlashService); service = app.get(FlashService);
flashsRepository = app.get(DI.flashsRepository); flashsRepository = app.get(DI.flashsRepository);
flashLikesRepository = app.get(DI.flashLikesRepository);
usersRepository = app.get(DI.usersRepository); usersRepository = app.get(DI.usersRepository);
userProfilesRepository = app.get(DI.userProfilesRepository); userProfilesRepository = app.get(DI.userProfilesRepository);
idService = app.get(IdService); idService = app.get(IdService);
@ -88,6 +92,7 @@ describe('FlashService', () => {
await usersRepository.createQueryBuilder().delete().execute(); await usersRepository.createQueryBuilder().delete().execute();
await userProfilesRepository.createQueryBuilder().delete().execute(); await userProfilesRepository.createQueryBuilder().delete().execute();
await flashsRepository.createQueryBuilder().delete().execute(); await flashsRepository.createQueryBuilder().delete().execute();
await flashLikesRepository.createQueryBuilder().delete().execute();
}); });
afterAll(async () => { afterAll(async () => {

View file

@ -21,73 +21,73 @@ describe('ReactionService', () => {
}); });
describe('normalize', () => { describe('normalize', () => {
test('絵文字リアクションはそのまま', async () => { test('絵文字リアクションはそのまま', () => {
assert.strictEqual(await reactionService.normalize('👍'), '👍'); assert.strictEqual(reactionService.normalize('👍'), '👍');
assert.strictEqual(await reactionService.normalize('🍅'), '🍅'); assert.strictEqual(reactionService.normalize('🍅'), '🍅');
}); });
test('既存のリアクションは絵文字化する pudding', async () => { test('既存のリアクションは絵文字化する pudding', () => {
assert.strictEqual(await reactionService.normalize('pudding'), '🍮'); assert.strictEqual(reactionService.normalize('pudding'), '🍮');
}); });
test('既存のリアクションは絵文字化する like', async () => { test('既存のリアクションは絵文字化する like', () => {
assert.strictEqual(await reactionService.normalize('like'), '👍'); assert.strictEqual(reactionService.normalize('like'), '👍');
}); });
test('既存のリアクションは絵文字化する love', async () => { test('既存のリアクションは絵文字化する love', () => {
assert.strictEqual(await reactionService.normalize('love'), '❤'); assert.strictEqual(reactionService.normalize('love'), '❤');
}); });
test('既存のリアクションは絵文字化する laugh', async () => { test('既存のリアクションは絵文字化する laugh', () => {
assert.strictEqual(await reactionService.normalize('laugh'), '😆'); assert.strictEqual(reactionService.normalize('laugh'), '😆');
}); });
test('既存のリアクションは絵文字化する hmm', async () => { test('既存のリアクションは絵文字化する hmm', () => {
assert.strictEqual(await reactionService.normalize('hmm'), '🤔'); assert.strictEqual(reactionService.normalize('hmm'), '🤔');
}); });
test('既存のリアクションは絵文字化する surprise', async () => { test('既存のリアクションは絵文字化する surprise', () => {
assert.strictEqual(await reactionService.normalize('surprise'), '😮'); assert.strictEqual(reactionService.normalize('surprise'), '😮');
}); });
test('既存のリアクションは絵文字化する congrats', async () => { test('既存のリアクションは絵文字化する congrats', () => {
assert.strictEqual(await reactionService.normalize('congrats'), '🎉'); assert.strictEqual(reactionService.normalize('congrats'), '🎉');
}); });
test('既存のリアクションは絵文字化する angry', async () => { test('既存のリアクションは絵文字化する angry', () => {
assert.strictEqual(await reactionService.normalize('angry'), '💢'); assert.strictEqual(reactionService.normalize('angry'), '💢');
}); });
test('既存のリアクションは絵文字化する confused', async () => { test('既存のリアクションは絵文字化する confused', () => {
assert.strictEqual(await reactionService.normalize('confused'), '😥'); assert.strictEqual(reactionService.normalize('confused'), '😥');
}); });
test('既存のリアクションは絵文字化する rip', async () => { test('既存のリアクションは絵文字化する rip', () => {
assert.strictEqual(await reactionService.normalize('rip'), '😇'); assert.strictEqual(reactionService.normalize('rip'), '😇');
}); });
test('既存のリアクションは絵文字化する star', async () => { test('既存のリアクションは絵文字化する star', () => {
assert.strictEqual(await reactionService.normalize('star'), '⭐'); assert.strictEqual(reactionService.normalize('star'), '⭐');
}); });
test('異体字セレクタ除去', async () => { test('異体字セレクタ除去', () => {
assert.strictEqual(await reactionService.normalize('㊗️'), '㊗'); assert.strictEqual(reactionService.normalize('㊗️'), '㊗');
}); });
test('異体字セレクタ除去 必要なし', async () => { test('異体字セレクタ除去 必要なし', () => {
assert.strictEqual(await reactionService.normalize('㊗'), '㊗'); assert.strictEqual(reactionService.normalize('㊗'), '㊗');
}); });
test('fallback - null', async () => { test('fallback - null', () => {
assert.strictEqual(await reactionService.normalize(null), '❤'); assert.strictEqual(reactionService.normalize(null), '❤');
}); });
test('fallback - empty', async () => { test('fallback - empty', () => {
assert.strictEqual(await reactionService.normalize(''), '❤'); assert.strictEqual(reactionService.normalize(''), '❤');
}); });
test('fallback - unknown', async () => { test('fallback - unknown', () => {
assert.strictEqual(await reactionService.normalize('unknown'), '❤'); assert.strictEqual(reactionService.normalize('unknown'), '❤');
}); });
}); });

View file

@ -14,15 +14,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Paginator } from '@/utility/paginator.js'; import * as Misskey from 'misskey-js';
import type { IPaginator } from '@/utility/paginator.js';
import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
paginator: Paginator; paginator: IPaginator;
noGap?: boolean; noGap?: boolean;
extractor?: (item: any) => any; extractor?: (item: any) => Misskey.entities.Channel;
}>(), { }>(), {
extractor: (item) => item, extractor: (item) => item,
}); });

View file

@ -1,7 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MkDateSeparatedList from './MkDateSeparatedList.vue';
void MkDateSeparatedList;

View file

@ -1,254 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) -->
<script lang="ts">
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js';
import { getDateText } from '@/utility/timeline-date-separate.js';
export default defineComponent({
props: {
items: {
type: Array,
required: true,
},
direction: {
type: String,
required: false,
default: 'down',
},
reversed: {
type: Boolean,
required: false,
default: false,
},
noGap: {
type: Boolean,
required: false,
default: false,
},
ad: {
type: Boolean,
required: false,
default: false,
},
},
setup(props, { slots, expose }) {
const $style = useCssModule(); // 使
if (props.items.length === 0) return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default) return;
const el = slots.default({
item: item,
})[0];
if (el.key == null && item.id) el.key = item.id;
const date = new Date(item.createdAt);
const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
if (
i !== props.items.length - 1 &&
nextDate != null && (
date.getFullYear() !== nextDate.getFullYear() ||
date.getMonth() !== nextDate.getMonth() ||
date.getDate() !== nextDate.getDate()
)
) {
const separator = h('div', {
class: $style['separator'],
key: item.id + ':separator',
}, h('p', {
class: $style['date'],
}, [
h('span', {
class: $style['date-1'],
}, [
h('i', {
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
}),
getDateText(date),
]),
h('span', {
class: $style['date-2'],
}, [
getDateText(nextDate),
h('i', {
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
}),
]),
]));
return [el, separator];
} else {
if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
return [h('div', {
key: item.id + ':ad',
class: $style['ad-wrapper'],
}, [h(MkAd, {
prefer: ['horizontal', 'horizontal-big'],
})]), el];
} else {
return el;
}
}
});
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap((node) => node ?? []);
const keys = new Set(nodes.map((node) => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
os.toast(instances.reduce((a, c) => `${a} at ${c.type.name}`, `[DEBUG_6864 (${id})]: ${nodes.length - keys.size} duplicated keys found`));
console.warn({ id, debugId: 6864, stack: instances });
}
}
return children;
};
function onBeforeLeave(el: Element) {
if (!(el instanceof HTMLElement)) return;
el.style.top = `${el.offsetTop}px`;
el.style.left = `${el.offsetLeft}px`;
}
function onLeaveCancelled(el: Element) {
if (!(el instanceof HTMLElement)) return;
el.style.top = '';
el.style.left = '';
}
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const classes = {
[$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',
};
return () => prefer.s.animation ? h(TransitionGroup, {
class: classes,
name: 'list',
tag: 'div',
onBeforeLeave,
onLeaveCancelled,
}, { default: renderChildren }) : h('div', {
class: classes,
}, { default: renderChildren });
},
});
</script>
<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);
}
> .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);
}
> *:empty {
display: none;
}
}
&:not(.date-separated-list-nogap) > *:not(:last-child) {
margin-bottom: var(--MI-margin);
}
}
.date-separated-list-nogap {
> * {
margin: 0 !important;
border: none;
border-radius: 0;
box-shadow: none;
&:not(:last-child) {
border-bottom: solid 0.5px var(--MI_THEME-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(--MI_THEME-dateLabelFg);
}
.date-1 {
margin-right: 8px;
}
.date-1-icon {
margin-right: 8px;
}
.date-2 {
margin-left: 8px;
}
.date-2-icon {
margin-left: 8px;
}
.ad-wrapper {
padding: 8px;
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
}
</style>

View file

@ -684,13 +684,8 @@ defineExpose({
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
> .group { > .group {
&:not(.index) { &:not(.index) {
padding: 4px 0 8px 0; padding: 4px 0 8px 0;

View file

@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import tinycolor from 'tinycolor2';
import { instanceName as localInstanceName } from '@@/js/config.js'; import { instanceName as localInstanceName } from '@@/js/config.js';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { instance as localInstance } from '@/instance.js'; import { instance as localInstance } from '@/instance.js';
@ -44,33 +43,10 @@ const faviconUrl = computed(() => {
return getProxiedImageUrlNullable(imageSrc); return getProxiedImageUrlNullable(imageSrc);
}); });
type ITickerColors = {
readonly bg: string;
readonly fg: string;
};
const TICKER_YUV_THRESHOLD = 191 as const;
const TICKER_FG_COLOR_LIGHT = '#ffffff' as const;
const TICKER_FG_COLOR_DARK = '#2f2f2fcc' as const;
function getTickerColors(bgHex: string): ITickerColors {
const tinycolorInstance = tinycolor(bgHex);
const { r, g, b } = tinycolorInstance.toRgb();
const yuv = 0.299 * r + 0.587 * g + 0.114 * b;
const fgHex = yuv > TICKER_YUV_THRESHOLD ? TICKER_FG_COLOR_DARK : TICKER_FG_COLOR_LIGHT;
return {
fg: fgHex,
bg: bgHex,
} as const satisfies ITickerColors;
}
const themeColorStyle = computed<CSSProperties>(() => { const themeColorStyle = computed<CSSProperties>(() => {
const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777'; const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
const colors = getTickerColors(themeColor);
return { return {
background: `linear-gradient(90deg, ${colors.bg}, ${colors.bg}00)`, background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
color: colors.fg,
}; };
}); });
</script> </script>
@ -84,6 +60,7 @@ $height: 2ex;
height: $height; height: $height;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
overflow: clip; overflow: clip;
color: #fff;
// text-shadow使 // text-shadow使
@ -106,5 +83,10 @@ $height: 2ex;
font-weight: bold; font-weight: bold;
white-space: nowrap; white-space: nowrap;
overflow: visible; overflow: visible;
// text-shadow使
color: var(--MI_THEME-fg);
-webkit-text-stroke: var(--MI_THEME-panel) .225em;
paint-order: stroke fill;
} }
</style> </style>

View file

@ -729,7 +729,7 @@ function emitUpdReaction(emoji: string, delta: number) {
} }
&:hover > .article > .main > .footer > .footerButton { &:hover > .article > .main > .footer > .footerButton {
opacity: 1; color: var(--MI_THEME-fg);
} }
&.showActionsOnlyHover { &.showActionsOnlyHover {
@ -1004,7 +1004,7 @@ function emitUpdReaction(emoji: string, delta: number) {
.footerButton { .footerButton {
margin: 0; margin: 0;
padding: 8px; padding: 8px;
opacity: 0.7; color: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 70%); // opacity
&:not(:last-child) { &:not(:last-child) {
margin-right: 28px; margin-right: 28px;
@ -1018,7 +1018,6 @@ function emitUpdReaction(emoji: string, delta: number) {
.footerButtonCount { .footerButtonCount {
display: inline; display: inline;
margin: 0 0 0 8px; margin: 0 0 0 8px;
opacity: 0.7;
} }
@container (max-width: 580px) { @container (max-width: 580px) {

View file

@ -31,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination> </MkPagination>
</template> </template>
<script lang="ts" setup generic="T extends Paginator"> <script lang="ts" setup generic="T extends IPaginator<Misskey.entities.Note>">
import type { Paginator } from '@/utility/paginator.js'; import * as Misskey from 'misskey-js';
import type { IPaginator } from '@/utility/paginator.js';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-else key="_root_" class="_gaps"> <div v-else key="_root_" class="_gaps">
<slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot> <slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<div v-if="paginator.order.value === 'oldest'"> <div v-if="paginator.order.value === 'oldest'">
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()"> <MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
{{ i18n.ts.loadMore }} {{ i18n.ts.loadMore }}
@ -44,11 +44,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</component> </component>
</template> </template>
<script lang="ts" setup generic="T extends Paginator, I = UnwrapRef<T['items']>"> <script lang="ts" setup generic="T extends IPaginator">
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { onMounted, watch } from 'vue'; import { onMounted, watch, unref } from 'vue';
import type { UnwrapRef } from 'vue'; import type { UnwrapRef } from 'vue';
import type { Paginator } from '@/utility/paginator.js'; import type { IPaginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
@ -95,7 +95,7 @@ if (props.paginator.computedParams) {
defineSlots<{ defineSlots<{
empty: () => void; empty: () => void;
default: (props: { items: I }) => void; default: (props: { items: UnwrapRef<T['items']> }) => void;
}>(); }>();
</script> </script>

View file

@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts" setup generic="T extends Paginator"> <script lang="ts" setup generic="T extends IPaginator">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import type { Paginator } from '@/utility/paginator.js'; import type { IPaginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';

View file

@ -693,9 +693,7 @@ async function onPaste(ev: ClipboardEvent) {
} }
if (pastedFiles.length > 0) { if (pastedFiles.length > 0) {
ev.preventDefault(); ev.preventDefault();
os.launchUploader(pastedFiles, {}).then(driveFiles => { uploader.addFiles(pastedFiles);
files.value.push(...driveFiles);
});
return; return;
} }
@ -730,9 +728,7 @@ async function onPaste(ev: ClipboardEvent) {
const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0'); const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
os.launchUploader([file], {}).then(driveFiles => { uploader.addFiles([file]);
files.value.push(...driveFiles);
});
}); });
} }
} }
@ -776,9 +772,7 @@ function onDrop(ev: DragEvent): void {
// //
if (ev.dataTransfer && ev.dataTransfer.files.length > 0) { if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
ev.preventDefault(); ev.preventDefault();
os.launchUploader(Array.from(ev.dataTransfer.files), {}).then(driveFiles => { uploader.addFiles(Array.from(ev.dataTransfer.files));
files.value.push(...driveFiles);
});
return; return;
} }
@ -1195,7 +1189,7 @@ function showDraftMenu(ev: MouseEvent) {
if (!canSaveAsServerDraft.value) { if (!canSaveAsServerDraft.value) {
return os.alert({ return os.alert({
type: 'error', type: 'error',
text: i18n.ts._drafts.cannotCreateDraftOfRenote, text: i18n.ts._drafts.cannotCreateDraft,
}); });
} }
saveServerDraft(); saveServerDraft();

View file

@ -56,10 +56,12 @@ const emit = defineEmits<{
}>(); }>();
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number { function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
if (event.touches && event.touches[0] && event.touches[0].screenY != null) { if (('touches' in event) && event.touches[0] && event.touches[0].screenY != null) {
return event.touches[0].screenY; return event.touches[0].screenY;
} else { } else if ('screenY' in event) {
return event.screenY; return event.screenY;
} else {
return 0; // TS
} }
} }

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="label"> <div class="label">
<slot name="label"></slot> <slot name="label"></slot>
</div> </div>
<div v-adaptive-border class="body"> <div v-adaptive-border class="body" :class="{ 'disabled': disabled }">
<slot name="prefix"></slot> <slot name="prefix"></slot>
<div ref="containerEl" class="container"> <div ref="containerEl" class="container">
<div class="track"> <div class="track">
@ -180,6 +180,8 @@ function onMouseenter() {
let lastClickTime: number | null = null; let lastClickTime: number | null = null;
function onMousedown(ev: MouseEvent | TouchEvent) { function onMousedown(ev: MouseEvent | TouchEvent) {
if (props.disabled) return; // Prevent interaction if disabled
ev.preventDefault(); ev.preventDefault();
tooltipForDragShowing.value = true; tooltipForDragShowing.value = true;
@ -292,6 +294,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
border: solid 1px var(--MI_THEME-panel); border: solid 1px var(--MI_THEME-panel);
border-radius: 6px; border-radius: 6px;
&.disabled {
pointer-events: none;
opacity: 0.6;
}
> .container { > .container {
flex: 1; flex: 1;
position: relative; position: relative;

View file

@ -24,6 +24,7 @@ const elRef = useTemplateRef('elRef');
if (props.withTooltip) { if (props.withTooltip) {
useTooltip(elRef, (showing) => { useTooltip(elRef, (showing) => {
if (elRef.value == null) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
showing, showing,
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'), reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),

View file

@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
role: Misskey.entities.Role; role: Misskey.entities.Role;
forModeration: boolean; forModeration: boolean;
detailed: boolean; detailed?: boolean;
}>(), { }>(), {
detailed: true, detailed: true,
}); });

View file

@ -174,7 +174,7 @@ watch([modelValue, () => props.items], () => {
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
function show() { function show() {
if (opening.value) return; if (opening.value || props.disabled || props.readonly) return;
focus(); focus();
opening.value = true; opening.value = true;

View file

@ -59,9 +59,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue'; import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js'; import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
import type { BasicTimelineType } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js';
import type { SoundStore } from '@/preferences/def.js'; import type { SoundStore } from '@/preferences/def.js';
import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
@ -101,12 +103,12 @@ provide('inTimeline', true);
provide('tl_withSensitive', computed(() => props.withSensitive)); provide('tl_withSensitive', computed(() => props.withSensitive));
provide('inChannel', computed(() => props.src === 'channel')); provide('inChannel', computed(() => props.src === 'channel'));
let paginator: Paginator; let paginator: IPaginator<Misskey.entities.Note>;
if (props.src === 'antenna') { if (props.src === 'antenna') {
paginator = markRaw(new Paginator('antennas/notes', { paginator = markRaw(new Paginator('antennas/notes', {
computedParams: computed(() => ({ computedParams: computed(() => ({
antennaId: props.antenna, antennaId: props.antenna!,
})), })),
useShallowRef: true, useShallowRef: true,
})); }));
@ -160,21 +162,21 @@ if (props.src === 'antenna') {
computedParams: computed(() => ({ computedParams: computed(() => ({
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list!,
})), })),
useShallowRef: true, useShallowRef: true,
})); }));
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
paginator = markRaw(new Paginator('channels/timeline', { paginator = markRaw(new Paginator('channels/timeline', {
computedParams: computed(() => ({ computedParams: computed(() => ({
channelId: props.channel, channelId: props.channel!,
})), })),
useShallowRef: true, useShallowRef: true,
})); }));
} else if (props.src === 'role') { } else if (props.src === 'role') {
paginator = markRaw(new Paginator('roles/notes', { paginator = markRaw(new Paginator('roles/notes', {
computedParams: computed(() => ({ computedParams: computed(() => ({
roleId: props.role, roleId: props.role!,
})), })),
useShallowRef: true, useShallowRef: true,
})); }));
@ -223,6 +225,20 @@ onUnmounted(() => {
} }
}); });
const visibility = useDocumentVisibility();
let isPausingUpdate = false;
watch(visibility, () => {
if (visibility.value === 'hidden') {
isPausingUpdate = true;
} else { // 'visible'
isPausingUpdate = false;
if (isTop()) {
releaseQueue();
}
}
});
let adInsertionCounter = 0; let adInsertionCounter = 0;
const MIN_POLLING_INTERVAL = 1000 * 10; const MIN_POLLING_INTERVAL = 1000 * 10;
@ -236,7 +252,7 @@ if (!store.s.realtimeMode) {
// TODO: 1TL // TODO: 1TL
useInterval(async () => { useInterval(async () => {
paginator.fetchNewer({ paginator.fetchNewer({
toQueue: !isTop(), toQueue: !isTop() || isPausingUpdate,
}); });
}, POLLING_INTERVAL, { }, POLLING_INTERVAL, {
immediate: false, immediate: false,
@ -245,7 +261,7 @@ if (!store.s.realtimeMode) {
useGlobalEvent('notePosted', (note) => { useGlobalEvent('notePosted', (note) => {
paginator.fetchNewer({ paginator.fetchNewer({
toQueue: !isTop(), toQueue: !isTop() || isPausingUpdate,
}); });
}); });
} }
@ -256,17 +272,17 @@ useGlobalEvent('noteDeleted', (noteId) => {
function releaseQueue() { function releaseQueue() {
paginator.releaseQueue(); paginator.releaseQueue();
scrollToTop(rootEl.value); scrollToTop(rootEl.value!);
} }
function prepend(note: Misskey.entities.Note) { function prepend(note: Misskey.entities.Note & MisskeyEntity) {
adInsertionCounter++; adInsertionCounter++;
if (instance.notesPerOneAd > 0 && adInsertionCounter % instance.notesPerOneAd === 0) { if (instance.notesPerOneAd > 0 && adInsertionCounter % instance.notesPerOneAd === 0) {
note._shouldInsertAd_ = true; note._shouldInsertAd_ = true;
} }
if (isTop()) { if (isTop() && !isPausingUpdate) {
paginator.prepend(note); paginator.prepend(note);
} else { } else {
paginator.enqueue(note); paginator.enqueue(note);
@ -281,12 +297,13 @@ function prepend(note: Misskey.entities.Note) {
} }
} }
let connection: Misskey.ChannelConnection | null = null; let connection: Misskey.IChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null; let connection2: Misskey.IChannelConnection | null = null;
const stream = store.s.realtimeMode ? useStream() : null; const stream = store.s.realtimeMode ? useStream() : null;
function connectChannel() { function connectChannel() {
if (stream == null) return;
if (props.src === 'antenna') { if (props.src === 'antenna') {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connection = stream.useChannel('antenna', {
@ -507,7 +524,6 @@ defineExpose({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1em; gap: 1em;
opacity: 0.75;
padding: 8px 8px; padding: 8px 8px;
margin: 0 auto; margin: 0 auto;
border-bottom: solid 0.5px var(--MI_THEME-divider); border-bottom: solid 0.5px var(--MI_THEME-divider);

View file

@ -45,6 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue'; import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
import type { notificationTypes } from '@@/js/const.js'; import type { notificationTypes } from '@@/js/const.js';
import XNotification from '@/components/MkNotification.vue'; import XNotification from '@/components/MkNotification.vue';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
@ -92,6 +94,49 @@ if (!store.s.realtimeMode) {
}); });
} }
function isTop() {
if (scrollContainer == null) return true;
if (rootEl.value == null) return true;
const scrollTop = scrollContainer.scrollTop;
const tlTop = rootEl.value.offsetTop - scrollContainer.offsetTop;
return scrollTop <= tlTop;
}
function releaseQueue() {
paginator.releaseQueue();
scrollToTop(rootEl.value!);
}
let scrollContainer: HTMLElement | null = null;
function onScrollContainerScroll() {
if (isTop()) {
paginator.releaseQueue();
}
}
watch(rootEl, (el) => {
if (el && scrollContainer == null) {
scrollContainer = getScrollContainer(el);
if (scrollContainer == null) return;
scrollContainer.addEventListener('scroll', onScrollContainerScroll, { passive: true }); // scrollendios
}
}, { immediate: true });
const visibility = useDocumentVisibility();
let isPausingUpdate = false;
watch(visibility, () => {
if (visibility.value === 'hidden') {
isPausingUpdate = true;
} else { // 'visible'
isPausingUpdate = false;
if (isTop()) {
releaseQueue();
}
}
});
function onNotification(notification) { function onNotification(notification) {
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false; const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
if (isMuted || window.document.visibilityState === 'visible') { if (isMuted || window.document.visibilityState === 'visible') {
@ -101,7 +146,11 @@ function onNotification(notification) {
} }
if (!isMuted) { if (!isMuted) {
paginator.prepend(notification); if (isTop() && !isPausingUpdate) {
paginator.prepend(notification);
} else {
paginator.enqueue(notification);
}
} }
} }
@ -109,7 +158,7 @@ function reload() {
return paginator.reload(); return paginator.reload();
} }
let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null; let connection: Misskey.IChannelConnection<Misskey.Channels['main']> | null = null;
onMounted(() => { onMounted(() => {
paginator.init(); paginator.init();
@ -186,7 +235,6 @@ defineExpose({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1em; gap: 1em;
opacity: 0.75;
padding: 8px 8px; padding: 8px 8px;
margin: 0 auto; margin: 0 auto;
border-bottom: solid 0.5px var(--MI_THEME-divider); border-bottom: solid 0.5px var(--MI_THEME-divider);

View file

@ -169,10 +169,6 @@ onUnmounted(() => {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
} }
.tabsInner { .tabsInner {

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.left"> <div :class="$style.left">
<slot v-if="item.type === 'event'" name="left" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot> <slot v-if="item.type === 'event'" name="left" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot>
</div> </div>
<div :class="[$style.center, item.type === 'date' ? $style.date : '']"> <div :class="[$style.center, item.type === 'date' ? $style.date : '', i === 0 ? $style.first : '', i === items.length - 1 ? $style.last : '']">
<div :class="$style.centerLine"></div> <div :class="$style.centerLine"></div>
<div :class="$style.centerPoint"></div> <div :class="$style.centerPoint"></div>
</div> </div>
@ -143,6 +143,22 @@ const items = computed<TlItem<T>[]>(() => {
border-radius: 50%; border-radius: 50%;
} }
} }
&.first {
.centerLine {
height: 50%;
top: auto;
bottom: 0;
}
}
&.last {
.centerLine {
height: 50%;
top: 0;
bottom: auto;
}
}
} }
.centerLine { .centerLine {

View file

@ -9,22 +9,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.root"> <div :class="$style.root">
<MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/> <MkUserInfo v-for="item in items" :key="item.id" :user="extractor(item)"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Paginator } from '@/utility/paginator.js'; import * as Misskey from 'misskey-js';
import type { IPaginator } from '@/utility/paginator.js';
import MkUserInfo from '@/components/MkUserInfo.vue'; import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
paginator: Paginator; paginator: IPaginator;
noGap?: boolean; noGap?: boolean;
extractor?: (item: any) => any; extractor?: (item: any) => Misskey.entities.UserDetailed;
}>(), { }>(), {
extractor: (item) => item, extractor: (item) => item,
}); });

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :paginator="pinnedUsersPaginator"> <MkPagination :paginator="pinnedUsersPaginator">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.users"> <div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/> <XUser v-for="item in items" :key="item.id" :user="item"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :paginator="popularUsersPaginator"> <MkPagination :paginator="popularUsersPaginator">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.users"> <div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/> <XUser v-for="item in items" :key="item.id" :user="item"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
@ -34,7 +34,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';

View file

@ -194,10 +194,6 @@ onUnmounted(() => {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
} }
.tabsInner { .tabsInner {

View file

@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import { toUnicode as decodePunycode } from 'punycode.js'; import { toUnicode as decodePunycode } from 'punycode.js';
import { url as local } from '@@/js/config.js'; import { url as local } from '@@/js/config.js';
import { maybeMakeRelative } from '@@/js/url.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useTooltip } from '@/composables/use-tooltip.js'; import { useTooltip } from '@/composables/use-tooltip.js';
import { isEnabledUrlPreview } from '@/utility/url-preview.js'; import { isEnabledUrlPreview } from '@/utility/url-preview.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
import { maybeMakeRelative } from '@@/js/url.js';
function safeURIDecode(str: string): string { function safeURIDecode(str: string): string {
try { try {
@ -94,7 +94,7 @@ const target = self ? null : '_blank';
} }
.schema { .schema {
opacity: 0.5; color: color(from currentcolor srgb r g b / 0.5); // DOMopacity
} }
.hostname { .hostname {
@ -102,11 +102,11 @@ const target = self ? null : '_blank';
} }
.pathname { .pathname {
opacity: 0.8; color: color(from currentcolor srgb r g b / 0.8); // DOMopacity
} }
.query { .query {
opacity: 0.5; color: color(from currentcolor srgb r g b / 0.5); // DOMopacity
} }
.hash { .hash {

View file

@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue'; import type { UploaderFeatures } from '@/composables/use-uploader.js';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -837,7 +837,7 @@ export function launchUploader(
options?: { options?: {
folderId?: string | null; folderId?: string | null;
multiple?: boolean; multiple?: boolean;
features?: UploaderDialogFeatures; features?: UploaderFeatures;
}, },
): Promise<Misskey.entities.DriveFile[]> { ): Promise<Misskey.entities.DriveFile[]> {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {

View file

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'"> <FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
<div class="_gaps_s"> <div class="_gaps_s">
<MkInfo> <MkInfo>
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }} {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name ?? host }) }}
</MkInfo> </MkInfo>
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external> <FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
<template #icon><i class="ti ti-code"></i></template> <template #icon><i class="ti ti-code"></i></template>
@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue'; import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue';
import { version } from '@@/js/config.js'; import { host, version } from '@@/js/config.js';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -414,6 +414,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
const containerEl = useTemplateRef('containerEl'); const containerEl = useTemplateRef('containerEl');
function iconLoaded() { function iconLoaded() {
if (containerEl.value == null) return;
const emojis = prefer.s.emojiPalettes[0].emojis; const emojis = prefer.s.emojiPalettes[0].emojis;
const containerWidth = containerEl.value.offsetWidth; const containerWidth = containerEl.value.offsetWidth;
for (let i = 0; i < 32; i++) { for (let i = 0; i < 32; i++) {
@ -431,6 +432,7 @@ function iconLoaded() {
} }
function gravity() { function gravity() {
if (containerEl.value == null) return;
if (!easterEggReady) return; if (!easterEggReady) return;
easterEggReady = false; easterEggReady = false;
easterEggEngine.value = physics(containerEl.value); easterEggEngine.value = physics(containerEl.value);

View file

@ -0,0 +1,38 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
<MkPagination :paginator="paginator">
<template #default="{ items }">
<XMessage v-for="item in items" :key="item.id" :message="item" :isSearchResult="true"/>
</template>
</MkPagination>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, markRaw } from 'vue';
import XMessage from './chat/XMessage.vue';
import { i18n } from '@/i18n.js';
import MkInfo from '@/components/MkInfo.vue';
import { Paginator } from '@/utility/paginator.js';
import MkPagination from '@/components/MkPagination.vue';
const props = defineProps<{
fileId: string;
}>();
const realFileId = computed(() => props.fileId);
const paginator = markRaw(new Paginator('drive/files/attached-chat-messages', {
limit: 10,
params: {
fileId: realFileId.value,
},
}));
</script>

View file

@ -44,8 +44,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
</div> </div>
<div v-else-if="tab === 'notes' && info" class="_gaps_m"> <div v-else-if="tab === 'usage' && info" class="_gaps_m">
<XNotes :fileId="fileId"/> <MkTabs
v-model:tab="usageTab"
:tabs="[{
key: 'note',
title: 'Note',
}, {
key: 'chat',
title: 'Chat',
}]"
/>
<XNotes v-if="usageTab === 'note'" :fileId="fileId"/>
<XChat v-else-if="usageTab === 'chat'" :fileId="fileId"/>
</div> </div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m"> <div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
@ -86,12 +97,15 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { iAmAdmin, iAmModerator } from '@/i.js'; import { iAmAdmin, iAmModerator } from '@/i.js';
import MkTabs from '@/components/MkTabs.vue';
const tab = ref('overview'); const tab = ref('overview');
const file = ref<Misskey.entities.DriveFile | null>(null); const file = ref<Misskey.entities.DriveFile | null>(null);
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null); const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
const isSensitive = ref<boolean>(false); const isSensitive = ref<boolean>(false);
const usageTab = ref<'note' | 'chat'>('note');
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
const props = defineProps<{ const props = defineProps<{
fileId: string, fileId: string,
@ -147,9 +161,9 @@ const headerTabs = computed(() => [{
title: i18n.ts.overview, title: i18n.ts.overview,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
}, iAmModerator ? { }, iAmModerator ? {
key: 'notes', key: 'usage',
title: i18n.ts._fileViewer.attachedNotes, title: i18n.ts._fileViewer.usage,
icon: 'ti ti-pencil', icon: 'ti ti-plus',
} : null, iAmModerator ? { } : null, iAmModerator ? {
key: 'ip', key: 'ip',
title: 'IP', title: 'IP',

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="local">{{ i18n.ts.local }}</option> <option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option> <option value="remote">{{ i18n.ts.remote }}</option>
</MkSelect> </MkSelect>
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams.value.origin === 'local'"> <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams?.value?.origin === 'local'">
<template #label>{{ i18n.ts.host }}</template> <template #label>{{ i18n.ts.host }}</template>
</MkInput> </MkInput>
</div> </div>
@ -44,7 +44,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
const origin = ref<Misskey.entities.AdminDriveFilesRequest['origin']>('local'); const origin = ref<NonNullable<Misskey.entities.AdminDriveFilesRequest['origin']>>('local');
const type = ref<string | null>(null); const type = ref<string | null>(null);
const searchHost = ref(''); const searchHost = ref('');
const userId = ref(''); const userId = ref('');

View file

@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :paginator="paginator"> <MkPagination :paginator="paginator">
<template #default="{ items }"> <template #default="{ items }">
<div class="_gaps_s"> <div class="_gaps_s">
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/> <MkInviteCode v-for="item in items" :key="item.id" :invite="item" :onDeleted="deleted" moderator/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed, markRaw, ref, useTemplateRef } from 'vue'; import { computed, markRaw, ref, useTemplateRef } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -68,8 +69,8 @@ import MkInviteCode from '@/components/MkInviteCode.vue';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
const type = ref('all'); const type = ref<NonNullable<Misskey.entities.AdminInviteListRequest['type']>>('all');
const sort = ref('+createdAt'); const sort = ref<NonNullable<Misskey.entities.AdminInviteListRequest['sort']>>('+createdAt');
const paginator = markRaw(new Paginator('admin/invite/list', { const paginator = markRaw(new Paginator('admin/invite/list', {
limit: 10, limit: 10,

View file

@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items }"> <template #default="{ items }">
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedItems.includes(item.id) }]"> <div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpened]: expandedItems.includes(item.id) }]">
<div :class="$style.userItemMain"> <div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`"> <MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
<MkUserCardMini :user="item.user"/> <MkUserCardMini :user="item.user"/>
@ -76,12 +76,12 @@ const props = defineProps<{
const usersPaginator = markRaw(new Paginator('admin/roles/users', { const usersPaginator = markRaw(new Paginator('admin/roles/users', {
limit: 20, limit: 20,
computedParams: computed(() => ({ computedParams: computed(() => props.id ? ({
roleId: props.id, roleId: props.id,
})), }) : undefined),
})); }));
const expandedItems = ref([]); const expandedItems = ref<string[]>([]);
const role = reactive(await misskeyApi('admin/roles/show', { const role = reactive(await misskeyApi('admin/roles/show', {
roleId: props.id, roleId: props.id,
@ -199,7 +199,7 @@ definePage(() => ({
transition: transform 0.1s ease-out; transition: transform 0.1s ease-out;
} }
.userItem.userItemOpend { .userItem.userItemOpened {
.chevron { .chevron {
transform: rotateX(180deg); transform: rotateX(180deg);
} }

View file

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix>@</template> <template #prefix>@</template>
<template #label>{{ i18n.ts.username }}</template> <template #label>{{ i18n.ts.username }}</template>
</MkInput> </MkInput>
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams.value.origin === 'local'"> <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams?.value?.origin === 'local'">
<template #prefix>@</template> <template #prefix>@</template>
<template #label>{{ i18n.ts.host }}</template> <template #label>{{ i18n.ts.host }}</template>
</MkInput> </MkInput>
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination v-slot="{items}" :paginator="paginator"> <MkPagination v-slot="{items}" :paginator="paginator">
<div :class="$style.users"> <div :class="$style.users">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${user.updatedAt ? dateString(user.updatedAt) : 'Unknown'}`" :class="$style.user" :to="`/admin/user/${user.id}`">
<MkUserCardMini :user="user"/> <MkUserCardMini :user="user"/>
</MkA> </MkA>
</div> </div>

View file

@ -25,7 +25,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
const props = defineProps<{ const props = defineProps<{
messageId?: string; messageId: string;
}>(); }>();
const initializing = ref(true); const initializing = ref(true);

View file

@ -197,7 +197,7 @@ async function initialize() {
connection.value.on('deleted', onDeleted); connection.value.on('deleted', onDeleted);
connection.value.on('react', onReact); connection.value.on('react', onReact);
connection.value.on('unreact', onUnreact); connection.value.on('unreact', onUnreact);
} else { } else if (props.roomId) {
const [rResult, mResult] = await Promise.allSettled([ const [rResult, mResult] = await Promise.allSettled([
misskeyApi('chat/rooms/show', { roomId: props.roomId }), misskeyApi('chat/rooms/show', { roomId: props.roomId }),
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }), misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),

View file

@ -76,7 +76,8 @@ watch(() => props.clipId, async () => {
clip.value = await misskeyApi('clips/show', { clip.value = await misskeyApi('clips/show', {
clipId: props.clipId, clipId: props.clipId,
}); });
favorited.value = clip.value.isFavorited;
favorited.value = clip.value!.isFavorited ?? false;
}, { }, {
immediate: true, immediate: true,
}); });
@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.edit, text: i18n.ts.edit,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
if (clip.value == null) return;
const { canceled, result } = await os.form(clip.value.name, { const { canceled, result } = await os.form(clip.value.name, {
name: { name: {
type: 'string', type: 'string',
@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
default: clip.value.isPublic, default: clip.value.isPublic,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('clips/update', { os.apiWithDialog('clips/update', {
@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
text: i18n.ts.delete, text: i18n.ts.delete,
danger: true, danger: true,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
if (clip.value == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),

View file

@ -10,9 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template> <template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items }"> <template #default="{ items }">
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> <MkNote v-for="item in items" :key="item.id" :note="item.note" :class="$style.note"/>
<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
</MkDateSeparatedList>
</template> </template>
</MkPagination> </MkPagination>
</div> </div>
@ -23,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';

View file

@ -6,7 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true"> <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
<div class="_spacer" style="--MI_SPACER-w: 700px;"> <div class="_spacer" style="--MI_SPACER-w: 700px;">
<div v-if="tab === 'featured'"> <div v-if="tab === 'search'">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" type="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
<MkPagination v-if="searchPaginator" v-slot="{items}" :key="searchKey" :paginator="searchPaginator">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
</MkPagination>
</div>
</div>
<div v-else-if="tab === 'featured'">
<MkPagination v-slot="{items}" :paginator="featuredFlashsPaginator"> <MkPagination v-slot="{items}" :paginator="featuredFlashsPaginator">
<div class="_gaps_s"> <div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
@ -26,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-else-if="tab === 'liked'"> <div v-else-if="tab === 'liked'">
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator"> <MkPagination v-slot="{items}" :paginator="likedFlashsPaginator" withControl>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> <MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
</div> </div>
@ -37,10 +51,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, markRaw, ref } from 'vue'; import { computed, markRaw, ref, shallowRef } from 'vue';
import type { IPaginator } from '@/utility/paginator.js';
import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
@ -50,6 +66,10 @@ const router = useRouter();
const tab = ref('featured'); const tab = ref('featured');
const searchQuery = ref('');
const searchPaginator = shallowRef<Paginator<'flash/search'> | null>(null);
const searchKey = ref(0);
const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', { const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
limit: 5, limit: 5,
offsetMode: true, offsetMode: true,
@ -59,12 +79,28 @@ const myFlashsPaginator = markRaw(new Paginator('flash/my', {
})); }));
const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', { const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', {
limit: 5, limit: 5,
canSearch: true,
searchParamName: 'search',
})); }));
function create() { function create() {
router.push('/play/new'); router.push('/play/new');
} }
function search() {
if (searchQuery.value.trim() === '') {
return;
}
searchPaginator.value = markRaw(new Paginator('flash/search', {
params: {
query: searchQuery.value,
},
}));
searchKey.value++;
}
const headerActions = computed(() => [{ const headerActions = computed(() => [{
icon: 'ti ti-plus', icon: 'ti ti-plus',
text: i18n.ts.create, text: i18n.ts.create,
@ -72,6 +108,10 @@ const headerActions = computed(() => [{
}]); }]);
const headerTabs = computed(() => [{ const headerTabs = computed(() => [{
key: 'search',
title: i18n.ts.search,
icon: 'ti ti-search',
}, {
key: 'featured', key: 'featured',
title: i18n.ts._play.featured, title: i18n.ts._play.featured,
icon: 'ti ti-flare', icon: 'ti ti-flare',

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<MkKeyValue> <MkKeyValue>
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template> <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
<template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template> <template #value><MkUrl v-if="url" :url="url" :showUrlPreview="false"></MkUrl></template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue> <MkKeyValue>
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
@ -151,7 +151,7 @@ async function fetch() {
case 'theme': case 'theme':
try { try {
const metaRaw = parseThemeCode(res.data); const metaRaw = parseThemeCode(res.data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, props, desc: description, ...meta } = metaRaw; const { id, props, desc: description, ...meta } = metaRaw;
data.value = { data.value = {
type: 'theme', type: 'theme',

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;"> <div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div v-if="tab === 'overview'" class="_gaps_m"> <div v-if="tab === 'overview'" class="_gaps_m">
<div :class="$style.faviconAndName"> <div :class="$style.faviconAndName">
<img :src="faviconUrl" alt="" :class="$style.icon"/> <img v-if="faviconUrl" :src="faviconUrl" alt="" :class="$style.icon"/>
<span :class="$style.name">{{ instance.name || `(${i18n.ts.unknown})` }}</span> <span :class="$style.name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
</div> </div>
<div style="display: flex; flex-direction: column; gap: 1em;"> <div style="display: flex; flex-direction: column; gap: 1em;">

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :paginator="paginator"> <MkPagination :paginator="paginator">
<template #default="{ items }"> <template #default="{ items }">
<div class="_gaps_s"> <div class="_gaps_s">
<MkInviteCode v-for="item in (items as Misskey.entities.InviteCode[])" :key="item.id" :invite="item" :onDeleted="deleted"/> <MkInviteCode v-for="item in items" :key="item.id" :invite="item" :onDeleted="deleted"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>

View file

@ -62,24 +62,29 @@ function fetchList(): void {
} }
function like() { function like() {
if (list.value == null) return;
os.apiWithDialog('users/lists/favorite', { os.apiWithDialog('users/lists/favorite', {
listId: list.value.id, listId: list.value.id,
}).then(() => { }).then(() => {
if (list.value == null) return;
list.value.isLiked = true; list.value.isLiked = true;
list.value.likedCount++; list.value.likedCount++;
}); });
} }
function unlike() { function unlike() {
if (list.value == null) return;
os.apiWithDialog('users/lists/unfavorite', { os.apiWithDialog('users/lists/unfavorite', {
listId: list.value.id, listId: list.value.id,
}).then(() => { }).then(() => {
if (list.value == null) return;
list.value.isLiked = false; list.value.isLiked = false;
list.value.likedCount--; list.value.likedCount--;
}); });
} }
async function create() { async function create() {
if (list.value == null) return;
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName, title: i18n.ts.enterListName,
}); });

View file

@ -64,6 +64,7 @@ async function create() {
default: false, default: false,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('clips/create', result); os.apiWithDialog('clips/create', result);

View file

@ -100,7 +100,7 @@ const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
limit: 10, limit: 10,
initialId: props.noteId, initialId: props.noteId,
initialDirection: 'older', initialDirection: 'older',
computedParams: computed(() => note.value ? ({ computedParams: computed(() => note.value && note.value.channelId != null ? ({
channelId: note.value.channelId, channelId: note.value.channelId,
}) : undefined), }) : undefined),
})); }));
@ -109,7 +109,7 @@ const nextChannelPaginator = markRaw(new Paginator('channels/timeline', {
limit: 10, limit: 10,
initialId: props.noteId, initialId: props.noteId,
initialDirection: 'newer', initialDirection: 'newer',
computedParams: computed(() => note.value ? ({ computedParams: computed(() => note.value && note.value.channelId != null ? ({
channelId: note.value.channelId, channelId: note.value.channelId,
}) : undefined), }) : undefined),
})); }));

View file

@ -135,9 +135,9 @@ const page = ref<Misskey.entities.Page | null>(null);
const error = ref<any>(null); const error = ref<any>(null);
const otherPostsPaginator = markRaw(new Paginator('users/pages', { const otherPostsPaginator = markRaw(new Paginator('users/pages', {
limit: 6, limit: 6,
computedParams: computed(() => ({ computedParams: computed(() => page.value ? ({
userId: page.value.user.id, userId: page.value.user.id,
})), }) : undefined),
})); }));
const path = computed(() => props.username + '/' + props.pageName); const path = computed(() => props.username + '/' + props.pageName);

View file

@ -79,7 +79,9 @@ async function createKey() {
default: scope.value.join('/'), default: scope.value.join('/'),
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('i/registry/set', { os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'), scope: result.scope.split('/'),
key: result.key, key: result.key,

View file

@ -56,7 +56,9 @@ async function createKey() {
label: i18n.ts._registry.scope, label: i18n.ts._registry.scope,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('i/registry/set', { os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'), scope: result.scope.split('/'),
key: result.key, key: result.key,

View file

@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as Misskey from 'misskey-js';
import { computed, markRaw, ref, watch } from 'vue'; import { computed, markRaw, ref, watch } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import type { StyleValue } from 'vue'; import type { StyleValue } from 'vue';
@ -62,7 +63,7 @@ import MkSelect from '@/components/MkSelect.vue';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
const sortMode = ref('+size'); const sortMode = ref<Misskey.entities.DriveFilesRequest['sort']>('+size');
const paginator = markRaw(new Paginator('drive/files', { const paginator = markRaw(new Paginator('drive/files', {
limit: 10, limit: 10,
computedParams: computed(() => ({ sort: sortMode.value })), computedParams: computed(() => ({ sort: sortMode.value })),

View file

@ -208,9 +208,9 @@ const blockingPaginator = markRaw(new Paginator('blocking/list', {
limit: 10, limit: 10,
})); }));
const expandedRenoteMuteItems = ref([]); const expandedRenoteMuteItems = ref<string[]>([]);
const expandedMuteItems = ref([]); const expandedMuteItems = ref<string[]>([]);
const expandedBlockItems = ref([]); const expandedBlockItems = ref<string[]>([]);
const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord'); const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
@ -253,7 +253,7 @@ async function unblock(user, ev) {
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
} }
async function toggleRenoteMuteItem(item) { async function toggleRenoteMuteItem(item: { id: string }) {
if (expandedRenoteMuteItems.value.includes(item.id)) { if (expandedRenoteMuteItems.value.includes(item.id)) {
expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id); expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
} else { } else {
@ -261,7 +261,7 @@ async function toggleRenoteMuteItem(item) {
} }
} }
async function toggleMuteItem(item) { async function toggleMuteItem(item: { id: string }) {
if (expandedMuteItems.value.includes(item.id)) { if (expandedMuteItems.value.includes(item.id)) {
expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id); expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id);
} else { } else {
@ -269,7 +269,7 @@ async function toggleMuteItem(item) {
} }
} }
async function toggleBlockItem(item) { async function toggleBlockItem(item: { id: string }) {
if (expandedBlockItems.value.includes(item.id)) { if (expandedBlockItems.value.includes(item.id)) {
expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id); expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id);
} else { } else {

View file

@ -59,10 +59,21 @@ const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
async function init() { async function init() {
let noteText = ''; let noteText = '';
if (title.value) noteText += `[ ${title.value} ]\n`; if (title.value) {
// Google noteText += `[ ${title.value} ]\n`;
if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, '');
else if (text && title.value !== text) noteText += `${text}\n`; //#region add text to note text
if (text?.startsWith(title.value)) {
// For the Google app https://github.com/misskey-dev/misskey/issues/16224
noteText += text.replace(title.value, '').trimStart();
} else if (text) {
noteText += `${text}\n`;
}
//#endregion
} else if (text) {
noteText += `${text}\n`;
}
if (url) { if (url) {
try { try {
// Normalize the URL to URL-encoded and puny-coded from with the URL constructor. // Normalize the URL to URL-encoded and puny-coded from with the URL constructor.

View file

@ -25,7 +25,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName class="name" :user="user" :nowrap="true"/> <MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
@ -44,7 +43,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="false" class="name"/> <MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div> </div>

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm v-if="note.text" :text="note.text" :author="note.user"/> <Mfm v-if="note.text" :text="note.text" :author="note.user"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div> </div>
<div v-if="note.files && note.files.length > 0" :class="$style.richcontent"> <div v-if="note.files && note.files.length > 0 && (note.cw == null || showContent)" :class="$style.richcontent">
<MkMediaList :mediaList="note.files.slice(0, 4)"/> <MkMediaList :mediaList="note.files.slice(0, 4)"/>
</div> </div>
<div v-if="note.reactionCount > 0" :class="$style.reactions"> <div v-if="note.reactionCount > 0" :class="$style.reactions">

View file

@ -14,12 +14,13 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import type { FormWithDefault } from '@/utility/form.js';
export type Plugin = { export type Plugin = {
installId: string; installId: string;
name: string; name: string;
active: boolean; active: boolean;
config?: Record<string, { default: any }>; config?: FormWithDefault;
configData: Record<string, any>; configData: Record<string, any>;
src: string | null; src: string | null;
version: string; version: string;
@ -155,6 +156,13 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
export async function uninstallPlugin(plugin: Plugin) { export async function uninstallPlugin(plugin: Plugin) {
abortPlugin(plugin); abortPlugin(plugin);
prefer.commit('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId)); prefer.commit('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
Object.keys(window.localStorage).forEach(key => {
if (key.startsWith('aiscript:plugins:' + plugin.installId)) {
window.localStorage.removeItem(key);
}
});
if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) { if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) {
await os.apiWithDialog('i/revoke-token', { await os.apiWithDialog('i/revoke-token', {
token: store.s.pluginTokens[plugin.installId], token: store.s.pluginTokens[plugin.installId],
@ -233,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> {
pluginLogs.value.set(plugin.installId, []); pluginLogs.value.set(plugin.installId, []);
function systemLog(message: string, isError = false): void { function systemLog(message: string, isError = false): void {
pluginLogs.value.get(plugin.installId)?.push({ pluginLogs.value.get(plugin!.installId)?.push({
at: Date.now(), at: Date.now(),
isSystem: true, isSystem: true,
message, message,

View file

@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', {
}, },
memo: { memo: {
where: 'account', where: 'account',
default: null, default: null as string | null,
}, },
reactionAcceptance: { reactionAcceptance: {
where: 'account', where: 'account',

View file

@ -22,11 +22,6 @@
} }
} }
::selection {
color: var(--MI_THEME-fgOnAccent);
background-color: var(--MI_THEME-accent);
}
html { html {
overflow: auto; overflow: auto;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -45,27 +40,6 @@ html {
&, * { &, * {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin; scrollbar-width: thin;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: inherit;
}
&::-webkit-scrollbar-thumb {
background: var(--MI_THEME-scrollbarHandle);
&:hover {
background: var(--MI_THEME-scrollbarHandleHover);
}
&:active {
background: var(--MI_THEME-accent);
}
}
} }
&.f-1 { &.f-1 {
@ -110,6 +84,11 @@ html::view-transition-old(theme-changing) {
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
html::selection {
color: var(--MI_THEME-fgOnAccent);
background-color: var(--MI_THEME-accent);
}
@keyframes themeChangingOld { @keyframes themeChangingOld {
0% { 0% {
opacity: 1; opacity: 1;

View file

@ -4,10 +4,10 @@
*/ */
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { Component, ComputedRef, Ref } from 'vue'; import type { Component, ComputedRef, Ref, MaybeRef } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> }; type ComponentProps<T extends Component> = { [K in keyof CP<T>]: MaybeRef<CP<T>[K]> };
type MenuRadioOptionsDef = Record<string, any>; type MenuRadioOptionsDef = Record<string, any>;
@ -15,22 +15,107 @@ type Text = string | ComputedRef<string>;
export type MenuAction = (ev: MouseEvent) => void; export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = { type: 'divider' }; export interface MenuButton {
export type MenuNull = undefined; type?: 'button';
export type MenuLabel = { type: 'label', text: Text, caption?: Text }; text: Text;
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; caption?: Text;
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean }; icon?: string;
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; indicate?: boolean;
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> }; danger?: boolean;
export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction }; active?: boolean | ComputedRef<boolean>;
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> }; avatar?: Misskey.entities.User;
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> }; action: MenuAction;
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> }; }
export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuPending = { type: 'pending' }; interface MenuBase {
type: string;
}
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent; export interface MenuDivider extends MenuBase {
type: 'divider';
}
export interface MenuLabel extends MenuBase {
type: 'label';
text: Text;
caption?: Text;
}
export interface MenuLink extends MenuBase {
type: 'link';
to: string;
text: Text;
caption?: Text;
icon?: string;
indicate?: boolean;
avatar?: Misskey.entities.User;
}
export interface MenuA extends MenuBase {
type: 'a';
href: string;
target?: string;
download?: string;
text: Text;
caption?: Text;
icon?: string;
indicate?: boolean;
}
export interface MenuUser extends MenuBase {
type: 'user';
user: Misskey.entities.User;
active?: boolean;
indicate?: boolean;
action: MenuAction;
}
export interface MenuSwitch extends MenuBase {
type: 'switch';
ref: Ref<boolean>;
text: Text;
caption?: Text;
icon?: string;
disabled?: boolean | Ref<boolean>;
}
export interface MenuRadio extends MenuBase {
type: 'radio';
text: Text;
caption?: Text;
icon?: string;
ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>;
options: MenuRadioOptionsDef;
disabled?: boolean | Ref<boolean>;
}
export interface MenuRadioOption extends MenuBase {
type: 'radioOption';
text: Text;
caption?: Text;
action: MenuAction;
active?: boolean | ComputedRef<boolean>;
}
export interface MenuComponent<T extends Component = any> extends MenuBase {
type: 'component';
component: T;
props?: ComponentProps<T>;
}
export interface MenuParent extends MenuBase {
type: 'parent';
text: Text;
caption?: Text;
icon?: string;
children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]);
}
export interface MenuPending extends MenuBase {
type: 'pending';
}
type OuterMenuItem = MenuDivider | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuComponent | MenuParent>; type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuComponent | MenuParent>;
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent; export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;

View file

@ -6,9 +6,9 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
export interface PostFormProps { export interface PostFormProps {
reply?: Misskey.entities.Note; reply?: Misskey.entities.Note | null;
renote?: Misskey.entities.Note; renote?: Misskey.entities.Note | null;
channel?: Misskey.entities.Channel; // TODO channel?: Misskey.entities.Channel | null; // TODO
mention?: Misskey.entities.User; mention?: Misskey.entities.User;
specified?: Misskey.entities.UserDetailed; specified?: Misskey.entities.UserDetailed;
initialText?: string; initialText?: string;

View file

@ -368,10 +368,6 @@ function onDrop(ev) {
> .body { > .body {
background: transparent !important; background: transparent !important;
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: transparent;
}
} }
} }
@ -397,10 +393,6 @@ function onDrop(ev) {
> .body { > .body {
background: var(--MI_THEME-bg) !important; background: var(--MI_THEME-bg) !important;
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: inherit;
}
} }
} }
} }
@ -487,9 +479,5 @@ function onDrop(ev) {
container-type: size; container-type: size;
background-color: var(--MI_THEME-bg); background-color: var(--MI_THEME-bg);
scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel); scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
&::-webkit-scrollbar-track {
background: var(--MI_THEME-panel);
}
} }
</style> </style>

View file

@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
label: i18n.ts.sound, label: i18n.ts.sound,
default: soundSetting.value.type ?? 'none', default: soundSetting.value.type ?? 'none',
enum: soundsTypes.map(f => ({ enum: soundsTypes.map(f => ({
value: f ?? 'none', label: getSoundTypeName(f), value: f ?? 'none' as Exclude<SoundType, null> | 'none',
label: getSoundTypeName(f),
})), })),
}, },
soundFile: { soundFile: {
@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
}, },
}, },
}); });
if (canceled) return; if (canceled) return;
const res = buildSoundStore(result); const res = buildSoundStore(result);
if (res) soundSetting.value = res; if (res) soundSetting.value = res;
function buildSoundStore(result: any): SoundStore | null { function buildSoundStore(r: NonNullable<typeof result>): SoundStore | null {
const type = (result.type === 'none' ? null : result.type) as SoundType; const type = (r.type === 'none' ? null : r.type);
const volume = result.volume as number; const volume = r.volume;
const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
if (type === '_driveFile_') { if (type === '_driveFile_') {
if (!fileUrl || !fileId) { if (!fileUrl || !fileId) {

View file

@ -126,7 +126,6 @@ const onContextmenu = (ev) => {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
$ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px; $widgets-hide-threshold: 1090px;
.root { .root {

View file

@ -5,55 +5,59 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
type EnumItem = string | { export type EnumItem = string | {
label: string; label: string;
value: string; value: unknown;
}; };
type Hidden = boolean | ((v: any) => boolean); type Hidden = boolean | ((v: any) => boolean);
export type FormItem = { interface FormItemBase {
label?: string; label?: string;
hidden?: Hidden;
}
export interface StringFormItem extends FormItemBase {
type: 'string'; type: 'string';
default?: string | null; default?: string | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden;
multiline?: boolean; multiline?: boolean;
treatAsMfm?: boolean; treatAsMfm?: boolean;
} | { }
label?: string;
export interface NumberFormItem extends FormItemBase {
type: 'number'; type: 'number';
default?: number | null; default?: number | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden;
step?: number; step?: number;
} | { }
label?: string;
export interface BooleanFormItem extends FormItemBase {
type: 'boolean'; type: 'boolean';
default?: boolean | null; default?: boolean | null;
description?: string; description?: string;
hidden?: Hidden; }
} | {
label?: string; export interface EnumFormItem extends FormItemBase {
type: 'enum'; type: 'enum';
default?: string | null; default?: string | null;
required?: boolean; required?: boolean;
hidden?: Hidden;
enum: EnumItem[]; enum: EnumItem[];
} | { }
label?: string;
export interface RadioFormItem extends FormItemBase {
type: 'radio'; type: 'radio';
default?: unknown | null; default?: unknown | null;
required?: boolean; required?: boolean;
hidden?: Hidden;
options: { options: {
label: string; label: string;
value: unknown; value: unknown;
}[]; }[];
} | { }
label?: string;
export interface RangeFormItem extends FormItemBase {
type: 'range'; type: 'range';
default?: number | null; default?: number | null;
description?: string; description?: string;
@ -62,42 +66,80 @@ export type FormItem = {
min: number; min: number;
max: number; max: number;
textConverter?: (value: number) => string; textConverter?: (value: number) => string;
hidden?: Hidden; }
} | {
label?: string; export interface ObjectFormItem extends FormItemBase {
type: 'object'; type: 'object';
default?: Record<string, unknown> | null; default?: Record<string, unknown> | null;
hidden: Hidden; }
} | {
label?: string; export interface ArrayFormItem extends FormItemBase {
type: 'array'; type: 'array';
default?: unknown[] | null; default?: unknown[] | null;
hidden: Hidden; }
} | {
export interface ButtonFormItem extends FormItemBase {
type: 'button'; type: 'button';
content?: string; content?: string;
hidden?: Hidden;
action: (ev: MouseEvent, v: any) => void; action: (ev: MouseEvent, v: any) => void;
} | { }
export interface DriveFileFormItem extends FormItemBase {
type: 'drive-file'; type: 'drive-file';
defaultFileId?: string | null; defaultFileId?: string | null;
hidden?: Hidden;
validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>; validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
}; }
export type FormItem =
StringFormItem |
NumberFormItem |
BooleanFormItem |
EnumFormItem |
RadioFormItem |
RangeFormItem |
ObjectFormItem |
ArrayFormItem |
ButtonFormItem |
DriveFileFormItem;
export type Form = Record<string, FormItem>; export type Form = Record<string, FormItem>;
export type FormItemWithDefault = FormItem & {
default: unknown;
};
export type FormWithDefault = Record<string, FormItemWithDefault>;
type GetRadioItemType<Item extends RadioFormItem = RadioFormItem> = Item['options'][number]['value'];
type GetEnumItemType<Item extends EnumFormItem, E = Item['enum'][number]> = E extends { value: unknown } ? E['value'] : E;
type InferDefault<T, Fallback> = T extends { default: infer D }
? D extends undefined ? Fallback : D
: Fallback;
type NonNullableIfRequired<T, Item extends FormItem> =
Item extends { required: false } ? T | null | undefined : NonNullable<T>;
type GetItemType<Item extends FormItem> = type GetItemType<Item extends FormItem> =
Item['type'] extends 'string' ? string : Item extends StringFormItem
Item['type'] extends 'number' ? number : ? NonNullableIfRequired<InferDefault<Item, string>, Item>
Item['type'] extends 'boolean' ? boolean : : Item extends NumberFormItem
Item['type'] extends 'radio' ? unknown : ? NonNullableIfRequired<InferDefault<Item, number>, Item>
Item['type'] extends 'range' ? number : : Item extends BooleanFormItem
Item['type'] extends 'enum' ? string : ? boolean
Item['type'] extends 'array' ? unknown[] : : Item extends RadioFormItem
Item['type'] extends 'object' ? Record<string, unknown> : ? GetRadioItemType<Item>
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : : Item extends RangeFormItem
never; ? NonNullableIfRequired<InferDefault<RangeFormItem, number>, Item>
: Item extends EnumFormItem
? GetEnumItemType<Item>
: Item extends ArrayFormItem
? NonNullableIfRequired<InferDefault<ArrayFormItem, unknown[]>, Item>
: Item extends ObjectFormItem
? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item>
: Item extends DriveFileFormItem
? Misskey.entities.DriveFile | undefined
: never;
export type GetFormResultType<F extends Form> = { export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>; [P in keyof F]: GetItemType<F[P]>;

View file

@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: {
const { canceled, result } = await os.form(i18n.ts.createNewClip, { const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: { name: {
type: 'string', type: 'string',
default: null, default: null as string | null,
label: i18n.ts.name, label: i18n.ts.name,
}, },
description: { description: {
@ -180,6 +180,7 @@ export function getNoteMenu(props: {
currentClip?: Misskey.entities.Clip; currentClip?: Misskey.entities.Clip;
}) { }) {
const appearNote = getAppearNote(props.note); const appearNote = getAppearNote(props.note);
const link = appearNote.url ?? appearNote.uri;
const cleanups = [] as (() => void)[]; const cleanups = [] as (() => void)[];
@ -189,6 +190,7 @@ export function getNoteMenu(props: {
text: i18n.ts.noteDeleteConfirm, text: i18n.ts.noteDeleteConfirm,
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
if ($i == null) return;
misskeyApi('notes/delete', { misskeyApi('notes/delete', {
noteId: appearNote.id, noteId: appearNote.id,
@ -208,6 +210,7 @@ export function getNoteMenu(props: {
text: i18n.ts.deleteAndEditConfirm, text: i18n.ts.deleteAndEditConfirm,
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
if ($i == null) return;
misskeyApi('notes/delete', { misskeyApi('notes/delete', {
noteId: appearNote.id, noteId: appearNote.id,
@ -317,22 +320,25 @@ export function getNoteMenu(props: {
action: copyContent, action: copyContent,
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
if (appearNote.url || appearNote.uri) { if (link) {
menuItems.push({ menuItems.push({
icon: 'ti ti-link', icon: 'ti ti-link',
text: i18n.ts.copyRemoteLink, text: i18n.ts.copyRemoteLink,
action: () => { action: () => {
copyToClipboard(appearNote.url ?? appearNote.uri); copyToClipboard(link);
}, },
}, { }, {
icon: 'ti ti-external-link', icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote, text: i18n.ts.showOnRemote,
action: () => { action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); window.open(link, '_blank', 'noopener');
}, },
}); });
} else { } else {
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed)); const embedMenu = getNoteEmbedCodeMenu(appearNote, i18n.ts.embed);
if (embedMenu != null) {
menuItems.push(embedMenu);
}
} }
if (isSupportShare()) { if (isSupportShare()) {
@ -475,22 +481,25 @@ export function getNoteMenu(props: {
action: copyContent, action: copyContent,
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
if (appearNote.url || appearNote.uri) { if (link != null) {
menuItems.push({ menuItems.push({
icon: 'ti ti-link', icon: 'ti ti-link',
text: i18n.ts.copyRemoteLink, text: i18n.ts.copyRemoteLink,
action: () => { action: () => {
copyToClipboard(appearNote.url ?? appearNote.uri); copyToClipboard(link);
}, },
}, { }, {
icon: 'ti ti-external-link', icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote, text: i18n.ts.showOnRemote,
action: () => { action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); window.open(link, '_blank', 'noopener');
}, },
}); });
} else { } else {
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed)); const embedMenu = getNoteEmbedCodeMenu(appearNote, i18n.ts.embed);
if (embedMenu != null) {
menuItems.push(embedMenu);
}
} }
} }
@ -625,7 +634,7 @@ export function getRenoteMenu(props: {
}); });
} }
}, },
}, (props.mock) ? undefined : { }, ...(props.mock ? [] : [{
text: i18n.ts.quote, text: i18n.ts.quote,
icon: 'ti ti-quote', icon: 'ti ti-quote',
action: () => { action: () => {
@ -633,7 +642,7 @@ export function getRenoteMenu(props: {
renote: appearNote, renote: appearNote,
}); });
}, },
}]); }])]);
normalExternalChannelRenoteItems.push({ normalExternalChannelRenoteItems.push({
type: 'parent', type: 'parent',

View file

@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const userDetailed = await misskeyApi('users/show', { const userDetailed = await misskeyApi('users/show', {
userId: user.id, userId: user.id,
}); });
const { canceled, result } = await os.form(i18n.ts.editMemo, { const { canceled, result } = await os.form(i18n.ts.editMemo, {
memo: { memo: {
type: 'string', type: 'string',
@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
default: userDetailed.memo, default: userDetailed.memo,
}, },
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('users/update-memo', { os.apiWithDialog('users/update-memo', {

View file

@ -5,7 +5,7 @@
import { ref, shallowRef, triggerRef } from 'vue'; import { ref, shallowRef, triggerRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue'; import type { ComputedRef, Ref, ShallowRef } from 'vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
const MAX_ITEMS = 30; const MAX_ITEMS = 30;
@ -17,14 +17,60 @@ export type MisskeyEntity = {
id: string; id: string;
createdAt: string; createdAt: string;
_shouldInsertAd_?: boolean; _shouldInsertAd_?: boolean;
[x: string]: any;
}; };
export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })> { type FilterByEpRes<E extends Record<string, any>> = {
[K in keyof E]: E[K]['res'] extends Array<{ id: string }> ? K : never
}[keyof E];
export type PaginatorCompatibleEndpointPaths = FilterByEpRes<Misskey.Endpoints>;
export type PaginatorCompatibleEndpoints = {
[K in PaginatorCompatibleEndpointPaths]: Misskey.Endpoints[K];
};
export interface IPaginator<T = unknown, _T = T & MisskeyEntity> {
/** /**
* *
*/ */
public items: ShallowRef<T[]> | Ref<T[]>; items: Ref<_T[]> | ShallowRef<_T[]>;
queuedAheadItemsCount: Ref<number>;
fetching: Ref<boolean>;
fetchingOlder: Ref<boolean>;
fetchingNewer: Ref<boolean>;
canFetchOlder: Ref<boolean>;
canSearch: boolean;
error: Ref<boolean>;
computedParams: ComputedRef<Misskey.Endpoints[PaginatorCompatibleEndpointPaths]['req'] | null | undefined> | null;
initialId: MisskeyEntity['id'] | null;
initialDate: number | null;
initialDirection: 'newer' | 'older';
noPaging: boolean;
searchQuery: Ref<null | string>;
order: Ref<'newest' | 'oldest'>;
init(): Promise<void>;
reload(): Promise<void>;
fetchOlder(): Promise<void>;
fetchNewer(options?: { toQueue?: boolean }): Promise<void>;
trim(trigger?: boolean): void;
unshiftItems(newItems: (_T)[]): void;
pushItems(oldItems: (_T)[]): void;
prepend(item: _T): void;
enqueue(item: _T): void;
releaseQueue(): void;
removeItem(id: string): void;
updateItem(id: string, updater: (item: _T) => _T): void;
}
export class Paginator<
Endpoint extends PaginatorCompatibleEndpointPaths,
E extends PaginatorCompatibleEndpoints[Endpoint] = PaginatorCompatibleEndpoints[Endpoint],
T extends E['res'][number] & MisskeyEntity = E['res'][number] & MisskeyEntity,
SRef extends boolean = false,
> implements IPaginator {
/**
*
*/
public items: SRef extends true ? ShallowRef<T[]> : Ref<T[]>;
public queuedAheadItemsCount = ref(0); public queuedAheadItemsCount = ref(0);
public fetching = ref(true); public fetching = ref(true);
@ -35,18 +81,18 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
public error = ref(false); public error = ref(false);
private endpoint: Endpoint; private endpoint: Endpoint;
private limit: number; private limit: number;
private params: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']); private params: E['req'] | (() => E['req']);
public computedParams: ComputedRef<Misskey.Endpoints[Endpoint]['req']> | null; public computedParams: ComputedRef<E['req'] | null | undefined> | null;
public initialId: MisskeyEntity['id'] | null = null; public initialId: MisskeyEntity['id'] | null = null;
public initialDate: number | null = null; public initialDate: number | null = null;
public initialDirection: 'newer' | 'older'; public initialDirection: 'newer' | 'older';
private offsetMode: boolean; private offsetMode: boolean;
public noPaging: boolean; public noPaging: boolean;
public searchQuery = ref<null | string>(''); public searchQuery = ref<null | string>('');
private searchParamName: string; private searchParamName: keyof E['req'] | 'search';
private canFetchDetection: 'safe' | 'limit' | null = null; private canFetchDetection: 'safe' | 'limit' | null = null;
private aheadQueue: T[] = []; private aheadQueue: T[] = [];
private useShallowRef: boolean; private useShallowRef: SRef;
// 配列内の要素をどのような順序で並べるか // 配列内の要素をどのような順序で並べるか
// newest: 新しいものが先頭 (default) // newest: 新しいものが先頭 (default)
@ -56,8 +102,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
constructor(endpoint: Endpoint, props: { constructor(endpoint: Endpoint, props: {
limit?: number; limit?: number;
params?: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']); params?: E['req'] | (() => E['req']);
computedParams?: ComputedRef<Misskey.Endpoints[Endpoint]['req']>; computedParams?: ComputedRef<E['req'] | null | undefined>;
/** /**
* APIのような * APIのような
@ -75,14 +121,19 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨 // 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
canFetchDetection?: 'safe' | 'limit'; canFetchDetection?: 'safe' | 'limit';
useShallowRef?: boolean; useShallowRef?: SRef;
canSearch?: boolean; canSearch?: boolean;
searchParamName?: keyof Misskey.Endpoints[Endpoint]['req']; searchParamName?: keyof E['req'];
}) { }) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.useShallowRef = props.useShallowRef ?? false; this.useShallowRef = (props.useShallowRef ?? false) as SRef;
this.items = this.useShallowRef ? shallowRef([] as T[]) : ref([] as T[]); if (this.useShallowRef) {
this.items = shallowRef<T[]>([]);
} else {
this.items = ref<T[]>([]) as Ref<T[]>;
}
this.limit = props.limit ?? FIRST_FETCH_LIMIT; this.limit = props.limit ?? FIRST_FETCH_LIMIT;
this.params = props.params ?? {}; this.params = props.params ?? {};
this.computedParams = props.computedParams ?? null; this.computedParams = props.computedParams ?? null;
@ -130,7 +181,7 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
this.queuedAheadItemsCount.value = 0; this.queuedAheadItemsCount.value = 0;
this.fetching.value = true; this.fetching.value = true;
await misskeyApi<T[]>(this.endpoint, { const data: E['req'] = {
...(typeof this.params === 'function' ? this.params() : this.params), ...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}), ...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
@ -145,39 +196,46 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
untilId: this.initialId ?? undefined, untilId: this.initialId ?? undefined,
untilDate: this.initialDate ?? undefined, untilDate: this.initialDate ?? undefined,
} : {}), } : {}),
}).then(res => { };
// 逆順で返ってくるので
if ((this.initialId || this.initialDate) && this.initialDirection === 'newer') {
res.reverse();
}
for (let i = 0; i < res.length; i++) { const apiRes = (await misskeyApi(this.endpoint, data).catch(err => {
const item = res[i];
if (i === 3) item._shouldInsertAd_ = true;
}
this.pushItems(res);
if (this.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
if (res.length === 0 || this.noPaging) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
}
this.error.value = false;
this.fetching.value = false;
}, err => {
this.error.value = true; this.error.value = true;
this.fetching.value = false; this.fetching.value = false;
}); return null;
})) as T[] | null;
if (apiRes == null) {
return;
}
// 逆順で返ってくるので
if ((this.initialId || this.initialDate) && this.initialDirection === 'newer') {
apiRes.reverse();
}
for (let i = 0; i < apiRes.length; i++) {
const item = apiRes[i];
if (i === 3) item._shouldInsertAd_ = true;
}
this.pushItems(apiRes);
if (this.canFetchDetection === 'limit') {
if (apiRes.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
if (apiRes.length === 0 || this.noPaging) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
}
this.error.value = false;
this.fetching.value = false;
} }
public reload(): Promise<void> { public reload(): Promise<void> {
@ -187,7 +245,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
public async fetchOlder(): Promise<void> { public async fetchOlder(): Promise<void> {
if (!this.canFetchOlder.value || this.fetching.value || this.fetchingOlder.value || this.items.value.length === 0) return; if (!this.canFetchOlder.value || this.fetching.value || this.fetchingOlder.value || this.items.value.length === 0) return;
this.fetchingOlder.value = true; this.fetchingOlder.value = true;
await misskeyApi<T[]>(this.endpoint, {
const data: E['req'] = {
...(typeof this.params === 'function' ? this.params() : this.params), ...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}), ...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
@ -197,37 +256,46 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
} : { } : {
untilId: this.getOldestId(), untilId: this.getOldestId(),
}), }),
}).then(res => { };
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 10) item._shouldInsertAd_ = true;
}
this.pushItems(res); const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => {
return null;
})) as T[] | null;
if (this.canFetchDetection === 'limit') { this.fetchingOlder.value = false;
if (res.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false; if (apiRes == null) {
} else { return;
this.canFetchOlder.value = true; }
}
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) { for (let i = 0; i < apiRes.length; i++) {
if (res.length === 0) { const item = apiRes[i];
this.canFetchOlder.value = false; if (i === 10) item._shouldInsertAd_ = true;
} else { }
this.canFetchOlder.value = true;
} this.pushItems(apiRes);
if (this.canFetchDetection === 'limit') {
if (apiRes.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
} }
}).finally(() => { } else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
this.fetchingOlder.value = false; if (apiRes.length === 0) {
}); this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
}
} }
public async fetchNewer(options: { public async fetchNewer(options: {
toQueue?: boolean; toQueue?: boolean;
} = {}): Promise<void> { } = {}): Promise<void> {
this.fetchingNewer.value = true; this.fetchingNewer.value = true;
await misskeyApi<T[]>(this.endpoint, {
const data: E['req'] = {
...(typeof this.params === 'function' ? this.params() : this.params), ...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}), ...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
@ -237,25 +305,29 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
} : { } : {
sinceId: this.getNewestId(), sinceId: this.getNewestId(),
}), }),
}).then(res => { };
if (res.length === 0) return; // これやらないと余計なre-renderが走る
if (options.toQueue) { const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => {
this.aheadQueue.unshift(...res.toReversed()); return null;
if (this.aheadQueue.length > MAX_QUEUE_ITEMS) { })) as T[] | null;
this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS);
} this.fetchingNewer.value = false;
this.queuedAheadItemsCount.value = this.aheadQueue.length;
} else { if (apiRes == null || apiRes.length === 0) return; // これやらないと余計なre-renderが走る
if (this.order.value === 'oldest') {
this.pushItems(res); if (options.toQueue) {
} else { this.aheadQueue.unshift(...apiRes.toReversed());
this.unshiftItems(res.toReversed()); if (this.aheadQueue.length > MAX_QUEUE_ITEMS) {
} this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS);
} }
}).finally(() => { this.queuedAheadItemsCount.value = this.aheadQueue.length;
this.fetchingNewer.value = false; } else {
}); if (this.order.value === 'oldest') {
this.pushItems(apiRes);
} else {
this.unshiftItems(apiRes.toReversed());
}
}
} }
public trim(trigger = true): void { public trim(trigger = true): void {
@ -309,13 +381,13 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
} }
} }
public updateItem(id: string, updator: (item: T) => T): void { public updateItem(id: string, updater: (item: T) => T): void {
// TODO: queueのも更新 // TODO: queueのも更新
const index = this.items.value.findIndex(x => x.id === id); const index = this.items.value.findIndex(x => x.id === id);
if (index !== -1) { if (index !== -1) {
const item = this.items.value[index]!; const item = this.items.value[index]!;
this.items.value[index] = updator(item); this.items.value[index] = updater(item);
if (this.useShallowRef) triggerRef(this.items); if (this.useShallowRef) triggerRef(this.items);
} }
} }

View file

@ -35,6 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { deepClone } from '@/utility/clone.js';
const props = defineProps<{ const props = defineProps<{
activity: { activity: {
total: number; total: number;
@ -44,10 +46,21 @@ const props = defineProps<{
}[] }[]
}>(); }>();
for (const d of props.activity) { const activity = deepClone(props.activity).map(d => ({
d.total = d.notes + d.replies + d.renotes; ...d,
} total: d.notes + d.replies + d.renotes,
const peak = Math.max(...props.activity.map(d => d.total)); x: 0,
date: {
year: 0,
month: 0,
day: 0,
weekday: 0,
},
v: 0,
color: '',
}));
const peak = Math.max(...activity.map(d => d.total));
const now = new Date(); const now = new Date();
const year = now.getFullYear(); const year = now.getFullYear();
@ -55,7 +68,7 @@ const month = now.getMonth();
const day = now.getDate(); const day = now.getDate();
let x = 20; let x = 20;
props.activity.slice().forEach((d, i) => { activity.slice().forEach((d, i) => {
d.x = x; d.x = x;
const date = new Date(year, month, day - i); const date = new Date(year, month, day - i);

View file

@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
import XCalendar from './WidgetActivity.calendar.vue'; import XCalendar from './WidgetActivity.calendar.vue';
import XChart from './WidgetActivity.chart.vue'; import XChart from './WidgetActivity.chart.vue';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApiGet } from '@/utility/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { $i } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const $i = ensureSignin();
const name = 'activity'; const name = 'activity';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
view: { view: {
type: 'number' as const, type: 'number',
default: 0, default: 0,
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
const name = 'ai'; const name = 'ai';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -42,6 +42,8 @@ const touched = () => {
}; };
const onMousemove = (ev: MouseEvent) => { const onMousemove = (ev: MouseEvent) => {
if (!live2d.value || !live2d.value.contentWindow) return;
const iframeRect = live2d.value.getBoundingClientRect(); const iframeRect = live2d.value.getBoundingClientRect();
live2d.value.contentWindow.postMessage({ live2d.value.contentWindow.postMessage({
type: 'moveCursor', type: 'moveCursor',

View file

@ -23,7 +23,7 @@ import { ref } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@ -35,16 +35,16 @@ const name = 'aiscript';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: '(1 + 1)', default: '(1 + 1)',
hidden: true, hidden: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -106,7 +106,7 @@ const run = async () => {
} catch (err) { } catch (err) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err, text: err instanceof Error ? err.message : String(err),
}); });
} }
}; };

View file

@ -18,7 +18,7 @@ import type { Ref } from 'vue';
import { Interpreter, Parser } from '@syuilo/aiscript'; import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -31,15 +31,15 @@ const name = 'aiscriptApp';
const widgetPropsDef = { const widgetPropsDef = {
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: '', default: '',
}, },
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -92,7 +92,7 @@ async function run() {
os.alert({ os.alert({
type: 'error', type: 'error',
title: 'AiScript Error', title: 'AiScript Error',
text: err.message, text: err instanceof Error ? err.message : String(err),
}); });
} }
} }

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.bdayFRoot"> <div :class="$style.bdayFRoot">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-else-if="users.length > 0" :class="$style.bdayFGrid"> <div v-else-if="users.length > 0" :class="$style.bdayFGrid">
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar> <MkAvatar v-for="user in users" :key="user.id" :user="user.followee!" link preview></MkAvatar>
</div> </div>
<div v-else :class="$style.bdayFFallback"> <div v-else :class="$style.bdayFFallback">
<MkResult type="empty"/> <MkResult type="empty"/>
@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings;
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { Interpreter, Parser } from '@syuilo/aiscript'; import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -25,19 +25,19 @@ const name = 'button';
const widgetPropsDef = { const widgetPropsDef = {
label: { label: {
type: 'string' as const, type: 'string',
default: 'BUTTON', default: 'BUTTON',
}, },
colored: { colored: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
script: { script: {
type: 'string' as const, type: 'string',
multiline: true, multiline: true,
default: 'Mk:dialog("hello" "world")', default: 'Mk:dialog("hello" "world")',
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -81,7 +81,7 @@ const run = async () => {
} catch (err) { } catch (err) {
os.alert({ os.alert({
type: 'error', type: 'error',
text: err, text: err instanceof Error ? err.message : String(err),
}); });
} }
}; };

View file

@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
@ -49,10 +49,10 @@ const name = 'calendar';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { } from 'vue'; import { } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkChatHistories from '@/components/MkChatHistories.vue'; import MkChatHistories from '@/components/MkChatHistories.vue';
@ -28,10 +28,10 @@ const name = 'chat';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import MkClickerGame from '@/components/MkClickerGame.vue'; import MkClickerGame from '@/components/MkClickerGame.vue';
@ -22,10 +22,10 @@ const name = 'clicker';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue'; import { computed } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import MkAnalogClock from '@/components/MkAnalogClock.vue'; import MkAnalogClock from '@/components/MkAnalogClock.vue';
import MkDigitalClock from '@/components/MkDigitalClock.vue'; import MkDigitalClock from '@/components/MkDigitalClock.vue';
@ -43,76 +43,92 @@ const name = 'clock';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
size: { size: {
type: 'radio' as const, type: 'radio',
default: 'medium', default: 'medium',
options: [{ options: [{
value: 'small', label: i18n.ts.small, value: 'small' as const,
label: i18n.ts.small,
}, { }, {
value: 'medium', label: i18n.ts.medium, value: 'medium' as const,
label: i18n.ts.medium,
}, { }, {
value: 'large', label: i18n.ts.large, value: 'large' as const,
label: i18n.ts.large,
}], }],
}, },
thickness: { thickness: {
type: 'radio' as const, type: 'radio',
default: 0.2, default: 0.2,
options: [{ options: [{
value: 0.1, label: 'thin', value: 0.1 as const,
label: 'thin',
}, { }, {
value: 0.2, label: 'medium', value: 0.2 as const,
label: 'medium',
}, { }, {
value: 0.3, label: 'thick', value: 0.3 as const,
label: 'thick',
}], }],
}, },
graduations: { graduations: {
type: 'radio' as const, type: 'radio',
default: 'numbers', default: 'numbers',
options: [{ options: [{
value: 'none', label: 'None', value: 'none' as const,
label: 'None',
}, { }, {
value: 'dots', label: 'Dots', value: 'dots' as const,
label: 'Dots',
}, { }, {
value: 'numbers', label: 'Numbers', value: 'numbers' as const,
label: 'Numbers',
}], }],
}, },
fadeGraduations: { fadeGraduations: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
sAnimation: { sAnimation: {
type: 'radio' as const, type: 'radio',
default: 'elastic', default: 'elastic',
options: [{ options: [{
value: 'none', label: 'None', value: 'none' as const,
label: 'None',
}, { }, {
value: 'elastic', label: 'Elastic', value: 'elastic' as const,
label: 'Elastic',
}, { }, {
value: 'easeOut', label: 'Ease out', value: 'easeOut' as const,
label: 'Ease out',
}], }],
}, },
twentyFour: { twentyFour: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
label: { label: {
type: 'radio' as const, type: 'radio',
default: 'none', default: 'none',
options: [{ options: [{
value: 'none', label: 'None', value: 'none' as const,
label: 'None',
}, { }, {
value: 'time', label: 'Time', value: 'time' as const,
label: 'Time',
}, { }, {
value: 'tz', label: 'TZ', value: 'tz' as const,
label: 'TZ',
}, { }, {
value: 'timeAndTz', label: 'Time + TZ', value: 'timeAndTz' as const,
label: 'Time + TZ',
}], }],
}, },
timezone: { timezone: {
type: 'enum' as const, type: 'enum',
default: null, default: null,
enum: [...timezones.map((tz) => ({ enum: [...timezones.map((tz) => ({
label: tz.name, label: tz.name,
@ -122,7 +138,7 @@ const widgetPropsDef = {
value: null, value: null,
}], }],
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue'; import { computed } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { timezones } from '@/utility/timezones.js'; import { timezones } from '@/utility/timezones.js';
import MkDigitalClock from '@/components/MkDigitalClock.vue'; import MkDigitalClock from '@/components/MkDigitalClock.vue';
@ -25,24 +25,24 @@ const name = 'digitalClock';
const widgetPropsDef = { const widgetPropsDef = {
transparent: { transparent: {
type: 'boolean' as const, type: 'boolean',
default: false, default: false,
}, },
fontSize: { fontSize: {
type: 'number' as const, type: 'number',
default: 1.5, default: 1.5,
step: 0.1, step: 0.1,
}, },
showMs: { showMs: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
showLabel: { showLabel: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
timezone: { timezone: {
type: 'enum' as const, type: 'enum',
default: null, default: null,
enum: [...timezones.map((tz) => ({ enum: [...timezones.map((tz) => ({
label: tz.name, label: tz.name,
@ -52,7 +52,7 @@ const widgetPropsDef = {
value: null, value: null,
}], }],
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-federation class="mkw-federation">
<template #icon><i class="ti ti-whirl"></i></template> <template #icon><i class="ti ti-whirl"></i></template>
<template #header>{{ i18n.ts._widgets.federation }}</template> <template #header>{{ i18n.ts._widgets.federation }}</template>
@ -30,7 +30,7 @@ import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue'; import MkMiniChart from '@/components/MkMiniChart.vue';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
@ -42,10 +42,10 @@ const name = 'federation';
const widgetPropsDef = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean',
default: true, default: true,
}, },
}; } satisfies FormWithDefault;
type WidgetProps = GetFormResultType<typeof widgetPropsDef>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;

Some files were not shown because too many files have changed in this diff Show more