Defer fetching user profiles for events

Previously, events for which the sender, target (state_key) or remover
was missing from the room members would have their profile fetched
from network when registering the event into models.

This could cause very slow past events loading times for rooms, since
the event registering function (which contained the profile retrieval
directives) is run sequentially event-by-event.

Missing profiles are now lazy-loaded when events come into the
user's view in the QML timeline.
This commit is contained in:
miruka 2020-05-20 03:42:40 -04:00
parent bc5549195b
commit 63af4be1e2
6 changed files with 87 additions and 32 deletions

View File

@ -1,13 +1,12 @@
# TODO # 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 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, - 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 it won't be visible in timeline no matter what the user config is
- fix: there are rooms without messages on first sync - fix: there are rooms without messages on first sync
- avatar loading performance problem? - fix binding loops?
- update docstrings - update docstrings
- update flatpak nio required version - update flatpak nio required version

View File

@ -221,10 +221,10 @@ class Backend:
async def get_profile(self, user_id: str) -> nio.ProfileGetResponse: async def get_profile(self, user_id: str) -> nio.ProfileGetResponse:
"""Cache and return the matrix profile of `user_id`.""" """Cache and return the matrix profile of `user_id`."""
async with self.get_profile_locks[user_id]:
if user_id in self.profile_cache: if user_id in self.profile_cache:
return self.profile_cache[user_id] return self.profile_cache[user_id]
async with self.get_profile_locks[user_id]:
client = await self.get_any_client() client = await self.get_any_client()
response = await client.get_profile(user_id) response = await client.get_profile(user_id)

View File

@ -1283,28 +1283,67 @@ class MatrixClient(nio.AsyncClient):
HTML.rooms_user_id_names[room.room_id].pop(user_id, None) HTML.rooms_user_id_names[room.room_id].pop(user_id, None)
async def get_member_name_avatar( async def get_event_profiles(self, room_id: str, event_id: str) -> None:
self, room_id: str, user_id: str, """Fetch from network an event's sender, target and remover's profile.
) -> Tuple[str, str]:
"""Return a room member's display name and avatar.
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()`. profile is retrieved using `MatrixClient.backend.get_profile()`.
""" """
try: 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: try:
info = await self.backend.get_profile(user_id) 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: except MatrixError:
return ("", "") return ("", "", False)
else:
return (item.display_name, item.avatar_url)
async def register_nio_event( async def register_nio_event(
@ -1312,20 +1351,21 @@ class MatrixClient(nio.AsyncClient):
room: nio.MatrixRoom, room: nio.MatrixRoom,
ev: nio.Event, ev: nio.Event,
event_id: str = "", event_id: str = "",
override_fetch_profile: Optional[bool] = None,
**fields, **fields,
) -> None: ) -> None:
"""Register a `nio.Event` as a `Event` object in our model.""" """Register a `nio.Event` as a `Event` object in our model."""
await self.register_nio_room(room) await self.register_nio_room(room)
sender_name, sender_avatar = \ sender_name, sender_avatar, must_fetch_sender = \
await self.get_member_name_avatar(room.room_id, ev.sender) await self.get_member_profile(room.room_id, ev.sender)
target_id = getattr(ev, "state_key", "") or "" target_id = getattr(ev, "state_key", "") or ""
target_name, target_avatar = \ target_name, target_avatar, must_fetch_target = \
await self.get_member_name_avatar(room.room_id, target_id) \ await self.get_member_profile(room.room_id, target_id) \
if target_id else ("", "") if target_id else ("", "", False)
content = fields.get("content", "").strip() content = fields.get("content", "").strip()
@ -1349,6 +1389,10 @@ class MatrixClient(nio.AsyncClient):
target_name = target_name, target_name = target_name,
target_avatar = target_avatar, target_avatar = target_avatar,
links = Event.parse_links(content), 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, **fields,
) )

View File

@ -217,6 +217,7 @@ class Event(ModelItem):
sender_id: str = field() sender_id: str = field()
sender_name: str = field() sender_name: str = field()
sender_avatar: str = field() sender_avatar: str = field()
fetch_profile: bool = False
content: str = "" content: str = ""
inline_content: str = "" inline_content: str = ""

View File

@ -196,6 +196,10 @@ class NioCallbacks:
async def onRedactedEvent(self, room, ev, event_id: str = "") -> None: 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( await self.client.register_nio_event(
room, room,
ev, ev,
@ -207,10 +211,8 @@ class NioCallbacks:
), ),
redacter_id = ev.redacter or "", redacter_id = ev.redacter or "",
redacter_name = redacter_name = redacter_name,
(await self.client.get_member_name_avatar( override_fetch_profile = True,
room.room_id, ev.redacter,
))[0] if ev.redacter else "",
) )

View File

@ -17,6 +17,8 @@ HColumnLayout {
property var hoveredMediaTypeUrl: [] property var hoveredMediaTypeUrl: []
property var fetchProfilesFuture: null
// Remember timeline goes from newest message at index 0 to oldest // Remember timeline goes from newest message at index 0 to oldest
readonly property var previousModel: eventList.model.get(model.index + 1) readonly property var previousModel: eventList.model.get(model.index + 1)
readonly property var nextModel: 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 // HSelectableLabel's MouseArea hover events
onCursorShapeChanged: eventList.cursorShape = cursorShape 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() { function json() {
let event = ModelStore.get(chat.userId, chat.roomId, "events") let event = ModelStore.get(chat.userId, chat.roomId, "events")