mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): refresh mutual communities on membership change (#2559)
This commit is contained in:
1 parent
00c1b19809
commit
6c0ce9369b
6 files changed
+126
-32
No files matched your search
@@ -23,6 +23,7 @@ import ReadStates from '@app/features/read_state/state/ReadStates';
|
|||||||
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
||||||
import Nagbar from '@app/features/ui/state/Nagbar';
|
import Nagbar from '@app/features/ui/state/Nagbar';
|
||||||
import UserGuildSettings from '@app/features/user/state/UserGuildSettings';
|
import UserGuildSettings from '@app/features/user/state/UserGuildSettings';
|
||||||
|
import UserProfile from '@app/features/user/state/UserProfile';
|
||||||
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
||||||
import {FAVORITES_GUILD_ID} from '@fluxer/constants/src/AppConstants';
|
import {FAVORITES_GUILD_ID} from '@fluxer/constants/src/AppConstants';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export function handleGuildCreate(data: GuildReadyData, _context: GatewayHandler
|
|||||||
GuildCount.handleGuildCreate(data);
|
GuildCount.handleGuildCreate(data);
|
||||||
if (!isSync) {
|
if (!isSync) {
|
||||||
MemberSidebar.handleGuildCreate(data.id);
|
MemberSidebar.handleGuildCreate(data.id);
|
||||||
|
UserProfile.handleGuildCreate();
|
||||||
}
|
}
|
||||||
if (!data.unavailable) {
|
if (!data.unavailable) {
|
||||||
Channels.handleGuildCreate(data);
|
Channels.handleGuildCreate(data);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import MentionFeed from '@app/features/notification/state/MentionFeed';
|
|||||||
import Permission from '@app/features/permissions/state/Permission';
|
import Permission from '@app/features/permissions/state/Permission';
|
||||||
import Presence from '@app/features/presence/state/Presence';
|
import Presence from '@app/features/presence/state/Presence';
|
||||||
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
||||||
|
import UserProfile from '@app/features/user/state/UserProfile';
|
||||||
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
||||||
import Webhooks from '@app/features/webhook/state/Webhooks';
|
import Webhooks from '@app/features/webhook/state/Webhooks';
|
||||||
import type {Guild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
import type {Guild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||||
@@ -52,5 +53,6 @@ export function handleGuildDelete(data: GuildDeletePayload, _context: GatewayHan
|
|||||||
Messages.handleGuildUnavailable(data.id, data.unavailable ?? false);
|
Messages.handleGuildUnavailable(data.id, data.unavailable ?? false);
|
||||||
Messages.handleCleanup();
|
Messages.handleCleanup();
|
||||||
MentionFeed.handleGuildDelete(data.id);
|
MentionFeed.handleGuildDelete(data.id);
|
||||||
|
UserProfile.handleGuildDelete(data.unavailable);
|
||||||
QuickSwitcher.recomputeIfOpen();
|
QuickSwitcher.recomputeIfOpen();
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import GuildMembers from '@app/features/member/state/GuildMembers';
|
|||||||
import MemberSearch from '@app/features/member/state/MemberSearch';
|
import MemberSearch from '@app/features/member/state/MemberSearch';
|
||||||
import Permission from '@app/features/permissions/state/Permission';
|
import Permission from '@app/features/permissions/state/Permission';
|
||||||
import Presence from '@app/features/presence/state/Presence';
|
import Presence from '@app/features/presence/state/Presence';
|
||||||
|
import UserProfile from '@app/features/user/state/UserProfile';
|
||||||
import Users from '@app/features/user/state/Users';
|
import Users from '@app/features/user/state/Users';
|
||||||
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||||
import type {User} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
import type {User} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||||
@@ -21,4 +22,5 @@ export function handleGuildMemberAdd(data: GuildMemberAddPayload, _context: Gate
|
|||||||
GuildVerification.handleGuildMemberUpdate(data.guild_id);
|
GuildVerification.handleGuildMemberUpdate(data.guild_id);
|
||||||
Presence.handleGuildMemberAdd(data.guild_id, data.user.id);
|
Presence.handleGuildMemberAdd(data.guild_id, data.user.id);
|
||||||
MemberSearch.handleMemberAdd(data.guild_id, data.user.id);
|
MemberSearch.handleMemberAdd(data.guild_id, data.user.id);
|
||||||
|
UserProfile.handleGuildMemberAdd(data.user.id);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import GuildMembers from '@app/features/member/state/GuildMembers';
|
|||||||
import MemberSearch from '@app/features/member/state/MemberSearch';
|
import MemberSearch from '@app/features/member/state/MemberSearch';
|
||||||
import Permission from '@app/features/permissions/state/Permission';
|
import Permission from '@app/features/permissions/state/Permission';
|
||||||
import Presence from '@app/features/presence/state/Presence';
|
import Presence from '@app/features/presence/state/Presence';
|
||||||
|
import UserProfile from '@app/features/user/state/UserProfile';
|
||||||
import Users from '@app/features/user/state/Users';
|
import Users from '@app/features/user/state/Users';
|
||||||
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||||
|
|
||||||
@@ -19,4 +20,5 @@ export function handleGuildMemberRemove(data: GuildMemberRemovePayload, _context
|
|||||||
GuildMembers.handleMemberRemove(data.guild_id, data.user.id);
|
GuildMembers.handleMemberRemove(data.guild_id, data.user.id);
|
||||||
Permission.handleGuildMemberUpdate(data.user.id);
|
Permission.handleGuildMemberUpdate(data.user.id);
|
||||||
Presence.handleGuildMemberRemove(data.guild_id, data.user.id);
|
Presence.handleGuildMemberRemove(data.guild_id, data.user.id);
|
||||||
|
UserProfile.handleGuildMemberRemove(data.user.id);
|
||||||
}
|
}
|
||||||
@@ -24,26 +24,30 @@ class UserProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleGatewayReady(): void {
|
handleGatewayReady(): void {
|
||||||
Object.values(this.profileTimeouts).forEach(clearTimeout);
|
this.clearAllProfiles();
|
||||||
this.profiles = {};
|
|
||||||
this.profileTimeouts = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProfileInvalidate(userId: string, guildId?: string): void {
|
handleProfileInvalidate(userId: string, guildId?: string): void {
|
||||||
const targetGuildId = guildId ?? ME;
|
const targetGuildId = guildId ?? ME;
|
||||||
this.clearProfileTimeout(userId, targetGuildId);
|
this.clearProfileTimeout(userId, targetGuildId);
|
||||||
const userProfiles = this.profiles[userId];
|
this.removeProfile(userId, targetGuildId);
|
||||||
if (!userProfiles) return;
|
}
|
||||||
const {[targetGuildId]: _, ...remainingGuildProfiles} = userProfiles;
|
|
||||||
if (Object.keys(remainingGuildProfiles).length === 0) {
|
handleGuildMemberAdd(userId: string): void {
|
||||||
const {[userId]: __, ...remainingProfiles} = this.profiles;
|
this.clearUserProfiles(userId);
|
||||||
this.profiles = remainingProfiles;
|
}
|
||||||
} else {
|
|
||||||
this.profiles = {
|
handleGuildMemberRemove(userId: string): void {
|
||||||
...this.profiles,
|
this.clearUserProfiles(userId);
|
||||||
[userId]: remainingGuildProfiles,
|
}
|
||||||
};
|
|
||||||
}
|
handleGuildCreate(): void {
|
||||||
|
this.clearAllProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGuildDelete(unavailable?: boolean): void {
|
||||||
|
if (unavailable) return;
|
||||||
|
this.clearAllProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProfileCreate(profile: Profile): void {
|
handleProfileCreate(profile: Profile): void {
|
||||||
@@ -82,6 +86,37 @@ class UserProfile {
|
|||||||
this.profileTimeouts = updatedTimeouts;
|
this.profileTimeouts = updatedTimeouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearAllProfiles(): void {
|
||||||
|
Object.values(this.profileTimeouts).forEach(clearTimeout);
|
||||||
|
this.profiles = {};
|
||||||
|
this.profileTimeouts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearUserProfiles(userId: string): void {
|
||||||
|
const userProfiles = this.profiles[userId];
|
||||||
|
if (!userProfiles) return;
|
||||||
|
for (const guildId of Object.keys(userProfiles)) {
|
||||||
|
this.clearProfileTimeout(userId, guildId);
|
||||||
|
}
|
||||||
|
const {[userId]: _, ...remainingProfiles} = this.profiles;
|
||||||
|
this.profiles = remainingProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeProfile(userId: string, guildId: string): void {
|
||||||
|
const userProfiles = this.profiles[userId];
|
||||||
|
if (!userProfiles) return;
|
||||||
|
const {[guildId]: _, ...remainingGuildProfiles} = userProfiles;
|
||||||
|
if (Object.keys(remainingGuildProfiles).length === 0) {
|
||||||
|
const {[userId]: __, ...remainingProfiles} = this.profiles;
|
||||||
|
this.profiles = remainingProfiles;
|
||||||
|
} else {
|
||||||
|
this.profiles = {
|
||||||
|
...this.profiles,
|
||||||
|
[userId]: remainingGuildProfiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private createTimeoutKey(userId: string, guildId: string): string {
|
private createTimeoutKey(userId: string, guildId: string): string {
|
||||||
return `${userId}:${guildId}`;
|
return `${userId}:${guildId}`;
|
||||||
}
|
}
|
||||||
@@ -101,23 +136,8 @@ class UserProfile {
|
|||||||
this.clearProfileTimeout(userId, guildId);
|
this.clearProfileTimeout(userId, guildId);
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const userProfiles = this.profiles[userId];
|
this.removeProfile(userId, guildId);
|
||||||
if (!userProfiles) {
|
const {[timeoutKey]: _, ...remainingTimeouts} = this.profileTimeouts;
|
||||||
const {[timeoutKey]: _, ...remainingTimeouts} = this.profileTimeouts;
|
|
||||||
this.profileTimeouts = remainingTimeouts;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {[guildId]: _, ...remainingGuildProfiles} = userProfiles;
|
|
||||||
if (Object.keys(remainingGuildProfiles).length === 0) {
|
|
||||||
const {[userId]: __, ...remainingProfiles} = this.profiles;
|
|
||||||
this.profiles = remainingProfiles;
|
|
||||||
} else {
|
|
||||||
this.profiles = {
|
|
||||||
...this.profiles,
|
|
||||||
[userId]: remainingGuildProfiles,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const {[timeoutKey]: ___, ...remainingTimeouts} = this.profileTimeouts;
|
|
||||||
this.profileTimeouts = remainingTimeouts;
|
this.profileTimeouts = remainingTimeouts;
|
||||||
});
|
});
|
||||||
}, PROFILE_TIMEOUT_MS);
|
}, PROFILE_TIMEOUT_MS);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import type {Profile} from '@app/features/user/models/Profile';
|
||||||
|
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('@app/features/auth/state/Authentication', () => ({default: {currentUserId: null}}));
|
||||||
|
|
||||||
|
const {default: UserProfile} = await import('@app/features/user/state/UserProfile');
|
||||||
|
|
||||||
|
const GUILD_ID = '1000';
|
||||||
|
const TARGET_ID = '2000';
|
||||||
|
|
||||||
|
function cache(userId: string = TARGET_ID, guildId?: string): void {
|
||||||
|
UserProfile.handleProfileCreate({
|
||||||
|
userId,
|
||||||
|
guildId: guildId ?? null,
|
||||||
|
mutualGuilds: [{id: GUILD_ID, nick: null}],
|
||||||
|
} as unknown as Profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UserProfile cache invalidation', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
UserProfile.handleGatewayReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops every cached scope for a member removed from a community', () => {
|
||||||
|
cache();
|
||||||
|
cache(TARGET_ID, GUILD_ID);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)?.mutualGuilds).toHaveLength(1);
|
||||||
|
UserProfile.handleGuildMemberRemove(TARGET_ID);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID, GUILD_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops every cached scope for a member who joined a community', () => {
|
||||||
|
cache();
|
||||||
|
UserProfile.handleGuildMemberAdd(TARGET_ID);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps other users cached when one member is removed', () => {
|
||||||
|
cache();
|
||||||
|
cache('3000');
|
||||||
|
UserProfile.handleGuildMemberRemove(TARGET_ID);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||||
|
expect(UserProfile.getProfile('3000')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops cached profiles when the viewer leaves a community', () => {
|
||||||
|
cache();
|
||||||
|
UserProfile.handleGuildDelete(false);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps cached profiles when a community only goes unavailable', () => {
|
||||||
|
cache();
|
||||||
|
UserProfile.handleGuildDelete(true);
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops cached profiles when the viewer joins a community', () => {
|
||||||
|
cache();
|
||||||
|
UserProfile.handleGuildCreate();
|
||||||
|
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user