From 3c949f0b8113e88f474b7e27b1c0abcfe0664081 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 11 May 2025 03:34:47 -0400 Subject: [PATCH 001/420] overhaul trending polls * Split into local, global, and completed sections * Don't require credential, but check for local/global timeline perms * Fix rate limit * Return polls where the current user has already voted * Return non-public polls if the user has permission to view them * Apply user/instance blocks * Fetch polls + notes + users in a single step to speed up pack --- locales/index.d.ts | 12 +++ .../endpoints/notes/polls/recommendation.ts | 94 +++++++++++++++---- .../frontend/src/pages/explore.featured.vue | 44 ++++++++- .../misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 8 +- sharkey-locales/en-US.yml | 4 + 6 files changed, 140 insertions(+), 24 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 69c63cc714..8a3799ce58 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -13069,6 +13069,18 @@ export interface Locale extends ILocale { * Users popular on {name} */ "popularUsersLocal": ParameterizedString<"name">; + /** + * Polls trending on {host} + */ + "pollsOnLocal": ParameterizedString<"host">; + /** + * Polls trending on the global network + */ + "pollsOnRemote": string; + /** + * Polls that have ended recently + */ + "pollsExpired": string; /** * Silenced */ diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 33a9c281b3..8fed8ae590 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -9,13 +9,13 @@ import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepo import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['notes'], - requireCredential: true, - kind: 'read:account', - res: { type: 'array', optional: false, nullable: false, @@ -26,10 +26,24 @@ export const meta = { }, }, - // 2 calls per second + errors: { + ltlDisabled: { + message: 'Local timeline has been disabled.', + code: 'LTL_DISABLED', + id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', + }, + gtlDisabled: { + message: 'Global timeline has been disabled.', + code: 'GTL_DISABLED', + id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', + }, + }, + + // Up to 10 calls, then 2 per second limit: { - duration: 1000, - max: 2, + type: 'bucket', + size: 10, + dripRate: 500, }, } as const; @@ -39,6 +53,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, excludeChannels: { type: 'boolean', default: false }, + local: { type: 'boolean', nullable: true, default: null }, + expired: { type: 'boolean', default: false }, }, required: [], } as const; @@ -59,18 +75,54 @@ export default class extends Endpoint { // eslint- private mutingsRepository: MutingsRepository, private noteEntityService: NoteEntityService, + private readonly queryService: QueryService, + private readonly roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const query = this.pollsRepository.createQueryBuilder('poll') - .where('poll.userHost IS NULL') - .andWhere('poll.userId != :meId', { meId: me.id }) - .andWhere('poll.noteVisibility = \'public\'') - .andWhere(new Brackets(qb => { + .innerJoinAndSelect('poll.note', 'note') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('reply.user', 'replyUser') + ; + + if (me) { + query.andWhere('poll.userId != :meId', { meId: me.id }); + } + + if (ps.expired) { + query.andWhere('poll.expiresAt IS NOT NULL'); + query.andWhere('poll.expiresAt < :expiresMax', { + expiresMax: new Date(), + }); + query.andWhere('poll.expiresAt >= :expiresMin', { + expiresMin: new Date(Date.now() - (1000 * 60 * 60 * 24 * 7)), + }); + } else { + query.andWhere(new Brackets(qb => { qb .where('poll.expiresAt IS NULL') .orWhere('poll.expiresAt > :now', { now: new Date() }); })); + } + const policies = await this.roleService.getUserPolicies(me?.id ?? null); + if (ps.local != null) { + if (ps.local) { + if (!policies.ltlAvailable) throw new ApiError(meta.errors.ltlDisabled); + query.andWhere('poll.userHost IS NULL'); + } else { + if (!policies.gtlAvailable) throw new ApiError(meta.errors.gtlDisabled); + query.andWhere('poll.userHost IS NOT NULL'); + } + } else { + if (!policies.ltlAvailable) throw new ApiError(meta.errors.ltlDisabled); + if (!policies.gtlAvailable) throw new ApiError(meta.errors.gtlDisabled); + } + + /* //#region exclude arleady voted polls const votedQuery = this.pollVotesRepository.createQueryBuilder('vote') .select('vote.noteId') @@ -81,16 +133,15 @@ export default class extends Endpoint { // eslint- query.setParameters(votedQuery.getParameters()); //#endregion + */ - //#region mute - const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - query - .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); - - query.setParameters(mutingQuery.getParameters()); + //#region block/mute/vis + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(query); + if (me) { + this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + } //#endregion //#region exclude channels @@ -107,6 +158,7 @@ export default class extends Endpoint { // eslint- if (polls.length === 0) return []; + /* const notes = await this.notesRepository.find({ where: { id: In(polls.map(poll => poll.noteId)), @@ -115,6 +167,10 @@ export default class extends Endpoint { // eslint- id: 'DESC', }, }); + */ + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const notes = polls.map(poll => poll.note!); return await this.noteEntityService.packMany(notes, me, { detail: true, diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index a47e3efbc8..32ee8e2a40 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -10,27 +10,67 @@ SPDX-License-Identifier: AGPL-3.0-only - +
+ + + + + + + + + + + + + + +
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index ee26a8911e..9dc7398062 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -161,6 +161,8 @@ SPDX-License-Identifier: AGPL-3.0-only + + @@ -170,6 +172,7 @@ SPDX-License-Identifier: AGPL-3.0-only From b7b748872927211a7468cdb9c1e762147a3bb9a2 Mon Sep 17 00:00:00 2001 From: piuvas Date: Tue, 27 May 2025 13:53:34 -0300 Subject: [PATCH 027/420] port solution to instance mutelist. --- .../src/pages/settings/mute-block.instance-mute.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue index a0a40e4c72..a9f884c025 100644 --- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue @@ -27,11 +27,12 @@ const $i = ensureSignin(); const instanceMutes = ref($i.mutedInstances.join('\n')); const changed = ref(false); +const autochange = ref(false); async function save() { let mutes = instanceMutes.value .trim().split('\n') - .map(el => el.trim()) + .map(el => el.toLowercase().trim()) .filter(el => el); await misskeyApi('i/update', { @@ -41,10 +42,15 @@ async function save() { changed.value = false; // Refresh filtered list to signal to the user how they've been saved - instanceMutes.value = mutes.join('\n'); + if (instanceMutes.value !== mutes.join('\n')) { + instanceMutes.value = mutes.join('\n'); + autochange.value = true; + } else { autochange.value = false; } } watch(instanceMutes, () => { - changed.value = true; + if (!autochange.value) { + changed.value = true; + } }); From 79c4c79386a9129895784dc047903cda64851a26 Mon Sep 17 00:00:00 2001 From: piuvas Date: Tue, 27 May 2025 14:08:00 -0300 Subject: [PATCH 028/420] fix lint and typecheck. --- packages/backend/src/core/WebhookTestService.ts | 1 + .../frontend/src/pages/settings/attribution-domains-setting.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 2f8cfea7f7..406998dbc7 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -76,6 +76,7 @@ function generateDummyUser(override?: Partial): MiUser { mandatoryCW: null, rejectQuotes: false, allowUnsignedFetch: 'staff', + attributionDomains: [], ...override, }; } diff --git a/packages/frontend/src/pages/settings/attribution-domains-setting.vue b/packages/frontend/src/pages/settings/attribution-domains-setting.vue index 1b887f9ba8..245ab8fe74 100644 --- a/packages/frontend/src/pages/settings/attribution-domains-setting.vue +++ b/packages/frontend/src/pages/settings/attribution-domains-setting.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only From 7a003dec73b79cbfed2ca4586f7a77652b55371d Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 29 May 2025 12:41:36 -0400 Subject: [PATCH 063/420] use SkTransitionGroup for all dynamic TransitionGroup components --- packages/frontend/src/components/MkDateSeparatedList.vue | 5 ++--- packages/frontend/src/components/MkNotifications.vue | 7 ++++--- packages/frontend/src/components/MkReactionsViewer.vue | 6 +++--- packages/frontend/src/components/MkTimeline.vue | 6 +++--- packages/frontend/src/ui/_common_/common.vue | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 63e6b74154..8cf4e5fa2d 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -16,6 +16,7 @@ import { instance } from '@/instance.js'; import { prefer } from '@/preferences.js'; import { getDateText } from '@/utility/timeline-date-separate.js'; import { $i } from '@/i.js'; +import SkTransitionGroup from '@/components/SkTransitionGroup.vue'; export default defineComponent({ props: { @@ -146,14 +147,12 @@ export default defineComponent({ [$style['direction-up']]: props.direction === 'up', }; - return () => prefer.s.animation ? h(TransitionGroup, { + return () => h(SkTransitionGroup, { class: classes, name: 'list', tag: 'div', onBeforeLeave, onLeaveCancelled, - }, { default: renderChildren }) : h('div', { - class: classes, }, { default: renderChildren }); }, }); diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 54edf771ed..46e98462dc 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { prefer } from '@/preferences.js'; +import SkTransitionGroup from '@/components/SkTransitionGroup.vue'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 945640ab41..30264f5e5b 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + + + From 979c7628b1d2b21bd9dd9d13ec0110bde883f074 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 29 May 2025 22:20:21 -0400 Subject: [PATCH 088/420] disable status badge strip in admin-user and instance-info --- locales/index.d.ts | 28 +++++ packages/backend/src/core/UtilityService.ts | 9 ++ .../core/entities/InstanceEntityService.ts | 1 + .../models/json-schema/federation-instance.ts | 4 + .../server/api/endpoints/admin/show-user.ts | 6 + packages/frontend/src/pages/admin-user.vue | 103 ++++++++++++++++-- packages/frontend/src/pages/instance-info.vue | 58 +++++++++- packages/misskey-js/src/autogen/types.ts | 2 + sharkey-locales/en-US.yml | 9 +- 9 files changed, 211 insertions(+), 9 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 2392d51c45..2f3bc664ff 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -13145,10 +13145,38 @@ export interface Locale extends ILocale { * Last posted: {at} */ "lastPosted": ParameterizedString<"at">; + /** + * NSFW + */ + "nsfw": string; /** * Raw */ "raw": string; + /** + * CW + */ + "cw": string; + /** + * Media Silenced + */ + "mediaSilenced": string; + /** + * Bubble + */ + "bubble": string; + /** + * Verified + */ + "verified": string; + /** + * Not Verified + */ + "notVerified": string; + /** + * Hibernated + */ + "hibernated": string; } declare const locales: { [lang: string]: Locale; diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 170afc72dc..ee17906d55 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -67,6 +67,15 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isBubbledHost(host: string | null): boolean { + if (host == null) return false; + + // TODO remove null conditional after merging lab/persisted-instance-blocks + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return this.meta.bubbleInstances?.includes(host); + } + @bindThis public concatNoteContentsForKeyWordCheck(content: { cw?: string | null; diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index fcc9bed3bd..08a7765d40 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -62,6 +62,7 @@ export class InstanceEntityService { rejectReports: instance.rejectReports, rejectQuotes: instance.rejectQuotes, moderationNote: iAmModerator ? instance.moderationNote : null, + isBubbled: this.utilityService.isBubbledHost(instance.host), }; } diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 57d4466ffa..fd6eddf594 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -135,5 +135,9 @@ export const packedFederationInstanceSchema = { type: 'string', optional: true, nullable: true, }, + isBubbled: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1579719246..6a77fc177f 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -122,6 +122,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + isAdministrator: { + type: 'boolean', + optional: false, nullable: false, + }, isSystem: { type: 'boolean', optional: false, nullable: false, @@ -257,6 +261,7 @@ export default class extends Endpoint { // eslint- } const isModerator = await this.roleService.isModerator(user); + const isAdministrator = await this.roleService.isAdministrator(user); const isSilenced = user.isSilenced || !(await this.roleService.getUserPolicies(user.id)).canPublicNote; const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); @@ -289,6 +294,7 @@ export default class extends Endpoint { // eslint- mutedInstances: profile.mutedInstances, notificationRecieveConfig: profile.notificationRecieveConfig, isModerator: isModerator, + isAdministrator: isAdministrator, isSystem: isSystemAccount(user), isSilenced: isSilenced, isSuspended: user.isSuspended, diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 63352f6ca3..f7db436bc8 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -20,16 +20,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ user.id }} - - {{ i18n.ts.notApproved }} - {{ i18n.ts.approved }} - {{ i18n.ts.suspended }} - {{ i18n.ts.silenced }} - {{ i18n.ts.moderator }} - + + {{ i18n.ts.isSystemAccount }} @@ -248,6 +243,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; +import type { Badge } from '@/components/SkBadgeStrip.vue'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -272,6 +268,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkInput from '@/components/MkInput.vue'; import MkNumber from '@/components/MkNumber.vue'; import { copyToClipboard } from '@/utility/copy-to-clipboard'; +import SkBadgeStrip from '@/components/SkBadgeStrip.vue'; const props = withDefaults(defineProps<{ userId: string; @@ -304,6 +301,98 @@ const filesPagination = { })), }; +const badges = computed(() => { + const arr: Badge[] = []; + if (info.value && user.value) { + if (info.value.isSuspended) { + arr.push({ + key: 'suspended', + label: i18n.ts.suspended, + style: 'error', + }); + } + + if (info.value.isSilenced) { + arr.push({ + key: 'silenced', + label: i18n.ts.silenced, + style: 'warning', + }); + } + + if (info.value.alwaysMarkNsfw) { + arr.push({ + key: 'nsfw', + label: i18n.ts.nsfw, + style: 'warning', + }); + } + + if (user.value.mandatoryCW) { + arr.push({ + key: 'cw', + label: i18n.ts.cw, + style: 'warning', + }); + } + + if (info.value.isHibernated) { + arr.push({ + key: 'hibernated', + label: i18n.ts.hibernated, + style: 'neutral', + }); + } + + if (info.value.isAdministrator) { + arr.push({ + key: 'admin', + label: i18n.ts.administrator, + style: 'success', + }); + } else if (info.value.isModerator) { + arr.push({ + key: 'mod', + label: i18n.ts.moderator, + style: 'success', + }); + } + + if (user.value.host == null) { + if (info.value.email) { + if (info.value.emailVerified) { + arr.push({ + key: 'verified', + label: i18n.ts.verified, + style: 'success', + }); + } else { + arr.push({ + key: 'not_verified', + label: i18n.ts.notVerified, + style: 'success', + }); + } + } + + if (info.value.approved) { + arr.push({ + key: 'approved', + label: i18n.ts.approved, + style: 'success', + }); + } else { + arr.push({ + key: 'not_approved', + label: i18n.ts.notApproved, + style: 'warning', + }); + } + } + } + return arr; +}); + const announcementsStatus = ref<'active' | 'archived'>('active'); const announcementsPagination = { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 5d14b6bf2c..356bb4273b 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -24,6 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only + + + @@ -200,10 +203,11 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue index a31c5eff28..9f9feeed49 100644 --- a/packages/frontend/src/components/page/page.vue +++ b/packages/frontend/src/components/page/page.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only From 5e6c6fccc482283bfd0fc369557ffab6cf8617bd Mon Sep 17 00:00:00 2001 From: Outvi V Date: Sat, 31 May 2025 10:09:46 +0800 Subject: [PATCH 132/420] chore: lint --- .../src/components/page/page.note.vue | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index 98f72f0d9d..f459a437d2 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -25,52 +25,54 @@ const props = defineProps<{ const note = ref(null); -let timeoutId = null; +// eslint-disable-next-line id-denylist +let timeoutId: ReturnType | null = null; async function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve() + return new Promise((resolve) => { + window.setTimeout(() => { + resolve(); }, ms); }); } -async function retryOnThrottle(f: ()=>Promise, retryCount: number = 5): Promise { - let lastResult: T = undefined!; - const r = Math.random(); - for(let i=0; i(f: ()=>Promise, retryCount = 5): Promise { + let lastResult: T; + for (let i = 0; i < retryCount; i++) { const [ok, resultOrError] = await f() .then(result => [true, result]) .catch(err => [false, err]); - - lastResult = resultOrError; + + lastResult = resultOrError; if (ok) { break; } // RATE_LIMIT_EXCEEDED - if (resultOrError?.id === "d5826d14-3982-4d2e-8011-b9e9f02499ef") { + if (resultOrError?.id === 'd5826d14-3982-4d2e-8011-b9e9f02499ef') { await sleep(resultOrError?.info?.fullResetMs ?? 1000); continue; } throw resultOrError; } - return lastResult; + return lastResult; } onMounted(() => { if (props.block.note == null) return; - timeoutId = setTimeout(() => { + timeoutId = window.setTimeout(() => { retryOnThrottle(() => misskeyApi('notes/show', { noteId: props.block.note })).then(result => { - note.value = result; + note.value = result; }); }, 500 * props.index); // rate limit is 2 reqs per sec }); onUnmounted(() => { - if (timeoutId !== null) clearTimeout(timeoutId); + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } }); From dcb62a3fcc2c781bc4571adc4adb5523c2cf962e Mon Sep 17 00:00:00 2001 From: piuvas Date: Fri, 30 May 2025 23:18:37 -0300 Subject: [PATCH 133/420] Update navbar.vue --- packages/frontend/src/ui/_common_/navbar.vue | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 6bf0dfc17c..503bfb1602 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -299,6 +300,18 @@ function menuEdit() { backdrop-filter: var(--MI-blur, blur(8px)); } + .banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + } + .instance { position: relative; display: block; From 44f9be3efb62b5b9a40fb1e6024b9f05d2bc54d4 Mon Sep 17 00:00:00 2001 From: piuvas Date: Sat, 31 May 2025 09:46:43 -0300 Subject: [PATCH 134/420] fix indentation. --- packages/frontend/src/ui/_common_/navbar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 503bfb1602..5cf06cd59d 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -300,7 +300,7 @@ function menuEdit() { backdrop-filter: var(--MI-blur, blur(8px)); } - .banner { + .banner { position: absolute; top: 0; left: 0; From e36ea27517c43352fe3173f54e558ad549388f0e Mon Sep 17 00:00:00 2001 From: Outvi V Date: Sat, 31 May 2025 22:50:21 +0800 Subject: [PATCH 135/420] fix(page.note): throw (not return) on all attempts throttled --- .../frontend/src/components/page/page.note.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index f459a437d2..cc1075d42a 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -37,13 +37,15 @@ async function sleep(ms: number): Promise { } async function retryOnThrottle(f: ()=>Promise, retryCount = 5): Promise { - let lastResult: T; + let lastOk: boolean; + let lastResultOrError: T; for (let i = 0; i < retryCount; i++) { const [ok, resultOrError] = await f() .then(result => [true, result]) .catch(err => [false, err]); - lastResult = resultOrError; + lastOk = ok; + lastResultOrError = resultOrError; if (ok) { break; @@ -55,9 +57,16 @@ async function retryOnThrottle(f: ()=>Promise, retryCount = 5): Promise continue; } + // Throw for non-throttling errors throw resultOrError; } - return lastResult; + + if (lastOk) { + return lastResultOrError; + } else { + // Give up after getting throttled too many times + throw lastResultOrError; + } } onMounted(() => { From 0c49f9daf117ac36fface97aee1772562323ad02 Mon Sep 17 00:00:00 2001 From: Marie Date: Sat, 31 May 2025 14:50:53 +0000 Subject: [PATCH 136/420] Add back in tossface --- packages/frontend/src/pages/settings/preferences.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 683fe3ee32..8b89325db7 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -107,6 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
From 6fcc6ba17c2aa159c2820da2720a417774ee00a9 Mon Sep 17 00:00:00 2001 From: Marie Date: Sat, 31 May 2025 19:35:02 +0000 Subject: [PATCH 137/420] fix indenting --- packages/frontend/src/ui/_common_/navbar.vue | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 5cf06cd59d..f0c62aa8e2 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -300,17 +300,17 @@ function menuEdit() { backdrop-filter: var(--MI-blur, blur(8px)); } - .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - } + .banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + } .instance { position: relative; From de7f7984cdd0255e6f84414b0861d547511248a4 Mon Sep 17 00:00:00 2001 From: Outvi V Date: Sun, 1 Jun 2025 06:30:28 +0800 Subject: [PATCH 138/420] chore: move `retryOnThrottled` to frontend-shared --- .../frontend-shared/js/retry-on-throttled.ts | 40 +++++++++++++++++ .../src/components/page/page.note.vue | 44 +------------------ 2 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 packages/frontend-shared/js/retry-on-throttled.ts diff --git a/packages/frontend-shared/js/retry-on-throttled.ts b/packages/frontend-shared/js/retry-on-throttled.ts new file mode 100644 index 0000000000..4234a18b51 --- /dev/null +++ b/packages/frontend-shared/js/retry-on-throttled.ts @@ -0,0 +1,40 @@ +async function sleep(ms: number): Promise { + return new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + }, ms); + }); +} + +export async function retryOnThrottled(f: ()=>Promise, retryCount = 5): Promise { + let lastOk = false; + let lastResultOrError: T; + for (let i = 0; i < retryCount; i++) { + const [ok, resultOrError] = await f() + .then(result => [true, result]) + .catch(err => [false, err]); + + lastOk = ok; + lastResultOrError = resultOrError; + + if (ok) { + break; + } + + // RATE_LIMIT_EXCEEDED + if (resultOrError?.id === 'd5826d14-3982-4d2e-8011-b9e9f02499ef') { + await sleep(resultOrError?.info?.fullResetMs ?? 1000); + continue; + } + + // Throw for non-throttling errors + throw resultOrError; + } + + if (lastOk) { + return lastResultOrError!; + } else { + // Give up after getting throttled too many times + throw lastResultOrError!; + } +} \ No newline at end of file diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index cc1075d42a..3a3c0e4c7b 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + + diff --git a/packages/frontend/src/utility/timeline-date-separate.ts b/packages/frontend/src/utility/timeline-date-separate.ts index e1bc9790b9..ef946b11d6 100644 --- a/packages/frontend/src/utility/timeline-date-separate.ts +++ b/packages/frontend/src/utility/timeline-date-separate.ts @@ -4,7 +4,7 @@ */ import { computed } from 'vue'; -import type { Ref } from 'vue'; +import type { Ref, ComputedRef } from 'vue'; export function getDateText(dateInstance: Date) { const date = dateInstance.getDate(); @@ -25,7 +25,7 @@ export type DateSeparetedTimelineItem = { nextText: string; }; -export function makeDateSeparatedTimelineComputedRef(items: Ref) { +export function makeDateSeparatedTimelineComputedRef(items: Ref | ComputedRef) { return computed[]>(() => { const tl: DateSeparetedTimelineItem[] = []; for (let i = 0; i < items.value.length; i++) { From 645e27fc9e8fcb1c20edd44f4ab5083db319b173 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:05:31 -0400 Subject: [PATCH 237/420] add date separation to report UI --- packages/frontend/src/pages/admin/abuses.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 4ec4372492..6531a3e49d 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -48,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> -
- -
+ + +
@@ -67,6 +67,7 @@ import { definePage } from '@/page.js'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import { store } from '@/store.js'; +import SkDateSeparatedList from '@/components/SkDateSeparatedList.vue'; const reports = useTemplateRef('reports'); From ffa0f06ea032ff6c5fd9daf5baa216cc8431ae94 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:06:20 -0400 Subject: [PATCH 238/420] allow callers to pass in hint objects to admin-user and instance-info --- packages/frontend/src/pages/admin-user.vue | 35 +++++++++++++------ packages/frontend/src/pages/instance-info.vue | 13 ++++--- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 0596b165fd..f629078aac 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -273,8 +273,14 @@ import SkBadgeStrip from '@/components/SkBadgeStrip.vue'; const props = withDefaults(defineProps<{ userId: string; initialTab?: string; + userHint?: Misskey.entities.UserDetailed; + infoHint?: Misskey.entities.AdminShowUserResponse; + ipsHint?: Misskey.entities.AdminGetUserIpsResponse; }>(), { initialTab: 'overview', + userHint: undefined, + infoHint: undefined, + ipsHint: undefined, }); const tab = ref(props.initialTab); @@ -405,16 +411,23 @@ const announcementsPagination = { }; const expandedRoles = ref([]); -function createFetcher() { - return () => Promise.all([misskeyApi('users/show', { - userId: props.userId, - }), misskeyApi('admin/show-user', { - userId: props.userId, - }), iAmAdmin ? misskeyApi('admin/get-user-ips', { - userId: props.userId, - }) : Promise.resolve(null), iAmAdmin ? misskeyApi('ap/get', { - uri: `${url}/users/${props.userId}`, - }).catch(() => null) : null]).then(([_user, _info, _ips, _ap]) => { +function createFetcher(withHint = true) { + return () => Promise.all([ + (withHint && props.userHint) ? props.userHint : misskeyApi('users/show', { + userId: props.userId, + }), + (withHint && props.infoHint) ? props.infoHint : misskeyApi('admin/show-user', { + userId: props.userId, + }), + iAmAdmin + ? (withHint && props.ipsHint) ? props.ipsHint : misskeyApi('admin/get-user-ips', { + userId: props.userId, + }) + : null, + iAmAdmin ? misskeyApi('ap/get', { + uri: `${url}/users/${props.userId}`, + }).catch(() => null) : null], + ).then(([_user, _info, _ips, _ap]) => { user.value = _user; info.value = _info; ips.value = _ips; @@ -432,7 +445,7 @@ function createFetcher() { async function refreshUser() { // Not a typo - createFetcher() returns a function() - await createFetcher()(); + await createFetcher(false)(); } async function onMandatoryCWChanged(value: string) { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index b60bdf3a72..93f673288c 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -238,9 +238,14 @@ import SkBadgeStrip from '@/components/SkBadgeStrip.vue'; const $style = useCssModule(); -const props = defineProps<{ +const props = withDefaults(defineProps<{ host: string; -}>(); + metaHint?: Misskey.entities.AdminMetaResponse; + instanceHint?: Misskey.entities.FederationInstance; +}>(), { + metaHint: undefined, + instanceHint: undefined, +}); const tab = ref('overview'); @@ -365,8 +370,8 @@ async function saveModerationNote() { async function fetch(): Promise { const [m, i] = await Promise.all([ - iAmAdmin ? misskeyApi('admin/meta') : null, - misskeyApi('federation/show-instance', { + props.metaHint ?? (iAmAdmin ? misskeyApi('admin/meta') : null), + props.instanceHint ?? misskeyApi('federation/show-instance', { host: props.host, }), ]); From 82ec78ef73f69da3a0412150b4bc97215f233364 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:08:07 -0400 Subject: [PATCH 239/420] improvements to MkAbuseReport: * Improved styling for user/instance IDs * Show target instance as a section * Load reports and sections faster * Rename "moderation note" to "staff notes" for clarity * Preview reported notes directly --- locales/index.d.ts | 8 +++ .../frontend/src/components/MkAbuseReport.vue | 66 ++++++++++++++++--- sharkey-locales/en-US.yml | 3 + 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index cee973a0a2..84001cb3c5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -13157,6 +13157,14 @@ export interface Locale extends ILocale { * Timeout in milliseconds for translation API requests. */ "translationTimeoutCaption": string; + /** + * Staff notes + */ + "staffNotes": string; + /** + * Icon of {name} + */ + "instanceIconAlt": ParameterizedString<"name">; /** * Attribution Domains */ diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index c52fdb898e..b2999d3899 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -30,13 +30,31 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - +
- + +
+
+ + + + + + +
+
@@ -45,22 +63,23 @@ SPDX-License-Identifier: AGPL-3.0-only
+
- + - +
- +
- +
@@ -78,8 +97,9 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 4196e3ea09..2856923771 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -598,6 +598,9 @@ roleAutomatic: "automatic" translationTimeoutLabel: "Translation timeout" translationTimeoutCaption: "Timeout in milliseconds for translation API requests." +staffNotes: "Staff notes" +instanceIconAlt: "Icon of {name}" + attributionDomains: "Attribution Domains" attributionDomainsDescription: "A list of domains whose content can be attributed to you on link previews, separated by new-line. Any subdomain will also be valid. The following needs to be on the webpage:" writtenBy: "Written by {user}" From f17e4641888d82e28daaf125280a68fc13277cc9 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:19:30 -0400 Subject: [PATCH 240/420] fix WebhookTestService again --- packages/backend/src/core/WebhookTestService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 8c1508df24..7c85a32427 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -77,6 +77,7 @@ function generateDummyUser(override?: Partial): MiUser { mandatoryCW: null, rejectQuotes: false, allowUnsignedFetch: 'staff', + userProfile: null, attributionDomains: [], ...override, }; @@ -363,10 +364,15 @@ export class WebhookTestService { id: 'dummy-abuse-report1', targetUserId: 'dummy-target-user', targetUser: null, + targetUserProfile: null, + targetUserInstance: null, reporterId: 'dummy-reporter-user', reporter: null, + reporterProfile: null, + reporterInstance: null, assigneeId: null, assignee: null, + assigneeProfile: null, resolved: false, forwarded: false, comment: 'This is a dummy report for testing purposes.', From ee3cd216f7cdb3f9e57eca717f547e60c6bd89e5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:19:40 -0400 Subject: [PATCH 241/420] fix TS errors in ReversiService --- packages/backend/src/core/ReversiService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index e31d9e5b1a..b57ab6d9cb 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -588,6 +588,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null, movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null, instance: null, + userProfile: null, } : null, user2: parsed.user2 != null ? { ...parsed.user2, @@ -599,6 +600,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null, movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null, instance: null, + userProfile: null, } : null, }; } else { From 838ac6daa9a90983cc19670cf3b5b786bddfe01b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:19:46 -0400 Subject: [PATCH 242/420] fix unit tests --- packages/backend/test/unit/AbuseReportNotificationService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts index 6d555326fb..55ec755dc7 100644 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ b/packages/backend/test/unit/AbuseReportNotificationService.ts @@ -367,10 +367,15 @@ describe('AbuseReportNotificationService', () => { id: idService.gen(), targetUserId: alice.id, targetUser: alice, + targetUserProfile: null, + targetUserInstance: null, reporterId: bob.id, reporter: bob, + reporterProfile: null, + reporterInstance: null, assigneeId: null, assignee: null, + assigneeProfile: null, resolved: false, forwarded: false, comment: 'test', From 067c5d450006052556f37adb39172dc976c26b10 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 02:22:44 -0400 Subject: [PATCH 243/420] pass index through SkDateSeparatedList.vue --- packages/frontend/src/components/SkDateSeparatedList.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/SkDateSeparatedList.vue b/packages/frontend/src/components/SkDateSeparatedList.vue index 8c62347c0c..239d0c1939 100644 --- a/packages/frontend/src/components/SkDateSeparatedList.vue +++ b/packages/frontend/src/components/SkDateSeparatedList.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only