diff --git a/CHANGELOG.md b/CHANGELOG.md index c8fd80063d..ed725314b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ ### Client - Fix: サーバーメトリクスが90度傾いている - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 +- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように +- ドライブファイルのメニューで画像をクロップできるように +- 画像を動画と同様に簡単に隠せるように ### Server - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました diff --git a/locales/index.d.ts b/locales/index.d.ts index af6b803278..dea00d783f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -139,8 +139,10 @@ export interface Locale { "suspendConfirm": string; "unsuspendConfirm": string; "selectList": string; + "editList": string; "selectChannel": string; "selectAntenna": string; + "editAntenna": string; "selectWidget": string; "editWidgets": string; "editWidgetsExit": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e7202bfbb5..d9d227a0b6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -136,8 +136,10 @@ unblockConfirm: "ブロック解除しますか?" suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" +editList: "リストを編集" selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" +editAntenna: "アンテナを編集" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 5b7359074e..48ff00c8ce 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; -import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; +import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -69,7 +69,7 @@ export class QueueService { if (content == null) return null; if (to == null) return null; - const data = { + const data: DeliverJobData = { user: { id: user.id, }, @@ -88,6 +88,38 @@ export class QueueService { }); } + /** + * ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい + * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください + * @param content IActivity | null + * @param inboxes `Map` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox) + * @returns void + */ + @bindThis + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { + const opts = { + attempts: this.config.deliverJobMaxAttempts ?? 12, + backoff: { + type: 'custom', + }, + removeOnComplete: true, + removeOnFail: true, + }; + + await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({ + name: d[0], + data: { + user, + content, + to: d[0], + isSharedInbox: d[1], + } as DeliverJobData, + opts, + }))); + + return; + } + @bindThis public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { const data = { diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 2d9e7a14ee..ca148916dc 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -56,25 +55,18 @@ export class ApDbResolverService implements OnApplicationShutdown { @bindThis public parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - // the host part of a URL is case insensitive, so use the 'i' flag. - const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - local: true, - type: matchLocal[1], - id: matchLocal[2], - rest: matchLocal[3], - }; - } else { - return { - local: false, - uri, - }; - } + const separator = '/'; + + const uri = new URL(getApId(value)); + if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; + + const [, type, id, ...rest] = uri.pathname.split(separator); + return { + local: true, + type, + id, + rest: rest.length === 0 ? undefined : rest.join(separator), + }; } /** diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 62a2a33a19..6e910eb538 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -7,6 +7,8 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { IActivity } from '@/core/activitypub/type.js'; +import { ThinUser } from '@/queue/types.js'; interface IRecipe { type: string; @@ -21,10 +23,10 @@ interface IDirectRecipe extends IRecipe { to: RemoteUser; } -const isFollowers = (recipe: any): recipe is IFollowersRecipe => +const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => recipe.type === 'Followers'; -const isDirect = (recipe: any): recipe is IDirectRecipe => +const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => recipe.type === 'Direct'; @Injectable() @@ -46,11 +48,11 @@ export class ApDeliverManagerService { /** * Deliver activity to followers + * @param actor * @param activity Activity - * @param from Followee */ @bindThis - public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -64,11 +66,12 @@ export class ApDeliverManagerService { /** * Deliver activity to user + * @param actor * @param activity Activity * @param to Target user */ @bindThis - public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -81,7 +84,7 @@ export class ApDeliverManagerService { } @bindThis - public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { + public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) { return new DeliverManager( this.userEntityService, this.followingsRepository, @@ -94,12 +97,15 @@ export class ApDeliverManagerService { } class DeliverManager { - private actor: { id: User['id']; host: null; }; - private activity: any; + private actor: ThinUser; + private activity: IActivity | null; private recipes: IRecipe[] = []; /** * Constructor + * @param userEntityService + * @param followingsRepository + * @param queueService * @param actor Actor * @param activity Activity to deliver */ @@ -109,9 +115,15 @@ class DeliverManager { private queueService: QueueService, actor: { id: User['id']; host: null; }, - activity: any, + activity: IActivity | null, ) { - this.actor = actor; + // 型で弾いてはいるが一応ローカルユーザーかチェック + if (actor.host != null) throw new Error('actor.host must be null'); + + // パフォーマンス向上のためキューに突っ込むのはidのみに絞る + this.actor = { + id: actor.id, + }; this.activity = activity; } @@ -155,9 +167,8 @@ class DeliverManager { */ @bindThis public async execute() { - if (!this.userEntityService.isLocalUser(this.actor)) return; - // The value flags whether it is shared or not. + // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); /* @@ -201,9 +212,6 @@ class DeliverManager { .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); // deliver - for (const inbox of inboxes) { - // inbox[0]: inbox, inbox[1]: whether it is sharedInbox - this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]); - } + this.queueService.deliverMany(this.actor, this.activity, inboxes); } } diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug index 1d62778ce1..2a4954ec8b 100644 --- a/packages/backend/src/server/web/views/info-card.pug +++ b/packages/backend/src/server/web/views/info-card.pug @@ -47,4 +47,4 @@ html header#banner(style=`background-image: url(${meta.bannerUrl})`) div#title= meta.name || host div#content - div#description= meta.description + div#description!= meta.description diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 82363499b7..b2d60d36c4 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -47,6 +47,7 @@ const emit = defineEmits<{ const props = defineProps<{ file: misskey.entities.DriveFile; aspectRatio: number; + uploadFolder?: string | null; }>(); const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); @@ -58,11 +59,17 @@ let loading = $ref(true); const ok = async () => { const promise = new Promise(async (res) => { const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); - croppedCanvas.toBlob(blob => { + croppedCanvas?.toBlob(blob => { + if (!blob) return; const formData = new FormData(); formData.append('file', blob); - formData.append('i', $i.token); - if (defaultStore.state.uploadFolder) { + formData.append('name', `cropped_${props.file.name}`); + formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); + formData.append('comment', props.file.comment ?? 'null'); + formData.append('i', $i!.token); + if (props.uploadFolder || props.uploadFolder === null) { + formData.append('folderId', props.uploadFolder ?? 'null'); + } else if (defaultStore.state.uploadFolder) { formData.append('folderId', defaultStore.state.uploadFolder); } @@ -82,12 +89,12 @@ const ok = async () => { const f = await promise; emit('ok', f); - dialogEl.close(); + dialogEl!.close(); }; const cancel = () => { emit('cancel'); - dialogEl.close(); + dialogEl!.close(); }; const onImageLoad = () => { @@ -100,7 +107,7 @@ const onImageLoad = () => { }; onMounted(() => { - cropper = new Cropper(imgEl, { + cropper = new Cropper(imgEl!, { }); const computedStyle = getComputedStyle(document.documentElement); @@ -112,13 +119,13 @@ onMounted(() => { selection.outlined = true; window.setTimeout(() => { - cropper.getCropperImage()!.$center('contain'); + cropper!.getCropperImage()!.$center('contain'); selection.$center(); }, 100); // モーダルオープンアニメーションが終わったあとで再度調整 window.setTimeout(() => { - cropper.getCropperImage()!.$center('contain'); + cropper!.getCropperImage()!.$center('contain'); selection.$center(); }, 500); }); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index f0641161be..3a75f8293f 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -44,6 +44,7 @@ import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; + folder: Misskey.entities.DriveFolder | null; isSelected?: boolean; selectMode?: boolean; }>(), { @@ -65,12 +66,12 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getDriveFileMenu(props.file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } } function onContextmenu(ev: MouseEvent) { - os.contextMenu(getDriveFileMenu(props.file), ev); + os.contextMenu(getDriveFileMenu(props.file, props.folder), ev); } function onDragstart(ev: DragEvent) { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 52aef450d9..19508fe4de 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -65,6 +65,7 @@ v-anim="i" :class="$style.file" :file="file" + :folder="folder" :selectMode="select === 'file'" :isSelected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index cb229fa241..4e36defb7c 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -108,7 +108,7 @@ function waitForDecode() { .then(() => { loaded = true; }, error => { - console.error('Error occured during decoding image', img.value, error); + console.error('Error occurred during decoding image', img.value, error); throw Error(error); }); } else { @@ -180,7 +180,7 @@ async function draw() { render(props.hash, work); drawImage(work); } catch (error) { - console.error('Error occured during drawing blurhash', error); + console.error('Error occurred during drawing blurhash', error); } } } diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index b29871c363..df49bcb26d 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -33,6 +33,7 @@
NSFW
+ @@ -113,6 +114,21 @@ function showMenu(ev: MouseEvent) { align-items: center; } +.hide { + display: block; + position: absolute; + border-radius: 6px; + background-color: var(--fg); + color: var(--accentLighten); + font-size: 14px; + opacity: .5; + padding: 3px 6px; + text-align: center; + cursor: pointer; + top: 12px; + right: 12px; +} + .hiddenTextWrapper { display: table-cell; text-align: center; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5c65569683..5b37a117de 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -66,7 +66,7 @@
{{ maxTextLength - textLength }}
- +
@@ -410,7 +410,11 @@ function updateFileName(file, name) { files[files.findIndex(x => x.id === file.id)].name = name; } -function upload(file: File, name?: string) { +function replaceFile(file: misskey.entities.DriveFile, newFile: misskey.entities.DriveFile): void { + files[files.findIndex(x => x.id === file.id)] = newFile; +} + +function upload(file: File, name?: string): void { uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 18fa142ebc..c50d025ab3 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -16,6 +16,7 @@