From 7601bf98eef34edff1e341f456651521f4fa2a82 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 21 Sep 2026 15:34:18 +0200 Subject: [PATCH] fix(channel): sync a cleared group DM name without a reload (#2882) --- fluxer_api/src/api/channel/ChannelMappers.ts | 2 +- .../channel/tests/GroupDmNameClear.test.ts | 62 +++++++++++++++++++ fluxer_api/src/api/openapi/openapi.json | 2 +- .../src/features/channel/models/Channel.ts | 4 +- .../src/domains/channel/ChannelSchemas.ts | 4 +- 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 fluxer_api/src/api/channel/tests/GroupDmNameClear.test.ts diff --git a/fluxer_api/src/api/channel/ChannelMappers.ts b/fluxer_api/src/api/channel/ChannelMappers.ts index fee8dbd00..41d43c23f 100644 --- a/fluxer_api/src/api/channel/ChannelMappers.ts +++ b/fluxer_api/src/api/channel/ChannelMappers.ts @@ -150,7 +150,7 @@ function serializeGroupDMChannel(channel: Channel): ChannelResponse { return { ...serializeBaseChannelFields(channel), ...serializeMessageableFields(channel), - name: channel.name ?? undefined, + name: channel.name ?? null, icon: channel.iconHash ?? null, owner_id: channel.ownerId ? channel.ownerId.toString() : null, nicks: nicknameMap.size > 0 ? nicks : undefined, diff --git a/fluxer_api/src/api/channel/tests/GroupDmNameClear.test.ts b/fluxer_api/src/api/channel/tests/GroupDmNameClear.test.ts new file mode 100644 index 000000000..1458d7407 --- /dev/null +++ b/fluxer_api/src/api/channel/tests/GroupDmNameClear.test.ts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {createFriendship, createGroupDmChannel, getChannel} from '@app/api/channel/tests/ChannelTestUtils'; +import {ensureSessionStarted} from '@app/api/message/tests/MessageTestUtils'; +import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {NoopGatewayService} from '@app/api/test/NoopGatewayService'; +import {HTTP_STATUS} from '@app/api/test/TestConstants'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; +import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; +import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; + +describe('Group DM name clear', () => { + let harness: ApiTestHarness; + beforeAll(async () => { + harness = await createApiTestHarness(); + }); + beforeEach(async () => { + await harness.reset(); + }); + afterAll(async () => { + await harness?.shutdown(); + }); + it.each([ + ['an empty string', ''], + ['null', null], + ])('sends a null name to every recipient when cleared with %s', async (_label, clearedName) => { + const user1 = await createTestAccount(harness); + const user2 = await createTestAccount(harness); + const user3 = await createTestAccount(harness); + await ensureSessionStarted(harness, user1.token); + await ensureSessionStarted(harness, user2.token); + await ensureSessionStarted(harness, user3.token); + await createFriendship(harness, user1, user2); + await createFriendship(harness, user1, user3); + const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]); + await createBuilder(harness, user1.token) + .patch(`/channels/${groupDm.id}`) + .body({name: 'Weekend plans'}) + .expect(HTTP_STATUS.OK) + .execute(); + const dispatchSpy = vi.spyOn(NoopGatewayService.prototype, 'dispatchPresence'); + try { + const cleared = await createBuilder(harness, user1.token) + .patch(`/channels/${groupDm.id}`) + .body({name: clearedName}) + .expect(HTTP_STATUS.OK) + .execute(); + expect(cleared).toHaveProperty('name', null); + const channelUpdates = dispatchSpy.mock.calls.filter(([params]) => params.event === 'CHANNEL_UPDATE'); + expect(channelUpdates.map(([params]) => params.userId.toString()).sort()).toEqual( + [user1.userId, user2.userId, user3.userId].sort(), + ); + for (const [params] of channelUpdates) { + expect(params.data).toHaveProperty('name', null); + } + } finally { + dispatchSpy.mockRestore(); + } + expect(await getChannel(harness, user2.token, groupDm.id)).toHaveProperty('name', null); + }); +}); diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 782ee1dd2..b0b346bae 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -24224,7 +24224,7 @@ "description": "The ID of the guild this channel belongs to", "$ref": "#/components/schemas/SnowflakeStringType" }, - "name": {"description": "The name of the channel", "type": "string"}, + "name": {"description": "The name of the channel", "type": ["string", "null"]}, "topic": {"description": "The topic of the channel", "type": ["string", "null"]}, "url": { "description": "The URL associated with the channel", diff --git a/fluxer_app/src/features/channel/models/Channel.ts b/fluxer_app/src/features/channel/models/Channel.ts index 51e560032..70c7283cd 100644 --- a/fluxer_app/src/features/channel/models/Channel.ts +++ b/fluxer_app/src/features/channel/models/Channel.ts @@ -87,7 +87,7 @@ export class Channel { this.instanceId = options?.instanceId ?? RuntimeConfig.localInstanceDomain; this.id = channel.id; this.guildId = channel.guild_id; - this.name = channel.name; + this.name = channel.name ?? undefined; noteText(this.name); this.topic = channel.topic ?? null; this.url = channel.url ?? null; @@ -223,7 +223,7 @@ export class Channel { { id: this.id, guild_id: updates.guild_id ?? this.guildId, - name: updates.name ?? this.name, + name: updates.name !== undefined ? updates.name : this.name, topic: updates.topic !== undefined ? updates.topic : this.topic, url: updates.url !== undefined ? updates.url : this.url, icon: updates.icon !== undefined ? updates.icon : this.icon, diff --git a/packages/schema/src/domains/channel/ChannelSchemas.ts b/packages/schema/src/domains/channel/ChannelSchemas.ts index ec25f0e95..b73f91952 100644 --- a/packages/schema/src/domains/channel/ChannelSchemas.ts +++ b/packages/schema/src/domains/channel/ChannelSchemas.ts @@ -50,7 +50,7 @@ export type CallEligibilityResponse = z.infer; export const ChannelResponse = z.object({ id: SnowflakeStringType.describe('The unique identifier (snowflake) for this channel'), guild_id: SnowflakeStringType.optional().describe('The ID of the guild this channel belongs to'), - name: z.string().optional().describe('The name of the channel'), + name: z.string().nullish().describe('The name of the channel'), topic: z.string().nullish().describe('The topic of the channel'), url: z.url().nullish().describe('The URL associated with the channel'), icon: z.string().nullish().describe('The icon hash of the channel (for group DMs)'), @@ -141,7 +141,7 @@ export interface ChannelOverwrite { export interface Channel { readonly id: string; readonly guild_id?: string; - readonly name?: string; + readonly name?: string | null; readonly topic?: string | null; readonly url?: string | null; readonly icon?: string | null;