diff --git a/TODO.md b/TODO.md index 06c094a9..67e4ff28 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,12 @@ # TODO -- Defer retrieving profiles for events from members not anymore in the room +- add room members loading indicator - fix lag when clicking accounts in the AccountBar with a very long room list - - fix: on startup, if a room's last event is a membership change, it won't be visible in timeline no matter what the user config is - fix: there are rooms without messages on first sync -- avatar loading performance problem? +- fix binding loops? - update docstrings - update flatpak nio required version diff --git a/src/backend/backend.py b/src/backend/backend.py index 57814418..fd81c71d 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -221,10 +221,10 @@ class Backend: async def get_profile(self, user_id: str) -> nio.ProfileGetResponse: """Cache and return the matrix profile of `user_id`.""" - if user_id in self.profile_cache: - return self.profile_cache[user_id] - async with self.get_profile_locks[user_id]: + if user_id in self.profile_cache: + return self.profile_cache[user_id] + client = await self.get_any_client() response = await client.get_profile(user_id) diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index e2e44bde..bf457c80 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -1283,49 +1283,89 @@ class MatrixClient(nio.AsyncClient): HTML.rooms_user_id_names[room.room_id].pop(user_id, None) - async def get_member_name_avatar( - self, room_id: str, user_id: str, - ) -> Tuple[str, str]: - """Return a room member's display name and avatar. + async def get_event_profiles(self, room_id: str, event_id: str) -> None: + """Fetch from network an event's sender, target and remover's profile. - If the member isn't found in the room (e.g. they left), their + This should be called from QML, see `MatrixClient.get_member_profile`'s + docstring. + """ + + ev: Event = self.models[self.user_id, room_id, "events"][event_id] + + if not ev.fetch_profile: + return + + get_profile = partial( + self.get_member_profile, room_id, can_fetch_from_network=True, + ) + + if not ev.sender_name and not ev.sender_avatar: + sender_name, sender_avatar, _ = await get_profile(ev.sender_id) + ev.sender_name = sender_name + ev.sender_avatar = sender_avatar + + if ev.target_id and not ev.target_name and not ev.target_avatar: + target_name, target_avatar, _ = await get_profile(ev.target_id) + ev.target_name = target_name + ev.target_avatar = target_avatar + + if ev.redacter_id and not ev.redacter_name: + redacter_name, _, _ = await get_profile(ev.target_id) + ev.redacter_name = redacter_name + + ev.fetch_profile = False + + + async def get_member_profile( + self, room_id: str, user_id: str, can_fetch_from_network: bool = False, + ) -> Tuple[str, str, bool]: + """Return a room member's (display_name, avatar, should_lazy_fetch) + + The returned tuple's last element tells whether + `MatrixClient.get_event_profiles()` should be called by QML + with `can_fetch_from_network = True` when appropriate, + e.g. when this message comes in the user's view. + + If the member isn't found in the room (e.g. they left) and + `can_fetch_from_network` is `True`, their profile is retrieved using `MatrixClient.backend.get_profile()`. """ try: - item = self.models[self.user_id, room_id, "members"][user_id] + member = self.models[self.user_id, room_id, "members"][user_id] + return (member.display_name, member.avatar_url, False) + + except KeyError: # e.g. member is not in the room anymore + if not can_fetch_from_network: + return ("", "", True) - except KeyError: # e.g. user is not anymore in the room try: info = await self.backend.get_profile(user_id) - return (info.displayname or "", info.avatar_url or "") - + return (info.displayname or "", info.avatar_url or "", False) except MatrixError: - return ("", "") - - else: - return (item.display_name, item.avatar_url) + return ("", "", False) async def register_nio_event( self, - room: nio.MatrixRoom, - ev: nio.Event, - event_id: str = "", + room: nio.MatrixRoom, + ev: nio.Event, + event_id: str = "", + override_fetch_profile: Optional[bool] = None, **fields, ) -> None: """Register a `nio.Event` as a `Event` object in our model.""" await self.register_nio_room(room) - sender_name, sender_avatar = \ - await self.get_member_name_avatar(room.room_id, ev.sender) + sender_name, sender_avatar, must_fetch_sender = \ + await self.get_member_profile(room.room_id, ev.sender) target_id = getattr(ev, "state_key", "") or "" - target_name, target_avatar = \ - await self.get_member_name_avatar(room.room_id, target_id) \ - if target_id else ("", "") + target_name, target_avatar, must_fetch_target = \ + await self.get_member_profile(room.room_id, target_id) \ + if target_id else ("", "", False) content = fields.get("content", "").strip() @@ -1349,6 +1389,10 @@ class MatrixClient(nio.AsyncClient): target_name = target_name, target_avatar = target_avatar, links = Event.parse_links(content), + fetch_profile = + (must_fetch_sender or must_fetch_target) + if override_fetch_profile is None else + override_fetch_profile, **fields, ) diff --git a/src/backend/models/items.py b/src/backend/models/items.py index 6bd16c62..665454dd 100644 --- a/src/backend/models/items.py +++ b/src/backend/models/items.py @@ -217,6 +217,7 @@ class Event(ModelItem): sender_id: str = field() sender_name: str = field() sender_avatar: str = field() + fetch_profile: bool = False content: str = "" inline_content: str = "" diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py index 0bb838a9..d2808383 100644 --- a/src/backend/nio_callbacks.py +++ b/src/backend/nio_callbacks.py @@ -196,6 +196,10 @@ class NioCallbacks: async def onRedactedEvent(self, room, ev, event_id: str = "") -> None: + redacter_name, _, must_fetch_redacter = \ + await self.client.get_member_profile(room.room_id, ev.redacter) \ + if ev.redacter else ("", "", False) + await self.client.register_nio_event( room, ev, @@ -206,11 +210,9 @@ class NioCallbacks: type(ev), ev.redacter, ev.sender, ev.reason, ), - redacter_id = ev.redacter or "", - redacter_name = - (await self.client.get_member_name_avatar( - room.room_id, ev.redacter, - ))[0] if ev.redacter else "", + redacter_id = ev.redacter or "", + redacter_name = redacter_name, + override_fetch_profile = True, ) diff --git a/src/gui/Pages/Chat/Timeline/EventDelegate.qml b/src/gui/Pages/Chat/Timeline/EventDelegate.qml index e50e1b5f..17c6ee78 100644 --- a/src/gui/Pages/Chat/Timeline/EventDelegate.qml +++ b/src/gui/Pages/Chat/Timeline/EventDelegate.qml @@ -17,6 +17,8 @@ HColumnLayout { property var hoveredMediaTypeUrl: [] + property var fetchProfilesFuture: null + // Remember timeline goes from newest message at index 0 to oldest readonly property var previousModel: eventList.model.get(model.index + 1) readonly property var nextModel: eventList.model.get(model.index - 1) @@ -64,6 +66,13 @@ HColumnLayout { // HSelectableLabel's MouseArea hover events onCursorShapeChanged: eventList.cursorShape = cursorShape + Component.onCompleted: if (model.fetch_profile) py.callClientCoro( + chat.userId, "get_event_profiles", [chat.roomId, model.id], + ) + + Component.onDestruction: + if (fetchProfilesFuture) fetchProfilesFuture.cancel() + function json() { let event = ModelStore.get(chat.userId, chat.roomId, "events")