diff --git a/src/backend/backend.py b/src/backend/backend.py index 1e879692..565c2828 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -300,14 +300,15 @@ class Backend: """ async def update(client: MatrixClient) -> None: - room = self.models[client.user_id, "rooms"].get(room_id) - local = room.local_unreads or room.local_highlights + room = self.models[client.user_id, "rooms"].get(room_id) - if room and (room.unreads or room.highlights or local): - room.unreads = 0 - room.highlights = 0 - room.local_unreads = False - room.local_highlights = False + if room: + room.set_fields( + unreads = 0, + highlights = 0, + local_unreads = False, + local_highlights = False, + ) await client.update_account_unread_counts() await client.update_receipt_marker(room_id, event_id) diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index a1e2f307..cd32f3c0 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -353,10 +353,12 @@ class MatrixClient(nio.AsyncClient): resp = await self.backend.get_profile(self.user_id, use_cache=False) - account = self.models["accounts"][self.user_id] - account.profile_updated = datetime.now() - account.display_name = resp.displayname or "" - account.avatar_url = resp.avatar_url or "" + account = self.models["accounts"][self.user_id] + account.set_fields( + profile_updated = datetime.now(), + display_name = resp.displayname or "", + avatar_url = resp.avatar_url or "", + ) async def get_server_config(self) -> int: @@ -528,8 +530,10 @@ class MatrixClient(nio.AsyncClient): upload_item.uploaded = transferred def on_speed_changed(speed: float) -> None: - upload_item.speed = speed - upload_item.time_left = monitor.remaining_time or timedelta(0) + upload_item.set_fields( + speed = speed, + time_left = monitor.remaining_time or timedelta(0), + ) monitor.on_transferred = on_transferred monitor.on_speed_changed = on_speed_changed @@ -548,9 +552,11 @@ class MatrixClient(nio.AsyncClient): raise nio.TransferCancelledError() except (MatrixError, OSError) as err: - upload_item.status = UploadStatus.Error - upload_item.error = type(err) - upload_item.error_args = err.args + upload_item.set_fields( + status = UploadStatus.Error, + error = type(err), + error_args = err.args, + ) # Wait for cancellation from UI, see parent send_file() method while True: @@ -605,9 +611,11 @@ class MatrixClient(nio.AsyncClient): thumb_ext = "png" if thumb_info.mime == "image/png" else "jpg" thumb_name = f"{path.stem}_thumbnail.{thumb_ext}" - upload_item.status = UploadStatus.Uploading - upload_item.filepath = Path(thumb_name) - upload_item.total_size = len(thumb_data) + upload_item.set_fields( + status = UploadStatus.Uploading, + filepath = Path(thumb_name), + total_size = len(thumb_data), + ) try: upload_item.total_size = thumb_info.size @@ -1384,11 +1392,13 @@ class MatrixClient(nio.AsyncClient): if room.local_highlights: local_highlights = True - account = self.models["accounts"][self.user_id] - account.total_unread = unreads - account.total_highlights = highlights - account.local_unreads = local_unreads - account.local_highlights = local_highlights + account = self.models["accounts"][self.user_id] + account.set_fields( + total_unread = unreads, + total_highlights = highlights, + local_unreads = local_unreads, + local_highlights = local_highlights, + ) async def event_is_past(self, ev: Union[nio.Event, Event]) -> bool: @@ -1552,13 +1562,11 @@ class MatrixClient(nio.AsyncClient): 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 + ev.set_fields(sender_name=sender_name, 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 + ev.set_fields(target_name=target_name, target_avatar=target_avatar) if ev.redacter_id and not ev.redacter_name: redacter_name, _, _ = await get_profile(ev.target_id) diff --git a/src/backend/models/model_item.py b/src/backend/models/model_item.py index 234087a6..fb57c5ed 100644 --- a/src/backend/models/model_item.py +++ b/src/backend/models/model_item.py @@ -33,32 +33,34 @@ class ModelItem: def __setattr__(self, name: str, value) -> None: """If this item is in a `Model`, alert it of attribute changes.""" - if name == "parent_model" or not self.parent_model: + if ( + name == "parent_model" or + not self.parent_model or + getattr(self, name) == value + ): super().__setattr__(name, value) return - if getattr(self, name) == value: - return - super().__setattr__(name, value) + self._notify_parent_model({name: self.serialize_field(name)}) + + def _notify_parent_model(self, changed_fields: Dict[str, Any]) -> None: parent = self.parent_model - if not parent.sync_id: + if not parent or not parent.sync_id or not changed_fields: return - fields = {name: self.serialize_field(name)} - with parent.write_lock: index_then = parent._sorted_data.index(self) parent._sorted_data.sort() index_now = parent._sorted_data.index(self) - ModelItemSet(parent.sync_id, index_then, index_now, fields) + ModelItemSet(parent.sync_id, index_then, index_now, changed_fields) for sync_id, proxy in parent.proxies.items(): if sync_id != parent.sync_id: - proxy.source_item_set(parent, self.id, self, fields) + proxy.source_item_set(parent, self.id, self, changed_fields) def __delattr__(self, name: str) -> None: @@ -82,3 +84,20 @@ class ModelItem: name.startswith("_") or name in ("parent_model", "serialized") ) } + + + def set_fields(self, **fields: Any) -> None: + """Set multiple fields's values at once. + + The parent model will be resorted only once, and one `ModelItemSet` + event will be sent informing QML of all the changed fields. + """ + + for name, value in fields.copy().items(): + if getattr(self, name) == value: + del fields[name] + else: + super().__setattr__(name, value) + fields[name] = self.serialize_field(name) + + self._notify_parent_model(fields) diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py index ff3c66b4..5e5eec58 100644 --- a/src/backend/nio_callbacks.py +++ b/src/backend/nio_callbacks.py @@ -388,9 +388,11 @@ class NioCallbacks: account = self.models["accounts"][self.user_id] if account.profile_updated < ev_date: - account.profile_updated = ev_date - account.display_name = now.get("displayname") or "" - account.avatar_url = now.get("avatar_url") or "" + account.set_fields( + profile_updated = ev_date, + display_name = now.get("displayname") or "", + avatar_url = now.get("avatar_url") or "", + ) if self.client.backend.ui_settings["hideProfileChangeEvents"]: return None @@ -568,6 +570,8 @@ class NioCallbacks: member = model.get(receipt.user_id) if member: - member.last_read_event = receipt.event_id - timestamp = receipt.timestamp / 1000 - member.last_read_at = datetime.fromtimestamp(timestamp) + timestamp = receipt.timestamp / 1000 + member.set_fields( + last_read_event = receipt.event_id, + last_read_at = datetime.fromtimestamp(timestamp), + )