Rework Backend, models and items organization

This commit is contained in:
miruka 2019-05-11 15:52:56 -04:00
parent 6051ba187a
commit bbc4c15ad3
22 changed files with 258 additions and 169 deletions

View File

@ -4,6 +4,7 @@
- Cleanup unused icons - Cleanup unused icons
- Bug fixes - Bug fixes
- dataclass-like `default_factory` for ListItem
- Local echo messages all have the same time - Local echo messages all have the same time
- Prevent briefly seeing login screen if there are accounts to - Prevent briefly seeing login screen if there are accounts to
resumeSession for but they take time to appear resumeSession for but they take time to appear
@ -47,6 +48,7 @@
- Links preview - Links preview
- Client improvements - Client improvements
- nio.MatrixRoom has `typing_users`, no need to handle it on our own
- Don't send setTypingState False when focus lost if nothing in sendbox - Don't send setTypingState False when focus lost if nothing in sendbox
- Initial sync filter and lazy load, see weechat-matrix `_handle_login()` - Initial sync filter and lazy load, see weechat-matrix `_handle_login()`
- See also `handle_response()`'s `keys_query` request - See also `handle_response()`'s `keys_query` request

View File

@ -2,14 +2,17 @@
# This file is part of harmonyqml, licensed under GPLv3. # This file is part of harmonyqml, licensed under GPLv3.
import os import os
import random
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from typing import Deque, Dict, Optional, Sequence, Set from typing import Deque, Dict, Optional, Sequence, Set, Tuple
from atomicfile import AtomicFile from atomicfile import AtomicFile
from PyQt5.QtCore import QObject, QStandardPaths, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, QStandardPaths, pyqtProperty, pyqtSlot
from .html_filter import HtmlFilter from .html_filter import HtmlFilter
from .model import ListModel, ListModelMap from .model import ListModel, ListModelMap
from .model.items import User
from .network_manager import NioErrorResponse
from .pyqt_future import futurize from .pyqt_future import futurize
@ -18,8 +21,6 @@ class Backend(QObject):
super().__init__(parent) super().__init__(parent)
self.pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=6) self.pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=6)
self._queried_displaynames: Dict[str, str] = {}
self.past_tokens: Dict[str, str] = {} self.past_tokens: Dict[str, str] = {}
self.fully_loaded_rooms: Set[str] = set() self.fully_loaded_rooms: Set[str] = set()
@ -28,9 +29,17 @@ class Backend(QObject):
from .client_manager import ClientManager from .client_manager import ClientManager
self._client_manager: ClientManager = ClientManager(self) self._client_manager: ClientManager = ClientManager(self)
self._accounts: ListModel = ListModel(parent=parent) self._accounts: ListModel = ListModel(parent=parent)
self._room_events: ListModelMap = ListModelMap(Deque, parent)
self._devices: ListModelMap = ListModelMap(parent=parent) self._room_events: ListModelMap = ListModelMap(
container = Deque,
parent = self
)
self._users: ListModel = ListModel(
default_factory = self._query_user,
parent = self
)
from .signal_manager import SignalManager from .signal_manager import SignalManager
self._signal_manager: SignalManager = SignalManager(self) self._signal_manager: SignalManager = SignalManager(self)
@ -55,38 +64,31 @@ class Backend(QObject):
return self._room_events return self._room_events
@pyqtProperty("QVariant", constant=True) @pyqtProperty("QVariant", constant=True)
def devices(self): def users(self):
return self._devices return self._users
@pyqtProperty("QVariant", constant=True) @pyqtProperty("QVariant", constant=True)
def signals(self): def signals(self):
return self._signal_manager return self._signal_manager
@pyqtSlot(str, result="QVariant") def _query_user(self, user_id: str) -> User:
@pyqtSlot(str, bool, result="QVariant") client = random.choice(tuple(self.clients.values())) # nosec
@futurize(max_running=1, consider_args=True)
def getUserDisplayName(self, user_id: str, can_block: bool = True) -> str:
if user_id in self._queried_displaynames:
return self._queried_displaynames[user_id]
for client in self.clients.values(): @futurize(running_value=user_id)
for room in client.nio.rooms.values(): def get_displayname(self) -> str:
displayname = room.user_name(user_id) print("querying", user_id)
try:
response = client.net.talk(client.nio.get_displayname, user_id)
return response.displayname or user_id
except NioErrorResponse:
return user_id
if displayname: return User(
return displayname userId = user_id,
displayName = get_displayname(self),
return self._query_user_displayname(user_id) if can_block else user_id devices = ListModel(),
)
def _query_user_displayname(self, user_id: str) -> str:
client = next(iter(self.clients.values()))
response = client.net.talk(client.nio.get_displayname, user_id)
displayname = getattr(response, "displayname", "") or user_id
self._queried_displaynames[user_id] = displayname
return displayname
@pyqtSlot(str, result=float) @pyqtSlot(str, result=float)
@ -146,7 +148,7 @@ class Backend(QObject):
cl = self.clients cl = self.clients
ac = self.accounts ac = self.accounts
re = self.roomEvents re = self.roomEvents
de = self.devices us = self.users
tcl = lambda user: cl[f"@test_{user}:matrix.org"] tcl = lambda user: cl[f"@test_{user}:matrix.org"]

View File

@ -28,7 +28,7 @@ class Client(QObject):
roomSyncPrevBatchTokenReceived = pyqtSignal(str, str) roomSyncPrevBatchTokenReceived = pyqtSignal(str, str)
roomPastPrevBatchTokenReceived = pyqtSignal(str, str) roomPastPrevBatchTokenReceived = pyqtSignal(str, str)
roomEventReceived = pyqtSignal(str, str, dict) roomEventReceived = pyqtSignal(str, str, dict)
roomTypingUsersUpdated = pyqtSignal(str, list) roomTypingMembersUpdated = pyqtSignal(str, list)
messageAboutToBeSent = pyqtSignal(str, dict) messageAboutToBeSent = pyqtSignal(str, dict)
@ -208,7 +208,7 @@ class Client(QObject):
for ev in room_info.ephemeral: for ev in room_info.ephemeral:
if isinstance(ev, nio.TypingNoticeEvent): if isinstance(ev, nio.TypingNoticeEvent):
self.roomTypingUsersUpdated.emit(room_id, ev.users) self.roomTypingMembersUpdated.emit(room_id, ev.users)
else: else:
print("ephemeral event: ", ev) print("ephemeral event: ", ev)

View File

@ -2,11 +2,10 @@
# This file is part of harmonyqt, licensed under GPLv3. # This file is part of harmonyqt, licensed under GPLv3.
import json import json
import os
import platform import platform
import threading import threading
from collections.abc import Mapping from collections.abc import Mapping
from typing import Dict, Iterable, Optional from typing import Dict
from atomicfile import AtomicFile from atomicfile import AtomicFile
from PyQt5.QtCore import ( from PyQt5.QtCore import (

View File

@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional
from PyQt5.QtCore import QDateTime, QSortFilterProxyModel from PyQt5.QtCore import QDateTime, QSortFilterProxyModel
from ..pyqt_future import PyQtFuture
from .list_item import ListItem from .list_item import ListItem
from .list_model import ListModel from .list_model import ListModel
@ -11,21 +12,15 @@ class Account(ListItem):
_required_init_values = {"userId", "roomCategories"} _required_init_values = {"userId", "roomCategories"}
_constant = {"userId", "roomCategories"} _constant = {"userId", "roomCategories"}
userId: str = "" userId: str = ""
roomCategories: ListModel = ListModel() roomCategories: ListModel = ListModel()
displayName: Optional[str] = None
avatarUrl: Optional[str] = None
statusMessage: Optional[str] = None
class RoomCategory(ListItem): class RoomCategory(ListItem):
_required_init_values = {"name", "rooms", "sortedRooms"} _required_init_values = {"name", "rooms", "sortedRooms"}
_constant = {"rooms", "sortedRooms"} _constant = {"name", "rooms", "sortedRooms"}
name: str = "" 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() sortedRooms: QSortFilterProxyModel = QSortFilterProxyModel()
@ -37,13 +32,16 @@ class Room(ListItem):
roomId: str = "" roomId: str = ""
displayName: str = "" displayName: str = ""
topic: Optional[str] = None topic: Optional[str] = None
typingUsers: List[str] = []
lastEventDateTime: Optional[QDateTime] = None lastEventDateTime: Optional[QDateTime] = None
typingMembers: List[str] = []
members: List[str] = []
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 RoomEvent(ListItem): class RoomEvent(ListItem):
_required_init_values = {"type", "dict"} _required_init_values = {"type", "dict"}
_constant = {"type"} _constant = {"type"}
@ -56,6 +54,20 @@ class RoomEvent(ListItem):
# ---------- # ----------
class User(ListItem):
_required_init_values = {"userId", "devices"}
_constant = {"userId", "devices"}
# Use PyQtFutures because the info might or might not need a request
# to be fetched, and we don't want to block the UI in any case.
# QML's property binding ability is used on the PyQtFuture.value
userId: str = ""
displayName: Optional[PyQtFuture] = None
avatarUrl: Optional[PyQtFuture] = None
statusMessage: Optional[PyQtFuture] = None
devices: ListModel = ListModel()
class Trust(Enum): class Trust(Enum):
blacklisted = -1 blacklisted = -1
undecided = 0 undecided = 0

View File

@ -154,27 +154,18 @@ class ListItem(QObject, metaclass=_ListItemMeta):
def __repr__(self) -> str: def __repr__(self) -> str:
from .list_model import ListModel
multiline = any((
isinstance(v, ListModel) for _, v in self._props.values()
))
prop_strings = ( prop_strings = (
"\033[{0}m{1}{2}={2}{3}\033[0m".format( "\033[{0};34m{1}\033[0,{0}m = \033[{0};32m{2}\033[0m".format(
1 if p == self.mainKey else 0, # 1 = term bold 1 if p == self.mainKey else 0, # 1 = term bold
p, p,
" " if multiline else "", repr(getattr(self, p))
getattr(self, p)
) for p in list(self._props.keys()) + self._direct_props ) for p in list(self._props.keys()) + self._direct_props
) )
if any((isinstance(v, ListModel) for _, v in self._props.values())): return "\033[35m%s\033[0m(\n%s\n)" % (
return "%s(\n%s\n)" % ( type(self).__name__,
type(self).__name__, textwrap.indent(",\n".join(prop_strings), prefix=" " * 4)
textwrap.indent(",\n".join(prop_strings), prefix=" " * 4) )
)
return "%s(%s)" % (type(self).__name__, ", ".join(prop_strings))
@pyqtSlot(result=str) @pyqtSlot(result=str)

View File

@ -30,21 +30,24 @@ class ListModel(QAbstractListModel):
countChanged = pyqtSignal(int) countChanged = pyqtSignal(int)
def __init__(self, def __init__(self,
initial_data: Optional[List[NewItem]] = None, initial_data: Optional[List[NewItem]] = None,
container: Callable[..., MutableSequence] = list, container: Callable[..., MutableSequence] = list,
parent: QObject = None) -> None: default_factory: Optional[Callable[[str], ListItem]] = None,
parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
self._data: MutableSequence[ListItem] = container() self._data: MutableSequence[ListItem] = container()
self.default_factory = default_factory
if initial_data: if initial_data:
self.extend(initial_data) self.extend(initial_data)
def __repr__(self) -> str: def __repr__(self) -> str:
if not self._data: if not self._data:
return "%s()" % type(self).__name__ return "\033[35m%s\033[0m()" % type(self).__name__
return "%s(\n%s\n)" % ( return "\033[35m%s\033[0m(\n%s\n)" % (
type(self).__name__, type(self).__name__,
textwrap.indent( textwrap.indent(
",\n".join((repr(item) for item in self._data)), ",\n".join((repr(item) for item in self._data)),
@ -56,7 +59,7 @@ class ListModel(QAbstractListModel):
def __contains__(self, index: Index) -> bool: def __contains__(self, index: Index) -> bool:
if isinstance(index, str): if isinstance(index, str):
try: try:
self.indexWhere(self.mainKey, index) self.indexWhere(index)
return True return True
except ValueError: except ValueError:
return False return False
@ -84,6 +87,10 @@ class ListModel(QAbstractListModel):
return iter(self._data) return iter(self._data)
def __bool__(self) -> bool:
return bool(self._data)
@pyqtSlot(result=str) @pyqtSlot(result=str)
def repr(self) -> str: def repr(self) -> str:
return self.__repr__() return self.__repr__()
@ -157,14 +164,22 @@ class ListModel(QAbstractListModel):
return len(self) return len(self)
@pyqtSlot(str, "QVariant", result=int) @pyqtSlot("QVariant", result=int)
def indexWhere(self, prop: str, is_value: Any) -> int: def indexWhere(self,
main_key_is_value: Any,
_can_use_default_factory: bool = True) -> int:
for i, item in enumerate(self._data): for i, item in enumerate(self._data):
if getattr(item, prop) == is_value: if getattr(item, self.mainKey) == main_key_is_value:
return i return i
raise ValueError(f"No item in model data with " if _can_use_default_factory and self.default_factory:
f"property {prop!r} set to {is_value!r}.") return self.append(self.default_factory(main_key_is_value))
raise ValueError(
f"No item in model data with "
f"property {self.mainKey} is set to {main_key_is_value!r}."
)
@pyqtSlot(int, result="QVariant") @pyqtSlot(int, result="QVariant")
@ -173,19 +188,25 @@ class ListModel(QAbstractListModel):
@pyqtSlot(str, "QVariant", result="QVariant") @pyqtSlot(str, "QVariant", result="QVariant")
def get(self, index: Index, default: Any = _GetFail()) -> ListItem: def get(self, index: Index, default: Any = _GetFail()) -> ListItem:
try: try:
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = \
if isinstance(index, str) else index self.indexWhere(index, _can_use_default_factory=False) \
if isinstance(index, str) else index
return self._data[i_index] return self._data[i_index]
except (ValueError, IndexError): except (ValueError, IndexError):
if isinstance(default, _GetFail): if isinstance(default, _GetFail):
if self.default_factory and isinstance(index, str):
item = self.default_factory(index)
self.append(item)
return item
raise raise
return default return default
@pyqtSlot(int, "QVariantMap") @pyqtSlot(int, "QVariantMap", result=int)
def insert(self, index: int, value: NewItem) -> None: def insert(self, index: int, value: NewItem) -> int:
value = self._convert_new_value(value) value = self._convert_new_value(value)
self.beginInsertRows(QModelIndex(), index, index) self.beginInsertRows(QModelIndex(), index, index)
@ -199,11 +220,12 @@ class ListModel(QAbstractListModel):
self.countChanged.emit(len(self)) self.countChanged.emit(len(self))
self.changed.emit() self.changed.emit()
return index
@pyqtSlot("QVariantMap") @pyqtSlot("QVariantMap", result=int)
def append(self, value: NewItem) -> None: def append(self, value: NewItem) -> int:
self.insert(len(self), value) return self.insert(len(self), value)
@pyqtSlot(list) @pyqtSlot(list)
@ -222,7 +244,7 @@ class ListModel(QAbstractListModel):
ignore_roles: Sequence[str] = ()) -> int: ignore_roles: Sequence[str] = ()) -> int:
value = self._convert_new_value(value) value = self._convert_new_value(value)
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = self.indexWhere(index, _can_use_default_factory=False) \
if isinstance(index, str) else index if isinstance(index, str) else index
to_update = self[i_index] to_update = self[i_index]
@ -272,7 +294,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, list) @pyqtSlot(int, list)
@pyqtSlot(str, list) @pyqtSlot(str, list)
def set(self, index: Index, value: NewItem) -> None: def set(self, index: Index, value: NewItem) -> None:
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = self.indexWhere(index) \
if isinstance(index, str) else index if isinstance(index, str) else index
qidx = QAbstractListModel.index(self, i_index, 0) qidx = QAbstractListModel.index(self, i_index, 0)
@ -285,7 +307,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, str, "QVariant") @pyqtSlot(int, str, "QVariant")
@pyqtSlot(str, str, "QVariant") @pyqtSlot(str, str, "QVariant")
def setProperty(self, index: Index, prop: str, value: Any) -> None: def setProperty(self, index: Index, prop: str, value: Any) -> None:
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = self.indexWhere(index) \
if isinstance(index, str) else index if isinstance(index, str) else index
if getattr(self[i_index], prop) != value: if getattr(self[i_index], prop) != value:
@ -301,7 +323,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(str, int, int) @pyqtSlot(str, int, int)
def move(self, from_: Index, to: int, n: int = 1) -> None: def move(self, from_: Index, to: int, n: int = 1) -> None:
# pylint: disable=invalid-name # pylint: disable=invalid-name
i_from: int = self.indexWhere(self.mainKey, from_) \ i_from: int = self.indexWhere(from_) \
if isinstance(from_, str) else from_ if isinstance(from_, str) else from_
qlast = i_from + n - 1 qlast = i_from + n - 1
@ -332,7 +354,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int) @pyqtSlot(int)
@pyqtSlot(str) @pyqtSlot(str)
def remove(self, index: Index) -> None: def remove(self, index: Index) -> None:
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = self.indexWhere(index) \
if isinstance(index, str) else index if isinstance(index, str) else index
self.beginRemoveRows(QModelIndex(), i_index, i_index) self.beginRemoveRows(QModelIndex(), i_index, i_index)
@ -347,7 +369,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def pop(self, index: Index, default: Any = _PopFail()) -> ListItem: def pop(self, index: Index, default: Any = _PopFail()) -> ListItem:
try: try:
i_index: int = self.indexWhere(self.mainKey, index) \ i_index: int = self.indexWhere(index) \
if isinstance(index, str) else index if isinstance(index, str) else index
item = self[i_index] item = self[i_index]

View File

@ -1,4 +1,4 @@
from typing import Any, Callable, DefaultDict, MutableSequence from typing import Any, DefaultDict
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
@ -6,15 +6,15 @@ from .list_model import ListModel
class ListModelMap(QObject): class ListModelMap(QObject):
def __init__(self, def __init__(self, *models_args, parent: QObject = None, **models_kwargs
models_container: Callable[..., MutableSequence] = list, ) -> None:
parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
models_kwargs["parent"] = self
# Set the parent to prevent item garbage-collection on the C++ side # Set the parent to prevent item garbage-collection on the C++ side
self.dict: DefaultDict[Any, ListModel] = \ self.dict: DefaultDict[Any, ListModel] = \
DefaultDict( DefaultDict(
lambda: ListModel(container=models_container, parent=self) lambda: ListModel(*models_args, **models_kwargs)
) )

View File

@ -7,7 +7,7 @@ import sys
import time import time
import traceback import traceback
from concurrent.futures import Executor, Future from concurrent.futures import Executor, Future
from typing import Callable, Deque, Optional, Tuple, Union from typing import Any, Callable, Deque, Optional, Tuple, Union
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
@ -15,10 +15,14 @@ from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
class PyQtFuture(QObject): class PyQtFuture(QObject):
gotResult = pyqtSignal("QVariant") gotResult = pyqtSignal("QVariant")
def __init__(self, future: Future, parent: QObject) -> None: def __init__(self,
future: Future,
running_value: Any = None,
parent: Optional[QObject] = None) -> None:
super().__init__(parent) super().__init__(parent)
self.future = future self.future = future
self._result = None self.running_value = running_value
self._result = None
self.future.add_done_callback( self.future.add_done_callback(
lambda future: self.gotResult.emit(future.result()) lambda future: self.gotResult.emit(future.result())
@ -64,7 +68,7 @@ class PyQtFuture(QObject):
@pyqtProperty("QVariant", notify=gotResult) @pyqtProperty("QVariant", notify=gotResult)
def value(self): def value(self):
return self.future.result() if self.done else None return self.future.result() if self.done else self.running_value
def add_done_callback(self, fn: Callable[[Future], None]) -> None: def add_done_callback(self, fn: Callable[[Future], None]) -> None:
@ -79,7 +83,8 @@ _PENDING: Deque[_Task] = Deque()
def futurize(max_running: Optional[int] = None, def futurize(max_running: Optional[int] = None,
consider_args: bool = False, consider_args: bool = False,
discard_if_max_running: bool = False, discard_if_max_running: bool = False,
pyqt: bool = True) -> Callable: pyqt: bool = True,
running_value: Any = None) -> Callable:
def decorator(func: Callable) -> Callable: def decorator(func: Callable) -> Callable:
@ -145,7 +150,9 @@ def futurize(max_running: Optional[int] = None,
del _RUNNING[_RUNNING.index(task)] del _RUNNING[_RUNNING.index(task)]
future = self.pool.submit(run_and_catch_errs) future = self.pool.submit(run_and_catch_errs)
return PyQtFuture(future, self) if pyqt else future return PyQtFuture(
future=future, running_value=running_value, parent=self
) if pyqt else future
return wrapper return wrapper

View File

@ -1,6 +1,7 @@
# Copyright 2019 miruka # Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3. # This file is part of harmonyqml, licensed under GPLv3.
from concurrent.futures import ThreadPoolExecutor
from threading import Lock from threading import Lock
from typing import Any, Deque, Dict, List, Optional from typing import Any, Deque, Dict, List, Optional
@ -12,9 +13,10 @@ 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 ( from .model.items import (
Account, Device, ListModel, Room, RoomCategory, RoomEvent Account, Device, ListModel, Room, RoomCategory, RoomEvent, User
) )
from .model.sort_filter_proxy import SortFilterProxy from .model.sort_filter_proxy import SortFilterProxy
from .pyqt_future import futurize
Inviter = Optional[Dict[str, str]] Inviter = Optional[Dict[str, str]]
LeftEvent = Optional[Dict[str, str]] LeftEvent = Optional[Dict[str, str]]
@ -27,6 +29,8 @@ class SignalManager(QObject):
def __init__(self, backend: Backend) -> None: def __init__(self, backend: Backend) -> None:
super().__init__(parent=backend) super().__init__(parent=backend)
self.pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=6)
self.backend = backend self.backend = backend
self.last_room_events: Deque[str] = Deque(maxlen=1000) self.last_room_events: Deque[str] = Deque(maxlen=1000)
@ -37,7 +41,23 @@ class SignalManager(QObject):
def onClientAdded(self, client: Client) -> None: def onClientAdded(self, client: Client) -> None:
# Build an Account item for the Backend.accounts model if client.userId in self.backend.accounts:
return
# An user might already exist in the model, e.g. if another account
# was in a room with the account that we just connected to
self.backend.users.upsert(
where_main_key_is = client.userId,
update_with = User(
userId = client.userId,
displayName = self.backend.users[client.userId].displayName,
# Devices are added later, we might need to upload keys before
# but we want to show the accounts ASAP in the client side pane
devices = ListModel(),
)
)
# Backend.accounts
room_categories_kwargs: List[Dict[str, Any]] = [ room_categories_kwargs: List[Dict[str, Any]] = [
{"name": "Invites", "rooms": ListModel()}, {"name": "Invites", "rooms": ListModel()},
{"name": "Rooms", "rooms": ListModel()}, {"name": "Rooms", "rooms": ListModel()},
@ -57,25 +77,31 @@ class SignalManager(QObject):
roomCategories = ListModel([ roomCategories = ListModel([
RoomCategory(**kws) for kws in room_categories_kwargs RoomCategory(**kws) for kws in room_categories_kwargs
]), ]),
displayName = self.backend.getUserDisplayName(client.userId),
)) ))
# Upload our E2E keys to the matrix server if needed # Upload our E2E keys to the matrix server if needed
if not client.nio.olm_account_shared: if not client.nio.olm_account_shared:
client.uploadE2EKeys() client.uploadE2EKeys()
# Add our devices to the Backend.devices model # Add all devices nio knows for this account
store = client.nio.device_store store = client.nio.device_store
for user_id in store.users: for user_id in store.users:
self.backend.devices[user_id].clear() user = self.backend.users.get(user_id, None)
self.backend.devices[user_id].extend([ if not user:
Device( self.backend.users.append(
deviceId = dev.id, User(userId=user_id, devices=ListModel())
ed25519Key = dev.ed25519, )
trust = client.getDeviceTrust(dev),
) for dev in store.active_user_devices(user_id) for device in store.active_user_devices(user_id):
]) self.backend.users[client.userId].devices.upsert(
where_main_key_is = device.id,
update_with = Device(
deviceId = device.id,
ed25519Key = device.ed25519,
trust = client.getDeviceTrust(device),
)
)
# Finally, connect all client signals # Finally, connect all client signals
self.connectClient(client) self.connectClient(client)
@ -107,12 +133,32 @@ class SignalManager(QObject):
return None if name == "Empty room?" else name return None if name == "Empty room?" else name
def _add_users_from_nio_room(self, room: nio.rooms.MatrixRoom) -> None:
for user in room.users.values():
@futurize(running_value=user.display_name)
def get_displayname(self, user) -> str:
# pylint:disable=unused-argument
return user.display_name
self.backend.users.upsert(
where_main_key_is = user.user_id,
update_with = User(
userId = user.user_id,
displayName = get_displayname(self, user),
devices = ListModel()
),
ignore_roles = ("devices",),
)
def onRoomInvited(self, def onRoomInvited(self,
client: Client, client: Client,
room_id: str, room_id: str,
inviter: Inviter = None) -> None: inviter: Inviter = None) -> None:
nio_room = client.nio.invited_rooms[room_id] nio_room = client.nio.invited_rooms[room_id]
self._add_users_from_nio_room(nio_room)
categories = self.backend.accounts[client.userId].roomCategories categories = self.backend.accounts[client.userId].roomCategories
previous_room = categories["Rooms"].rooms.pop(room_id, None) previous_room = categories["Rooms"].rooms.pop(room_id, None)
@ -126,9 +172,9 @@ class SignalManager(QObject):
topic = nio_room.topic, topic = nio_room.topic,
inviter = inviter, inviter = inviter,
lastEventDateTime = QDateTime.currentDateTime(), # FIXME lastEventDateTime = QDateTime.currentDateTime(), # FIXME
members = list(nio_room.users.keys()),
), ),
new_index_if_insert = 0, ignore_roles = ("typingMembers"),
ignore_roles = ("typingUsers"),
) )
signal = self.roomCategoryChanged signal = self.roomCategoryChanged
@ -139,7 +185,9 @@ class SignalManager(QObject):
def onRoomJoined(self, client: Client, room_id: str) -> None: def onRoomJoined(self, client: Client, room_id: str) -> None:
nio_room = client.nio.rooms[room_id] nio_room = client.nio.rooms[room_id]
self._add_users_from_nio_room(nio_room)
categories = self.backend.accounts[client.userId].roomCategories categories = self.backend.accounts[client.userId].roomCategories
previous_invite = categories["Invites"].rooms.pop(room_id, None) previous_invite = categories["Invites"].rooms.pop(room_id, None)
@ -151,9 +199,9 @@ class SignalManager(QObject):
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,
members = list(nio_room.users.keys()),
), ),
new_index_if_insert = 0, ignore_roles = ("typingMembers", "lastEventDateTime"),
ignore_roles = ("typingUsers", "lastEventDateTime"),
) )
signal = self.roomCategoryChanged signal = self.roomCategoryChanged
@ -188,8 +236,7 @@ class SignalManager(QObject):
if left_time else QDateTime.currentDateTime() if left_time else QDateTime.currentDateTime()
), ),
), ),
new_index_if_insert = 0, ignore_roles = ("members", "lastEventDateTime"),
ignore_roles = ("typingUsers", "lastEventDateTime"),
) )
signal = self.roomCategoryChanged signal = self.roomCategoryChanged
@ -304,14 +351,14 @@ class SignalManager(QObject):
self._set_room_last_event(client.userId, room_id, new_event) self._set_room_last_event(client.userId, room_id, new_event)
def onRoomTypingUsersUpdated(self, def onRoomTypingMembersUpdated(self,
client: Client, client: Client,
room_id: str, room_id: str,
users: List[str]) -> None: users: List[str]) -> None:
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.setProperty(room_id, "typingUsers", users) categ.rooms.setProperty(room_id, "typingMembers", users)
break break
except ValueError: except ValueError:
pass pass
@ -355,9 +402,16 @@ class SignalManager(QObject):
user_id: str, user_id: str,
device_id: str, device_id: str,
ed25519_key: str) -> None: ed25519_key: str) -> None:
nio_device = client.nio.device_store[user_id][device_id] nio_device = client.nio.device_store[user_id][device_id]
self.backend.devices[user_id].upsert( user = self.backend.users.get(user_id, None)
if not user:
self.backend.users.append(
User(userId=user_id, devices=ListModel())
)
self.backend.users[user_id].devices.upsert(
where_main_key_is = device_id, where_main_key_is = device_id,
update_with = Device( update_with = Device(
deviceId = device_id, deviceId = device_id,
@ -369,4 +423,7 @@ class SignalManager(QObject):
def onDeviceIsDeleted(self, _: Client, user_id: str, device_id: str def onDeviceIsDeleted(self, _: Client, user_id: str, device_id: str
) -> None: ) -> None:
self.backend.devices[user_id].pop(device_id, None) try:
del self.backend.users[user_id].devices[device_id]
except ValueError:
pass

View File

@ -2,16 +2,10 @@ import QtQuick 2.7
import "../Base" import "../Base"
Rectangle { Rectangle {
property bool hidden: false property var name: null
property var name: null // null, string or PyQtFuture property var imageUrl: null
property var imageSource: null
property int dimension: 36 property int dimension: 36
property bool hidden: false
readonly property string resolvedName:
! name ? "?" :
typeof(name) == "string" ? name :
(name.value ? name.value : "?")
width: dimension width: dimension
height: hidden ? 1 : dimension height: hidden ? 1 : dimension
@ -20,21 +14,21 @@ Rectangle {
opacity: hidden ? 0 : 1 opacity: hidden ? 0 : 1
color: resolvedName === "?" ? color: name ?
HStyle.avatar.background.unknown :
Qt.hsla( Qt.hsla(
Backend.hueFromString(resolvedName), Backend.hueFromString(name),
HStyle.avatar.background.saturation, HStyle.avatar.background.saturation,
HStyle.avatar.background.lightness, HStyle.avatar.background.lightness,
HStyle.avatar.background.alpha HStyle.avatar.background.alpha
) ) :
HStyle.avatar.background.unknown
HLabel { HLabel {
z: 1 z: 1
anchors.centerIn: parent anchors.centerIn: parent
visible: ! hidden visible: ! hidden
text: resolvedName.charAt(0) text: name ? name.charAt(0) : "?"
color: HStyle.avatar.letter color: HStyle.avatar.letter
font.pixelSize: parent.height / 1.4 font.pixelSize: parent.height / 1.4
} }
@ -42,9 +36,9 @@ Rectangle {
HImage { HImage {
z: 2 z: 2
anchors.fill: parent anchors.fill: parent
visible: ! hidden && imageSource !== null visible: ! hidden && imageUrl
Component.onCompleted: if (imageSource) {source = imageSource} Component.onCompleted: if (imageUrl) { source = imageUrl }
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: dimension sourceSize.width: dimension
} }

View File

@ -96,7 +96,7 @@ QtObject {
property color background: colors.background1 property color background: colors.background1
} }
readonly property QtObject typingUsers: QtObject { readonly property QtObject typingMembers: QtObject {
property color background: colors.background0 property color background: colors.background0
} }

View File

@ -7,7 +7,7 @@ Banner {
color: HStyle.chat.inviteBanner.background color: HStyle.chat.inviteBanner.background
avatar.name: inviter ? inviter.displayname : "" avatar.name: inviter ? inviter.displayname : ""
//avatar.imageSource: inviter ? inviter.avatar_url : "" //avatar.imageUrl: inviter ? inviter.avatar_url : ""
labelText: labelText:
(inviter ? (inviter ?

View File

@ -14,8 +14,11 @@ HColumnLayout {
.roomCategories.get(category) .roomCategories.get(category)
.rooms.get(roomId) .rooms.get(roomId)
readonly property var sender: Backend.users.get(userId)
readonly property bool hasUnknownDevices: readonly property bool hasUnknownDevices:
Backend.clients.get(userId).roomHasUnknownDevices(roomId) category == "Rooms" ?
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
id: chatPage id: chatPage
onFocusChanged: sendBox.setFocus() onFocusChanged: sendBox.setFocus()
@ -38,7 +41,7 @@ HColumnLayout {
Layout.fillHeight: true Layout.fillHeight: true
} }
TypingUsersBar {} TypingMembersBar {}
InviteBanner { InviteBanner {
visible: category === "Invites" visible: category === "Invites"
@ -46,12 +49,12 @@ HColumnLayout {
} }
UnknownDevicesBanner { UnknownDevicesBanner {
visible: category === "Rooms" && hasUnknownDevices visible: category == "Rooms" && hasUnknownDevices
} }
SendBox { SendBox {
id: sendBox id: sendBox
visible: category === "Rooms" && ! hasUnknownDevices visible: category == "Rooms" && ! hasUnknownDevices
} }
LeftBanner { LeftBanner {

View File

@ -16,7 +16,7 @@ Row {
HAvatar { HAvatar {
id: avatar id: avatar
name: displayName name: sender.displayName.value
hidden: combine hidden: combine
dimension: 28 dimension: 28
} }
@ -26,12 +26,12 @@ Row {
id: contentLabel id: contentLabel
text: "<font color='" + text: "<font color='" +
Qt.hsla(Backend.hueFromString(displayName.value || dict.sender), Qt.hsla(Backend.hueFromString(sender.displayName.value),
HStyle.chat.event.saturation, HStyle.chat.event.saturation,
HStyle.chat.event.lightness, HStyle.chat.event.lightness,
1) + 1) +
"'>" + "'>" +
(displayName.value || dict.sender) + " " + sender.displayName.value + " " +
ChatJS.getEventText(type, dict) + ChatJS.getEventText(type, dict) +
"&nbsp;&nbsp;" + "&nbsp;&nbsp;" +

View File

@ -10,7 +10,7 @@ Row {
HAvatar { HAvatar {
id: avatar id: avatar
hidden: combine hidden: combine
name: displayName name: sender.displayName.value
} }
Rectangle { Rectangle {
@ -37,7 +37,7 @@ Row {
visible: height > 0 visible: height > 0
id: nameLabel id: nameLabel
text: displayName.value || dict.sender text: sender.displayName.value
color: Qt.hsla(Backend.hueFromString(text), color: Qt.hsla(Backend.hueFromString(text),
HStyle.displayName.saturation, HStyle.displayName.saturation,
HStyle.displayName.lightness, HStyle.displayName.lightness,

View File

@ -26,8 +26,7 @@ Column {
readonly property bool isUndecryptableEvent: readonly property bool isUndecryptableEvent:
type === "OlmEvent" || type === "MegolmEvent" type === "OlmEvent" || type === "MegolmEvent"
readonly property var displayName: readonly property var sender: Backend.users.get(dict.sender)
Backend.getUserDisplayName(dict.sender)
readonly property bool isOwn: readonly property bool isOwn:
chatPage.userId === dict.sender chatPage.userId === dict.sender

View File

@ -18,7 +18,7 @@ HGlassRectangle {
HAvatar { HAvatar {
id: avatar id: avatar
name: Backend.getUserDisplayName(chatPage.userId) name: chatPage.sender.displayName.value
dimension: root.Layout.minimumHeight dimension: root.Layout.minimumHeight
} }

View File

@ -4,9 +4,9 @@ import "../Base"
import "utils.js" as ChatJS import "utils.js" as ChatJS
HGlassRectangle { HGlassRectangle {
property var typingUsers: chatPage.roomInfo.typingUsers property var typingMembers: chatPage.roomInfo.typingMembers
color: HStyle.chat.typingUsers.background color: HStyle.chat.typingMembers.background
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0 Layout.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0
@ -16,7 +16,7 @@ HGlassRectangle {
id: usersLabel id: usersLabel
anchors.fill: parent anchors.fill: parent
text: ChatJS.getTypingUsersText(typingUsers, chatPage.userId) text: ChatJS.getTypingMembersText(typingMembers, chatPage.userId)
elide: Text.ElideMiddle elide: Text.ElideMiddle
maximumLineCount: 1 maximumLineCount: 1
} }

View File

@ -84,9 +84,7 @@ function getHistoryVisibilityEventText(dict) {
function getStateDisplayName(dict) { function getStateDisplayName(dict) {
// The dict.content.displayname may be outdated, prefer // The dict.content.displayname may be outdated, prefer
// retrieving it fresh // retrieving it fresh
var name = Backend.getUserDisplayName(dict.state_key, false) return Backend.users.get(dict.state_key).displayName.value
return name === dict.state_key ?
dict.content.displayname : name.result()
} }
@ -168,7 +166,7 @@ function getLeftBannerText(leftEvent) {
if (info.membership) if (info.membership)
var name = Backend.getUserDisplayName(leftEvent.sender, false).result() var name = Backend.users.get(leftEvent.sender).displayName.value
return "<b>" + name + "</b> " + return "<b>" + name + "</b> " +
(info.membership == "ban" ? (info.membership == "ban" ?
@ -187,19 +185,19 @@ function getLeftBannerText(leftEvent) {
function getLeftBannerAvatarName(leftEvent, accountId) { function getLeftBannerAvatarName(leftEvent, accountId) {
if (! leftEvent || leftEvent.state_key == leftEvent.sender) { if (! leftEvent || leftEvent.state_key == leftEvent.sender) {
return Backend.getUserDisplayName(accountId, false).result() return Backend.users.get(accountId).displayName.value
} }
return Backend.getUserDisplayName(leftEvent.sender, false).result() return Backend.users.get(leftEvent.sender).displayName.value
} }
function getTypingUsersText(users, ourAccountId) { function getTypingMembersText(users, ourAccountId) {
var names = [] var names = []
for (var i = 0; i < users.length; i++) { for (var i = 0; i < users.length; i++) {
if (users[i] !== ourAccountId) { if (users[i] !== ourAccountId) {
names.push(Backend.getUserDisplayName(users[i], false).result()) names.push(Backend.users.get(users[i]).displayName.value)
} }
} }

View File

@ -6,6 +6,8 @@ Column {
id: accountDelegate id: accountDelegate
width: parent.width width: parent.width
property var user: Backend.users.get(userId)
property string roomCategoriesListUserId: userId property string roomCategoriesListUserId: userId
property bool expanded: true property bool expanded: true
@ -16,7 +18,7 @@ Column {
HAvatar { HAvatar {
id: avatar id: avatar
name: displayName name: user.displayName.value
} }
HColumnLayout { HColumnLayout {
@ -25,7 +27,7 @@ Column {
HLabel { HLabel {
id: accountLabel id: accountLabel
text: displayName.value || userId text: user.displayName.value
elide: HLabel.ElideRight elide: HLabel.ElideRight
maximumLineCount: 1 maximumLineCount: 1
Layout.fillWidth: true Layout.fillWidth: true
@ -35,7 +37,7 @@ Column {
HTextField { HTextField {
id: statusEdit id: statusEdit
text: statusMessage || "" text: user.statusMessage || ""
placeholderText: qsTr("Set status message") placeholderText: qsTr("Set status message")
font.pixelSize: HStyle.fontSize.small font.pixelSize: HStyle.fontSize.small
background: null background: null

View File

@ -6,7 +6,8 @@ function getLastRoomEventText(roomId, accountId) {
if (eventsModel.count < 1) { return "" } if (eventsModel.count < 1) { return "" }
var ev = eventsModel.get(0) var ev = eventsModel.get(0)
var name = Backend.getUserDisplayName(ev.dict.sender, false).result() var name = Backend.users.get(ev.dict.sender).displayName.value
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent" var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
if (undecryptable || ev.type.startsWith("RoomMessage")) { if (undecryptable || ev.type.startsWith("RoomMessage")) {