Fix room sorting for good

Do it the right way with a QSortFilterProxyModel.
This commit is contained in:
miruka 2019-05-06 13:07:00 -04:00
parent 0e5c5619cf
commit cdf6190cba
11 changed files with 178 additions and 70 deletions

View File

@ -56,6 +56,7 @@
- Banner name color instead of bold
- Missing nio support
- Invite events are missing their timestamps
- Left room events
- `org.matrix.room.preview_urls` event
- `m.room.aliases` event

View File

@ -105,9 +105,10 @@ class Backend(QObject):
@pyqtSlot(list)
def pdb(self, additional_data: Sequence = ()) -> None:
# pylint: disable=all
a = additional_data
c = self.clients
m = self.models
ad = additional_data
cl = self.clients
ac = self.accounts
re = self.roomEvents
tcl = lambda user: c[f"@test_{user}:matrix.org"]

View File

@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional
from PyQt5.QtCore import QDateTime
from PyQt5.QtCore import QDateTime, QSortFilterProxyModel
from .list_item import ListItem
from .list_model import ListModel
@ -20,24 +20,26 @@ class Room(ListItem):
_required_init_values = {"roomId", "displayName"}
_constant = {"roomId"}
roomId: str = ""
displayName: str = ""
topic: Optional[str] = None
typingUsers: List[str] = []
roomId: str = ""
displayName: str = ""
topic: Optional[str] = None
typingUsers: List[str] = []
lastEventDateTime: Optional[QDateTime] = None
inviter: Optional[Dict[str, str]] = None
leftEvent: Optional[Dict[str, str]] = None
class RoomCategory(ListItem):
_required_init_values = {"name", "rooms"}
_constant = {"rooms"}
_required_init_values = {"name", "rooms", "sortedRooms"}
_constant = {"rooms", "sortedRooms"}
name: str = ""
# Must be provided at init, else it will be the same object
# for every RoomCategory
rooms: ListModel = ListModel()
rooms: ListModel = ListModel()
sortedRooms: QSortFilterProxyModel = QSortFilterProxyModel()
class Account(ListItem):

View File

@ -118,7 +118,7 @@ class ListItem(QObject, metaclass=_ListItemMeta):
# Set properties from provided positional arguments
for prop, value in zip(self._props, args):
setattr(self, f"_{prop}", value)
setattr(self, f"_{prop}", self._set_parent(value))
already_set.add(prop)
# Set properties from provided keyword arguments
@ -129,7 +129,7 @@ class ListItem(QObject, metaclass=_ListItemMeta):
if prop not in self._props:
raise TypeError(f"{method} got an unexpected keyword "
f"argument {prop!r}")
setattr(self, f"_{prop}", value)
setattr(self, f"_{prop}", self._set_parent(value))
already_set.add(prop)
# Check for required init arguments not provided
@ -140,7 +140,13 @@ class ListItem(QObject, metaclass=_ListItemMeta):
# Set default values for properties not provided in arguments
for prop in set(self._props) - already_set:
setattr(self, f"_{prop}", self._props[prop][1])
setattr(self, f"_{prop}", self._set_parent(self._props[prop][1]))
def _set_parent(self, value: Any) -> Any:
if isinstance(value, QObject):
value.setParent(self)
return value
def __repr__(self) -> str:

View File

@ -89,10 +89,15 @@ class ListModel(QAbstractListModel):
return self._data[0].mainKey if self._data else None
def roleNumbers(self) -> Dict[str, int]:
return {name: Qt.UserRole + i
for i, name in enumerate(self.roles, 1)} \
if self._data else {}
def roleNames(self) -> Dict[int, bytes]:
return {Qt.UserRole + i: bytes(f, "utf-8")
for i, f in enumerate(self.roles, 1)} \
return {Qt.UserRole + i: bytes(name, "utf-8")
for i, name in enumerate(self.roles, 1)} \
if self._data else {}
@ -219,8 +224,10 @@ class ListModel(QAbstractListModel):
except AttributeError: # constant/not settable
pass
qidx = QAbstractListModel.index(self, i_index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
qidx = QAbstractListModel.index(self, i_index, 0)
updated = [number for name, number in self.roleNumbers().items()
if name not in ignore_roles]
self.dataChanged.emit(qidx, qidx, updated)
self.changed.emit()
return i_index
@ -265,7 +272,7 @@ class ListModel(QAbstractListModel):
setattr(self[i_index], prop, value)
qidx = QAbstractListModel.index(self, i_index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.dataChanged.emit(qidx, qidx, (self.roleNumbers()[prop],))
self.changed.emit()

View File

@ -0,0 +1,69 @@
from typing import Dict
from PyQt5.QtCore import (
QObject, QSortFilterProxyModel, Qt, pyqtProperty, pyqtSignal, pyqtSlot
)
from .list_model import ListModel
class SortFilterProxy(QSortFilterProxyModel):
sortByRoleChanged = pyqtSignal()
def __init__(self,
source_model: ListModel,
sort_by_role: str,
ascending: bool = True,
parent: QObject = None) -> None:
super().__init__(parent)
self.setDynamicSortFilter(False)
self.ascending = ascending
self.sortByRoleChanged.connect(self._set_sort_role)
self.setSourceModel(source_model)
source_model.rolesSet.connect(self._set_sort_role)
source_model.changed.connect(self._sort)
self._sort_by_role = sort_by_role
self._set_sort_role()
@pyqtProperty(str, notify=sortByRoleChanged)
def sortByRole(self) -> str:
return self._sort_by_role
@sortByRole.setter # type: ignore
def sortByRole(self, role: str) -> None:
self._sort_by_role = role
self.sortByRoleChanged.emit()
def _set_sort_role(self) -> None:
numbers = self.sourceModel().roleNumbers()
try:
self.setSortRole(numbers[self._sort_by_role])
except KeyError:
pass # Model doesn't have its roles set yet (empty model)
def __repr__(self) -> str:
return "%s(sortByRole=%r, sourceModel=%r)" % (
type(self).__name__, self.sortByRole, self.sourceModel(),
)
@pyqtSlot(result=str)
def repr(self) -> str:
return self.__repr__()
def roleNames(self) -> Dict[int, bytes]:
return self.sourceModel().roleNames()
def _sort(self) -> None:
order = Qt.AscendingOrder if self.ascending else Qt.DescendingOrder
self.sort(0, order)

View File

@ -12,6 +12,7 @@ from nio.rooms import MatrixRoom
from .backend import Backend
from .client import Client
from .model.items import Account, ListModel, Room, RoomCategory, RoomEvent
from .model.sort_filter_proxy import SortFilterProxy
Inviter = Optional[Dict[str, str]]
LeftEvent = Optional[Dict[str, str]]
@ -34,12 +35,24 @@ class SignalManager(QObject):
def onClientAdded(self, client: Client) -> None:
self.connectClient(client)
room_categories_kwargs: List[Dict[str, Any]] = [
{"name": "Invites", "rooms": ListModel()},
{"name": "Rooms", "rooms": ListModel()},
{"name": "Left", "rooms": ListModel()},
]
for i, _ in enumerate(room_categories_kwargs):
proxy = SortFilterProxy(
source_model = room_categories_kwargs[i]["rooms"],
sort_by_role = "lastEventDateTime",
ascending = False,
)
room_categories_kwargs[i]["sortedRooms"] = proxy
self.backend.accounts.append(Account(
userId = client.userId,
roomCategories = ListModel([
RoomCategory("Invites", ListModel()),
RoomCategory("Rooms", ListModel()),
RoomCategory("Left", ListModel()),
RoomCategory(**kws) for kws in room_categories_kwargs
]),
displayName = self.backend.getUserDisplayName(client.userId),
))
@ -82,12 +95,18 @@ class SignalManager(QObject):
categories["Rooms"].rooms.pop(room_id, None)
categories["Left"].rooms.pop(room_id, None)
categories["Invites"].rooms.upsert(room_id, Room(
roomId = room_id,
displayName = self._get_room_displayname(nio_room),
topic = nio_room.topic,
inviter = inviter,
), 0, 0)
categories["Invites"].rooms.upsert(
where_main_key_is = room_id,
update_with = Room(
roomId = room_id,
displayName = self._get_room_displayname(nio_room),
topic = nio_room.topic,
inviter = inviter,
lastEventDateTime = QDateTime.currentDateTime(), # FIXME
),
new_index_if_insert = 0,
ignore_roles = ("typingUsers"),
)
def onRoomJoined(self, client: Client, room_id: str) -> None:
@ -97,11 +116,16 @@ class SignalManager(QObject):
categories["Invites"].rooms.pop(room_id, None)
categories["Left"].rooms.pop(room_id, None)
categories["Rooms"].rooms.upsert(room_id, Room(
roomId = room_id,
displayName = self._get_room_displayname(nio_room),
topic = nio_room.topic,
), 0, 0)
categories["Rooms"].rooms.upsert(
where_main_key_is = room_id,
update_with = Room(
roomId = room_id,
displayName = self._get_room_displayname(nio_room),
topic = nio_room.topic,
),
new_index_if_insert = 0,
ignore_roles = ("typingUsers", "lastEventDateTime"),
)
def onRoomLeft(self,
@ -114,12 +138,23 @@ class SignalManager(QObject):
previous = previous or categories["Invites"].rooms.pop(room_id, None)
previous = previous or categories["Left"].rooms.get(room_id, None)
categories["Left"].rooms.upsert(0, Room(
roomId = room_id,
displayName = previous.displayName if previous else None,
topic = previous.topic if previous else None,
leftEvent = left_event,
), 0, 0)
left_time = left_event.get("server_timestamp") if left_event else None
categories["Left"].rooms.upsert(
where_main_key_is = room_id,
update_with = Room(
roomId = room_id,
displayName = previous.displayName if previous else None,
topic = previous.topic if previous else None,
leftEvent = left_event,
lastEventDateTime = (
QDateTime.fromMSecsSinceEpoch(left_time)
if left_time else QDateTime.currentDateTime()
),
),
new_index_if_insert = 0,
ignore_roles = ("typingUsers", "lastEventDateTime"),
)
def onRoomSyncPrevBatchTokenReceived(self,
_: Client,
@ -141,27 +176,14 @@ class SignalManager(QObject):
self.backend.past_tokens[room_id] = token
def _move_room(self, account_id: str, room_id: str, new_event: RoomEvent
) -> None:
# Find in which category our room is
for categ in self.backend.accounts[account_id].roomCategories:
if room_id not in categ.rooms:
continue
# Found the category, now find before which room we must move to
for index, room in enumerate(categ.rooms):
if not self.backend.roomEvents[room.roomId]:
# That other room has no events, move before it
categ.rooms.move(room_id, index)
return
other_room_last_event = self.backend.roomEvents[room.roomId][0]
if new_event.dateTime > other_room_last_event.dateTime:
# Our last event is newer than that other room, move before
categ.rooms.move(room_id, max(0, index - 1))
return
return
def _set_room_last_event(self, user_id: str, room_id: str, event: RoomEvent
) -> None:
for categ in self.backend.accounts[user_id].roomCategories:
if room_id in categ.rooms:
# Use setProperty to make sure to trigger model changed signals
categ.rooms.setProperty(
room_id, "lastEventDateTime", event.dateTime
)
def onRoomEventReceived(self,
@ -197,7 +219,7 @@ class SignalManager(QObject):
if self._events_in_transfer:
local_echoes_met: int = 0
update_at: Optional[int] = None
update_at: Optional[int] = None
# Find if any locally echoed event corresponds to new_event
for i, event in enumerate(model):
@ -235,7 +257,7 @@ class SignalManager(QObject):
with self._lock:
new_event = process()
if new_event:
self._move_room(client.userId, room_id, new_event)
self._set_room_last_event(client.userId, room_id, new_event)
def onRoomTypingUsersUpdated(self,
@ -245,7 +267,7 @@ class SignalManager(QObject):
categories = self.backend.accounts[client.userId].roomCategories
for categ in categories:
try:
categ.rooms[room_id].typingUsers = users
categ.rooms.setProperty(room_id, "typingUsers", users)
break
except ValueError:
pass
@ -272,7 +294,7 @@ class SignalManager(QObject):
model.insert(0, event)
self._events_in_transfer += 1
self._move_room(client.userId, room_id, event)
self._set_room_last_event(client.userId, room_id, event)
def onRoomAboutToBeForgotten(self, client: Client, room_id: str) -> None:

View File

@ -42,10 +42,8 @@ MouseArea {
)
}
Connections {
target: Backend.roomEvents.get(roomId)
onChanged: subtitleLabel.text = subtitleLabel.getText()
}
property var lastEvTime: lastEventDateTime
onLastEvTimeChanged: subtitleLabel.text = getText()
id: subtitleLabel
visible: text !== ""

View File

@ -8,6 +8,7 @@ ListView {
id: roomList
spacing: accountList.spacing
model: Backend.accounts.get(userId).roomCategories.get(category).rooms
model:
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
delegate: RoomDelegate {}
}

View File

@ -62,5 +62,7 @@ Item {
popExit: null
pushExit: null
}
Keys.onEscapePressed: Backend.pdb() // TODO: only if debug mode True
}
}

View File

@ -2,7 +2,6 @@
# This file is part of harmonyqml, licensed under GPLv3.
import signal
import sys
from pathlib import Path
from typing import Any, Dict, Generator