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:
parent
11d900965a
commit
1d0cce402e
4
Makefile
4
Makefile
|
@ -6,11 +6,13 @@ PKG_DIR = harmonyqml
|
|||
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}
|
||||
|
|
5
TODO.md
5
TODO.md
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
79
harmonyqml/backend/pyqt_future.py
Normal file
79
harmonyqml/backend/pyqt_future.py
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 +
|
||||
" <font size=" + smallSize + "px color='gray'>" +
|
||||
Qt.formatDateTime(date_time, "hh:mm:ss") +
|
||||
"</font></font>"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user