Fix presence indicators not showing without cache (#32880)
* Fix presence indicators not showing on initial login without cache * Fix lint exhaustive-deps : useEffect room
This commit is contained in:
@@ -7,7 +7,14 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ReactNode, useEffect, useState } from "react";
|
import React, { type JSX, type ReactNode, useEffect, useState } from "react";
|
||||||
import { ClientEvent, type Room, type RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
ClientEvent,
|
||||||
|
type Room,
|
||||||
|
type RoomMember,
|
||||||
|
RoomStateEvent,
|
||||||
|
type User,
|
||||||
|
UserEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { isPresenceEnabled } from "../../../utils/presence";
|
import { isPresenceEnabled } from "../../../utils/presence";
|
||||||
@@ -65,12 +72,12 @@ export const useDmMember = (room?: Room): RoomMember | null => {
|
|||||||
return dmMember;
|
return dmMember;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPresence(member: RoomMember | null): Presence | null {
|
function getPresenceFromUser(user: User | null | undefined): Presence | null {
|
||||||
if (!member?.user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const presence = member.user.presence;
|
const presence = user.presence;
|
||||||
const isOnline = member.user.currentlyActive || presence === "online";
|
const isOnline = user.currentlyActive || presence === "online";
|
||||||
if (BUSY_PRESENCE_NAME.matches(member.user.presence)) {
|
if (BUSY_PRESENCE_NAME.matches(presence)) {
|
||||||
return Presence.Busy;
|
return Presence.Busy;
|
||||||
}
|
}
|
||||||
if (isOnline) {
|
if (isOnline) {
|
||||||
@@ -86,15 +93,25 @@ function getPresence(member: RoomMember | null): Presence | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPresence(room: Room, member: RoomMember | null): Presence | null {
|
||||||
|
// Fall back to client.getUser() when member.user is not yet linked during initial sync
|
||||||
|
const user = member?.user ?? (member ? room.client.getUser(member.userId) : null);
|
||||||
|
return getPresenceFromUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
export const usePresence = (room: Room, member: RoomMember | null): Presence | null => {
|
export const usePresence = (room: Room, member: RoomMember | null): Presence | null => {
|
||||||
const [presence, setPresence] = useState<Presence | null>(getPresence(member));
|
const [presence, setPresence] = useState<Presence | null>(getPresence(room, member));
|
||||||
const updatePresence = (): void => {
|
const updatePresence = (): void => {
|
||||||
setPresence(getPresence(member));
|
setPresence(getPresence(room, member));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEventEmitter(member?.user, UserEvent.Presence, updatePresence);
|
useEventEmitter(member?.user, UserEvent.Presence, updatePresence);
|
||||||
useEventEmitter(member?.user, UserEvent.CurrentlyActive, updatePresence);
|
useEventEmitter(member?.user, UserEvent.CurrentlyActive, updatePresence);
|
||||||
useEffect(updatePresence, [member]);
|
// Also listen at client level to catch presence events when member.user is not yet linked
|
||||||
|
useEventEmitter(room.client, UserEvent.Presence, (_event: unknown, user: User) => {
|
||||||
|
if (user?.userId === member?.userId) updatePresence();
|
||||||
|
});
|
||||||
|
useEffect(updatePresence, [room, member]);
|
||||||
|
|
||||||
if (getJoinedNonFunctionalMembers(room).length !== 2 || !isPresenceEnabled(room.client)) return null;
|
if (getJoinedNonFunctionalMembers(room).length !== 2 || !isPresenceEnabled(room.client)) return null;
|
||||||
return presence;
|
return presence;
|
||||||
|
|||||||
@@ -99,4 +99,51 @@ describe("WithPresenceIndicator", () => {
|
|||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["online", "Online"],
|
||||||
|
["offline", "Offline"],
|
||||||
|
["unavailable", "Away"],
|
||||||
|
])(
|
||||||
|
"renders presence indicator when member.user is not linked but client has user data",
|
||||||
|
async (presenceStr, renderedStr) => {
|
||||||
|
mocked(isPresenceEnabled).mockReturnValue(true);
|
||||||
|
|
||||||
|
const DM_USER_ID = "@bob:foo.bar";
|
||||||
|
const dmRoomMap = {
|
||||||
|
getUserIdForRoomId: () => {
|
||||||
|
return DM_USER_ID;
|
||||||
|
},
|
||||||
|
} as unknown as DMRoomMap;
|
||||||
|
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||||
|
|
||||||
|
// member.user is not set: simulates the race condition on fresh login with no cache
|
||||||
|
// where the room list renders before member.user is linked
|
||||||
|
room.getMember = jest.fn((userId) => {
|
||||||
|
return new RoomMember(room.roomId, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// But client.getUser() has the presence data
|
||||||
|
const user = new User(DM_USER_ID);
|
||||||
|
user.presence = presenceStr;
|
||||||
|
mockClient.getUser = jest.fn((userId) => (userId === DM_USER_ID ? user : null));
|
||||||
|
|
||||||
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!;
|
||||||
|
expect(presence).toBeVisible();
|
||||||
|
await userEvent.hover(presence!);
|
||||||
|
|
||||||
|
const tooltip = await waitFor(() => {
|
||||||
|
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!);
|
||||||
|
expect(tooltip).toBeVisible();
|
||||||
|
return tooltip;
|
||||||
|
});
|
||||||
|
|
||||||
|
// component should fall back to reading client.getUser() which does have the presence data
|
||||||
|
// so it should render correctly
|
||||||
|
expect(tooltip).toHaveTextContent(renderedStr);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user