New backend work
Models, account connection, fetching user profiles, show connected accounts in sidebar
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
src/app.py
35
src/app.py
@@ -26,7 +26,7 @@ class App:
|
||||
debug = False
|
||||
|
||||
if "-d" in cli_flags or "--debug" in cli_flags:
|
||||
self._run_in_loop(self._exit_on_app_file_change())
|
||||
self.run_in_loop(self._exit_on_app_file_change())
|
||||
debug = True
|
||||
|
||||
from .backend import Backend
|
||||
@@ -47,28 +47,43 @@ class App:
|
||||
self.loop.run_forever()
|
||||
|
||||
|
||||
def _run_in_loop(self, coro: Coroutine) -> Future:
|
||||
def run_in_loop(self, coro: Coroutine) -> Future:
|
||||
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
||||
|
||||
|
||||
def _call_coro(self, coro: Coroutine) -> str:
|
||||
uuid = str(uuid4())
|
||||
|
||||
self.run_in_loop(coro).add_done_callback(
|
||||
lambda future: CoroutineDone(uuid=uuid, result=future.result())
|
||||
)
|
||||
return uuid
|
||||
|
||||
|
||||
def call_backend_coro(self,
|
||||
name: str,
|
||||
args: Optional[List[str]] = None,
|
||||
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
||||
# To be used from QML
|
||||
|
||||
coro = getattr(self.backend, name)(*args or [], **kwargs or {})
|
||||
uuid = str(uuid4())
|
||||
|
||||
self._run_in_loop(coro).add_done_callback(
|
||||
lambda future: CoroutineDone(uuid=uuid, result=future.result())
|
||||
return self._call_coro(
|
||||
getattr(self.backend, name)(*args or [], **kwargs or {})
|
||||
)
|
||||
|
||||
|
||||
def call_client_coro(self,
|
||||
account_id: str,
|
||||
name: str,
|
||||
args: Optional[List[str]] = None,
|
||||
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
||||
client = self.backend.clients[account_id] # type: ignore
|
||||
return self._call_coro(
|
||||
getattr(client, name)(*args or [], **kwargs or {})
|
||||
)
|
||||
return uuid
|
||||
|
||||
|
||||
def pdb(self, additional_data: Sequence = ()) -> None:
|
||||
# pylint: disable=all
|
||||
ad = additional_data
|
||||
rl = self.run_in_loop
|
||||
ba = self.backend
|
||||
cl = self.backend.clients # type: ignore
|
||||
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from atomicfile import AtomicFile
|
||||
|
||||
from .app import App
|
||||
from .events import users
|
||||
from .matrix_client import MatrixClient
|
||||
|
||||
SavedAccounts = Dict[str, Dict[str, str]]
|
||||
@@ -34,6 +35,7 @@ class Backend:
|
||||
)
|
||||
await client.login(password)
|
||||
self.clients[client.user_id] = client
|
||||
users.AccountUpdated(client.user_id)
|
||||
|
||||
|
||||
async def resume_client(self,
|
||||
@@ -46,12 +48,14 @@ class Backend:
|
||||
)
|
||||
await client.resume(user_id=user_id, token=token, device_id=device_id)
|
||||
self.clients[client.user_id] = client
|
||||
users.AccountUpdated(client.user_id)
|
||||
|
||||
|
||||
async def logout_client(self, user_id: str) -> None:
|
||||
client = self.clients.pop(user_id, None)
|
||||
if client:
|
||||
await client.close()
|
||||
await client.logout()
|
||||
users.AccountDeleted(user_id)
|
||||
|
||||
|
||||
async def logout_all_clients(self) -> None:
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,19 +8,23 @@ from .event import Event
|
||||
|
||||
@dataclass
|
||||
class RoomUpdated(Event):
|
||||
user_id: str = field()
|
||||
category: str = field()
|
||||
room_id: str = field()
|
||||
display_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
topic: Optional[str] = None
|
||||
last_event_date: Optional[datetime] = None
|
||||
|
||||
inviter: Optional[Dict[str, str]] = None
|
||||
inviter: Optional[str] = None
|
||||
left_event: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomDeleted(Event):
|
||||
room_id: str = field()
|
||||
user_id: str = field()
|
||||
category: str = field()
|
||||
room_id: str = field()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@@ -22,9 +22,10 @@ class AccountDeleted(Event):
|
||||
|
||||
@dataclass
|
||||
class UserUpdated(Event):
|
||||
user_id: str = field()
|
||||
display_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
user_id: str = field()
|
||||
display_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
status_message: Optional[str] = None
|
||||
|
||||
|
||||
# Devices
|
||||
|
@@ -1,7 +1,14 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import platform
|
||||
from contextlib import suppress
|
||||
from typing import Optional
|
||||
|
||||
import nio
|
||||
|
||||
from . import __about__
|
||||
from .events import rooms, users
|
||||
|
||||
|
||||
class MatrixClient(nio.AsyncClient):
|
||||
def __init__(self,
|
||||
@@ -9,8 +16,12 @@ class MatrixClient(nio.AsyncClient):
|
||||
homeserver: str = "https://matrix.org",
|
||||
device_id: Optional[str] = None) -> None:
|
||||
|
||||
# TODO: ensure homeserver starts with a scheme://
|
||||
self.sync_task: Optional[asyncio.Task] = None
|
||||
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
|
||||
|
||||
self.connect_callbacks()
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
|
||||
@@ -18,5 +29,96 @@ class MatrixClient(nio.AsyncClient):
|
||||
)
|
||||
|
||||
|
||||
def connect_callbacks(self) -> None:
|
||||
for name in dir(nio.responses):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
|
||||
obj = getattr(nio.responses, name)
|
||||
if inspect.isclass(obj) and issubclass(obj, nio.Response):
|
||||
with suppress(AttributeError):
|
||||
self.add_response_callback(getattr(self, f"on{name}"), obj)
|
||||
|
||||
|
||||
async def start_syncing(self) -> None:
|
||||
self.sync_task = asyncio.ensure_future( # type: ignore
|
||||
self.sync_forever(timeout=10_000)
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def default_device_name(self) -> str:
|
||||
os_ = f" on {platform.system()}".rstrip()
|
||||
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""
|
||||
return f"{__about__.__pretty_name__}{os_}"
|
||||
|
||||
|
||||
async def login(self, password: str) -> None:
|
||||
response = await super().login(password, self.default_device_name)
|
||||
|
||||
if isinstance(response, nio.LoginError):
|
||||
print(response)
|
||||
else:
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def resume(self, user_id: str, token: str, device_id: str) -> None:
|
||||
self.receive_response(nio.LoginResponse(user_id, device_id, token))
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def logout(self) -> None:
|
||||
if self.sync_task:
|
||||
self.sync_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self.sync_task
|
||||
|
||||
await self.close()
|
||||
|
||||
|
||||
async def request_user_update_event(self, user_id: str) -> None:
|
||||
response = await self.get_profile(user_id)
|
||||
|
||||
users.UserUpdated(
|
||||
user_id = user_id,
|
||||
display_name = response.displayname,
|
||||
avatar_url = response.avatar_url,
|
||||
status_message = None, # TODO
|
||||
)
|
||||
|
||||
|
||||
# Callbacks for nio responses
|
||||
|
||||
async def onSyncResponse(self, response: nio.SyncResponse) -> None:
|
||||
for room_id in response.rooms.invite:
|
||||
room: nio.rooms.MatrixRoom = self.invited_rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Invites",
|
||||
room_id = room_id,
|
||||
display_name = room.display_name,
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
inviter = room.inviter,
|
||||
)
|
||||
|
||||
for room_id in response.rooms.join:
|
||||
room = self.rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Rooms",
|
||||
room_id = room_id,
|
||||
display_name = room.display_name,
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
)
|
||||
|
||||
for room_id in response.rooms.left:
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Left",
|
||||
room_id = room_id,
|
||||
# left_event TODO
|
||||
)
|
||||
|
@@ -37,16 +37,27 @@ ListModel {
|
||||
return results
|
||||
}
|
||||
|
||||
function upsert(where_role, is, new_item) {
|
||||
function forEachWhere(where_role, is, max, func) {
|
||||
var items = getWhere(where_role, is, max)
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
func(item)
|
||||
}
|
||||
}
|
||||
|
||||
function upsert(where_role, is, new_item, update_if_exist) {
|
||||
// new_item can contain only the keys we're interested in updating
|
||||
|
||||
var indices = getIndices(where_role, is, 1)
|
||||
|
||||
if (indices.length == 0) {
|
||||
listModel.append(new_item)
|
||||
} else {
|
||||
return listModel.get(listModel.count)
|
||||
}
|
||||
|
||||
if (update_if_exist != false) {
|
||||
listModel.set(indices[0], new_item)
|
||||
}
|
||||
return listModel.get(indices[0])
|
||||
}
|
||||
|
||||
function pop(index) {
|
||||
@@ -54,4 +65,38 @@ ListModel {
|
||||
listModel.remove(index)
|
||||
return item
|
||||
}
|
||||
|
||||
function popWhere(where_role, is, max) {
|
||||
var indices = getIndices(where_role, is, max)
|
||||
var results = []
|
||||
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
results.push(listModel.get(indices[i]))
|
||||
listModel.remove(indices[i])
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
function toObject(item_list) {
|
||||
item_list = item_list || listModel
|
||||
var obj_list = []
|
||||
|
||||
for (var i = 0; i < item_list.count; i++) {
|
||||
var item = item_list.get(i)
|
||||
var obj = JSON.parse(JSON.stringify(item))
|
||||
|
||||
for (var role in obj) {
|
||||
if (obj[role]["objectName"] != undefined) {
|
||||
obj[role] = toObject(item[role])
|
||||
}
|
||||
}
|
||||
obj_list.push(obj)
|
||||
}
|
||||
return obj_list
|
||||
}
|
||||
|
||||
function toJson() {
|
||||
return JSON.stringify(toObject(), null, 4)
|
||||
}
|
||||
}
|
||||
|
@@ -1,2 +1,5 @@
|
||||
// FIXME: Obsolete method, but need Qt 5.12+ for standard JS modules import
|
||||
Qt.include("app.js")
|
||||
Qt.include("users.js")
|
||||
Qt.include("rooms.js")
|
||||
Qt.include("rooms_timeline.js")
|
||||
|
60
src/qml/EventHandlers/rooms.js
Normal file
60
src/qml/EventHandlers/rooms.js
Normal file
@@ -0,0 +1,60 @@
|
||||
function clientId(user_id, category, room_id) {
|
||||
return user_id + " " + room_id + " " + category
|
||||
}
|
||||
|
||||
|
||||
function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
|
||||
topic, last_event_date, inviter, left_event) {
|
||||
|
||||
var client_id = clientId(user_id, category, room_id)
|
||||
var rooms = models.rooms
|
||||
|
||||
if (category == "Invites") {
|
||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id))
|
||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
||||
}
|
||||
else if (category == "Rooms") {
|
||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
||||
}
|
||||
else if (category == "Left") {
|
||||
var old_room =
|
||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id)) ||
|
||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
||||
|
||||
if (old_room) {
|
||||
display_name = old_room.displayName
|
||||
avatar_url = old_room.avatarUrl
|
||||
topic = old_room.topic
|
||||
inviter = old_room.topic
|
||||
}
|
||||
}
|
||||
|
||||
rooms.upsert("clientId", client_id , {
|
||||
"clientId": client_id,
|
||||
"userId": user_id,
|
||||
"category": category,
|
||||
"roomId": room_id,
|
||||
"displayName": display_name,
|
||||
"avatarUrl": avatar_url,
|
||||
"topic": topic,
|
||||
"lastEventDate": last_event_date,
|
||||
"inviter": inviter,
|
||||
"leftEvent": left_event
|
||||
})
|
||||
//print("room up", rooms.toJson())
|
||||
}
|
||||
|
||||
|
||||
function onRoomDeleted(user_id, category, room_id) {
|
||||
var client_id = clientId(user_id, category, room_id)
|
||||
return models.rooms.popWhere("clientId", client_id, 1)
|
||||
}
|
||||
|
||||
|
||||
function onRoomMemberUpdated(room_id, user_id, typing) {
|
||||
}
|
||||
|
||||
|
||||
function onRoomMemberDeleted(room_id, user_id) {
|
||||
}
|
0
src/qml/EventHandlers/rooms_timeline.js
Normal file
0
src/qml/EventHandlers/rooms_timeline.js
Normal file
24
src/qml/EventHandlers/users.js
Normal file
24
src/qml/EventHandlers/users.js
Normal file
@@ -0,0 +1,24 @@
|
||||
function onAccountUpdated(user_id) {
|
||||
models.accounts.append({"userId": user_id})
|
||||
}
|
||||
|
||||
function AccountDeleted(user_id) {
|
||||
models.accounts.popWhere("userId", user_id, 1)
|
||||
}
|
||||
|
||||
function onUserUpdated(user_id, display_name, avatar_url, status_message) {
|
||||
models.users.upsert("userId", user_id, {
|
||||
"userId": user_id,
|
||||
"displayName": display_name,
|
||||
"avatarUrl": avatar_url,
|
||||
"statusMessage": status_message
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function onDeviceUpdated(user_id, device_id, ed25519_key, trust, display_name,
|
||||
last_seen_ip, last_seen_date) {
|
||||
}
|
||||
|
||||
function onDeviceDeleted(user_id, device_id) {
|
||||
}
|
34
src/qml/Models.qml
Normal file
34
src/qml/Models.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick 2.7
|
||||
import "Base"
|
||||
|
||||
QtObject {
|
||||
property HListModel accounts: HListModel {}
|
||||
|
||||
property HListModel users: HListModel {
|
||||
function getUser(as_account_id, wanted_user_id) {
|
||||
wanted_user_id = wanted_user_id || as_account_id
|
||||
|
||||
var found = users.getWhere("userId", wanted_user_id, 1)
|
||||
if (found.length > 0) { return found[0] }
|
||||
|
||||
users.append({
|
||||
"userId": wanted_user_id,
|
||||
"displayName": "",
|
||||
"avatarUrl": "",
|
||||
"statusMessage": ""
|
||||
})
|
||||
|
||||
py.callClientCoro(
|
||||
as_account_id, "request_user_update_event", [wanted_user_id]
|
||||
)
|
||||
|
||||
return users.getWhere("userId", wanted_user_id, 1)[0]
|
||||
}
|
||||
}
|
||||
|
||||
property HListModel devices: HListModel {}
|
||||
|
||||
property HListModel rooms: HListModel {}
|
||||
|
||||
property HListModel timelines: HListModel {}
|
||||
}
|
@@ -6,16 +6,25 @@ import "EventHandlers/includes.js" as EventHandlers
|
||||
Python {
|
||||
id: py
|
||||
|
||||
signal ready(bool accountsToLoad)
|
||||
|
||||
property bool ready: false
|
||||
property var pendingCoroutines: ({})
|
||||
|
||||
property bool loadingAccounts: false
|
||||
|
||||
function callCoro(name, args, kwargs, callback) {
|
||||
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
|
||||
pendingCoroutines[uuid] = callback || function() {}
|
||||
})
|
||||
}
|
||||
|
||||
function callClientCoro(account_id, name, args, kwargs, callback) {
|
||||
var args = [account_id, name, args, kwargs]
|
||||
|
||||
call("APP.call_client_coro", args, function(uuid){
|
||||
pendingCoroutines[uuid] = callback || function() {}
|
||||
})
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
for (var func in EventHandlers) {
|
||||
if (EventHandlers.hasOwnProperty(func)) {
|
||||
@@ -29,8 +38,14 @@ Python {
|
||||
window.debug = debug_on
|
||||
|
||||
callCoro("has_saved_accounts", [], {}, function(has) {
|
||||
print(has)
|
||||
py.ready(has)
|
||||
loadingAccounts = has
|
||||
py.ready = true
|
||||
|
||||
if (has) {
|
||||
py.callCoro("load_saved_accounts", [], {}, function() {
|
||||
loadingAccounts = false
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@@ -6,7 +6,9 @@ Column {
|
||||
id: accountDelegate
|
||||
width: parent.width
|
||||
|
||||
property var user: Backend.users.get(userId)
|
||||
// Avoid binding loop by using Component.onCompleted
|
||||
property var user: null
|
||||
Component.onCompleted: user = models.users.getUser(userId)
|
||||
|
||||
property string roomCategoriesListUserId: userId
|
||||
property bool expanded: true
|
||||
@@ -18,7 +20,7 @@ Column {
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: user.displayName.value
|
||||
name: user.displayName
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
@@ -27,7 +29,7 @@ Column {
|
||||
|
||||
HLabel {
|
||||
id: accountLabel
|
||||
text: user.displayName.value
|
||||
text: user.displayName || user.userId
|
||||
elide: HLabel.ElideRight
|
||||
maximumLineCount: 1
|
||||
Layout.fillWidth: true
|
||||
|
@@ -6,6 +6,6 @@ HListView {
|
||||
id: accountList
|
||||
clip: true
|
||||
|
||||
model: Backend.accounts
|
||||
model: models.accounts
|
||||
delegate: AccountDelegate {}
|
||||
}
|
||||
|
@@ -8,7 +8,10 @@ import "SidePane"
|
||||
Item {
|
||||
id: mainUI
|
||||
|
||||
property bool accountsLoggedIn: Backend.clients.count > 0
|
||||
property bool accountsPresent:
|
||||
models.accounts.count > 0 || py.loadingAccounts
|
||||
onAccountsPresentChanged:
|
||||
pageStack.showPage(accountsPresent ? "Default" : "SignIn")
|
||||
|
||||
HImage {
|
||||
id: mainUIBackground
|
||||
@@ -26,7 +29,7 @@ Item {
|
||||
|
||||
SidePane {
|
||||
id: sidePane
|
||||
visible: accountsLoggedIn
|
||||
visible: accountsPresent
|
||||
collapsed: width < Layout.minimumWidth + normalSpacing
|
||||
|
||||
property int parentWidth: parent.width
|
||||
@@ -68,17 +71,6 @@ Item {
|
||||
)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: py
|
||||
onReady: function(accountsToLoad) {
|
||||
pageStack.showPage(accountsToLoad ? "Default" : "SignIn")
|
||||
if (accountsToLoad) {
|
||||
py.callCoro("load_saved_accounts")
|
||||
// initialRoomTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
// TODO: remove this, debug
|
||||
id: initialRoomTimer
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import "Base"
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
@@ -7,7 +8,7 @@ ApplicationWindow {
|
||||
height: 480
|
||||
visible: true
|
||||
color: "black"
|
||||
title: "Test"
|
||||
title: "Harmony QML"
|
||||
|
||||
property bool debug: false
|
||||
property bool ready: false
|
||||
@@ -23,6 +24,10 @@ ApplicationWindow {
|
||||
id: py
|
||||
}
|
||||
|
||||
Models {
|
||||
id: models
|
||||
}
|
||||
|
||||
LoadingScreen {
|
||||
id: loadingScreen
|
||||
anchors.fill: parent
|
||||
@@ -38,7 +43,7 @@ ApplicationWindow {
|
||||
source: uiLoader.ready ? "UI.qml" : ""
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 100 }
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user