Compare commits
10 Commits
5b8249ebf6
...
e60abb509a
Author | SHA1 | Date |
---|---|---|
|
e60abb509a | |
|
5ac2116449 | |
|
66c3666d0c | |
|
7fe3b4f86c | |
|
1857052b32 | |
|
a503bc04a1 | |
|
795b8366b5 | |
|
2fcb50273d | |
|
70232d3d73 | |
|
979cfc1bcd |
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,3 +1,15 @@
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### General
|
||||||
|
-
|
||||||
|
|
||||||
|
### Client
|
||||||
|
-
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
|
||||||
|
- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727`
|
||||||
|
|
||||||
## 2025.4.1
|
## 2025.4.1
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -898,6 +898,10 @@ export interface Locale extends ILocale {
|
||||||
* ソフトウェア
|
* ソフトウェア
|
||||||
*/
|
*/
|
||||||
"software": string;
|
"software": string;
|
||||||
|
/**
|
||||||
|
* ソフトウェア名
|
||||||
|
*/
|
||||||
|
"softwareName": string;
|
||||||
/**
|
/**
|
||||||
* バージョン
|
* バージョン
|
||||||
*/
|
*/
|
||||||
|
@ -5879,6 +5883,10 @@ export interface Locale extends ILocale {
|
||||||
* サーバー応答なしのため停止中
|
* サーバー応答なしのため停止中
|
||||||
*/
|
*/
|
||||||
"autoSuspendedForNotResponding": string;
|
"autoSuspendedForNotResponding": string;
|
||||||
|
/**
|
||||||
|
* 配信停止中のソフトウェアであるため停止中
|
||||||
|
*/
|
||||||
|
"softwareSuspended": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
|
@ -6364,6 +6372,14 @@ export interface Locale extends ILocale {
|
||||||
* 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
|
* 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
|
||||||
*/
|
*/
|
||||||
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
|
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
|
||||||
|
/**
|
||||||
|
* 配信停止中のソフトウェア
|
||||||
|
*/
|
||||||
|
"deliverSuspendedSoftware": string;
|
||||||
|
/**
|
||||||
|
* 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。
|
||||||
|
*/
|
||||||
|
"deliverSuspendedSoftwareDescription": string;
|
||||||
};
|
};
|
||||||
"_accountMigration": {
|
"_accountMigration": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -220,6 +220,7 @@ silenceThisInstance: "サーバーをサイレンス"
|
||||||
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
||||||
operations: "操作"
|
operations: "操作"
|
||||||
software: "ソフトウェア"
|
software: "ソフトウェア"
|
||||||
|
softwareName: "ソフトウェア名"
|
||||||
version: "バージョン"
|
version: "バージョン"
|
||||||
metadata: "メタデータ"
|
metadata: "メタデータ"
|
||||||
withNFiles: "{n}つのファイル"
|
withNFiles: "{n}つのファイル"
|
||||||
|
@ -1479,6 +1480,7 @@ _delivery:
|
||||||
manuallySuspended: "手動停止中"
|
manuallySuspended: "手動停止中"
|
||||||
goneSuspended: "サーバー削除のため停止中"
|
goneSuspended: "サーバー削除のため停止中"
|
||||||
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
||||||
|
softwareSuspended: "配信停止中のソフトウェアであるため停止中"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1617,6 +1619,8 @@ _serverSettings:
|
||||||
openRegistration: "アカウントの作成をオープンにする"
|
openRegistration: "アカウントの作成をオープンにする"
|
||||||
openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。"
|
openRegistrationWarning: "登録を開放することはリスクが伴います。サーバーを常に監視し、トラブルが発生した際にすぐに対応できる体制がある場合のみオンにすることを推奨します。"
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
||||||
|
deliverSuspendedSoftware: "配信停止中のソフトウェア"
|
||||||
|
deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.4.1-rc.0",
|
"version": "2025.4.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class DeliverSuspendedSoftware1743403874305 {
|
||||||
|
name = 'DeliverSuspendedSoftware1743403874305'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "deliverSuspendedSoftware" jsonb NOT NULL DEFAULT '[]'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deliverSuspendedSoftware"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -169,6 +169,7 @@
|
||||||
"sanitize-html": "2.16.0",
|
"sanitize-html": "2.16.0",
|
||||||
"secure-json-parse": "3.0.2",
|
"secure-json-parse": "3.0.2",
|
||||||
"sharp": "0.34.1",
|
"sharp": "0.34.1",
|
||||||
|
"semver": "7.7.1",
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
|
|
|
@ -36,6 +36,7 @@ type TimelineOptions = {
|
||||||
excludeNoFiles?: boolean;
|
excludeNoFiles?: boolean;
|
||||||
excludeReplies?: boolean;
|
excludeReplies?: boolean;
|
||||||
excludePureRenotes: boolean;
|
excludePureRenotes: boolean;
|
||||||
|
ignoreAuthorFromUserSuspension?: boolean;
|
||||||
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,6 +140,23 @@ export class FanoutTimelineEndpointService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const parentFilter = filter;
|
||||||
|
filter = (note) => {
|
||||||
|
const noteJoined = note as MiNote & {
|
||||||
|
renoteUser: MiUser | null;
|
||||||
|
replyUser: MiUser | null;
|
||||||
|
};
|
||||||
|
if (!ps.ignoreAuthorFromUserSuspension) {
|
||||||
|
if (note.user!.isSuspended) return false;
|
||||||
|
}
|
||||||
|
if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false;
|
||||||
|
if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false;
|
||||||
|
|
||||||
|
return parentFilter(note);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const redisTimeline: MiNote[] = [];
|
const redisTimeline: MiNote[] = [];
|
||||||
let readFromRedis = 0;
|
let readFromRedis = 0;
|
||||||
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
||||||
|
|
|
@ -287,4 +287,26 @@ export class QueryService {
|
||||||
.andWhere(instanceSuspension('renoteUser'));
|
.andWhere(instanceSuspension('renoteUser'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Requirements: user replyUser renoteUser must be joined
|
||||||
|
@bindThis
|
||||||
|
public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
|
||||||
|
if (excludeAuthor) {
|
||||||
|
const brakets = (user: string) => new Brackets(qb => qb
|
||||||
|
.where(`note.${user}Id IS NULL`)
|
||||||
|
.orWhere(`user.id = ${user}.id`)
|
||||||
|
.orWhere(`${user}.isSuspended = FALSE`));
|
||||||
|
q
|
||||||
|
.andWhere(brakets('replyUser'))
|
||||||
|
.andWhere(brakets('renoteUser'));
|
||||||
|
} else {
|
||||||
|
const brakets = (user: string) => new Brackets(qb => qb
|
||||||
|
.where(`note.${user}Id IS NULL`)
|
||||||
|
.orWhere(`${user}.isSuspended = FALSE`));
|
||||||
|
q
|
||||||
|
.andWhere('user.isSuspended = FALSE')
|
||||||
|
.andWhere(brakets('replyUser'))
|
||||||
|
.andWhere(brakets('renoteUser'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,6 +235,7 @@ export class SearchService {
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
||||||
|
@ -297,11 +298,17 @@ export class SearchService {
|
||||||
])
|
])
|
||||||
: [new Set<string>(), new Set<string>()];
|
: [new Set<string>(), new Set<string>()];
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note');
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) });
|
query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) });
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
|
|
||||||
const notes = (await query.getMany()).filter(note => {
|
const notes = (await query.getMany()).filter(note => {
|
||||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
import { URL, domainToASCII } from 'node:url';
|
import { URL, domainToASCII } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
|
import semver from 'semver';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta, SoftwareSuspension } from '@/models/Meta.js';
|
||||||
|
import { MiInstance } from '@/models/Instance.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UtilityService {
|
export class UtilityService {
|
||||||
|
@ -143,4 +145,20 @@ export class UtilityService {
|
||||||
const host = this.extractDbHost(uri);
|
const host = this.extractDbHost(uri);
|
||||||
return this.isFederationAllowedHost(host);
|
return this.isFederationAllowedHost(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isDeliverSuspendedSoftware(software: Pick<MiInstance, 'softwareName' | 'softwareVersion'>): SoftwareSuspension | undefined {
|
||||||
|
if (software.softwareName == null) return undefined;
|
||||||
|
if (software.softwareVersion == null) {
|
||||||
|
// software version is null; suspend iff versionRange is *
|
||||||
|
return this.meta.deliverSuspendedSoftware.find(x =>
|
||||||
|
x.software === software.softwareName
|
||||||
|
&& x.versionRange.trim() === '*');
|
||||||
|
} else {
|
||||||
|
const softwareVersion = software.softwareVersion;
|
||||||
|
return this.meta.deliverSuspendedSoftware.find(x =>
|
||||||
|
x.software === software.softwareName
|
||||||
|
&& semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class InstanceEntityService {
|
||||||
me?: { id: MiUser['id']; } | null | undefined,
|
me?: { id: MiUser['id']; } | null | undefined,
|
||||||
): Promise<Packed<'FederationInstance'>> {
|
): Promise<Packed<'FederationInstance'>> {
|
||||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||||
|
const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
|
@ -41,8 +42,8 @@ export class InstanceEntityService {
|
||||||
followingCount: instance.followingCount,
|
followingCount: instance.followingCount,
|
||||||
followersCount: instance.followersCount,
|
followersCount: instance.followersCount,
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.suspensionState !== 'none',
|
isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended),
|
||||||
suspensionState: instance.suspensionState,
|
suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState,
|
||||||
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
|
|
|
@ -664,4 +664,14 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public googleAnalyticsMeasurementId: string | null;
|
public googleAnalyticsMeasurementId: string | null;
|
||||||
|
|
||||||
|
@Column('jsonb', {
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
|
public deliverSuspendedSoftware: SoftwareSuspension[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SoftwareSuspension = {
|
||||||
|
software: string,
|
||||||
|
versionRange: string,
|
||||||
|
};
|
||||||
|
|
|
@ -229,7 +229,6 @@ export class MiNote {
|
||||||
comment: '[Denormalized]',
|
comment: '[Denormalized]',
|
||||||
})
|
})
|
||||||
public renoteUserHost: string | null;
|
public renoteUserHost: string | null;
|
||||||
//#endregion
|
|
||||||
|
|
||||||
constructor(data: Partial<MiNote>) {
|
constructor(data: Partial<MiNote>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const packedFederationInstanceSchema = {
|
||||||
suspensionState: {
|
suspensionState: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
|
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'],
|
||||||
},
|
},
|
||||||
isBlocked: {
|
isBlocked: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
@ -71,6 +71,15 @@ export class DeliverProcessorService {
|
||||||
return 'skip (suspended)';
|
return 'skip (suspended)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const i = await (this.meta.enableStatsForFederatedInstances
|
||||||
|
? this.federatedInstanceService.fetchOrRegister(host)
|
||||||
|
: this.federatedInstanceService.fetch(host));
|
||||||
|
|
||||||
|
// suspend server by software
|
||||||
|
if (i != null && this.utilityService.isDeliverSuspendedSoftware(i)) {
|
||||||
|
return 'skip (software suspended)';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
||||||
|
|
||||||
|
@ -79,10 +88,6 @@ export class DeliverProcessorService {
|
||||||
|
|
||||||
// Update instance stats
|
// Update instance stats
|
||||||
process.nextTick(async () => {
|
process.nextTick(async () => {
|
||||||
const i = await (this.meta.enableStatsForFederatedInstances
|
|
||||||
? this.federatedInstanceService.fetchOrRegister(host)
|
|
||||||
: this.federatedInstanceService.fetch(host));
|
|
||||||
|
|
||||||
if (i == null) return;
|
if (i == null) return;
|
||||||
|
|
||||||
if (i.isNotResponding) {
|
if (i.isNotResponding) {
|
||||||
|
|
|
@ -528,6 +528,24 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deliverSuspendedSoftware: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
software: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
versionRange: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -672,6 +690,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
federation: instance.federation,
|
federation: instance.federation,
|
||||||
federationHosts: instance.federationHosts,
|
federationHosts: instance.federationHosts,
|
||||||
|
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,6 +185,17 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deliverSuspendedSoftware: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
software: { type: 'string' },
|
||||||
|
versionRange: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['software', 'versionRange'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -671,6 +682,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.federation = ps.federation;
|
set.federation = ps.federation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.deliverSuspendedSoftware !== undefined) {
|
||||||
|
set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(ps.federationHosts)) {
|
if (Array.isArray(ps.federationHosts)) {
|
||||||
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
|
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
|
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
|
@ -122,6 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
|
@ -85,9 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
|
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
// this.queryService.generateSuspendedUserQueryForNote(query); // To avoid problems with removing notes, ignoring suspended user for now
|
||||||
|
if (me) {
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
|
@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
|
|
||||||
const notes = (await query.getMany()).filter(note => {
|
const notes = (await query.getMany()).filter(note => {
|
||||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
|
|
@ -244,6 +244,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
|
@ -157,6 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
|
@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
|
@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
|
@ -185,6 +185,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
|
@ -103,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
|
|
||||||
const notes = (await query.getMany()).filter(note => {
|
const notes = (await query.getMany()).filter(note => {
|
||||||
if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
|
if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
|
||||||
|
|
|
@ -130,6 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
useDbFallback: true,
|
useDbFallback: true,
|
||||||
ignoreAuthorFromMute: true,
|
ignoreAuthorFromMute: true,
|
||||||
ignoreAuthorFromInstanceBlock: true,
|
ignoreAuthorFromInstanceBlock: true,
|
||||||
|
ignoreAuthorFromUserSuspension: true,
|
||||||
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
||||||
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
||||||
excludePureRenotes: !ps.withRenotes,
|
excludePureRenotes: !ps.withRenotes,
|
||||||
|
@ -186,6 +187,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query, true);
|
this.queryService.generateBlockedHostQueryForNote(query, true);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query, true);
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
|
|
@ -99,10 +99,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
.andWhere('reaction.userId = :userId', { userId: ps.userId })
|
.andWhere('reaction.userId = :userId', { userId: ps.userId })
|
||||||
.leftJoinAndSelect('reaction.note', 'note');
|
.leftJoinAndSelect('reaction.note', 'note')
|
||||||
|
.leftJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBlockedHostQueryForNote(query);
|
||||||
|
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||||
|
|
||||||
const reactions = (await query
|
const reactions = (await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
|
|
|
@ -909,7 +909,7 @@ describe('クリップ', () => {
|
||||||
assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]);
|
assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートはhideされて返ってくる)', async () => {
|
test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートは含まれない)', async () => {
|
||||||
const publicClip = await create({ isPublic: true });
|
const publicClip = await create({ isPublic: true });
|
||||||
await addNote({ clipId: publicClip.id, noteId: aliceNote.id });
|
await addNote({ clipId: publicClip.id, noteId: aliceNote.id });
|
||||||
await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id });
|
await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id });
|
||||||
|
@ -919,8 +919,6 @@ describe('クリップ', () => {
|
||||||
const res = await notes({ clipId: publicClip.id }, { user: undefined });
|
const res = await notes({ clipId: publicClip.id }, { user: undefined });
|
||||||
const expects = [
|
const expects = [
|
||||||
aliceNote, aliceHomeNote,
|
aliceNote, aliceHomeNote,
|
||||||
// 認証なしだと非公開ノートは結果には含むけどhideされる。
|
|
||||||
hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote),
|
|
||||||
];
|
];
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
res.sort(compareBy(s => s.id)).map(x => x.id),
|
res.sort(compareBy(s => s.id)).map(x => x.id),
|
||||||
|
|
|
@ -7,8 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
|
||||||
import MkAd from '@/components/global/MkAd.vue';
|
import MkAd from '@/components/global/MkAd.vue';
|
||||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -19,7 +17,7 @@ import { getDateText } from '@/utility/timeline-date-separate.js';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array as PropType<MisskeyEntity[]>,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
|
|
|
@ -87,7 +87,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/>
|
<MkPoll
|
||||||
|
v-if="appearNote.poll"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:multiple="appearNote.poll.multiple"
|
||||||
|
:expiresAt="appearNote.poll.expiresAt"
|
||||||
|
:choices="$appearNote.pollChoices"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:emojiUrls="appearNote.emojis"
|
||||||
|
:class="$style.poll"
|
||||||
|
/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" style="margin-top: 6px;" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
<MkReactionsViewer
|
||||||
|
v-if="appearNote.reactionAcceptance !== 'likeOnly'"
|
||||||
|
style="margin-top: 6px;"
|
||||||
|
:reactions="$appearNote.reactions"
|
||||||
|
:reactionEmojis="$appearNote.reactionEmojis"
|
||||||
|
:myReaction="$appearNote.myReaction"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:maxNumber="16"
|
||||||
|
@mockUpdateMyReaction="emitUpdReaction"
|
||||||
|
>
|
||||||
<template #more>
|
<template #more>
|
||||||
<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
|
<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
@ -125,11 +143,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
<i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number($appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
|
@ -176,7 +194,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue';
|
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
|
@ -245,13 +263,13 @@ const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
const note = ref(deepClone(props.note));
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
|
@ -263,11 +281,19 @@ if (noteViewInterruptors.length > 0) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result as Misskey.entities.Note;
|
note = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
|
const appearNote = getAppearNote(note);
|
||||||
|
const $appearNote = reactive({
|
||||||
|
reactions: appearNote.reactions,
|
||||||
|
reactionCount: appearNote.reactionCount,
|
||||||
|
reactionEmojis: appearNote.reactionEmojis,
|
||||||
|
myReaction: appearNote.myReaction,
|
||||||
|
pollChoices: appearNote.poll?.choices,
|
||||||
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const menuButton = useTemplateRef('menuButton');
|
const menuButton = useTemplateRef('menuButton');
|
||||||
|
@ -275,32 +301,31 @@ const renoteButton = useTemplateRef('renoteButton');
|
||||||
const renoteTime = useTemplateRef('renoteTime');
|
const renoteTime = useTemplateRef('renoteTime');
|
||||||
const reactButton = useTemplateRef('reactButton');
|
const reactButton = useTemplateRef('reactButton');
|
||||||
const clipButton = useTemplateRef('clipButton');
|
const clipButton = useTemplateRef('clipButton');
|
||||||
const appearNote = computed(() => getAppearNote(note.value));
|
|
||||||
const galleryEl = useTemplateRef('galleryEl');
|
const galleryEl = useTemplateRef('galleryEl');
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
|
||||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null);
|
||||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
const isLong = shouldCollapsed(appearNote, urls.value ?? []);
|
||||||
const collapsed = ref(appearNote.value.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords, true));
|
||||||
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i?.id));
|
||||||
const renoteCollapsed = ref(
|
const renoteCollapsed = ref(
|
||||||
prefer.s.collapseRenotes && isRenote && (
|
prefer.s.collapseRenotes && isRenote && (
|
||||||
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
($i && ($i.id === note.userId || $i.id === appearNote.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
||||||
(appearNote.value.myReaction != null)
|
($appearNote.myReaction != null)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
type: 'lookup',
|
type: 'lookup',
|
||||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
url: `https://${host}/notes/${appearNote.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||||
|
@ -357,7 +382,7 @@ const keymap = {
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (renoteCollapsed.value) {
|
if (renoteCollapsed.value) {
|
||||||
renoteCollapsed.value = false;
|
renoteCollapsed.value = false;
|
||||||
} else if (appearNote.value.cw != null) {
|
} else if (appearNote.cw != null) {
|
||||||
showContent.value = !showContent.value;
|
showContent.value = !showContent.value;
|
||||||
} else if (isLong) {
|
} else if (isLong) {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
|
@ -380,10 +405,10 @@ const keymap = {
|
||||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
|
@ -392,20 +417,20 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
watch(() => props.note, (to) => {
|
watch(() => props.note, (to) => {
|
||||||
note.value = deepClone(to);
|
note = deepClone(to);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
} else {
|
} else {
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
parentNote: note,
|
parentNote: note,
|
||||||
isDeletedRef: isDeleted,
|
$note: $appearNote,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.mock) {
|
if (!props.mock) {
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 11,
|
limit: 11,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -416,19 +441,19 @@ if (!props.mock) {
|
||||||
const { dispose } = os.popup(MkUsersTooltip, {
|
const { dispose } = os.popup(MkUsersTooltip, {
|
||||||
showing,
|
showing,
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.renoteCount,
|
count: appearNote.renoteCount,
|
||||||
targetElement: renoteButton.value,
|
targetElement: renoteButton.value,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
useTooltip(reactButton, async (showing) => {
|
useTooltip(reactButton, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: appearNote.value.reactionCount,
|
_cacheKey_: $appearNote.reactionCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
@ -439,7 +464,7 @@ if (!props.mock) {
|
||||||
showing,
|
showing,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.reactionCount,
|
count: $appearNote.reactionCount,
|
||||||
targetElement: reactButton.value!,
|
targetElement: reactButton.value!,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -452,7 +477,7 @@ function renote(viaKeyboard = false) {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
|
||||||
os.popupMenu(menu, renoteButton.value, {
|
os.popupMenu(menu, renoteButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
});
|
});
|
||||||
|
@ -464,8 +489,8 @@ function reply(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.channel,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -474,7 +499,7 @@ function reply(): void {
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -482,10 +507,10 @@ function react(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
});
|
});
|
||||||
|
@ -501,7 +526,7 @@ function react(): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
|
reactionPicker.show(reactButton.value ?? null, note, async (reaction) => {
|
||||||
if (prefer.s.confirmOnReact) {
|
if (prefer.s.confirmOnReact) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -519,16 +544,16 @@ function react(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
|
||||||
claimAchievement('reactWithoutRead');
|
claimAchievement('reactWithoutRead');
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -537,8 +562,8 @@ function react(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
function undoReact(): void {
|
||||||
const oldReaction = targetNote.myReaction;
|
const oldReaction = $appearNote.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -547,15 +572,15 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: targetNote.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if ($appearNote.myReaction == null) {
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
undoReact(appearNote.value);
|
undoReact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,7 +596,7 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -581,7 +606,7 @@ function showMenu(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,7 +615,7 @@ async function clip(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(): void {
|
function showRenoteMenu(): void {
|
||||||
|
@ -605,7 +630,7 @@ function showRenoteMenu(): void {
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: note.value.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
|
@ -616,23 +641,23 @@ function showRenoteMenu(): void {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts.renoteDetails,
|
text: i18n.ts.renoteDetails,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
to: notePage(note.value),
|
to: notePage(note),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMyRenote) {
|
if (isMyRenote) {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
renoteDetailsMenu,
|
renoteDetailsMenu,
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getUnrenote(),
|
getUnrenote(),
|
||||||
], renoteTime.value);
|
], renoteTime.value);
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
renoteDetailsMenu,
|
renoteDetailsMenu,
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
|
||||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||||
], renoteTime.value);
|
], renoteTime.value);
|
||||||
}
|
}
|
||||||
|
@ -656,7 +681,7 @@ function focusAfter() {
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
misskeyApi('promo/read', {
|
misskeyApi('promo/read', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll
|
||||||
|
v-if="appearNote.poll"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:multiple="appearNote.poll.multiple"
|
||||||
|
:expiresAt="appearNote.poll.expiresAt"
|
||||||
|
:choices="$appearNote.pollChoices"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:emojiUrls="appearNote.emojis"
|
||||||
|
:class="$style.poll"
|
||||||
|
/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,7 +133,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" style="margin-top: 6px;" :note="appearNote"/>
|
<MkReactionsViewer
|
||||||
|
v-if="appearNote.reactionAcceptance !== 'likeOnly'"
|
||||||
|
style="margin-top: 6px;"
|
||||||
|
:reactions="$appearNote.reactions"
|
||||||
|
:reactionEmojis="$appearNote.reactionEmojis"
|
||||||
|
:myReaction="$appearNote.myReaction"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:maxNumber="16"
|
||||||
|
@mockUpdateMyReaction="emitUpdReaction"
|
||||||
|
/>
|
||||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||||
<i class="ti ti-arrow-back-up"></i>
|
<i class="ti ti-arrow-back-up"></i>
|
||||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||||
|
@ -143,11 +161,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
<i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number($appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
|
@ -182,9 +200,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
||||||
<div :class="$style.reactionTabs">
|
<div :class="$style.reactionTabs">
|
||||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
<button v-for="reaction in Object.keys($appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
||||||
<MkReactionIcon :reaction="reaction"/>
|
<MkReactionIcon :reaction="reaction"/>
|
||||||
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
|
<span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
|
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
|
||||||
|
@ -211,13 +229,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
|
import { computed, inject, onMounted, provide, reactive, ref, useTemplateRef } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
|
||||||
import type { Keymap } from '@/utility/hotkey.js';
|
import type { Keymap } from '@/utility/hotkey.js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
@ -267,13 +284,13 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
const note = ref(deepClone(props.note));
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
|
@ -285,11 +302,19 @@ if (noteViewInterruptors.length > 0) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result as Misskey.entities.Note;
|
note = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
|
const appearNote = getAppearNote(note);
|
||||||
|
const $appearNote = reactive({
|
||||||
|
reactions: appearNote.reactions,
|
||||||
|
reactionCount: appearNote.reactionCount,
|
||||||
|
reactionEmojis: appearNote.reactionEmojis,
|
||||||
|
myReaction: appearNote.myReaction,
|
||||||
|
pollChoices: appearNote.poll?.choices,
|
||||||
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const menuButton = useTemplateRef('menuButton');
|
const menuButton = useTemplateRef('menuButton');
|
||||||
|
@ -297,24 +322,23 @@ const renoteButton = useTemplateRef('renoteButton');
|
||||||
const renoteTime = useTemplateRef('renoteTime');
|
const renoteTime = useTemplateRef('renoteTime');
|
||||||
const reactButton = useTemplateRef('reactButton');
|
const reactButton = useTemplateRef('reactButton');
|
||||||
const clipButton = useTemplateRef('clipButton');
|
const clipButton = useTemplateRef('clipButton');
|
||||||
const appearNote = computed(() => getAppearNote(note.value));
|
|
||||||
const galleryEl = useTemplateRef('galleryEl');
|
const galleryEl = useTemplateRef('galleryEl');
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||||
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null;
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i?.id);
|
||||||
|
|
||||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
type: 'lookup',
|
type: 'lookup',
|
||||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
url: `https://${host}/notes/${appearNote.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
|
@ -328,7 +352,7 @@ const keymap = {
|
||||||
},
|
},
|
||||||
'o': () => galleryEl.value?.openGallery(),
|
'o': () => galleryEl.value?.openGallery(),
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (appearNote.value.cw != null) {
|
if (appearNote.cw != null) {
|
||||||
showContent.value = !showContent.value;
|
showContent.value = !showContent.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -341,10 +365,10 @@ const keymap = {
|
||||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
|
@ -354,19 +378,19 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
const tab = ref(props.initialTab);
|
const tab = ref(props.initialTab);
|
||||||
const reactionTabType = ref<string | null>(null);
|
const reactionTabType = ref<string | null>(null);
|
||||||
|
|
||||||
const renotesPagination = computed<Paging>(() => ({
|
const renotesPagination = computed(() => ({
|
||||||
endpoint: 'notes/renotes',
|
endpoint: 'notes/renotes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const reactionsPagination = computed<Paging>(() => ({
|
const reactionsPagination = computed(() => ({
|
||||||
endpoint: 'notes/reactions',
|
endpoint: 'notes/reactions',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
type: reactionTabType.value,
|
type: reactionTabType.value,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -374,12 +398,12 @@ const reactionsPagination = computed<Paging>(() => ({
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
parentNote: note,
|
parentNote: note,
|
||||||
isDeletedRef: isDeleted,
|
$note: $appearNote,
|
||||||
});
|
});
|
||||||
|
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 11,
|
limit: 11,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -390,19 +414,19 @@ useTooltip(renoteButton, async (showing) => {
|
||||||
const { dispose } = os.popup(MkUsersTooltip, {
|
const { dispose } = os.popup(MkUsersTooltip, {
|
||||||
showing,
|
showing,
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.renoteCount,
|
count: appearNote.renoteCount,
|
||||||
targetElement: renoteButton.value,
|
targetElement: renoteButton.value,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
useTooltip(reactButton, async (showing) => {
|
useTooltip(reactButton, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: appearNote.value.reactionCount,
|
_cacheKey_: $appearNote.reactionCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
@ -413,7 +437,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
showing,
|
showing,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.reactionCount,
|
count: $appearNote.reactionCount,
|
||||||
targetElement: reactButton.value!,
|
targetElement: reactButton.value!,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -425,7 +449,7 @@ function renote() {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
const { menu } = getRenoteMenu({ note: note, renoteButton });
|
||||||
os.popupMenu(menu, renoteButton.value);
|
os.popupMenu(menu, renoteButton.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,8 +457,8 @@ function reply(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.channel,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -443,14 +467,14 @@ function reply(): void {
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
});
|
});
|
||||||
|
@ -466,7 +490,7 @@ function react(): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
|
reactionPicker.show(reactButton.value ?? null, note, async (reaction) => {
|
||||||
if (prefer.s.confirmOnReact) {
|
if (prefer.s.confirmOnReact) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -479,15 +503,15 @@ function react(): void {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${appearNote.value.id}`, {
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
|
||||||
claimAchievement('reactWithoutRead');
|
claimAchievement('reactWithoutRead');
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -505,10 +529,10 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if (appearNote.myReaction == null) {
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
undoReact(appearNote.value);
|
undoReact(appearNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,18 +544,18 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(): void {
|
function showMenu(): void {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted });
|
||||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip(): Promise<void> {
|
async function clip(): Promise<void> {
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(): void {
|
function showRenoteMenu(): void {
|
||||||
|
@ -543,7 +567,7 @@ function showRenoteMenu(): void {
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: note.value.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
|
@ -563,7 +587,7 @@ const repliesLoaded = ref(false);
|
||||||
function loadReplies() {
|
function loadReplies() {
|
||||||
repliesLoaded.value = true;
|
repliesLoaded.value = true;
|
||||||
misskeyApi('notes/children', {
|
misskeyApi('notes/children', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
replies.value = res;
|
replies.value = res;
|
||||||
|
@ -574,9 +598,9 @@ const conversationLoaded = ref(false);
|
||||||
|
|
||||||
function loadConversation() {
|
function loadConversation() {
|
||||||
conversationLoaded.value = true;
|
conversationLoaded.value = true;
|
||||||
if (appearNote.value.replyId == null) return;
|
if (appearNote.replyId == null) return;
|
||||||
misskeyApi('notes/conversation', {
|
misskeyApi('notes/conversation', {
|
||||||
noteId: appearNote.value.replyId,
|
noteId: appearNote.replyId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
conversation.value = res.reverse();
|
conversation.value = res.reverse();
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
<div v-else-if="paginator.items.value.size === 0" key="_empty_">
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
<slot name="empty">
|
<slot name="empty">
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img :src="infoImageUrl" draggable="false"/>
|
<img :src="infoImageUrl" draggable="false"/>
|
||||||
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:moveClass=" $style.transition_x_move"
|
:moveClass=" $style.transition_x_move"
|
||||||
tag="div"
|
tag="div"
|
||||||
>
|
>
|
||||||
<template v-for="(notification, i) in Array.from(paginator.items.value.values())" :key="notification.id">
|
<template v-for="(notification, i) in paginator.items.value" :key="notification.id">
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
|
||||||
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
|
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
<div v-else-if="paginator.items.value.size === 0" key="_empty_">
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
<slot name="empty">
|
<slot name="empty">
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img :src="infoImageUrl" draggable="false"/>
|
<img :src="infoImageUrl" draggable="false"/>
|
||||||
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
</div>
|
</div>
|
||||||
<slot :items="Array.from(paginator.items.value.values())" :fetching="paginator.fetching.value || paginator.moreFetching.value"></slot>
|
<slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.moreFetching.value"></slot>
|
||||||
<div v-show="!pagination.reversed && paginator.canFetchMore.value" key="_more_">
|
<div v-show="!pagination.reversed && paginator.canFetchMore.value" key="_more_">
|
||||||
<MkButton v-if="!paginator.moreFetching.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="paginator.moreFetching.value" primary rounded @click="paginator.fetchOlder">
|
<MkButton v-if="!paginator.moreFetching.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="paginator.moreFetching.value" primary rounded @click="paginator.fetchOlder">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ [$style.done]: closed || isVoted }">
|
<div :class="{ [$style.done]: closed || isVoted }">
|
||||||
<ul :class="$style.choices">
|
<ul :class="$style.choices">
|
||||||
<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
|
<li v-for="(choice, i) in choices" :key="i" :class="$style.choice" @click="vote(i)">
|
||||||
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||||
<span :class="$style.fg">
|
<span :class="$style.fg">
|
||||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
|
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
|
||||||
|
@ -40,7 +40,9 @@ import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
poll: NonNullable<Misskey.entities.Note['poll']>;
|
multiple: NonNullable<Misskey.entities.Note['poll']>['multiple'];
|
||||||
|
expiresAt: NonNullable<Misskey.entities.Note['poll']>['expiresAt'];
|
||||||
|
choices: NonNullable<Misskey.entities.Note['poll']>['choices'];
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
emojiUrls?: Record<string, string>;
|
emojiUrls?: Record<string, string>;
|
||||||
author?: Misskey.entities.UserLite;
|
author?: Misskey.entities.UserLite;
|
||||||
|
@ -48,9 +50,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
const remaining = ref(-1);
|
const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
|
const isVoted = computed(() => !props.multiple && props.choices.some(c => c.isVoted));
|
||||||
const timer = computed(() => i18n.tsx._poll[
|
const timer = computed(() => i18n.tsx._poll[
|
||||||
remaining.value >= 86400 ? 'remainingDays' :
|
remaining.value >= 86400 ? 'remainingDays' :
|
||||||
remaining.value >= 3600 ? 'remainingHours' :
|
remaining.value >= 3600 ? 'remainingHours' :
|
||||||
|
@ -70,9 +72,9 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 期限付きアンケート
|
// 期限付きアンケート
|
||||||
if (props.poll.expiresAt) {
|
if (props.expiresAt) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
||||||
if (remaining.value === 0) {
|
if (remaining.value === 0) {
|
||||||
showResult.value = true;
|
showResult.value = true;
|
||||||
}
|
}
|
||||||
|
@ -91,7 +93,7 @@ const vote = async (id) => {
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
|
text: i18n.tsx.voteConfirm({ choice: props.choices[id].text }),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ const vote = async (id) => {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
choice: id,
|
choice: id,
|
||||||
});
|
});
|
||||||
if (!showResult.value) showResult.value = !props.poll.multiple;
|
if (!showResult.value) showResult.value = !props.multiple;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,7 @@ import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -883,12 +884,15 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
posting.value = true;
|
posting.value = true;
|
||||||
misskeyApi('notes/create', postData, token).then(() => {
|
misskeyApi('notes/create', postData, token).then((res) => {
|
||||||
if (props.freezeAfterPosted) {
|
if (props.freezeAfterPosted) {
|
||||||
posted.value = true;
|
posted.value = true;
|
||||||
} else {
|
} else {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalEvents.emit('notePosted', res.createdNote);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
deleteDraft();
|
deleteDraft();
|
||||||
emit('posted');
|
emit('posted');
|
||||||
|
|
|
@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="buttonEl"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
|
:class="[$style.root, { [$style.reacted]: myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
@contextmenu.prevent.stop="menu"
|
@contextmenu.prevent.stop="menu"
|
||||||
>
|
>
|
||||||
<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||||
<span :class="$style.count">{{ count }}</span>
|
<span :class="$style.count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -29,7 +29,6 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import { useTooltip } from '@/use/use-tooltip.js';
|
import { useTooltip } from '@/use/use-tooltip.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
import { claimAchievement } from '@/utility/achievements.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as sound from '@/utility/sound.js';
|
import * as sound from '@/utility/sound.js';
|
||||||
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
||||||
|
@ -39,10 +38,12 @@ import { DI } from '@/di.js';
|
||||||
import { noteEvents } from '@/use/use-note-capture.js';
|
import { noteEvents } from '@/use/use-note-capture.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
noteId: Misskey.entities.Note['id'];
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
count: number;
|
count: number;
|
||||||
isInitial: boolean;
|
isInitial: boolean;
|
||||||
note: Misskey.entities.Note;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const mock = inject(DI.mock, false);
|
const mock = inject(DI.mock, false);
|
||||||
|
@ -57,14 +58,16 @@ const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./,
|
||||||
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
|
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
|
||||||
|
|
||||||
const canToggle = computed(() => {
|
const canToggle = computed(() => {
|
||||||
return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
// TODO
|
||||||
|
//return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
||||||
|
return !props.reaction.match(/@\w/) && $i && emoji.value;
|
||||||
});
|
});
|
||||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||||
|
|
||||||
async function toggleReaction() {
|
async function toggleReaction() {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -82,19 +85,19 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`unreacted:${props.note.id}`, {
|
noteEvents.emit(`unreacted:${props.noteId}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
emoji: emoji.value,
|
emoji: emoji.value,
|
||||||
});
|
});
|
||||||
if (oldReaction !== props.reaction) {
|
if (oldReaction !== props.reaction) {
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${props.note.id}`, {
|
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
emoji: emoji.value,
|
emoji: emoji.value,
|
||||||
|
@ -120,18 +123,19 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
noteEvents.emit(`reacted:${props.note.id}`, {
|
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||||
userId: $i!.id,
|
userId: $i!.id,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
emoji: emoji.value,
|
emoji: emoji.value,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
// TODO: 上位コンポーネントでやる
|
||||||
claimAchievement('reactWithoutRead');
|
//if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
||||||
}
|
// claimAchievement('reactWithoutRead');
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +179,7 @@ onMounted(() => {
|
||||||
if (!mock) {
|
if (!mock) {
|
||||||
useTooltip(buttonEl, async (showing) => {
|
useTooltip(buttonEl, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: props.count,
|
_cacheKey_: props.count,
|
||||||
|
|
|
@ -13,7 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:moveClass="$style.transition_x_move"
|
:moveClass="$style.transition_x_move"
|
||||||
tag="div" :class="$style.root"
|
tag="div" :class="$style.root"
|
||||||
>
|
>
|
||||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
|
<XReaction
|
||||||
|
v-for="[reaction, count] in _reactions"
|
||||||
|
:key="reaction"
|
||||||
|
:reaction="reaction"
|
||||||
|
:reactionEmojis="props.reactionEmojis"
|
||||||
|
:count="count"
|
||||||
|
:isInitial="initialReactions.has(reaction)"
|
||||||
|
:noteId="props.noteId"
|
||||||
|
:myReaction="props.myReaction"
|
||||||
|
@reactionToggled="onMockToggleReaction"
|
||||||
|
/>
|
||||||
<slot v-if="hasMoreReactions" name="more"/>
|
<slot v-if="hasMoreReactions" name="more"/>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
@ -27,7 +37,10 @@ import { prefer } from '@/preferences.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
noteId: Misskey.entities.Note['id'];
|
||||||
|
reactions: Misskey.entities.Note['reactions'];
|
||||||
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
maxNumber?: number;
|
maxNumber?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
maxNumber: Infinity,
|
maxNumber: Infinity,
|
||||||
|
@ -39,33 +52,33 @@ const emit = defineEmits<{
|
||||||
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
|
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const initialReactions = new Set(Object.keys(props.note.reactions));
|
const initialReactions = new Set(Object.keys(props.reactions));
|
||||||
|
|
||||||
const reactions = ref<[string, number][]>([]);
|
const _reactions = ref<[string, number][]>([]);
|
||||||
const hasMoreReactions = ref(false);
|
const hasMoreReactions = ref(false);
|
||||||
|
|
||||||
if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
|
if (props.myReaction && !Object.keys(_reactions.value).includes(props.myReaction)) {
|
||||||
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
|
_reactions.value[props.myReaction] = props.reactions[props.myReaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMockToggleReaction(emoji: string, count: number) {
|
function onMockToggleReaction(emoji: string, count: number) {
|
||||||
if (!mock) return;
|
if (!mock) return;
|
||||||
|
|
||||||
const i = reactions.value.findIndex((item) => item[0] === emoji);
|
const i = _reactions.value.findIndex((item) => item[0] === emoji);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
|
|
||||||
emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
|
emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
||||||
let newReactions: [string, number][] = [];
|
let newReactions: [string, number][] = [];
|
||||||
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
||||||
|
|
||||||
for (let i = 0; i < reactions.value.length; i++) {
|
for (let i = 0; i < _reactions.value.length; i++) {
|
||||||
const reaction = reactions.value[i][0];
|
const reaction = _reactions.value[i][0];
|
||||||
if (reaction in newSource && newSource[reaction] !== 0) {
|
if (reaction in newSource && newSource[reaction] !== 0) {
|
||||||
reactions.value[i][1] = newSource[reaction];
|
_reactions.value[i][1] = newSource[reaction];
|
||||||
newReactions.push(reactions.value[i]);
|
newReactions.push(_reactions.value[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,11 +92,11 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
||||||
|
|
||||||
newReactions = newReactions.slice(0, props.maxNumber);
|
newReactions = newReactions.slice(0, props.maxNumber);
|
||||||
|
|
||||||
if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
if (props.myReaction && !newReactions.map(([x]) => x).includes(props.myReaction)) {
|
||||||
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
newReactions.push([props.myReaction, newSource[props.myReaction]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions.value = newReactions;
|
_reactions.value = newReactions;
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
<div v-else-if="paginator.items.value.size === 0" key="_empty_">
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
<slot name="empty">
|
<slot name="empty">
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img :src="infoImageUrl" draggable="false"/>
|
<img :src="infoImageUrl" draggable="false"/>
|
||||||
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:moveClass=" $style.transition_x_move"
|
:moveClass=" $style.transition_x_move"
|
||||||
tag="div"
|
tag="div"
|
||||||
>
|
>
|
||||||
<template v-for="(note, i) in Array.from(paginator.items.value.values())" :key="note.id">
|
<template v-for="(note, i) in paginator.items.value" :key="note.id">
|
||||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
||||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
<div :class="$style.ad">
|
<div :class="$style.ad">
|
||||||
|
@ -67,6 +67,7 @@ import MkNote from '@/components/MkNote.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||||
|
@ -122,6 +123,15 @@ if (!store.s.realtimeMode) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalEvents.on('notePosted', (note: Misskey.entities.Note) => {
|
||||||
|
const isTop = rootEl.value == null ? false : isHeadVisible(rootEl.value, 16);
|
||||||
|
if (isTop) {
|
||||||
|
paginator.fetchNewer({
|
||||||
|
toQueue: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function releaseQueue() {
|
function releaseQueue() {
|
||||||
paginator.releaseQueue();
|
paginator.releaseQueue();
|
||||||
scrollToTop(rootEl.value);
|
scrollToTop(rootEl.value);
|
||||||
|
@ -311,6 +321,7 @@ refreshEndpointAndChannel();
|
||||||
|
|
||||||
const paginator = usePagination({
|
const paginator = usePagination({
|
||||||
ctx: paginationQuery,
|
ctx: paginationQuery,
|
||||||
|
useShallowRef: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -10,4 +10,5 @@ export const globalEvents = new EventEmitter<{
|
||||||
themeChanging: () => void;
|
themeChanging: () => void;
|
||||||
themeChanged: () => void;
|
themeChanged: () => void;
|
||||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||||
|
notePosted: (note: Misskey.entities.Note) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -230,6 +230,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
|
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||||
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
|
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-list"></i></template>
|
||||||
|
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div :class="$style.metadataRoot" class="_gaps_s">
|
||||||
|
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
|
||||||
|
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
|
||||||
|
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
|
||||||
|
<div :class="$style.dragItemForm">
|
||||||
|
<FormSplit :minWidth="200">
|
||||||
|
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
|
||||||
|
</MkInput>
|
||||||
|
</FormSplit>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
@ -368,10 +393,12 @@ const urlPreviewForm = useForm({
|
||||||
const federationForm = useForm({
|
const federationForm = useForm({
|
||||||
federation: meta.federation,
|
federation: meta.federation,
|
||||||
federationHosts: meta.federationHosts.join('\n'),
|
federationHosts: meta.federationHosts.join('\n'),
|
||||||
|
deliverSuspendedSoftware: meta.deliverSuspendedSoftware,
|
||||||
}, async (state) => {
|
}, async (state) => {
|
||||||
await os.apiWithDialog('admin/update-meta', {
|
await os.apiWithDialog('admin/update-meta', {
|
||||||
federation: state.federation,
|
federation: state.federation,
|
||||||
federationHosts: state.federationHosts.split('\n'),
|
federationHosts: state.federationHosts.split('\n'),
|
||||||
|
deliverSuspendedSoftware: state.deliverSuspendedSoftware,
|
||||||
});
|
});
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
});
|
});
|
||||||
|
@ -398,4 +425,53 @@ definePage(() => ({
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metadataRoot {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldDragItem {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
|
||||||
|
@container (max-width: 452px) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragItemHandle {
|
||||||
|
cursor: grab;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 8px 0 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragItemRemove {
|
||||||
|
@extend .dragItemHandle;
|
||||||
|
|
||||||
|
color: #ff2a2a;
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragItemForm {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
|
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
|
||||||
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
|
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance || suspensionState == 'softwareSuspended'" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
||||||
|
@ -164,7 +164,7 @@ const tab = ref('overview');
|
||||||
const chartSrc = ref<ChartSrc>('instance-requests');
|
const chartSrc = ref<ChartSrc>('instance-requests');
|
||||||
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
||||||
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||||
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
|
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'>('none');
|
||||||
const isBlocked = ref(false);
|
const isBlocked = ref(false);
|
||||||
const isSilenced = ref(false);
|
const isSilenced = ref(false);
|
||||||
const isMediaSilenced = ref(false);
|
const isMediaSilenced = ref(false);
|
||||||
|
|
|
@ -27,7 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
|
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.reactionCount > 0" :class="$style.reactions">
|
<div v-if="note.reactionCount > 0" :class="$style.reactions">
|
||||||
<MkReactionsViewer :note="note" :maxNumber="16"/>
|
<!-- TODO -->
|
||||||
|
<!--<MkReactionsViewer :note="note" :maxNumber="16"/>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, reactive, watch } from 'vue';
|
||||||
import type { Reactive } from 'vue';
|
import type { Reactive } from 'vue';
|
||||||
|
import { deepEqual } from '@/utility/deep-equal';
|
||||||
|
|
||||||
function copy<T>(v: T): T {
|
function copy<T>(v: T): T {
|
||||||
return JSON.parse(JSON.stringify(v));
|
return JSON.parse(JSON.stringify(v));
|
||||||
|
@ -27,7 +28,7 @@ export function useForm<T extends Record<string, any>>(initialState: T, save: (n
|
||||||
|
|
||||||
watch([currentState, previousState], () => {
|
watch([currentState, previousState], () => {
|
||||||
for (const key in modifiedStates) {
|
for (const key in modifiedStates) {
|
||||||
modifiedStates[key] = currentState[key] !== previousState[key];
|
modifiedStates[key] = !deepEqual(currentState[key], previousState[key]);
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import type { Ref, ShallowRef } from 'vue';
|
import type { Reactive, Ref } from 'vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
|
@ -28,7 +28,7 @@ const pollingQueue = new Map<string, {
|
||||||
lastAddedAt: number;
|
lastAddedAt: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function pollingEnqueue(note: Misskey.entities.Note) {
|
function pollingEnqueue(note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>) {
|
||||||
if (pollingQueue.has(note.id)) {
|
if (pollingQueue.has(note.id)) {
|
||||||
const data = pollingQueue.get(note.id)!;
|
const data = pollingQueue.get(note.id)!;
|
||||||
pollingQueue.set(note.id, {
|
pollingQueue.set(note.id, {
|
||||||
|
@ -44,7 +44,7 @@ function pollingEnqueue(note: Misskey.entities.Note) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollingDequeue(note: Misskey.entities.Note) {
|
function pollingDequeue(note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>) {
|
||||||
const data = pollingQueue.get(note.id);
|
const data = pollingQueue.get(note.id);
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
@ -85,28 +85,28 @@ window.setInterval(() => {
|
||||||
}, POLLING_INTERVAL);
|
}, POLLING_INTERVAL);
|
||||||
|
|
||||||
function pollingSubscribe(props: {
|
function pollingSubscribe(props: {
|
||||||
note: Ref<Misskey.entities.Note>;
|
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
|
||||||
isDeletedRef: Ref<boolean>;
|
$note: ReactiveNoteData;
|
||||||
}) {
|
}) {
|
||||||
const note = props.note;
|
const { note, $note } = props;
|
||||||
|
|
||||||
function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void {
|
function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void {
|
||||||
note.value.reactions = data.reactions;
|
$note.reactions = data.reactions;
|
||||||
note.value.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0);
|
$note.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0);
|
||||||
note.value.reactionEmojis = data.reactionEmojis;
|
$note.reactionEmojis = data.reactionEmojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollingEnqueue(note.value);
|
pollingEnqueue(note);
|
||||||
fetchEvent.on(note.value.id, onFetched);
|
fetchEvent.on(note.id, onFetched);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
pollingDequeue(note.value);
|
pollingDequeue(note);
|
||||||
fetchEvent.off(note.value.id, onFetched);
|
fetchEvent.off(note.id, onFetched);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function realtimeSubscribe(props: {
|
function realtimeSubscribe(props: {
|
||||||
note: Ref<Misskey.entities.Note>;
|
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
|
||||||
isDeletedRef: Ref<boolean>;
|
isDeletedRef: Ref<boolean>;
|
||||||
}): void {
|
}): void {
|
||||||
const note = props.note;
|
const note = props.note;
|
||||||
|
@ -115,7 +115,7 @@ function realtimeSubscribe(props: {
|
||||||
function onStreamNoteUpdated(noteData): void {
|
function onStreamNoteUpdated(noteData): void {
|
||||||
const { type, id, body } = noteData;
|
const { type, id, body } = noteData;
|
||||||
|
|
||||||
if (id !== note.value.id) return;
|
if (id !== note.id) return;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'reacted': {
|
case 'reacted': {
|
||||||
|
@ -152,12 +152,12 @@ function realtimeSubscribe(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function capture(withHandler = false): void {
|
function capture(withHandler = false): void {
|
||||||
connection.send('sr', { id: note.value.id });
|
connection.send('sr', { id: note.id });
|
||||||
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
|
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decapture(withHandler = false): void {
|
function decapture(withHandler = false): void {
|
||||||
connection.send('un', { id: note.value.id });
|
connection.send('un', { id: note.id });
|
||||||
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
|
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,40 +174,46 @@ function realtimeSubscribe(props: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNoteCapture(props: {
|
type ReactiveNoteData = Reactive<{
|
||||||
note: Ref<Misskey.entities.Note>;
|
reactions: Misskey.entities.Note['reactions'];
|
||||||
parentNote: Ref<Misskey.entities.Note> | null;
|
reactionCount: Misskey.entities.Note['reactionCount'];
|
||||||
isDeletedRef: Ref<boolean>;
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
}) {
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
const note = props.note;
|
pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices'];
|
||||||
const parentNote = props.parentNote;
|
}>;
|
||||||
|
|
||||||
noteEvents.on(`reacted:${note.value.id}`, onReacted);
|
export function useNoteCapture(props: {
|
||||||
noteEvents.on(`unreacted:${note.value.id}`, onUnreacted);
|
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
|
||||||
noteEvents.on(`pollVoted:${note.value.id}`, onPollVoted);
|
parentNote: Misskey.entities.Note | null;
|
||||||
noteEvents.on(`deleted:${note.value.id}`, onDeleted);
|
$note: ReactiveNoteData;
|
||||||
|
}) {
|
||||||
|
const { note, parentNote, $note } = props;
|
||||||
|
|
||||||
|
noteEvents.on(`reacted:${note.id}`, onReacted);
|
||||||
|
noteEvents.on(`unreacted:${note.id}`, onUnreacted);
|
||||||
|
noteEvents.on(`pollVoted:${note.id}`, onPollVoted);
|
||||||
|
noteEvents.on(`deleted:${note.id}`, onDeleted);
|
||||||
|
|
||||||
let latestReactedKey: string | null = null;
|
let latestReactedKey: string | null = null;
|
||||||
let latestUnreactedKey: string | null = null;
|
let latestUnreactedKey: string | null = null;
|
||||||
let latestPollVotedKey: string | null = null;
|
let latestPollVotedKey: string | null = null;
|
||||||
|
|
||||||
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
||||||
console.log('reacted', ctx);
|
|
||||||
const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
|
const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
|
||||||
if (newReactedKey === latestReactedKey) return;
|
if (newReactedKey === latestReactedKey) return;
|
||||||
latestReactedKey = newReactedKey;
|
latestReactedKey = newReactedKey;
|
||||||
|
|
||||||
if (ctx.emoji && !(ctx.emoji.name in note.value.reactionEmojis)) {
|
if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) {
|
||||||
note.value.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
|
$note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCount = note.value.reactions[ctx.reaction] || 0;
|
const currentCount = $note.reactions[ctx.reaction] || 0;
|
||||||
|
|
||||||
note.value.reactions[ctx.reaction] = currentCount + 1;
|
$note.reactions[ctx.reaction] = currentCount + 1;
|
||||||
note.value.reactionCount += 1;
|
$note.reactionCount += 1;
|
||||||
|
|
||||||
if ($i && (ctx.userId === $i.id)) {
|
if ($i && (ctx.userId === $i.id)) {
|
||||||
note.value.myReaction = ctx.reaction;
|
$note.myReaction = ctx.reaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,14 +222,14 @@ export function useNoteCapture(props: {
|
||||||
if (newUnreactedKey === latestUnreactedKey) return;
|
if (newUnreactedKey === latestUnreactedKey) return;
|
||||||
latestUnreactedKey = newUnreactedKey;
|
latestUnreactedKey = newUnreactedKey;
|
||||||
|
|
||||||
const currentCount = note.value.reactions[ctx.reaction] || 0;
|
const currentCount = $note.reactions[ctx.reaction] || 0;
|
||||||
|
|
||||||
note.value.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
|
$note.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
|
||||||
note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
|
$note.reactionCount = Math.max(0, $note.reactionCount - 1);
|
||||||
if (note.value.reactions[ctx.reaction] === 0) delete note.value.reactions[ctx.reaction];
|
if ($note.reactions[ctx.reaction] === 0) delete $note.reactions[ctx.reaction];
|
||||||
|
|
||||||
if ($i && (ctx.userId === $i.id)) {
|
if ($i && (ctx.userId === $i.id)) {
|
||||||
note.value.myReaction = null;
|
$note.myReaction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +238,7 @@ export function useNoteCapture(props: {
|
||||||
if (newPollVotedKey === latestPollVotedKey) return;
|
if (newPollVotedKey === latestPollVotedKey) return;
|
||||||
latestPollVotedKey = newPollVotedKey;
|
latestPollVotedKey = newPollVotedKey;
|
||||||
|
|
||||||
const choices = [...note.value.poll.choices];
|
const choices = [...$note.pollChoices];
|
||||||
choices[ctx.choice] = {
|
choices[ctx.choice] = {
|
||||||
...choices[ctx.choice],
|
...choices[ctx.choice],
|
||||||
votes: choices[ctx.choice].votes + 1,
|
votes: choices[ctx.choice].votes + 1,
|
||||||
|
@ -241,30 +247,30 @@ export function useNoteCapture(props: {
|
||||||
} : {}),
|
} : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
note.value.poll.choices = choices;
|
$note.pollChoices = choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDeleted(): void {
|
function onDeleted(): void {
|
||||||
props.isDeletedRef.value = true;
|
$note.isDeleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
noteEvents.off(`reacted:${note.value.id}`, onReacted);
|
noteEvents.off(`reacted:${note.id}`, onReacted);
|
||||||
noteEvents.off(`unreacted:${note.value.id}`, onUnreacted);
|
noteEvents.off(`unreacted:${note.id}`, onUnreacted);
|
||||||
noteEvents.off(`pollVoted:${note.value.id}`, onPollVoted);
|
noteEvents.off(`pollVoted:${note.id}`, onPollVoted);
|
||||||
noteEvents.off(`deleted:${note.value.id}`, onDeleted);
|
noteEvents.off(`deleted:${note.id}`, onDeleted);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない
|
// 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない
|
||||||
// ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する
|
// ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する
|
||||||
// TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない
|
// TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない
|
||||||
if (parentNote == null) {
|
if (parentNote == null) {
|
||||||
if ((Date.now() - new Date(note.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
||||||
// リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない
|
// リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((Date.now() - new Date(parentNote.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
||||||
// リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない
|
// リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, isRef, onMounted, ref, watch } from 'vue';
|
import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -38,26 +38,13 @@ export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoint
|
||||||
offsetMode?: boolean;
|
offsetMode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
export function usePagination<T extends MisskeyEntity>(props: {
|
||||||
return entities.map(en => [en.id, en]);
|
ctx: PagingCtx;
|
||||||
}
|
useShallowRef?: boolean;
|
||||||
|
|
||||||
export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['endpoint']]['res']>(props: {
|
|
||||||
ctx: Ctx;
|
|
||||||
}) {
|
}) {
|
||||||
/**
|
const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
|
||||||
* 表示するアイテムのソース
|
const queue = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
|
||||||
* 最新が0番目
|
|
||||||
*/
|
|
||||||
const items = ref<Map<string, T>>(new Map());
|
|
||||||
|
|
||||||
const queue = ref<T[]>([]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初期化中かどうか(trueならMkLoadingで全て隠す)
|
|
||||||
*/
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const moreFetching = ref(false);
|
const moreFetching = ref(false);
|
||||||
const canFetchMore = ref(false);
|
const canFetchMore = ref(false);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
@ -65,11 +52,21 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
|
||||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
||||||
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
|
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
|
||||||
|
|
||||||
|
function getNewestId() {
|
||||||
|
// 様々な要因により並び順は保証されないのでソートが必要
|
||||||
|
return items.value.map(x => x.id).sort().at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOldestId() {
|
||||||
|
// 様々な要因により並び順は保証されないのでソートが必要
|
||||||
|
return items.value.map(x => x.id).sort().at(0);
|
||||||
|
}
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
items.value = new Map();
|
items.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
||||||
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, {
|
await misskeyApi<T[]>(props.ctx.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: props.ctx.limit ?? FIRST_FETCH_LIMIT,
|
limit: props.ctx.limit ?? FIRST_FETCH_LIMIT,
|
||||||
allowPartial: true,
|
allowPartial: true,
|
||||||
|
@ -80,11 +77,11 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.length === 0 || props.ctx.noPaging) {
|
if (res.length === 0 || props.ctx.noPaging) {
|
||||||
concatItems(res);
|
pushItems(res);
|
||||||
canFetchMore.value = false;
|
canFetchMore.value = false;
|
||||||
} else {
|
} else {
|
||||||
if (props.ctx.reversed) moreFetching.value = true;
|
if (props.ctx.reversed) moreFetching.value = true;
|
||||||
concatItems(res);
|
pushItems(res);
|
||||||
canFetchMore.value = true;
|
canFetchMore.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,16 +98,16 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOlder(): Promise<void> {
|
async function fetchOlder(): Promise<void> {
|
||||||
if (!canFetchMore.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
if (!canFetchMore.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
||||||
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, {
|
await misskeyApi<T[]>(props.ctx.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.ctx.offsetMode ? {
|
...(props.ctx.offsetMode ? {
|
||||||
offset: items.value.size,
|
offset: items.value.length,
|
||||||
} : {
|
} : {
|
||||||
untilId: Array.from(items.value.keys()).at(-1),
|
untilId: getOldestId(),
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
@ -122,7 +119,7 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
|
||||||
canFetchMore.value = false;
|
canFetchMore.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
items.value = new Map([...items.value, ...arrayToEntries(res)]);
|
items.value.push(...res);
|
||||||
canFetchMore.value = true;
|
canFetchMore.value = true;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}
|
}
|
||||||
|
@ -135,42 +132,48 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
|
||||||
toQueue?: boolean;
|
toQueue?: boolean;
|
||||||
} = {}): Promise<void> {
|
} = {}): Promise<void> {
|
||||||
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
||||||
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, {
|
await misskeyApi<T[]>(props.ctx.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.ctx.offsetMode ? {
|
...(props.ctx.offsetMode ? {
|
||||||
offset: items.value.size,
|
offset: items.value.length,
|
||||||
} : {
|
} : {
|
||||||
sinceId: Array.from(items.value.keys()).at(0),
|
sinceId: getNewestId(),
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (options.toQueue) {
|
if (options.toQueue) {
|
||||||
queue.value.unshift(...res.toReversed());
|
queue.value.unshift(...res.toReversed());
|
||||||
|
if (props.useShallowRef) triggerRef(queue);
|
||||||
} else {
|
} else {
|
||||||
items.value = new Map([...arrayToEntries(res.toReversed()), ...items.value]);
|
items.value.unshift(...res.toReversed());
|
||||||
|
if (props.useShallowRef) triggerRef(items);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function trim() {
|
function trim() {
|
||||||
if (items.value.size >= MAX_ITEMS) canFetchMore.value = true;
|
if (items.value.length >= MAX_ITEMS) canFetchMore.value = true;
|
||||||
items.value = new Map([...items.value].slice(0, MAX_ITEMS));
|
items.value = items.value.slice(0, MAX_ITEMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unshiftItems(newItems: T[]) {
|
function unshiftItems(newItems: T[]) {
|
||||||
items.value = new Map([...arrayToEntries(newItems), ...items.value]);
|
items.value.unshift(...newItems);
|
||||||
|
if (props.useShallowRef) triggerRef(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function concatItems(oldItems: T[]) {
|
function pushItems(oldItems: T[]) {
|
||||||
items.value = new Map([...items.value, ...arrayToEntries(oldItems)]);
|
items.value.push(...oldItems);
|
||||||
|
if (props.useShallowRef) triggerRef(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepend(item: T) {
|
function prepend(item: T) {
|
||||||
unshiftItems([item]);
|
items.value.unshift(item);
|
||||||
|
if (props.useShallowRef) triggerRef(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueue(item: T) {
|
function enqueue(item: T) {
|
||||||
queue.value.unshift(item);
|
queue.value.unshift(item);
|
||||||
|
if (props.useShallowRef) triggerRef(queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseQueue() {
|
function releaseQueue() {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { genEmbedCode } from '@/utility/get-embed-code.js';
|
import { genEmbedCode } from '@/utility/get-embed-code.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
export async function getNoteClipMenu(props: {
|
export async function getNoteClipMenu(props: {
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -569,8 +570,9 @@ export function getRenoteMenu(props: {
|
||||||
misskeyApi('notes/create', {
|
misskeyApi('notes/create', {
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
channelId: appearNote.channelId,
|
channelId: appearNote.channelId,
|
||||||
}).then(() => {
|
}).then((res) => {
|
||||||
os.toast(i18n.ts.renoted);
|
os.toast(i18n.ts.renoted);
|
||||||
|
globalEvents.emit('notePosted', res.createdNote);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -617,8 +619,9 @@ export function getRenoteMenu(props: {
|
||||||
localOnly,
|
localOnly,
|
||||||
visibility,
|
visibility,
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
}).then(() => {
|
}).then((res) => {
|
||||||
os.toast(i18n.ts.renoted);
|
os.toast(i18n.ts.renoted);
|
||||||
|
globalEvents.emit('notePosted', res.createdNote);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -658,8 +661,9 @@ export function getRenoteMenu(props: {
|
||||||
misskeyApi('notes/create', {
|
misskeyApi('notes/create', {
|
||||||
renoteId: appearNote.id,
|
renoteId: appearNote.id,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
}).then(() => {
|
}).then((res) => {
|
||||||
os.toast(i18n.tsx.renotedToX({ name: channel.name }));
|
os.toast(i18n.tsx.renotedToX({ name: channel.name }));
|
||||||
|
globalEvents.emit('notePosted', res.createdNote);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
},
|
},
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"include": [
|
"include": [
|
||||||
|
"./lib/**/*.ts",
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
"./src/**/*.vue",
|
"./src/**/*.vue",
|
||||||
"./test/**/*.ts",
|
"./test/**/*.ts",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2025.4.1-rc.0",
|
"version": "2025.4.1",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
|
@ -4989,7 +4989,7 @@ export type components = {
|
||||||
isNotResponding: boolean;
|
isNotResponding: boolean;
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
|
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended';
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
/** @example misskey */
|
/** @example misskey */
|
||||||
softwareName: string | null;
|
softwareName: string | null;
|
||||||
|
@ -8765,6 +8765,10 @@ export type operations = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
federation: 'all' | 'specified' | 'none';
|
federation: 'all' | 'specified' | 'none';
|
||||||
federationHosts: string[];
|
federationHosts: string[];
|
||||||
|
deliverSuspendedSoftware: {
|
||||||
|
software: string;
|
||||||
|
versionRange: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -11431,6 +11435,10 @@ export type operations = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
federation?: 'all' | 'none' | 'specified';
|
federation?: 'all' | 'none' | 'specified';
|
||||||
federationHosts?: string[];
|
federationHosts?: string[];
|
||||||
|
deliverSuspendedSoftware?: {
|
||||||
|
software: string;
|
||||||
|
versionRange: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -390,6 +390,9 @@ importers:
|
||||||
secure-json-parse:
|
secure-json-parse:
|
||||||
specifier: 3.0.2
|
specifier: 3.0.2
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
|
semver:
|
||||||
|
specifier: 7.7.1
|
||||||
|
version: 7.7.1
|
||||||
sharp:
|
sharp:
|
||||||
specifier: 0.34.1
|
specifier: 0.34.1
|
||||||
version: 0.34.1
|
version: 0.34.1
|
||||||
|
@ -9564,11 +9567,6 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
semver@7.6.3:
|
|
||||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
semver@7.7.1:
|
semver@7.7.1:
|
||||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -17051,7 +17049,7 @@ snapshots:
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
nth-check: 2.1.1
|
nth-check: 2.1.1
|
||||||
postcss-selector-parser: 6.1.2
|
postcss-selector-parser: 6.1.2
|
||||||
semver: 7.6.3
|
semver: 7.7.1
|
||||||
vue-eslint-parser: 10.1.3(eslint@9.25.1)
|
vue-eslint-parser: 10.1.3(eslint@9.25.1)
|
||||||
xml-name-validator: 4.0.0
|
xml-name-validator: 4.0.0
|
||||||
|
|
||||||
|
@ -20987,8 +20985,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
|
|
||||||
semver@7.6.3: {}
|
|
||||||
|
|
||||||
semver@7.7.1: {}
|
semver@7.7.1: {}
|
||||||
|
|
||||||
send@0.19.0:
|
send@0.19.0:
|
||||||
|
|
Loading…
Reference in New Issue