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
- 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

View File

@ -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()
@ -29,8 +30,16 @@ class Backend(QObject):
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._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)
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()))
@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)
displayname = getattr(response, "displayname", "") or user_id
return response.displayname or user_id
except NioErrorResponse:
return 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"]

View File

@ -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)

View File

@ -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 (

View File

@ -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
@ -13,19 +14,13 @@ class Account(ListItem):
userId: str = ""
roomCategories: ListModel = ListModel()
displayName: Optional[str] = None
avatarUrl: Optional[str] = None
statusMessage: Optional[str] = None
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
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

View File

@ -154,28 +154,19 @@ 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)" % (
return "\033[35m%s\033[0m(\n%s\n)" % (
type(self).__name__,
textwrap.indent(",\n".join(prop_strings), prefix=" " * 4)
)
return "%s(%s)" % (type(self).__name__, ", ".join(prop_strings))
@pyqtSlot(result=str)
def repr(self) -> str:

View File

@ -32,19 +32,22 @@ class ListModel(QAbstractListModel):
def __init__(self,
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) \
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]

View File

@ -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)
)

View File

@ -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,9 +15,13 @@ 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.running_value = running_value
self._result = None
self.future.add_done_callback(
@ -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

View File

@ -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]
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
@ -140,6 +186,8 @@ class SignalManager(QObject):
def onRoomJoined(self, client: Client, room_id: str) -> None:
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,
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

View File

@ -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
}

View File

@ -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
}

View File

@ -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 ?

View File

@ -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 {

View File

@ -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) +
"&nbsp;&nbsp;" +

View File

@ -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,

View File

@ -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

View File

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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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")) {