Start rewriting backend with pyotherside+asyncio
12
src/__about__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""<SHORTDESC>"""
|
||||
|
||||
__pkg_name__ = "harmonyqml"
|
||||
__pretty_name__ = "Harmony QML"
|
||||
__version__ = "0.1.0"
|
||||
__status__ = "Development"
|
||||
# __status__ = "Production"
|
||||
|
||||
__author__ = "miruka"
|
||||
__email__ = "miruka@disroot.org"
|
||||
|
||||
__license__ = "GPLv3"
|
1
src/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .app import APP
|
BIN
src/__pycache__/__about__.cpython-36.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
src/__pycache__/app.cpython-36.pyc
Normal file
BIN
src/__pycache__/backend.cpython-36.pyc
Normal file
BIN
src/__pycache__/matrix_client.cpython-36.pyc
Normal file
83
src/app.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import asyncio
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Any, Coroutine, Dict, List, Optional, Sequence
|
||||
from uuid import uuid4
|
||||
|
||||
from appdirs import AppDirs
|
||||
|
||||
from . import __about__
|
||||
from .events.system import CoroutineDone, AppExitRequested
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self) -> None:
|
||||
self.appdirs = AppDirs(appname=__about__.__pkg_name__, roaming=True)
|
||||
|
||||
self.backend = None
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.loop_thread = Thread(target=self._loop_starter)
|
||||
self.loop_thread.start()
|
||||
|
||||
|
||||
def start(self, cli_flags: Sequence[str] = ()) -> bool:
|
||||
debug = False
|
||||
|
||||
if "-d" in cli_flags or "--debug" in cli_flags:
|
||||
self._run_in_loop(self._exit_on_app_file_change())
|
||||
debug = True
|
||||
|
||||
from .backend import Backend
|
||||
self.backend = Backend(app=self) # type: ignore
|
||||
|
||||
return debug
|
||||
|
||||
|
||||
async def _exit_on_app_file_change(self) -> None:
|
||||
from watchgod import awatch
|
||||
|
||||
async for _ in awatch(Path(__file__).resolve().parent):
|
||||
AppExitRequested(231)
|
||||
|
||||
|
||||
def _loop_starter(self) -> None:
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
|
||||
|
||||
def _run_in_loop(self, coro: Coroutine) -> Future:
|
||||
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
||||
|
||||
|
||||
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 uuid
|
||||
|
||||
|
||||
def pdb(self, additional_data: Sequence = ()) -> None:
|
||||
# pylint: disable=all
|
||||
ad = additional_data
|
||||
ba = self.backend
|
||||
cl = self.backend.clients # type: ignore
|
||||
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
||||
|
||||
import json
|
||||
jd = lambda obj: print(json.dumps(obj, indent=4, ensure_ascii=False))
|
||||
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
|
||||
|
||||
APP = App()
|
122
src/backend.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from atomicfile import AtomicFile
|
||||
|
||||
from .app import App
|
||||
from .matrix_client import MatrixClient
|
||||
|
||||
SavedAccounts = Dict[str, Dict[str, str]]
|
||||
CONFIG_LOCK = asyncio.Lock()
|
||||
|
||||
|
||||
class Backend:
|
||||
def __init__(self, app: App) -> None:
|
||||
self.app = app
|
||||
self.clients: Dict[str, MatrixClient] = {}
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}(clients={self.clients!r})"
|
||||
|
||||
|
||||
# Clients management
|
||||
|
||||
async def login_client(self,
|
||||
user: str,
|
||||
password: str,
|
||||
device_id: Optional[str] = None,
|
||||
homeserver: str = "https://matrix.org") -> None:
|
||||
client = MatrixClient(
|
||||
user=user, homeserver=homeserver, device_id=device_id
|
||||
)
|
||||
await client.login(password)
|
||||
self.clients[client.user_id] = client
|
||||
|
||||
|
||||
async def resume_client(self,
|
||||
user_id: str,
|
||||
token: str,
|
||||
device_id: str,
|
||||
homeserver: str = "https://matrix.org") -> None:
|
||||
client = MatrixClient(
|
||||
user=user_id, homeserver=homeserver, device_id=device_id
|
||||
)
|
||||
await client.resume(user_id=user_id, token=token, device_id=device_id)
|
||||
self.clients[client.user_id] = client
|
||||
|
||||
|
||||
async def logout_client(self, user_id: str) -> None:
|
||||
client = self.clients.pop(user_id, None)
|
||||
if client:
|
||||
await client.close()
|
||||
|
||||
|
||||
async def logout_all_clients(self) -> None:
|
||||
await asyncio.gather(*(
|
||||
self.logout_client(user_id) for user_id in self.clients.copy()
|
||||
))
|
||||
|
||||
|
||||
# Saved account operations - TODO: Use aiofiles?
|
||||
|
||||
@property
|
||||
def saved_accounts_path(self) -> Path:
|
||||
return Path(self.app.appdirs.user_config_dir) / "accounts.json"
|
||||
|
||||
|
||||
@property
|
||||
def saved_accounts(self) -> SavedAccounts:
|
||||
try:
|
||||
return json.loads(self.saved_accounts_path.read_text())
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return {}
|
||||
|
||||
|
||||
async def has_saved_accounts(self) -> bool:
|
||||
return bool(self.saved_accounts)
|
||||
|
||||
|
||||
async def load_saved_accounts(self) -> Tuple[str, ...]:
|
||||
async def resume(user_id: str, info: Dict[str, str]) -> str:
|
||||
await self.resume_client(
|
||||
user_id = user_id,
|
||||
token = info["token"],
|
||||
device_id = info["device_id"],
|
||||
homeserver = info["homeserver"],
|
||||
)
|
||||
return user_id
|
||||
|
||||
return await asyncio.gather(*(
|
||||
resume(uid, info) for uid, info in self.saved_accounts.items()
|
||||
))
|
||||
|
||||
|
||||
async def save_account(self, client: MatrixClient) -> None:
|
||||
await self._write_config({
|
||||
**self.saved_accounts,
|
||||
client.userId: {
|
||||
"hostname": client.nio.host,
|
||||
"token": client.nio.access_token,
|
||||
"device_id": client.nio.device_id,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async def forget_account(self, user_id: str) -> None:
|
||||
await self._write_config({
|
||||
uid: info
|
||||
for uid, info in self.saved_accounts.items() if uid != user_id
|
||||
})
|
||||
|
||||
|
||||
async def _write_config(self, accounts: SavedAccounts) -> None:
|
||||
js = json.dumps(accounts, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
with CONFIG_LOCK:
|
||||
self.saved_accounts_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with AtomicFile(self.saved_accounts_path, "w") as new:
|
||||
new.write(js)
|
0
src/events/__init__.py
Normal file
BIN
src/events/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
src/events/__pycache__/event.cpython-36.pyc
Normal file
BIN
src/events/__pycache__/system.cpython-36.pyc
Normal file
23
src/events/event.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from enum import Enum
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pyotherside
|
||||
|
||||
|
||||
class AutoStrEnum(Enum):
|
||||
@staticmethod
|
||||
def _generate_next_value_(name, *_):
|
||||
return name
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
def __post_init__(self) -> None:
|
||||
# CPython >= 3.6 or any Python >= 3.7 needed for correct dict order
|
||||
args = [
|
||||
# pylint: disable=no-member
|
||||
getattr(self, field)
|
||||
for field in self.__dataclass_fields__ # type: ignore
|
||||
]
|
||||
pyotherside.send(type(self).__name__, *args)
|
36
src/events/rooms.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomUpdated(Event):
|
||||
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
|
||||
left_event: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomDeleted(Event):
|
||||
room_id: str = field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomMemberUpdated(Event):
|
||||
room_id: str = field()
|
||||
user_id: str = field()
|
||||
typing: bool = field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomMemberDeleted(Event):
|
||||
room_id: str = field()
|
||||
user_id: str = field()
|
8
src/events/rooms_timeline.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
16
src/events/system.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import Any
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppExitRequested(Event):
|
||||
exit_code: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoroutineDone(Event):
|
||||
uuid: str = field()
|
||||
result: Any = None
|
52
src/events/users.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import Event
|
||||
|
||||
# Logged-in accounts
|
||||
|
||||
@dataclass
|
||||
class AccountUpdated(Event):
|
||||
user_id: str = field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountDeleted(Event):
|
||||
user_id: str = field()
|
||||
|
||||
|
||||
# Accounts and room members details
|
||||
|
||||
@dataclass
|
||||
class UserUpdated(Event):
|
||||
user_id: str = field()
|
||||
display_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
# Devices
|
||||
|
||||
class Trust(Enum):
|
||||
blacklisted = -1
|
||||
undecided = 0
|
||||
trusted = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceUpdated(Event):
|
||||
user_id: str = field()
|
||||
device_id: str = field()
|
||||
ed25519_key: str = field()
|
||||
trust: Trust = Trust.undecided
|
||||
display_name: Optional[str] = None
|
||||
last_seen_ip: Optional[str] = None
|
||||
last_seen_date: Optional[datetime] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceDeleted(Event):
|
||||
user_id: str = field()
|
||||
device_id: str = field()
|
1
src/icons/email.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 12.713l-11.985-9.713h23.971l-11.986 9.713zm-5.425-1.822l-6.575-5.329v12.501l6.575-7.172zm10.85 0l6.575 7.172v-12.501l-6.575 5.329zm-1.557 1.261l-3.868 3.135-3.868-3.135-8.11 8.848h23.956l-8.11-8.848z"/></svg>
|
After Width: | Height: | Size: 304 B |
1
src/icons/expand.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z"/></svg>
|
After Width: | Height: | Size: 146 B |
51
src/icons/forget_room.svg
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="forget_room.svg"
|
||||
inkscape:version="">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="0.81355932"
|
||||
inkscape:cy="12.610169"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M3 6v18h18v-18h-18zm5 14c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm5 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm5 0c0 .552-.448 1-1 1s-1-.448-1-1v-10c0-.552.448-1 1-1s1 .448 1 1v10zm4-18v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.315c0 .901.73 2 1.631 2h5.712z"
|
||||
id="path2"
|
||||
style="fill:#ab0937;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
7
src/icons/hourglass.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g><path d="M926,68v-8.3c0-27.4-22.4-49.8-49.8-49.8H123.7C96.4,10,74,32.4,74,59.8V68c0,27.4,22.4,49.8,49.8,49.8H143c9.9,0,9.1,9.2,9.1,13.9c0,55.3,22.7,119.2,40,160c34.8,81.9,114.1,149,179.2,196.4c11.9,8.7,9,16.7,2.1,22.2c-63.1,50.6-146.1,115.2-181.3,198c-17.1,40.2-39.3,102.7-40,157.4c-0.1,5.6,2.2,16.5-11.2,16.5h-17.1C96.4,882.2,74,904.6,74,932v8.3c0,27.4,22.4,49.8,49.8,49.8h752.5c27.4,0,49.8-22.4,49.8-49.8V932c0-27.4-22.4-49.8-49.8-49.8h-10.8c-19.8,0-17.8-14.8-17.6-22.5c1.4-51.1-13.6-109.1-35.6-152.7c-48.9-97-125.9-158.7-173.4-194.4c-10.3-7.7-11.3-17.3,0-25.1C687.8,453.8,763.4,390,812.3,293c23.4-46.5,38.9-109.3,35.1-162.6c-0.3-4.3-0.6-12.6,10.6-12.6h18.3C903.6,117.8,926,95.4,926,68z M772.6,273c-45.5,90.3-118.4,154.8-181.9,193.8c-3.8,2.3-11.1,8.1-11.1,24.3v19.3c0,17.1,7.8,20.9,11.8,23.4c63.4,39,135.8,103.4,181.1,193.3c20.9,41.6,31.8,91.8,30.7,131.8c-0.2,8.3,3,23.4-18.7,23.4H214.9c-19,0-18.3-9.5-18.2-14.7c0.3-36.4,12.9-86.8,36.3-141.8c40.6-95.5,125.9-155.1,182.7-190.7c2.5-1.6,7.4-4.4,7.4-19.5v-28.9c0-15.6-7.5-21.7-11.4-24.2c-56.7-35.8-139-94.9-178.7-188.1c-23.7-55.6-36.2-106.5-36.3-142.9c0-4.8,0.6-13.6,11.2-13.6h582.9c11.8,0,11.6,8.9,12,13.6C806.1,172.7,795.2,228,772.6,273z"/><path d="M488.4,563.6l-194,242.7c-6.4,8-3.3,14.6,7,14.6h397.2c10.3,0,13.4-6.6,7-14.6l-194-242.7C505.2,555.6,494.8,555.6,488.4,563.6z"/><path d="M486.8,450.8c7.3,7.2,19.2,7.2,26.4,0l97.6-97.3c7.3-7.2,4.8-13.2-5.5-13.2H394.7c-10.3,0-12.7,5.9-5.5,13.2L486.8,450.8z"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
51
src/icons/invite_accept.svg
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="invite_accept.svg"
|
||||
inkscape:version="">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="-28.271186"
|
||||
inkscape:cy="12"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M9 21.035l-9-8.638 2.791-2.87 6.156 5.874 12.21-12.436 2.843 2.817z"
|
||||
id="path2"
|
||||
style="fill:#0d8967;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
51
src/icons/invite_decline.svg
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg14"
|
||||
sodipodi:docname="invite_decline.svg"
|
||||
inkscape:version="">
|
||||
<metadata
|
||||
id="metadata20">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs18" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview16"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="6.9152542"
|
||||
inkscape:cy="17.084746"
|
||||
inkscape:current-layer="svg14" />
|
||||
<path
|
||||
d="M23 20.168l-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"
|
||||
id="path12"
|
||||
style="fill:#ab0938;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
src/icons/join.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 9v-4l8 7-8 7v-4h-8v-6h8zm-2 10v-.083c-1.178.685-2.542 1.083-4 1.083-4.411 0-8-3.589-8-8s3.589-8 8-8c1.458 0 2.822.398 4 1.083v-2.245c-1.226-.536-2.577-.838-4-.838-5.522 0-10 4.477-10 10s4.478 10 10 10c1.423 0 2.774-.302 4-.838v-2.162z"/></svg>
|
After Width: | Height: | Size: 339 B |
1
src/icons/none.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>
|
After Width: | Height: | Size: 86 B |
1
src/icons/phone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.48 22.926l-1.193.658c-6.979 3.621-19.082-17.494-12.279-21.484l1.145-.637 3.714 6.467-1.139.632c-2.067 1.245 2.76 9.707 4.879 8.545l1.162-.642 3.711 6.461zm-9.808-22.926l-1.68.975 3.714 6.466 1.681-.975-3.715-6.466zm8.613 14.997l-1.68.975 3.714 6.467 1.681-.975-3.715-6.467z"/></svg>
|
After Width: | Height: | Size: 378 B |
1
src/icons/placeholder.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12z"/></svg>
|
After Width: | Height: | Size: 250 B |
1
src/icons/reduced_menu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 6h-24v-4h24v4zm0 4h-24v4h24v-4zm0 8h-24v4h24v-4z"/></svg>
|
After Width: | Height: | Size: 153 B |
1
src/icons/reduced_room_buttons.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 12c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm9 0c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm9 0c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/></svg>
|
After Width: | Height: | Size: 288 B |
1
src/icons/room_view_files.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22 13v-13h-20v24h8.409c4.857 0 3.335-8 3.335-8 3.009.745 8.256.419 8.256-3zm-4-7h-12v-1h12v1zm0 3h-12v-1h12v1zm0 3h-12v-1h12v1zm-2.091 6.223c2.047.478 4.805-.279 6.091-1.179-1.494 1.998-5.23 5.708-7.432 6.881 1.156-1.168 1.563-4.234 1.341-5.702z"/></svg>
|
After Width: | Height: | Size: 347 B |
1
src/icons/room_view_history.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 12c0 6.627-5.373 12-12 12s-12-5.373-12-12h2c0 5.514 4.486 10 10 10s10-4.486 10-10-4.486-10-10-10c-2.777 0-5.287 1.141-7.099 2.977l2.061 2.061-6.962 1.354 1.305-7.013 2.179 2.18c2.172-2.196 5.182-3.559 8.516-3.559 6.627 0 12 5.373 12 12zm-13-6v8h7v-2h-5v-6h-2z"/></svg>
|
After Width: | Height: | Size: 364 B |
1
src/icons/room_view_members.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20.822 18.096c-3.439-.794-6.64-1.49-5.09-4.418 4.72-8.912 1.251-13.678-3.732-13.678-5.082 0-8.464 4.949-3.732 13.678 1.597 2.945-1.725 3.641-5.09 4.418-3.073.71-3.188 2.236-3.178 4.904l.004 1h23.99l.004-.969c.012-2.688-.092-4.222-3.176-4.935z"/></svg>
|
After Width: | Height: | Size: 344 B |
1
src/icons/room_view_notifications.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.137 3.945c-.644-.374-1.042-1.07-1.041-1.82v-.003c.001-1.172-.938-2.122-2.096-2.122s-2.097.95-2.097 2.122v.003c.001.751-.396 1.446-1.041 1.82-4.667 2.712-1.985 11.715-6.862 13.306v1.749h20v-1.749c-4.877-1.591-2.195-10.594-6.863-13.306zm-3.137-2.945c.552 0 1 .449 1 1 0 .552-.448 1-1 1s-1-.448-1-1c0-.551.448-1 1-1zm3 20c0 1.598-1.392 3-2.971 3s-3.029-1.402-3.029-3h6z"/></svg>
|
After Width: | Height: | Size: 471 B |
1
src/icons/room_view_settings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 18h-2v5h-2v-5h-2v-3h6v3zm-2-17h-2v12h2v-12zm11 7h-6v3h2v12h2v-12h2v-3zm-2-7h-2v5h2v-5zm11 14h-6v3h2v5h2v-5h2v-3zm-2-14h-2v12h2v-12z"/></svg>
|
After Width: | Height: | Size: 235 B |
1
src/icons/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.822 20.88l-6.353-6.354c.93-1.465 1.467-3.2 1.467-5.059.001-5.219-4.247-9.467-9.468-9.467s-9.468 4.248-9.468 9.468c0 5.221 4.247 9.469 9.468 9.469 1.768 0 3.421-.487 4.839-1.333l6.396 6.396 3.119-3.12zm-20.294-11.412c0-3.273 2.665-5.938 5.939-5.938 3.275 0 5.94 2.664 5.94 5.938 0 3.275-2.665 5.939-5.94 5.939-3.274 0-5.939-2.664-5.939-5.939z"/></svg>
|
After Width: | Height: | Size: 446 B |
1
src/icons/set_status.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9.963 8.261c-.566-.585-.536-1.503.047-2.07l5.948-5.768c.291-.281.664-.423 1.035-.423.376 0 .75.146 1.035.44l-8.065 7.821zm-9.778 14.696c-.123.118-.185.277-.185.436 0 .333.271.607.607.607.152 0 .305-.057.423-.171l.999-.972-.845-.872-.999.972zm8.44-11.234l-3.419 3.314c-1.837 1.781-2.774 3.507-3.64 5.916l1.509 1.559c2.434-.79 4.187-1.673 6.024-3.455l3.418-3.315-3.892-4.019zm9.97-10.212l-8.806 8.54 4.436 4.579 8.806-8.538c.645-.626.969-1.458.969-2.291 0-2.784-3.373-4.261-5.405-2.29z"/></svg>
|
After Width: | Height: | Size: 585 B |
1
src/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>
|
After Width: | Height: | Size: 811 B |
1
src/icons/status.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="12"/></svg>
|
After Width: | Height: | Size: 121 B |
51
src/icons/unknown_devices_inspect.svg
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="unknown_devices_inspect.svg"
|
||||
inkscape:version="">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.8756036"
|
||||
inkscape:cx="-105.68924"
|
||||
inkscape:cy="18.344365"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M23.822 20.88l-6.353-6.354c.93-1.465 1.467-3.2 1.467-5.059.001-5.219-4.247-9.467-9.468-9.467s-9.468 4.248-9.468 9.468c0 5.221 4.247 9.469 9.468 9.469 1.768 0 3.421-.487 4.839-1.333l6.396 6.396 3.119-3.12zm-20.294-11.412c0-3.273 2.665-5.938 5.939-5.938 3.275 0 5.94 2.664 5.94 5.938 0 3.275-2.665 5.939-5.94 5.939-3.274 0-5.939-2.664-5.939-5.939z"
|
||||
id="path2"
|
||||
style="fill:#9a8308;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
src/icons/unknown_devices_warning.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 10v-4c0-3.313-2.687-6-6-6s-6 2.687-6 6v4h-3v14h18v-14h-3zm-5 7.723v2.277h-2v-2.277c-.595-.347-1-.984-1-1.723 0-1.104.896-2 2-2s2 .896 2 2c0 .738-.404 1.376-1 1.723zm-5-7.723v-4c0-2.206 1.794-4 4-4 2.205 0 4 1.794 4 4v4h-8z"/></svg>
|
After Width: | Height: | Size: 327 B |
1
src/icons/username.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20.822 18.096c-3.439-.794-6.64-1.49-5.09-4.418 4.72-8.912 1.251-13.678-3.732-13.678-5.082 0-8.464 4.949-3.732 13.678 1.597 2.945-1.725 3.641-5.09 4.418-3.073.71-3.188 2.236-3.178 4.904l.004 1h23.99l.004-.969c.012-2.688-.092-4.222-3.176-4.935z"/></svg>
|
After Width: | Height: | Size: 344 B |
BIN
src/images/login_background.jpg
Normal file
After Width: | Height: | Size: 1.8 MiB |
22
src/matrix_client.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from typing import Optional
|
||||
|
||||
import nio
|
||||
|
||||
|
||||
class MatrixClient(nio.AsyncClient):
|
||||
def __init__(self,
|
||||
user: str,
|
||||
homeserver: str = "https://matrix.org",
|
||||
device_id: Optional[str] = None) -> None:
|
||||
|
||||
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
|
||||
type(self).__name__, self.user_id, self.homeserver, self.device_id
|
||||
)
|
||||
|
||||
|
||||
async def resume(self, user_id: str, token: str, device_id: str) -> None:
|
||||
self.receive_response(nio.LoginResponse(user_id, device_id, token))
|
45
src/qml/Base/HAvatar.qml
Normal file
@@ -0,0 +1,45 @@
|
||||
import QtQuick 2.7
|
||||
import "../Base"
|
||||
|
||||
Rectangle {
|
||||
property var name: null
|
||||
property var imageUrl: null
|
||||
property int dimension: HStyle.avatar.size
|
||||
property bool hidden: false
|
||||
|
||||
width: dimension
|
||||
height: hidden ? 1 : dimension
|
||||
implicitWidth: dimension
|
||||
implicitHeight: hidden ? 1 : dimension
|
||||
|
||||
opacity: hidden ? 0 : 1
|
||||
|
||||
color: name ?
|
||||
Qt.hsla(
|
||||
Backend.hueFromString(name),
|
||||
HStyle.avatar.background.saturation,
|
||||
HStyle.avatar.background.lightness,
|
||||
HStyle.avatar.background.alpha
|
||||
) :
|
||||
HStyle.avatar.background.unknown
|
||||
|
||||
HLabel {
|
||||
z: 1
|
||||
anchors.centerIn: parent
|
||||
visible: ! hidden
|
||||
|
||||
text: name ? name.charAt(0) : "?"
|
||||
color: HStyle.avatar.letter
|
||||
font.pixelSize: parent.height / 1.4
|
||||
}
|
||||
|
||||
HImage {
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
visible: ! hidden && imageUrl
|
||||
|
||||
Component.onCompleted: if (imageUrl) { source = imageUrl }
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: dimension
|
||||
}
|
||||
}
|
138
src/qml/Base/HButton.qml
Normal file
@@ -0,0 +1,138 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
Button {
|
||||
property int horizontalMargin: 0
|
||||
property int verticalMargin: 0
|
||||
|
||||
property string iconName: ""
|
||||
property var iconDimension: null
|
||||
property var iconTransform: null
|
||||
property bool circle: false
|
||||
|
||||
property int fontSize: HStyle.fontSize.normal
|
||||
property color backgroundColor: HStyle.controls.button.background
|
||||
property alias overlayOpacity: buttonBackgroundOverlay.opacity
|
||||
property bool checkedLightens: false
|
||||
|
||||
property bool loading: false
|
||||
|
||||
property int contentWidth: 0
|
||||
|
||||
readonly property alias visibility: button.visible
|
||||
onVisibilityChanged: if (! visibility) { loading = false }
|
||||
|
||||
signal canceled
|
||||
signal clicked
|
||||
signal doubleClicked
|
||||
signal entered
|
||||
signal exited
|
||||
signal pressAndHold
|
||||
signal pressed
|
||||
signal released
|
||||
|
||||
function loadingUntilFutureDone(future) {
|
||||
loading = true
|
||||
future.onGotResult.connect(function() { loading = false })
|
||||
}
|
||||
|
||||
id: button
|
||||
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
color: Qt.lighter(
|
||||
backgroundColor, checked ? (checkedLightens ? 1.3 : 0.7) : 1.0
|
||||
)
|
||||
radius: circle ? height : 0
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: HStyle.animationDuration / 2 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: buttonBackgroundOverlay
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "black"
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: HStyle.animationDuration / 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: buttonContent
|
||||
|
||||
HRowLayout {
|
||||
id: contentLayout
|
||||
spacing: button.text && iconName ? 5 : 0
|
||||
Component.onCompleted: contentWidth = implicitWidth
|
||||
|
||||
HIcon {
|
||||
svgName: loading ? "hourglass" : iconName
|
||||
dimension: iconDimension || contentLayout.height
|
||||
transform: iconTransform
|
||||
|
||||
Layout.topMargin: verticalMargin
|
||||
Layout.bottomMargin: verticalMargin
|
||||
Layout.leftMargin: horizontalMargin
|
||||
Layout.rightMargin: horizontalMargin
|
||||
}
|
||||
|
||||
HLabel {
|
||||
text: button.text
|
||||
font.pixelSize: fontSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: loadingOverlay
|
||||
HRowLayout {
|
||||
HIcon {
|
||||
svgName: "hourglass"
|
||||
Layout.preferredWidth: contentWidth || -1
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Loader {
|
||||
sourceComponent:
|
||||
loading && ! iconName ? loadingOverlay : buttonContent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onCanceled: button.canceled()
|
||||
onClicked: button.clicked()
|
||||
onDoubleClicked: button.doubleClicked()
|
||||
onEntered: {
|
||||
overlayOpacity = checked ? 0 : 0.15
|
||||
button.entered()
|
||||
}
|
||||
onExited: {
|
||||
overlayOpacity = 0
|
||||
button.exited()
|
||||
}
|
||||
onPressAndHold: button.pressAndHold()
|
||||
onPressed: {
|
||||
overlayOpacity += 0.15
|
||||
button.pressed()
|
||||
}
|
||||
onReleased: {
|
||||
if (checkable) { checked = ! checked }
|
||||
overlayOpacity = checked ? 0 : 0.15
|
||||
button.released()
|
||||
}
|
||||
}
|
||||
}
|
11
src/qml/Base/HColumnLayout.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
spacing: 0
|
||||
|
||||
property int totalSpacing:
|
||||
spacing * Math.max(0, (columnLayout.visibleChildren.length - 1))
|
||||
}
|
10
src/qml/Base/HIcon.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
HImage {
|
||||
property var svgName: null
|
||||
property int dimension: 20
|
||||
|
||||
source: "../../icons/" + (svgName || "none") + ".svg"
|
||||
sourceSize.width: svgName ? dimension : 0
|
||||
sourceSize.height: svgName ? dimension : 0
|
||||
}
|
8
src/qml/Base/HImage.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
cache: true
|
||||
mipmap: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
60
src/qml/Base/HInterfaceBox.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
HScalingBox {
|
||||
id: interfaceBox
|
||||
|
||||
property alias title: interfaceTitle.text
|
||||
property alias buttonModel: interfaceButtonsRepeater.model
|
||||
property var buttonCallbacks: []
|
||||
property string enterButtonTarget: ""
|
||||
|
||||
default property alias body: interfaceBody.children
|
||||
|
||||
function clickEnterButtonTarget() {
|
||||
for (var i = 0; i < buttonModel.length; i++) {
|
||||
var btn = interfaceButtonsRepeater.itemAt(i)
|
||||
if (btn.name === enterButtonTarget) { btn.clicked() }
|
||||
}
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
anchors.fill: parent
|
||||
id: mainColumn
|
||||
|
||||
HRowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.margins: interfaceBox.margins
|
||||
|
||||
HLabel {
|
||||
id: interfaceTitle
|
||||
font.pixelSize: HStyle.fontSize.big
|
||||
}
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HColumnLayout { id: interfaceBody }
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HRowLayout {
|
||||
Repeater {
|
||||
id: interfaceButtonsRepeater
|
||||
model: []
|
||||
|
||||
HButton {
|
||||
property string name: modelData.name
|
||||
|
||||
id: button
|
||||
text: modelData.text
|
||||
iconName: modelData.iconName || ""
|
||||
onClicked: buttonCallbacks[modelData.name](button)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.avatar.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/qml/Base/HLabel.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Label {
|
||||
font.family: HStyle.fontFamily.sans
|
||||
font.pixelSize: HStyle.fontSize.normal
|
||||
textFormat: Label.PlainText
|
||||
|
||||
color: HStyle.colors.foreground
|
||||
style: Label.Outline
|
||||
styleColor: HStyle.colors.textBorder
|
||||
}
|
57
src/qml/Base/HListModel.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
ListModel {
|
||||
// To initialize a HListModel with items,
|
||||
// use `Component.onCompleted: extend([{"foo": 1, "bar": 2}, ...])`
|
||||
|
||||
id: listModel
|
||||
|
||||
function extend(new_items) {
|
||||
for (var i = 0; i < new_items.length; i++) {
|
||||
listModel.append(new_items[i])
|
||||
}
|
||||
}
|
||||
|
||||
function getIndices(where_role, is, max) { // max: undefined or int
|
||||
var results = []
|
||||
|
||||
for (var i = 0; i < listModel.count; i++) {
|
||||
if (listModel.get(i)[where_role] == is) {
|
||||
results.push(i)
|
||||
|
||||
if (max && results.length >= max) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function getWhere(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]))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function upsert(where_role, is, new_item) {
|
||||
// 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 {
|
||||
listModel.set(indices[0], new_item)
|
||||
}
|
||||
}
|
||||
|
||||
function pop(index) {
|
||||
var item = listModel.get(index)
|
||||
listModel.remove(index)
|
||||
return item
|
||||
}
|
||||
}
|
24
src/qml/Base/HListView.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
ListView {
|
||||
property int duration: HStyle.animationDuration
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation { properties: "x,y"; from: 100; duration: duration }
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
NumberAnimation { properties: "x,y"; duration: duration }
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
NumberAnimation { properties: "x,y"; duration: duration }
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
ParallelAnimation {
|
||||
NumberAnimation { property: "opacity"; to: 0; duration: duration }
|
||||
NumberAnimation { properties: "x,y"; to: 100; duration: duration }
|
||||
}
|
||||
}
|
||||
}
|
34
src/qml/Base/HNoticePage.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRowLayout {
|
||||
property alias label: noticeLabel
|
||||
property alias text: noticeLabel.text
|
||||
property alias color: noticeLabel.color
|
||||
property alias font: noticeLabel.font
|
||||
property alias backgroundColor: noticeLabelBackground.color
|
||||
property alias radius: noticeLabelBackground.radius
|
||||
|
||||
HLabel {
|
||||
id: noticeLabel
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
padding: 3
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
|
||||
Layout.margins: 10
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.maximumWidth:
|
||||
parent.width - Layout.leftMargin - Layout.rightMargin
|
||||
|
||||
opacity: width > Layout.leftMargin + Layout.rightMargin ? 1 : 0
|
||||
|
||||
background: Rectangle {
|
||||
id: noticeLabelBackground
|
||||
color: HStyle.box.background
|
||||
radius: HStyle.box.radius
|
||||
}
|
||||
}
|
||||
}
|
6
src/qml/Base/HRectangle.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
Rectangle {
|
||||
id: rectangle
|
||||
color: HStyle.sidePane.background
|
||||
}
|
22
src/qml/Base/HRichLabel.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
HLabel {
|
||||
id: label
|
||||
textFormat: Text.RichText
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onPositionChanged: function (event) {
|
||||
cursorShape = label.linkAt(event.x, event.y) ?
|
||||
Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
|
||||
onClicked: function(event) {
|
||||
var link = label.linkAt(event.x, event.y)
|
||||
if (link) { Qt.openUrlExternally(link) }
|
||||
}
|
||||
}
|
||||
}
|
10
src/qml/Base/HRowLayout.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
spacing: 0
|
||||
|
||||
property int totalSpacing:
|
||||
spacing * Math.max(0, (rowLayout.visibleChildren.length - 1))
|
||||
}
|
15
src/qml/Base/HScalingBox.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
HRectangle {
|
||||
property real widthForHeight: 0.75
|
||||
property int baseHeight: 300
|
||||
property int startScalingUpAboveHeight: 1080
|
||||
|
||||
readonly property int baseWidth: baseHeight * widthForHeight
|
||||
readonly property int margins: baseHeight * 0.03
|
||||
|
||||
color: HStyle.box.background
|
||||
height: Math.min(parent.height, baseHeight)
|
||||
width: Math.min(parent.width, baseWidth)
|
||||
scale: Math.max(1, parent.height / startScalingUpAboveHeight)
|
||||
}
|
33
src/qml/Base/HScrollableTextArea.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
ScrollView {
|
||||
property alias backgroundColor: textAreaBackground.color
|
||||
property alias placeholderText: textArea.placeholderText
|
||||
property alias text: textArea.text
|
||||
property alias area: textArea
|
||||
|
||||
default property alias textAreaData: textArea.data
|
||||
|
||||
id: scrollView
|
||||
clip: true
|
||||
|
||||
TextArea {
|
||||
id: textArea
|
||||
readOnly: ! visible
|
||||
selectByMouse: true
|
||||
|
||||
wrapMode: TextEdit.Wrap
|
||||
font.family: HStyle.fontFamily.sans
|
||||
font.pixelSize: HStyle.fontSize.normal
|
||||
|
||||
color: HStyle.colors.foreground
|
||||
background: Rectangle {
|
||||
id: textAreaBackground
|
||||
color: HStyle.controls.textArea.background
|
||||
}
|
||||
|
||||
Keys.forwardTo: [scrollView]
|
||||
}
|
||||
}
|
||||
|
7
src/qml/Base/HSpacer.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
24
src/qml/Base/HSplitView.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 1.4 as Controls1
|
||||
|
||||
//https://doc.qt.io/qt-5/qml-qtquick-controls-splitview.html
|
||||
Controls1.SplitView {
|
||||
id: splitView
|
||||
|
||||
property bool anyHovered: false
|
||||
property bool anyPressed: false
|
||||
property bool anyResizing: false
|
||||
|
||||
property bool canAutoSize: true
|
||||
onAnyPressedChanged: canAutoSize = false
|
||||
|
||||
handleDelegate: Item {
|
||||
readonly property bool hovered: styleData.hovered
|
||||
readonly property bool pressed: styleData.pressed
|
||||
readonly property bool resizing: styleData.resizing
|
||||
|
||||
onHoveredChanged: splitView.anyHovered = hovered
|
||||
onPressedChanged: splitView.anyPressed = pressed
|
||||
onResizingChanged: splitView.anyResizing = resizing
|
||||
}
|
||||
}
|
11
src/qml/Base/HStatusAvatar.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
HAvatar {
|
||||
HImage {
|
||||
id: status
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
source: "../../icons/status.svg"
|
||||
sourceSize.width: 12
|
||||
}
|
||||
}
|
139
src/qml/Base/HStyle.qml
Normal file
@@ -0,0 +1,139 @@
|
||||
pragma Singleton
|
||||
import QtQuick 2.7
|
||||
|
||||
QtObject {
|
||||
id: style
|
||||
|
||||
property int animationDuration: 100
|
||||
|
||||
readonly property QtObject fontSize: QtObject {
|
||||
property int smallest: 6
|
||||
property int smaller: 8
|
||||
property int small: 12
|
||||
property int normal: 16
|
||||
property int big: 24
|
||||
property int bigger: 32
|
||||
property int biggest: 48
|
||||
}
|
||||
|
||||
readonly property QtObject fontFamily: QtObject {
|
||||
property string sans: "SFNS Display"
|
||||
property string serif: "Roboto Slab"
|
||||
property string mono: "Hack"
|
||||
}
|
||||
|
||||
property int radius: 5
|
||||
|
||||
readonly property QtObject colors: QtObject {
|
||||
property color background0: Qt.hsla(0, 0, 0.8, 0.5)
|
||||
property color background1: Qt.hsla(0, 0, 0.8, 0.7)
|
||||
property color foreground: "black"
|
||||
property color foregroundDim: Qt.hsla(0, 0, 0.2, 1)
|
||||
property color foregroundError: Qt.hsla(0.95, 0.64, 0.32, 1)
|
||||
property color textBorder: Qt.hsla(0, 0, 0, 0.07)
|
||||
}
|
||||
|
||||
readonly property QtObject controls: QtObject {
|
||||
readonly property QtObject button: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject textField: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject textArea: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
}
|
||||
|
||||
readonly property QtObject sidePane: QtObject {
|
||||
property color background: colors.background1
|
||||
|
||||
readonly property QtObject settingsButton: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject filterRooms: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
}
|
||||
|
||||
readonly property QtObject chat: QtObject {
|
||||
readonly property QtObject selectViewBar: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject roomHeader: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject roomEventList: QtObject {
|
||||
property color background: "transparent"
|
||||
}
|
||||
|
||||
readonly property QtObject message: QtObject {
|
||||
property color background: colors.background1
|
||||
property color body: colors.foreground
|
||||
property color date: colors.foregroundDim
|
||||
}
|
||||
|
||||
readonly property QtObject event: QtObject {
|
||||
property color background: colors.background1
|
||||
property real saturation: 0.22
|
||||
property real lightness: 0.24
|
||||
property color date: colors.foregroundDim
|
||||
}
|
||||
|
||||
readonly property QtObject daybreak: QtObject {
|
||||
property color background: colors.background1
|
||||
property color foreground: colors.foreground
|
||||
property int radius: style.radius
|
||||
}
|
||||
|
||||
readonly property QtObject inviteBanner: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject leftBanner: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject unknownDevices: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
|
||||
readonly property QtObject typingMembers: QtObject {
|
||||
property color background: colors.background0
|
||||
}
|
||||
|
||||
readonly property QtObject sendBox: QtObject {
|
||||
property color background: colors.background1
|
||||
}
|
||||
}
|
||||
|
||||
readonly property QtObject box: QtObject {
|
||||
property color background: colors.background0
|
||||
property int radius: style.radius
|
||||
}
|
||||
|
||||
readonly property QtObject avatar: QtObject {
|
||||
property int size: 36
|
||||
property int radius: style.radius
|
||||
property color letter: "white"
|
||||
|
||||
readonly property QtObject background: QtObject {
|
||||
property real saturation: 0.22
|
||||
property real lightness: 0.5
|
||||
property real alpha: 1
|
||||
property color unknown: Qt.hsla(0, 0, 0.22, 1)
|
||||
}
|
||||
}
|
||||
|
||||
readonly property QtObject displayName: QtObject {
|
||||
property real saturation: 0.32
|
||||
property real lightness: 0.3
|
||||
}
|
||||
|
||||
property int bottomElementsHeight: 32
|
||||
}
|
17
src/qml/Base/HTextField.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
TextField {
|
||||
property alias backgroundColor: textFieldBackground.color
|
||||
|
||||
font.family: HStyle.fontFamily.sans
|
||||
font.pixelSize: HStyle.fontSize.normal
|
||||
|
||||
color: HStyle.colors.foreground
|
||||
background: Rectangle {
|
||||
id: textFieldBackground
|
||||
color: HStyle.controls.textField.background
|
||||
}
|
||||
|
||||
selectByMouse: true
|
||||
}
|
1
src/qml/Base/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton HStyle 1.0 HStyle.qml
|
90
src/qml/Chat/Banners/Banner.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
id: banner
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
|
||||
property alias avatar: bannerAvatar
|
||||
property alias icon: bannerIcon
|
||||
property alias labelText: bannerLabel.text
|
||||
property alias buttonModel: bannerRepeater.model
|
||||
property var buttonCallbacks: []
|
||||
|
||||
HRowLayout {
|
||||
id: bannerRow
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: bannerAvatar
|
||||
dimension: banner.Layout.preferredHeight
|
||||
}
|
||||
|
||||
HIcon {
|
||||
id: bannerIcon
|
||||
dimension: bannerLabel.implicitHeight
|
||||
visible: Boolean(svgName)
|
||||
|
||||
Layout.leftMargin: 5
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: bannerLabel
|
||||
textFormat: Text.StyledText
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
|
||||
visible:
|
||||
bannerRow.width - bannerAvatar.width - bannerButtons.width > 30
|
||||
|
||||
Layout.maximumWidth:
|
||||
bannerRow.width -
|
||||
bannerAvatar.width - bannerButtons.width -
|
||||
Layout.leftMargin - Layout.rightMargin
|
||||
|
||||
Layout.leftMargin: 5
|
||||
Layout.rightMargin: Layout.leftMargin
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HRowLayout {
|
||||
id: bannerButtons
|
||||
|
||||
function getButtonsWidth() {
|
||||
var total = 0
|
||||
|
||||
for (var i = 0; i < bannerRepeater.count; i++) {
|
||||
total += bannerRepeater.itemAt(i).implicitWidth
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
property bool compact:
|
||||
bannerRow.width <
|
||||
bannerAvatar.width +
|
||||
bannerLabel.implicitWidth +
|
||||
bannerLabel.Layout.leftMargin +
|
||||
bannerLabel.Layout.rightMargin +
|
||||
getButtonsWidth()
|
||||
|
||||
Repeater {
|
||||
id: bannerRepeater
|
||||
model: []
|
||||
|
||||
HButton {
|
||||
id: button
|
||||
text: modelData.text
|
||||
iconName: modelData.iconName
|
||||
onClicked: buttonCallbacks[modelData.name](button)
|
||||
|
||||
Layout.maximumWidth: bannerButtons.compact ? height : -1
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
src/qml/Chat/Banners/InviteBanner.qml
Normal file
@@ -0,0 +1,41 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
Banner {
|
||||
property var inviter: null
|
||||
|
||||
color: HStyle.chat.inviteBanner.background
|
||||
|
||||
avatar.name: inviter ? inviter.displayname : ""
|
||||
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
||||
|
||||
labelText:
|
||||
(inviter ?
|
||||
("<b>" + inviter.displayname + "</b>") : qsTr("Someone")) +
|
||||
" " + qsTr("invited you to join the room.")
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "accept",
|
||||
text: qsTr("Accept"),
|
||||
iconName: "invite_accept",
|
||||
},
|
||||
{
|
||||
name: "decline",
|
||||
text: qsTr("Decline"),
|
||||
iconName: "invite_decline",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"accept": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).joinRoom(chatPage.roomId)
|
||||
},
|
||||
|
||||
"decline": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).leaveRoom(chatPage.roomId)
|
||||
}
|
||||
}
|
||||
}
|
28
src/qml/Chat/Banners/LeftBanner.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Banner {
|
||||
property var leftEvent: null
|
||||
|
||||
color: HStyle.chat.leftBanner.background
|
||||
|
||||
avatar.name: ChatJS.getLeftBannerAvatarName(leftEvent, chatPage.userId)
|
||||
labelText: ChatJS.getLeftBannerText(leftEvent)
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "forget",
|
||||
text: qsTr("Forget"),
|
||||
iconName: "forget_room",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"forget": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).forgetRoom(chatPage.roomId)
|
||||
pageStack.clear()
|
||||
},
|
||||
}
|
||||
}
|
25
src/qml/Chat/Banners/UnknownDevicesBanner.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Banner {
|
||||
color: HStyle.chat.unknownDevices.background
|
||||
|
||||
avatar.visible: false
|
||||
icon.svgName: "unknown_devices_warning"
|
||||
labelText: "Unknown devices are present in this encrypted room."
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "inspect",
|
||||
text: qsTr("Inspect"),
|
||||
iconName: "unknown_devices_inspect",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"inspect": function(button) {
|
||||
print("show")
|
||||
},
|
||||
}
|
||||
}
|
148
src/qml/Chat/Chat.qml
Normal file
@@ -0,0 +1,148 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
import "Banners"
|
||||
import "RoomEventList"
|
||||
import "RoomSidePane"
|
||||
|
||||
HColumnLayout {
|
||||
property string userId: ""
|
||||
property string category: ""
|
||||
property string roomId: ""
|
||||
|
||||
readonly property var roomInfo:
|
||||
Backend.accounts.get(userId)
|
||||
.roomCategories.get(category)
|
||||
.rooms.get(roomId)
|
||||
|
||||
readonly property var sender: Backend.users.get(userId)
|
||||
|
||||
readonly property bool hasUnknownDevices:
|
||||
category == "Rooms" ?
|
||||
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
|
||||
|
||||
id: chatPage
|
||||
onFocusChanged: sendBox.setFocus()
|
||||
|
||||
Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
|
||||
function(forUserId, forRoomId, previous, now) {
|
||||
if (chatPage && forUserId == userId && forRoomId == roomId) {
|
||||
chatPage.category = now
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
RoomHeader {
|
||||
id: roomHeader
|
||||
displayName: roomInfo.displayName
|
||||
topic: roomInfo.topic || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.avatar.size
|
||||
}
|
||||
|
||||
|
||||
HSplitView {
|
||||
id: chatSplitView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RoomEventList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
TypingMembersBar {}
|
||||
|
||||
InviteBanner {
|
||||
visible: category === "Invites"
|
||||
inviter: roomInfo.inviter
|
||||
}
|
||||
|
||||
UnknownDevicesBanner {
|
||||
visible: category == "Rooms" && hasUnknownDevices
|
||||
}
|
||||
|
||||
SendBox {
|
||||
id: sendBox
|
||||
visible: category == "Rooms" && ! hasUnknownDevices
|
||||
}
|
||||
|
||||
LeftBanner {
|
||||
visible: category === "Left"
|
||||
leftEvent: roomInfo.leftEvent
|
||||
}
|
||||
}
|
||||
|
||||
RoomSidePane {
|
||||
id: roomSidePane
|
||||
|
||||
activeView: roomHeader.activeButton
|
||||
property int oldWidth: width
|
||||
onActiveViewChanged:
|
||||
activeView ? restoreAnimation.start() : hideAnimation.start()
|
||||
|
||||
NumberAnimation {
|
||||
id: hideAnimation
|
||||
target: roomSidePane
|
||||
properties: "width"
|
||||
duration: HStyle.animationDuration
|
||||
from: target.width
|
||||
to: 0
|
||||
|
||||
onStarted: {
|
||||
target.oldWidth = target.width
|
||||
target.Layout.minimumWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: restoreAnimation
|
||||
target: roomSidePane
|
||||
properties: "width"
|
||||
duration: HStyle.animationDuration
|
||||
from: 0
|
||||
to: target.oldWidth
|
||||
|
||||
onStopped: target.Layout.minimumWidth = Qt.binding(
|
||||
function() { return HStyle.avatar.size }
|
||||
)
|
||||
}
|
||||
|
||||
collapsed: width < HStyle.avatar.size + 8
|
||||
|
||||
property bool wasSnapped: false
|
||||
property int referenceWidth: roomHeader.buttonsWidth
|
||||
onReferenceWidthChanged: {
|
||||
if (chatSplitView.canAutoSize || wasSnapped) {
|
||||
if (wasSnapped) { chatSplitView.canAutoSize = true }
|
||||
width = referenceWidth
|
||||
}
|
||||
}
|
||||
|
||||
property int currentWidth: width
|
||||
onCurrentWidthChanged: {
|
||||
if (referenceWidth != width &&
|
||||
referenceWidth - 15 < width &&
|
||||
width < referenceWidth + 15)
|
||||
{
|
||||
currentWidth = referenceWidth
|
||||
width = referenceWidth
|
||||
wasSnapped = true
|
||||
currentWidth = Qt.binding(
|
||||
function() { return roomSidePane.width }
|
||||
)
|
||||
} else {
|
||||
wasSnapped = false
|
||||
}
|
||||
}
|
||||
|
||||
width: referenceWidth // Initial width
|
||||
Layout.minimumWidth: HStyle.avatar.size
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
||||
}
|
9
src/qml/Chat/RoomEventList/Daybreak.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: dateTime.toLocaleDateString()
|
||||
color: HStyle.chat.daybreak.foreground
|
||||
backgroundColor: HStyle.chat.daybreak.background
|
||||
radius: HStyle.chat.daybreak.radius
|
||||
}
|
53
src/qml/Chat/RoomEventList/EventContent.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Row {
|
||||
id: eventContent
|
||||
spacing: standardSpacing / 2
|
||||
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
width: Math.min(
|
||||
roomEventListView.width - avatar.width - eventContent.spacing,
|
||||
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||
contentLabel.implicitWidth
|
||||
)
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: sender.displayName.value
|
||||
hidden: combine
|
||||
dimension: 28
|
||||
}
|
||||
|
||||
HLabel {
|
||||
width: parent.width
|
||||
|
||||
id: contentLabel
|
||||
text: "<font color='" +
|
||||
Qt.hsla(Backend.hueFromString(sender.displayName.value),
|
||||
HStyle.chat.event.saturation,
|
||||
HStyle.chat.event.lightness,
|
||||
1) +
|
||||
"'>" +
|
||||
sender.displayName.value + " " +
|
||||
ChatJS.getEventText(type, dict) +
|
||||
|
||||
" " +
|
||||
"<font size=" + HStyle.fontSize.small + "px " +
|
||||
"color=" + HStyle.chat.event.date + ">" +
|
||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||
"</font> " +
|
||||
"</font>"
|
||||
|
||||
textFormat: Text.RichText
|
||||
background: Rectangle {color: HStyle.chat.event.background}
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: verticalPadding
|
||||
bottomPadding: verticalPadding
|
||||
}
|
||||
}
|
80
src/qml/Chat/RoomEventList/MessageContent.qml
Normal file
@@ -0,0 +1,80 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
Row {
|
||||
id: messageContent
|
||||
spacing: standardSpacing / 2
|
||||
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
hidden: combine
|
||||
name: sender.displayName.value
|
||||
dimension: 48
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: HStyle.chat.message.background
|
||||
|
||||
//width: nameLabel.implicitWidth
|
||||
width: Math.min(
|
||||
roomEventListView.width - avatar.width - messageContent.spacing,
|
||||
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||
Math.max(
|
||||
nameLabel.visible ? nameLabel.implicitWidth : 0,
|
||||
contentLabel.implicitWidth
|
||||
)
|
||||
)
|
||||
height: nameLabel.height + contentLabel.implicitHeight
|
||||
|
||||
Column {
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
HLabel {
|
||||
height: combine ? 0 : implicitHeight
|
||||
width: parent.width
|
||||
visible: height > 0
|
||||
|
||||
id: nameLabel
|
||||
text: sender.displayName.value
|
||||
color: Qt.hsla(Backend.hueFromString(text),
|
||||
HStyle.displayName.saturation,
|
||||
HStyle.displayName.lightness,
|
||||
1)
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: isOwn ? Text.AlignRight : Text.AlignLeft
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: verticalPadding
|
||||
}
|
||||
|
||||
HRichLabel {
|
||||
width: parent.width
|
||||
|
||||
id: contentLabel
|
||||
text: (dict.formatted_body ?
|
||||
Backend.htmlFilter.filter(dict.formatted_body) :
|
||||
dict.body) +
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px color=" + HStyle.chat.message.date + ">" +
|
||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||
"</font>" +
|
||||
(isLocalEcho ?
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px>⏳</font>" : "")
|
||||
textFormat: Text.RichText
|
||||
color: HStyle.chat.message.body
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: nameLabel.visible ? 0 : verticalPadding
|
||||
bottomPadding: verticalPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
src/qml/Chat/RoomEventList/RoomEventDelegate.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Column {
|
||||
id: roomEventDelegate
|
||||
|
||||
function minsBetween(date1, date2) {
|
||||
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
|
||||
}
|
||||
|
||||
function getIsMessage(type_) { return type_.startsWith("RoomMessage") }
|
||||
|
||||
function getPreviousItem() {
|
||||
return index < roomEventListView.model.count - 1 ?
|
||||
roomEventListView.model.get(index + 1) : null
|
||||
}
|
||||
|
||||
property var previousItem: getPreviousItem()
|
||||
signal reloadPreviousItem()
|
||||
onReloadPreviousItem: previousItem = getPreviousItem()
|
||||
|
||||
readonly property bool isMessage: getIsMessage(type)
|
||||
|
||||
readonly property bool isUndecryptableEvent:
|
||||
type === "OlmEvent" || type === "MegolmEvent"
|
||||
|
||||
readonly property var sender: Backend.users.get(dict.sender)
|
||||
|
||||
readonly property bool isOwn:
|
||||
chatPage.userId === dict.sender
|
||||
|
||||
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
||||
|
||||
readonly property bool combine:
|
||||
previousItem &&
|
||||
! talkBreak &&
|
||||
! dayBreak &&
|
||||
getIsMessage(previousItem.type) === isMessage &&
|
||||
previousItem.dict.sender === dict.sender &&
|
||||
minsBetween(previousItem.dateTime, dateTime) <= 5
|
||||
|
||||
readonly property bool dayBreak:
|
||||
isFirstEvent ||
|
||||
previousItem &&
|
||||
dateTime.getDate() != previousItem.dateTime.getDate()
|
||||
|
||||
readonly property bool talkBreak:
|
||||
previousItem &&
|
||||
! dayBreak &&
|
||||
minsBetween(previousItem.dateTime, dateTime) >= 20
|
||||
|
||||
|
||||
property int standardSpacing: 16
|
||||
property int horizontalPadding: 6
|
||||
property int verticalPadding: 4
|
||||
|
||||
ListView.onAdd: {
|
||||
var nextDelegate = roomEventListView.contentItem.children[index]
|
||||
if (nextDelegate) { nextDelegate.reloadPreviousItem() }
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
|
||||
topPadding:
|
||||
isFirstEvent ? 0 :
|
||||
dayBreak ? standardSpacing * 2 :
|
||||
talkBreak ? standardSpacing * 3 :
|
||||
combine ? standardSpacing / 4 :
|
||||
standardSpacing
|
||||
|
||||
Loader {
|
||||
source: dayBreak ? "Daybreak.qml" : ""
|
||||
width: roomEventDelegate.width
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: dayBreak
|
||||
width: parent.width
|
||||
height: topPadding
|
||||
}
|
||||
|
||||
Loader {
|
||||
source: isMessage ? "MessageContent.qml" : "EventContent.qml"
|
||||
anchors.right: isOwn ? parent.right : undefined
|
||||
}
|
||||
}
|
43
src/qml/Chat/RoomEventList/RoomEventList.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
property int space: 8
|
||||
|
||||
color: HStyle.chat.roomEventList.background
|
||||
|
||||
HListView {
|
||||
id: roomEventListView
|
||||
delegate: RoomEventDelegate {}
|
||||
model: Backend.roomEvents.get(chatPage.roomId)
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: space
|
||||
anchors.rightMargin: space
|
||||
|
||||
topMargin: space
|
||||
bottomMargin: space
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
// Keep x scroll pages cached, to limit images having to be
|
||||
// reloaded from network.
|
||||
cacheBuffer: height * 6
|
||||
|
||||
// Declaring this "alias" provides the on... signal
|
||||
property real yPos: visibleArea.yPosition
|
||||
|
||||
onYPosChanged: {
|
||||
if (chatPage.category != "Invites" && yPos <= 0.1) {
|
||||
Backend.loadPastEvents(chatPage.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HNoticePage {
|
||||
text: qsTr("Nothing to show here yet...")
|
||||
|
||||
visible: roomEventListView.model.count < 1
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
102
src/qml/Chat/RoomHeader.qml
Normal file
@@ -0,0 +1,102 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
property string displayName: ""
|
||||
property string topic: ""
|
||||
|
||||
property alias buttonsImplicitWidth: viewButtons.implicitWidth
|
||||
property int buttonsWidth: viewButtons.Layout.preferredWidth
|
||||
property var activeButton: "members"
|
||||
|
||||
property bool collapseButtons: width < 400
|
||||
|
||||
id: roomHeader
|
||||
color: HStyle.chat.roomHeader.background
|
||||
|
||||
HRowLayout {
|
||||
id: row
|
||||
spacing: 8
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: displayName
|
||||
dimension: roomHeader.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: roomName
|
||||
text: displayName
|
||||
font.pixelSize: HStyle.fontSize.big
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: Math.max(
|
||||
0,
|
||||
row.width - row.totalSpacing - avatar.width -
|
||||
viewButtons.width -
|
||||
(expandButton.visible ? expandButton.width : 0)
|
||||
)
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: roomTopic
|
||||
text: topic
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: Math.max(
|
||||
0,
|
||||
row.width - row.totalSpacing - avatar.width -
|
||||
roomName.width - viewButtons.width -
|
||||
(expandButton.visible ? expandButton.width : 0)
|
||||
)
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
Row {
|
||||
id: viewButtons
|
||||
Layout.preferredWidth: collapseButtons ? 0 : implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
"members", "files", "notifications", "history", "settings"
|
||||
]
|
||||
HButton {
|
||||
iconName: "room_view_" + modelData
|
||||
iconDimension: 22
|
||||
autoExclusive: true
|
||||
checked: activeButton == modelData
|
||||
onClicked: activeButton = activeButton == modelData ?
|
||||
null : modelData
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
id: buttonsAnimation
|
||||
duration: HStyle.animationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HButton {
|
||||
id: expandButton
|
||||
z: 1
|
||||
anchors.right: parent.right
|
||||
opacity: collapseButtons ? 1 : 0
|
||||
visible: opacity > 0
|
||||
iconName: "reduced_room_buttons"
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: buttonsAnimation.duration * 2 }
|
||||
}
|
||||
}
|
||||
}
|
37
src/qml/Chat/RoomSidePane/MemberDelegate.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
MouseArea {
|
||||
id: memberDelegate
|
||||
width: memberList.width
|
||||
height: childrenRect.height
|
||||
|
||||
property var member: Backend.users.get(userId)
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
spacing: memberList.spacing
|
||||
|
||||
HAvatar {
|
||||
id: memberAvatar
|
||||
name: member.displayName.value
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth:
|
||||
parent.width - parent.totalSpacing - memberAvatar.width
|
||||
|
||||
HLabel {
|
||||
id: memberName
|
||||
text: member.displayName.value
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
src/qml/Chat/RoomSidePane/MembersView.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
property bool collapsed: false
|
||||
property int normalSpacing: collapsed ? 0 : 8
|
||||
|
||||
Behavior on normalSpacing {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
|
||||
HListView {
|
||||
id: memberList
|
||||
|
||||
spacing: normalSpacing
|
||||
topMargin: normalSpacing
|
||||
bottomMargin: normalSpacing
|
||||
Layout.leftMargin: normalSpacing
|
||||
Layout.rightMargin: normalSpacing
|
||||
|
||||
model: chatPage.roomInfo.sortedMembers
|
||||
delegate: MemberDelegate {}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: filterField
|
||||
placeholderText: qsTr("Filter members")
|
||||
backgroundColor: HStyle.sidePane.filterRooms.background
|
||||
|
||||
// Without this, if the user types in the field, changes of room, then
|
||||
// comes back, the field will be empty but the filter still applied.
|
||||
Component.onCompleted:
|
||||
text = Backend.clients.get(chatPage.userId).getMemberFilter(
|
||||
chatPage.category, chatPage.roomId
|
||||
)
|
||||
|
||||
onTextChanged: Backend.clients.get(chatPage.userId).setMemberFilter(
|
||||
chatPage.category, chatPage.roomId, text
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
}
|
||||
}
|
15
src/qml/Chat/RoomSidePane/RoomSidePane.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
id: roomSidePane
|
||||
|
||||
property bool collapsed: false
|
||||
property var activeView: null
|
||||
|
||||
MembersView {
|
||||
anchors.fill: parent
|
||||
collapsed: parent.collapsed
|
||||
}
|
||||
}
|
72
src/qml/Chat/SendBox.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
function setFocus() { textArea.forceActiveFocus() }
|
||||
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: HStyle.bottomElementsHeight
|
||||
Layout.preferredHeight: textArea.implicitHeight
|
||||
// parent.height / 2 causes binding loop?
|
||||
Layout.maximumHeight: pageStack.height / 2
|
||||
color: HStyle.chat.sendBox.background
|
||||
|
||||
HRowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: chatPage.sender.displayName.value
|
||||
dimension: root.Layout.minimumHeight
|
||||
}
|
||||
|
||||
HScrollableTextArea {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: textArea
|
||||
placeholderText: qsTr("Type a message...")
|
||||
backgroundColor: "transparent"
|
||||
area.focus: true
|
||||
|
||||
property bool textChangedSinceLostFocus: false
|
||||
|
||||
function setTyping(typing) {
|
||||
Backend.clients.get(chatPage.userId)
|
||||
.setTypingState(chatPage.roomId, typing)
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
setTyping(Boolean(text))
|
||||
textChangedSinceLostFocus = true
|
||||
}
|
||||
area.onEditingFinished: { // when lost focus
|
||||
if (text && textChangedSinceLostFocus) {
|
||||
setTyping(false)
|
||||
textChangedSinceLostFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
event.accepted = true
|
||||
|
||||
if (event.modifiers & Qt.ShiftModifier ||
|
||||
event.modifiers & Qt.ControlModifier ||
|
||||
event.modifiers & Qt.AltModifier) {
|
||||
textArea.insert(textArea.cursorPosition, "\n")
|
||||
return
|
||||
}
|
||||
|
||||
if (textArea.text === "") { return }
|
||||
Backend.clients.get(chatPage.userId)
|
||||
.sendMarkdown(chatPage.roomId, textArea.text)
|
||||
area.clear()
|
||||
}
|
||||
|
||||
// Numpad enter
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
}
|
||||
}
|
||||
}
|
23
src/qml/Chat/TypingMembersBar.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
import "utils.js" as ChatJS
|
||||
|
||||
HRectangle {
|
||||
property var typingMembers: chatPage.roomInfo.typingMembers
|
||||
|
||||
color: HStyle.chat.typingMembers.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
|
||||
HLabel {
|
||||
id: usersLabel
|
||||
anchors.fill: parent
|
||||
|
||||
text: ChatJS.getTypingMembersText(typingMembers, chatPage.userId)
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
210
src/qml/Chat/utils.js
Normal file
@@ -0,0 +1,210 @@
|
||||
function getEventText(type, dict) {
|
||||
switch (type) {
|
||||
case "RoomCreateEvent":
|
||||
return (dict.federate ? "allowed" : "blocked") +
|
||||
" users on other matrix servers " +
|
||||
(dict.federate ? "to join" : "from joining") +
|
||||
" this room."
|
||||
break
|
||||
|
||||
case "RoomGuestAccessEvent":
|
||||
return (dict.guest_access === "can_join" ? "allowed " : "forbad") +
|
||||
"guests to join the room."
|
||||
break
|
||||
|
||||
case "RoomJoinRulesEvent":
|
||||
return "made the room " +
|
||||
(dict.join_rule === "public." ? "public" : "invite only.")
|
||||
break
|
||||
|
||||
case "RoomHistoryVisibilityEvent":
|
||||
return getHistoryVisibilityEventText(dict)
|
||||
break
|
||||
|
||||
case "PowerLevelsEvent":
|
||||
return "changed the room's permissions."
|
||||
|
||||
case "RoomMemberEvent":
|
||||
return getMemberEventText(dict)
|
||||
break
|
||||
|
||||
case "RoomAliasEvent":
|
||||
return "set the room's main address to " +
|
||||
dict.canonical_alias + "."
|
||||
break
|
||||
|
||||
case "RoomNameEvent":
|
||||
return "changed the room's name to \"" + dict.name + "\"."
|
||||
break
|
||||
|
||||
case "RoomTopicEvent":
|
||||
return "changed the room's topic to \"" + dict.topic + "\"."
|
||||
break
|
||||
|
||||
case "RoomEncryptionEvent":
|
||||
return "turned on encryption for this room."
|
||||
break
|
||||
|
||||
case "OlmEvent":
|
||||
case "MegolmEvent":
|
||||
return "hasn't sent your device the keys to decrypt this message."
|
||||
|
||||
default:
|
||||
console.log(type + "\n" + JSON.stringify(dict, null, 4) + "\n")
|
||||
return "did something this client does not understand."
|
||||
|
||||
//case "CallEvent": TODO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getHistoryVisibilityEventText(dict) {
|
||||
switch (dict.history_visibility) {
|
||||
case "shared":
|
||||
var end = "all room members."
|
||||
break
|
||||
|
||||
case "world_readable":
|
||||
var end = "any member or outsider."
|
||||
break
|
||||
|
||||
case "joined":
|
||||
var end = "all room members, since the point they joined."
|
||||
break
|
||||
|
||||
case "invited":
|
||||
var end = "all room members, since the point they were invited."
|
||||
break
|
||||
}
|
||||
|
||||
return "made future history visible to " + end
|
||||
}
|
||||
|
||||
|
||||
function getStateDisplayName(dict) {
|
||||
// The dict.content.displayname may be outdated, prefer
|
||||
// retrieving it fresh
|
||||
return Backend.users.get(dict.state_key).displayName.value
|
||||
}
|
||||
|
||||
|
||||
function getMemberEventText(dict) {
|
||||
var info = dict.content, prev = dict.prev_content
|
||||
|
||||
if (! prev || (info.membership != prev.membership)) {
|
||||
var reason = info.reason ? (" Reason: " + info.reason) : ""
|
||||
|
||||
switch (info.membership) {
|
||||
case "join":
|
||||
return prev && prev.membership === "invite" ?
|
||||
"accepted the invitation." : "joined the room."
|
||||
break
|
||||
|
||||
case "invite":
|
||||
return "invited " + getStateDisplayName(dict) + " to the room."
|
||||
break
|
||||
|
||||
case "leave":
|
||||
if (dict.state_key === dict.sender) {
|
||||
return (prev && prev.membership === "invite" ?
|
||||
"declined the invitation." : "left the room.") +
|
||||
reason
|
||||
}
|
||||
|
||||
var name = getStateDisplayName(dict)
|
||||
return (prev && prev.membership === "invite" ?
|
||||
"withdrew " + name + "'s invitation." :
|
||||
|
||||
prev && prev.membership == "ban" ?
|
||||
"unbanned " + name + " from the room." :
|
||||
|
||||
"kicked out " + name + " from the room.") +
|
||||
reason
|
||||
break
|
||||
|
||||
case "ban":
|
||||
var name = getStateDisplayName(dict)
|
||||
return "banned " + name + " from the room." + reason
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var changed = []
|
||||
|
||||
if (prev && (info.avatar_url != prev.avatar_url)) {
|
||||
changed.push("profile picture")
|
||||
}
|
||||
|
||||
if (prev && (info.displayname != prev.displayname)) {
|
||||
changed.push("display name from \"" +
|
||||
(prev.displayname || dict.state_key) + '" to "' +
|
||||
(info.displayname || dict.state_key) + '"')
|
||||
}
|
||||
|
||||
if (changed.length > 0) {
|
||||
return "changed their " + changed.join(" and ") + "."
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
function getLeftBannerText(leftEvent) {
|
||||
if (! leftEvent) {
|
||||
return "You are not member of this room."
|
||||
}
|
||||
|
||||
var info = leftEvent.content
|
||||
var prev = leftEvent.prev_content
|
||||
var reason = info.reason ? (" Reason: " + info.reason) : ""
|
||||
|
||||
if (leftEvent.state_key === leftEvent.sender) {
|
||||
return (prev && prev.membership === "invite" ?
|
||||
"You declined to join the room." : "You left the room.") +
|
||||
reason
|
||||
}
|
||||
|
||||
if (info.membership)
|
||||
|
||||
var name = Backend.users.get(leftEvent.sender).displayName.value
|
||||
|
||||
return "<b>" + name + "</b> " +
|
||||
(info.membership == "ban" ?
|
||||
"banned you from the room." :
|
||||
|
||||
prev && prev.membership === "invite" ?
|
||||
"canceled your invitation." :
|
||||
|
||||
prev && prev.membership == "ban" ?
|
||||
"unbanned you from the room." :
|
||||
|
||||
"kicked you out of the room.") +
|
||||
reason
|
||||
}
|
||||
|
||||
|
||||
function getLeftBannerAvatarName(leftEvent, accountId) {
|
||||
if (! leftEvent || leftEvent.state_key == leftEvent.sender) {
|
||||
return Backend.users.get(accountId).displayName.value
|
||||
}
|
||||
|
||||
return Backend.users.get(leftEvent.sender).displayName.value
|
||||
}
|
||||
|
||||
|
||||
function getTypingMembersText(users, ourAccountId) {
|
||||
var names = []
|
||||
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
if (users[i] !== ourAccountId) {
|
||||
names.push(Backend.users.get(users[i]).displayName.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (names.length < 1) { return "" }
|
||||
|
||||
return "🖋 " +
|
||||
[names.slice(0, -1).join(", "), names.slice(-1)[0]]
|
||||
.join(names.length < 2 ? "" : " and ") +
|
||||
(names.length > 1 ? " are" : " is") + " typing…"
|
||||
}
|
2
src/qml/EventHandlers/includes.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// FIXME: Obsolete method, but need Qt 5.12+ for standard JS modules import
|
||||
Qt.include("system.js")
|
8
src/qml/EventHandlers/system.js
Normal file
@@ -0,0 +1,8 @@
|
||||
function onAppExitRequested(exit_code) {
|
||||
Qt.exit(exit_code)
|
||||
}
|
||||
|
||||
function onCoroutineDone(uuid, result) {
|
||||
py.pendingCoroutines[uuid](result)
|
||||
delete pendingCoroutines[uuid]
|
||||
}
|
10
src/qml/LoadingScreen.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Rectangle {
|
||||
color: "lightgray"
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
5
src/qml/Pages/Default.qml
Normal file
@@ -0,0 +1,5 @@
|
||||
import "../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: "Select or add a room to start."
|
||||
}
|
43
src/qml/Pages/RememberAccount.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
Item {
|
||||
property string loginWith: "username"
|
||||
property var client: null
|
||||
|
||||
HInterfaceBox {
|
||||
id: rememberBox
|
||||
title: "Sign in"
|
||||
anchors.centerIn: parent
|
||||
|
||||
enterButtonTarget: "yes"
|
||||
|
||||
buttonModel: [
|
||||
{ name: "yes", text: qsTr("Yes") },
|
||||
{ name: "no", text: qsTr("No") },
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"yes": function(button) {
|
||||
Backend.clients.remember(client)
|
||||
pageStack.showPage("Default")
|
||||
},
|
||||
"no": function(button) { pageStack.showPage("Default") },
|
||||
}
|
||||
|
||||
HLabel {
|
||||
text: qsTr(
|
||||
"Do you want to remember this account?\n\n" +
|
||||
"If yes, the " + loginWith + " and an access token will be " +
|
||||
"stored to automatically sign in on this device."
|
||||
)
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
Layout.margins: rememberBox.margins
|
||||
Layout.maximumWidth: rememberBox.width - Layout.margins * 2
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
}
|
||||
}
|
83
src/qml/Pages/SignIn.qml
Normal file
@@ -0,0 +1,83 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
Item {
|
||||
property string loginWith: "username"
|
||||
onFocusChanged: identifierField.forceActiveFocus()
|
||||
|
||||
HInterfaceBox {
|
||||
id: signInBox
|
||||
title: "Sign in"
|
||||
anchors.centerIn: parent
|
||||
|
||||
enterButtonTarget: "login"
|
||||
|
||||
buttonModel: [
|
||||
{ name: "register", text: qsTr("Register") },
|
||||
{ name: "login", text: qsTr("Login") },
|
||||
{ name: "forgot", text: qsTr("Forgot?") }
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"register": function(button) {},
|
||||
|
||||
"login": function(button) {
|
||||
var future = Backend.clients.new(
|
||||
"matrix.org", identifierField.text, passwordField.text
|
||||
)
|
||||
button.loadingUntilFutureDone(future)
|
||||
future.onGotResult.connect(function(client) {
|
||||
pageStack.showPage(
|
||||
"RememberAccount",
|
||||
{"loginWith": loginWith, "client": client}
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
"forgot": function(button) {}
|
||||
}
|
||||
|
||||
HRowLayout {
|
||||
spacing: signInBox.margins * 1.25
|
||||
Layout.margins: signInBox.margins
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Repeater {
|
||||
model: ["username", "email", "phone"]
|
||||
|
||||
HButton {
|
||||
iconName: modelData
|
||||
circle: true
|
||||
checked: loginWith == modelData
|
||||
autoExclusive: true
|
||||
checkedLightens: true
|
||||
onClicked: loginWith = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: identifierField
|
||||
placeholderText: qsTr(
|
||||
loginWith === "email" ? "Email" :
|
||||
loginWith === "phone" ? "Phone" :
|
||||
"Username"
|
||||
)
|
||||
onAccepted: signInBox.clickEnterButtonTarget()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: signInBox.margins
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: passwordField
|
||||
placeholderText: qsTr("Password")
|
||||
echoMode: HTextField.Password
|
||||
onAccepted: signInBox.clickEnterButtonTarget()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: signInBox.margins
|
||||
}
|
||||
}
|
||||
}
|
38
src/qml/Python.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import io.thp.pyotherside 1.5
|
||||
import "EventHandlers/includes.js" as EventHandlers
|
||||
|
||||
Python {
|
||||
id: py
|
||||
|
||||
signal ready(bool accountsToLoad)
|
||||
|
||||
property var pendingCoroutines: ({})
|
||||
|
||||
function callCoro(name, args, kwargs, callback) {
|
||||
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
|
||||
pendingCoroutines[uuid] = callback || function() {}
|
||||
})
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
for (var func in EventHandlers) {
|
||||
if (EventHandlers.hasOwnProperty(func)) {
|
||||
setHandler(func.replace(/^on/, ""), EventHandlers[func])
|
||||
}
|
||||
}
|
||||
|
||||
addImportPath("../..")
|
||||
importNames("src", ["APP"], function() {
|
||||
call("APP.start", [Qt.application.arguments], function(debug_on) {
|
||||
window.debug = debug_on
|
||||
|
||||
callCoro("has_saved_accounts", [], {}, function(has) {
|
||||
print(has)
|
||||
py.ready(has)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
80
src/qml/SidePane/AccountDelegate.qml
Normal file
@@ -0,0 +1,80 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
Column {
|
||||
id: accountDelegate
|
||||
width: parent.width
|
||||
|
||||
property var user: Backend.users.get(userId)
|
||||
|
||||
property string roomCategoriesListUserId: userId
|
||||
property bool expanded: true
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
id: row
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: user.displayName.value
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
HLabel {
|
||||
id: accountLabel
|
||||
text: user.displayName.value
|
||||
elide: HLabel.ElideRight
|
||||
maximumLineCount: 1
|
||||
Layout.fillWidth: true
|
||||
leftPadding: 6
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: statusEdit
|
||||
text: user.statusMessage || ""
|
||||
placeholderText: qsTr("Set status message")
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
background: null
|
||||
|
||||
padding: 0
|
||||
leftPadding: accountLabel.leftPadding
|
||||
rightPadding: leftPadding
|
||||
Layout.fillWidth: true
|
||||
|
||||
onEditingFinished: {
|
||||
//Backend.setStatusMessage(userId, text)
|
||||
pageStack.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExpandButton {
|
||||
expandableItem: accountDelegate
|
||||
Layout.preferredHeight: row.height
|
||||
}
|
||||
}
|
||||
|
||||
RoomCategoriesList {
|
||||
id: roomCategoriesList
|
||||
interactive: false // no scrolling
|
||||
visible: height > 0
|
||||
width: parent.width
|
||||
height: childrenRect.height * (accountDelegate.expanded ? 1 : 0)
|
||||
clip: heightAnimation.running
|
||||
|
||||
userId: roomCategoriesListUserId
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
id: heightAnimation;
|
||||
duration: HStyle.animationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/qml/SidePane/AccountList.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
id: accountList
|
||||
clip: true
|
||||
|
||||
model: Backend.accounts
|
||||
delegate: AccountDelegate {}
|
||||
}
|
22
src/qml/SidePane/ExpandButton.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HButton {
|
||||
property var expandableItem: null
|
||||
|
||||
id: expandButton
|
||||
iconName: "expand"
|
||||
iconDimension: 16
|
||||
backgroundColor: "transparent"
|
||||
onClicked: expandableItem.expanded = ! expandableItem.expanded
|
||||
|
||||
iconTransform: Rotation {
|
||||
origin.x: expandButton.iconDimension / 2
|
||||
origin.y: expandButton.iconDimension / 2
|
||||
angle: expandableItem.expanded ? 90 : 180
|
||||
Behavior on angle {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
}
|
||||
}
|
25
src/qml/SidePane/PaneToolBar.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRowLayout {
|
||||
id: toolBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
|
||||
HButton {
|
||||
iconName: "settings"
|
||||
backgroundColor: HStyle.sidePane.settingsButton.background
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: filterField
|
||||
placeholderText: qsTr("Filter rooms")
|
||||
backgroundColor: HStyle.sidePane.filterRooms.background
|
||||
|
||||
onTextChanged: Backend.setRoomFilter(text)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height
|
||||
}
|
||||
}
|
11
src/qml/SidePane/RoomCategoriesList.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
property string userId: ""
|
||||
|
||||
id: roomCategoriesList
|
||||
model: Backend.accounts.get(userId).roomCategories
|
||||
delegate: RoomCategoryDelegate {}
|
||||
}
|
60
src/qml/SidePane/RoomCategoryDelegate.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
Column {
|
||||
id: roomCategoryDelegate
|
||||
width: roomCategoriesList.width
|
||||
|
||||
property int normalHeight: childrenRect.height // avoid binding loop
|
||||
|
||||
opacity: roomList.model.count > 0 ? 1 : 0
|
||||
height: normalHeight * opacity
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
|
||||
property string roomListUserId: userId
|
||||
property bool expanded: true
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
|
||||
HLabel {
|
||||
id: roomCategoryLabel
|
||||
text: name
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ExpandButton {
|
||||
expandableItem: roomCategoryDelegate
|
||||
iconDimension: 12
|
||||
}
|
||||
}
|
||||
|
||||
RoomList {
|
||||
id: roomList
|
||||
interactive: false // no scrolling
|
||||
visible: height > 0
|
||||
width: roomCategoriesList.width - accountList.Layout.leftMargin
|
||||
opacity: roomCategoryDelegate.expanded ? 1 : 0
|
||||
height: childrenRect.height * opacity
|
||||
clip: listHeightAnimation.running
|
||||
|
||||
userId: roomListUserId
|
||||
category: name
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
id: listHeightAnimation
|
||||
duration: HStyle.animationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
src/qml/SidePane/RoomDelegate.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
import "utils.js" as SidePaneJS
|
||||
|
||||
MouseArea {
|
||||
id: roomDelegate
|
||||
width: roomList.width
|
||||
height: childrenRect.height
|
||||
|
||||
onClicked: pageStack.showRoom(roomList.userId, roomList.category, roomId)
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
spacing: roomList.spacing
|
||||
|
||||
HAvatar {
|
||||
id: roomAvatar
|
||||
name: displayName
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth:
|
||||
parent.width - parent.totalSpacing - roomAvatar.width
|
||||
|
||||
HLabel {
|
||||
id: roomLabel
|
||||
text: displayName ? displayName : "<i>Empty room</i>"
|
||||
textFormat: Text.StyledText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
|
||||
HLabel {
|
||||
function getText() {
|
||||
return SidePaneJS.getLastRoomEventText(
|
||||
roomId, roomList.userId
|
||||
)
|
||||
}
|
||||
|
||||
property var lastEvTime: lastEventDateTime
|
||||
onLastEvTimeChanged: subtitleLabel.text = getText()
|
||||
|
||||
id: subtitleLabel
|
||||
visible: text !== ""
|
||||
text: getText()
|
||||
textFormat: Text.StyledText
|
||||
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/qml/SidePane/RoomList.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
property string userId: ""
|
||||
property string category: ""
|
||||
|
||||
id: roomList
|
||||
spacing: accountList.spacing
|
||||
model:
|
||||
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
|
||||
delegate: RoomDelegate {}
|
||||
}
|
30
src/qml/SidePane/SidePane.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
id: sidePane
|
||||
|
||||
property int normalSpacing: 8
|
||||
property bool collapsed: false
|
||||
|
||||
HColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
AccountList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: collapsed ? 0 : normalSpacing
|
||||
topMargin: spacing
|
||||
bottomMargin: spacing
|
||||
Layout.leftMargin: spacing
|
||||
|
||||
Behavior on spacing {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
}
|
||||
|
||||
PaneToolBar {}
|
||||
}
|
||||
}
|
29
src/qml/SidePane/utils.js
Normal file
@@ -0,0 +1,29 @@
|
||||
.import "../Chat/utils.js" as ChatJS
|
||||
|
||||
|
||||
function getLastRoomEventText(roomId, accountId) {
|
||||
var eventsModel = Backend.roomEvents.get(roomId)
|
||||
if (eventsModel.count < 1) { return "" }
|
||||
var ev = eventsModel.get(0)
|
||||
|
||||
var name = Backend.users.get(ev.dict.sender).displayName.value
|
||||
|
||||
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
|
||||
|
||||
if (undecryptable || ev.type.startsWith("RoomMessage")) {
|
||||
var color = Qt.hsla(Backend.hueFromString(name), 0.32, 0.3, 1)
|
||||
|
||||
return "<font color='" + color + "'>" +
|
||||
name +
|
||||
":</font> " +
|
||||
(undecryptable ?
|
||||
"<font color='darkred'>" + qsTr("Undecryptable") + "<font>" :
|
||||
ev.dict.body)
|
||||
} else {
|
||||
return "<font color='" + (undecryptable ? "darkred" : "#444") + "'>" +
|
||||
name +
|
||||
" " +
|
||||
ChatJS.getEventText(ev.type, ev.dict) +
|
||||
"</font>"
|
||||
}
|
||||
}
|
106
src/qml/UI.qml
Normal file
@@ -0,0 +1,106 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.7
|
||||
import "Base"
|
||||
import "SidePane"
|
||||
|
||||
Item {
|
||||
id: mainUI
|
||||
|
||||
property bool accountsLoggedIn: Backend.clients.count > 0
|
||||
|
||||
HImage {
|
||||
id: mainUIBackground
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: "../images/login_background.jpg"
|
||||
sourceSize.width: Screen.width
|
||||
sourceSize.height: Screen.height
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
}
|
||||
|
||||
HSplitView {
|
||||
id: uiSplitView
|
||||
anchors.fill: parent
|
||||
|
||||
SidePane {
|
||||
id: sidePane
|
||||
visible: accountsLoggedIn
|
||||
collapsed: width < Layout.minimumWidth + normalSpacing
|
||||
|
||||
property int parentWidth: parent.width
|
||||
property int collapseBelow: 120
|
||||
|
||||
function set_width() {
|
||||
width = parent.width * 0.3 < collapseBelow ?
|
||||
Layout.minimumWidth : Math.min(parent.width * 0.3, 300)
|
||||
}
|
||||
|
||||
onParentWidthChanged: if (uiSplitView.canAutoSize) { set_width() }
|
||||
|
||||
width: set_width() // Initial width
|
||||
Layout.minimumWidth: HStyle.avatar.size
|
||||
Layout.maximumWidth: parent.width
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
// Don't slow down the user manually resizing
|
||||
duration:
|
||||
(uiSplitView.canAutoSize &&
|
||||
parent.width * 0.3 < sidePane.collapseBelow * 1.2) ?
|
||||
HStyle.animationDuration : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: pageStack
|
||||
|
||||
function showPage(name, properties) {
|
||||
pageStack.replace("Pages/" + name + ".qml", properties || {})
|
||||
}
|
||||
|
||||
function showRoom(userId, category, roomId) {
|
||||
pageStack.replace(
|
||||
"Chat/Chat.qml",
|
||||
{ userId: userId, category: category, roomId: roomId }
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
interval: 5000
|
||||
repeat: false
|
||||
onTriggered: pageStack.showRoom(
|
||||
"@test_mary:matrix.org",
|
||||
"Rooms",
|
||||
"!TSXGsbBbdwsdylIOJZ:matrix.org"
|
||||
)
|
||||
}
|
||||
|
||||
onCurrentItemChanged: if (currentItem) {
|
||||
currentItem.forceActiveFocus()
|
||||
}
|
||||
|
||||
// Buggy
|
||||
replaceExit: null
|
||||
popExit: null
|
||||
pushExit: null
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: if (window.debug) { py.call("APP.pdb") }
|
||||
}
|
||||
}
|