mirror of
https://github.com/misskey-dev/misskey
synced 2025-07-09 21:22:51 +02:00
Compare commits
40 commits
2025.6.4-a
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
c165749a29 | ||
|
c4fdf5a47c | ||
|
288f0abeac | ||
|
89ed8be8ff | ||
|
a8abb03d17 | ||
|
c2a01551a7 | ||
|
553ccff77c | ||
|
9dddc84750 | ||
|
004cfd5e4b | ||
|
40a35968f0 | ||
|
e6ec15e397 | ||
|
8430256f22 | ||
|
abde15979b | ||
|
f128682200 | ||
|
cc4cdd1ec0 | ||
|
075df75afa | ||
|
64eb338d65 | ||
|
d986da745b | ||
|
50f5b29290 | ||
|
a460bb7913 | ||
|
7cf1eccd04 | ||
|
73397e1b7e | ||
|
bf17092b41 | ||
|
6d1018f42b | ||
|
7667011266 | ||
|
a45ccc18b4 | ||
|
c29a4d9503 | ||
|
5caf2b27cf | ||
|
dd87d26bdc | ||
|
b7a6301c2e | ||
|
73e8d950df | ||
|
45033974f7 | ||
|
7acfbc23d6 | ||
|
a9a746edce | ||
|
179d990c39 | ||
|
7c44881ca8 | ||
|
ccbc4cffaa | ||
|
706244925d | ||
|
09a5e4b10a | ||
|
c48acad04b |
128 changed files with 1485 additions and 878 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -6,8 +6,12 @@
|
|||
"files.associations": {
|
||||
"*.test.ts": "typescript"
|
||||
},
|
||||
"jest.jestCommandLine": "pnpm run jest",
|
||||
"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": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,20 +1,29 @@
|
|||
## 2025.6.4
|
||||
## 2025.7.0
|
||||
|
||||
### General
|
||||
- Feat: ノートの下書き機能
|
||||
- Feat: クリップ内でノートを検索できるように
|
||||
- Feat: Playを検索できるように
|
||||
- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように
|
||||
|
||||
### Client
|
||||
- Feat: モデログを検索できるように
|
||||
- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように
|
||||
- Enhance: ファイルアップロード前にキャプション設定を行えるように
|
||||
- Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように
|
||||
- Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善
|
||||
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように
|
||||
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように
|
||||
- Enhance: レンダリングパフォーマンスの向上
|
||||
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
|
||||
- Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正
|
||||
- Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正
|
||||
- Fix: 非ログイン時のハイライトノートの画像がCWの有無を考慮せず表示される問題を修正
|
||||
- Fix: レンジ選択・ドロップダウンにて、操作を無効にすべきところで無効にならない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に
|
||||
- Enhance: メールの送信者としてサーバー名を表示するように (サーバー名が設定されている場合)
|
||||
- Fix: ジョブキューのProgressの値を正しく計算する
|
||||
|
||||
|
||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "Esborrany "
|
|||
_drafts:
|
||||
select: "Seleccionar esborrany"
|
||||
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"
|
||||
deleteAreYouSure: "Vols esborrar els esborranys?"
|
||||
noDrafts: "No hi ha esborranys"
|
||||
|
|
|
@ -3182,7 +3182,6 @@ drafts: "Drafts"
|
|||
_drafts:
|
||||
select: "Select Draft"
|
||||
cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded."
|
||||
cannotCreateDraftOfRenote: "You cannot create a draft of a renote."
|
||||
delete: "Delete Draft"
|
||||
deleteAreYouSure: "Delete draft?"
|
||||
noDrafts: "No drafts"
|
||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "Borrador"
|
|||
_drafts:
|
||||
select: "Seleccionar borradores"
|
||||
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"
|
||||
deleteAreYouSure: "¿Quieres borrar el borrador?"
|
||||
noDrafts: "No hay borradores disponibles."
|
||||
|
|
|
@ -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>."
|
||||
monthAndDay: "{day} {month}"
|
||||
search: "Penelusuran"
|
||||
reset: "Reset"
|
||||
notifications: "Notifikasi"
|
||||
username: "Nama Pengguna"
|
||||
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"
|
||||
fetchingAsApObject: "Mengambil data dari Fediverse..."
|
||||
ok: "OK"
|
||||
|
@ -45,6 +49,7 @@ pin: "Sematkan ke profil"
|
|||
unpin: "Lepas sematan dari profil"
|
||||
copyContent: "Salin konten"
|
||||
copyLink: "Salin tautan"
|
||||
copyRemoteLink: "Salin tautan jarak jauh"
|
||||
copyLinkRenote: "Salin tautan renote"
|
||||
delete: "Hapus"
|
||||
deleteAndEdit: "Hapus dan sunting"
|
||||
|
@ -212,8 +217,10 @@ perDay: "per Hari"
|
|||
stopActivityDelivery: "Berhenti mengirim aktivitas"
|
||||
blockThisInstance: "Blokir instansi ini"
|
||||
silenceThisInstance: "Senyapkan instansi ini"
|
||||
mediaSilenceThisInstance: "Server media senyap"
|
||||
operations: "Tindakan"
|
||||
software: "Perangkat lunak"
|
||||
softwareName: "Nama Perangkat Lunak"
|
||||
version: "Versi"
|
||||
metadata: "Metadata"
|
||||
withNFiles: "{n} berkas"
|
||||
|
@ -1040,7 +1047,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi
|
|||
disableFederationOk: "Matikan federasi"
|
||||
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
|
||||
emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
|
||||
postToTheChannel: "Catat ke kanal"
|
||||
postToTheChannel: "Buat Catatan ke Kanal"
|
||||
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
|
||||
reactionAcceptance: "Penerimaan reaksi"
|
||||
likeOnly: "Hanya suka"
|
||||
|
@ -2400,7 +2407,7 @@ _deck:
|
|||
main: "Utama"
|
||||
widgets: "Widget"
|
||||
notifications: "Notifikasi"
|
||||
tl: "Lini masa"
|
||||
tl: "Beranda"
|
||||
antenna: "Antena"
|
||||
list: "Daftar"
|
||||
channel: "Kanal"
|
||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -10890,6 +10890,10 @@ export interface Locale extends ILocale {
|
|||
* 添付されているノート
|
||||
*/
|
||||
"attachedNotes": string;
|
||||
/**
|
||||
* 利用
|
||||
*/
|
||||
"usage": string;
|
||||
/**
|
||||
* このページは、このファイルをアップロードしたユーザーしか閲覧できません。
|
||||
*/
|
||||
|
@ -12270,9 +12274,9 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"cannotCreateDraftAnymore": string;
|
||||
/**
|
||||
* リノートの下書きは作成できません。
|
||||
* この内容では下書きを作成できません。
|
||||
*/
|
||||
"cannotCreateDraftOfRenote": string;
|
||||
"cannotCreateDraft": string;
|
||||
/**
|
||||
* 下書きを削除
|
||||
*/
|
||||
|
|
|
@ -2885,6 +2885,7 @@ _fileViewer:
|
|||
url: "URL"
|
||||
uploadedAt: "追加日"
|
||||
attachedNotes: "添付されているノート"
|
||||
usage: "利用"
|
||||
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
|
||||
|
||||
_externalResourceInstaller:
|
||||
|
@ -3288,7 +3289,7 @@ drafts: "下書き"
|
|||
_drafts:
|
||||
select: "下書きを選択"
|
||||
cannotCreateDraftAnymore: "下書きの作成可能数を超えています。"
|
||||
cannotCreateDraftOfRenote: "リノートの下書きは作成できません。"
|
||||
cannotCreateDraft: "この内容では下書きを作成できません。"
|
||||
delete: "下書きを削除"
|
||||
deleteAreYouSure: "下書きを削除しますか?"
|
||||
noDrafts: "下書きはありません"
|
||||
|
|
|
@ -3182,7 +3182,6 @@ drafts: "초안"
|
|||
_drafts:
|
||||
select: "초안 선택"
|
||||
cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다."
|
||||
cannotCreateDraftOfRenote: "리노트 초안은 작성할 수 없습니다."
|
||||
delete: "초안 삭제\n"
|
||||
deleteAreYouSure: "초안을 삭제하시겠습니까?"
|
||||
noDrafts: "초안 없음\n"
|
||||
|
|
|
@ -3182,6 +3182,5 @@ drafts: "Rascunhos"
|
|||
_drafts:
|
||||
select: "Selecionar Rascunho"
|
||||
cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido."
|
||||
cannotCreateDraftOfRenote: "Você não pode criar o rascunho de uma repostagem."
|
||||
delete: "Excluir Rascunho"
|
||||
restore: "Redefinir"
|
||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "草稿"
|
|||
_drafts:
|
||||
select: "选择草稿"
|
||||
cannotCreateDraftAnymore: "已超过可创建的草稿数量。"
|
||||
cannotCreateDraftOfRenote: "无法创建转帖草稿。"
|
||||
cannotCreateDraft: "此内容无法创建草稿。"
|
||||
delete: "删除草稿"
|
||||
deleteAreYouSure: "要删除草稿吗?"
|
||||
noDrafts: "没有草稿"
|
||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "草稿\n"
|
|||
_drafts:
|
||||
select: "選擇草槁"
|
||||
cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n"
|
||||
cannotCreateDraftOfRenote: "無法建立轉發的草稿。\n"
|
||||
cannotCreateDraft: "無法以此內容建立草稿。\n"
|
||||
delete: "刪除草稿"
|
||||
deleteAreYouSure: "確定要刪除草稿嗎?\n"
|
||||
noDrafts: "沒有草稿。\n"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.6.4-alpha.3",
|
||||
"version": "2025.7.0-beta.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
15
packages/backend/migration/1750729939704-FixAvatarUrl.js
Normal file
15
packages/backend/migration/1750729939704-FixAvatarUrl.js
Normal 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)`);
|
||||
}
|
||||
}
|
|
@ -145,7 +145,10 @@ export class EmailService {
|
|||
try {
|
||||
// TODO: htmlサニタイズ
|
||||
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,
|
||||
subject: subject,
|
||||
text: text,
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
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
|
||||
|
@ -15,6 +18,11 @@ export class FlashService {
|
|||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -37,4 +45,43 @@ export class FlashService {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ export class MiUser {
|
|||
|
||||
// avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
length: 1024, nullable: true,
|
||||
})
|
||||
public avatarUrl: string | null;
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export const packedFlashSchema = {
|
|||
},
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
|
|
|
@ -168,6 +168,7 @@ export * as 'clips/update' from './endpoints/clips/update.js';
|
|||
export * as 'drive' from './endpoints/drive.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-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/create' from './endpoints/drive/files/create.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/unlike' from './endpoints/flash/unlike.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/delete' from './endpoints/following/delete.js';
|
||||
export * as 'following/invalidate' from './endpoints/following/invalidate.js';
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
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 => {
|
||||
qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 { DI } from '@/di-symbols.js';
|
||||
import { FlashService } from '@/core/FlashService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'flash'],
|
||||
|
@ -46,6 +45,7 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -53,20 +53,18 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private flashLikeEntityService: FlashLikeEntityService,
|
||||
private queryService: QueryService,
|
||||
private flashService: FlashService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('like.userId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('like.flash', 'flash');
|
||||
|
||||
const likes = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
const likes = await this.flashService.myLikes(me.id, {
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
sinceDate: ps.sinceDate,
|
||||
untilDate: ps.untilDate,
|
||||
limit: ps.limit,
|
||||
search: ps.search,
|
||||
});
|
||||
|
||||
return this.flashLikeEntityService.packMany(likes, me);
|
||||
});
|
||||
|
|
59
packages/backend/src/server/api/endpoints/flash/search.ts
Normal file
59
packages/backend/src/server/api/endpoints/flash/search.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -7,9 +7,10 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FlashService } from '@/core/FlashService.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 { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
|
||||
describe('FlashService', () => {
|
||||
let app: TestingModule;
|
||||
|
@ -18,6 +19,7 @@ describe('FlashService', () => {
|
|||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let flashsRepository: FlashsRepository;
|
||||
let flashLikesRepository: FlashLikesRepository;
|
||||
let usersRepository: UsersRepository;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let idService: IdService;
|
||||
|
@ -65,6 +67,7 @@ describe('FlashService', () => {
|
|||
app = await Test.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
CoreModule,
|
||||
],
|
||||
providers: [
|
||||
FlashService,
|
||||
|
@ -75,6 +78,7 @@ describe('FlashService', () => {
|
|||
service = app.get(FlashService);
|
||||
|
||||
flashsRepository = app.get(DI.flashsRepository);
|
||||
flashLikesRepository = app.get(DI.flashLikesRepository);
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
idService = app.get(IdService);
|
||||
|
@ -88,6 +92,7 @@ describe('FlashService', () => {
|
|||
await usersRepository.createQueryBuilder().delete().execute();
|
||||
await userProfilesRepository.createQueryBuilder().delete().execute();
|
||||
await flashsRepository.createQueryBuilder().delete().execute();
|
||||
await flashLikesRepository.createQueryBuilder().delete().execute();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
|
@ -21,73 +21,73 @@ describe('ReactionService', () => {
|
|||
});
|
||||
|
||||
describe('normalize', () => {
|
||||
test('絵文字リアクションはそのまま', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('👍'), '👍');
|
||||
assert.strictEqual(await reactionService.normalize('🍅'), '🍅');
|
||||
test('絵文字リアクションはそのまま', () => {
|
||||
assert.strictEqual(reactionService.normalize('👍'), '👍');
|
||||
assert.strictEqual(reactionService.normalize('🍅'), '🍅');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する pudding', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('pudding'), '🍮');
|
||||
test('既存のリアクションは絵文字化する pudding', () => {
|
||||
assert.strictEqual(reactionService.normalize('pudding'), '🍮');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する like', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('like'), '👍');
|
||||
test('既存のリアクションは絵文字化する like', () => {
|
||||
assert.strictEqual(reactionService.normalize('like'), '👍');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する love', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('love'), '❤');
|
||||
test('既存のリアクションは絵文字化する love', () => {
|
||||
assert.strictEqual(reactionService.normalize('love'), '❤');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する laugh', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('laugh'), '😆');
|
||||
test('既存のリアクションは絵文字化する laugh', () => {
|
||||
assert.strictEqual(reactionService.normalize('laugh'), '😆');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する hmm', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('hmm'), '🤔');
|
||||
test('既存のリアクションは絵文字化する hmm', () => {
|
||||
assert.strictEqual(reactionService.normalize('hmm'), '🤔');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する surprise', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('surprise'), '😮');
|
||||
test('既存のリアクションは絵文字化する surprise', () => {
|
||||
assert.strictEqual(reactionService.normalize('surprise'), '😮');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する congrats', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('congrats'), '🎉');
|
||||
test('既存のリアクションは絵文字化する congrats', () => {
|
||||
assert.strictEqual(reactionService.normalize('congrats'), '🎉');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する angry', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('angry'), '💢');
|
||||
test('既存のリアクションは絵文字化する angry', () => {
|
||||
assert.strictEqual(reactionService.normalize('angry'), '💢');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する confused', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('confused'), '😥');
|
||||
test('既存のリアクションは絵文字化する confused', () => {
|
||||
assert.strictEqual(reactionService.normalize('confused'), '😥');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する rip', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('rip'), '😇');
|
||||
test('既存のリアクションは絵文字化する rip', () => {
|
||||
assert.strictEqual(reactionService.normalize('rip'), '😇');
|
||||
});
|
||||
|
||||
test('既存のリアクションは絵文字化する star', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('star'), '⭐');
|
||||
test('既存のリアクションは絵文字化する star', () => {
|
||||
assert.strictEqual(reactionService.normalize('star'), '⭐');
|
||||
});
|
||||
|
||||
test('異体字セレクタ除去', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('㊗️'), '㊗');
|
||||
test('異体字セレクタ除去', () => {
|
||||
assert.strictEqual(reactionService.normalize('㊗️'), '㊗');
|
||||
});
|
||||
|
||||
test('異体字セレクタ除去 必要なし', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('㊗'), '㊗');
|
||||
test('異体字セレクタ除去 必要なし', () => {
|
||||
assert.strictEqual(reactionService.normalize('㊗'), '㊗');
|
||||
});
|
||||
|
||||
test('fallback - null', async () => {
|
||||
assert.strictEqual(await reactionService.normalize(null), '❤');
|
||||
test('fallback - null', () => {
|
||||
assert.strictEqual(reactionService.normalize(null), '❤');
|
||||
});
|
||||
|
||||
test('fallback - empty', async () => {
|
||||
assert.strictEqual(await reactionService.normalize(''), '❤');
|
||||
test('fallback - empty', () => {
|
||||
assert.strictEqual(reactionService.normalize(''), '❤');
|
||||
});
|
||||
|
||||
test('fallback - unknown', async () => {
|
||||
assert.strictEqual(await reactionService.normalize('unknown'), '❤');
|
||||
test('fallback - unknown', () => {
|
||||
assert.strictEqual(reactionService.normalize('unknown'), '❤');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,15 +14,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<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 MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: Paginator;
|
||||
paginator: IPaginator;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => any;
|
||||
extractor?: (item: any) => Misskey.entities.Channel;
|
||||
}>(), {
|
||||
extractor: (item) => item,
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MkDateSeparatedList from './MkDateSeparatedList.vue';
|
||||
void MkDateSeparatedList;
|
|
@ -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>
|
||||
|
|
@ -684,13 +684,8 @@ defineExpose({
|
|||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .group {
|
||||
&:not(.index) {
|
||||
padding: 4px 0 8px 0;
|
||||
|
|
|
@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { instanceName as localInstanceName } from '@@/js/config.js';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { instance as localInstance } from '@/instance.js';
|
||||
|
@ -44,33 +43,10 @@ const faviconUrl = computed(() => {
|
|||
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 themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
|
||||
const colors = getTickerColors(themeColor);
|
||||
return {
|
||||
background: `linear-gradient(90deg, ${colors.bg}, ${colors.bg}00)`,
|
||||
color: colors.fg,
|
||||
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -84,6 +60,7 @@ $height: 2ex;
|
|||
height: $height;
|
||||
border-radius: 4px 0 0 4px;
|
||||
overflow: clip;
|
||||
color: #fff;
|
||||
|
||||
// text-shadowは重いから使うな
|
||||
|
||||
|
@ -106,5 +83,10 @@ $height: 2ex;
|
|||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
|
||||
// text-shadowは重いから使うな
|
||||
color: var(--MI_THEME-fg);
|
||||
-webkit-text-stroke: var(--MI_THEME-panel) .225em;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -729,7 +729,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
}
|
||||
|
||||
&:hover > .article > .main > .footer > .footerButton {
|
||||
opacity: 1;
|
||||
color: var(--MI_THEME-fg);
|
||||
}
|
||||
|
||||
&.showActionsOnlyHover {
|
||||
|
@ -1004,7 +1004,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
.footerButton {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
color: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 70%); // opacityなど不透明度で表現するとレンダリングパフォーマンスに影響するので通常の色の混合で代用
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 28px;
|
||||
|
@ -1018,7 +1018,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
.footerButtonCount {
|
||||
display: inline;
|
||||
margin: 0 0 0 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@container (max-width: 580px) {
|
||||
|
|
|
@ -31,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T extends Paginator">
|
||||
import type { Paginator } from '@/utility/paginator.js';
|
||||
<script lang="ts" setup generic="T extends IPaginator<Misskey.entities.Note>">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { IPaginator } from '@/utility/paginator.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<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'">
|
||||
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
|
||||
{{ i18n.ts.loadMore }}
|
||||
|
@ -44,11 +44,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</component>
|
||||
</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 { onMounted, watch } from 'vue';
|
||||
import { onMounted, watch, unref } 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 { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
@ -95,7 +95,7 @@ if (props.paginator.computedParams) {
|
|||
|
||||
defineSlots<{
|
||||
empty: () => void;
|
||||
default: (props: { items: I }) => void;
|
||||
default: (props: { items: UnwrapRef<T['items']> }) => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T extends Paginator">
|
||||
<script lang="ts" setup generic="T extends IPaginator">
|
||||
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 { i18n } from '@/i18n.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
|
|
@ -693,9 +693,7 @@ async function onPaste(ev: ClipboardEvent) {
|
|||
}
|
||||
if (pastedFiles.length > 0) {
|
||||
ev.preventDefault();
|
||||
os.launchUploader(pastedFiles, {}).then(driveFiles => {
|
||||
files.value.push(...driveFiles);
|
||||
});
|
||||
uploader.addFiles(pastedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -730,9 +728,7 @@ async function onPaste(ev: ClipboardEvent) {
|
|||
|
||||
const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
|
||||
const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
|
||||
os.launchUploader([file], {}).then(driveFiles => {
|
||||
files.value.push(...driveFiles);
|
||||
});
|
||||
uploader.addFiles([file]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -776,9 +772,7 @@ function onDrop(ev: DragEvent): void {
|
|||
// ファイルだったら
|
||||
if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
|
||||
ev.preventDefault();
|
||||
os.launchUploader(Array.from(ev.dataTransfer.files), {}).then(driveFiles => {
|
||||
files.value.push(...driveFiles);
|
||||
});
|
||||
uploader.addFiles(Array.from(ev.dataTransfer.files));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1195,7 +1189,7 @@ function showDraftMenu(ev: MouseEvent) {
|
|||
if (!canSaveAsServerDraft.value) {
|
||||
return os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._drafts.cannotCreateDraftOfRenote,
|
||||
text: i18n.ts._drafts.cannotCreateDraft,
|
||||
});
|
||||
}
|
||||
saveServerDraft();
|
||||
|
|
|
@ -56,10 +56,12 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
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;
|
||||
} else {
|
||||
} else if ('screenY' in event) {
|
||||
return event.screenY;
|
||||
} else {
|
||||
return 0; // TSを黙らせるため
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="label">
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
<div v-adaptive-border class="body">
|
||||
<div v-adaptive-border class="body" :class="{ 'disabled': disabled }">
|
||||
<slot name="prefix"></slot>
|
||||
<div ref="containerEl" class="container">
|
||||
<div class="track">
|
||||
|
@ -180,6 +180,8 @@ function onMouseenter() {
|
|||
let lastClickTime: number | null = null;
|
||||
|
||||
function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||
if (props.disabled) return; // Prevent interaction if disabled
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
tooltipForDragShowing.value = true;
|
||||
|
@ -292,6 +294,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
|||
border: solid 1px var(--MI_THEME-panel);
|
||||
border-radius: 6px;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
> .container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
|
|
@ -24,6 +24,7 @@ const elRef = useTemplateRef('elRef');
|
|||
|
||||
if (props.withTooltip) {
|
||||
useTooltip(elRef, (showing) => {
|
||||
if (elRef.value == null) return;
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
|
||||
showing,
|
||||
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
|
||||
|
|
|
@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
role: Misskey.entities.Role;
|
||||
forModeration: boolean;
|
||||
detailed: boolean;
|
||||
detailed?: boolean;
|
||||
}>(), {
|
||||
detailed: true,
|
||||
});
|
||||
|
|
|
@ -174,7 +174,7 @@ watch([modelValue, () => props.items], () => {
|
|||
}, { immediate: true, deep: true });
|
||||
|
||||
function show() {
|
||||
if (opening.value) return;
|
||||
if (opening.value || props.disabled || props.readonly) return;
|
||||
focus();
|
||||
|
||||
opening.value = true;
|
||||
|
|
|
@ -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 * as Misskey from 'misskey-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 { BasicTimelineType } from '@/timelines.js';
|
||||
import type { SoundStore } from '@/preferences/def.js';
|
||||
import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
|
@ -101,12 +103,12 @@ provide('inTimeline', true);
|
|||
provide('tl_withSensitive', computed(() => props.withSensitive));
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
let paginator: Paginator;
|
||||
let paginator: IPaginator<Misskey.entities.Note>;
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
paginator = markRaw(new Paginator('antennas/notes', {
|
||||
computedParams: computed(() => ({
|
||||
antennaId: props.antenna,
|
||||
antennaId: props.antenna!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
}));
|
||||
|
@ -160,21 +162,21 @@ if (props.src === 'antenna') {
|
|||
computedParams: computed(() => ({
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
listId: props.list!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
}));
|
||||
} else if (props.src === 'channel') {
|
||||
paginator = markRaw(new Paginator('channels/timeline', {
|
||||
computedParams: computed(() => ({
|
||||
channelId: props.channel,
|
||||
channelId: props.channel!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
}));
|
||||
} else if (props.src === 'role') {
|
||||
paginator = markRaw(new Paginator('roles/notes', {
|
||||
computedParams: computed(() => ({
|
||||
roleId: props.role,
|
||||
roleId: props.role!,
|
||||
})),
|
||||
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;
|
||||
|
||||
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
|
@ -236,7 +252,7 @@ if (!store.s.realtimeMode) {
|
|||
// TODO: 先頭のノートの作成日時が1日以上前であれば流速が遅いTLと見做してインターバルを通常より延ばす
|
||||
useInterval(async () => {
|
||||
paginator.fetchNewer({
|
||||
toQueue: !isTop(),
|
||||
toQueue: !isTop() || isPausingUpdate,
|
||||
});
|
||||
}, POLLING_INTERVAL, {
|
||||
immediate: false,
|
||||
|
@ -245,7 +261,7 @@ if (!store.s.realtimeMode) {
|
|||
|
||||
useGlobalEvent('notePosted', (note) => {
|
||||
paginator.fetchNewer({
|
||||
toQueue: !isTop(),
|
||||
toQueue: !isTop() || isPausingUpdate,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -256,17 +272,17 @@ useGlobalEvent('noteDeleted', (noteId) => {
|
|||
|
||||
function releaseQueue() {
|
||||
paginator.releaseQueue();
|
||||
scrollToTop(rootEl.value);
|
||||
scrollToTop(rootEl.value!);
|
||||
}
|
||||
|
||||
function prepend(note: Misskey.entities.Note) {
|
||||
function prepend(note: Misskey.entities.Note & MisskeyEntity) {
|
||||
adInsertionCounter++;
|
||||
|
||||
if (instance.notesPerOneAd > 0 && adInsertionCounter % instance.notesPerOneAd === 0) {
|
||||
note._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (isTop()) {
|
||||
if (isTop() && !isPausingUpdate) {
|
||||
paginator.prepend(note);
|
||||
} else {
|
||||
paginator.enqueue(note);
|
||||
|
@ -281,12 +297,13 @@ function prepend(note: Misskey.entities.Note) {
|
|||
}
|
||||
}
|
||||
|
||||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let connection: Misskey.IChannelConnection | null = null;
|
||||
let connection2: Misskey.IChannelConnection | null = null;
|
||||
|
||||
const stream = store.s.realtimeMode ? useStream() : null;
|
||||
|
||||
function connectChannel() {
|
||||
if (stream == null) return;
|
||||
if (props.src === 'antenna') {
|
||||
if (props.antenna == null) return;
|
||||
connection = stream.useChannel('antenna', {
|
||||
|
@ -507,7 +524,6 @@ defineExpose({
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
opacity: 0.75;
|
||||
padding: 8px 8px;
|
||||
margin: 0 auto;
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
|
|
|
@ -45,6 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-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 XNotification from '@/components/MkNotification.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 }); // ほんとはscrollendにしたいけどiosが非対応
|
||||
}
|
||||
}, { 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) {
|
||||
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
|
||||
if (isMuted || window.document.visibilityState === 'visible') {
|
||||
|
@ -101,7 +146,11 @@ function onNotification(notification) {
|
|||
}
|
||||
|
||||
if (!isMuted) {
|
||||
paginator.prepend(notification);
|
||||
if (isTop() && !isPausingUpdate) {
|
||||
paginator.prepend(notification);
|
||||
} else {
|
||||
paginator.enqueue(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +158,7 @@ function reload() {
|
|||
return paginator.reload();
|
||||
}
|
||||
|
||||
let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null;
|
||||
let connection: Misskey.IChannelConnection<Misskey.Channels['main']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
paginator.init();
|
||||
|
@ -186,7 +235,6 @@ defineExpose({
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
opacity: 0.75;
|
||||
padding: 8px 8px;
|
||||
margin: 0 auto;
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
|
|
|
@ -169,10 +169,6 @@ onUnmounted(() => {
|
|||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsInner {
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.left">
|
||||
<slot v-if="item.type === 'event'" name="left" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot>
|
||||
</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.centerPoint"></div>
|
||||
</div>
|
||||
|
@ -143,6 +143,22 @@ const items = computed<TlItem<T>[]>(() => {
|
|||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.first {
|
||||
.centerLine {
|
||||
height: 50%;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.last {
|
||||
.centerLine {
|
||||
height: 50%;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centerLine {
|
||||
|
|
|
@ -9,22 +9,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template #default="{ items }">
|
||||
<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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<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 MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: Paginator;
|
||||
paginator: IPaginator;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => any;
|
||||
extractor?: (item: any) => Misskey.entities.UserDetailed;
|
||||
}>(), {
|
||||
extractor: (item) => item,
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :paginator="pinnedUsersPaginator">
|
||||
<template #default="{ items }">
|
||||
<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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :paginator="popularUsersPaginator">
|
||||
<template #default="{ items }">
|
||||
<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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
@ -34,7 +34,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { markRaw } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
|
|
@ -194,10 +194,6 @@ onUnmounted(() => {
|
|||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsInner {
|
||||
|
|
|
@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { toUnicode as decodePunycode } from 'punycode.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 { useTooltip } from '@/composables/use-tooltip.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 {
|
||||
try {
|
||||
|
@ -94,7 +94,7 @@ const target = self ? null : '_blank';
|
|||
}
|
||||
|
||||
.schema {
|
||||
opacity: 0.5;
|
||||
color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||
}
|
||||
|
||||
.hostname {
|
||||
|
@ -102,11 +102,11 @@ const target = self ? null : '_blank';
|
|||
}
|
||||
|
||||
.pathname {
|
||||
opacity: 0.8;
|
||||
color: color(from currentcolor srgb r g b / 0.8); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||
}
|
||||
|
||||
.query {
|
||||
opacity: 0.5;
|
||||
color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||
}
|
||||
|
||||
.hash {
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
|||
import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||
import type { MenuItem } from '@/types/menu.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 MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
@ -837,7 +837,7 @@ export function launchUploader(
|
|||
options?: {
|
||||
folderId?: string | null;
|
||||
multiple?: boolean;
|
||||
features?: UploaderDialogFeatures;
|
||||
features?: UploaderFeatures;
|
||||
},
|
||||
): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise(async (res, rej) => {
|
||||
|
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
|
||||
<div class="_gaps_s">
|
||||
<MkInfo>
|
||||
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
|
||||
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name ?? host }) }}
|
||||
</MkInfo>
|
||||
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
|
@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
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 FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -414,6 +414,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
|
|||
const containerEl = useTemplateRef('containerEl');
|
||||
|
||||
function iconLoaded() {
|
||||
if (containerEl.value == null) return;
|
||||
const emojis = prefer.s.emojiPalettes[0].emojis;
|
||||
const containerWidth = containerEl.value.offsetWidth;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
|
@ -431,6 +432,7 @@ function iconLoaded() {
|
|||
}
|
||||
|
||||
function gravity() {
|
||||
if (containerEl.value == null) return;
|
||||
if (!easterEggReady) return;
|
||||
easterEggReady = false;
|
||||
easterEggEngine.value = physics(containerEl.value);
|
||||
|
|
38
packages/frontend/src/pages/admin-file.chat.vue
Normal file
38
packages/frontend/src/pages/admin-file.chat.vue
Normal 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>
|
|
@ -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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'notes' && info" class="_gaps_m">
|
||||
<XNotes :fileId="fileId"/>
|
||||
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
|
||||
<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 v-else-if="tab === 'ip' && info" class="_gaps_m">
|
||||
<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 { definePage } from '@/page.js';
|
||||
import { iAmAdmin, iAmModerator } from '@/i.js';
|
||||
import MkTabs from '@/components/MkTabs.vue';
|
||||
|
||||
const tab = ref('overview');
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
|
||||
const isSensitive = ref<boolean>(false);
|
||||
const usageTab = ref<'note' | 'chat'>('note');
|
||||
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
|
||||
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
fileId: string,
|
||||
|
@ -147,9 +161,9 @@ const headerTabs = computed(() => [{
|
|||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, iAmModerator ? {
|
||||
key: 'notes',
|
||||
title: i18n.ts._fileViewer.attachedNotes,
|
||||
icon: 'ti ti-pencil',
|
||||
key: 'usage',
|
||||
title: i18n.ts._fileViewer.usage,
|
||||
icon: 'ti ti-plus',
|
||||
} : null, iAmModerator ? {
|
||||
key: 'ip',
|
||||
title: 'IP',
|
||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</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>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.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 searchHost = ref('');
|
||||
const userId = ref('');
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :paginator="paginator">
|
||||
<template #default="{ items }">
|
||||
<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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, markRaw, ref, useTemplateRef } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -68,8 +69,8 @@ import MkInviteCode from '@/components/MkInviteCode.vue';
|
|||
import { definePage } from '@/page.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const type = ref('all');
|
||||
const sort = ref('+createdAt');
|
||||
const type = ref<NonNullable<Misskey.entities.AdminInviteListRequest['type']>>('all');
|
||||
const sort = ref<NonNullable<Misskey.entities.AdminInviteListRequest['sort']>>('+createdAt');
|
||||
|
||||
const paginator = markRaw(new Paginator('admin/invite/list', {
|
||||
limit: 10,
|
||||
|
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template #default="{ items }">
|
||||
<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">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
|
@ -76,12 +76,12 @@ const props = defineProps<{
|
|||
|
||||
const usersPaginator = markRaw(new Paginator('admin/roles/users', {
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
computedParams: computed(() => props.id ? ({
|
||||
roleId: props.id,
|
||||
})),
|
||||
}) : undefined),
|
||||
}));
|
||||
|
||||
const expandedItems = ref([]);
|
||||
const expandedItems = ref<string[]>([]);
|
||||
|
||||
const role = reactive(await misskeyApi('admin/roles/show', {
|
||||
roleId: props.id,
|
||||
|
@ -199,7 +199,7 @@ definePage(() => ({
|
|||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.userItem.userItemOpend {
|
||||
.userItem.userItemOpened {
|
||||
.chevron {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
</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 #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkPagination v-slot="{items}" :paginator="paginator">
|
||||
<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"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { definePage } from '@/page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
messageId?: string;
|
||||
messageId: string;
|
||||
}>();
|
||||
|
||||
const initializing = ref(true);
|
||||
|
|
|
@ -197,7 +197,7 @@ async function initialize() {
|
|||
connection.value.on('deleted', onDeleted);
|
||||
connection.value.on('react', onReact);
|
||||
connection.value.on('unreact', onUnreact);
|
||||
} else {
|
||||
} else if (props.roomId) {
|
||||
const [rResult, mResult] = await Promise.allSettled([
|
||||
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
|
||||
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
|
||||
|
|
|
@ -76,7 +76,8 @@ watch(() => props.clipId, async () => {
|
|||
clip.value = await misskeyApi('clips/show', {
|
||||
clipId: props.clipId,
|
||||
});
|
||||
favorited.value = clip.value.isFavorited;
|
||||
|
||||
favorited.value = clip.value!.isFavorited ?? false;
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.edit,
|
||||
handler: async (): Promise<void> => {
|
||||
if (clip.value == null) return;
|
||||
|
||||
const { canceled, result } = await os.form(clip.value.name, {
|
||||
name: {
|
||||
type: 'string',
|
||||
|
@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
default: clip.value.isPublic,
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('clips/update', {
|
||||
|
@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
handler: async (): Promise<void> => {
|
||||
if (clip.value == null) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
|
||||
|
|
|
@ -10,9 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</MkDateSeparatedList>
|
||||
<MkNote v-for="item in items" :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
@ -23,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { markRaw } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
|
|
@ -6,7 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||
<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">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
|
@ -26,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator">
|
||||
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator" withControl>
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
|
||||
</div>
|
||||
|
@ -37,10 +51,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<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 MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
@ -50,6 +66,10 @@ const router = useRouter();
|
|||
|
||||
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', {
|
||||
limit: 5,
|
||||
offsetMode: true,
|
||||
|
@ -59,12 +79,28 @@ const myFlashsPaginator = markRaw(new Paginator('flash/my', {
|
|||
}));
|
||||
const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', {
|
||||
limit: 5,
|
||||
canSearch: true,
|
||||
searchParamName: 'search',
|
||||
}));
|
||||
|
||||
function create() {
|
||||
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(() => [{
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.create,
|
||||
|
@ -72,6 +108,10 @@ const headerActions = computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}, {
|
||||
key: 'featured',
|
||||
title: i18n.ts._play.featured,
|
||||
icon: 'ti ti-flare',
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<MkKeyValue>
|
||||
<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>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
|
||||
|
@ -151,7 +151,7 @@ async function fetch() {
|
|||
case 'theme':
|
||||
try {
|
||||
const metaRaw = parseThemeCode(res.data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
const { id, props, desc: description, ...meta } = metaRaw;
|
||||
data.value = {
|
||||
type: 'theme',
|
||||
|
|
|
@ -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="tab === 'overview'" class="_gaps_m">
|
||||
<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>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :paginator="paginator">
|
||||
<template #default="{ items }">
|
||||
<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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
|
|
|
@ -62,24 +62,29 @@ function fetchList(): void {
|
|||
}
|
||||
|
||||
function like() {
|
||||
if (list.value == null) return;
|
||||
os.apiWithDialog('users/lists/favorite', {
|
||||
listId: list.value.id,
|
||||
}).then(() => {
|
||||
if (list.value == null) return;
|
||||
list.value.isLiked = true;
|
||||
list.value.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
function unlike() {
|
||||
if (list.value == null) return;
|
||||
os.apiWithDialog('users/lists/unfavorite', {
|
||||
listId: list.value.id,
|
||||
}).then(() => {
|
||||
if (list.value == null) return;
|
||||
list.value.isLiked = false;
|
||||
list.value.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (list.value == null) return;
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts.enterListName,
|
||||
});
|
||||
|
|
|
@ -64,6 +64,7 @@ async function create() {
|
|||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('clips/create', result);
|
||||
|
|
|
@ -100,7 +100,7 @@ const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
|||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'older',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
computedParams: computed(() => note.value && note.value.channelId != null ? ({
|
||||
channelId: note.value.channelId,
|
||||
}) : undefined),
|
||||
}));
|
||||
|
@ -109,7 +109,7 @@ const nextChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
|||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'newer',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
computedParams: computed(() => note.value && note.value.channelId != null ? ({
|
||||
channelId: note.value.channelId,
|
||||
}) : undefined),
|
||||
}));
|
||||
|
|
|
@ -135,9 +135,9 @@ const page = ref<Misskey.entities.Page | null>(null);
|
|||
const error = ref<any>(null);
|
||||
const otherPostsPaginator = markRaw(new Paginator('users/pages', {
|
||||
limit: 6,
|
||||
computedParams: computed(() => ({
|
||||
computedParams: computed(() => page.value ? ({
|
||||
userId: page.value.user.id,
|
||||
})),
|
||||
}) : undefined),
|
||||
}));
|
||||
const path = computed(() => props.username + '/' + props.pageName);
|
||||
|
||||
|
|
|
@ -79,7 +79,9 @@ async function createKey() {
|
|||
default: scope.value.join('/'),
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
|
|
|
@ -56,7 +56,9 @@ async function createKey() {
|
|||
label: i18n.ts._registry.scope,
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
|
|
|
@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, markRaw, ref, watch } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
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 { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const sortMode = ref('+size');
|
||||
const sortMode = ref<Misskey.entities.DriveFilesRequest['sort']>('+size');
|
||||
const paginator = markRaw(new Paginator('drive/files', {
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({ sort: sortMode.value })),
|
||||
|
|
|
@ -208,9 +208,9 @@ const blockingPaginator = markRaw(new Paginator('blocking/list', {
|
|||
limit: 10,
|
||||
}));
|
||||
|
||||
const expandedRenoteMuteItems = ref([]);
|
||||
const expandedMuteItems = ref([]);
|
||||
const expandedBlockItems = ref([]);
|
||||
const expandedRenoteMuteItems = ref<string[]>([]);
|
||||
const expandedMuteItems = ref<string[]>([]);
|
||||
const expandedBlockItems = ref<string[]>([]);
|
||||
|
||||
const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
|
||||
|
||||
|
@ -253,7 +253,7 @@ async function unblock(user, ev) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function toggleRenoteMuteItem(item) {
|
||||
async function toggleRenoteMuteItem(item: { id: string }) {
|
||||
if (expandedRenoteMuteItems.value.includes(item.id)) {
|
||||
expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
|
||||
} 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)) {
|
||||
expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id);
|
||||
} 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)) {
|
||||
expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id);
|
||||
} else {
|
||||
|
|
|
@ -59,10 +59,21 @@ const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
|
|||
|
||||
async function init() {
|
||||
let noteText = '';
|
||||
if (title.value) noteText += `[ ${title.value} ]\n`;
|
||||
// Googleニュース対策
|
||||
if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, '');
|
||||
else if (text && title.value !== text) noteText += `${text}\n`;
|
||||
if (title.value) {
|
||||
noteText += `[ ${title.value} ]\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) {
|
||||
try {
|
||||
// Normalize the URL to URL-encoded and puny-coded from with the URL constructor.
|
||||
|
|
|
@ -25,7 +25,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkUserName class="name" :user="user" :nowrap="true"/>
|
||||
<div class="bottom">
|
||||
<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.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">
|
||||
|
@ -44,7 +43,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||
<div class="bottom">
|
||||
<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.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</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)"/>
|
||||
</div>
|
||||
<div v-if="note.reactionCount > 0" :class="$style.reactions">
|
||||
|
|
|
@ -14,12 +14,13 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import type { FormWithDefault } from '@/utility/form.js';
|
||||
|
||||
export type Plugin = {
|
||||
installId: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
config?: Record<string, { default: any }>;
|
||||
config?: FormWithDefault;
|
||||
configData: Record<string, any>;
|
||||
src: string | null;
|
||||
version: string;
|
||||
|
@ -155,6 +156,13 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
|
|||
export async function uninstallPlugin(plugin: Plugin) {
|
||||
abortPlugin(plugin);
|
||||
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)) {
|
||||
await os.apiWithDialog('i/revoke-token', {
|
||||
token: store.s.pluginTokens[plugin.installId],
|
||||
|
@ -233,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> {
|
|||
pluginLogs.value.set(plugin.installId, []);
|
||||
|
||||
function systemLog(message: string, isError = false): void {
|
||||
pluginLogs.value.get(plugin.installId)?.push({
|
||||
pluginLogs.value.get(plugin!.installId)?.push({
|
||||
at: Date.now(),
|
||||
isSystem: true,
|
||||
message,
|
||||
|
|
|
@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', {
|
|||
},
|
||||
memo: {
|
||||
where: 'account',
|
||||
default: null,
|
||||
default: null as string | null,
|
||||
},
|
||||
reactionAcceptance: {
|
||||
where: 'account',
|
||||
|
|
|
@ -22,11 +22,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
background-color: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
|
@ -45,27 +40,6 @@ html {
|
|||
&, * {
|
||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||
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 {
|
||||
|
@ -110,6 +84,11 @@ html::view-transition-old(theme-changing) {
|
|||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
html::selection {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
background-color: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
@keyframes themeChangingOld {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
||||
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>;
|
||||
|
||||
|
@ -15,22 +15,107 @@ type Text = string | ComputedRef<string>;
|
|||
|
||||
export type MenuAction = (ev: MouseEvent) => void;
|
||||
|
||||
export type MenuDivider = { type: 'divider' };
|
||||
export type MenuNull = undefined;
|
||||
export type MenuLabel = { type: 'label', text: Text, caption?: Text };
|
||||
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean };
|
||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<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 };
|
||||
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
||||
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
||||
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 interface MenuButton {
|
||||
type?: 'button';
|
||||
text: Text;
|
||||
caption?: Text;
|
||||
icon?: string;
|
||||
indicate?: boolean;
|
||||
danger?: boolean;
|
||||
active?: boolean | ComputedRef<boolean>;
|
||||
avatar?: Misskey.entities.User;
|
||||
action: MenuAction;
|
||||
}
|
||||
|
||||
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>;
|
||||
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
|
||||
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export interface PostFormProps {
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: Misskey.entities.Channel; // TODO
|
||||
reply?: Misskey.entities.Note | null;
|
||||
renote?: Misskey.entities.Note | null;
|
||||
channel?: Misskey.entities.Channel | null; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
|
|
|
@ -368,10 +368,6 @@ function onDrop(ev) {
|
|||
> .body {
|
||||
background: transparent !important;
|
||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,10 +393,6 @@ function onDrop(ev) {
|
|||
> .body {
|
||||
background: var(--MI_THEME-bg) !important;
|
||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -487,9 +479,5 @@ function onDrop(ev) {
|
|||
container-type: size;
|
||||
background-color: var(--MI_THEME-bg);
|
||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
|
|||
label: i18n.ts.sound,
|
||||
default: soundSetting.value.type ?? 'none',
|
||||
enum: soundsTypes.map(f => ({
|
||||
value: f ?? 'none', label: getSoundTypeName(f),
|
||||
value: f ?? 'none' as Exclude<SoundType, null> | 'none',
|
||||
label: getSoundTypeName(f),
|
||||
})),
|
||||
},
|
||||
soundFile: {
|
||||
|
@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
const res = buildSoundStore(result);
|
||||
if (res) soundSetting.value = res;
|
||||
|
||||
function buildSoundStore(result: any): SoundStore | null {
|
||||
const type = (result.type === 'none' ? null : result.type) as SoundType;
|
||||
const volume = result.volume as number;
|
||||
const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
|
||||
const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
|
||||
function buildSoundStore(r: NonNullable<typeof result>): SoundStore | null {
|
||||
const type = (r.type === 'none' ? null : r.type);
|
||||
const volume = r.volume;
|
||||
const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
|
||||
const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
|
||||
|
||||
if (type === '_driveFile_') {
|
||||
if (!fileUrl || !fileId) {
|
||||
|
|
|
@ -126,7 +126,6 @@ const onContextmenu = (ev) => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
$widgets-hide-threshold: 1090px;
|
||||
|
||||
.root {
|
||||
|
|
|
@ -5,55 +5,59 @@
|
|||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
type EnumItem = string | {
|
||||
export type EnumItem = string | {
|
||||
label: string;
|
||||
value: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
type Hidden = boolean | ((v: any) => boolean);
|
||||
|
||||
export type FormItem = {
|
||||
interface FormItemBase {
|
||||
label?: string;
|
||||
hidden?: Hidden;
|
||||
}
|
||||
|
||||
export interface StringFormItem extends FormItemBase {
|
||||
type: 'string';
|
||||
default?: string | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
multiline?: boolean;
|
||||
treatAsMfm?: boolean;
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface NumberFormItem extends FormItemBase {
|
||||
type: 'number';
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
step?: number;
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface BooleanFormItem extends FormItemBase {
|
||||
type: 'boolean';
|
||||
default?: boolean | null;
|
||||
description?: string;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface EnumFormItem extends FormItemBase {
|
||||
type: 'enum';
|
||||
default?: string | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
enum: EnumItem[];
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface RadioFormItem extends FormItemBase {
|
||||
type: 'radio';
|
||||
default?: unknown | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
options: {
|
||||
label: string;
|
||||
value: unknown;
|
||||
}[];
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface RangeFormItem extends FormItemBase {
|
||||
type: 'range';
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
|
@ -62,42 +66,80 @@ export type FormItem = {
|
|||
min: number;
|
||||
max: number;
|
||||
textConverter?: (value: number) => string;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface ObjectFormItem extends FormItemBase {
|
||||
type: 'object';
|
||||
default?: Record<string, unknown> | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface ArrayFormItem extends FormItemBase {
|
||||
type: 'array';
|
||||
default?: unknown[] | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
}
|
||||
|
||||
export interface ButtonFormItem extends FormItemBase {
|
||||
type: 'button';
|
||||
content?: string;
|
||||
hidden?: Hidden;
|
||||
action: (ev: MouseEvent, v: any) => void;
|
||||
} | {
|
||||
}
|
||||
|
||||
export interface DriveFileFormItem extends FormItemBase {
|
||||
type: 'drive-file';
|
||||
defaultFileId?: string | null;
|
||||
hidden?: Hidden;
|
||||
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 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> =
|
||||
Item['type'] extends 'string' ? string :
|
||||
Item['type'] extends 'number' ? number :
|
||||
Item['type'] extends 'boolean' ? boolean :
|
||||
Item['type'] extends 'radio' ? unknown :
|
||||
Item['type'] extends 'range' ? number :
|
||||
Item['type'] extends 'enum' ? string :
|
||||
Item['type'] extends 'array' ? unknown[] :
|
||||
Item['type'] extends 'object' ? Record<string, unknown> :
|
||||
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
|
||||
never;
|
||||
Item extends StringFormItem
|
||||
? NonNullableIfRequired<InferDefault<Item, string>, Item>
|
||||
: Item extends NumberFormItem
|
||||
? NonNullableIfRequired<InferDefault<Item, number>, Item>
|
||||
: Item extends BooleanFormItem
|
||||
? boolean
|
||||
: Item extends RadioFormItem
|
||||
? GetRadioItemType<Item>
|
||||
: Item extends RangeFormItem
|
||||
? 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> = {
|
||||
[P in keyof F]: GetItemType<F[P]>;
|
||||
|
|
|
@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: {
|
|||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
name: {
|
||||
type: 'string',
|
||||
default: null,
|
||||
default: null as string | null,
|
||||
label: i18n.ts.name,
|
||||
},
|
||||
description: {
|
||||
|
@ -180,6 +180,7 @@ export function getNoteMenu(props: {
|
|||
currentClip?: Misskey.entities.Clip;
|
||||
}) {
|
||||
const appearNote = getAppearNote(props.note);
|
||||
const link = appearNote.url ?? appearNote.uri;
|
||||
|
||||
const cleanups = [] as (() => void)[];
|
||||
|
||||
|
@ -189,6 +190,7 @@ export function getNoteMenu(props: {
|
|||
text: i18n.ts.noteDeleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
if ($i == null) return;
|
||||
|
||||
misskeyApi('notes/delete', {
|
||||
noteId: appearNote.id,
|
||||
|
@ -208,6 +210,7 @@ export function getNoteMenu(props: {
|
|||
text: i18n.ts.deleteAndEditConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
if ($i == null) return;
|
||||
|
||||
misskeyApi('notes/delete', {
|
||||
noteId: appearNote.id,
|
||||
|
@ -317,22 +320,25 @@ export function getNoteMenu(props: {
|
|||
action: copyContent,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||
|
||||
if (appearNote.url || appearNote.uri) {
|
||||
if (link) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyRemoteLink,
|
||||
action: () => {
|
||||
copyToClipboard(appearNote.url ?? appearNote.uri);
|
||||
copyToClipboard(link);
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
window.open(link, '_blank', 'noopener');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed));
|
||||
const embedMenu = getNoteEmbedCodeMenu(appearNote, i18n.ts.embed);
|
||||
if (embedMenu != null) {
|
||||
menuItems.push(embedMenu);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSupportShare()) {
|
||||
|
@ -475,22 +481,25 @@ export function getNoteMenu(props: {
|
|||
action: copyContent,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||
|
||||
if (appearNote.url || appearNote.uri) {
|
||||
if (link != null) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyRemoteLink,
|
||||
action: () => {
|
||||
copyToClipboard(appearNote.url ?? appearNote.uri);
|
||||
copyToClipboard(link);
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
window.open(link, '_blank', 'noopener');
|
||||
},
|
||||
});
|
||||
} 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,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
|
@ -633,7 +642,7 @@ export function getRenoteMenu(props: {
|
|||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}]);
|
||||
}])]);
|
||||
|
||||
normalExternalChannelRenoteItems.push({
|
||||
type: 'parent',
|
||||
|
|
|
@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
const userDetailed = await misskeyApi('users/show', {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const { canceled, result } = await os.form(i18n.ts.editMemo, {
|
||||
memo: {
|
||||
type: 'string',
|
||||
|
@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
default: userDetailed.memo,
|
||||
},
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('users/update-memo', {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { ref, shallowRef, triggerRef } from 'vue';
|
||||
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';
|
||||
|
||||
const MAX_ITEMS = 30;
|
||||
|
@ -17,14 +17,60 @@ export type MisskeyEntity = {
|
|||
id: string;
|
||||
createdAt: string;
|
||||
_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 fetching = ref(true);
|
||||
|
@ -35,18 +81,18 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
|
|||
public error = ref(false);
|
||||
private endpoint: Endpoint;
|
||||
private limit: number;
|
||||
private params: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']);
|
||||
public computedParams: ComputedRef<Misskey.Endpoints[Endpoint]['req']> | null;
|
||||
private params: E['req'] | (() => E['req']);
|
||||
public computedParams: ComputedRef<E['req'] | null | undefined> | null;
|
||||
public initialId: MisskeyEntity['id'] | null = null;
|
||||
public initialDate: number | null = null;
|
||||
public initialDirection: 'newer' | 'older';
|
||||
private offsetMode: boolean;
|
||||
public noPaging: boolean;
|
||||
public searchQuery = ref<null | string>('');
|
||||
private searchParamName: string;
|
||||
private searchParamName: keyof E['req'] | 'search';
|
||||
private canFetchDetection: 'safe' | 'limit' | null = null;
|
||||
private aheadQueue: T[] = [];
|
||||
private useShallowRef: boolean;
|
||||
private useShallowRef: SRef;
|
||||
|
||||
// 配列内の要素をどのような順序で並べるか
|
||||
// newest: 新しいものが先頭 (default)
|
||||
|
@ -56,8 +102,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
|
|||
|
||||
constructor(endpoint: Endpoint, props: {
|
||||
limit?: number;
|
||||
params?: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']);
|
||||
computedParams?: ComputedRef<Misskey.Endpoints[Endpoint]['req']>;
|
||||
params?: E['req'] | (() => E['req']);
|
||||
computedParams?: ComputedRef<E['req'] | null | undefined>;
|
||||
|
||||
/**
|
||||
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
||||
|
@ -75,14 +121,19 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
|
|||
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
|
||||
canFetchDetection?: 'safe' | 'limit';
|
||||
|
||||
useShallowRef?: boolean;
|
||||
useShallowRef?: SRef;
|
||||
|
||||
canSearch?: boolean;
|
||||
searchParamName?: keyof Misskey.Endpoints[Endpoint]['req'];
|
||||
searchParamName?: keyof E['req'];
|
||||
}) {
|
||||
this.endpoint = endpoint;
|
||||
this.useShallowRef = props.useShallowRef ?? false;
|
||||
this.items = this.useShallowRef ? shallowRef([] as T[]) : ref([] as T[]);
|
||||
this.useShallowRef = (props.useShallowRef ?? false) as SRef;
|
||||
if (this.useShallowRef) {
|
||||
this.items = shallowRef<T[]>([]);
|
||||
} else {
|
||||
this.items = ref<T[]>([]) as Ref<T[]>;
|
||||
}
|
||||
|
||||
this.limit = props.limit ?? FIRST_FETCH_LIMIT;
|
||||
this.params = props.params ?? {};
|
||||
this.computedParams = props.computedParams ?? null;
|
||||
|
@ -130,7 +181,7 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
|
|||
this.queuedAheadItemsCount.value = 0;
|
||||
this.fetching.value = true;
|
||||
|
||||
await misskeyApi<T[]>(this.endpoint, {
|
||||
const data: E['req'] = {
|
||||
...(typeof this.params === 'function' ? this.params() : this.params),
|
||||
...(this.computedParams ? this.computedParams.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,
|
||||
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 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 => {
|
||||
const apiRes = (await misskeyApi(this.endpoint, data).catch(err => {
|
||||
this.error.value = true;
|
||||
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> {
|
||||
|
@ -187,7 +245,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.
|
|||
public async fetchOlder(): Promise<void> {
|
||||
if (!this.canFetchOlder.value || this.fetching.value || this.fetchingOlder.value || this.items.value.length === 0) return;
|
||||
this.fetchingOlder.value = true;
|
||||
await misskeyApi<T[]>(this.endpoint, {
|
||||
|
||||
const data: E['req'] = {
|
||||
...(typeof this.params === 'function' ? this.params() : this.params),
|
||||
...(this.computedParams ? this.computedParams.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(),
|
||||
}),
|
||||
}).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') {
|
||||
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.canFetchOlder.value = false;
|
||||
} else {
|
||||
this.canFetchOlder.value = true;
|
||||
}
|
||||
this.fetchingOlder.value = false;
|
||||
|
||||
if (apiRes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < apiRes.length; i++) {
|
||||
const item = apiRes[i];
|
||||
if (i === 10) 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;
|
||||
}
|
||||
}).finally(() => {
|
||||
this.fetchingOlder.value = false;
|
||||
});
|
||||
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
|
||||
if (apiRes.length === 0) {
|
||||
this.canFetchOlder.value = false;
|
||||
} else {
|
||||
this.canFetchOlder.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchNewer(options: {
|
||||
toQueue?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
this.fetchingNewer.value = true;
|
||||
await misskeyApi<T[]>(this.endpoint, {
|
||||
|
||||
const data: E['req'] = {
|
||||
...(typeof this.params === 'function' ? this.params() : this.params),
|
||||
...(this.computedParams ? this.computedParams.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(),
|
||||
}),
|
||||
}).then(res => {
|
||||
if (res.length === 0) return; // これやらないと余計なre-renderが走る
|
||||
};
|
||||
|
||||
if (options.toQueue) {
|
||||
this.aheadQueue.unshift(...res.toReversed());
|
||||
if (this.aheadQueue.length > MAX_QUEUE_ITEMS) {
|
||||
this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS);
|
||||
}
|
||||
this.queuedAheadItemsCount.value = this.aheadQueue.length;
|
||||
} else {
|
||||
if (this.order.value === 'oldest') {
|
||||
this.pushItems(res);
|
||||
} else {
|
||||
this.unshiftItems(res.toReversed());
|
||||
}
|
||||
const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => {
|
||||
return null;
|
||||
})) as T[] | null;
|
||||
|
||||
this.fetchingNewer.value = false;
|
||||
|
||||
if (apiRes == null || apiRes.length === 0) return; // これやらないと余計なre-renderが走る
|
||||
|
||||
if (options.toQueue) {
|
||||
this.aheadQueue.unshift(...apiRes.toReversed());
|
||||
if (this.aheadQueue.length > MAX_QUEUE_ITEMS) {
|
||||
this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.fetchingNewer.value = false;
|
||||
});
|
||||
this.queuedAheadItemsCount.value = this.aheadQueue.length;
|
||||
} else {
|
||||
if (this.order.value === 'oldest') {
|
||||
this.pushItems(apiRes);
|
||||
} else {
|
||||
this.unshiftItems(apiRes.toReversed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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のも更新
|
||||
|
||||
const index = this.items.value.findIndex(x => x.id === id);
|
||||
if (index !== -1) {
|
||||
const item = this.items.value[index]!;
|
||||
this.items.value[index] = updator(item);
|
||||
this.items.value[index] = updater(item);
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const props = defineProps<{
|
||||
activity: {
|
||||
total: number;
|
||||
|
@ -44,10 +46,21 @@ const props = defineProps<{
|
|||
}[]
|
||||
}>();
|
||||
|
||||
for (const d of props.activity) {
|
||||
d.total = d.notes + d.replies + d.renotes;
|
||||
}
|
||||
const peak = Math.max(...props.activity.map(d => d.total));
|
||||
const activity = deepClone(props.activity).map(d => ({
|
||||
...d,
|
||||
total: d.notes + d.replies + d.renotes,
|
||||
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 year = now.getFullYear();
|
||||
|
@ -55,7 +68,7 @@ const month = now.getMonth();
|
|||
const day = now.getDate();
|
||||
|
||||
let x = 20;
|
||||
props.activity.slice().forEach((d, i) => {
|
||||
activity.slice().forEach((d, i) => {
|
||||
d.x = x;
|
||||
|
||||
const date = new Date(year, month, day - i);
|
||||
|
|
|
@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js';
|
|||
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
||||
import XCalendar from './WidgetActivity.calendar.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 MkContainer from '@/components/MkContainer.vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const name = 'activity';
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
transparent: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
view: {
|
||||
type: 'number' as const,
|
||||
type: 'number',
|
||||
default: 0,
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import { useWidgetPropsManager } 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 widgetPropsDef = {
|
||||
transparent: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
@ -42,6 +42,8 @@ const touched = () => {
|
|||
};
|
||||
|
||||
const onMousemove = (ev: MouseEvent) => {
|
||||
if (!live2d.value || !live2d.value.contentWindow) return;
|
||||
|
||||
const iframeRect = live2d.value.getBoundingClientRect();
|
||||
live2d.value.contentWindow.postMessage({
|
||||
type: 'moveCursor',
|
||||
|
|
|
@ -23,7 +23,7 @@ import { ref } from 'vue';
|
|||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import { useWidgetPropsManager } 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 MkContainer from '@/components/MkContainer.vue';
|
||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||
|
@ -35,16 +35,16 @@ const name = 'aiscript';
|
|||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
script: {
|
||||
type: 'string' as const,
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
default: '(1 + 1)',
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
@ -106,7 +106,7 @@ const run = async () => {
|
|||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
text: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import type { Ref } from 'vue';
|
|||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||
import { useWidgetPropsManager } 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 { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
@ -31,15 +31,15 @@ const name = 'aiscriptApp';
|
|||
|
||||
const widgetPropsDef = {
|
||||
script: {
|
||||
type: 'string' as const,
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
default: '',
|
||||
},
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
@ -92,7 +92,7 @@ async function run() {
|
|||
os.alert({
|
||||
type: 'error',
|
||||
title: 'AiScript Error',
|
||||
text: err.message,
|
||||
text: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.bdayFRoot">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<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 v-else :class="$style.bdayFFallback">
|
||||
<MkResult type="empty"/>
|
||||
|
@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { useWidgetPropsManager } 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 { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings;
|
|||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||
import { useWidgetPropsManager } 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 { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
@ -25,19 +25,19 @@ const name = 'button';
|
|||
|
||||
const widgetPropsDef = {
|
||||
label: {
|
||||
type: 'string' as const,
|
||||
type: 'string',
|
||||
default: 'BUTTON',
|
||||
},
|
||||
colored: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
script: {
|
||||
type: 'string' as const,
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
default: 'Mk:dialog("hello" "world")',
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
@ -81,7 +81,7 @@ const run = async () => {
|
|||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
text: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref } from 'vue';
|
||||
import { useWidgetPropsManager } 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 { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
|
@ -49,10 +49,10 @@ const name = 'calendar';
|
|||
|
||||
const widgetPropsDef = {
|
||||
transparent: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { } from 'vue';
|
||||
import { useWidgetPropsManager } 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 { i18n } from '@/i18n.js';
|
||||
import MkChatHistories from '@/components/MkChatHistories.vue';
|
||||
|
@ -28,10 +28,10 @@ const name = 'chat';
|
|||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { useWidgetPropsManager } 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 MkClickerGame from '@/components/MkClickerGame.vue';
|
||||
|
||||
|
@ -22,10 +22,10 @@ const name = 'clicker';
|
|||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed } from 'vue';
|
||||
import { useWidgetPropsManager } 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 MkAnalogClock from '@/components/MkAnalogClock.vue';
|
||||
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
||||
|
@ -43,76 +43,92 @@ const name = 'clock';
|
|||
|
||||
const widgetPropsDef = {
|
||||
transparent: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: 'radio' as const,
|
||||
type: 'radio',
|
||||
default: 'medium',
|
||||
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: {
|
||||
type: 'radio' as const,
|
||||
type: 'radio',
|
||||
default: 0.2,
|
||||
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: {
|
||||
type: 'radio' as const,
|
||||
type: 'radio',
|
||||
default: 'numbers',
|
||||
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: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
sAnimation: {
|
||||
type: 'radio' as const,
|
||||
type: 'radio',
|
||||
default: 'elastic',
|
||||
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: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: 'radio' as const,
|
||||
type: 'radio',
|
||||
default: 'none',
|
||||
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: {
|
||||
type: 'enum' as const,
|
||||
type: 'enum',
|
||||
default: null,
|
||||
enum: [...timezones.map((tz) => ({
|
||||
label: tz.name,
|
||||
|
@ -122,7 +138,7 @@ const widgetPropsDef = {
|
|||
value: null,
|
||||
}],
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed } from 'vue';
|
||||
import { useWidgetPropsManager } 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 MkDigitalClock from '@/components/MkDigitalClock.vue';
|
||||
|
||||
|
@ -25,24 +25,24 @@ const name = 'digitalClock';
|
|||
|
||||
const widgetPropsDef = {
|
||||
transparent: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
fontSize: {
|
||||
type: 'number' as const,
|
||||
type: 'number',
|
||||
default: 1.5,
|
||||
step: 0.1,
|
||||
},
|
||||
showMs: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
showLabel: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
timezone: {
|
||||
type: 'enum' as const,
|
||||
type: 'enum',
|
||||
default: null,
|
||||
enum: [...timezones.map((tz) => ({
|
||||
label: tz.name,
|
||||
|
@ -52,7 +52,7 @@ const widgetPropsDef = {
|
|||
value: null,
|
||||
}],
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<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 #header>{{ i18n.ts._widgets.federation }}</template>
|
||||
|
||||
|
@ -30,7 +30,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { useWidgetPropsManager } 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 MkMiniChart from '@/components/MkMiniChart.vue';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
|
@ -42,10 +42,10 @@ const name = 'federation';
|
|||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
} satisfies FormWithDefault;
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue