Proper display name retrieval implementation

For any name not found in rooms data, rely on new
nio.HttpClient.get_displayname() function to get and cache it,
e.g. for our own name if no room is joined and past events from users
who left the room.

@futurize now returns PyQtFuture objects, wrapper for the
concurrent.futures.Future objects that can be used from QML,
to ensure name retrieval does not block the GUI.
This commit is contained in:
miruka 2019-04-19 02:07:01 -04:00
parent 11d900965a
commit 1d0cce402e
16 changed files with 146 additions and 58 deletions

View File

@ -3,14 +3,16 @@
PKG_DIR = harmonyqml
PYTHON = python3
PIP = pip3
PYLINT = pylint
CLOC = cloc
PYTHON = python3
PIP = pip3
PYLINT = pylint
VULTURE = vulture
CLOC = cloc
ARCHIVE_FORMATS = gztar
INSTALL_FLAGS = --user --editable
PYLINT_FLAGS = --output-format colorized
VULTURE_FLAGS = --min-confidence 100
CLOC_FLAGS = --ignore-whitespace
.PHONY: all clean dist install upload test
@ -45,4 +47,6 @@ upload: dist
test:
- ${PYLINT} ${PYLINT_FLAGS} ${PKG_DIR} *.py
@echo
- ${VULTURE} ${PKG_DIR} ${VULTURE_FLAGS}
@echo
${CLOC} ${CLOC_FLAGS} ${PKG_DIR}

View File

@ -2,7 +2,6 @@
- Invited → Accept/Deny dialog
- Keep the room header name and topic updated
- Merge login page
- Show actual display name for AccountDelegate
- When inviting someone to direct chat, room is "Empty room" until accepted,
it should be the peer's display name instead.
@ -19,8 +18,6 @@
- Migrate more JS functions to their own files
- Accept room\_id arg for getUser
- Set Qt parents for all QObject
- `<pre>` scrollbar on overflow
@ -39,3 +36,5 @@
- ![A picture](https://picsum.photos/256/256) not clickable?
- On sync, check messages API, if a limited sync timeline was received
- Graphic bug when resizing window vertically for side pane?

View File

@ -2,18 +2,23 @@
# This file is part of harmonyqml, licensed under GPLv3.
import hashlib
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
from .html_filter import HtmlFilter
from .model.items import User
from .model.qml_models import QMLModels
from .pyqt_future import futurize
class Backend(QObject):
def __init__(self) -> None:
super().__init__()
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()
@ -21,7 +26,6 @@ class Backend(QObject):
self._client_manager: ClientManager = ClientManager(self)
self._models: QMLModels = QMLModels()
self._html_filter: HtmlFilter = HtmlFilter()
# a = self._client_manager; m = self._models
from .signal_manager import SignalManager
self._signal_manager: SignalManager = SignalManager(self)
@ -42,16 +46,30 @@ class Backend(QObject):
return self._html_filter
@pyqtSlot(str, result="QVariantMap")
def getUser(self, user_id: str) -> Dict[str, str]:
@pyqtSlot(str, result="QVariant")
@pyqtSlot(str, bool, result="QVariant")
@futurize
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.clientManager.clients.values():
for room in client.nio.rooms.values():
displayname = room.user_name(user_id)
name = room.user_name(user_id)
if name:
return User(user_id=user_id, display_name=name)._asdict()
if displayname:
return displayname
return User(user_id=user_id, display_name=user_id)._asdict()
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.clientManager.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)

View File

@ -1,13 +1,9 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
import functools
import logging
import sys
import traceback
from concurrent.futures import Future, ThreadPoolExecutor
from threading import Event, currentThread
from typing import Callable, DefaultDict
from concurrent.futures import ThreadPoolExecutor
from threading import Event
from typing import DefaultDict
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
@ -15,6 +11,7 @@ import nio
import nio.responses as nr
from .network_manager import NetworkManager
from .pyqt_future import futurize
# One pool per hostname/remote server;
# multiple Client for different accounts on the same server can exist.
@ -22,22 +19,6 @@ _POOLS: DefaultDict[str, ThreadPoolExecutor] = \
DefaultDict(lambda: ThreadPoolExecutor(max_workers=6))
def futurize(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Future:
def run_and_catch_errs():
# Without this, exceptions are silently ignored
try:
func(*args, **kwargs)
except Exception:
traceback.print_exc()
logging.error("Exiting %s due to exception.", currentThread())
sys.exit(1)
return args[0].pool.submit(run_and_catch_errs) # args[0] = self
return wrapper
class Client(QObject):
roomInvited = pyqtSignal(str)
roomJoined = pyqtSignal(str)
@ -98,7 +79,6 @@ class Client(QObject):
self.net_sync.write(self.nio_sync.connect())
self.nio_sync.receive_response(response)
self.startSyncing()
@pyqtSlot(str, str, str)
@ -111,7 +91,6 @@ class Client(QObject):
self.net_sync.write(self.nio_sync.connect())
self.nio_sync.receive_response(response)
self.startSyncing()
@pyqtSlot()

View File

@ -65,6 +65,7 @@ class ClientManager(QObject):
def _on_connected(self, client: Client) -> None:
self.clients[client.userID] = client
self.clientAdded.emit(client)
client.startSyncing()
@pyqtSlot(str)

View File

@ -5,10 +5,12 @@ from typing import Dict, List, NamedTuple, Optional
from PyQt5.QtCore import QDateTime
from ..pyqt_future import PyQtFuture
class User(NamedTuple):
user_id: str
display_name: str
display_name: PyQtFuture
avatar_url: Optional[str] = None
status_message: Optional[str] = None

View File

@ -0,0 +1,79 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
import functools
import logging
import sys
import traceback
from concurrent.futures import Future
from threading import currentThread
from typing import Callable, Optional, Union
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
class PyQtFuture(QObject):
gotResult = pyqtSignal()
def __init__(self, future: Future, parent: QObject) -> None:
super().__init__(parent)
self.future = future
self._result = None
self.future.add_done_callback(lambda _: self.gotResult.emit())
def __repr__(self) -> str:
return "%s(%s)" % (type(self).__name__, repr(self.future))
@pyqtSlot()
def cancel(self):
self.future.cancel()
@pyqtProperty(bool)
def cancelled(self):
return self.future.cancelled()
@pyqtProperty(bool)
def running(self):
return self.future.running()
@pyqtProperty(bool)
def done(self):
return self.future.done()
@pyqtSlot(result="QVariant")
@pyqtSlot(int, result="QVariant")
@pyqtSlot(float, result="QVariant")
def result(self, timeout: Optional[Union[int, float]] = None):
return self.future.result(timeout)
@pyqtProperty("QVariant", notify=gotResult)
def value(self):
return self.future.result() if self.done else None
def add_done_callback(self, fn: Callable[[Future], None]) -> None:
self.future.add_done_callback(fn)
def futurize(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(self, *args, **kwargs) -> PyQtFuture:
def run_and_catch_errs():
# Without this, exceptions are silently ignored
try:
return func(self, *args, **kwargs)
except Exception:
traceback.print_exc()
logging.error("Exiting %s due to exception.", currentThread())
sys.exit(1)
return PyQtFuture(self.pool.submit(run_and_catch_errs), self)
return wrapper

View File

@ -32,7 +32,7 @@ class SignalManager(QObject):
self.connectClient(client)
self.backend.models.accounts.append(User(
user_id = client.userID,
display_name = client.userID.lstrip("@").split(":")[0],
display_name = self.backend.getUserDisplayName(client.userID),
))
@ -68,7 +68,7 @@ class SignalManager(QObject):
item = Room(
room_id = room_id,
display_name = room.name or room.canonical_alias or group_name(),
description = getattr(room, "topic", ""), # FIXME: outside init
description = room.topic,
)
try:

View File

@ -4,10 +4,15 @@ import QtQuick.Layouts 1.4
Item {
property bool invisible: false
property var name: null
property var name: null // null, string or PyQtFuture
property var imageSource: null
property int dimmension: 48
readonly property string resolved_name:
! name ? "?" :
typeof(name) == "string" ? name :
(name.value ? name.value : "?")
id: "root"
width: dimmension
height: invisible ? 1 : dimmension
@ -16,13 +21,13 @@ Item {
id: "letterRectangle"
anchors.fill: parent
visible: ! invisible && imageSource === null
color: name ?
Qt.hsla(Backend.hueFromString(name), 0.22, 0.5, 1) :
Qt.hsla(0, 0, 0.22, 1)
color: resolved_name === "?" ?
Qt.hsla(0, 0, 0.22, 1) :
Qt.hsla(Backend.hueFromString(resolved_name), 0.22, 0.5, 1)
HLabel {
anchors.centerIn: parent
text: name ? name.charAt(0) : "?"
text: resolved_name.charAt(0)
color: "white"
font.pixelSize: letterRectangle.height / 1.4
}

View File

@ -6,7 +6,7 @@ import "utils.js" as ChatJS
RowLayout {
id: row
spacing: standardSpacing
spacing: standardSpacing / 2
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
anchors.right: isOwn ? parent.right : undefined
@ -24,7 +24,7 @@ RowLayout {
id: contentLabel
text: "<font color='" +
(isUndecryptableEvent ? "darkred" : "gray") + "'>" +
displayName + " " + contentText +
(displayName.value || dict.sender) + " " + contentText +
"&nbsp;&nbsp;<font size=" + smallSize + "px color='gray'>" +
Qt.formatDateTime(date_time, "hh:mm:ss") +
"</font></font>"

View File

@ -17,7 +17,7 @@ Row {
Base.HLabel {
visible: ! combine
id: nameLabel
text: displayName
text: displayName.value || dict.sender
background: Rectangle {color: "#DDD"}
color: isOwn ? "teal" : "purple"
elide: Text.ElideRight

View File

@ -21,8 +21,8 @@ Column {
readonly property bool isUndecryptableEvent:
type === "OlmEvent" || type === "MegolmEvent"
readonly property string displayName:
Backend.getUser(dict.sender).display_name
readonly property var displayName:
Backend.getUserDisplayName(dict.sender)
readonly property bool isOwn:
chatPage.user_id === dict.sender

View File

@ -20,7 +20,7 @@ Rectangle {
Base.Avatar {
id: "avatar"
name: Backend.getUser(chatPage.user_id).display_name
name: Backend.getUserDisplayName(chatPage.user_id)
dimmension: root.Layout.minimumHeight
//visible: textArea.text === ""
visible: textArea.height <= root.Layout.minimumHeight

View File

@ -91,8 +91,9 @@ function get_member_event_text(dict) {
break
case "invite":
var name = Backend.getUser(dict.state_key).display_name
var name = name === dict.state_key ? info.displayname : name
var name = Backend.getUserDisplayName(dict.state_key, false)
var name = name === dict.state_key ?
info.displayname : name.result()
return "invited " + name + " to the room."
break

View File

@ -21,7 +21,7 @@ ColumnLayout {
Base.HLabel {
id: "accountLabel"
text: display_name
text: display_name.value || user_id
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true

View File

@ -15,7 +15,7 @@ function get_last_room_event_text(room_id) {
if (! found) { return "" }
var name = Backend.getUser(ev.dict.sender).display_name
var name = Backend.getUserDisplayName(ev.dict.sender, false).result()
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
if (undecryptable || ev.type.startsWith("RoomMessage")) {