Merge remote-tracking branch 'origin/develop' into fetch-outbox
This commit is contained in:
commit
630e97bd06
|
@ -21,6 +21,9 @@
|
||||||
### Client
|
### Client
|
||||||
- Fix: サーバーメトリクスが90度傾いている
|
- Fix: サーバーメトリクスが90度傾いている
|
||||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||||
|
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||||
|
- ドライブファイルのメニューで画像をクロップできるように
|
||||||
|
- 画像を動画と同様に簡単に隠せるように
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||||
|
|
|
@ -139,8 +139,10 @@ export interface Locale {
|
||||||
"suspendConfirm": string;
|
"suspendConfirm": string;
|
||||||
"unsuspendConfirm": string;
|
"unsuspendConfirm": string;
|
||||||
"selectList": string;
|
"selectList": string;
|
||||||
|
"editList": string;
|
||||||
"selectChannel": string;
|
"selectChannel": string;
|
||||||
"selectAntenna": string;
|
"selectAntenna": string;
|
||||||
|
"editAntenna": string;
|
||||||
"selectWidget": string;
|
"selectWidget": string;
|
||||||
"editWidgets": string;
|
"editWidgets": string;
|
||||||
"editWidgetsExit": string;
|
"editWidgetsExit": string;
|
||||||
|
|
|
@ -136,8 +136,10 @@ unblockConfirm: "ブロック解除しますか?"
|
||||||
suspendConfirm: "凍結しますか?"
|
suspendConfirm: "凍結しますか?"
|
||||||
unsuspendConfirm: "解凍しますか?"
|
unsuspendConfirm: "解凍しますか?"
|
||||||
selectList: "リストを選択"
|
selectList: "リストを選択"
|
||||||
|
editList: "リストを編集"
|
||||||
selectChannel: "チャンネルを選択"
|
selectChannel: "チャンネルを選択"
|
||||||
selectAntenna: "アンテナを選択"
|
selectAntenna: "アンテナを選択"
|
||||||
|
editAntenna: "アンテナを編集"
|
||||||
selectWidget: "ウィジェットを選択"
|
selectWidget: "ウィジェットを選択"
|
||||||
editWidgets: "ウィジェットを編集"
|
editWidgets: "ウィジェットを編集"
|
||||||
editWidgetsExit: "編集を終了"
|
editWidgetsExit: "編集を終了"
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.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 { 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 httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export class QueueService {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
const data = {
|
const data: DeliverJobData = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
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<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||||
|
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
|
@bindThis
|
||||||
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||||
const data = {
|
const data = {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import escapeRegexp from 'escape-regexp';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -56,25 +55,18 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public parseUri(value: string | IObject): UriParseResult {
|
public parseUri(value: string | IObject): UriParseResult {
|
||||||
const uri = getApId(value);
|
const separator = '/';
|
||||||
|
|
||||||
// the host part of a URL is case insensitive, so use the 'i' flag.
|
const uri = new URL(getApId(value));
|
||||||
const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
|
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
||||||
const matchLocal = uri.match(localRegex);
|
|
||||||
|
|
||||||
if (matchLocal) {
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
local: true,
|
local: true,
|
||||||
type: matchLocal[1],
|
type,
|
||||||
id: matchLocal[2],
|
id,
|
||||||
rest: matchLocal[3],
|
rest: rest.length === 0 ? undefined : rest.join(separator),
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
local: false,
|
|
||||||
uri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,8 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
|
import { ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
interface IRecipe {
|
interface IRecipe {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -21,10 +23,10 @@ interface IDirectRecipe extends IRecipe {
|
||||||
to: RemoteUser;
|
to: RemoteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||||
recipe.type === 'Followers';
|
recipe.type === 'Followers';
|
||||||
|
|
||||||
const isDirect = (recipe: any): recipe is IDirectRecipe =>
|
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||||
recipe.type === 'Direct';
|
recipe.type === 'Direct';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -46,11 +48,11 @@ export class ApDeliverManagerService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to followers
|
* Deliver activity to followers
|
||||||
|
* @param actor
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
* @param from Followee
|
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -64,11 +66,12 @@ export class ApDeliverManagerService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to user
|
* Deliver activity to user
|
||||||
|
* @param actor
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
* @param to Target user
|
* @param to Target user
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -81,7 +84,7 @@ export class ApDeliverManagerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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(
|
return new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -94,12 +97,15 @@ export class ApDeliverManagerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeliverManager {
|
class DeliverManager {
|
||||||
private actor: { id: User['id']; host: null; };
|
private actor: ThinUser;
|
||||||
private activity: any;
|
private activity: IActivity | null;
|
||||||
private recipes: IRecipe[] = [];
|
private recipes: IRecipe[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
* @param userEntityService
|
||||||
|
* @param followingsRepository
|
||||||
|
* @param queueService
|
||||||
* @param actor Actor
|
* @param actor Actor
|
||||||
* @param activity Activity to deliver
|
* @param activity Activity to deliver
|
||||||
*/
|
*/
|
||||||
|
@ -109,9 +115,15 @@ class DeliverManager {
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
|
||||||
actor: { id: User['id']; host: null; },
|
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;
|
this.activity = activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,9 +167,8 @@ class DeliverManager {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async execute() {
|
public async execute() {
|
||||||
if (!this.userEntityService.isLocalUser(this.actor)) return;
|
|
||||||
|
|
||||||
// The value flags whether it is shared or not.
|
// The value flags whether it is shared or not.
|
||||||
|
// key: inbox URL, value: whether it is sharedInbox
|
||||||
const inboxes = new Map<string, boolean>();
|
const inboxes = new Map<string, boolean>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -201,9 +212,6 @@ class DeliverManager {
|
||||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
for (const inbox of inboxes) {
|
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
|
|
||||||
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,4 +47,4 @@ html
|
||||||
header#banner(style=`background-image: url(${meta.bannerUrl})`)
|
header#banner(style=`background-image: url(${meta.bannerUrl})`)
|
||||||
div#title= meta.name || host
|
div#title= meta.name || host
|
||||||
div#content
|
div#content
|
||||||
div#description= meta.description
|
div#description!= meta.description
|
||||||
|
|
|
@ -47,6 +47,7 @@ const emit = defineEmits<{
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
file: misskey.entities.DriveFile;
|
file: misskey.entities.DriveFile;
|
||||||
aspectRatio: number;
|
aspectRatio: number;
|
||||||
|
uploadFolder?: string | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
|
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
|
||||||
|
@ -58,11 +59,17 @@ let loading = $ref(true);
|
||||||
const ok = async () => {
|
const ok = async () => {
|
||||||
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
|
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
|
||||||
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
||||||
croppedCanvas.toBlob(blob => {
|
croppedCanvas?.toBlob(blob => {
|
||||||
|
if (!blob) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob);
|
formData.append('file', blob);
|
||||||
formData.append('i', $i.token);
|
formData.append('name', `cropped_${props.file.name}`);
|
||||||
if (defaultStore.state.uploadFolder) {
|
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);
|
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,12 +89,12 @@ const ok = async () => {
|
||||||
const f = await promise;
|
const f = await promise;
|
||||||
|
|
||||||
emit('ok', f);
|
emit('ok', f);
|
||||||
dialogEl.close();
|
dialogEl!.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
dialogEl.close();
|
dialogEl!.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImageLoad = () => {
|
const onImageLoad = () => {
|
||||||
|
@ -100,7 +107,7 @@ const onImageLoad = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
cropper = new Cropper(imgEl, {
|
cropper = new Cropper(imgEl!, {
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
@ -112,13 +119,13 @@ onMounted(() => {
|
||||||
selection.outlined = true;
|
selection.outlined = true;
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center('contain');
|
cropper!.getCropperImage()!.$center('contain');
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// モーダルオープンアニメーションが終わったあとで再度調整
|
// モーダルオープンアニメーションが終わったあとで再度調整
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center('contain');
|
cropper!.getCropperImage()!.$center('contain');
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { getDriveFileMenu } from '@/scripts/get-drive-file-menu';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
file: Misskey.entities.DriveFile;
|
file: Misskey.entities.DriveFile;
|
||||||
|
folder: Misskey.entities.DriveFolder | null;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
selectMode?: boolean;
|
selectMode?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
@ -65,12 +66,12 @@ function onClick(ev: MouseEvent) {
|
||||||
if (props.selectMode) {
|
if (props.selectMode) {
|
||||||
emit('chosen', props.file);
|
emit('chosen', props.file);
|
||||||
} else {
|
} 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) {
|
function onContextmenu(ev: MouseEvent) {
|
||||||
os.contextMenu(getDriveFileMenu(props.file), ev);
|
os.contextMenu(getDriveFileMenu(props.file, props.folder), ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragstart(ev: DragEvent) {
|
function onDragstart(ev: DragEvent) {
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
v-anim="i"
|
v-anim="i"
|
||||||
:class="$style.file"
|
:class="$style.file"
|
||||||
:file="file"
|
:file="file"
|
||||||
|
:folder="folder"
|
||||||
:selectMode="select === 'file'"
|
:selectMode="select === 'file'"
|
||||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||||
@chosen="chooseFile"
|
@chosen="chooseFile"
|
||||||
|
|
|
@ -108,7 +108,7 @@ function waitForDecode() {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}, error => {
|
}, error => {
|
||||||
console.error('Error occured during decoding image', img.value, error);
|
console.error('Error occurred during decoding image', img.value, error);
|
||||||
throw Error(error);
|
throw Error(error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -180,7 +180,7 @@ async function draw() {
|
||||||
render(props.hash, work);
|
render(props.hash, work);
|
||||||
drawImage(work);
|
drawImage(work);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error occured during drawing blurhash', error);
|
console.error('Error occurred during drawing blurhash', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||||
</div>
|
</div>
|
||||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
|
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
|
||||||
|
<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -113,6 +114,21 @@ function showMenu(ev: MouseEvent) {
|
||||||
align-items: center;
|
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 {
|
.hiddenTextWrapper {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
|
@ -410,7 +410,11 @@ function updateFileName(file, name) {
|
||||||
files[files.findIndex(x => x.id === file.id)].name = 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 => {
|
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
|
||||||
files.push(res);
|
files.push(res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -30,8 +31,9 @@ const props = defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any[]): void;
|
(ev: 'update:modelValue', value: any[]): void;
|
||||||
(ev: 'detach', id: string): void;
|
(ev: 'detach', id: string): void;
|
||||||
(ev: 'changeSensitive'): void;
|
(ev: 'changeSensitive', file: misskey.entities.DriveFile, isSensitive: boolean): void;
|
||||||
(ev: 'changeName'): void;
|
(ev: 'changeName', file: misskey.entities.DriveFile, newName: string): void;
|
||||||
|
(ev: 'replaceFile', file: misskey.entities.DriveFile, newFile: misskey.entities.DriveFile): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let menuShowing = false;
|
let menuShowing = false;
|
||||||
|
@ -85,8 +87,15 @@ async function describe(file) {
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFileMenu(file, ev: MouseEvent) {
|
async function crop(file: misskey.entities.DriveFile): Promise<void> {
|
||||||
|
const newFile = await os.cropImage(file, { aspectRatio: NaN });
|
||||||
|
emit('replaceFile', file, newFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFileMenu(file: misskey.entities.DriveFile, ev: MouseEvent): void {
|
||||||
if (menuShowing) return;
|
if (menuShowing) return;
|
||||||
|
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.renameFile,
|
text: i18n.ts.renameFile,
|
||||||
icon: 'ti ti-forms',
|
icon: 'ti ti-forms',
|
||||||
|
@ -99,7 +108,11 @@ function showFileMenu(file, ev: MouseEvent) {
|
||||||
text: i18n.ts.describeFile,
|
text: i18n.ts.describeFile,
|
||||||
icon: 'ti ti-text-caption',
|
icon: 'ti ti-text-caption',
|
||||||
action: () => { describe(file); },
|
action: () => { describe(file); },
|
||||||
}, {
|
}, ...isImage ? [{
|
||||||
|
text: i18n.ts.cropImage,
|
||||||
|
icon: 'ti ti-crop',
|
||||||
|
action: () : void => { crop(file); },
|
||||||
|
}] : [], {
|
||||||
text: i18n.ts.attachCancel,
|
text: i18n.ts.attachCancel,
|
||||||
icon: 'ti ti-circle-x',
|
icon: 'ti ti-circle-x',
|
||||||
action: () => { detachMedia(file.id); },
|
action: () => { detachMedia(file.id); },
|
||||||
|
|
|
@ -460,11 +460,13 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
|
||||||
|
|
||||||
export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||||
aspectRatio: number;
|
aspectRatio: number;
|
||||||
|
uploadFolder?: string | null;
|
||||||
}): Promise<Misskey.entities.DriveFile> {
|
}): Promise<Misskey.entities.DriveFile> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
|
||||||
file: image,
|
file: image,
|
||||||
aspectRatio: options.aspectRatio,
|
aspectRatio: options.aspectRatio,
|
||||||
|
uploadFolder: options.uploadFolder,
|
||||||
}, {
|
}, {
|
||||||
ok: x => {
|
ok: x => {
|
||||||
resolve(x);
|
resolve(x);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { defineAsyncComponent } from 'vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { MenuItem } from '@/types/menu';
|
||||||
|
|
||||||
function rename(file: Misskey.entities.DriveFile) {
|
function rename(file: Misskey.entities.DriveFile) {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
|
@ -66,7 +67,8 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
|
export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
return [{
|
return [{
|
||||||
text: i18n.ts.rename,
|
text: i18n.ts.rename,
|
||||||
icon: 'ti ti-forms',
|
icon: 'ti ti-forms',
|
||||||
|
@ -79,7 +81,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
|
||||||
text: i18n.ts.describeFile,
|
text: i18n.ts.describeFile,
|
||||||
icon: 'ti ti-text-caption',
|
icon: 'ti ti-text-caption',
|
||||||
action: () => describe(file),
|
action: () => describe(file),
|
||||||
}, null, {
|
}, ...isImage ? [{
|
||||||
|
text: i18n.ts.cropImage,
|
||||||
|
icon: 'ti ti-crop',
|
||||||
|
action: () => os.cropImage(file, {
|
||||||
|
aspectRatio: NaN,
|
||||||
|
uploadFolder: folder ? folder.id : folder
|
||||||
|
}),
|
||||||
|
}] : [], null, {
|
||||||
text: i18n.ts.createNoteFromTheFile,
|
text: i18n.ts.createNoteFromTheFile,
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
action: () => os.post({
|
action: () => os.post({
|
||||||
|
|
|
@ -44,11 +44,22 @@ async function setAntenna() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [{
|
function editAntenna() {
|
||||||
|
os.pageWindow('my/antennas/' + props.column.antennaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.selectAntenna,
|
text: i18n.ts.selectAntenna,
|
||||||
action: setAntenna,
|
action: setAntenna,
|
||||||
}];
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-settings',
|
||||||
|
text: i18n.ts.editAntenna,
|
||||||
|
action: editAntenna,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
function focus() {
|
function focus() {
|
||||||
|
|
|
@ -42,9 +42,20 @@ async function setList() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [{
|
function editList() {
|
||||||
|
os.pageWindow('my/lists/' + props.column.listId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.selectList,
|
text: i18n.ts.selectList,
|
||||||
action: setList,
|
action: setList,
|
||||||
}];
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-settings',
|
||||||
|
text: i18n.ts.editList,
|
||||||
|
action: editList,
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue