mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(threads): show presence in thread member list (#3368)
This commit is contained in:
1 parent
bef5e33eec
commit
b4c614d21a
4 files changed
+215
-37
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {OutlineFrame} from '@app/features/app/components/layout/OutlineFrame';
|
import {OutlineFrame} from '@app/features/app/components/layout/OutlineFrame';
|
||||||
|
import Authentication from '@app/features/auth/state/Authentication';
|
||||||
import styles from '@app/features/channel/components/ChannelMembers.module.css';
|
import styles from '@app/features/channel/components/ChannelMembers.module.css';
|
||||||
import {MemberListContainer} from '@app/features/channel/components/MemberListContainer';
|
import {MemberListContainer} from '@app/features/channel/components/MemberListContainer';
|
||||||
import {MemberListItem} from '@app/features/channel/components/MemberListItem';
|
import {MemberListItem} from '@app/features/channel/components/MemberListItem';
|
||||||
@@ -10,11 +11,13 @@ import Guilds from '@app/features/guild/state/Guilds';
|
|||||||
import {OFFLINE_DESCRIPTOR, ONLINE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
import {OFFLINE_DESCRIPTOR, ONLINE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||||
|
import LocalPresence from '@app/features/presence/state/LocalPresence';
|
||||||
import ThreadRoster, {type ThreadRosterMember} from '@app/features/threads/state/ThreadRoster';
|
import ThreadRoster, {type ThreadRosterMember} from '@app/features/threads/state/ThreadRoster';
|
||||||
import type {User} from '@app/features/user/models/User';
|
import type {User} from '@app/features/user/models/User';
|
||||||
|
import type {CustomStatus} from '@app/features/user/state/CustomStatus';
|
||||||
import Users from '@app/features/user/state/Users';
|
import Users from '@app/features/user/state/Users';
|
||||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||||
import {StatusTypes} from '@fluxer/constants/src/StatusConstants';
|
import {isOfflineStatus, type StatusType} from '@fluxer/constants/src/StatusConstants';
|
||||||
import {useLingui} from '@lingui/react/macro';
|
import {useLingui} from '@lingui/react/macro';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import {useEffect} from 'react';
|
import {useEffect} from 'react';
|
||||||
@@ -28,10 +31,16 @@ export function useThreadRosterSubscription(thread: Channel | undefined): void {
|
|||||||
}, [guildId, threadId]);
|
}, [guildId, threadId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RosterRow {
|
||||||
|
user: User;
|
||||||
|
status: StatusType;
|
||||||
|
customStatus: CustomStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface RosterGroupProps {
|
interface RosterGroupProps {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
members: ReadonlyArray<{user: User; online: boolean}>;
|
members: ReadonlyArray<RosterRow>;
|
||||||
thread: Channel;
|
thread: Channel;
|
||||||
ownerId: string | null;
|
ownerId: string | null;
|
||||||
}
|
}
|
||||||
@@ -57,14 +66,15 @@ const RosterGroup = observer(({id, label, members, thread, ownerId}: RosterGroup
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.membersList} data-flx="threads.thread-members-panel.roster-group.members-list">
|
<div className={styles.membersList} data-flx="threads.thread-members-panel.roster-group.members-list">
|
||||||
{members.map(({user, online}) => (
|
{members.map(({user, status, customStatus}) => (
|
||||||
<MemberListItem
|
<MemberListItem
|
||||||
key={user.id}
|
key={user.id}
|
||||||
user={user}
|
user={user}
|
||||||
channelId={thread.id}
|
channelId={thread.id}
|
||||||
guildId={thread.guildId}
|
guildId={thread.guildId}
|
||||||
guildMember={thread.guildId ? (GuildMembers.getMember(thread.guildId, user.id) ?? undefined) : undefined}
|
guildMember={thread.guildId ? (GuildMembers.getMember(thread.guildId, user.id) ?? undefined) : undefined}
|
||||||
status={online ? undefined : StatusTypes.OFFLINE}
|
status={status}
|
||||||
|
customStatus={customStatus}
|
||||||
isOwner={user.id === ownerId}
|
isOwner={user.id === ownerId}
|
||||||
disableBackdrop={true}
|
disableBackdrop={true}
|
||||||
data-flx="threads.thread-members-panel.roster-group.member-list-item"
|
data-flx="threads.thread-members-panel.roster-group.member-list-item"
|
||||||
@@ -76,14 +86,17 @@ const RosterGroup = observer(({id, label, members, thread, ownerId}: RosterGroup
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveMembers(
|
function resolveMembers(guildId: string | undefined, roster: ReadonlyArray<ThreadRosterMember>): Array<RosterRow> {
|
||||||
guildId: string | undefined,
|
const members: Array<RosterRow> = [];
|
||||||
roster: ReadonlyArray<ThreadRosterMember>,
|
|
||||||
): Array<{user: User; online: boolean}> {
|
|
||||||
const members: Array<{user: User; online: boolean}> = [];
|
|
||||||
for (const entry of roster) {
|
for (const entry of roster) {
|
||||||
const user = Users.getUser(entry.userId);
|
const user = Users.getUser(entry.userId);
|
||||||
if (user) members.push({user, online: entry.online});
|
if (!user) continue;
|
||||||
|
if (user.id === Authentication.currentUserId) {
|
||||||
|
const status = LocalPresence.getStatus();
|
||||||
|
members.push({user, status, customStatus: isOfflineStatus(status) ? null : LocalPresence.customStatus});
|
||||||
|
} else {
|
||||||
|
members.push({user, status: entry.status, customStatus: entry.customStatus});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
members.sort((a, b) =>
|
members.sort((a, b) =>
|
||||||
NicknameUtils.getNickname(a.user, guildId).localeCompare(NicknameUtils.getNickname(b.user, guildId)),
|
NicknameUtils.getNickname(a.user, guildId).localeCompare(NicknameUtils.getNickname(b.user, guildId)),
|
||||||
@@ -110,7 +123,7 @@ export const ThreadMembersList = observer(({thread}: {thread: Channel}) => {
|
|||||||
<RosterGroup
|
<RosterGroup
|
||||||
id="online"
|
id="online"
|
||||||
label={i18n._(ONLINE_DESCRIPTOR)}
|
label={i18n._(ONLINE_DESCRIPTOR)}
|
||||||
members={members.filter((member) => member.online)}
|
members={members.filter((member) => !isOfflineStatus(member.status))}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
ownerId={ownerId}
|
ownerId={ownerId}
|
||||||
data-flx="threads.thread-members-panel.thread-members-list.roster-group.online"
|
data-flx="threads.thread-members-panel.thread-members-list.roster-group.online"
|
||||||
@@ -118,7 +131,7 @@ export const ThreadMembersList = observer(({thread}: {thread: Channel}) => {
|
|||||||
<RosterGroup
|
<RosterGroup
|
||||||
id="offline"
|
id="offline"
|
||||||
label={i18n._(OFFLINE_DESCRIPTOR)}
|
label={i18n._(OFFLINE_DESCRIPTOR)}
|
||||||
members={members.filter((member) => !member.online)}
|
members={members.filter((member) => isOfflineStatus(member.status))}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
ownerId={ownerId}
|
ownerId={ownerId}
|
||||||
data-flx="threads.thread-members-panel.thread-members-list.roster-group.offline"
|
data-flx="threads.thread-members-panel.thread-members-list.roster-group.offline"
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const selectedGuild = observable({selectedGuildId: null as string | null});
|
|||||||
const updateGuildSubscriptions = vi.fn();
|
const updateGuildSubscriptions = vi.fn();
|
||||||
const gateway = observable({isReady: false, socket: {isConnected: () => true, updateGuildSubscriptions}});
|
const gateway = observable({isReady: false, socket: {isConnected: () => true, updateGuildSubscriptions}});
|
||||||
|
|
||||||
|
vi.mock('@lingui/core/macro', () => ({
|
||||||
|
msg: (descriptor: unknown) => descriptor,
|
||||||
|
t: (descriptor: unknown) => descriptor,
|
||||||
|
}));
|
||||||
vi.mock('@app/features/app/state/RuntimeConfig', () => ({default: {localInstanceDomain: 'fluxer.test'}}));
|
vi.mock('@app/features/app/state/RuntimeConfig', () => ({default: {localInstanceDomain: 'fluxer.test'}}));
|
||||||
vi.mock('@app/features/user/state/Users', () => ({
|
vi.mock('@app/features/user/state/Users', () => ({
|
||||||
default: {getUser: () => undefined, cacheUsers: () => {}, getCurrentUser: () => ({id: 'me', mfaEnabled: true})},
|
default: {getUser: () => undefined, cacheUsers: () => {}, getCurrentUser: () => ({id: 'me', mfaEnabled: true})},
|
||||||
@@ -416,12 +420,13 @@ describe('control arm', () => {
|
|||||||
expect(vi.mocked(http.get)).not.toHaveBeenCalled();
|
expect(vi.mocked(http.get)).not.toHaveBeenCalled();
|
||||||
expect(vi.mocked(http.post)).not.toHaveBeenCalled();
|
expect(vi.mocked(http.post)).not.toHaveBeenCalled();
|
||||||
ThreadGuilds.handleGuild(guildObject([]));
|
ThreadGuilds.handleGuild(guildObject([]));
|
||||||
|
await Promise.resolve();
|
||||||
expect(updateGuildSubscriptions).toHaveBeenCalledWith({
|
expect(updateGuildSubscriptions).toHaveBeenCalledWith({
|
||||||
subscriptions: {[GUILD]: {threads: true, thread_member_lists: []}},
|
subscriptions: {[GUILD]: {threads: true, thread_member_lists: []}},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends thread member lists only for subscribed threads of the active guild', () => {
|
it('sends thread member lists only for subscribed threads of the active guild', async () => {
|
||||||
ThreadGuilds.handleGuild(guildObject([]));
|
ThreadGuilds.handleGuild(guildObject([]));
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
selectedGuild.selectedGuildId = GUILD;
|
selectedGuild.selectedGuildId = GUILD;
|
||||||
@@ -429,10 +434,59 @@ describe('control arm', () => {
|
|||||||
});
|
});
|
||||||
ThreadSubscriptions.handleConnectionReady();
|
ThreadSubscriptions.handleConnectionReady();
|
||||||
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
await Promise.resolve();
|
||||||
expect(updateGuildSubscriptions).toHaveBeenLastCalledWith({
|
expect(updateGuildSubscriptions).toHaveBeenLastCalledWith({
|
||||||
subscriptions: {[GUILD]: {threads: true, thread_member_lists: [THREAD_A]}},
|
subscriptions: {[GUILD]: {threads: true, thread_member_lists: [THREAD_A]}},
|
||||||
});
|
});
|
||||||
release();
|
release();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches thread member lists in a single request', async () => {
|
||||||
|
ThreadGuilds.handleGuild(guildObject([]));
|
||||||
|
runInAction(() => {
|
||||||
|
selectedGuild.selectedGuildId = GUILD;
|
||||||
|
gateway.isReady = true;
|
||||||
|
});
|
||||||
|
ThreadSubscriptions.handleConnectionReady();
|
||||||
|
const releaseA = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
await Promise.resolve();
|
||||||
|
updateGuildSubscriptions.mockClear();
|
||||||
|
releaseA();
|
||||||
|
const releaseB = ThreadRoster.subscribe(GUILD, THREAD_B);
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(updateGuildSubscriptions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateGuildSubscriptions).toHaveBeenCalledWith({
|
||||||
|
subscriptions: {[GUILD]: {threads: true, thread_member_lists: [THREAD_B]}},
|
||||||
|
});
|
||||||
|
releaseB();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the previous guild thread member lists when the selected guild changes', async () => {
|
||||||
|
const otherGuild = '1500000000000000099';
|
||||||
|
ThreadGuilds.handleGuild(guildObject([]));
|
||||||
|
ThreadGuilds.handleGuild({...guildObject([]), id: otherGuild});
|
||||||
|
runInAction(() => {
|
||||||
|
selectedGuild.selectedGuildId = GUILD;
|
||||||
|
gateway.isReady = true;
|
||||||
|
});
|
||||||
|
ThreadSubscriptions.handleConnectionReady();
|
||||||
|
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
await Promise.resolve();
|
||||||
|
updateGuildSubscriptions.mockClear();
|
||||||
|
runInAction(() => {
|
||||||
|
selectedGuild.selectedGuildId = otherGuild;
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(updateGuildSubscriptions).toHaveBeenCalledWith({
|
||||||
|
subscriptions: {
|
||||||
|
[GUILD]: {thread_member_lists: []},
|
||||||
|
[otherGuild]: {threads: true, thread_member_lists: []},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
release();
|
||||||
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serializes a control channel without any new keys', () => {
|
it('serializes a control channel without any new keys', () => {
|
||||||
@@ -509,6 +563,101 @@ describe('control arm', () => {
|
|||||||
expect(isSyncExcludedChannelId(PARENT)).toBe(false);
|
expect(isSyncExcludedChannelId(PARENT)).toBe(false);
|
||||||
expect(isSyncExcludedChannelId('1500000000000000999')).toBe(false);
|
expect(isSyncExcludedChannelId('1500000000000000999')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clears the previous guild thread member lists after a guild switch during a reconnect', async () => {
|
||||||
|
const otherGuild = '1500000000000000099';
|
||||||
|
ThreadGuilds.handleGuild(guildObject([]));
|
||||||
|
ThreadGuilds.handleGuild({...guildObject([]), id: otherGuild});
|
||||||
|
runInAction(() => {
|
||||||
|
selectedGuild.selectedGuildId = GUILD;
|
||||||
|
gateway.isReady = true;
|
||||||
|
});
|
||||||
|
ThreadSubscriptions.handleConnectionReady();
|
||||||
|
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
await Promise.resolve();
|
||||||
|
runInAction(() => {
|
||||||
|
gateway.isReady = false;
|
||||||
|
});
|
||||||
|
release();
|
||||||
|
runInAction(() => {
|
||||||
|
selectedGuild.selectedGuildId = otherGuild;
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
updateGuildSubscriptions.mockClear();
|
||||||
|
runInAction(() => {
|
||||||
|
gateway.isReady = true;
|
||||||
|
});
|
||||||
|
ThreadSubscriptions.handleConnectionReady();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(updateGuildSubscriptions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateGuildSubscriptions).toHaveBeenCalledWith({
|
||||||
|
subscriptions: {
|
||||||
|
[GUILD]: {thread_member_lists: []},
|
||||||
|
[otherGuild]: {threads: true, thread_member_lists: []},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('thread roster', () => {
|
||||||
|
const ONLINE = '1600000000000000001';
|
||||||
|
const IDLE = '1600000000000000002';
|
||||||
|
const OFFLINE = '1600000000000000003';
|
||||||
|
|
||||||
|
function listUpdate(members: Array<{user_id: string; presence: Record<string, unknown>}>) {
|
||||||
|
return {
|
||||||
|
guild_id: GUILD,
|
||||||
|
thread_id: THREAD_A,
|
||||||
|
members: members.map((entry) => ({...entry, join_timestamp: null, flags: 0, member: null})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('keeps each member status and custom status from the list update', () => {
|
||||||
|
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
ThreadRoster.handleListUpdate(
|
||||||
|
listUpdate([
|
||||||
|
{user_id: ONLINE, presence: {status: 'online', afk: false, mobile: false, custom_status: null}},
|
||||||
|
{
|
||||||
|
user_id: IDLE,
|
||||||
|
presence: {
|
||||||
|
status: 'idle',
|
||||||
|
afk: true,
|
||||||
|
mobile: false,
|
||||||
|
custom_status: {emoji_id: '1471401583408697449', emoji_name: 'nahua_smug', text: 'im spooky goop'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{user_id: OFFLINE, presence: {status: 'offline', afk: false, mobile: false}},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(ThreadRoster.getMembers(THREAD_A)).toEqual([
|
||||||
|
{userId: ONLINE, joinTimestamp: null, status: 'online', customStatus: null},
|
||||||
|
{
|
||||||
|
userId: IDLE,
|
||||||
|
joinTimestamp: null,
|
||||||
|
status: 'idle',
|
||||||
|
customStatus: expect.objectContaining({text: 'im spooky goop', emojiName: 'nahua_smug'}),
|
||||||
|
},
|
||||||
|
{userId: OFFLINE, joinTimestamp: null, status: 'offline', customStatus: null},
|
||||||
|
]);
|
||||||
|
release();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces the roster on every list update and ignores updates after release', () => {
|
||||||
|
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||||
|
ThreadRoster.handleListUpdate(
|
||||||
|
listUpdate([
|
||||||
|
{user_id: ONLINE, presence: {status: 'online'}},
|
||||||
|
{user_id: IDLE, presence: {status: 'idle'}},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
ThreadRoster.handleListUpdate(listUpdate([{user_id: ONLINE, presence: {status: 'dnd'}}]));
|
||||||
|
expect(ThreadRoster.getMembers(THREAD_A)).toEqual([
|
||||||
|
{userId: ONLINE, joinTimestamp: null, status: 'dnd', customStatus: null},
|
||||||
|
]);
|
||||||
|
release();
|
||||||
|
ThreadRoster.handleListUpdate(listUpdate([{user_id: ONLINE, presence: {status: 'online'}}]));
|
||||||
|
expect(ThreadRoster.getMembers(THREAD_A)).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('control arm message grouping', () => {
|
describe('control arm message grouping', () => {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||||
import {type StatusType, StatusTypes} from '@fluxer/constants/src/StatusConstants';
|
import {
|
||||||
|
type CustomStatus,
|
||||||
|
fromGatewayCustomStatus,
|
||||||
|
type GatewayCustomStatusPayload,
|
||||||
|
} from '@app/features/user/state/CustomStatus';
|
||||||
|
import {isOfflineStatus, normalizeStatus, type StatusType} from '@fluxer/constants/src/StatusConstants';
|
||||||
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||||
import {makeAutoObservable, observable} from 'mobx';
|
import {makeAutoObservable, observable} from 'mobx';
|
||||||
|
|
||||||
@@ -15,20 +20,15 @@ export interface ThreadMemberListPayload {
|
|||||||
join_timestamp: string | null;
|
join_timestamp: string | null;
|
||||||
flags: number;
|
flags: number;
|
||||||
member: GuildMemberData | null;
|
member: GuildMemberData | null;
|
||||||
presence?: {status?: string} | null;
|
presence?: {status?: string | null; custom_status?: GatewayCustomStatusPayload | null} | null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThreadRosterMember {
|
export interface ThreadRosterMember {
|
||||||
readonly userId: string;
|
readonly userId: string;
|
||||||
readonly joinTimestamp: string | null;
|
readonly joinTimestamp: string | null;
|
||||||
readonly online: boolean;
|
readonly status: StatusType;
|
||||||
}
|
readonly customStatus: CustomStatus | null;
|
||||||
|
|
||||||
const OFFLINE_STATUSES = new Set<string>([StatusTypes.OFFLINE, StatusTypes.INVISIBLE]);
|
|
||||||
|
|
||||||
function isOnline(status: string | undefined): boolean {
|
|
||||||
return status != null && !OFFLINE_STATUSES.has(status as StatusType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThreadRoster {
|
class ThreadRoster {
|
||||||
@@ -80,10 +80,12 @@ class ThreadRoster {
|
|||||||
if (entry.member) {
|
if (entry.member) {
|
||||||
GuildMembers.hydrateIfMissing(payload.guild_id, entry.member);
|
GuildMembers.hydrateIfMissing(payload.guild_id, entry.member);
|
||||||
}
|
}
|
||||||
|
const status = normalizeStatus(entry.presence?.status);
|
||||||
members.push({
|
members.push({
|
||||||
userId: entry.user_id,
|
userId: entry.user_id,
|
||||||
joinTimestamp: entry.join_timestamp,
|
joinTimestamp: entry.join_timestamp,
|
||||||
online: isOnline(entry.presence?.status),
|
status,
|
||||||
|
customStatus: isOfflineStatus(status) ? null : fromGatewayCustomStatus(entry.presence?.custom_status),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.rosters.set(payload.thread_id, members);
|
this.rosters.set(payload.thread_id, members);
|
||||||
|
|||||||
@@ -25,9 +25,14 @@ class ThreadSubscriptions {
|
|||||||
armed = false;
|
armed = false;
|
||||||
private sent = new Map<string, SentState>();
|
private sent = new Map<string, SentState>();
|
||||||
private lastGuildId: string | null = null;
|
private lastGuildId: string | null = null;
|
||||||
|
private listGuilds = new Set<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable<this, 'sent' | 'lastGuildId'>(this, {sent: false, lastGuildId: false}, {autoBind: true});
|
makeAutoObservable<this, 'sent' | 'lastGuildId' | 'listGuilds'>(
|
||||||
|
this,
|
||||||
|
{sent: false, lastGuildId: false, listGuilds: false},
|
||||||
|
{autoBind: true},
|
||||||
|
);
|
||||||
deferUntilModulesLoaded(() => {
|
deferUntilModulesLoaded(() => {
|
||||||
reaction(
|
reaction(
|
||||||
(): Desired => {
|
(): Desired => {
|
||||||
@@ -42,7 +47,7 @@ class ThreadSubscriptions {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
(desired) => this.apply(desired),
|
(desired) => this.apply(desired),
|
||||||
{equals: compareStructural, fireImmediately: true},
|
{equals: compareStructural, fireImmediately: true, scheduler: (run) => queueMicrotask(run)},
|
||||||
);
|
);
|
||||||
reaction(
|
reaction(
|
||||||
() => GatewayConnection.isReady,
|
() => GatewayConnection.isReady,
|
||||||
@@ -74,21 +79,29 @@ class ThreadSubscriptions {
|
|||||||
if (this.lastGuildId) this.sent.delete(this.lastGuildId);
|
if (this.lastGuildId) this.sent.delete(this.lastGuildId);
|
||||||
this.lastGuildId = desired.guildId;
|
this.lastGuildId = desired.guildId;
|
||||||
}
|
}
|
||||||
if (!desired.ready || !desired.guildId || !desired.active) return;
|
if (!desired.ready) return;
|
||||||
|
const guildId = desired.active ? desired.guildId : null;
|
||||||
|
const subscriptions: Record<string, {threads?: boolean; thread_member_lists: Array<string>}> = {};
|
||||||
|
for (const listGuildId of this.listGuilds) {
|
||||||
|
if (listGuildId !== guildId) subscriptions[listGuildId] = {thread_member_lists: []};
|
||||||
|
}
|
||||||
const memberLists = desired.memberLists.join(',');
|
const memberLists = desired.memberLists.join(',');
|
||||||
const previous = this.sent.get(desired.guildId);
|
const previous = guildId ? this.sent.get(guildId) : undefined;
|
||||||
if (previous?.threads && previous.memberLists === memberLists) return;
|
if (guildId && !(previous?.threads && previous.memberLists === memberLists)) {
|
||||||
|
subscriptions[guildId] = {threads: true, thread_member_lists: [...desired.memberLists]};
|
||||||
|
}
|
||||||
|
if (Object.keys(subscriptions).length === 0) return;
|
||||||
const socket = GatewayConnection.socket;
|
const socket = GatewayConnection.socket;
|
||||||
if (!socket?.isConnected()) return;
|
if (!socket?.isConnected()) return;
|
||||||
socket.updateGuildSubscriptions({
|
socket.updateGuildSubscriptions({subscriptions});
|
||||||
subscriptions: {
|
for (const clearedGuildId of Object.keys(subscriptions)) {
|
||||||
[desired.guildId]: {
|
if (clearedGuildId !== guildId) this.listGuilds.delete(clearedGuildId);
|
||||||
threads: true,
|
}
|
||||||
thread_member_lists: [...desired.memberLists],
|
if (guildId && subscriptions[guildId]) {
|
||||||
},
|
this.sent.set(guildId, {threads: true, memberLists});
|
||||||
},
|
if (desired.memberLists.length > 0) this.listGuilds.add(guildId);
|
||||||
});
|
else this.listGuilds.delete(guildId);
|
||||||
this.sent.set(desired.guildId, {threads: true, memberLists});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleActiveGuildsChanged(activeGuildIds: ReadonlyArray<string>): void {
|
private handleActiveGuildsChanged(activeGuildIds: ReadonlyArray<string>): void {
|
||||||
@@ -98,6 +111,7 @@ class ThreadSubscriptions {
|
|||||||
if (active.has(guildId)) continue;
|
if (active.has(guildId)) continue;
|
||||||
this.sent.delete(guildId);
|
this.sent.delete(guildId);
|
||||||
if (socket?.isConnected()) {
|
if (socket?.isConnected()) {
|
||||||
|
this.listGuilds.delete(guildId);
|
||||||
socket.updateGuildSubscriptions({
|
socket.updateGuildSubscriptions({
|
||||||
subscriptions: {[guildId]: {threads: false, thread_member_lists: []}},
|
subscriptions: {[guildId]: {threads: false, thread_member_lists: []}},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user