Fix room sorting for good
Do it the right way with a QSortFilterProxyModel.
This commit is contained in:
parent
0e5c5619cf
commit
cdf6190cba
1
TODO.md
1
TODO.md
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -24,20 +24,22 @@ class Room(ListItem):
|
|||
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()
|
||||
sortedRooms: QSortFilterProxyModel = QSortFilterProxyModel()
|
||||
|
||||
|
||||
class Account(ListItem):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {}
|
||||
|
||||
|
||||
|
@ -220,7 +225,9 @@ class ListModel(QAbstractListModel):
|
|||
pass
|
||||
|
||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
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()
|
||||
|
||||
|
||||
|
|
69
harmonyqml/backend/model/sort_filter_proxy.py
Normal file
69
harmonyqml/backend/model/sort_filter_proxy.py
Normal 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)
|
|
@ -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(
|
||||
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,
|
||||
), 0, 0)
|
||||
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(
|
||||
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,
|
||||
), 0, 0)
|
||||
),
|
||||
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(
|
||||
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,
|
||||
), 0, 0)
|
||||
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
|
||||
def _set_room_last_event(self, user_id: str, room_id: str, 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
|
||||
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,
|
||||
|
@ -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:
|
||||
|
|
|
@ -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 !== ""
|
||||
|
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
@ -62,5 +62,7 @@ Item {
|
|||
popExit: null
|
||||
pushExit: null
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: Backend.pdb() // TODO: only if debug mode True
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user