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
|
- Banner name color instead of bold
|
||||||
|
|
||||||
- Missing nio support
|
- Missing nio support
|
||||||
|
- Invite events are missing their timestamps
|
||||||
- Left room events
|
- Left room events
|
||||||
- `org.matrix.room.preview_urls` event
|
- `org.matrix.room.preview_urls` event
|
||||||
- `m.room.aliases` event
|
- `m.room.aliases` event
|
||||||
|
|
|
@ -105,9 +105,10 @@ class Backend(QObject):
|
||||||
@pyqtSlot(list)
|
@pyqtSlot(list)
|
||||||
def pdb(self, additional_data: Sequence = ()) -> None:
|
def pdb(self, additional_data: Sequence = ()) -> None:
|
||||||
# pylint: disable=all
|
# pylint: disable=all
|
||||||
a = additional_data
|
ad = additional_data
|
||||||
c = self.clients
|
cl = self.clients
|
||||||
m = self.models
|
ac = self.accounts
|
||||||
|
re = self.roomEvents
|
||||||
|
|
||||||
tcl = lambda user: c[f"@test_{user}:matrix.org"]
|
tcl = lambda user: c[f"@test_{user}:matrix.org"]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Any, Dict, List, Optional
|
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_item import ListItem
|
||||||
from .list_model import ListModel
|
from .list_model import ListModel
|
||||||
|
@ -24,20 +24,22 @@ class Room(ListItem):
|
||||||
displayName: str = ""
|
displayName: str = ""
|
||||||
topic: Optional[str] = None
|
topic: Optional[str] = None
|
||||||
typingUsers: List[str] = []
|
typingUsers: List[str] = []
|
||||||
|
lastEventDateTime: Optional[QDateTime] = None
|
||||||
|
|
||||||
inviter: Optional[Dict[str, str]] = None
|
inviter: Optional[Dict[str, str]] = None
|
||||||
leftEvent: Optional[Dict[str, str]] = None
|
leftEvent: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
|
|
||||||
class RoomCategory(ListItem):
|
class RoomCategory(ListItem):
|
||||||
_required_init_values = {"name", "rooms"}
|
_required_init_values = {"name", "rooms", "sortedRooms"}
|
||||||
_constant = {"rooms"}
|
_constant = {"rooms", "sortedRooms"}
|
||||||
|
|
||||||
name: str = ""
|
name: str = ""
|
||||||
|
|
||||||
# Must be provided at init, else it will be the same object
|
# Must be provided at init, else it will be the same object
|
||||||
# for every RoomCategory
|
# for every RoomCategory
|
||||||
rooms: ListModel = ListModel()
|
rooms: ListModel = ListModel()
|
||||||
|
sortedRooms: QSortFilterProxyModel = QSortFilterProxyModel()
|
||||||
|
|
||||||
|
|
||||||
class Account(ListItem):
|
class Account(ListItem):
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ListItem(QObject, metaclass=_ListItemMeta):
|
||||||
|
|
||||||
# Set properties from provided positional arguments
|
# Set properties from provided positional arguments
|
||||||
for prop, value in zip(self._props, args):
|
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)
|
already_set.add(prop)
|
||||||
|
|
||||||
# Set properties from provided keyword arguments
|
# Set properties from provided keyword arguments
|
||||||
|
@ -129,7 +129,7 @@ class ListItem(QObject, metaclass=_ListItemMeta):
|
||||||
if prop not in self._props:
|
if prop not in self._props:
|
||||||
raise TypeError(f"{method} got an unexpected keyword "
|
raise TypeError(f"{method} got an unexpected keyword "
|
||||||
f"argument {prop!r}")
|
f"argument {prop!r}")
|
||||||
setattr(self, f"_{prop}", value)
|
setattr(self, f"_{prop}", self._set_parent(value))
|
||||||
already_set.add(prop)
|
already_set.add(prop)
|
||||||
|
|
||||||
# Check for required init arguments not provided
|
# 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
|
# Set default values for properties not provided in arguments
|
||||||
for prop in set(self._props) - already_set:
|
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:
|
def __repr__(self) -> str:
|
||||||
|
|
|
@ -89,10 +89,15 @@ class ListModel(QAbstractListModel):
|
||||||
return self._data[0].mainKey if self._data else None
|
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]:
|
def roleNames(self) -> Dict[int, bytes]:
|
||||||
return {Qt.UserRole + i: bytes(f, "utf-8")
|
return {Qt.UserRole + i: bytes(name, "utf-8")
|
||||||
for i, f in enumerate(self.roles, 1)} \
|
for i, name in enumerate(self.roles, 1)} \
|
||||||
if self._data else {}
|
if self._data else {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,7 +225,9 @@ class ListModel(QAbstractListModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
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()
|
self.changed.emit()
|
||||||
return i_index
|
return i_index
|
||||||
|
|
||||||
|
@ -265,7 +272,7 @@ class ListModel(QAbstractListModel):
|
||||||
|
|
||||||
setattr(self[i_index], prop, value)
|
setattr(self[i_index], prop, value)
|
||||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
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()
|
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 .backend import Backend
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .model.items import Account, ListModel, Room, RoomCategory, RoomEvent
|
from .model.items import Account, ListModel, Room, RoomCategory, RoomEvent
|
||||||
|
from .model.sort_filter_proxy import SortFilterProxy
|
||||||
|
|
||||||
Inviter = Optional[Dict[str, str]]
|
Inviter = Optional[Dict[str, str]]
|
||||||
LeftEvent = Optional[Dict[str, str]]
|
LeftEvent = Optional[Dict[str, str]]
|
||||||
|
@ -34,12 +35,24 @@ class SignalManager(QObject):
|
||||||
def onClientAdded(self, client: Client) -> None:
|
def onClientAdded(self, client: Client) -> None:
|
||||||
self.connectClient(client)
|
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(
|
self.backend.accounts.append(Account(
|
||||||
userId = client.userId,
|
userId = client.userId,
|
||||||
roomCategories = ListModel([
|
roomCategories = ListModel([
|
||||||
RoomCategory("Invites", ListModel()),
|
RoomCategory(**kws) for kws in room_categories_kwargs
|
||||||
RoomCategory("Rooms", ListModel()),
|
|
||||||
RoomCategory("Left", ListModel()),
|
|
||||||
]),
|
]),
|
||||||
displayName = self.backend.getUserDisplayName(client.userId),
|
displayName = self.backend.getUserDisplayName(client.userId),
|
||||||
))
|
))
|
||||||
|
@ -82,12 +95,18 @@ class SignalManager(QObject):
|
||||||
categories["Rooms"].rooms.pop(room_id, None)
|
categories["Rooms"].rooms.pop(room_id, None)
|
||||||
categories["Left"].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,
|
roomId = room_id,
|
||||||
displayName = self._get_room_displayname(nio_room),
|
displayName = self._get_room_displayname(nio_room),
|
||||||
topic = nio_room.topic,
|
topic = nio_room.topic,
|
||||||
inviter = inviter,
|
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:
|
def onRoomJoined(self, client: Client, room_id: str) -> None:
|
||||||
|
@ -97,11 +116,16 @@ class SignalManager(QObject):
|
||||||
categories["Invites"].rooms.pop(room_id, None)
|
categories["Invites"].rooms.pop(room_id, None)
|
||||||
categories["Left"].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,
|
roomId = room_id,
|
||||||
displayName = self._get_room_displayname(nio_room),
|
displayName = self._get_room_displayname(nio_room),
|
||||||
topic = nio_room.topic,
|
topic = nio_room.topic,
|
||||||
), 0, 0)
|
),
|
||||||
|
new_index_if_insert = 0,
|
||||||
|
ignore_roles = ("typingUsers", "lastEventDateTime"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def onRoomLeft(self,
|
def onRoomLeft(self,
|
||||||
|
@ -114,12 +138,23 @@ class SignalManager(QObject):
|
||||||
previous = previous or categories["Invites"].rooms.pop(room_id, None)
|
previous = previous or categories["Invites"].rooms.pop(room_id, None)
|
||||||
previous = previous or categories["Left"].rooms.get(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,
|
roomId = room_id,
|
||||||
displayName = previous.displayName if previous else None,
|
displayName = previous.displayName if previous else None,
|
||||||
topic = previous.topic if previous else None,
|
topic = previous.topic if previous else None,
|
||||||
leftEvent = left_event,
|
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,
|
def onRoomSyncPrevBatchTokenReceived(self,
|
||||||
_: Client,
|
_: Client,
|
||||||
|
@ -141,27 +176,14 @@ class SignalManager(QObject):
|
||||||
self.backend.past_tokens[room_id] = token
|
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:
|
) -> None:
|
||||||
# Find in which category our room is
|
for categ in self.backend.accounts[user_id].roomCategories:
|
||||||
for categ in self.backend.accounts[account_id].roomCategories:
|
if room_id in categ.rooms:
|
||||||
if room_id not in categ.rooms:
|
# Use setProperty to make sure to trigger model changed signals
|
||||||
continue
|
categ.rooms.setProperty(
|
||||||
|
room_id, "lastEventDateTime", event.dateTime
|
||||||
# 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 onRoomEventReceived(self,
|
def onRoomEventReceived(self,
|
||||||
|
@ -235,7 +257,7 @@ class SignalManager(QObject):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
new_event = process()
|
new_event = process()
|
||||||
if new_event:
|
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,
|
def onRoomTypingUsersUpdated(self,
|
||||||
|
@ -245,7 +267,7 @@ class SignalManager(QObject):
|
||||||
categories = self.backend.accounts[client.userId].roomCategories
|
categories = self.backend.accounts[client.userId].roomCategories
|
||||||
for categ in categories:
|
for categ in categories:
|
||||||
try:
|
try:
|
||||||
categ.rooms[room_id].typingUsers = users
|
categ.rooms.setProperty(room_id, "typingUsers", users)
|
||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
@ -272,7 +294,7 @@ class SignalManager(QObject):
|
||||||
model.insert(0, event)
|
model.insert(0, event)
|
||||||
self._events_in_transfer += 1
|
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:
|
def onRoomAboutToBeForgotten(self, client: Client, room_id: str) -> None:
|
||||||
|
|
|
@ -42,10 +42,8 @@ MouseArea {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
property var lastEvTime: lastEventDateTime
|
||||||
target: Backend.roomEvents.get(roomId)
|
onLastEvTimeChanged: subtitleLabel.text = getText()
|
||||||
onChanged: subtitleLabel.text = subtitleLabel.getText()
|
|
||||||
}
|
|
||||||
|
|
||||||
id: subtitleLabel
|
id: subtitleLabel
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
|
|
|
@ -8,6 +8,7 @@ ListView {
|
||||||
|
|
||||||
id: roomList
|
id: roomList
|
||||||
spacing: accountList.spacing
|
spacing: accountList.spacing
|
||||||
model: Backend.accounts.get(userId).roomCategories.get(category).rooms
|
model:
|
||||||
|
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
|
||||||
delegate: RoomDelegate {}
|
delegate: RoomDelegate {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,5 +62,7 @@ Item {
|
||||||
popExit: null
|
popExit: null
|
||||||
pushExit: 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.
|
# This file is part of harmonyqml, licensed under GPLv3.
|
||||||
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Generator
|
from typing import Any, Dict, Generator
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user