New backend work
Models, account connection, fetching user profiles, show connected accounts in sidebar
This commit is contained in:
parent
e5bdf6a497
commit
a1b4d8900f
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
__pycache__
|
||||||
|
.mypy_cache
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
*.egg-info
|
||||||
|
*.pyc
|
||||||
|
*.qmlc
|
||||||
|
*.jsc
|
||||||
|
|
||||||
|
.pylintrc
|
||||||
|
|
||||||
|
tmp-*
|
98
TODO.md
98
TODO.md
|
@ -0,0 +1,98 @@
|
||||||
|
- license headers
|
||||||
|
- replace "property var" by "property <object>" where applicable
|
||||||
|
- [debug mode](https://docs.python.org/3/library/asyncio-dev.html)
|
||||||
|
|
||||||
|
OLD
|
||||||
|
|
||||||
|
- Refactoring
|
||||||
|
- Migrate more JS functions to their own files / Implement in Python instead
|
||||||
|
- Don't bake in size properties for components
|
||||||
|
|
||||||
|
- Bug fixes
|
||||||
|
- dataclass-like `default_factory` for ListItem
|
||||||
|
- Prevent briefly seeing login screen if there are accounts to
|
||||||
|
resumeSession for but they take time to appear
|
||||||
|
- 100% CPU usage when hitting top edge to trigger messages loading
|
||||||
|
- Sending `![A picture](https://picsum.photos/256/256)` → not clickable?
|
||||||
|
- Icons, images and HStyle singleton aren't reloaded
|
||||||
|
- `MessageDelegate.qml:63: TypeError: 'reloadPreviousItem' not a function`
|
||||||
|
- RoomEventsList scrolling when resizing the window
|
||||||
|
|
||||||
|
- UI
|
||||||
|
- Invite to room
|
||||||
|
- Accounts delegates background
|
||||||
|
- SidePane delegates hover effect
|
||||||
|
- Server selection
|
||||||
|
- Register/Forgot? for SignIn dialog
|
||||||
|
- Scaling
|
||||||
|
- See [Text.fontSizeMode](https://doc.qt.io/qt-5/qml-qtquick-text.html#fontSizeMode-prop)
|
||||||
|
- Add room
|
||||||
|
- Leave room
|
||||||
|
- Forget room warning popup
|
||||||
|
- Prevent using the SendBox if no permission (power levels)
|
||||||
|
- Spinner when loading past room events, images or clicking buttons
|
||||||
|
- Better theming/styling system
|
||||||
|
- See about <https://doc.qt.io/qt-5/qtquickcontrols2-configuration.html>
|
||||||
|
- Settings page
|
||||||
|
- Multiaccount aliases
|
||||||
|
- Message/text selection
|
||||||
|
|
||||||
|
- Major features
|
||||||
|
- E2E
|
||||||
|
- Device verification
|
||||||
|
- Edit/delete own devices
|
||||||
|
- Request room keys from own other devices
|
||||||
|
- Auto-trust accounts within the same client
|
||||||
|
- Import/export keys
|
||||||
|
- Uploads
|
||||||
|
- QQuickImageProvider
|
||||||
|
- Read receipts
|
||||||
|
- Status message and presence
|
||||||
|
- Links preview
|
||||||
|
|
||||||
|
- Client improvements
|
||||||
|
- Filtering rooms: search more than display names?
|
||||||
|
- nio.MatrixRoom has `typing_users`, no need to handle it on our own
|
||||||
|
- Initial sync filter and lazy load, see weechat-matrix `_handle_login()`
|
||||||
|
- See also `handle_response()`'s `keys_query` request
|
||||||
|
- HTTP/2
|
||||||
|
- `retry_after_ms` when rate-limited
|
||||||
|
- Direct chats category
|
||||||
|
- On sync, check messages API, if a limited sync timeline was received
|
||||||
|
- Markdown: don't turn #things (no space) and `thing\n---` into title,
|
||||||
|
disable `__` syntax for bold/italic
|
||||||
|
- Push instead of replacing in stack view (remove getMemberFilter when done)
|
||||||
|
- Make links in room subtitle clickable, formatting?
|
||||||
|
- `<pre>` scrollbar on overflow
|
||||||
|
- Handle cases where an avatar char is # or @ (#alias room, @user\_id)
|
||||||
|
- When inviting someone to direct chat, room is "Empty room" until accepted,
|
||||||
|
it should be the peer's display name instead.
|
||||||
|
- Keep an accounts order
|
||||||
|
- See `Qt.callLater()` potential usages
|
||||||
|
- Banner name color instead of bold
|
||||||
|
- Animate RoomEventDelegate DayBreak apparition
|
||||||
|
|
||||||
|
- Missing nio support
|
||||||
|
- MatrixRoom invited members list
|
||||||
|
- Invite events are missing their timestamps (needed for sorting)
|
||||||
|
- Left room events after client reboot
|
||||||
|
- `org.matrix.room.preview_urls` event
|
||||||
|
- `m.room.aliases` event
|
||||||
|
- Support "Empty room (was ...)" after peer left
|
||||||
|
|
||||||
|
- Waiting for approval/release
|
||||||
|
- nio avatars
|
||||||
|
- olm/olm-devel 0.3.1 in void repos
|
||||||
|
|
||||||
|
- Distribution
|
||||||
|
- Review setup.py, add dependencies
|
||||||
|
- README.md
|
||||||
|
- Use PyInstaller or pyqtdeploy
|
||||||
|
- Test command:
|
||||||
|
```
|
||||||
|
pyinstaller --onefile --windowed --name harmonyqml \
|
||||||
|
--add-data 'harmonyqml/components:harmonyqml/components' \
|
||||||
|
--additional-hooks-dir . \
|
||||||
|
--upx-dir ~/opt/upx-3.95-amd64_linux \
|
||||||
|
run.py
|
||||||
|
```
|
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
|
debug = False
|
||||||
|
|
||||||
if "-d" in cli_flags or "--debug" in cli_flags:
|
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
|
debug = True
|
||||||
|
|
||||||
from .backend import Backend
|
from .backend import Backend
|
||||||
|
@ -47,28 +47,43 @@ class App:
|
||||||
self.loop.run_forever()
|
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)
|
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,
|
def call_backend_coro(self,
|
||||||
name: str,
|
name: str,
|
||||||
args: Optional[List[str]] = None,
|
args: Optional[List[str]] = None,
|
||||||
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
||||||
# To be used from QML
|
return self._call_coro(
|
||||||
|
getattr(self.backend, name)(*args or [], **kwargs or {})
|
||||||
coro = getattr(self.backend, name)(*args or [], **kwargs or {})
|
)
|
||||||
uuid = str(uuid4())
|
|
||||||
|
|
||||||
self._run_in_loop(coro).add_done_callback(
|
def call_client_coro(self,
|
||||||
lambda future: CoroutineDone(uuid=uuid, result=future.result())
|
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:
|
def pdb(self, additional_data: Sequence = ()) -> None:
|
||||||
# pylint: disable=all
|
# pylint: disable=all
|
||||||
ad = additional_data
|
ad = additional_data
|
||||||
|
rl = self.run_in_loop
|
||||||
ba = self.backend
|
ba = self.backend
|
||||||
cl = self.backend.clients # type: ignore
|
cl = self.backend.clients # type: ignore
|
||||||
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
from atomicfile import AtomicFile
|
from atomicfile import AtomicFile
|
||||||
|
|
||||||
from .app import App
|
from .app import App
|
||||||
|
from .events import users
|
||||||
from .matrix_client import MatrixClient
|
from .matrix_client import MatrixClient
|
||||||
|
|
||||||
SavedAccounts = Dict[str, Dict[str, str]]
|
SavedAccounts = Dict[str, Dict[str, str]]
|
||||||
|
@ -34,6 +35,7 @@ class Backend:
|
||||||
)
|
)
|
||||||
await client.login(password)
|
await client.login(password)
|
||||||
self.clients[client.user_id] = client
|
self.clients[client.user_id] = client
|
||||||
|
users.AccountUpdated(client.user_id)
|
||||||
|
|
||||||
|
|
||||||
async def resume_client(self,
|
async def resume_client(self,
|
||||||
|
@ -46,12 +48,14 @@ class Backend:
|
||||||
)
|
)
|
||||||
await client.resume(user_id=user_id, token=token, device_id=device_id)
|
await client.resume(user_id=user_id, token=token, device_id=device_id)
|
||||||
self.clients[client.user_id] = client
|
self.clients[client.user_id] = client
|
||||||
|
users.AccountUpdated(client.user_id)
|
||||||
|
|
||||||
|
|
||||||
async def logout_client(self, user_id: str) -> None:
|
async def logout_client(self, user_id: str) -> None:
|
||||||
client = self.clients.pop(user_id, None)
|
client = self.clients.pop(user_id, None)
|
||||||
if client:
|
if client:
|
||||||
await client.close()
|
await client.logout()
|
||||||
|
users.AccountDeleted(user_id)
|
||||||
|
|
||||||
|
|
||||||
async def logout_all_clients(self) -> None:
|
async def logout_all_clients(self) -> None:
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -8,18 +8,22 @@ from .event import Event
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RoomUpdated(Event):
|
class RoomUpdated(Event):
|
||||||
|
user_id: str = field()
|
||||||
|
category: str = field()
|
||||||
room_id: str = field()
|
room_id: str = field()
|
||||||
display_name: Optional[str] = None
|
display_name: Optional[str] = None
|
||||||
avatar_url: Optional[str] = None
|
avatar_url: Optional[str] = None
|
||||||
topic: Optional[str] = None
|
topic: Optional[str] = None
|
||||||
last_event_date: Optional[datetime] = None
|
last_event_date: Optional[datetime] = None
|
||||||
|
|
||||||
inviter: Optional[Dict[str, str]] = None
|
inviter: Optional[str] = None
|
||||||
left_event: Optional[Dict[str, str]] = None
|
left_event: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RoomDeleted(Event):
|
class RoomDeleted(Event):
|
||||||
|
user_id: str = field()
|
||||||
|
category: str = field()
|
||||||
room_id: str = field()
|
room_id: str = field()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class UserUpdated(Event):
|
||||||
user_id: str = field()
|
user_id: str = field()
|
||||||
display_name: Optional[str] = None
|
display_name: Optional[str] = None
|
||||||
avatar_url: Optional[str] = None
|
avatar_url: Optional[str] = None
|
||||||
|
status_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Devices
|
# Devices
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import platform
|
||||||
|
from contextlib import suppress
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
|
||||||
|
from . import __about__
|
||||||
|
from .events import rooms, users
|
||||||
|
|
||||||
|
|
||||||
class MatrixClient(nio.AsyncClient):
|
class MatrixClient(nio.AsyncClient):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
@ -9,8 +16,12 @@ class MatrixClient(nio.AsyncClient):
|
||||||
homeserver: str = "https://matrix.org",
|
homeserver: str = "https://matrix.org",
|
||||||
device_id: Optional[str] = None) -> None:
|
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)
|
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
|
||||||
|
|
||||||
|
self.connect_callbacks()
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
|
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:
|
async def resume(self, user_id: str, token: str, device_id: str) -> None:
|
||||||
self.receive_response(nio.LoginResponse(user_id, device_id, token))
|
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
|
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
|
// new_item can contain only the keys we're interested in updating
|
||||||
|
|
||||||
var indices = getIndices(where_role, is, 1)
|
var indices = getIndices(where_role, is, 1)
|
||||||
|
|
||||||
if (indices.length == 0) {
|
if (indices.length == 0) {
|
||||||
listModel.append(new_item)
|
listModel.append(new_item)
|
||||||
} else {
|
return listModel.get(listModel.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update_if_exist != false) {
|
||||||
listModel.set(indices[0], new_item)
|
listModel.set(indices[0], new_item)
|
||||||
}
|
}
|
||||||
|
return listModel.get(indices[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
function pop(index) {
|
function pop(index) {
|
||||||
|
@ -54,4 +65,38 @@ ListModel {
|
||||||
listModel.remove(index)
|
listModel.remove(index)
|
||||||
return item
|
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
|
// FIXME: Obsolete method, but need Qt 5.12+ for standard JS modules import
|
||||||
Qt.include("app.js")
|
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 {
|
Python {
|
||||||
id: py
|
id: py
|
||||||
|
|
||||||
signal ready(bool accountsToLoad)
|
property bool ready: false
|
||||||
|
|
||||||
property var pendingCoroutines: ({})
|
property var pendingCoroutines: ({})
|
||||||
|
|
||||||
|
property bool loadingAccounts: false
|
||||||
|
|
||||||
function callCoro(name, args, kwargs, callback) {
|
function callCoro(name, args, kwargs, callback) {
|
||||||
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
|
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
|
||||||
pendingCoroutines[uuid] = callback || function() {}
|
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: {
|
Component.onCompleted: {
|
||||||
for (var func in EventHandlers) {
|
for (var func in EventHandlers) {
|
||||||
if (EventHandlers.hasOwnProperty(func)) {
|
if (EventHandlers.hasOwnProperty(func)) {
|
||||||
|
@ -29,8 +38,14 @@ Python {
|
||||||
window.debug = debug_on
|
window.debug = debug_on
|
||||||
|
|
||||||
callCoro("has_saved_accounts", [], {}, function(has) {
|
callCoro("has_saved_accounts", [], {}, function(has) {
|
||||||
print(has)
|
loadingAccounts = has
|
||||||
py.ready(has)
|
py.ready = true
|
||||||
|
|
||||||
|
if (has) {
|
||||||
|
py.callCoro("load_saved_accounts", [], {}, function() {
|
||||||
|
loadingAccounts = false
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,9 @@ Column {
|
||||||
id: accountDelegate
|
id: accountDelegate
|
||||||
width: parent.width
|
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 string roomCategoriesListUserId: userId
|
||||||
property bool expanded: true
|
property bool expanded: true
|
||||||
|
@ -18,7 +20,7 @@ Column {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
name: user.displayName.value
|
name: user.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
|
@ -27,7 +29,7 @@ Column {
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
id: accountLabel
|
id: accountLabel
|
||||||
text: user.displayName.value
|
text: user.displayName || user.userId
|
||||||
elide: HLabel.ElideRight
|
elide: HLabel.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
@ -6,6 +6,6 @@ HListView {
|
||||||
id: accountList
|
id: accountList
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
model: Backend.accounts
|
model: models.accounts
|
||||||
delegate: AccountDelegate {}
|
delegate: AccountDelegate {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,10 @@ import "SidePane"
|
||||||
Item {
|
Item {
|
||||||
id: mainUI
|
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 {
|
HImage {
|
||||||
id: mainUIBackground
|
id: mainUIBackground
|
||||||
|
@ -26,7 +29,7 @@ Item {
|
||||||
|
|
||||||
SidePane {
|
SidePane {
|
||||||
id: sidePane
|
id: sidePane
|
||||||
visible: accountsLoggedIn
|
visible: accountsPresent
|
||||||
collapsed: width < Layout.minimumWidth + normalSpacing
|
collapsed: width < Layout.minimumWidth + normalSpacing
|
||||||
|
|
||||||
property int parentWidth: parent.width
|
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 {
|
Timer {
|
||||||
// TODO: remove this, debug
|
// TODO: remove this, debug
|
||||||
id: initialRoomTimer
|
id: initialRoomTimer
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import "Base"
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: window
|
id: window
|
||||||
|
@ -7,7 +8,7 @@ ApplicationWindow {
|
||||||
height: 480
|
height: 480
|
||||||
visible: true
|
visible: true
|
||||||
color: "black"
|
color: "black"
|
||||||
title: "Test"
|
title: "Harmony QML"
|
||||||
|
|
||||||
property bool debug: false
|
property bool debug: false
|
||||||
property bool ready: false
|
property bool ready: false
|
||||||
|
@ -23,6 +24,10 @@ ApplicationWindow {
|
||||||
id: py
|
id: py
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Models {
|
||||||
|
id: models
|
||||||
|
}
|
||||||
|
|
||||||
LoadingScreen {
|
LoadingScreen {
|
||||||
id: loadingScreen
|
id: loadingScreen
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -38,7 +43,7 @@ ApplicationWindow {
|
||||||
source: uiLoader.ready ? "UI.qml" : ""
|
source: uiLoader.ready ? "UI.qml" : ""
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
NumberAnimation { duration: 100 }
|
NumberAnimation { duration: HStyle.animationDuration }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user