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
# This file is part of harmonyqml, licensed under GPLv3.
from .dummy import DummyBackend
from .backend import Backend

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
from concurrent.futures import Future, ThreadPoolExecutor
from threading import Event
from typing import Callable, DefaultDict
from typing import Callable, DefaultDict, Dict
from PyQt5.QtCore import QObject, pyqtSlot
import nio
import nio.responses as nr
from .model.items import User
# One pool per hostname/remote server;
# multiple Client for different accounts on the same server can exist.
_POOLS: DefaultDict[str, ThreadPoolExecutor] = \
@ -38,7 +40,7 @@ class Client(QObject):
self.pool: ThreadPoolExecutor = _POOLS[self.host]
from .net import NetworkManager
from .network_manager import NetworkManager
self.net: NetworkManager = NetworkManager(self)
self._stop_sync: Event = Event()
@ -75,11 +77,25 @@ class Client(QObject):
self.net.write(self.nio.disconnect())
@pyqtSlot()
@futurize
def startSyncing(self) -> None:
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():
self._stop_sync.clear()
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 platform
import threading
from concurrent.futures import Future
from typing import Dict, Optional
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__
@ -23,6 +24,10 @@ _CONFIG_LOCK = threading.Lock()
class ClientManager(QObject):
clientAdded = pyqtSignal(Client)
clientDeleted = pyqtSignal(str)
def __init__(self) -> None:
super().__init__()
self._clients: Dict[str, Client] = {}
@ -32,7 +37,7 @@ class ClientManager(QObject):
return f"{type(self).__name__}(clients={self.clients!r})"
@pyqtProperty("QVariantMap")
@pyqtProperty("QVariantMap", constant=True)
def clients(self):
return self._clients
@ -40,13 +45,9 @@ class ClientManager(QObject):
@pyqtSlot()
def configLoad(self) -> None:
for user_id, info in self.configAccounts().items():
cli = Client(info["hostname"], user_id)
def on_done(_: Future, cli=cli) -> None:
self._clients[cli.nio.user_id] = cli
cli.resumeSession(user_id, info["token"], info["device_id"])\
.add_done_callback(on_done)
client = Client(info["hostname"], user_id)
client.resumeSession(user_id, info["token"], info["device_id"])\
.add_done_callback(lambda _, c=client: self._on_connected(c))
@pyqtSlot(str, str, str)
@ -54,22 +55,25 @@ class ClientManager(QObject):
def new(self, hostname: str, username: str, password: str,
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)
def delete(self, user_id: str) -> None:
client = self._clients.pop(user_id, None)
client = self.clients.pop(user_id, None)
if client:
self.clientDeleted.emit(user_id)
client.logout()
@pyqtProperty(str)
@pyqtProperty(str, constant=True)
def defaultDeviceName(self) -> str: # pylint: disable=no-self-use
os_ = f" on {platform.system()}".rstrip()
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()
@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)
def insert(self, index: int, value: NewValue) -> None:
value = self._convert_new_value(value)
@ -176,6 +186,10 @@ class ListModel(MutableSequence):
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:
"Set role of the item at *index* to *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:
Backend.getUser(sender_id).display_name
Backend.getUser(chatPage.room.room_id, sender_id).display_name
readonly property bool isOwn:
chatPage.user.user_id === sender_id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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