fix: allow disabling member lists

This commit is contained in:
Hampus Kraft 2026-02-18 21:42:30 +00:00
parent 67267d509d
commit f1bfd080e2
No known key found for this signature in database
GPG Key ID: 6090864C465A454D
5 changed files with 141 additions and 21 deletions

View File

@ -20,6 +20,7 @@
import styles from '@app/components/channel/ChannelMembers.module.css';
import {MemberListContainer} from '@app/components/channel/MemberListContainer';
import {MemberListItem} from '@app/components/channel/MemberListItem';
import {MemberListUnavailableFallback} from '@app/components/channel/shared/MemberListUnavailableFallback';
import {OutlineFrame} from '@app/components/layout/OutlineFrame';
import {resolveMemberListPresence} from '@app/hooks/useMemberListPresence';
import {useMemberListSubscription} from '@app/hooks/useMemberListSubscription';
@ -34,6 +35,7 @@ import type {GroupDMMemberGroup} from '@app/utils/MemberListUtils';
import * as MemberListUtils from '@app/utils/MemberListUtils';
import * as NicknameUtils from '@app/utils/NicknameUtils';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
import {isOfflineStatus} from '@fluxer/constants/src/StatusConstants';
import {useLingui} from '@lingui/react/macro';
import clsx from 'clsx';
@ -152,11 +154,12 @@ interface LazyMemberListProps {
const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMemberListProps) {
const [subscribedRange, setSubscribedRange] = useState<[number, number]>(INITIAL_MEMBER_RANGE);
const memberListUpdatesDisabled = (guild.disabledOperations & GuildOperations.MEMBER_LIST_UPDATES) !== 0;
const {subscribe} = useMemberListSubscription({
guildId: guild.id,
channelId: channel.id,
enabled: true,
enabled: !memberListUpdatesDisabled,
allowInitialUnfocusedLoad: true,
});
@ -181,6 +184,14 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe
[subscribedRange, subscribe],
);
if (memberListUpdatesDisabled) {
return (
<MemberListContainer channelId={channel.id}>
<MemberListUnavailableFallback />
</MemberListContainer>
);
}
if (isLoading) {
return (
<MemberListContainer channelId={channel.id}>

View File

@ -17,10 +17,14 @@
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {GuildRecord} from '@app/records/GuildRecord';
import GuildMemberStore from '@app/stores/GuildMemberStore';
import GuildStore from '@app/stores/GuildStore';
import MemberSidebarStore from '@app/stores/MemberSidebarStore';
import {buildMemberListLayout} from '@app/utils/MemberListLayout';
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {Guild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import type {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {beforeEach, describe, expect, test} from 'vitest';
@ -60,10 +64,25 @@ function seedMembers(guildId: string, members: Array<{id: string; name: string}>
}
}
function createGuild(guildId: string, disabledOperations = 0): GuildRecord {
const guild: Guild = {
id: guildId,
name: `Guild ${guildId}`,
icon: null,
vanity_url_code: null,
owner_id: 'owner-1',
system_channel_id: null,
features: [],
disabled_operations: disabledOperations,
};
return new GuildRecord(guild);
}
describe('MemberSidebarStore', () => {
beforeEach(() => {
MemberSidebarStore.handleSessionInvalidated();
GuildMemberStore.handleConnectionOpen([]);
GuildStore.guilds = {};
});
test('stores members by member index when sync includes group entries', () => {
@ -357,4 +376,39 @@ describe('MemberSidebarStore', () => {
expect(Array.from(listState?.rows.keys() ?? [])).toEqual([0, 1, 2]);
expect(Array.from(listState?.items.values() ?? []).map((item) => item.data.user.id)).toEqual(['u-1', 'u-2']);
});
test('ignores list updates when member list updates are disabled for the guild', () => {
const guildId = 'guild-disabled-updates';
const listId = 'list-disabled-updates';
GuildStore.guilds[guildId] = createGuild(guildId, GuildOperations.MEMBER_LIST_UPDATES);
seedMembers(guildId, [{id: 'u-1', name: 'Alpha'}]);
MemberSidebarStore.handleListUpdate({
guildId,
listId,
memberCount: 1,
onlineCount: 1,
groups: [{id: 'online', count: 1}],
ops: [
{
op: 'SYNC',
range: [0, 1],
items: [{group: {id: 'online', count: 1}}, {member: {user: {id: 'u-1'}}}],
},
],
});
expect(MemberSidebarStore.getList(guildId, listId)).toBeUndefined();
});
test('treats subscribe attempts as no-op when member list updates are disabled for the guild', () => {
const guildId = 'guild-disabled-subscribe';
const channelId = 'channel-disabled-subscribe';
GuildStore.guilds[guildId] = createGuild(guildId, GuildOperations.MEMBER_LIST_UPDATES);
MemberSidebarStore.subscribeToChannel(guildId, channelId, [[0, 99]]);
expect(MemberSidebarStore.getList(guildId, channelId)).toBeUndefined();
expect(MemberSidebarStore.getSubscribedRanges(guildId, channelId)).toEqual([]);
});
});

View File

@ -23,6 +23,7 @@ import {CustomStatusEmitter} from '@app/lib/CustomStatusEmitter';
import {Logger} from '@app/lib/Logger';
import type {GuildMemberRecord} from '@app/records/GuildMemberRecord';
import GuildMemberStore from '@app/stores/GuildMemberStore';
import GuildStore from '@app/stores/GuildStore';
import GatewayConnectionStore from '@app/stores/gateway/GatewayConnectionStore';
import WindowStore from '@app/stores/WindowStore';
import {
@ -31,6 +32,7 @@ import {
getTotalMemberCount,
getTotalRowsFromLayout,
} from '@app/utils/MemberListLayout';
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
import type {StatusType} from '@fluxer/constants/src/StatusConstants';
import {StatusTypes} from '@fluxer/constants/src/StatusConstants';
import {makeAutoObservable} from 'mobx';
@ -165,6 +167,10 @@ class MemberSidebarStore {
ops: Array<MemberListOperation>;
}): void {
const {guildId, listId, channelId, memberCount, onlineCount, groups, ops} = params;
if (this.isMemberListUpdatesDisabled(guildId)) {
return;
}
const storageKey = listId;
const existingGuildLists = this.lists[guildId] ?? {};
const guildLists: Record<string, MemberListState> = {...existingGuildLists};
@ -445,6 +451,10 @@ class MemberSidebarStore {
}
subscribeToChannel(guildId: string, channelId: string, ranges: Array<[number, number]>): void {
if (this.isMemberListUpdatesDisabled(guildId)) {
return;
}
const storageKey = this.resolveListKey(guildId, channelId);
const socket = GatewayConnectionStore.socket;
@ -564,6 +574,14 @@ class MemberSidebarStore {
return listState.customStatuses.get(userId) ?? null;
}
private isMemberListUpdatesDisabled(guildId: string): boolean {
const guild = GuildStore.getGuild(guildId);
if (!guild) {
return false;
}
return (guild.disabledOperations & GuildOperations.MEMBER_LIST_UPDATES) !== 0;
}
private touchList(guildId: string, listId: string): void {
const now = Date.now();
if (!this.lastAccess[guildId]) {

View File

@ -23,7 +23,8 @@
decorate_member_data/3,
extract_member_for_event/3,
collect_and_send_push_notifications/3,
normalize_event/1
normalize_event/1,
is_member_list_updates_enabled/1
]).
-type guild_state() :: map().

View File

@ -22,6 +22,10 @@
handle_cast/2
]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-type guild_state() :: map().
-type user_id() :: integer().
-type session_id() :: binary().
@ -43,30 +47,36 @@ handle_cast({very_large_guild_member_list_deliver, Deliveries}, State) when is_l
-spec handle_lazy_subscribe(map(), guild_state()) -> {reply, ok, guild_state()}.
handle_lazy_subscribe(Request, State) ->
case maps:get(disable_member_list_updates, State, false) of
#{session_id := SessionId, channel_id := ChannelId, ranges := Ranges} = Request,
case should_ignore_member_list_subscribe(Ranges, State) of
true ->
{reply, ok, State};
false ->
#{session_id := SessionId, channel_id := ChannelId, ranges := Ranges} = Request,
Sessions0 = maps:get(sessions, State, #{}),
SessionUserId = get_session_user_id(SessionId, Sessions0),
case
is_integer(SessionUserId) andalso
guild_permissions:can_view_channel(SessionUserId, ChannelId, undefined, State)
of
true ->
GuildId = maps:get(id, State),
ListId = guild_member_list:calculate_list_id(ChannelId, State),
{NewState, ShouldSendSync, NormalizedRanges} =
guild_member_list:subscribe_ranges(SessionId, ListId, Ranges, State),
handle_lazy_subscribe_sync(
ShouldSendSync, NormalizedRanges, GuildId, ListId, ChannelId, SessionId, NewState
);
false ->
{reply, ok, State}
end
Sessions0 = maps:get(sessions, State, #{}),
SessionUserId = get_session_user_id(SessionId, Sessions0),
case
is_integer(SessionUserId) andalso
guild_permissions:can_view_channel(SessionUserId, ChannelId, undefined, State)
of
true ->
GuildId = maps:get(id, State),
ListId = guild_member_list:calculate_list_id(ChannelId, State),
{NewState, ShouldSendSync, NormalizedRanges} =
guild_member_list:subscribe_ranges(SessionId, ListId, Ranges, State),
handle_lazy_subscribe_sync(
ShouldSendSync, NormalizedRanges, GuildId, ListId, ChannelId, SessionId, NewState
);
false ->
{reply, ok, State}
end
end.
-spec should_ignore_member_list_subscribe(list(), guild_state()) -> boolean().
should_ignore_member_list_subscribe([], _State) ->
false;
should_ignore_member_list_subscribe(_Ranges, State) ->
not guild_dispatch:is_member_list_updates_enabled(State).
-spec handle_lazy_subscribe_sync(
boolean(), list(), integer(), term(), channel_id(), session_id(), guild_state()
) ->
@ -325,3 +335,29 @@ can_session_view_channel(SessionData, ChannelId, State) ->
_ ->
false
end.
-ifdef(TEST).
-spec disabled_operations_state(integer() | binary()) -> guild_state().
disabled_operations_state(Value) ->
#{data => #{<<"guild">> => #{<<"disabled_operations">> => Value}}}.
should_ignore_member_list_subscribe_ignores_non_empty_ranges_when_disabled_test() ->
?assertEqual(
true,
should_ignore_member_list_subscribe(
[{0, 99}],
disabled_operations_state(1 bsl 6)
)
).
should_ignore_member_list_subscribe_allows_empty_ranges_when_disabled_test() ->
?assertEqual(
false,
should_ignore_member_list_subscribe(
[],
disabled_operations_state(1 bsl 6)
)
).
-endif.