Rework models hierarchy, room categories models

This commit is contained in:
miruka
2019-05-02 14:20:21 -04:00
parent ada44cf6f7
commit 047225fded
23 changed files with 325 additions and 293 deletions

View File

@@ -110,22 +110,3 @@ class Backend(QObject):
from PyQt5.QtCore import pyqtRemoveInputHook
pyqtRemoveInputHook()
pdb.set_trace()
@pyqtSlot("QVariant", str, result=bool)
def EventIsOurProfileChanged(self, event: RoomEvent, account_id) -> bool:
# pylint: disable=unused-self
info = event.dict.get("content")
previous = event.dict.get("prev_content")
return (
event.type == "RoomMemberEvent" and
event.dict["sender"] == account_id and
bool(info) and
bool(previous) and
info["membership"] == previous["membership"] and
(
info.get("displayname") != previous.get("displayname") or
info.get("avatar_url") != previous.get("avatar_url")
)
)

View File

@@ -89,7 +89,6 @@ class ClientManager(QObject):
def deleteAll(self) -> None:
for user_id in self.clients.copy():
self.delete(user_id)
print("deleted", user_id, self.clients)
@pyqtProperty(str, constant=True)

View File

@@ -3,29 +3,7 @@ from typing import Any, Dict, List, Optional
from PyQt5.QtCore import QDateTime
from .list_item import ListItem
class User(ListItem):
_required_init_values = {"userId"}
_constant = {"userId"}
userId: str = ""
displayName: Optional[str] = None
avatarUrl: Optional[str] = None
statusMessage: Optional[str] = None
class Room(ListItem):
_required_init_values = {"roomId", "displayName"}
_constant = {"roomId"}
roomId: str = ""
displayName: str = ""
category: str = "Rooms"
topic: Optional[str] = None
typingUsers: List[str] = []
inviter: Optional[Dict[str, str]] = None
leftEvent: Optional[Dict[str, str]] = None
from .list_model import ListModel
class RoomEvent(ListItem):
@@ -36,3 +14,38 @@ class RoomEvent(ListItem):
dict: Dict[str, Any] = {}
dateTime: QDateTime = QDateTime.currentDateTime()
isLocalEcho: bool = False
class Room(ListItem):
_required_init_values = {"roomId", "displayName"}
_constant = {"roomId"}
roomId: str = ""
displayName: str = ""
topic: Optional[str] = None
typingUsers: List[str] = []
inviter: Optional[Dict[str, str]] = None
leftEvent: Optional[Dict[str, str]] = None
class RoomCategory(ListItem):
_required_init_values = {"name", "rooms"}
_constant = {"rooms"}
name: str = ""
# Must be provided at init, else it will be the same object
# for every RoomCategory
rooms: ListModel = ListModel()
class Account(ListItem):
_required_init_values = {"userId", "roomCategories"}
_constant = {"userId", "roomCategories"}
userId: str = ""
roomCategories: ListModel = ListModel() # same as RoomCategory.rooms
displayName: Optional[str] = None
avatarUrl: Optional[str] = None
statusMessage: Optional[str] = None

View File

@@ -15,15 +15,23 @@ Index = Union[int, str]
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
class _GetFail:
pass
class _PopFail:
pass
class ListModel(QAbstractListModel):
rolesSet = pyqtSignal()
changed = pyqtSignal()
countChanged = pyqtSignal(int)
def __init__(self,
parent: QObject,
initial_data: Optional[List[NewItem]] = None,
container: Callable[..., MutableSequence] = list) -> None:
container: Callable[..., MutableSequence] = list,
parent: QObject = None) -> None:
super().__init__(parent)
self._data: MutableSequence[ListItem] = container()
@@ -135,11 +143,19 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, result="QVariant")
@pyqtSlot(str, result="QVariant")
def get(self, index: Index) -> ListItem:
if isinstance(index, str):
index = self.indexWhere(self.mainKey, index)
@pyqtSlot(int, "QVariant", result="QVariant")
@pyqtSlot(str, "QVariant", result="QVariant")
def get(self, index: Index, default: Any = _GetFail()) -> ListItem:
try:
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
return self._data[index] # type: ignore
return self._data[i_index]
except (ValueError, IndexError):
if isinstance(default, _GetFail):
raise
return default
@pyqtSlot(int, "QVariantMap")
@@ -170,20 +186,20 @@ class ListModel(QAbstractListModel):
self.append(val)
@pyqtSlot(int, "QVariantMap")
@pyqtSlot(int, "QVariantMap", "QStringList")
@pyqtSlot(str, "QVariantMap")
@pyqtSlot(str, "QVariantMap", "QStringList")
@pyqtSlot(int, "QVariantMap", result=int)
@pyqtSlot(int, "QVariantMap", "QStringList", result=int)
@pyqtSlot(str, "QVariantMap", result=int)
@pyqtSlot(str, "QVariantMap", "QStringList", result=int)
def update(self,
index: Index,
value: NewItem,
ignore_roles: Sequence[str] = ()) -> None:
ignore_roles: Sequence[str] = ()) -> int:
value = self._convert_new_value(value)
if isinstance(index, str):
index = self.indexWhere(self.mainKey or value.mainKey, index)
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
to_update = self._data[index] # type: ignore
to_update = self[i_index]
for role in self.roles:
if role not in ignore_roles:
@@ -192,35 +208,40 @@ class ListModel(QAbstractListModel):
except AttributeError: # constant/not settable
pass
qidx = QAbstractListModel.index(self, index, 0)
qidx = QAbstractListModel.index(self, i_index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
return i_index
@pyqtSlot(str, "QVariantMap")
@pyqtSlot(str, "QVariantMap", int)
@pyqtSlot(str, "QVariantMap", int, "QStringList")
@pyqtSlot(str, "QVariantMap", int, int)
@pyqtSlot(str, "QVariantMap", int, int, "QStringList")
def upsert(self,
where_main_key_is_value: Any,
update_with: NewItem,
index_if_insert: Optional[int] = None,
ignore_roles: Sequence[str] = ()) -> None:
where_main_key_is: Any,
update_with: NewItem,
new_index_if_insert: Optional[int] = None,
new_index_if_update: Optional[int] = None,
ignore_roles: Sequence[str] = ()) -> None:
try:
self.update(where_main_key_is_value, update_with, ignore_roles)
index = self.update(where_main_key_is, update_with, ignore_roles)
except (IndexError, ValueError):
self.insert(index_if_insert or len(self), update_with)
self.insert(new_index_if_insert or len(self), update_with)
else:
if new_index_if_update:
self.move(index, new_index_if_update)
@pyqtSlot(int, list)
@pyqtSlot(str, list)
def set(self, index: Index, value: NewItem) -> None:
if isinstance(index, str):
index = self.indexWhere(self.mainKey, index)
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
qidx = QAbstractListModel.index(self, index, 0)
value = self._convert_new_value(value)
self._data[index] = value # type: ignore
qidx = QAbstractListModel.index(self, i_index, 0)
value = self._convert_new_value(value)
self._data[i_index] = value
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
@@ -228,11 +249,11 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, str, "QVariant")
@pyqtSlot(str, str, "QVariant")
def setProperty(self, index: Index, prop: str, value: Any) -> None:
if isinstance(index, str):
index = self.indexWhere(self.mainKey, index)
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
setattr(self._data[index], prop, value) # type: ignore
qidx = QAbstractListModel.index(self, index, 0)
setattr(self[i_index], prop, value)
qidx = QAbstractListModel.index(self, i_index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
@@ -269,17 +290,39 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int)
@pyqtSlot(str)
def remove(self, index: Index) -> None:
if isinstance(index, str):
index = self.indexWhere(self.mainKey, index)
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
self.beginRemoveRows(QModelIndex(), index, index)
del self._data[index] # type: ignore
self.beginRemoveRows(QModelIndex(), i_index, i_index)
del self._data[i_index]
self.endRemoveRows()
self.countChanged.emit(len(self))
self.changed.emit()
@pyqtSlot(int, result="QVariant")
@pyqtSlot(str, result="QVariant")
def pop(self, index: Index, default: Any = _PopFail()) -> ListItem:
try:
i_index: int = self.indexWhere(self.mainKey, index) \
if isinstance(index, str) else index
item = self[i_index]
except (ValueError, IndexError):
if isinstance(default, _PopFail):
raise
return default
self.beginRemoveRows(QModelIndex(), i_index, i_index)
del self._data[i_index]
self.endRemoveRows()
self.countChanged.emit(len(self))
self.changed.emit()
return item
@pyqtSlot()
def clear(self) -> None:
# Reimplemented for performance reasons (begin/endRemoveRows)

View File

@@ -7,15 +7,15 @@ from .list_model import ListModel
class ListModelMap(QObject):
def __init__(self,
parent: QObject,
models_container: Callable[..., MutableSequence] = list
) -> None:
models_container: Callable[..., MutableSequence] = list,
parent: QObject = None) -> None:
super().__init__(parent)
# Set the parent to prevent item garbage-collection on the C++ side
self.dict: DefaultDict[Any, ListModel] = \
DefaultDict(lambda: ListModel(parent = self,
container = models_container))
DefaultDict(
lambda: ListModel(container=models_container, parent=self)
)
@pyqtSlot(str, result="QVariant")

View File

@@ -12,10 +12,8 @@ from .list_model_map import ListModelMap
class QMLModels(QObject):
def __init__(self, parent: QObject) -> None:
super().__init__(parent)
self._accounts: ListModel = ListModel(parent)
self._rooms: ListModelMap = ListModelMap(parent)
self._room_events: ListModelMap = ListModelMap(parent,
models_container=Deque)
self._accounts: ListModel = ListModel(parent=parent)
self._room_events: ListModelMap = ListModelMap(Deque, parent)
@pyqtProperty(ListModel, constant=True)
@@ -23,11 +21,6 @@ class QMLModels(QObject):
return self._accounts
@pyqtProperty("QVariant", constant=True)
def rooms(self):
return self._rooms
@pyqtProperty("QVariant", constant=True)
def roomEvents(self):
return self._room_events

View File

@@ -2,7 +2,7 @@
# This file is part of harmonyqml, licensed under GPLv3.
from threading import Lock
from typing import Any, Deque, Dict, List, Optional, Sequence
from typing import Any, Deque, Dict, List, Optional
from PyQt5.QtCore import QDateTime, QObject, pyqtBoundSignal
@@ -11,7 +11,8 @@ from nio.rooms import MatrixRoom
from .backend import Backend
from .client import Client
from .model.items import Room, RoomEvent, User
from .model.items import Account, Room, RoomCategory, RoomEvent
from .model.list_model import ListModel
Inviter = Optional[Dict[str, str]]
LeftEvent = Optional[Dict[str, str]]
@@ -34,8 +35,14 @@ class SignalManager(QObject):
def onClientAdded(self, client: Client) -> None:
self.connectClient(client)
self.backend.models.accounts.append(User(
userId = client.userId,
self.backend.models.accounts.append(Account(
userId = client.userId,
roomCategories = ListModel([
RoomCategory("Invites", ListModel()),
RoomCategory("Rooms", ListModel()),
RoomCategory("Left", ListModel()),
]),
displayName = self.backend.getUserDisplayName(client.userId),
))
@@ -56,104 +63,65 @@ class SignalManager(QObject):
attr.connect(onSignal)
@staticmethod
def _get_room_displayname(nio_room: MatrixRoom) -> Optional[str]:
name = nio_room.name or nio_room.canonical_alias
if name:
return name
name = nio_room.group_name()
return None if name == "Empty room?" else name
def onRoomInvited(self,
client: Client,
room_id: str,
inviter: Inviter = None) -> None:
self._add_room(client, room_id, client.nio.invited_rooms[room_id],
"Invites", inviter=inviter)
nio_room = client.nio.invited_rooms[room_id]
categories = self.backend.models.accounts[client.userId].roomCategories
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)
def onRoomJoined(self, client: Client, room_id: str) -> None:
self._add_room(client, room_id, client.nio.rooms[room_id], "Rooms")
nio_room = client.nio.rooms[room_id]
categories = self.backend.models.accounts[client.userId].roomCategories
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)
def onRoomLeft(self,
client: Client,
room_id: str,
left_event: LeftEvent = None) -> None:
categories = self.backend.models.accounts[client.userId].roomCategories
self._add_room(client, room_id, client.nio.rooms.get(room_id), "Left",
left_event=left_event)
previous = categories["Rooms"].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)
def _move_room(self, account_id: str, room_id: str) -> None:
def get_newest_event_date_time(room_id: str) -> QDateTime:
for ev in self.backend.models.roomEvents[room_id]:
if not self.backend.EventIsOurProfileChanged(ev, account_id):
return ev.dateTime
return QDateTime.fromMSecsSinceEpoch(0)
rooms_model = self.backend.models.rooms[account_id]
room_index = rooms_model.indexWhere("roomId", room_id)
category = rooms_model[room_index].category
timestamp = get_newest_event_date_time(room_id)
def get_index(put_before_categories: Sequence[str],
put_after_categories: Sequence[str]) -> int:
for i, room in enumerate(rooms_model):
if room.category not in put_after_categories and \
(room.category in put_before_categories or
timestamp >= get_newest_event_date_time(room.roomId)):
return i
return len(rooms_model) - 1
to = 0
if category == "Invites":
to = get_index(["Rooms", "Left"], [])
if category == "Rooms":
to = get_index(["Left"], ["Invites"])
elif category == "Left":
to = get_index([], ["Invites", "Rooms", "Left"])
rooms_model.move(room_index, to)
def _add_room(self,
client: Client,
room_id: str,
room: MatrixRoom,
category: str = "Rooms",
inviter: Inviter = None,
left_event: LeftEvent = None) -> None:
if (inviter and left_event):
raise ValueError()
model = self.backend.models.rooms[client.userId]
no_update = []
def get_displayname() -> Optional[str]:
if not room:
no_update.append("displayName")
return room_id
name = room.name or room.canonical_alias
if name:
return name
name = room.group_name()
return None if name == "Empty room?" else name
item = Room(
categories["Left"].rooms.upsert(0, Room(
roomId = room_id,
displayName = get_displayname(),
category = category,
topic = room.topic if room else None,
inviter = inviter,
displayName = previous.displayName if previous else None,
topic = previous.topic if previous else None,
leftEvent = left_event,
)
model.upsert(room_id, item, ignore_roles=no_update)
with self._lock:
self._move_room(client.userId, room_id)
), 0, 0)
def onRoomSyncPrevBatchTokenReceived(self,
_: Client,
@@ -176,7 +144,7 @@ class SignalManager(QObject):
def onRoomEventReceived(self,
client: Client,
_: Client,
room_id: str,
etype: str,
edict: Dict[str, Any]) -> None:
@@ -192,6 +160,16 @@ class SignalManager(QObject):
.fromMSecsSinceEpoch(edict["server_timestamp"])
new_event = RoomEvent(type=etype, dateTime=date_time, dict=edict)
event_is_our_profile_changed = (
etype == "RoomMemberEvent" and
edict.get("sender") in self.backend.clientManager.clients and
((edict.get("content") or {}).get("membership") ==
(edict.get("prev_content") or {}).get("membership"))
)
if event_is_our_profile_changed:
return
if etype == "RoomCreateEvent":
self.backend.fully_loaded_rooms.add(room_id)
@@ -233,14 +211,20 @@ class SignalManager(QObject):
with self._lock:
process()
self._move_room(client.userId, room_id)
# self._move_room(client.userId, room_id)
def onRoomTypingUsersUpdated(self,
client: Client,
room_id: str,
users: List[str]) -> None:
self.backend.models.rooms[client.userId][room_id].typingUsers = users
categories = self.backend.models.accounts[client.userId].roomCategories
for categ in categories:
try:
categ.rooms[room_id].typingUsers = users
break
except ValueError:
pass
def onMessageAboutToBeSent(self,
@@ -264,10 +248,13 @@ class SignalManager(QObject):
model.insert(0, event)
self._events_in_transfer += 1
self._move_room(client.userId, room_id)
# self._move_room(client.userId, room_id)
def onRoomAboutToBeForgotten(self, client: Client, room_id: str) -> None:
with self._lock:
del self.backend.models.rooms[client.userId][room_id]
self.backend.models.roomEvents[room_id].clear()
categories = self.backend.models.accounts[client.userId].roomCategories
for categ in categories:
categ.rooms.pop(room_id, None)
self.backend.models.roomEvents[room_id].clear()