fix(direct): hide fresh bridge-ghost invites while the double puppet auto-accepts them
This commit is contained in:
parent
a286d1e2c6
commit
7bf177751b
2 changed files with 48 additions and 1 deletions
|
|
@ -64,6 +64,43 @@ export const isCatalogBotControlRoom = (
|
|||
presets: readonly BotPreset[]
|
||||
): boolean => presets.some((preset) => isBotControlRoom(mx, room, preset));
|
||||
|
||||
// How long a bridge-ghost invite stays hidden from the Direct invite list
|
||||
// while we expect the bridge's double puppet to auto-accept it. Long enough
|
||||
// to cover a busy initial chat sync; short enough that a broken double
|
||||
// puppet leaves the user a manual Accept button instead of a black hole.
|
||||
export const BRIDGE_GHOST_INVITE_GRACE_MS = 2 * 60 * 1000;
|
||||
|
||||
// «Is this pending invite from a bridge ghost puppet of a catalog bot?»
|
||||
// mautrix ghost mxids follow the `<network>_<remote id>` localpart
|
||||
// convention and the catalog bot ids ARE the network slugs (telegram /
|
||||
// whatsapp / discord — config.json is the trusted source), so the prefix
|
||||
// match needs no client-side knowledge beyond the operator config. Invite
|
||||
// rooms carry only stripped state — m.bridge is NOT in it — which is why
|
||||
// this matches on the inviter mxid instead of `isBridgedRoom`.
|
||||
//
|
||||
// Used to suppress the invite-row flicker during bridge sync: the bridge's
|
||||
// double puppet auto-accepts portal invites within seconds, and rendering
|
||||
// Accept/Decline for something that resolves itself reads as UI noise. Only
|
||||
// FRESH invites (younger than the grace window) are hidden — if auto-accept
|
||||
// is broken, the invite resurfaces with manual controls.
|
||||
export const isFreshBridgeGhostInvite = (
|
||||
mx: MatrixClient,
|
||||
room: Room,
|
||||
presets: readonly BotPreset[]
|
||||
): boolean => {
|
||||
const myUserId = mx.getUserId();
|
||||
if (!myUserId) return false;
|
||||
const memberEvent = room.getMember(myUserId)?.events.member;
|
||||
const inviter = memberEvent?.getSender();
|
||||
if (!inviter) return false;
|
||||
const ts = memberEvent?.getTs() ?? 0;
|
||||
if (Date.now() - ts > BRIDGE_GHOST_INVITE_GRACE_MS) return false;
|
||||
return presets.some((preset) => {
|
||||
const botServer = preset.mxid.slice(preset.mxid.indexOf(':'));
|
||||
return inviter.startsWith(`@${preset.id}_`) && inviter.endsWith(botServer);
|
||||
});
|
||||
};
|
||||
|
||||
// Returns the BotPreset that owns this room, or undefined if the room is
|
||||
// not a bot control DM. Used by the RoomMenu's «Show widget» item to
|
||||
// know which `/bots/:id` route to navigate to. Wraps the same matcher as
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { useSetting } from '../../../state/hooks/settings';
|
|||
import { settingsAtom } from '../../../state/settings';
|
||||
import { getMxIdLocalPart } from '../../../utils/matrix';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { useBotPresets } from '../../../features/bots/catalog';
|
||||
import { isFreshBridgeGhostInvite } from '../../../features/bots/room';
|
||||
|
||||
export type DirectInviteEntry = {
|
||||
room: Room;
|
||||
|
|
@ -53,6 +55,7 @@ export const useDirectInvites = (): DirectInviteEntry[] => {
|
|||
const inviteIds = useAtomValue(allInvitesAtom);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const [spamFilterEnabled] = useSetting(settingsAtom, 'inviteSpamFilter');
|
||||
const bots = useBotPresets();
|
||||
const myUserId = mx.getSafeUserId();
|
||||
|
||||
return useMemo(() => {
|
||||
|
|
@ -73,6 +76,13 @@ export const useDirectInvites = (): DirectInviteEntry[] => {
|
|||
inviteIds.forEach((roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
// Bridge portal invites get auto-accepted by the bridge's double
|
||||
// puppet within seconds of arriving — rendering Accept/Decline rows
|
||||
// that vanish on their own reads as flicker, especially during the
|
||||
// post-login burst when the bridge syncs dozens of chats. Fresh ghost
|
||||
// invites are suppressed; stale ones (auto-accept broken) fall
|
||||
// through and stay actionable.
|
||||
if (isFreshBridgeGhostInvite(mx, room, bots)) return;
|
||||
const me = room.getMember(myUserId);
|
||||
const senderId = me?.events.member?.getSender() ?? '';
|
||||
// Moderation signal — sender is banned in a room we share. This is a
|
||||
|
|
@ -93,5 +103,5 @@ export const useDirectInvites = (): DirectInviteEntry[] => {
|
|||
});
|
||||
out.sort((a, b) => b.ts - a.ts);
|
||||
return out;
|
||||
}, [mx, inviteIds, allRooms, myUserId, spamFilterEnabled]);
|
||||
}, [mx, inviteIds, allRooms, myUserId, spamFilterEnabled, bots]);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue