Rework Backend, models and items organization
This commit is contained in:
parent
6051ba187a
commit
bbc4c15ad3
2
TODO.md
2
TODO.md
|
@ -4,6 +4,7 @@
|
|||
- Cleanup unused icons
|
||||
|
||||
- Bug fixes
|
||||
- dataclass-like `default_factory` for ListItem
|
||||
- Local echo messages all have the same time
|
||||
- Prevent briefly seeing login screen if there are accounts to
|
||||
resumeSession for but they take time to appear
|
||||
|
@ -47,6 +48,7 @@
|
|||
- Links preview
|
||||
|
||||
- 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
|
||||
- Initial sync filter and lazy load, see weechat-matrix `_handle_login()`
|
||||
- See also `handle_response()`'s `keys_query` request
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
import os
|
||||
import random
|
||||
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 PyQt5.QtCore import QObject, QStandardPaths, pyqtProperty, pyqtSlot
|
||||
|
||||
from .html_filter import HtmlFilter
|
||||
from .model import ListModel, ListModelMap
|
||||
from .model.items import User
|
||||
from .network_manager import NioErrorResponse
|
||||
from .pyqt_future import futurize
|
||||
|
||||
|
||||
|
@ -18,8 +21,6 @@ class Backend(QObject):
|
|||
super().__init__(parent)
|
||||
self.pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=6)
|
||||
|
||||
self._queried_displaynames: Dict[str, str] = {}
|
||||
|
||||
self.past_tokens: Dict[str, str] = {}
|
||||
self.fully_loaded_rooms: Set[str] = set()
|
||||
|
||||
|
@ -28,9 +29,17 @@ class Backend(QObject):
|
|||
from .client_manager import ClientManager
|
||||
self._client_manager: ClientManager = ClientManager(self)
|
||||
|
||||
self._accounts: ListModel = ListModel(parent=parent)
|
||||
self._room_events: ListModelMap = ListModelMap(Deque, parent)
|
||||
self._devices: ListModelMap = ListModelMap(parent=parent)
|
||||
self._accounts: ListModel = ListModel(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
|
||||
self._signal_manager: SignalManager = SignalManager(self)
|
||||
|
@ -55,38 +64,31 @@ class Backend(QObject):
|
|||
return self._room_events
|
||||
|
||||
@pyqtProperty("QVariant", constant=True)
|
||||
def devices(self):
|
||||
return self._devices
|
||||
def users(self):
|
||||
return self._users
|
||||
|
||||
@pyqtProperty("QVariant", constant=True)
|
||||
def signals(self):
|
||||
return self._signal_manager
|
||||
|
||||
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
@pyqtSlot(str, bool, result="QVariant")
|
||||
@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]
|
||||
def _query_user(self, user_id: str) -> User:
|
||||
client = random.choice(tuple(self.clients.values())) # nosec
|
||||
|
||||
for client in self.clients.values():
|
||||
for room in client.nio.rooms.values():
|
||||
displayname = room.user_name(user_id)
|
||||
@futurize(running_value=user_id)
|
||||
def get_displayname(self) -> str:
|
||||
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 displayname
|
||||
|
||||
return self._query_user_displayname(user_id) if can_block else user_id
|
||||
|
||||
|
||||
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
|
||||
return User(
|
||||
userId = user_id,
|
||||
displayName = get_displayname(self),
|
||||
devices = ListModel(),
|
||||
)
|
||||
|
||||
|
||||
@pyqtSlot(str, result=float)
|
||||
|
@ -146,7 +148,7 @@ class Backend(QObject):
|
|||
cl = self.clients
|
||||
ac = self.accounts
|
||||
re = self.roomEvents
|
||||
de = self.devices
|
||||
us = self.users
|
||||
|
||||
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class Client(QObject):
|
|||
roomSyncPrevBatchTokenReceived = pyqtSignal(str, str)
|
||||
roomPastPrevBatchTokenReceived = pyqtSignal(str, str)
|
||||
roomEventReceived = pyqtSignal(str, str, dict)
|
||||
roomTypingUsersUpdated = pyqtSignal(str, list)
|
||||
roomTypingMembersUpdated = pyqtSignal(str, list)
|
||||
|
||||
messageAboutToBeSent = pyqtSignal(str, dict)
|
||||
|
||||
|
@ -208,7 +208,7 @@ class Client(QObject):
|
|||
|
||||
for ev in room_info.ephemeral:
|
||||
if isinstance(ev, nio.TypingNoticeEvent):
|
||||
self.roomTypingUsersUpdated.emit(room_id, ev.users)
|
||||
self.roomTypingMembersUpdated.emit(room_id, ev.users)
|
||||
else:
|
||||
print("ephemeral event: ", ev)
|
||||
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
# This file is part of harmonyqt, licensed under GPLv3.
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import threading
|
||||
from collections.abc import Mapping
|
||||
from typing import Dict, Iterable, Optional
|
||||
from typing import Dict
|
||||
|
||||
from atomicfile import AtomicFile
|
||||
from PyQt5.QtCore import (
|
||||
|
|
|
@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from PyQt5.QtCore import QDateTime, QSortFilterProxyModel
|
||||
|
||||
from ..pyqt_future import PyQtFuture
|
||||
from .list_item import ListItem
|
||||
from .list_model import ListModel
|
||||
|
||||
|
@ -11,21 +12,15 @@ class Account(ListItem):
|
|||
_required_init_values = {"userId", "roomCategories"}
|
||||
_constant = {"userId", "roomCategories"}
|
||||
|
||||
userId: str = ""
|
||||
roomCategories: ListModel = ListModel()
|
||||
displayName: Optional[str] = None
|
||||
avatarUrl: Optional[str] = None
|
||||
statusMessage: Optional[str] = None
|
||||
userId: str = ""
|
||||
roomCategories: ListModel = ListModel()
|
||||
|
||||
|
||||
class RoomCategory(ListItem):
|
||||
_required_init_values = {"name", "rooms", "sortedRooms"}
|
||||
_constant = {"rooms", "sortedRooms"}
|
||||
_constant = {"name", "rooms", "sortedRooms"}
|
||||
|
||||
name: str = ""
|
||||
|
||||
# Must be provided at init, else it will be the same object
|
||||
# for every RoomCategory
|
||||
name: str = ""
|
||||
rooms: ListModel = ListModel()
|
||||
sortedRooms: QSortFilterProxyModel = QSortFilterProxyModel()
|
||||
|
||||
|
@ -37,13 +32,16 @@ class Room(ListItem):
|
|||
roomId: str = ""
|
||||
displayName: str = ""
|
||||
topic: Optional[str] = None
|
||||
typingUsers: List[str] = []
|
||||
lastEventDateTime: Optional[QDateTime] = None
|
||||
typingMembers: List[str] = []
|
||||
members: List[str] = []
|
||||
|
||||
inviter: Optional[Dict[str, str]] = None
|
||||
leftEvent: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
# ----------
|
||||
|
||||
class RoomEvent(ListItem):
|
||||
_required_init_values = {"type", "dict"}
|
||||
_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):
|
||||
blacklisted = -1
|
||||
undecided = 0
|
||||
|
|
|
@ -154,27 +154,18 @@ class ListItem(QObject, metaclass=_ListItemMeta):
|
|||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
from .list_model import ListModel
|
||||
multiline = any((
|
||||
isinstance(v, ListModel) for _, v in self._props.values()
|
||||
))
|
||||
|
||||
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
|
||||
p,
|
||||
" " if multiline else "",
|
||||
getattr(self, p)
|
||||
repr(getattr(self, p))
|
||||
) for p in list(self._props.keys()) + self._direct_props
|
||||
)
|
||||
|
||||
if any((isinstance(v, ListModel) for _, v in self._props.values())):
|
||||
return "%s(\n%s\n)" % (
|
||||
type(self).__name__,
|
||||
textwrap.indent(",\n".join(prop_strings), prefix=" " * 4)
|
||||
)
|
||||
|
||||
return "%s(%s)" % (type(self).__name__, ", ".join(prop_strings))
|
||||
return "\033[35m%s\033[0m(\n%s\n)" % (
|
||||
type(self).__name__,
|
||||
textwrap.indent(",\n".join(prop_strings), prefix=" " * 4)
|
||||
)
|
||||
|
||||
|
||||
@pyqtSlot(result=str)
|
||||
|
|
|
@ -30,21 +30,24 @@ class ListModel(QAbstractListModel):
|
|||
countChanged = pyqtSignal(int)
|
||||
|
||||
def __init__(self,
|
||||
initial_data: Optional[List[NewItem]] = None,
|
||||
container: Callable[..., MutableSequence] = list,
|
||||
parent: QObject = None) -> None:
|
||||
initial_data: Optional[List[NewItem]] = None,
|
||||
container: Callable[..., MutableSequence] = list,
|
||||
default_factory: Optional[Callable[[str], ListItem]] = None,
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._data: MutableSequence[ListItem] = container()
|
||||
|
||||
self.default_factory = default_factory
|
||||
|
||||
if initial_data:
|
||||
self.extend(initial_data)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
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__,
|
||||
textwrap.indent(
|
||||
",\n".join((repr(item) for item in self._data)),
|
||||
|
@ -56,7 +59,7 @@ class ListModel(QAbstractListModel):
|
|||
def __contains__(self, index: Index) -> bool:
|
||||
if isinstance(index, str):
|
||||
try:
|
||||
self.indexWhere(self.mainKey, index)
|
||||
self.indexWhere(index)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
@ -84,6 +87,10 @@ class ListModel(QAbstractListModel):
|
|||
return iter(self._data)
|
||||
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._data)
|
||||
|
||||
|
||||
@pyqtSlot(result=str)
|
||||
def repr(self) -> str:
|
||||
return self.__repr__()
|
||||
|
@ -157,14 +164,22 @@ class ListModel(QAbstractListModel):
|
|||
return len(self)
|
||||
|
||||
|
||||
@pyqtSlot(str, "QVariant", result=int)
|
||||
def indexWhere(self, prop: str, is_value: Any) -> int:
|
||||
@pyqtSlot("QVariant", result=int)
|
||||
def indexWhere(self,
|
||||
main_key_is_value: Any,
|
||||
_can_use_default_factory: bool = True) -> int:
|
||||
|
||||
for i, item in enumerate(self._data):
|
||||
if getattr(item, prop) == is_value:
|
||||
if getattr(item, self.mainKey) == main_key_is_value:
|
||||
return i
|
||||
|
||||
raise ValueError(f"No item in model data with "
|
||||
f"property {prop!r} set to {is_value!r}.")
|
||||
if _can_use_default_factory and self.default_factory:
|
||||
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")
|
||||
|
@ -173,19 +188,25 @@ class ListModel(QAbstractListModel):
|
|||
@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
|
||||
i_index: int = \
|
||||
self.indexWhere(index, _can_use_default_factory=False) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
return self._data[i_index]
|
||||
|
||||
except (ValueError, IndexError):
|
||||
if isinstance(default, _GetFail):
|
||||
if self.default_factory and isinstance(index, str):
|
||||
item = self.default_factory(index)
|
||||
self.append(item)
|
||||
return item
|
||||
raise
|
||||
|
||||
return default
|
||||
|
||||
|
||||
@pyqtSlot(int, "QVariantMap")
|
||||
def insert(self, index: int, value: NewItem) -> None:
|
||||
@pyqtSlot(int, "QVariantMap", result=int)
|
||||
def insert(self, index: int, value: NewItem) -> int:
|
||||
value = self._convert_new_value(value)
|
||||
|
||||
self.beginInsertRows(QModelIndex(), index, index)
|
||||
|
@ -199,11 +220,12 @@ class ListModel(QAbstractListModel):
|
|||
|
||||
self.countChanged.emit(len(self))
|
||||
self.changed.emit()
|
||||
return index
|
||||
|
||||
|
||||
@pyqtSlot("QVariantMap")
|
||||
def append(self, value: NewItem) -> None:
|
||||
self.insert(len(self), value)
|
||||
@pyqtSlot("QVariantMap", result=int)
|
||||
def append(self, value: NewItem) -> int:
|
||||
return self.insert(len(self), value)
|
||||
|
||||
|
||||
@pyqtSlot(list)
|
||||
|
@ -222,7 +244,7 @@ class ListModel(QAbstractListModel):
|
|||
ignore_roles: Sequence[str] = ()) -> int:
|
||||
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
|
||||
|
||||
to_update = self[i_index]
|
||||
|
@ -272,7 +294,7 @@ class ListModel(QAbstractListModel):
|
|||
@pyqtSlot(int, list)
|
||||
@pyqtSlot(str, list)
|
||||
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
|
||||
|
||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
||||
|
@ -285,7 +307,7 @@ class ListModel(QAbstractListModel):
|
|||
@pyqtSlot(int, str, "QVariant")
|
||||
@pyqtSlot(str, str, "QVariant")
|
||||
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 getattr(self[i_index], prop) != value:
|
||||
|
@ -301,7 +323,7 @@ class ListModel(QAbstractListModel):
|
|||
@pyqtSlot(str, int, int)
|
||||
def move(self, from_: Index, to: int, n: int = 1) -> None:
|
||||
# pylint: disable=invalid-name
|
||||
i_from: int = self.indexWhere(self.mainKey, from_) \
|
||||
i_from: int = self.indexWhere(from_) \
|
||||
if isinstance(from_, str) else from_
|
||||
|
||||
qlast = i_from + n - 1
|
||||
|
@ -332,7 +354,7 @@ class ListModel(QAbstractListModel):
|
|||
@pyqtSlot(int)
|
||||
@pyqtSlot(str)
|
||||
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
|
||||
|
||||
self.beginRemoveRows(QModelIndex(), i_index, i_index)
|
||||
|
@ -347,7 +369,7 @@ class ListModel(QAbstractListModel):
|
|||
@pyqtSlot(str, result="QVariant")
|
||||
def pop(self, index: Index, default: Any = _PopFail()) -> ListItem:
|
||||
try:
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
i_index: int = self.indexWhere(index) \
|
||||
if isinstance(index, str) else index
|
||||
item = self[i_index]
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Callable, DefaultDict, MutableSequence
|
||||
from typing import Any, DefaultDict
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
|
@ -6,15 +6,15 @@ from .list_model import ListModel
|
|||
|
||||
|
||||
class ListModelMap(QObject):
|
||||
def __init__(self,
|
||||
models_container: Callable[..., MutableSequence] = list,
|
||||
parent: QObject = None) -> None:
|
||||
def __init__(self, *models_args, parent: QObject = None, **models_kwargs
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
models_kwargs["parent"] = self
|
||||
|
||||
# Set the parent to prevent item garbage-collection on the C++ side
|
||||
self.dict: DefaultDict[Any, ListModel] = \
|
||||
DefaultDict(
|
||||
lambda: ListModel(container=models_container, parent=self)
|
||||
lambda: ListModel(*models_args, **models_kwargs)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import sys
|
|||
import time
|
||||
import traceback
|
||||
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
|
||||
|
||||
|
@ -15,10 +15,14 @@ from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
|||
class PyQtFuture(QObject):
|
||||
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)
|
||||
self.future = future
|
||||
self._result = None
|
||||
self.future = future
|
||||
self.running_value = running_value
|
||||
self._result = None
|
||||
|
||||
self.future.add_done_callback(
|
||||
lambda future: self.gotResult.emit(future.result())
|
||||
|
@ -64,7 +68,7 @@ class PyQtFuture(QObject):
|
|||
|
||||
@pyqtProperty("QVariant", notify=gotResult)
|
||||
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:
|
||||
|
@ -79,7 +83,8 @@ _PENDING: Deque[_Task] = Deque()
|
|||
def futurize(max_running: Optional[int] = None,
|
||||
consider_args: 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:
|
||||
|
||||
|
@ -145,7 +150,9 @@ def futurize(max_running: Optional[int] = None,
|
|||
del _RUNNING[_RUNNING.index(task)]
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Lock
|
||||
from typing import Any, Deque, Dict, List, Optional
|
||||
|
||||
|
@ -12,9 +13,10 @@ from nio.rooms import MatrixRoom
|
|||
from .backend import Backend
|
||||
from .client import Client
|
||||
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 .pyqt_future import futurize
|
||||
|
||||
Inviter = Optional[Dict[str, str]]
|
||||
LeftEvent = Optional[Dict[str, str]]
|
||||
|
@ -27,6 +29,8 @@ class SignalManager(QObject):
|
|||
|
||||
def __init__(self, backend: Backend) -> None:
|
||||
super().__init__(parent=backend)
|
||||
self.pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=6)
|
||||
|
||||
self.backend = backend
|
||||
|
||||
self.last_room_events: Deque[str] = Deque(maxlen=1000)
|
||||
|
@ -37,7 +41,23 @@ class SignalManager(QObject):
|
|||
|
||||
|
||||
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]] = [
|
||||
{"name": "Invites", "rooms": ListModel()},
|
||||
{"name": "Rooms", "rooms": ListModel()},
|
||||
|
@ -57,25 +77,31 @@ class SignalManager(QObject):
|
|||
roomCategories = ListModel([
|
||||
RoomCategory(**kws) for kws in room_categories_kwargs
|
||||
]),
|
||||
displayName = self.backend.getUserDisplayName(client.userId),
|
||||
))
|
||||
|
||||
# Upload our E2E keys to the matrix server if needed
|
||||
if not client.nio.olm_account_shared:
|
||||
client.uploadE2EKeys()
|
||||
|
||||
# Add our devices to the Backend.devices model
|
||||
# Add all devices nio knows for this account
|
||||
store = client.nio.device_store
|
||||
|
||||
for user_id in store.users:
|
||||
self.backend.devices[user_id].clear()
|
||||
self.backend.devices[user_id].extend([
|
||||
Device(
|
||||
deviceId = dev.id,
|
||||
ed25519Key = dev.ed25519,
|
||||
trust = client.getDeviceTrust(dev),
|
||||
) for dev in store.active_user_devices(user_id)
|
||||
])
|
||||
user = self.backend.users.get(user_id, None)
|
||||
if not user:
|
||||
self.backend.users.append(
|
||||
User(userId=user_id, devices=ListModel())
|
||||
)
|
||||
|
||||
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
|
||||
self.connectClient(client)
|
||||
|
@ -107,12 +133,32 @@ class SignalManager(QObject):
|
|||
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,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
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
|
||||
|
||||
previous_room = categories["Rooms"].rooms.pop(room_id, None)
|
||||
|
@ -126,9 +172,9 @@ class SignalManager(QObject):
|
|||
topic = nio_room.topic,
|
||||
inviter = inviter,
|
||||
lastEventDateTime = QDateTime.currentDateTime(), # FIXME
|
||||
members = list(nio_room.users.keys()),
|
||||
),
|
||||
new_index_if_insert = 0,
|
||||
ignore_roles = ("typingUsers"),
|
||||
ignore_roles = ("typingMembers"),
|
||||
)
|
||||
|
||||
signal = self.roomCategoryChanged
|
||||
|
@ -139,7 +185,9 @@ class SignalManager(QObject):
|
|||
|
||||
|
||||
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
|
||||
|
||||
previous_invite = categories["Invites"].rooms.pop(room_id, None)
|
||||
|
@ -151,9 +199,9 @@ class SignalManager(QObject):
|
|||
roomId = room_id,
|
||||
displayName = self._get_room_displayname(nio_room),
|
||||
topic = nio_room.topic,
|
||||
members = list(nio_room.users.keys()),
|
||||
),
|
||||
new_index_if_insert = 0,
|
||||
ignore_roles = ("typingUsers", "lastEventDateTime"),
|
||||
ignore_roles = ("typingMembers", "lastEventDateTime"),
|
||||
)
|
||||
|
||||
signal = self.roomCategoryChanged
|
||||
|
@ -188,8 +236,7 @@ class SignalManager(QObject):
|
|||
if left_time else QDateTime.currentDateTime()
|
||||
),
|
||||
),
|
||||
new_index_if_insert = 0,
|
||||
ignore_roles = ("typingUsers", "lastEventDateTime"),
|
||||
ignore_roles = ("members", "lastEventDateTime"),
|
||||
)
|
||||
|
||||
signal = self.roomCategoryChanged
|
||||
|
@ -304,14 +351,14 @@ class SignalManager(QObject):
|
|||
self._set_room_last_event(client.userId, room_id, new_event)
|
||||
|
||||
|
||||
def onRoomTypingUsersUpdated(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
users: List[str]) -> None:
|
||||
def onRoomTypingMembersUpdated(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
users: List[str]) -> None:
|
||||
categories = self.backend.accounts[client.userId].roomCategories
|
||||
for categ in categories:
|
||||
try:
|
||||
categ.rooms.setProperty(room_id, "typingUsers", users)
|
||||
categ.rooms.setProperty(room_id, "typingMembers", users)
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
@ -355,9 +402,16 @@ class SignalManager(QObject):
|
|||
user_id: str,
|
||||
device_id: str,
|
||||
ed25519_key: str) -> None:
|
||||
|
||||
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,
|
||||
update_with = Device(
|
||||
deviceId = device_id,
|
||||
|
@ -369,4 +423,7 @@ class SignalManager(QObject):
|
|||
|
||||
def onDeviceIsDeleted(self, _: Client, user_id: str, device_id: str
|
||||
) -> None:
|
||||
self.backend.devices[user_id].pop(device_id, None)
|
||||
try:
|
||||
del self.backend.users[user_id].devices[device_id]
|
||||
except ValueError:
|
||||
pass
|
||||
|
|
|
@ -2,16 +2,10 @@ import QtQuick 2.7
|
|||
import "../Base"
|
||||
|
||||
Rectangle {
|
||||
property bool hidden: false
|
||||
property var name: null // null, string or PyQtFuture
|
||||
property var imageSource: null
|
||||
property var name: null
|
||||
property var imageUrl: null
|
||||
property int dimension: 36
|
||||
|
||||
|
||||
readonly property string resolvedName:
|
||||
! name ? "?" :
|
||||
typeof(name) == "string" ? name :
|
||||
(name.value ? name.value : "?")
|
||||
property bool hidden: false
|
||||
|
||||
width: dimension
|
||||
height: hidden ? 1 : dimension
|
||||
|
@ -20,21 +14,21 @@ Rectangle {
|
|||
|
||||
opacity: hidden ? 0 : 1
|
||||
|
||||
color: resolvedName === "?" ?
|
||||
HStyle.avatar.background.unknown :
|
||||
color: name ?
|
||||
Qt.hsla(
|
||||
Backend.hueFromString(resolvedName),
|
||||
Backend.hueFromString(name),
|
||||
HStyle.avatar.background.saturation,
|
||||
HStyle.avatar.background.lightness,
|
||||
HStyle.avatar.background.alpha
|
||||
)
|
||||
) :
|
||||
HStyle.avatar.background.unknown
|
||||
|
||||
HLabel {
|
||||
z: 1
|
||||
anchors.centerIn: parent
|
||||
visible: ! hidden
|
||||
|
||||
text: resolvedName.charAt(0)
|
||||
text: name ? name.charAt(0) : "?"
|
||||
color: HStyle.avatar.letter
|
||||
font.pixelSize: parent.height / 1.4
|
||||
}
|
||||
|
@ -42,9 +36,9 @@ Rectangle {
|
|||
HImage {
|
||||
z: 2
|
||||
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
|
||||
sourceSize.width: dimension
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ QtObject {
|
|||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject typingUsers: QtObject {
|
||||
readonly property QtObject typingMembers: QtObject {
|
||||
property color background: colors.background0
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ Banner {
|
|||
color: HStyle.chat.inviteBanner.background
|
||||
|
||||
avatar.name: inviter ? inviter.displayname : ""
|
||||
//avatar.imageSource: inviter ? inviter.avatar_url : ""
|
||||
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
||||
|
||||
labelText:
|
||||
(inviter ?
|
||||
|
|
|
@ -14,8 +14,11 @@ HColumnLayout {
|
|||
.roomCategories.get(category)
|
||||
.rooms.get(roomId)
|
||||
|
||||
readonly property var sender: Backend.users.get(userId)
|
||||
|
||||
readonly property bool hasUnknownDevices:
|
||||
Backend.clients.get(userId).roomHasUnknownDevices(roomId)
|
||||
category == "Rooms" ?
|
||||
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
|
||||
|
||||
id: chatPage
|
||||
onFocusChanged: sendBox.setFocus()
|
||||
|
@ -38,7 +41,7 @@ HColumnLayout {
|
|||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
TypingUsersBar {}
|
||||
TypingMembersBar {}
|
||||
|
||||
InviteBanner {
|
||||
visible: category === "Invites"
|
||||
|
@ -46,12 +49,12 @@ HColumnLayout {
|
|||
}
|
||||
|
||||
UnknownDevicesBanner {
|
||||
visible: category === "Rooms" && hasUnknownDevices
|
||||
visible: category == "Rooms" && hasUnknownDevices
|
||||
}
|
||||
|
||||
SendBox {
|
||||
id: sendBox
|
||||
visible: category === "Rooms" && ! hasUnknownDevices
|
||||
visible: category == "Rooms" && ! hasUnknownDevices
|
||||
}
|
||||
|
||||
LeftBanner {
|
||||
|
|
|
@ -16,7 +16,7 @@ Row {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: displayName
|
||||
name: sender.displayName.value
|
||||
hidden: combine
|
||||
dimension: 28
|
||||
}
|
||||
|
@ -26,12 +26,12 @@ Row {
|
|||
|
||||
id: contentLabel
|
||||
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.lightness,
|
||||
1) +
|
||||
"'>" +
|
||||
(displayName.value || dict.sender) + " " +
|
||||
sender.displayName.value + " " +
|
||||
ChatJS.getEventText(type, dict) +
|
||||
|
||||
" " +
|
||||
|
|
|
@ -10,7 +10,7 @@ Row {
|
|||
HAvatar {
|
||||
id: avatar
|
||||
hidden: combine
|
||||
name: displayName
|
||||
name: sender.displayName.value
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -37,7 +37,7 @@ Row {
|
|||
visible: height > 0
|
||||
|
||||
id: nameLabel
|
||||
text: displayName.value || dict.sender
|
||||
text: sender.displayName.value
|
||||
color: Qt.hsla(Backend.hueFromString(text),
|
||||
HStyle.displayName.saturation,
|
||||
HStyle.displayName.lightness,
|
||||
|
|
|
@ -26,8 +26,7 @@ Column {
|
|||
readonly property bool isUndecryptableEvent:
|
||||
type === "OlmEvent" || type === "MegolmEvent"
|
||||
|
||||
readonly property var displayName:
|
||||
Backend.getUserDisplayName(dict.sender)
|
||||
readonly property var sender: Backend.users.get(dict.sender)
|
||||
|
||||
readonly property bool isOwn:
|
||||
chatPage.userId === dict.sender
|
||||
|
|
|
@ -18,7 +18,7 @@ HGlassRectangle {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: Backend.getUserDisplayName(chatPage.userId)
|
||||
name: chatPage.sender.displayName.value
|
||||
dimension: root.Layout.minimumHeight
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import "../Base"
|
|||
import "utils.js" as ChatJS
|
||||
|
||||
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.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0
|
||||
|
@ -16,7 +16,7 @@ HGlassRectangle {
|
|||
id: usersLabel
|
||||
anchors.fill: parent
|
||||
|
||||
text: ChatJS.getTypingUsersText(typingUsers, chatPage.userId)
|
||||
text: ChatJS.getTypingMembersText(typingMembers, chatPage.userId)
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
|
@ -84,9 +84,7 @@ function getHistoryVisibilityEventText(dict) {
|
|||
function getStateDisplayName(dict) {
|
||||
// The dict.content.displayname may be outdated, prefer
|
||||
// retrieving it fresh
|
||||
var name = Backend.getUserDisplayName(dict.state_key, false)
|
||||
return name === dict.state_key ?
|
||||
dict.content.displayname : name.result()
|
||||
return Backend.users.get(dict.state_key).displayName.value
|
||||
}
|
||||
|
||||
|
||||
|
@ -168,7 +166,7 @@ function getLeftBannerText(leftEvent) {
|
|||
|
||||
if (info.membership)
|
||||
|
||||
var name = Backend.getUserDisplayName(leftEvent.sender, false).result()
|
||||
var name = Backend.users.get(leftEvent.sender).displayName.value
|
||||
|
||||
return "<b>" + name + "</b> " +
|
||||
(info.membership == "ban" ?
|
||||
|
@ -187,19 +185,19 @@ function getLeftBannerText(leftEvent) {
|
|||
|
||||
function getLeftBannerAvatarName(leftEvent, accountId) {
|
||||
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 = []
|
||||
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
if (users[i] !== ourAccountId) {
|
||||
names.push(Backend.getUserDisplayName(users[i], false).result())
|
||||
names.push(Backend.users.get(users[i]).displayName.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ Column {
|
|||
id: accountDelegate
|
||||
width: parent.width
|
||||
|
||||
property var user: Backend.users.get(userId)
|
||||
|
||||
property string roomCategoriesListUserId: userId
|
||||
property bool expanded: true
|
||||
|
||||
|
@ -16,7 +18,7 @@ Column {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: displayName
|
||||
name: user.displayName.value
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
|
@ -25,7 +27,7 @@ Column {
|
|||
|
||||
HLabel {
|
||||
id: accountLabel
|
||||
text: displayName.value || userId
|
||||
text: user.displayName.value
|
||||
elide: HLabel.ElideRight
|
||||
maximumLineCount: 1
|
||||
Layout.fillWidth: true
|
||||
|
@ -35,7 +37,7 @@ Column {
|
|||
|
||||
HTextField {
|
||||
id: statusEdit
|
||||
text: statusMessage || ""
|
||||
text: user.statusMessage || ""
|
||||
placeholderText: qsTr("Set status message")
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
background: null
|
||||
|
|
|
@ -6,7 +6,8 @@ function getLastRoomEventText(roomId, accountId) {
|
|||
if (eventsModel.count < 1) { return "" }
|
||||
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"
|
||||
|
||||
if (undecryptable || ev.type.startsWith("RoomMessage")) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user