diff --git a/fluxer_app/src/components/channel/ChannelMembers.tsx b/fluxer_app/src/components/channel/ChannelMembers.tsx
index e67b5df7..ab71ed68 100644
--- a/fluxer_app/src/components/channel/ChannelMembers.tsx
+++ b/fluxer_app/src/components/channel/ChannelMembers.tsx
@@ -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 (
+
+
+
+ );
+ }
+
if (isLoading) {
return (
diff --git a/fluxer_app/src/stores/MemberSidebarStore.test.tsx b/fluxer_app/src/stores/MemberSidebarStore.test.tsx
index 9969e513..4a4eabae 100644
--- a/fluxer_app/src/stores/MemberSidebarStore.test.tsx
+++ b/fluxer_app/src/stores/MemberSidebarStore.test.tsx
@@ -17,10 +17,14 @@
* along with Fluxer. If not, see .
*/
+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([]);
+ });
});
diff --git a/fluxer_app/src/stores/MemberSidebarStore.tsx b/fluxer_app/src/stores/MemberSidebarStore.tsx
index 0ca17204..b1139034 100644
--- a/fluxer_app/src/stores/MemberSidebarStore.tsx
+++ b/fluxer_app/src/stores/MemberSidebarStore.tsx
@@ -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;
}): 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 = {...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]) {
diff --git a/fluxer_gateway/src/guild/guild_dispatch.erl b/fluxer_gateway/src/guild/guild_dispatch.erl
index 2dbe672a..aced1d91 100644
--- a/fluxer_gateway/src/guild/guild_dispatch.erl
+++ b/fluxer_gateway/src/guild/guild_dispatch.erl
@@ -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().
diff --git a/fluxer_gateway/src/guild/guild_subscription_handler.erl b/fluxer_gateway/src/guild/guild_subscription_handler.erl
index 15df38d2..9c7e93bc 100644
--- a/fluxer_gateway/src/guild/guild_subscription_handler.erl
+++ b/fluxer_gateway/src/guild/guild_subscription_handler.erl
@@ -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.