Reorganize backend files, show accounts in UI

This commit is contained in:
miruka 2019-04-12 04:33:09 -04:00
parent 4f9a47027c
commit 5d4c7b8520
22 changed files with 214 additions and 238 deletions

View File

@ -1,5 +1,4 @@
# 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 .backend import Backend
from .dummy import DummyBackend

View File

@ -0,0 +1,42 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
import hashlib
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
from .client_manager import ClientManager
from .model.qml_models import QMLModels
class Backend(QObject):
def __init__(self) -> None:
super().__init__()
self._client_manager: ClientManager = ClientManager()
self._models: QMLModels = QMLModels()
from .signal_manager import SignalManager
self._signal_manager: SignalManager = SignalManager(self)
# a = self._client_manager; m = self._models
# from PyQt5.QtCore import pyqtRemoveInputHook as PRI
# import pdb; PRI(); pdb.set_trace()
self.clientManager.configLoad()
@pyqtProperty("QVariant", constant=True)
def clientManager(self):
return self._client_manager
@pyqtProperty("QVariant", constant=True)
def models(self):
return self._models
@pyqtSlot(str, result=float)
def hueFromString(self, string: str) -> float:
# pylint:disable=no-self-use
md5 = hashlib.md5(bytes(string, "utf-8")).hexdigest()
return float("0.%s" % int(md5[-10:], 16))

View File

@ -1,109 +0,0 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
import hashlib
from typing import Any, DefaultDict, Dict, NamedTuple, Optional
from PyQt5.QtCore import QDateTime, QObject, pyqtProperty, pyqtSlot
from .enums import Activity, MessageKind, Presence
from .list_model import ListModel, _QtListModel
class User(NamedTuple):
user_id: str
display_name: str
avatar_url: Optional[str] = None
status_message: Optional[str] = None
class Room(NamedTuple):
room_id: str
display_name: str
description: str = ""
unread_messages: int = 0
presence: Presence = Presence.none
activity: Activity = Activity.none
last_activity_timestamp_ms: Optional[int] = None
avatar_url: Optional[str] = None
class Message(NamedTuple):
sender_id: str
date_time: QDateTime
content: str
kind: MessageKind = MessageKind.text
sender_avatar: Optional[str] = None
class Backend(QObject):
def __init__(self) -> None:
super().__init__()
self._known_users: Dict[str, User] = {}
self.accounts: ListModel = ListModel()
self.rooms: DefaultDict[str, ListModel] = DefaultDict(ListModel)
self.messages: DefaultDict[str, ListModel] = DefaultDict(ListModel)
@pyqtProperty(_QtListModel, constant=True)
def accountsModel(self) -> _QtListModel:
return self.accounts.qt_model
@pyqtProperty("QVariantMap", constant=True)
def roomsModel(self) -> Dict[str, _QtListModel]:
return {account_id: l.qt_model for account_id, l in self.rooms.items()}
@pyqtProperty("QVariantMap", constant=True)
def messagesModel(self) -> Dict[str, _QtListModel]:
return {room_id: l.qt_model for room_id, l in self.messages.items()}
@pyqtSlot(str, str, str)
def sendMessage(self, sender_id: str, room_id: str, markdown: str) -> None:
self.localEcho(sender_id, room_id, markdown)
self.sendToServer(sender_id, room_id, markdown)
def localEcho(self, sender_id: str, room_id: str, html: str) -> None:
self.messages[room_id].append(Message(
sender_id, QDateTime.currentDateTime(), html,
))
def sendToServer(self, sender_id: str, room_id: str, html: str) -> None:
pass
@pyqtSlot(str, result="QVariantMap")
def getUser(self, user_id: str) -> Dict[str, Any]:
for user in self.accounts:
if user.user_id == user_id:
return user._asdict()
try:
return self._known_users[user_id]._asdict()
except KeyError:
name = user_id.lstrip("@").split(":")[0].capitalize()
user = User(user_id, name)
self._known_users[user_id] = user
return user._asdict()
@pyqtSlot(str, result=float)
def hueFromString(self, string: str) -> float:
# pylint: disable=no-self-use
md5 = hashlib.md5(bytes(string, "utf-8")).hexdigest()
return float("0.%s" % int(md5[-10:], 16))
@pyqtSlot(str, str)
def setStatusMessage(self, user_id: str, to: str) -> None:
for user in self.accounts:
if user.user_id == user_id:
user.status_message = to
break
else:
raise ValueError(f"{user_id} not found in Backend.accounts")

View File

@ -4,13 +4,15 @@
import functools import functools
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import Future, ThreadPoolExecutor
from threading import Event from threading import Event
from typing import Callable, DefaultDict from typing import Callable, DefaultDict, Dict
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
import nio import nio
import nio.responses as nr import nio.responses as nr
from .model.items import User
# One pool per hostname/remote server; # One pool per hostname/remote server;
# multiple Client for different accounts on the same server can exist. # multiple Client for different accounts on the same server can exist.
_POOLS: DefaultDict[str, ThreadPoolExecutor] = \ _POOLS: DefaultDict[str, ThreadPoolExecutor] = \
@ -38,7 +40,7 @@ class Client(QObject):
self.pool: ThreadPoolExecutor = _POOLS[self.host] self.pool: ThreadPoolExecutor = _POOLS[self.host]
from .net import NetworkManager from .network_manager import NetworkManager
self.net: NetworkManager = NetworkManager(self) self.net: NetworkManager = NetworkManager(self)
self._stop_sync: Event = Event() self._stop_sync: Event = Event()
@ -75,11 +77,25 @@ class Client(QObject):
self.net.write(self.nio.disconnect()) self.net.write(self.nio.disconnect())
@pyqtSlot()
@futurize @futurize
def startSyncing(self) -> None: def startSyncing(self) -> None:
while True: while True:
print(self, self.net.talk(self.nio.sync, timeout=10)) self.net.talk(self.nio.sync, timeout=10)
if self._stop_sync.is_set(): if self._stop_sync.is_set():
self._stop_sync.clear() self._stop_sync.clear()
break break
@pyqtSlot(str, str, result="QVariantMap")
def getUser(self, room_id: str, user_id: str) -> Dict[str, str]:
try:
name = self.nio.rooms[room_id].user_name(user_id)
except KeyError:
name = None
return User(
user_id = user_id,
display_name = name or user_id,
)._asdict()

View File

@ -6,11 +6,12 @@ import json
import os import os
import platform import platform
import threading import threading
from concurrent.futures import Future
from typing import Dict, Optional from typing import Dict, Optional
from atomicfile import AtomicFile from atomicfile import AtomicFile
from PyQt5.QtCore import QObject, QStandardPaths, pyqtProperty, pyqtSlot from PyQt5.QtCore import (
QObject, QStandardPaths, pyqtProperty, pyqtSignal, pyqtSlot
)
from harmonyqml import __about__ from harmonyqml import __about__
@ -23,6 +24,10 @@ _CONFIG_LOCK = threading.Lock()
class ClientManager(QObject): class ClientManager(QObject):
clientAdded = pyqtSignal(Client)
clientDeleted = pyqtSignal(str)
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._clients: Dict[str, Client] = {} self._clients: Dict[str, Client] = {}
@ -32,7 +37,7 @@ class ClientManager(QObject):
return f"{type(self).__name__}(clients={self.clients!r})" return f"{type(self).__name__}(clients={self.clients!r})"
@pyqtProperty("QVariantMap") @pyqtProperty("QVariantMap", constant=True)
def clients(self): def clients(self):
return self._clients return self._clients
@ -40,13 +45,9 @@ class ClientManager(QObject):
@pyqtSlot() @pyqtSlot()
def configLoad(self) -> None: def configLoad(self) -> None:
for user_id, info in self.configAccounts().items(): for user_id, info in self.configAccounts().items():
cli = Client(info["hostname"], user_id) client = Client(info["hostname"], user_id)
client.resumeSession(user_id, info["token"], info["device_id"])\
def on_done(_: Future, cli=cli) -> None: .add_done_callback(lambda _, c=client: self._on_connected(c))
self._clients[cli.nio.user_id] = cli
cli.resumeSession(user_id, info["token"], info["device_id"])\
.add_done_callback(on_done)
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
@ -54,22 +55,25 @@ class ClientManager(QObject):
def new(self, hostname: str, username: str, password: str, def new(self, hostname: str, username: str, password: str,
device_id: str = "") -> None: device_id: str = "") -> None:
cli = Client(hostname, username, device_id) client = Client(hostname, username, device_id)
client.login(password, self.defaultDeviceName)\
.add_done_callback(lambda _: self._on_connected(client))
def on_done(_: Future, cli=cli) -> None:
self._clients[cli.nio.user_id] = cli
cli.login(password, self.defaultDeviceName).add_done_callback(on_done) def _on_connected(self, client: Client) -> None:
self.clients[client.nio.user_id] = client
self.clientAdded.emit(client)
@pyqtSlot(str) @pyqtSlot(str)
def delete(self, user_id: str) -> None: def delete(self, user_id: str) -> None:
client = self._clients.pop(user_id, None) client = self.clients.pop(user_id, None)
if client: if client:
self.clientDeleted.emit(user_id)
client.logout() client.logout()
@pyqtProperty(str) @pyqtProperty(str, constant=True)
def defaultDeviceName(self) -> str: # pylint: disable=no-self-use def defaultDeviceName(self) -> str: # pylint: disable=no-self-use
os_ = f" on {platform.system()}".rstrip() os_ = f" on {platform.system()}".rstrip()
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else "" os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""

View File

@ -1,64 +0,0 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from PyQt5.QtCore import QDateTime, Qt
from .base import Backend, Message, Room, User
class DummyBackend(Backend):
def __init__(self) -> None:
super().__init__()
dt = lambda t: QDateTime.fromString(f"2019-03-19T{t}.123",
Qt.ISODateWithMs)
db = lambda t: QDateTime.fromString(f"2019-03-20T{t}.456",
Qt.ISODateWithMs)
self.accounts.extend([
User("@renko:matrix.org", "Renko", None, "Sleeping, zzz..."),
User("@mary:matrix.org", "Mary"),
])
self.rooms["@renko:matrix.org"].extend([
Room("!test:matrix.org", "Test", "Test room"),
Room("!mary:matrix.org", "Mary",
"Lorem ipsum sit dolor amet this is a long text to test "
"wrapping of room subtitle etc 1234 example foo bar abc", 2),
Room("!foo:matrix.org", "Another room"),
])
self.rooms["@mary:matrix.org"].extend([
Room("!test:matrix.org", "Test", "Test room"),
Room("!mary:matrix.org", "Renko", "Lorem ipsum sit dolor amet"),
])
self.messages["!test:matrix.org"].extend([
Message("@renko:matrix.org", dt("10:20:13"), "Lorem"),
Message("@renko:matrix.org", dt("10:22:01"), "Ipsum"),
Message("@renko:matrix.org", dt("10:22:50"), "Combine"),
Message("@renko:matrix.org", dt("10:30:41"),
"Time passed, don't combine"),
Message("@mary:matrix.org", dt("10:31:12"),
"Different person, don't combine"),
Message("@mary:matrix.org", dt("10:32:04"),
"But combine me"),
Message("@mary:matrix.org", dt("13:10:20"),
"Long time passed, conv break"),
Message("@renko:matrix.org", db("10:22:01"), "Daybreak"),
Message("@mary:matrix.org", db("10:22:03"),
"A longer message to test text wrapping. "
"Lorem ipsum dolor sit amet, consectetuer adipiscing "
"elit. Aenean commodo ligula "
"eget dolor. Aenean massa. Cem sociis natoque penaibs "
"et magnis dis parturient montes, nascetur ridiculus "
"mus. Donec quam. "),
])
self.messages["!mary:matrix.org"].extend([
Message("@mary:matrix.org", dt("10:22:23"), "First"),
Message("@mary:matrix.org", dt("12:24:10"), "Second"),
])
self.messages["!foo:matrix.org"].extend([])

View File

@ -1,4 +0,0 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from .backend import MatrixNioBackend

View File

@ -1,27 +0,0 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from typing import Any, DefaultDict, Dict, NamedTuple, Optional
from PyQt5.QtCore import QDateTime, QObject, pyqtProperty, pyqtSlot
from matrix_client.user import User as MatrixUser
from ..base import Backend, User
from .client_manager import ClientManager
class MatrixNioBackend(Backend):
def __init__(self) -> None:
super().__init__()
self._client_manager = ClientManager()
# a = self._client_manager
# from PyQt5.QtCore import pyqtRemoveInputHook as PRI; import pdb; PRI(); pdb.set_trace()
self._client_manager.configLoad()
@pyqtProperty("QVariant")
def clientManager(self):
return self._client_manager

View File

@ -0,0 +1,6 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from .list_model import ListModel, _QtListModel
from .qml_models import QMLModels
from . import enums, items

View File

@ -0,0 +1,34 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from typing import NamedTuple, Optional
from PyQt5.QtCore import QDateTime
from .enums import Activity, MessageKind, Presence
class User(NamedTuple):
user_id: str
display_name: str
avatar_url: Optional[str] = None
status_message: Optional[str] = None
class Room(NamedTuple):
room_id: str
display_name: str
description: str = ""
unread_messages: int = 0
presence: Presence = Presence.none
activity: Activity = Activity.none
last_activity_timestamp_ms: Optional[int] = None
avatar_url: Optional[str] = None
class Message(NamedTuple):
sender_id: str
date_time: QDateTime
content: str
kind: MessageKind = MessageKind.text
sender_avatar: Optional[str] = None

View File

@ -65,6 +65,16 @@ class _QtListModel(QAbstractListModel):
return self._list[index]._asdict() return self._list[index]._asdict()
@pyqtSlot(str, "QVariant", result=int)
def indexWhere(self, prop: str, is_value: Any) -> int:
for i, item in enumerate(self._list):
if getattr(item, prop) == is_value:
return i
raise ValueError(f"No {type(self._ref_namedlist)} in list with "
f"property {prop!r} set to {is_value!r}.")
@pyqtSlot(int, list) @pyqtSlot(int, list)
def insert(self, index: int, value: NewValue) -> None: def insert(self, index: int, value: NewValue) -> None:
value = self._convert_new_value(value) value = self._convert_new_value(value)
@ -176,6 +186,10 @@ class ListModel(MutableSequence):
self.qt_model.insert(index, value) self.qt_model.insert(index, value)
def indexWhere(self, prop: str, is_value: Any) -> int:
return self.qt_model.indexWhere(prop, is_value)
def setProperty(self, index: int, prop: str, value: Any) -> None: def setProperty(self, index: int, prop: str, value: Any) -> None:
"Set role of the item at *index* to *value*." "Set role of the item at *index* to *value*."
self.qt_model.setProperty(index, prop, value) self.qt_model.setProperty(index, prop, value)

View File

@ -0,0 +1,31 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from typing import DefaultDict, Dict
from PyQt5.QtCore import QObject, pyqtProperty
from .list_model import ListModel, _QtListModel
class QMLModels(QObject):
def __init__(self) -> None:
super().__init__()
self._accounts: ListModel = ListModel()
self._rooms: DefaultDict[str, ListModel] = DefaultDict(ListModel)
self._messages: DefaultDict[str, ListModel] = DefaultDict(ListModel)
@pyqtProperty(_QtListModel, constant=True)
def accounts(self) -> _QtListModel:
return self._accounts.qt_model
@pyqtProperty("QVariantMap", constant=True)
def rooms(self) -> Dict[str, _QtListModel]:
return {user_id: l.qt_model for user_id, l in self._rooms.items()}
@pyqtProperty("QVariantMap", constant=True)
def messages(self) -> Dict[str, _QtListModel]:
return {room_id: l.qt_model for room_id, l in self._messages.items()}

View File

@ -0,0 +1,33 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from PyQt5.QtCore import QObject
from .backend import Backend
from .client import Client
from .model.items import User
class SignalManager(QObject):
def __init__(self, backend: Backend) -> None:
super().__init__()
self.backend = backend
self.connectAll()
def connectAll(self) -> None:
be = self.backend
be.clientManager.clientAdded.connect(self.onClientAdded)
be.clientManager.clientDeleted.connect(self.onClientDeleted)
def onClientAdded(self, client: Client) -> None:
self.backend.models.accounts.append(User(
user_id = client.nio.user_id,
display_name = client.nio.user_id.lstrip("@").split(":")[0],
))
def onClientDeleted(self, user_id: str) -> None:
accs = self.backend.models.accounts
del accs[accs.indexWhere("user_id", user_id)]

View File

@ -11,7 +11,7 @@ Column {
} }
readonly property string displayName: readonly property string displayName:
Backend.getUser(sender_id).display_name Backend.getUser(chatPage.room.room_id, sender_id).display_name
readonly property bool isOwn: readonly property bool isOwn:
chatPage.user.user_id === sender_id chatPage.user.user_id === sender_id

View File

@ -13,7 +13,7 @@ Rectangle {
ListView { ListView {
id: messageListView id: messageListView
anchors.fill: parent anchors.fill: parent
model: Backend.messagesModel[chatPage.room.room_id] model: Backend.models.messages[chatPage.room.room_id]
delegate: MessageDelegate {} delegate: MessageDelegate {}
//highlight: Rectangle {color: "lightsteelblue"; radius: 5} //highlight: Rectangle {color: "lightsteelblue"; radius: 5}

View File

@ -67,7 +67,7 @@ ColumnLayout {
id: "roomList" id: "roomList"
visible: true visible: true
interactive: false // no scrolling interactive: false // no scrolling
user: Backend.getUser(user_id) for_user_id: user_id
Layout.minimumHeight: Layout.minimumHeight:
roomList.visible ? roomList.visible ?

View File

@ -5,7 +5,7 @@ import QtQuick.Layouts 1.4
ListView { ListView {
id: "accountList" id: "accountList"
spacing: 8 spacing: 8
model: Backend.accountsModel model: Backend.models.accounts
delegate: AccountDelegate {} delegate: AccountDelegate {}
clip: true clip: true
} }

View File

@ -9,7 +9,7 @@ MouseArea {
height: Math.max(roomLabel.height + subtitleLabel.height, avatar.height) height: Math.max(roomLabel.height + subtitleLabel.height, avatar.height)
onClicked: pageStack.show_room( onClicked: pageStack.show_room(
roomList.user, roomList.user_id,
roomList.model.get(index) roomList.model.get(index)
) )
@ -38,22 +38,23 @@ MouseArea {
} }
Base.HLabel { Base.HLabel {
function get_text() { function get_text() {
var msgs = Backend.messagesModel[room_id] var msgs = Backend.models.messages[room_id]
if (msgs.count < 1) { return "" } if (msgs.count < 1) { return "" }
var msg = msgs.get(-1) var msg = msgs.get(-1)
var color_ = (msg.sender_id === roomList.user.user_id ? var color_ = (msg.sender_id === roomList.user_id ?
"darkblue" : "purple") "darkblue" : "purple")
var client = Backend.clientManager.clients[RoomList.for_user_id]
return "<font color=\"" + color_ + "\">" + return "<font color=\"" + color_ + "\">" +
Backend.getUser(msg.sender_id).display_name + client.getUser(room_id, msg.sender_id).display_name +
":</font> " + ":</font> " +
msg.content msg.content
} }
id: subtitleLabel id: subtitleLabel
visible: text !== "" visible: text !== ""
text: Backend.messagesModel[room_id].reloadThis, get_text() text: Backend.models.messages[room_id].reloadThis, get_text()
textFormat: Text.StyledText textFormat: Text.StyledText
font.pixelSize: smallSize font.pixelSize: smallSize

View File

@ -4,7 +4,7 @@ import QtQuick.Layouts 1.4
import "../base" as Base import "../base" as Base
ListView { ListView {
property var user: null property var for_user_id: null
property int contentHeight: 0 property int contentHeight: 0
@ -21,6 +21,6 @@ ListView {
id: "roomList" id: "roomList"
spacing: 8 spacing: 8
model: Backend.roomsModel[user.user_id] model: Backend.models.rooms[for_user_id]
delegate: RoomDelegate {} delegate: RoomDelegate {}
} }

View File

@ -11,7 +11,7 @@ from PyQt5.QtQml import QQmlApplicationEngine
from .__about__ import __doc__ from .__about__ import __doc__
from .app import Application from .app import Application
from .backend.matrix_nio.backend import MatrixNioBackend as CurrentBackend from .backend.backend import Backend
# logging.basicConfig(level=logging.INFO) # logging.basicConfig(level=logging.INFO)
@ -23,7 +23,7 @@ class Engine(QQmlApplicationEngine):
parent: Optional[QObject] = None) -> None: parent: Optional[QObject] = None) -> None:
super().__init__(parent) super().__init__(parent)
self.app = app self.app = app
self.backend = CurrentBackend() self.backend = Backend()
self.app_dir = Path(sys.argv[0]).resolve().parent self.app_dir = Path(sys.argv[0]).resolve().parent
# Set QML properties # Set QML properties