Merge branch 'develop' into watermark
This commit is contained in:
commit
c8f0833da1
|
@ -52,18 +52,22 @@
|
||||||
- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更
|
- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更
|
||||||
- フロントエンドの読み込みサイズを軽量化しました
|
- フロントエンドの読み込みサイズを軽量化しました
|
||||||
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
|
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
|
||||||
|
- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正
|
||||||
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
|
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
|
||||||
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
|
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
|
||||||
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
|
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
- Enhance: リストやフォローをエクスポートする際にリプライを含むかどうかの情報を含むように
|
||||||
- Enhance: チャットルームの最大メンバー数を30人から50人に調整
|
- Enhance: チャットルームの最大メンバー数を30人から50人に調整
|
||||||
- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
|
- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
|
||||||
- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加
|
- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加
|
||||||
- Enhance: レートリミットの計算方法を調整 (#13997)
|
- Enhance: レートリミットの計算方法を調整 (#13997)
|
||||||
|
- Enhance: 外部サイトのOGPのキャッシュ期間を調整
|
||||||
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
|
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
|
||||||
- Fix: ユーザ除外アンテナをインポートできない問題を修正
|
- Fix: ユーザ除外アンテナをインポートできない問題を修正
|
||||||
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
|
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
|
||||||
|
- Fix: ミュート対象ユーザーが引用されているノートがRNされたときにミュートを貫通してしまう問題を修正 #16009
|
||||||
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
|
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
|
||||||
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正
|
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正
|
||||||
|
|
||||||
|
|
|
@ -1326,6 +1326,10 @@ export interface Locale extends ILocale {
|
||||||
* デバイスのダークモードと同期する
|
* デバイスのダークモードと同期する
|
||||||
*/
|
*/
|
||||||
"syncDeviceDarkMode": string;
|
"syncDeviceDarkMode": string;
|
||||||
|
/**
|
||||||
|
* 「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか?
|
||||||
|
*/
|
||||||
|
"switchDarkModeManuallyWhenSyncEnabledConfirm": ParameterizedString<"x">;
|
||||||
/**
|
/**
|
||||||
* ドライブ
|
* ドライブ
|
||||||
*/
|
*/
|
||||||
|
@ -9711,7 +9715,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"excludeInactiveUsers": string;
|
"excludeInactiveUsers": string;
|
||||||
/**
|
/**
|
||||||
* インポートした人による返信をTLに含むようにする
|
* 返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする
|
||||||
*/
|
*/
|
||||||
"withReplies": string;
|
"withReplies": string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -327,6 +327,7 @@ dark: "ダーク"
|
||||||
lightThemes: "明るいテーマ"
|
lightThemes: "明るいテーマ"
|
||||||
darkThemes: "暗いテーマ"
|
darkThemes: "暗いテーマ"
|
||||||
syncDeviceDarkMode: "デバイスのダークモードと同期する"
|
syncDeviceDarkMode: "デバイスのダークモードと同期する"
|
||||||
|
switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか?"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
fileName: "ファイル名"
|
fileName: "ファイル名"
|
||||||
selectFile: "ファイルを選択"
|
selectFile: "ファイルを選択"
|
||||||
|
@ -2557,7 +2558,7 @@ _exportOrImport:
|
||||||
userLists: "リスト"
|
userLists: "リスト"
|
||||||
excludeMutingUsers: "ミュートしているユーザーを除外"
|
excludeMutingUsers: "ミュートしているユーザーを除外"
|
||||||
excludeInactiveUsers: "使われていないアカウントを除外"
|
excludeInactiveUsers: "使われていないアカウントを除外"
|
||||||
withReplies: "インポートした人による返信をTLに含むようにする"
|
withReplies: "返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする"
|
||||||
|
|
||||||
_charts:
|
_charts:
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.5.1-beta.3",
|
"version": "2025.5.1-beta.4",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -120,6 +120,8 @@ export class FanoutTimelineEndpointService {
|
||||||
filter = (note) => {
|
filter = (note) => {
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||||
|
if (isUserRelated(note.renote, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||||
|
if (isUserRelated(note.renote, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||||
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
||||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||||
|
|
||||||
|
|
|
@ -77,9 +77,51 @@ export class QueryService {
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ミュートやブロックのようにすべてのタイムラインで共通に使用するフィルターを定義します。
|
||||||
|
*
|
||||||
|
* 特別な事情がない限り、各タイムラインはこの関数を呼び出してフィルターを適用してください。
|
||||||
|
*
|
||||||
|
* Notes for future maintainers:
|
||||||
|
* 1) この関数で生成するクエリと同等の処理が FanoutTimelineEndpointService にあります。
|
||||||
|
* この関数を変更した場合、FanoutTimelineEndpointService の方も変更する必要があります。
|
||||||
|
* 2) 以下のエンドポイントでは特別な事情があるため queryService のそれぞれの関数を呼び出しています。
|
||||||
|
* この関数を変更した場合、以下のエンドポイントの方も変更する必要があることがあります。
|
||||||
|
* - packages/backend/src/server/api/endpoints/clips/notes.ts
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public generateBaseNoteFilteringQuery(
|
||||||
|
query: SelectQueryBuilder<any>,
|
||||||
|
me: { id: MiUser['id'] } | null,
|
||||||
|
{
|
||||||
|
excludeUserFromMute,
|
||||||
|
excludeAuthor,
|
||||||
|
}: {
|
||||||
|
excludeUserFromMute?: MiUser['id'],
|
||||||
|
excludeAuthor?: boolean,
|
||||||
|
} = {},
|
||||||
|
): void {
|
||||||
|
this.generateBlockedHostQueryForNote(query, excludeAuthor);
|
||||||
|
this.generateSuspendedUserQueryForNote(query, excludeAuthor);
|
||||||
|
if (me) {
|
||||||
|
this.generateMutedUserQueryForNotes(query, me, { excludeUserFromMute });
|
||||||
|
this.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
this.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote', excludeUserFromMute });
|
||||||
|
this.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ここでいうBlockedは被Blockedの意
|
// ここでいうBlockedは被Blockedの意
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
public generateBlockedUserQueryForNotes(
|
||||||
|
q: SelectQueryBuilder<any>,
|
||||||
|
me: { id: MiUser['id'] },
|
||||||
|
{
|
||||||
|
noteColumn = 'note',
|
||||||
|
}: {
|
||||||
|
noteColumn?: string,
|
||||||
|
} = {},
|
||||||
|
): void {
|
||||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||||
.select('blocking.blockerId')
|
.select('blocking.blockerId')
|
||||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||||
|
@ -88,16 +130,20 @@ export class QueryService {
|
||||||
// 投稿の返信先の作者にブロックされていない かつ
|
// 投稿の返信先の作者にブロックされていない かつ
|
||||||
// 投稿の引用元の作者にブロックされていない
|
// 投稿の引用元の作者にブロックされていない
|
||||||
q
|
q
|
||||||
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
|
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.replyUserId IS NULL')
|
.where(`${noteColumn}.userId IS NULL`)
|
||||||
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
|
.orWhere(`${noteColumn}.userId NOT IN (${ blockingQuery.getQuery() })`);
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.renoteUserId IS NULL')
|
.where(`${noteColumn}.replyUserId IS NULL`)
|
||||||
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
|
.orWhere(`${noteColumn}.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where(`${noteColumn}.renoteUserId IS NULL`)
|
||||||
|
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(blockingQuery.getParameters());
|
q.setParameters(blockingQuery.getParameters());
|
||||||
|
@ -137,13 +183,23 @@ export class QueryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
|
public generateMutedUserQueryForNotes(
|
||||||
|
q: SelectQueryBuilder<any>,
|
||||||
|
me: { id: MiUser['id'] },
|
||||||
|
{
|
||||||
|
excludeUserFromMute,
|
||||||
|
noteColumn = 'note',
|
||||||
|
}: {
|
||||||
|
excludeUserFromMute?: MiUser['id'],
|
||||||
|
noteColumn?: string,
|
||||||
|
} = {},
|
||||||
|
): void {
|
||||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
if (exclude) {
|
if (excludeUserFromMute) {
|
||||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: excludeUserFromMute });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||||
|
@ -154,32 +210,36 @@ export class QueryService {
|
||||||
// 投稿の返信先の作者をミュートしていない かつ
|
// 投稿の返信先の作者をミュートしていない かつ
|
||||||
// 投稿の引用元の作者をミュートしていない
|
// 投稿の引用元の作者をミュートしていない
|
||||||
q
|
q
|
||||||
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
|
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.replyUserId IS NULL')
|
.where(`${noteColumn}.userId IS NULL`)
|
||||||
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
.orWhere(`${noteColumn}.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.renoteUserId IS NULL')
|
.where(`${noteColumn}.replyUserId IS NULL`)
|
||||||
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
.orWhere(`${noteColumn}.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where(`${noteColumn}.renoteUserId IS NULL`)
|
||||||
|
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
}))
|
}))
|
||||||
// mute instances
|
// mute instances
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.andWhere('note.userHost IS NULL')
|
.andWhere(`${noteColumn}.userHost IS NULL`)
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.userHost)`);
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.replyUserHost IS NULL')
|
.where(`${noteColumn}.replyUserHost IS NULL`)
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.replyUserHost)`);
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.renoteUserHost IS NULL')
|
.where(`${noteColumn}.renoteUserHost IS NULL`)
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.renoteUserHost)`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
|
|
|
@ -83,7 +83,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
canUseTranslator: true,
|
canUseTranslator: true,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
maxFileSizeMb: 10,
|
maxFileSizeMb: 30,
|
||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
canUpdateBioMedia: true,
|
canUpdateBioMedia: true,
|
||||||
pinLimit: 5,
|
pinLimit: 5,
|
||||||
|
|
|
@ -234,10 +234,7 @@ export class SearchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
return query.limit(pagination.limit).getMany();
|
return query.limit(pagination.limit).getMany();
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
public async addMember(target: MiUser, list: MiUserList, me: MiUser, options: { withReplies?: boolean } = {}) {
|
||||||
const currentCount = await this.userListMembershipsRepository.countBy({
|
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
|
@ -104,6 +104,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
userListUserId: list.userId,
|
userListUserId: list.userId,
|
||||||
|
withReplies: options.withReplies ?? false,
|
||||||
} as MiUserListMembership);
|
} as MiUserListMembership);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||||
|
|
|
@ -3,7 +3,17 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
|
import type { MiUser } from '@/models/_.js';
|
||||||
|
|
||||||
|
interface NoteLike {
|
||||||
|
userId: MiUser['id'];
|
||||||
|
reply?: NoteLike | null;
|
||||||
|
renote?: NoteLike | null;
|
||||||
|
replyUserId?: MiUser['id'] | null;
|
||||||
|
renoteUserId?: MiUser['id'] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUserRelated(note: NoteLike | null | undefined, userIds: Set<string>, ignoreAuthor = false): boolean {
|
||||||
if (!note) {
|
if (!note) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -12,13 +22,16 @@ export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = fa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
|
const replyUserId = note.replyUserId ?? note.reply?.userId;
|
||||||
|
if (replyUserId != null && replyUserId !== note.userId && userIds.has(replyUserId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
|
const renoteUserId = note.renoteUserId ?? note.renote?.userId;
|
||||||
|
if (renoteUserId != null && renoteUserId !== note.userId && userIds.has(renoteUserId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,8 @@ export class ExportFollowingProcessorService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = this.utilityService.getFullApAccount(u.username, u.host);
|
const userAcct = this.utilityService.getFullApAccount(u.username, u.host);
|
||||||
|
const content = `${userAcct},withReplies=${following.withReplies}`;
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -67,10 +67,12 @@ export class ExportUserListsProcessorService {
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(memberships.map(j => j.userId)),
|
id: In(memberships.map(j => j.userId)),
|
||||||
});
|
});
|
||||||
|
const usersWithReplies = new Set(memberships.filter(m => m.withReplies).map(m => m.userId));
|
||||||
|
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
const acct = this.utilityService.getFullApAccount(u.username, u.host);
|
const acct = this.utilityService.getFullApAccount(u.username, u.host);
|
||||||
const content = `${list.name},${acct}`;
|
// 3rd column and later will be key=value pairs
|
||||||
|
const content = `${list.name},${acct},withReplies=${usersWithReplies.has(u.id)}`;
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -67,8 +67,19 @@ export class ImportFollowingProcessorService {
|
||||||
const user = job.data.user;
|
const user = job.data.user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const acct = line.split(',')[0].trim();
|
const parts = line.split(',');
|
||||||
|
const acct = parts[0].trim();
|
||||||
const { username, host } = Acct.parse(acct);
|
const { username, host } = Acct.parse(acct);
|
||||||
|
let withReplies: boolean | null = null;
|
||||||
|
|
||||||
|
for (const keyValue of parts.slice(2)) {
|
||||||
|
const [key, value] = keyValue.split('=');
|
||||||
|
switch (key) {
|
||||||
|
case 'withReplies':
|
||||||
|
withReplies = value === 'true';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
|
@ -95,7 +106,7 @@ export class ImportFollowingProcessorService {
|
||||||
|
|
||||||
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
|
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
|
||||||
|
|
||||||
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
|
await this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: withReplies ?? job.data.withReplies }]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error: ${e}`);
|
this.logger.warn(`Error: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,8 +70,19 @@ export class ImportUserListsProcessorService {
|
||||||
linenum++;
|
linenum++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const listName = line.split(',')[0].trim();
|
const parts = line.split(',');
|
||||||
const { username, host } = Acct.parse(line.split(',')[1].trim());
|
const listName = parts[0].trim();
|
||||||
|
const { username, host } = Acct.parse(parts[1].trim());
|
||||||
|
let withReplies = false;
|
||||||
|
|
||||||
|
for (const keyValue of parts.slice(2)) {
|
||||||
|
const [key, value] = keyValue.split('=');
|
||||||
|
switch (key) {
|
||||||
|
case 'withReplies':
|
||||||
|
withReplies = value === 'true';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let list = await this.userListsRepository.findOneBy({
|
let list = await this.userListsRepository.findOneBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -100,7 +111,9 @@ export class ImportUserListsProcessorService {
|
||||||
|
|
||||||
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||||
|
|
||||||
this.userListService.addMember(target, list!, user);
|
await this.userListService.addMember(target, list, user, {
|
||||||
|
withReplies: withReplies,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error in line:${linenum} ${e}`);
|
this.logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,11 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
|
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
|
||||||
// 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.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
const notes = await query.getMany();
|
const notes = await query.getMany();
|
||||||
if (sinceId != null && untilId == null) {
|
if (sinceId != null && untilId == null) {
|
||||||
|
|
|
@ -121,12 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
return await query.limit(ps.limit).getMany();
|
||||||
|
|
|
@ -91,6 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (me) {
|
if (me) {
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' });
|
||||||
|
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const notes = await query
|
const notes = await query
|
||||||
|
|
|
@ -70,12 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
}
|
|
||||||
|
|
||||||
const notes = await query.limit(ps.limit).getMany();
|
const notes = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
|
|
@ -78,11 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
if (me) {
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -243,10 +243,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.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
|
|
|
@ -156,10 +156,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
|
|
|
@ -72,11 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
if (ps.visibility) {
|
if (ps.visibility) {
|
||||||
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
|
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
|
||||||
|
|
|
@ -72,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
const renotes = await query.limit(ps.limit).getMany();
|
const renotes = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
|
|
@ -56,10 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
|
|
@ -96,10 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ('tag' in ps) {
|
if ('tag' in ps) {
|
||||||
|
|
|
@ -199,10 +199,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.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
|
|
|
@ -184,10 +184,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.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
|
|
|
@ -102,10 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateBlockedHostQueryForNote(query);
|
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
|
|
||||||
const notes = await query.getMany();
|
const notes = await query.getMany();
|
||||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
|
@ -186,12 +186,10 @@ 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.generateBaseNoteFilteringQuery(query, me, {
|
||||||
this.queryService.generateSuspendedUserQueryForNote(query, true);
|
excludeAuthor: true,
|
||||||
if (me) {
|
excludeUserFromMute: ps.userId,
|
||||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
});
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -64,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.queryService.generateMutedUserQueryForUsers(query, me);
|
this.queryService.generateMutedUserQueryForUsers(query, me);
|
||||||
this.queryService.generateBlockQueryForUsers(query, me);
|
this.queryService.generateBlockQueryForUsers(query, me);
|
||||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||||
|
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
|
||||||
|
|
||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('following.followeeId')
|
.select('following.followeeId')
|
||||||
|
|
|
@ -94,8 +94,8 @@ export class UrlPreviewService {
|
||||||
summary.icon = this.wrap(summary.icon);
|
summary.icon = this.wrap(summary.icon);
|
||||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||||
|
|
||||||
// Cache 7days
|
// Cache 1day
|
||||||
reply.header('Cache-Control', 'max-age=604800, immutable');
|
reply.header('Cache-Control', 'max-age=86400, immutable');
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -345,6 +345,44 @@ describe('Timelines', () => {
|
||||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('following/create', { userId: bob.id }, alice);
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('following/create', { userId: bob.id }, alice);
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||||
|
|
||||||
|
@ -687,6 +725,42 @@ describe('Timelines', () => {
|
||||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
@ -1383,6 +1457,39 @@ describe('Timelines', () => {
|
||||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
|
||||||
|
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
await api('following/create', { userId: bob.id }, alice);
|
||||||
|
await api('mute/create', { userId: carol.id }, alice);
|
||||||
|
await setTimeout(1000);
|
||||||
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
|
||||||
|
const bobNote = await post(bob, { renoteId: daveNote.id });
|
||||||
|
|
||||||
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||||
|
|
||||||
|
@ -1391,6 +1498,8 @@ describe('Timelines', () => {
|
||||||
const bobNote1 = await post(bob, { text: 'hi' });
|
const bobNote1 = await post(bob, { text: 'hi' });
|
||||||
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
|
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
|
||||||
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
|
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
|
||||||
|
const bobNote4 = await post(bob, { renoteId: bobNote2.id });
|
||||||
|
const bobNote5 = await post(bob, { renoteId: bobNote3.id });
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
|
||||||
|
@ -1399,6 +1508,8 @@ describe('Timelines', () => {
|
||||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||||
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
|
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true);
|
||||||
|
assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
|
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
|
||||||
|
|
|
@ -10,7 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
tail === 'left' ? $style.left : $style.right,
|
tail === 'left' ? $style.left : $style.right,
|
||||||
negativeMargin === true && $style.negativeMargin,
|
negativeMargin === true && $style.negativeMargin,
|
||||||
shadow === true && $style.shadow,
|
shadow === true && $style.shadow,
|
||||||
accented === true && $style.accented
|
accented === true && $style.accented,
|
||||||
|
fullWidth === true && $style.fullWidth,
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div :class="$style.bg">
|
<div :class="$style.bg">
|
||||||
|
@ -32,11 +33,13 @@ withDefaults(defineProps<{
|
||||||
negativeMargin?: boolean;
|
negativeMargin?: boolean;
|
||||||
shadow?: boolean;
|
shadow?: boolean;
|
||||||
accented?: boolean;
|
accented?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
tail: 'right',
|
tail: 'right',
|
||||||
negativeMargin: false,
|
negativeMargin: false,
|
||||||
shadow: false,
|
shadow: false,
|
||||||
accented: false,
|
accented: false,
|
||||||
|
fullWidth: false,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -73,6 +76,14 @@ withDefaults(defineProps<{
|
||||||
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
|
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg {
|
.bg {
|
||||||
|
@ -85,6 +96,7 @@ withDefaults(defineProps<{
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 450px) {
|
@container (max-width: 450px) {
|
||||||
|
|
|
@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||||
<MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/>
|
<MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/>
|
||||||
<div :class="$style.body" @contextmenu.stop="onContextmenu">
|
<div :class="[$style.body, message.file != null ? $style.fullWidth : null]" @contextmenu.stop="onContextmenu">
|
||||||
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div>
|
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div>
|
||||||
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
|
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :fullWidth="message.file != null" :accented="isMe">
|
||||||
<Mfm
|
<Mfm
|
||||||
v-if="message.text"
|
v-if="message.text"
|
||||||
ref="text"
|
ref="text"
|
||||||
|
@ -259,6 +259,10 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
|
|
||||||
|
&.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
|
@ -9,8 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-adaptive-border class="rfqxtzch _panel">
|
<div v-adaptive-border class="rfqxtzch _panel">
|
||||||
<div class="toggle">
|
<div class="toggle">
|
||||||
<div class="toggleWrapper">
|
<div class="toggleWrapper">
|
||||||
<input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
|
<div class="toggle" :class="store.r.darkMode.value ? 'checked' : null" @click="toggleDarkMode()">
|
||||||
<label for="dn" class="toggle">
|
|
||||||
<span class="before">{{ i18n.ts.light }}</span>
|
<span class="before">{{ i18n.ts.light }}</span>
|
||||||
<span class="after">{{ i18n.ts.dark }}</span>
|
<span class="after">{{ i18n.ts.dark }}</span>
|
||||||
<span class="toggle__handler">
|
<span class="toggle__handler">
|
||||||
|
@ -24,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span class="star star--4"></span>
|
<span class="star star--4"></span>
|
||||||
<span class="star star--5"></span>
|
<span class="star star--5"></span>
|
||||||
<span class="star star--6"></span>
|
<span class="star star--6"></span>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sync">
|
<div class="sync">
|
||||||
|
@ -37,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<template v-if="!darkMode">
|
<template v-if="!store.r.darkMode.value">
|
||||||
<SearchMarker :keywords="['light', 'theme']">
|
<SearchMarker :keywords="['light', 'theme']">
|
||||||
<MkFolder :defaultOpen="true" :max-height="500">
|
<MkFolder :defaultOpen="true" :max-height="500">
|
||||||
<template #icon><i class="ti ti-sun"></i></template>
|
<template #icon><i class="ti ti-sun"></i></template>
|
||||||
|
@ -205,6 +204,7 @@ import JSON5 from 'json5';
|
||||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||||
import type { Theme } from '@/theme.js';
|
import type { Theme } from '@/theme.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
|
@ -257,7 +257,6 @@ const lightThemeId = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const darkMode = computed(store.makeGetterSetter('darkMode'));
|
|
||||||
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
|
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
|
||||||
const themesCount = installedThemes.value.length;
|
const themesCount = installedThemes.value.length;
|
||||||
|
|
||||||
|
@ -267,6 +266,21 @@ watch(syncDeviceDarkMode, () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function toggleDarkMode() {
|
||||||
|
const value = !store.r.darkMode.value;
|
||||||
|
if (syncDeviceDarkMode.value) {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
text: i18n.tsx.switchDarkModeManuallyWhenSyncEnabledConfirm({ x: i18n.ts.syncDeviceDarkMode }),
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
syncDeviceDarkMode.value = false;
|
||||||
|
store.set('darkMode', value);
|
||||||
|
} else {
|
||||||
|
store.set('darkMode', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const themesSyncEnabled = ref(prefer.isSyncEnabled('themes'));
|
const themesSyncEnabled = ref(prefer.isSyncEnabled('themes'));
|
||||||
|
|
||||||
function changeThemesSyncEnabled(value: boolean) {
|
function changeThemesSyncEnabled(value: boolean) {
|
||||||
|
@ -365,16 +379,6 @@ definePage(() => ({
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
padding: 0 100px;
|
padding: 0 100px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
||||||
input {
|
|
||||||
position: absolute;
|
|
||||||
left: -99em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dn:focus-visible ~ .toggle {
|
|
||||||
outline: 2px solid var(--MI_THEME-focus);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
|
@ -403,6 +407,61 @@ definePage(() => ({
|
||||||
right: -68px;
|
right: -68px;
|
||||||
color: var(--MI_THEME-fg);
|
color: var(--MI_THEME-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
background-color: #749DD6;
|
||||||
|
|
||||||
|
> .before {
|
||||||
|
color: var(--MI_THEME-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .after {
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle__handler {
|
||||||
|
background-color: #FFE5B5;
|
||||||
|
transform: translate3d(40px, 0, 0) rotate(0);
|
||||||
|
|
||||||
|
.crater { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--1 {
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--2 {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
transform: translate3d(-5px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--3 {
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
transform: translate3d(-7px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--4,
|
||||||
|
.star--5,
|
||||||
|
.star--6 {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--4 {
|
||||||
|
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--5 {
|
||||||
|
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star--6 {
|
||||||
|
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle__handler {
|
.toggle__handler {
|
||||||
|
@ -513,63 +572,6 @@ definePage(() => ({
|
||||||
height: 2px;
|
height: 2px;
|
||||||
transform: translate3d(3px,0,0);
|
transform: translate3d(3px,0,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked {
|
|
||||||
+ .toggle {
|
|
||||||
background-color: #749DD6;
|
|
||||||
|
|
||||||
> .before {
|
|
||||||
color: var(--MI_THEME-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .after {
|
|
||||||
color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle__handler {
|
|
||||||
background-color: #FFE5B5;
|
|
||||||
transform: translate3d(40px, 0, 0) rotate(0);
|
|
||||||
|
|
||||||
.crater { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--1 {
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--2 {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
transform: translate3d(-5px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--3 {
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
transform: translate3d(-7px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--4,
|
|
||||||
.star--5,
|
|
||||||
.star--6 {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0,0,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--4 {
|
|
||||||
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--5 {
|
|
||||||
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.star--6 {
|
|
||||||
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .sync {
|
> .sync {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2025.5.1-beta.3",
|
"version": "2025.5.1-beta.4",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
Loading…
Reference in New Issue