implement note mutings and move favorited/renoted status into note entity directly

This commit is contained in:
Hazelnoot 2025-06-10 00:46:52 -04:00
parent 9bebf7718f
commit 7200c3d6c8
24 changed files with 342 additions and 181 deletions

12
locales/index.d.ts vendored
View file

@ -11988,6 +11988,18 @@ export interface Locale extends ILocale {
* Boosts muted
*/
"renoteMuted": string;
/**
* Mute note
*/
"muteNote": string;
/**
* Unmute note
*/
"unmuteNote": string;
/**
* {name} said something in a muted post
*/
"userSaysSomethingInMutedNote": ParameterizedString<"name">;
/**
* {name} said something in a muted thread
*/

View file

@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddNoteThreadMutingIsPostMute1749523586531 {
name = 'AddNoteThreadMutingIsPostMute1749523586531'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`);
await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD "isPostMute" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "note_thread_muting"."isPostMute" IS 'If true, then this mute applies only to the referenced note. If false (default), then it applies to all replies as well.'`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_01f7ab05099400012e9a7fd42b" ON "note_thread_muting" ("userId", "threadId", "isPostMute") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_01f7ab05099400012e9a7fd42b"`);
await queryRunner.query(`COMMENT ON COLUMN "note_thread_muting"."isPostMute" IS 'If true, then this mute applies only to the referenced note. If false (default), then it applies to all replies as well.'`);
await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP COLUMN "isPostMute"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `);
}
}

View file

@ -47,6 +47,7 @@ export class CacheService implements OnApplicationShutdown {
public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: QuantumKVCache<Set<string>>;
public threadMutingsCache: QuantumKVCache<Set<string>>;
public noteMutingsCache: QuantumKVCache<Set<string>>;
public userFollowingsCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
public userFollowersCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
public hibernatedUserCache: QuantumKVCache<boolean>;
@ -152,13 +153,27 @@ export class CacheService implements OnApplicationShutdown {
this.threadMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'threadMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: muterId => this.noteThreadMutingsRepository
.find({ where: { userId: muterId }, select: { threadId: true } })
.find({ where: { userId: muterId, isPostMute: false }, select: { threadId: true } })
.then(ms => new Set(ms.map(m => m.threadId))),
bulkFetcher: muterIds => this.noteThreadMutingsRepository
.createQueryBuilder('muting')
.select('"muting"."userId"', 'userId')
.addSelect('array_agg("muting"."threadId")', 'threadIds')
.where({ userId: In(muterIds) })
.where({ userId: In(muterIds), isPostMute: false })
.getRawMany<{ userId: string, threadIds: string[] }>()
.then(ms => ms.map(m => [m.userId, new Set(m.threadIds)])),
});
this.noteMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'noteMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: muterId => this.noteThreadMutingsRepository
.find({ where: { userId: muterId, isPostMute: true }, select: { threadId: true } })
.then(ms => new Set(ms.map(m => m.threadId))),
bulkFetcher: muterIds => this.noteThreadMutingsRepository
.createQueryBuilder('muting')
.select('"muting"."userId"', 'userId')
.addSelect('array_agg("muting"."threadId")', 'threadIds')
.where({ userId: In(muterIds), isPostMute: true })
.getRawMany<{ userId: string, threadIds: string[] }>()
.then(ms => ms.map(m => [m.userId, new Set(m.threadIds)])),
});
@ -290,6 +305,8 @@ export class CacheService implements OnApplicationShutdown {
this.userFollowingsCache.delete(body.id),
this.userFollowersCache.delete(body.id),
this.hibernatedUserCache.delete(body.id),
this.threadMutingsCache.delete(body.id),
this.noteMutingsCache.delete(body.id),
]);
}
} else {
@ -560,7 +577,11 @@ export class CacheService implements OnApplicationShutdown {
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.threadMutingsCache.dispose();
this.noteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowersCache.dispose();
this.hibernatedUserCache.dispose();
}
@bindThis

View file

@ -394,6 +394,7 @@ export class WebhookTestService {
private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
return {
id: note.id,
threadId: note.threadId ?? note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
@ -403,6 +404,10 @@ export class WebhookTestService {
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
isMutingThread: false,
isMutingNote: false,
isFavorited: false,
isRenoted: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,

View file

@ -11,11 +11,12 @@ import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel, MiFollowing } from '@/models/_.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel, MiFollowing, NoteFavoritesRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { QueryService } from '@/core/QueryService.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { Config } from '@/config.js';
import type { OnModuleInit } from '@nestjs/common';
@ -55,6 +56,7 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
return appearNoteIds;
}
// noinspection ES6MissingAwait
@Injectable()
export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
@ -96,6 +98,10 @@ export class NoteEntityService implements OnModuleInit {
@Inject(DI.config)
private readonly config: Config,
@Inject(DI.noteFavoritesRepository)
private noteFavoritesRepository: NoteFavoritesRepository,
private readonly queryService: QueryService,
//private userEntityService: UserEntityService,
//private driveFileEntityService: DriveFileEntityService,
//private customEmojiService: CustomEmojiService,
@ -423,6 +429,9 @@ export class NoteEntityService implements OnModuleInit {
channels: Map<string, MiChannel>;
notes: Map<string, MiNote>;
mutedThreads: Set<string>;
mutedNotes: Set<string>;
favoriteNotes: Set<string>;
renotedNotes: Set<string>;
};
},
): Promise<Packed<'Note'>> {
@ -462,6 +471,23 @@ export class NoteEntityService implements OnModuleInit {
const packedUsers = options?._hint_?.packedUsers;
const threadId = note.threadId ?? note.id;
const [mutedThreads, mutedNotes, isFavorited, isRenoted] = await Promise.all([
// mutedThreads
opts._hint_?.mutedThreads
?? (meId ? this.cacheService.threadMutingsCache.fetch(meId) : new Set<string>()),
// mutedNotes
opts._hint_?.mutedNotes
?? (meId ? this.cacheService.noteMutingsCache.fetch(meId) : new Set<string>),
// isFavorited
opts._hint_?.favoriteNotes.has(note.id)
?? (meId ? this.noteFavoritesRepository.existsBy({ userId: meId, noteId: note.id }) : false),
// isRenoted
opts._hint_?.renotedNotes.has(note.id)
?? (meId ? this.queryService
.andIsRenote(this.notesRepository.createQueryBuilder('note'), 'note')
.andWhere({ renoteId: note.id, userId: meId })
.getExists() : false),
]);
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
@ -505,8 +531,10 @@ export class NoteEntityService implements OnModuleInit {
poll: opts._hint_?.polls.get(note.id),
myVotes: opts._hint_?.pollVotes.get(note.id)?.get(note.userId),
}) : undefined,
isMuting: opts._hint_?.mutedThreads.has(threadId)
?? (meId != null && this.cacheService.threadMutingsCache.fetch(meId).then(ms => ms.has(threadId))),
isMutingThread: mutedThreads.has(threadId),
isMutingNote: mutedNotes.has(note.id),
isFavorited,
isRenoted,
...(meId && Object.keys(reactions).length > 0 ? {
myReaction: this.populateMyReaction({
@ -654,7 +682,7 @@ export class NoteEntityService implements OnModuleInit {
const fileIds = new Set(targetNotes.flatMap(n => n.fileIds));
const mentionedUsers = new Set(targetNotes.flatMap(note => note.mentions));
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels, mutedThreads] = await Promise.all([
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels, mutedThreads, mutedNotes, favoriteNotes, renotedNotes] = await Promise.all([
// bufferedReactions & myReactionsMap
this.getReactions(targetNotes, me),
// packedFiles
@ -692,6 +720,22 @@ export class NoteEntityService implements OnModuleInit {
this.getChannels(targetNotes),
// mutedThreads
me ? this.cacheService.threadMutingsCache.fetch(me.id) : new Set<string>(),
// mutedNotes
me ? this.cacheService.noteMutingsCache.fetch(me.id) : new Set<string>(),
// favoriteNotes
me ? this.noteFavoritesRepository
.createQueryBuilder('favorite')
.select('favorite.noteId', 'noteId')
.where({ userId: me.id, noteId: In(noteIds) })
.getRawMany<{ noteId: string }>()
.then(fs => new Set(fs.map(f => f.noteId))) : new Set<string>(),
// renotedNotes
me ? this.queryService
.andIsRenote(this.notesRepository.createQueryBuilder('note'), 'note')
.andWhere({ renoteId: In(noteIds), userId: me.id })
.select('note.id', 'id')
.getRawMany<{ id: string }>()
.then(ns => new Set(ns.map(n => n.id))) : new Set<string>(),
// (not returned)
this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)),
]);
@ -711,6 +755,9 @@ export class NoteEntityService implements OnModuleInit {
channels,
notes: new Map(targetNotes.map(n => [n.id, n])),
mutedThreads,
mutedNotes,
favoriteNotes,
renotedNotes,
},
})));
}

View file

@ -8,7 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('note_thread_muting')
@Index(['userId', 'threadId'], { unique: true })
@Index(['userId', 'threadId', 'isPostMute'], { unique: true })
export class MiNoteThreadMuting {
@PrimaryColumn(id())
public id: string;
@ -30,4 +30,10 @@ export class MiNoteThreadMuting {
length: 256,
})
public threadId: string;
@Column('boolean', {
comment: 'If true, then this mute applies only to the referenced note. If false (default), then it applies to all replies as well.',
default: false,
})
public isPostMute: boolean;
}

View file

@ -173,7 +173,19 @@ export const packedNoteSchema = {
},
},
},
isMuting: {
isMutingThread: {
type: 'boolean',
optional: false, nullable: false,
},
isMutingNote: {
type: 'boolean',
optional: false, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: false, nullable: false,
},
isRenoted: {
type: 'boolean',
optional: false, nullable: false,
},

View file

@ -8,6 +8,7 @@ import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesReposit
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
export const meta = {
tags: ['notes'],
@ -27,6 +28,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isMutedNote: {
type: 'boolean',
optional: false, nullable: false,
},
isRenoted: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
@ -58,23 +67,37 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteFavoritesRepository: NoteFavoritesRepository,
private readonly cacheService: CacheService,
private readonly queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId });
const [favorite, threadMuting] = await Promise.all([
const [favorite, threadMuting, noteMuting, renoted] = await Promise.all([
// favorite
this.noteFavoritesRepository.exists({
where: {
userId: me.id,
noteId: note.id,
},
}),
// treadMuting
this.cacheService.threadMutingsCache.fetch(me.id).then(ms => ms.has(note.threadId ?? note.id)),
// noteMuting
this.cacheService.noteMutingsCache.fetch(me.id).then(ms => ms.has(note.id)),
// renoted
this.notesRepository
.createQueryBuilder('note')
.andWhere({ renoteId: note.id, userId: me.id })
.andWhere(qb => this.queryService
.andIsRenote(qb, 'note'))
.getExists(),
]);
return {
isFavorited: favorite,
isMutedThread: threadMuting,
isMutedNote: noteMuting,
isRenoted: renoted,
};
});
}

View file

@ -20,9 +20,11 @@ export const meta = {
kind: 'write:account',
// Up to 10 calls, then 1/second
limit: {
duration: ms('1hour'),
max: 10,
type: 'bucket',
size: 10,
dripRate: 1000,
},
errors: {
@ -38,6 +40,7 @@ export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
noteOnly: { type: 'boolean', default: false },
},
required: ['noteId'],
} as const;
@ -71,13 +74,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
*/
const threadId = note.threadId ?? note.id;
await this.noteThreadMutingsRepository.insert({
id: this.idService.gen(),
threadId: note.threadId ?? note.id,
threadId: ps.noteOnly ? note.id : threadId,
userId: me.id,
isPostMute: ps.noteOnly,
});
await this.cacheService.threadMutingsCache.refresh(me.id);
await Promise.all([
this.cacheService.threadMutingsCache.refresh(me.id),
this.cacheService.noteMutingsCache.refresh(me.id),
]);
});
}
}

View file

@ -26,10 +26,11 @@ export const meta = {
},
},
// 10 calls per hour (match create)
// Up to 20 calls, then 2/second
limit: {
duration: 1000 * 60 * 60,
max: 10,
type: 'bucket',
size: 20,
dripRate: 2000,
},
} as const;
@ -37,6 +38,7 @@ export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
noteOnly: { type: 'boolean', default: false },
},
required: ['noteId'],
} as const;
@ -56,12 +58,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
const threadId = note.threadId ?? note.id;
await this.noteThreadMutingsRepository.delete({
threadId: note.threadId ?? note.id,
threadId: ps.noteOnly ? note.id : threadId,
userId: me.id,
isPostMute: ps.noteOnly,
});
await this.cacheService.threadMutingsCache.refresh(me.id);
await Promise.all([
this.cacheService.threadMutingsCache.refresh(me.id),
this.cacheService.noteMutingsCache.refresh(me.id),
]);
});
}
}

View file

@ -6,7 +6,7 @@
import * as Redis from 'ioredis';
import { Inject } from '@nestjs/common';
import { FakeInternalEventService } from './FakeInternalEventService.js';
import type { BlockingsRepository, FollowingsRepository, MiUser, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { BlockingsRepository, FollowingsRepository, MiUser, MutingsRepository, NoteThreadMutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js';
import { MemoryKVCache, MemorySingleCache, RedisKVCache, RedisSingleCache } from '@/misc/cache.js';
import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js';
@ -50,6 +50,9 @@ export class NoOpCacheService extends CacheService {
@Inject(DI.followingsRepository)
followingsRepository: FollowingsRepository,
@Inject(DI.noteThreadMutingsRepository)
noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(UserEntityService)
userEntityService: UserEntityService,
) {
@ -65,6 +68,7 @@ export class NoOpCacheService extends CacheService {
blockingsRepository,
renoteMutingsRepository,
followingsRepository,
noteThreadMutingsRepository,
userEntityService,
fakeInternalEventService,
);
@ -82,6 +86,8 @@ export class NoOpCacheService extends CacheService {
this.userBlockingCache = NoOpQuantumKVCache.copy(this.userBlockingCache, fakeInternalEventService);
this.userBlockedCache = NoOpQuantumKVCache.copy(this.userBlockedCache, fakeInternalEventService);
this.renoteMutingsCache = NoOpQuantumKVCache.copy(this.renoteMutingsCache, fakeInternalEventService);
this.threadMutingsCache = NoOpQuantumKVCache.copy(this.threadMutingsCache, fakeInternalEventService);
this.noteMutingsCache = NoOpQuantumKVCache.copy(this.noteMutingsCache, fakeInternalEventService);
this.userFollowingsCache = NoOpQuantumKVCache.copy(this.userFollowingsCache, fakeInternalEventService);
this.userFollowersCache = NoOpQuantumKVCache.copy(this.userFollowersCache, fakeInternalEventService);
this.hibernatedUserCache = NoOpQuantumKVCache.copy(this.hibernatedUserCache, fakeInternalEventService);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!hardMuted && muted === false && !threadMuted"
v-if="!hardMuted && muted === false && !threadMuted && !noteMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -125,9 +125,9 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
@mousedown.prevent="appearNote.isRenoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -169,7 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
<div v-else>
<!--
@ -311,8 +311,7 @@ const selfNoteIds = computed(() => getSelfNoteIds(props.note));
const isLong = shouldCollapsed(appearNote.value, urls.value);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
const { muted, hardMuted, threadMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
@ -335,7 +334,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
let renoting = false;
@ -350,7 +349,7 @@ const keymap = {
},
'q': () => {
if (renoteCollapsed.value) return;
if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost);
if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost);
},
'm': () => {
if (renoteCollapsed.value) return;
@ -460,16 +459,6 @@ if (!props.mock) {
});
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
if (appearNote.value.reactionAcceptance === 'likeOnly') {
useTooltip(reactButton, async (showing) => {
const reactions = await misskeyApiGet('notes/reactions', {
@ -528,7 +517,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
@ -549,7 +538,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
}
@ -728,7 +717,7 @@ function undoRenote(note) : void {
noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!muted && !threadMuted"
v-if="!muted && !threadMuted && !noteMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -138,8 +138,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -227,7 +227,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -336,7 +336,6 @@ const galleryEl = useTemplateRef('galleryEl');
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
@ -352,24 +351,14 @@ const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
const { muted, threadMuted } = checkMutes(appearNote);
const { muted, threadMuted, noteMuted } = checkMutes(appearNote);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
let renoting = false;
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
@ -380,7 +369,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
const keymap = {
'r': () => reply(),
'e|a|plus': () => react(),
'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost); },
'q': () => { if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost); },
'm': () => showMenu(),
'c': () => {
if (!prefer.s.showClipButtonInNoteFooter) return;
@ -554,7 +543,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
} else {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -573,7 +562,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
}
@ -721,12 +710,12 @@ function undoReact(targetNote: Misskey.entities.Note): void {
}
function undoRenote() : void {
if (!renoted.value) return;
if (!appearNote.value.isRenoted) return;
misskeyApi('notes/unrenote', {
noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-show="!isDeleted" v-if="!muted && !threadMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div v-show="!isDeleted" v-if="!muted && !noteMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
@ -31,8 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="false" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -137,7 +137,6 @@ const el = shallowRef<HTMLElement>();
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const isDeleted = ref(false);
const renoted = ref(false);
const reactButton = shallowRef<HTMLElement>();
const clipButton = useTemplateRef('clipButton');
const renoteButton = shallowRef<HTMLElement>();
@ -145,7 +144,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
@ -172,7 +171,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
}
}
const { muted, threadMuted } = checkMutes(appearNote);
const { muted, noteMuted } = checkMutes(appearNote);
useNoteCapture({
rootEl: el,
@ -183,16 +182,6 @@ useNoteCapture({
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
function focus() {
el.value?.focus();
}
@ -270,12 +259,12 @@ function undoReact(note): void {
}
function undoRenote() : void {
if (!renoted.value) return;
if (!appearNote.value.isRenoted) return;
misskeyApi('notes/unrenote', {
noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -322,7 +311,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
});
} else {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -341,7 +330,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
visibility: visibility,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
});
}
}

View file

@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</header>
<div>
<div v-if="muted || threadMuted" :class="[$style.text, $style.muted]">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="note"></SkMutedNote>
<div v-if="muted || threadMuted || noteMuted" :class="[$style.text, $style.muted]">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="note"></SkMutedNote>
</div>
<Mfm v-else :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/>
</div>
@ -50,7 +50,7 @@ defineEmits<{
(event: 'select', user: Misskey.entities.UserLite): void
}>();
const { muted, hardMuted, threadMuted } = checkMutes(computed(() => props.note));
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(computed(() => props.note));
</script>
<style lang="scss" module>

View file

@ -4,7 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<I18n v-if="threadMuted" :src="i18n.ts.userSaysSomethingInMutedThread" tag="small">
<I18n v-if="noteMuted" :src="i18n.ts.userSaysSomethingInMutedNote" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else-if="threadMuted" :src="i18n.ts.userSaysSomethingInMutedThread" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
@ -40,11 +45,15 @@ import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
muted: false | 'sensitiveMute' | string[];
threadMuted: boolean;
threadMuted?: boolean;
noteMuted?: boolean;
note: Misskey.entities.Note;
}>();
}>(), {
threadMuted: false,
noteMuted: false,
});
const mutedWords = computed(() => Array.isArray(props.muted)
? props.muted.join(', ')

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!hardMuted && muted === false && !threadMuted"
v-if="!hardMuted && muted === false && !threadMuted && !noteMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -125,9 +125,9 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
@mousedown.prevent="appearNote.isRenoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -169,7 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
<div v-else>
<!--
@ -310,8 +310,7 @@ const selfNoteIds = computed(() => getSelfNoteIds(props.note));
const isLong = shouldCollapsed(appearNote.value, urls.value);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
const { muted, hardMuted, threadMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const { muted, hardMuted, threadMuted, noteMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
@ -334,7 +333,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
let renoting = false;
@ -349,7 +348,7 @@ const keymap = {
},
'q': () => {
if (renoteCollapsed.value) return;
if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost);
if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost);
},
'm': () => {
if (renoteCollapsed.value) return;
@ -459,16 +458,6 @@ if (!props.mock) {
});
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
if (appearNote.value.reactionAcceptance === 'likeOnly') {
useTooltip(reactButton, async (showing) => {
const reactions = await misskeyApiGet('notes/reactions', {
@ -527,7 +516,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
@ -548,7 +537,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
}
@ -727,7 +716,7 @@ function undoRenote(note) : void {
noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!muted && !threadMuted"
v-if="!muted && !threadMuted && !noteMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -142,8 +142,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -231,7 +231,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -341,7 +341,6 @@ const galleryEl = useTemplateRef('galleryEl');
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : []);
@ -357,24 +356,14 @@ const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
const { muted, threadMuted } = checkMutes(appearNote);
const { muted, threadMuted, noteMuted } = checkMutes(appearNote);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
let renoting = false;
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
@ -385,7 +374,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
const keymap = {
'r': () => reply(),
'e|a|plus': () => react(),
'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(prefer.s.visibilityOnBoost); },
'q': () => { if (canRenote.value && !appearNote.value.isRenoted && !renoting) renote(prefer.s.visibilityOnBoost); },
'm': () => showMenu(),
'c': () => {
if (!prefer.s.showClipButtonInNoteFooter) return;
@ -559,7 +548,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
} else {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -578,7 +567,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
}).finally(() => { renoting = false; });
}
}
@ -726,12 +715,12 @@ function undoReact(targetNote: Misskey.entities.Note): void {
}
function undoRenote() : void {
if (!renoted.value) return;
if (!appearNote.value.isRenoted) return;
misskeyApi('notes/unrenote', {
noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1, [$style.isReply]: props.isReply, [$style.detailed]: props.detailed }]">
<div v-show="!isDeleted" v-if="!muted && !noteMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1, [$style.isReply]: props.isReply, [$style.detailed]: props.detailed }]">
<div v-if="!hideLine" :class="$style.line"></div>
<div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
:style="appearNote.isRenoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop="appearNote.isRenoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="false" :note="appearNote"></SkMutedNote>
<SkMutedNote :muted="muted" :threadMuted="false" :noteMuted="noteMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -151,7 +151,6 @@ const el = shallowRef<HTMLElement>();
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const isDeleted = ref(false);
const renoted = ref(false);
const reactButton = shallowRef<HTMLElement>();
const clipButton = useTemplateRef('clipButton');
const renoteButton = shallowRef<HTMLElement>();
@ -159,7 +158,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const renoteTooltip = computeRenoteTooltip(renoted);
const renoteTooltip = computeRenoteTooltip(appearNote);
const defaultLike = computed(() => prefer.s.like ? prefer.s.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
@ -186,7 +185,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
}
}
const { muted } = checkMutes(appearNote);
const { muted, noteMuted } = checkMutes(appearNote);
useNoteCapture({
rootEl: el,
@ -197,16 +196,6 @@ useNoteCapture({
onDeleteCallback: props.detail && props.depth < prefer.s.numberOfReplies ? props.onDeleteCallback : undefined,
});
if ($i) {
misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
renoted.value = res.length > 0;
});
}
function focus() {
el.value?.focus();
}
@ -284,12 +273,12 @@ function undoReact(note): void {
}
function undoRenote() : void {
if (!renoted.value) return;
if (!appearNote.value.isRenoted) return;
misskeyApi('notes/unrenote', {
noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
appearNote.value.isRenoted = false;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -336,7 +325,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
});
} else {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -355,7 +344,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
visibility: visibility,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
appearNote.value.isRenoted = true;
});
}
}

View file

@ -82,9 +82,9 @@ export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (
];
}
export function computeRenoteTooltip(renoted: Ref<boolean>): ComputedRef<string> {
export function computeRenoteTooltip(note: ComputedRef<Misskey.entities.Note>): ComputedRef<string> {
return computed(() => {
if (renoted.value) return i18n.ts.unrenote;
if (note.value.isRenoted) return i18n.ts.unrenote;
if (prefer.s.showVisibilitySelectorOnBoost) return i18n.ts.renote;
return i18n.ts.renoteShift;
});

View file

@ -22,7 +22,12 @@ export function checkMutes(noteToCheck: ComputedRef<Misskey.entities.Note>, with
const threadMuted = computed(() => {
if (!muteEnable.value) return false;
return noteToCheck.value.isMuting;
return noteToCheck.value.isMutingThread;
});
const noteMuted = computed(() => {
if (!muteEnable.value) return false;
return noteToCheck.value.isMutingNote;
});
const hardMuted = computed(() => {
@ -30,7 +35,7 @@ export function checkMutes(noteToCheck: ComputedRef<Misskey.entities.Note>, with
return checkMute(noteToCheck.value, $i?.hardMutedWords, true);
});
return { muted, hardMuted, threadMuted };
return { muted, hardMuted, threadMuted, noteMuted };
}
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null): false;

View file

@ -213,9 +213,9 @@ export function getNoteMenu(props: {
noteId: appearNote.id,
});
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
os.post({ initialNote: appearNote, renote: appearNote.renote ?? undefined, reply: appearNote.reply ?? undefined, channel: appearNote.channel });
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
if ($i && Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
});
@ -224,8 +224,8 @@ export function getNoteMenu(props: {
function edit(): void {
os.post({
initialNote: appearNote,
renote: appearNote.renote,
reply: appearNote.reply,
renote: appearNote.renote ?? undefined,
reply: appearNote.reply ?? undefined,
channel: appearNote.channel,
editId: appearNote.id,
initialFiles: appearNote.files,
@ -242,6 +242,14 @@ export function getNoteMenu(props: {
function toggleThreadMute(mute: boolean): void {
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
noteId: appearNote.id,
noteOnly: false,
});
}
function toggleNoteMute(mute: boolean): void {
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
noteId: appearNote.id,
noteOnly: true,
});
}
@ -293,9 +301,11 @@ export function getNoteMenu(props: {
const menuItems: MenuItem[] = [];
if ($i) {
/*
const statePromise = misskeyApi('notes/state', {
noteId: appearNote.id,
});
*/
if (props.currentClip?.userId === $i.id) {
menuItems.push({
@ -352,15 +362,19 @@ export function getNoteMenu(props: {
menuItems.push({ type: 'divider' });
menuItems.push(statePromise.then(state => state.isFavorited ? {
icon: 'ti ti-star-off',
text: i18n.ts.unfavorite,
action: () => toggleFavorite(false),
} : {
icon: 'ti ti-star',
text: i18n.ts.favorite,
action: () => toggleFavorite(true),
}));
if (appearNote.isFavorited) {
menuItems.push({
icon: 'ti ti-star-off',
text: i18n.ts.unfavorite,
action: () => toggleFavorite(false),
});
} else {
menuItems.push({
icon: 'ti ti-star',
text: i18n.ts.favorite,
action: () => toggleFavorite(true),
});
}
menuItems.push({
type: 'parent',
@ -369,15 +383,33 @@ export function getNoteMenu(props: {
children: () => getNoteClipMenu(props),
});
menuItems.push(statePromise.then(state => state.isMutedThread ? {
icon: 'ti ti-message-off',
text: i18n.ts.unmuteThread,
action: () => toggleThreadMute(false),
} : {
icon: 'ti ti-message-off',
text: i18n.ts.muteThread,
action: () => toggleThreadMute(true),
}));
if (appearNote.isMutingThread) {
menuItems.push({
icon: 'ti ti-message-off',
text: i18n.ts.unmuteThread,
action: () => toggleThreadMute(false),
});
} else {
menuItems.push({
icon: 'ti ti-message-off',
text: i18n.ts.muteThread,
action: () => toggleThreadMute(true),
});
}
if (appearNote.isMutingNote) {
menuItems.push({
icon: 'ti ti-message-off',
text: i18n.ts.unmuteNote,
action: () => toggleNoteMute(false),
});
} else {
menuItems.push({
icon: 'ti ti-message-off',
text: i18n.ts.muteNote,
action: () => toggleNoteMute(true),
});
}
if (appearNote.userId === $i.id) {
if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {

View file

@ -4701,7 +4701,10 @@ export type components = {
votes: number;
}[];
}) | null;
isMuting: boolean;
isMutingThread: boolean;
isMutingNote: boolean;
isFavorited: boolean;
isRenoted: boolean;
emojis?: {
[key: string]: string;
};
@ -28520,6 +28523,8 @@ export type operations = {
'application/json': {
isFavorited: boolean;
isMutedThread: boolean;
isMutedNote: boolean;
isRenoted: boolean;
};
};
};
@ -28573,6 +28578,8 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
noteId: string;
/** @default false */
noteOnly?: boolean;
};
};
};
@ -28631,6 +28638,8 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
noteId: string;
/** @default false */
noteOnly?: boolean;
};
};
};

View file

@ -29,6 +29,9 @@ muted: "Muted"
renoteMute: "Mute Boosts"
renoteMuted: "Boosts muted"
renoteUnmute: "Unmute Boosts"
muteNote: "Mute note"
unmuteNote: "Unmute note"
userSaysSomethingInMutedNote: "{name} said something in a muted post"
userSaysSomethingInMutedThread: "{name} said something in a muted thread"
markAsNSFW: "Mark all media from user as NSFW"
markInstanceAsNSFW: "Mark as NSFW"