Build system, messages support and more
This commit is contained in:
parent
933341b7e6
commit
06c823aa67
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,12 +1,14 @@
|
|||
__pycache__
|
||||
.mypy_cache
|
||||
build
|
||||
dist
|
||||
*.egg-info
|
||||
*.pyc
|
||||
|
||||
*.qmlc
|
||||
*.jsc
|
||||
|
||||
.pylintrc
|
||||
|
||||
tmp-*
|
||||
|
||||
build
|
||||
dist
|
||||
.qmake.stash
|
||||
Makefile
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "submodules/SortFilterProxyModel"]
|
||||
path = submodules/SortFilterProxyModel
|
||||
url = https://github.com/oKcerG/SortFilterProxyModel
|
362
.pylintrc
Normal file
362
.pylintrc
Normal file
|
@ -0,0 +1,362 @@
|
|||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=lxml.etree
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=.git
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=6
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
#
|
||||
# C0111: Allow lack of literal """docstring""" (e.g. if assigning __doc__)
|
||||
# C0326: Allow aligning things like variable assignements
|
||||
# C0412: Remove annoying "imports are not grouped" because of :Isort with from
|
||||
# W0123: Allow eval()
|
||||
disable=C0111,C0326,C0412,W0123
|
||||
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=pyotherside
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install pyenchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=79
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style
|
||||
argument-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
# Naming style matching correct attribute names
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style
|
||||
attr-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=
|
||||
|
||||
# Naming style matching correct class attribute names
|
||||
class-attribute-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style
|
||||
class-attribute-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
# Naming style matching correct class names
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-style
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style
|
||||
const-rgx=(__[a-zA-Z0-9_]{2,65}__|[A-Z0-9_]{2,65})$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style
|
||||
function-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names= _, a, b, i, j, k, v, w, h, x, y, z
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style
|
||||
method-rgx=([a-z0-9_]{2,65}|on[A-Z][a-zA-Z0-9]{1,65})$
|
||||
|
||||
# Naming style matching correct module names
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style
|
||||
module-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style
|
||||
variable-rgx=[a-z0-9_]{2,65}$
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=99
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=99
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=99
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=99
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=99
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=99
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=99
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=99
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=999
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=0
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
# Catching this is safe enough in Python 3.
|
||||
#overgeneral-exceptions=Exception
|
||||
overgeneral-exceptions=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
generated-members=sh
|
16
README.md
16
README.md
|
@ -0,0 +1,16 @@
|
|||
# harmonyqml
|
||||
|
||||
## Dependencies setup
|
||||
|
||||
### [pyotherside](https://github.com/thp/pyotherside)
|
||||
|
||||
git clone https://github.com/thp/pyotherside
|
||||
cd pyotherside
|
||||
qmake
|
||||
make
|
||||
sudo make install
|
||||
|
||||
After this, verify the permissions of the installed plugin files.
|
||||
|
||||
sudo chmod 644 /usr/lib/qt5/qml/io/thp/pyotherside/*
|
||||
sudo chmod 755 /usr/lib/qt5/qml/io/thp/pyotherside/*.so
|
BIN
harmonyqml
Executable file
BIN
harmonyqml
Executable file
Binary file not shown.
68
harmonyqml.pro
Normal file
68
harmonyqml.pro
Normal file
|
@ -0,0 +1,68 @@
|
|||
TEMPLATE = app
|
||||
QT = quick
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
CONFIG += release warn_off c++11
|
||||
|
||||
BUILD_DIR = build
|
||||
MOC_DIR = $$BUILD_DIR/moc
|
||||
OBJECTS_DIR = $$BUILD_DIR/obj
|
||||
RCC_DIR = $$BUILD_DIR/rcc
|
||||
|
||||
QRC_FILE = $$BUILD_DIR/resources.qrc
|
||||
!no_embedded {
|
||||
RESOURCES += $$QRC_FILE
|
||||
}
|
||||
|
||||
SOURCES += src/main.cpp
|
||||
TARGET = harmonyqml
|
||||
|
||||
|
||||
# Libraries includes
|
||||
|
||||
include(submodules/SortFilterProxyModel/SortFilterProxyModel.pri)
|
||||
|
||||
|
||||
# Custom functions
|
||||
|
||||
defineReplace(glob_filenames) {
|
||||
for(pattern, ARGS) {
|
||||
results *= $$files(src/$${pattern}, true)
|
||||
}
|
||||
return($$results)
|
||||
}
|
||||
|
||||
|
||||
# Generate resource file
|
||||
|
||||
RESOURCE_FILES *= $$glob_filenames(qmldir, *.qml, *.js, *.py)
|
||||
RESOURCE_FILES *= $$glob_filenames( *.jpg, *.jpeg, *.png, *.svg)
|
||||
|
||||
file_content += '<!-- vim: set ft=xml : -->'
|
||||
file_content += '<!DOCTYPE RCC>'
|
||||
file_content += '<RCC version="1.0">'
|
||||
file_content += '<qresource prefix="/">'
|
||||
|
||||
for(file, RESOURCE_FILES) {
|
||||
alias = $$replace(file, src/, '')
|
||||
file_content += ' <file alias="$$alias">../$$file</file>'
|
||||
}
|
||||
|
||||
file_content += '</qresource>'
|
||||
file_content += '</RCC>'
|
||||
|
||||
write_file($$QRC_FILE, file_content)
|
||||
|
||||
|
||||
# Add stuff to `make clean`
|
||||
|
||||
# Allow cleaning folders instead of just files
|
||||
win32:QMAKE_DEL_FILE = rmdir /q /s
|
||||
unix:QMAKE_DEL_FILE = rm -rf
|
||||
|
||||
for(file, $$list($$glob_filenames(*.py))) {
|
||||
PYCACHE_DIRS *= $$dirname(file)/__pycache__
|
||||
}
|
||||
|
||||
QMAKE_CLEAN *= $$MOC_DIR $$OBJECTS_DIR $$RCC_DIR $$PYCACHE_DIRS $$QRC_FILE
|
||||
QMAKE_CLEAN *= $$BUILD_DIR Makefile .qmake.stash
|
||||
QMAKE_CLEAN *= $$glob_filenames(*.pyc, *.qmlc, *.jsc, *.egg-info)
|
10
live_reload.sh
Executable file
10
live_reload.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# pdb won't be usable with entr,
|
||||
# use https://pypi.org/project/remote-pdb/ instead
|
||||
|
||||
# no_embedded (resources) is used to speed up the compilation
|
||||
|
||||
find src harmonyqml.pro -type f |
|
||||
entr -cdnr sh -c \
|
||||
'qmake CONFIG+=no_embedded && make && ./harmonyqml --debug'
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
while true; do
|
||||
clear
|
||||
qml src/qml/Window.qml -- --debug
|
||||
exit_code="$?"
|
||||
if [ "$exit_code" != 231 ]; then break; fi
|
||||
done
|
|
@ -1,8 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
23
src/main.cpp
Normal file
23
src/main.cpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlComponent>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
QQmlEngine engine;
|
||||
QQmlContext *objectContext = new QQmlContext(engine.rootContext());
|
||||
|
||||
QQmlComponent component(
|
||||
&engine,
|
||||
QFileInfo::exists("qrc:/qml/Window.qml") ?
|
||||
"qrc:/qml/Window.qml" : "src/qml/Window.qml"
|
||||
);
|
||||
component.create(objectContext);
|
||||
|
||||
return app.exec();
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import platform
|
||||
from contextlib import suppress
|
||||
from typing import Optional
|
||||
|
||||
import nio
|
||||
|
||||
from . import __about__
|
||||
from .events import rooms, users
|
||||
|
||||
|
||||
class MatrixClient(nio.AsyncClient):
|
||||
def __init__(self,
|
||||
user: str,
|
||||
homeserver: str = "https://matrix.org",
|
||||
device_id: Optional[str] = None) -> None:
|
||||
|
||||
# TODO: ensure homeserver starts with a scheme://
|
||||
self.sync_task: Optional[asyncio.Task] = None
|
||||
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
|
||||
|
||||
self.connect_callbacks()
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
|
||||
type(self).__name__, self.user_id, self.homeserver, self.device_id
|
||||
)
|
||||
|
||||
|
||||
def connect_callbacks(self) -> None:
|
||||
for name in dir(nio.responses):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
|
||||
obj = getattr(nio.responses, name)
|
||||
if inspect.isclass(obj) and issubclass(obj, nio.Response):
|
||||
with suppress(AttributeError):
|
||||
self.add_response_callback(getattr(self, f"on{name}"), obj)
|
||||
|
||||
|
||||
async def start_syncing(self) -> None:
|
||||
self.sync_task = asyncio.ensure_future( # type: ignore
|
||||
self.sync_forever(timeout=10_000)
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def default_device_name(self) -> str:
|
||||
os_ = f" on {platform.system()}".rstrip()
|
||||
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""
|
||||
return f"{__about__.__pretty_name__}{os_}"
|
||||
|
||||
|
||||
async def login(self, password: str) -> None:
|
||||
response = await super().login(password, self.default_device_name)
|
||||
|
||||
if isinstance(response, nio.LoginError):
|
||||
print(response)
|
||||
else:
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def resume(self, user_id: str, token: str, device_id: str) -> None:
|
||||
self.receive_response(nio.LoginResponse(user_id, device_id, token))
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def logout(self) -> None:
|
||||
if self.sync_task:
|
||||
self.sync_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self.sync_task
|
||||
|
||||
await self.close()
|
||||
|
||||
|
||||
async def request_user_update_event(self, user_id: str) -> None:
|
||||
response = await self.get_profile(user_id)
|
||||
|
||||
users.UserUpdated(
|
||||
user_id = user_id,
|
||||
display_name = response.displayname,
|
||||
avatar_url = response.avatar_url,
|
||||
status_message = None, # TODO
|
||||
)
|
||||
|
||||
|
||||
# Callbacks for nio responses
|
||||
|
||||
async def onSyncResponse(self, response: nio.SyncResponse) -> None:
|
||||
for room_id in response.rooms.invite:
|
||||
room: nio.rooms.MatrixRoom = self.invited_rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Invites",
|
||||
room_id = room_id,
|
||||
display_name = room.display_name,
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
inviter = room.inviter,
|
||||
)
|
||||
|
||||
for room_id in response.rooms.join:
|
||||
room = self.rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Rooms",
|
||||
room_id = room_id,
|
||||
display_name = room.display_name,
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
)
|
||||
|
||||
for room_id in response.rooms.left:
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Left",
|
||||
room_id = room_id,
|
||||
# left_event TODO
|
||||
)
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import signal
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
@ -15,31 +16,16 @@ class App:
|
|||
def __init__(self) -> None:
|
||||
self.appdirs = AppDirs(appname=__about__.__pkg_name__, roaming=True)
|
||||
|
||||
self.backend = None
|
||||
from .backend import Backend
|
||||
self.backend = Backend(app=self)
|
||||
|
||||
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):
|
||||
ExitRequested(231)
|
||||
def is_debug_on(self, cli_flags: Sequence[str] = ()) -> bool:
|
||||
return "-d" in cli_flags or "--debug" in cli_flags
|
||||
|
||||
|
||||
def _loop_starter(self) -> None:
|
||||
|
@ -74,7 +60,7 @@ class App:
|
|||
name: str,
|
||||
args: Optional[List[str]] = None,
|
||||
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
||||
client = self.backend.clients[account_id] # type: ignore
|
||||
client = self.backend.clients[account_id]
|
||||
return self._call_coro(
|
||||
getattr(client, name)(*args or [], **kwargs or {})
|
||||
)
|
||||
|
@ -85,14 +71,19 @@ class App:
|
|||
ad = additional_data
|
||||
rl = self.run_in_loop
|
||||
ba = self.backend
|
||||
cl = self.backend.clients # type: ignore
|
||||
cl = self.backend.clients
|
||||
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()
|
||||
print("\n=> Run `socat readline tcp:127.0.0.1:4444` in a terminal "
|
||||
"to connect to pdb.")
|
||||
import remote_pdb
|
||||
remote_pdb.RemotePdb("127.0.0.1", 4444).set_trace()
|
||||
|
||||
|
||||
# Make CTRL-C work again
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
APP = App()
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from atomicfile import AtomicFile
|
||||
|
||||
|
@ -29,13 +29,14 @@ class Backend:
|
|||
user: str,
|
||||
password: str,
|
||||
device_id: Optional[str] = None,
|
||||
homeserver: str = "https://matrix.org") -> None:
|
||||
homeserver: str = "https://matrix.org") -> str:
|
||||
client = MatrixClient(
|
||||
user=user, homeserver=homeserver, device_id=device_id
|
||||
)
|
||||
await client.login(password)
|
||||
self.clients[client.user_id] = client
|
||||
users.AccountUpdated(client.user_id)
|
||||
return client.user_id
|
||||
|
||||
|
||||
async def resume_client(self,
|
||||
|
@ -98,13 +99,15 @@ class Backend:
|
|||
))
|
||||
|
||||
|
||||
async def save_account(self, client: MatrixClient) -> None:
|
||||
async def save_account(self, user_id: str) -> None:
|
||||
client = self.clients[user_id]
|
||||
|
||||
await self._write_config({
|
||||
**self.saved_accounts,
|
||||
client.userId: {
|
||||
"hostname": client.nio.host,
|
||||
"token": client.nio.access_token,
|
||||
"device_id": client.nio.device_id,
|
||||
client.user_id: {
|
||||
"homeserver": client.homeserver,
|
||||
"token": client.access_token,
|
||||
"device_id": client.device_id,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -119,7 +122,7 @@ class Backend:
|
|||
async def _write_config(self, accounts: SavedAccounts) -> None:
|
||||
js = json.dumps(accounts, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
with CONFIG_LOCK:
|
||||
async with CONFIG_LOCK:
|
||||
self.saved_accounts_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with AtomicFile(self.saved_accounts_path, "w") as new:
|
32
src/python/events/rooms_timeline.py
Normal file
32
src/python/events/rooms_timeline.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from datetime import datetime
|
||||
from enum import auto
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .event import AutoStrEnum, Event
|
||||
|
||||
|
||||
class EventType(AutoStrEnum):
|
||||
text = auto()
|
||||
html = auto()
|
||||
file = auto()
|
||||
image = auto()
|
||||
audio = auto()
|
||||
video = auto()
|
||||
location = auto()
|
||||
notice = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimelineEvent(Event):
|
||||
type: EventType = field()
|
||||
room_id: str = field()
|
||||
event_id: str = field()
|
||||
sender_id: str = field()
|
||||
date: datetime = field()
|
||||
is_local_echo: bool = field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class HtmlMessageReceived(TimelineEvent):
|
||||
content: str = field()
|
153
src/python/html_filter.py
Normal file
153
src/python/html_filter.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
import re
|
||||
|
||||
import mistune
|
||||
from lxml.html import HtmlElement, etree # nosec
|
||||
|
||||
import html_sanitizer.sanitizer as sanitizer
|
||||
|
||||
|
||||
class HtmlFilter:
|
||||
link_regexes = [re.compile(r, re.IGNORECASE) for r in [
|
||||
(r"(?P<body>.+://(?P<host>[a-z0-9._-]+)(?:/[/\-_.,a-z0-9%&?;=~]*)?"
|
||||
r"(?:\([/\-_.,a-z0-9%&?;=~]*\))?)"),
|
||||
r"mailto:(?P<body>[a-z0-9._-]+@(?P<host>[a-z0-9_.-]+[a-z]))",
|
||||
r"tel:(?P<body>[0-9+-]+)(?P<host>)",
|
||||
r"(?P<body>magnet:\?xt=urn:[a-z0-9]+:.+)(?P<host>)",
|
||||
]]
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._sanitizer = sanitizer.Sanitizer(self.sanitizer_settings)
|
||||
|
||||
# The whitespace remover doesn't take <pre> into account
|
||||
sanitizer.normalize_overall_whitespace = lambda html: html
|
||||
sanitizer.normalize_whitespace_in_text_or_tail = lambda el: el
|
||||
|
||||
# hard_wrap: convert all \n to <br> without required two spaces
|
||||
self._markdown_to_html = mistune.Markdown(hard_wrap=True)
|
||||
|
||||
|
||||
def from_markdown(self, text: str) -> str:
|
||||
return self.filter(self._markdown_to_html(text))
|
||||
|
||||
|
||||
def filter(self, html: str) -> str:
|
||||
html = self._sanitizer.sanitize(html)
|
||||
tree = etree.fromstring(html, parser=etree.HTMLParser())
|
||||
|
||||
if tree is None:
|
||||
return ""
|
||||
|
||||
for el in tree.iter("img"):
|
||||
el = self._wrap_img_in_a(el)
|
||||
|
||||
for el in tree.iter("a"):
|
||||
el = self._append_img_to_a(el)
|
||||
|
||||
result = b"".join((etree.tostring(el, encoding="utf-8")
|
||||
for el in tree[0].iterchildren()))
|
||||
|
||||
return str(result, "utf-8")
|
||||
|
||||
|
||||
@property
|
||||
def sanitizer_settings(self) -> dict:
|
||||
# https://matrix.org/docs/spec/client_server/latest.html#m-room-message-msgtypes
|
||||
return {
|
||||
"tags": {
|
||||
# TODO: mx-reply, audio, video
|
||||
"font", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"blockquote", "p", "a", "ul", "ol", "sup", "sub", "li",
|
||||
"b", "i", "s", "u", "code", "hr", "br",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"pre", "img",
|
||||
},
|
||||
"attributes": {
|
||||
# TODO: translate font attrs to qt html subset
|
||||
"font": {"data-mx-bg-color", "data-mx-color"},
|
||||
"a": {"href"},
|
||||
"img": {"width", "height", "alt", "title", "src"},
|
||||
"ol": {"start"},
|
||||
"code": {"class"},
|
||||
},
|
||||
"empty": {"hr", "br", "img"},
|
||||
"separate": {
|
||||
"a", "p", "li", "table", "tr", "th", "td", "br", "hr"
|
||||
},
|
||||
"whitespace": {},
|
||||
"add_nofollow": False,
|
||||
"autolink": { # FIXME: arg dict not working
|
||||
"link_regexes": self.link_regexes,
|
||||
"avoid_hosts": [],
|
||||
},
|
||||
"sanitize_href": lambda href: href,
|
||||
"element_preprocessors": [
|
||||
sanitizer.bold_span_to_strong,
|
||||
sanitizer.italic_span_to_em,
|
||||
sanitizer.tag_replacer("strong", "b"),
|
||||
sanitizer.tag_replacer("em", "i"),
|
||||
sanitizer.tag_replacer("strike", "s"),
|
||||
sanitizer.tag_replacer("del", "s"),
|
||||
sanitizer.tag_replacer("span", "font"),
|
||||
self._remove_empty_font,
|
||||
sanitizer.tag_replacer("form", "p"),
|
||||
sanitizer.tag_replacer("div", "p"),
|
||||
sanitizer.tag_replacer("caption", "p"),
|
||||
sanitizer.target_blank_noopener,
|
||||
],
|
||||
"element_postprocessors": [],
|
||||
"is_mergeable": lambda e1, e2: e1.attrib == e2.attrib,
|
||||
}
|
||||
|
||||
|
||||
def _remove_empty_font(self, el: HtmlElement) -> HtmlElement:
|
||||
if el.tag != "font":
|
||||
return el
|
||||
|
||||
if not self.sanitizer_settings["attributes"]["font"] & set(el.keys()):
|
||||
el.clear()
|
||||
|
||||
return el
|
||||
|
||||
|
||||
def _wrap_img_in_a(self, el: HtmlElement) -> HtmlElement:
|
||||
link = el.attrib.get("src", "")
|
||||
width = el.attrib.get("width", "256")
|
||||
height = el.attrib.get("height", "256")
|
||||
|
||||
if el.getparent().tag == "a" or el.tag != "img" or \
|
||||
not self._is_image_path(link):
|
||||
return el
|
||||
|
||||
el.tag = "a"
|
||||
el.attrib.clear()
|
||||
el.attrib["href"] = link
|
||||
el.append(etree.Element("img", src=link, width=width, height=height))
|
||||
return el
|
||||
|
||||
|
||||
def _append_img_to_a(self, el: HtmlElement) -> HtmlElement:
|
||||
link = el.attrib.get("href", "")
|
||||
|
||||
if not (el.tag == "a" and self._is_image_path(link)):
|
||||
return el
|
||||
|
||||
for _ in el.iter("img"): # if the <a> already has an <img> child
|
||||
return el
|
||||
|
||||
el.append(etree.Element("br"))
|
||||
el.append(etree.Element("img", src=link, width="256", height="256"))
|
||||
return el
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _is_image_path(link: str) -> bool:
|
||||
return bool(re.match(
|
||||
r".+\.(jpg|jpeg|png|gif|bmp|webp|tiff|svg)$", link, re.IGNORECASE
|
||||
))
|
||||
|
||||
|
||||
HTML_FILTER = HtmlFilter()
|
182
src/python/matrix_client.py
Normal file
182
src/python/matrix_client.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import logging as log
|
||||
import platform
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from types import ModuleType
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
import nio
|
||||
|
||||
from . import __about__
|
||||
from .events import rooms, users
|
||||
from .events.rooms_timeline import EventType, HtmlMessageReceived
|
||||
from .html_filter import HTML_FILTER
|
||||
|
||||
|
||||
class MatrixClient(nio.AsyncClient):
|
||||
def __init__(self,
|
||||
user: str,
|
||||
homeserver: str = "https://matrix.org",
|
||||
device_id: Optional[str] = None) -> None:
|
||||
|
||||
# TODO: ensure homeserver starts with a scheme://
|
||||
self.sync_task: Optional[asyncio.Future] = None
|
||||
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
|
||||
|
||||
self.connect_callbacks()
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
|
||||
type(self).__name__, self.user_id, self.homeserver, self.device_id
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _classes_defined_in(module: ModuleType) -> Dict[str, Type]:
|
||||
return {
|
||||
m[0]: m[1] for m in inspect.getmembers(module, inspect.isclass)
|
||||
if not m[0].startswith("_") and
|
||||
m[1].__module__.startswith(module.__name__)
|
||||
}
|
||||
|
||||
|
||||
def connect_callbacks(self) -> None:
|
||||
for name, class_ in self._classes_defined_in(nio.responses).items():
|
||||
with suppress(AttributeError):
|
||||
self.add_response_callback(getattr(self, f"on{name}"), class_)
|
||||
|
||||
# TODO: get this implemented in AsyncClient
|
||||
# for name, class_ in self._classes_defined_in(nio.events).items():
|
||||
# with suppress(AttributeError):
|
||||
# self.add_event_callback(getattr(self, f"on{name}"), class_)
|
||||
|
||||
|
||||
async def start_syncing(self) -> None:
|
||||
self.sync_task = asyncio.ensure_future(
|
||||
self.sync_forever(timeout=10_000)
|
||||
)
|
||||
|
||||
def callback(task):
|
||||
raise task.exception()
|
||||
|
||||
self.sync_task.add_done_callback(callback)
|
||||
|
||||
|
||||
@property
|
||||
def default_device_name(self) -> str:
|
||||
os_ = f" on {platform.system()}".rstrip()
|
||||
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""
|
||||
return f"{__about__.__pretty_name__}{os_}"
|
||||
|
||||
|
||||
async def login(self, password: str) -> None:
|
||||
response = await super().login(password, self.default_device_name)
|
||||
|
||||
if isinstance(response, nio.LoginError):
|
||||
print(response)
|
||||
else:
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def resume(self, user_id: str, token: str, device_id: str) -> None:
|
||||
response = nio.LoginResponse(user_id, device_id, token)
|
||||
await self.receive_response(response)
|
||||
await self.start_syncing()
|
||||
|
||||
|
||||
async def logout(self) -> None:
|
||||
if self.sync_task:
|
||||
self.sync_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self.sync_task
|
||||
|
||||
await self.close()
|
||||
|
||||
|
||||
async def request_user_update_event(self, user_id: str) -> None:
|
||||
response = await self.get_profile(user_id)
|
||||
|
||||
if isinstance(response, nio.ProfileGetError):
|
||||
log.warning("Error getting profile for %r: %s", user_id, response)
|
||||
|
||||
users.UserUpdated(
|
||||
user_id = user_id,
|
||||
display_name = getattr(response, "displayname", None),
|
||||
avatar_url = getattr(response, "avatar_url", None),
|
||||
status_message = None, # TODO
|
||||
)
|
||||
|
||||
|
||||
# Callbacks for nio responses
|
||||
|
||||
@staticmethod
|
||||
def _get_room_name(room: nio.rooms.MatrixRoom) -> Optional[str]:
|
||||
# FIXME: reimplanted because of nio's non-standard room.display_name
|
||||
name = room.name or room.canonical_alias
|
||||
if name:
|
||||
return name
|
||||
|
||||
name = room.group_name()
|
||||
return None if name == "Empty room?" else name
|
||||
|
||||
|
||||
async def onSyncResponse(self, resp: nio.SyncResponse) -> None:
|
||||
for room_id, info in resp.rooms.invite.items():
|
||||
room: nio.rooms.MatrixRoom = self.invited_rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Invites",
|
||||
room_id = room_id,
|
||||
display_name = self._get_room_name(room),
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
inviter = room.inviter,
|
||||
)
|
||||
|
||||
for room_id, info in resp.rooms.join.items():
|
||||
room = self.rooms[room_id]
|
||||
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Rooms",
|
||||
room_id = room_id,
|
||||
display_name = self._get_room_name(room),
|
||||
avatar_url = room.gen_avatar_url,
|
||||
topic = room.topic,
|
||||
)
|
||||
|
||||
asyncio.gather(*(
|
||||
getattr(self, f"on{type(ev).__name__}")(room_id, ev)
|
||||
for ev in info.timeline.events
|
||||
if hasattr(self, f"on{type(ev).__name__}")
|
||||
))
|
||||
|
||||
for room_id, info in resp.rooms.leave.items():
|
||||
rooms.RoomUpdated(
|
||||
user_id = self.user_id,
|
||||
category = "Left",
|
||||
room_id = room_id,
|
||||
# left_event TODO
|
||||
)
|
||||
|
||||
|
||||
# Callbacks for nio events
|
||||
|
||||
async def onRoomMessageText(self, room_id: str, ev: nio.RoomMessageText
|
||||
) -> None:
|
||||
is_html = ev.format == "org.matrix.custom.html"
|
||||
filter_ = HTML_FILTER.filter
|
||||
|
||||
HtmlMessageReceived(
|
||||
type = EventType.html if is_html else EventType.text,
|
||||
room_id = room_id,
|
||||
event_id = ev.event_id,
|
||||
sender_id = ev.sender,
|
||||
date = datetime.fromtimestamp(ev.server_timestamp / 1000),
|
||||
is_local_echo = False,
|
||||
content = filter_(ev.formatted_body) if is_html else ev.body,
|
||||
)
|
|
@ -7,7 +7,14 @@ Rectangle {
|
|||
property int dimension: HStyle.avatar.size
|
||||
property bool hidden: false
|
||||
|
||||
function hue_from_name(name) {
|
||||
function stripUserId(user_id) {
|
||||
return user_id.substring(1) // Remove leading @
|
||||
}
|
||||
function stripRoomName(name) {
|
||||
return name[0] == "#" ? name.substring(1) : name
|
||||
}
|
||||
|
||||
function hueFromName(name) {
|
||||
var hue = 0
|
||||
for (var i = 0; i < name.length; i++) {
|
||||
hue += name.charCodeAt(i) * 99
|
||||
|
@ -24,7 +31,7 @@ Rectangle {
|
|||
|
||||
color: name ?
|
||||
Qt.hsla(
|
||||
hue_from_name(name),
|
||||
hueFromName(name),
|
||||
HStyle.avatar.background.saturation,
|
||||
HStyle.avatar.background.lightness,
|
||||
HStyle.avatar.background.alpha
|
||||
|
|
|
@ -32,11 +32,6 @@ Button {
|
|||
signal pressed
|
||||
signal released
|
||||
|
||||
function loadingUntilFutureDone(future) {
|
||||
loading = true
|
||||
future.onGotResult.connect(function() { loading = false })
|
||||
}
|
||||
|
||||
id: button
|
||||
|
||||
background: Rectangle {
|
||||
|
|
|
@ -1,85 +1,112 @@
|
|||
import QtQuick 2.7
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
ListModel {
|
||||
SortFilterProxyModel {
|
||||
// To initialize a HListModel with items,
|
||||
// use `Component.onCompleted: extend([{"foo": 1, "bar": 2}, ...])`
|
||||
|
||||
id: listModel
|
||||
id: sortFilteredModel
|
||||
|
||||
property var model: ListModel {}
|
||||
sourceModel: model // Can't assign a "ListModel {}" directly here
|
||||
|
||||
function append(dict) { return model.append(dict) }
|
||||
function clear() { return model.clear() }
|
||||
function insert(index, dict) { return model.inset(index, dict) }
|
||||
function move(from, to, n) { return model.move(from, to, n) }
|
||||
function remove(index, count) { return model.remove(index, count) }
|
||||
function set(index, dict) { return model.set(index, dict) }
|
||||
function sync() { return model.sync() }
|
||||
function setProperty(index, prop, value) {
|
||||
return model.setProperty(index, prop, value)
|
||||
}
|
||||
|
||||
function extend(new_items) {
|
||||
for (var i = 0; i < new_items.length; i++) {
|
||||
listModel.append(new_items[i])
|
||||
model.append(new_items[i])
|
||||
}
|
||||
}
|
||||
|
||||
function getIndices(where_role, is, max) { // max: undefined or int
|
||||
function getIndices(where_roles_are, max_results, max_tries) {
|
||||
// max arguments: unefined or int
|
||||
var results = []
|
||||
|
||||
for (var i = 0; i < listModel.count; i++) {
|
||||
if (listModel.get(i)[where_role] == is) {
|
||||
results.push(i)
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i)
|
||||
var include = true
|
||||
|
||||
if (max && results.length >= max) {
|
||||
for (var role in where_roles_are) {
|
||||
if (item[role] != where_roles_are[role]) {
|
||||
include = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (include) {
|
||||
results.push(i)
|
||||
if (max_results && results.length >= max_results) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (max_tries && i >= max_tries) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function getWhere(where_role, is, max) {
|
||||
var indices = getIndices(where_role, is, max)
|
||||
var results = []
|
||||
function getWhere(roles_are, max_results, max_tries) {
|
||||
var indices = getIndices(roles_are, max_results, max_tries)
|
||||
var items = []
|
||||
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
results.push(listModel.get(indices[i]))
|
||||
items.push(model.get(indices[i]))
|
||||
}
|
||||
return results
|
||||
return items
|
||||
}
|
||||
|
||||
function forEachWhere(where_role, is, max, func) {
|
||||
var items = getWhere(where_role, is, max)
|
||||
function forEachWhere(roles_are, func, max_results, max_tries) {
|
||||
var items = getWhere(roles_are, max_results, max_tries)
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
func(item)
|
||||
func(items[i])
|
||||
}
|
||||
}
|
||||
|
||||
function upsert(where_role, is, new_item, update_if_exist) {
|
||||
// new_item can contain only the keys we're interested in updating
|
||||
|
||||
var indices = getIndices(where_role, is, 1)
|
||||
function upsert(where_roles_are, new_item, update_if_exist, max_tries) {
|
||||
var indices = getIndices(where_roles_are, 1, max_tries)
|
||||
|
||||
if (indices.length == 0) {
|
||||
listModel.append(new_item)
|
||||
return listModel.get(listModel.count)
|
||||
model.append(new_item)
|
||||
return model.get(model.count)
|
||||
}
|
||||
|
||||
if (update_if_exist != false) {
|
||||
listModel.set(indices[0], new_item)
|
||||
model.set(indices[0], new_item)
|
||||
}
|
||||
return listModel.get(indices[0])
|
||||
return model.get(indices[0])
|
||||
}
|
||||
|
||||
function pop(index) {
|
||||
var item = listModel.get(index)
|
||||
listModel.remove(index)
|
||||
var item = model.get(index)
|
||||
model.remove(index)
|
||||
return item
|
||||
}
|
||||
|
||||
function popWhere(where_role, is, max) {
|
||||
var indices = getIndices(where_role, is, max)
|
||||
var results = []
|
||||
function popWhere(roles_are, max_results, max_tries) {
|
||||
var indices = getIndices(roles_are, max_results, max_tries)
|
||||
var items = []
|
||||
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
results.push(listModel.get(indices[i]))
|
||||
listModel.remove(indices[i])
|
||||
items.push(model.get(indices[i]))
|
||||
model.remove(indices[i])
|
||||
}
|
||||
return results
|
||||
return items
|
||||
}
|
||||
|
||||
|
||||
function toObject(item_list) {
|
||||
item_list = item_list || listModel
|
||||
item_list = item_list || sortFilteredModel
|
||||
var obj_list = []
|
||||
|
||||
for (var i = 0; i < item_list.count; i++) {
|
||||
|
|
|
@ -6,6 +6,7 @@ Banner {
|
|||
|
||||
color: HStyle.chat.inviteBanner.background
|
||||
|
||||
// TODO: get disp name from models.users, inviter = userid now
|
||||
avatar.name: inviter ? inviter.displayname : ""
|
||||
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
||||
|
||||
|
|
|
@ -10,27 +10,27 @@ HColumnLayout {
|
|||
property string category: ""
|
||||
property string roomId: ""
|
||||
|
||||
readonly property var roomInfo:
|
||||
Backend.accounts.get(userId)
|
||||
.roomCategories.get(category)
|
||||
.rooms.get(roomId)
|
||||
readonly property var roomInfo: models.rooms.getWhere(
|
||||
{"userId": userId, "roomId": roomId, "category": category}, 1
|
||||
)[0]
|
||||
|
||||
readonly property var sender: Backend.users.get(userId)
|
||||
readonly property var sender:
|
||||
models.users.getWhere({"userId": userId}, 1)[0]
|
||||
|
||||
readonly property bool hasUnknownDevices:
|
||||
category == "Rooms" ?
|
||||
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
|
||||
readonly property bool hasUnknownDevices: false
|
||||
//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
|
||||
}
|
||||
}
|
||||
)
|
||||
//Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
|
||||
//function(forUserId, forRoomId, previous, now) {
|
||||
//if (chatPage && forUserId == userId && forRoomId == roomId) {
|
||||
//chatPage.category = now
|
||||
//}
|
||||
//}
|
||||
//)
|
||||
|
||||
RoomHeader {
|
||||
id: roomHeader
|
||||
|
@ -77,72 +77,72 @@ HColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
RoomSidePane {
|
||||
id: roomSidePane
|
||||
// RoomSidePane {
|
||||
//id: roomSidePane
|
||||
|
||||
activeView: roomHeader.activeButton
|
||||
property int oldWidth: width
|
||||
onActiveViewChanged:
|
||||
activeView ? restoreAnimation.start() : hideAnimation.start()
|
||||
//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
|
||||
//NumberAnimation {
|
||||
//id: hideAnimation
|
||||
//target: roomSidePane
|
||||
//properties: "width"
|
||||
//duration: HStyle.animationDuration
|
||||
//from: target.width
|
||||
//to: 0
|
||||
|
||||
onStarted: {
|
||||
target.oldWidth = target.width
|
||||
target.Layout.minimumWidth = 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
|
||||
//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 }
|
||||
)
|
||||
}
|
||||
//onStopped: target.Layout.minimumWidth = Qt.binding(
|
||||
//function() { return HStyle.avatar.size }
|
||||
//)
|
||||
//}
|
||||
|
||||
collapsed: width < HStyle.avatar.size + 8
|
||||
//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 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
|
||||
}
|
||||
}
|
||||
//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
|
||||
}
|
||||
//width: referenceWidth // Initial width
|
||||
//Layout.minimumWidth: HStyle.avatar.size
|
||||
//Layout.maximumWidth: parent.width
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import QtQuick 2.7
|
|||
import "../../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: dateTime.toLocaleDateString()
|
||||
text: model.date.toLocaleDateString()
|
||||
color: HStyle.chat.daybreak.foreground
|
||||
backgroundColor: HStyle.chat.daybreak.background
|
||||
radius: HStyle.chat.daybreak.radius
|
||||
|
|
|
@ -16,7 +16,7 @@ Row {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: sender.displayName.value
|
||||
name: sender.displayName || stripUserId(sender.userId)
|
||||
hidden: combine
|
||||
dimension: 28
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ Row {
|
|||
HAvatar {
|
||||
id: avatar
|
||||
hidden: combine
|
||||
name: sender.displayName.value
|
||||
name: senderInfo.displayName || stripUserId(model.senderId)
|
||||
dimension: 48
|
||||
}
|
||||
|
||||
|
@ -38,8 +38,8 @@ Row {
|
|||
visible: height > 0
|
||||
|
||||
id: nameLabel
|
||||
text: sender.displayName.value
|
||||
color: Qt.hsla(Backend.hueFromString(text),
|
||||
text: senderInfo.displayName || model.senderId
|
||||
color: Qt.hsla(avatar.hueFromName(avatar.name),
|
||||
HStyle.displayName.saturation,
|
||||
HStyle.displayName.lightness,
|
||||
1)
|
||||
|
@ -56,17 +56,16 @@ Row {
|
|||
width: parent.width
|
||||
|
||||
id: contentLabel
|
||||
text: (dict.formatted_body ?
|
||||
Backend.htmlFilter.filter(dict.formatted_body) :
|
||||
dict.body) +
|
||||
text: model.content +
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px color=" + HStyle.chat.message.date + ">" +
|
||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||
Qt.formatDateTime(model.date, "hh:mm:ss") +
|
||||
"</font>" +
|
||||
(isLocalEcho ?
|
||||
(model.isLocalEcho ?
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px>⏳</font>" : "")
|
||||
textFormat: Text.RichText
|
||||
textFormat: model.type == "text" ?
|
||||
Text.PlainText : Text.RichText
|
||||
color: HStyle.chat.message.body
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Column {
|
||||
id: roomEventDelegate
|
||||
|
@ -10,46 +9,47 @@ Column {
|
|||
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
|
||||
}
|
||||
|
||||
function getIsMessage(type) {
|
||||
return true
|
||||
}
|
||||
|
||||
property var previousItem: getPreviousItem()
|
||||
signal reloadPreviousItem()
|
||||
onReloadPreviousItem: previousItem = getPreviousItem()
|
||||
|
||||
readonly property bool isMessage: getIsMessage(type)
|
||||
property var senderInfo: null
|
||||
Component.onCompleted:
|
||||
senderInfo = models.users.getUser(chatPage.userId, senderId)
|
||||
|
||||
readonly property bool isUndecryptableEvent:
|
||||
type === "OlmEvent" || type === "MegolmEvent"
|
||||
//readonly property bool isMessage: ! model.type.match(/^event.*/)
|
||||
readonly property bool isMessage: getIsMessage(model.type)
|
||||
|
||||
readonly property var sender: Backend.users.get(dict.sender)
|
||||
readonly property bool isOwn: chatPage.userId === senderId
|
||||
|
||||
readonly property bool isOwn:
|
||||
chatPage.userId === dict.sender
|
||||
|
||||
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
||||
readonly property bool isFirstEvent: model.type == "eventCreate"
|
||||
|
||||
readonly property bool combine:
|
||||
previousItem &&
|
||||
! talkBreak &&
|
||||
! dayBreak &&
|
||||
getIsMessage(previousItem.type) === isMessage &&
|
||||
previousItem.dict.sender === dict.sender &&
|
||||
minsBetween(previousItem.dateTime, dateTime) <= 5
|
||||
previousItem.senderId === senderId &&
|
||||
minsBetween(previousItem.date, model.date) <= 5
|
||||
|
||||
readonly property bool dayBreak:
|
||||
isFirstEvent ||
|
||||
previousItem &&
|
||||
dateTime.getDate() != previousItem.dateTime.getDate()
|
||||
model.date.getDate() != previousItem.date.getDate()
|
||||
|
||||
readonly property bool talkBreak:
|
||||
previousItem &&
|
||||
! dayBreak &&
|
||||
minsBetween(previousItem.dateTime, dateTime) >= 20
|
||||
minsBetween(previousItem.date, model.date) >= 20
|
||||
|
||||
|
||||
property int standardSpacing: 16
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick 2.7
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
|
@ -8,10 +9,19 @@ HRectangle {
|
|||
|
||||
HListView {
|
||||
id: roomEventListView
|
||||
delegate: RoomEventDelegate {}
|
||||
model: Backend.roomEvents.get(chatPage.roomId)
|
||||
clip: true
|
||||
|
||||
model: HListModel {
|
||||
sourceModel: models.timelines
|
||||
|
||||
filters: ValueFilter {
|
||||
roleName: "roomId"
|
||||
value: chatPage.roomId
|
||||
}
|
||||
}
|
||||
|
||||
delegate: RoomEventDelegate {}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: space
|
||||
anchors.rightMargin: space
|
||||
|
@ -29,7 +39,7 @@ HRectangle {
|
|||
|
||||
onYPosChanged: {
|
||||
if (chatPage.category != "Invites" && yPos <= 0.1) {
|
||||
Backend.loadPastEvents(chatPage.roomId)
|
||||
//Backend.loadPastEvents(chatPage.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import QtQuick.Layouts 1.3
|
|||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
property string displayName: ""
|
||||
property var displayName: ""
|
||||
property string topic: ""
|
||||
|
||||
property alias buttonsImplicitWidth: viewButtons.implicitWidth
|
||||
|
@ -22,7 +22,7 @@ HRectangle {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: displayName
|
||||
name: stripRoomName(displayName) || qsTr("Empty room")
|
||||
dimension: roomHeader.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ MouseArea {
|
|||
|
||||
HAvatar {
|
||||
id: memberAvatar
|
||||
name: member.displayName.value
|
||||
name: member.displayName || stripUserId(member.userId)
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
|
|
|
@ -18,7 +18,7 @@ HRectangle {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: chatPage.sender.displayName.value
|
||||
name: chatPage.sender.displayName || stripUserId(chatPage.userId)
|
||||
dimension: root.Layout.minimumHeight
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
function clientId(user_id, category, room_id) {
|
||||
return user_id + " " + room_id + " " + category
|
||||
}
|
||||
|
||||
|
||||
function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
|
||||
topic, last_event_date, inviter, left_event) {
|
||||
|
||||
var client_id = clientId(user_id, category, room_id)
|
||||
models.roomCategories.upsert({"userId": user_id, "name": category}, {
|
||||
"userId": user_id,
|
||||
"name": category
|
||||
})
|
||||
|
||||
var rooms = models.rooms
|
||||
|
||||
function roles(for_category) {
|
||||
return {"userId": user_id, "roomId": room_id, "category": for_category}
|
||||
}
|
||||
|
||||
if (category == "Invites") {
|
||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id))
|
||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
||||
rooms.popWhere(roles("Rooms"), 1)
|
||||
rooms.popWhere(roles("Left"), 1)
|
||||
}
|
||||
else if (category == "Rooms") {
|
||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
||||
rooms.popWhere(roles("Invites"), 1)
|
||||
rooms.popWhere(roles("Left"), 1)
|
||||
}
|
||||
else if (category == "Left") {
|
||||
var old_room =
|
||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id)) ||
|
||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
||||
var old_room = rooms.popWhere(roles("Invites"), 1)[0] ||
|
||||
rooms.popWhere(roles("Rooms"), 1)[0]
|
||||
|
||||
if (old_room) {
|
||||
display_name = old_room.displayName
|
||||
|
@ -30,8 +32,7 @@ function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
|
|||
}
|
||||
}
|
||||
|
||||
rooms.upsert("clientId", client_id , {
|
||||
"clientId": client_id,
|
||||
rooms.upsert(roles(category), {
|
||||
"userId": user_id,
|
||||
"category": category,
|
||||
"roomId": room_id,
|
||||
|
@ -47,8 +48,8 @@ function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
|
|||
|
||||
|
||||
function onRoomDeleted(user_id, category, room_id) {
|
||||
var client_id = clientId(user_id, category, room_id)
|
||||
return models.rooms.popWhere("clientId", client_id, 1)
|
||||
var roles = {"userId": user_id, "roomId": room_id, "category": category}
|
||||
models.rooms.popWhere(roles, 1)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
function onHtmlMessageReceived(type, room_id, event_id, sender_id, date,
|
||||
is_local_echo, content) {
|
||||
models.timelines.upsert({"eventId": event_id}, {
|
||||
"type": type,
|
||||
"roomId": room_id,
|
||||
"eventId": event_id,
|
||||
"senderId": sender_id,
|
||||
"date": date,
|
||||
"isLocalEcho": is_local_echo,
|
||||
"content": content,
|
||||
}, true, 1000)
|
||||
}
|
|
@ -2,18 +2,17 @@ function onAccountUpdated(user_id) {
|
|||
models.accounts.append({"userId": user_id})
|
||||
}
|
||||
|
||||
function AccountDeleted(user_id) {
|
||||
models.accounts.popWhere("userId", user_id, 1)
|
||||
function onAccountDeleted(user_id) {
|
||||
models.accounts.popWhere({"userId": user_id}, 1)
|
||||
}
|
||||
|
||||
function onUserUpdated(user_id, display_name, avatar_url, status_message) {
|
||||
models.users.upsert("userId", user_id, {
|
||||
models.users.upsert({"userId": user_id}, {
|
||||
"userId": user_id,
|
||||
"displayName": display_name,
|
||||
"avatarUrl": avatar_url,
|
||||
"statusMessage": status_message
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function onDeviceUpdated(user_id, device_id, ed25519_key, trust, display_name,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick 2.7
|
||||
import SortFilterProxyModel 0.2
|
||||
import "Base"
|
||||
|
||||
QtObject {
|
||||
|
@ -8,7 +9,7 @@ QtObject {
|
|||
function getUser(as_account_id, wanted_user_id) {
|
||||
wanted_user_id = wanted_user_id || as_account_id
|
||||
|
||||
var found = users.getWhere("userId", wanted_user_id, 1)
|
||||
var found = users.getWhere({"userId": wanted_user_id}, 1)
|
||||
if (found.length > 0) { return found[0] }
|
||||
|
||||
users.append({
|
||||
|
@ -22,13 +23,20 @@ QtObject {
|
|||
as_account_id, "request_user_update_event", [wanted_user_id]
|
||||
)
|
||||
|
||||
return users.getWhere("userId", wanted_user_id, 1)[0]
|
||||
return users.getWhere({"userId": wanted_user_id}, 1)[0]
|
||||
}
|
||||
}
|
||||
|
||||
property HListModel devices: HListModel {}
|
||||
|
||||
property HListModel roomCategories: HListModel {}
|
||||
|
||||
property HListModel rooms: HListModel {}
|
||||
|
||||
property HListModel timelines: HListModel {}
|
||||
property HListModel timelines: HListModel {
|
||||
sorters: RoleSorter {
|
||||
roleName: "date"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import "../Base"
|
|||
|
||||
Item {
|
||||
property string loginWith: "username"
|
||||
property var client: null
|
||||
property string userId: ""
|
||||
|
||||
HInterfaceBox {
|
||||
id: rememberBox
|
||||
|
@ -20,10 +20,13 @@ Item {
|
|||
|
||||
buttonCallbacks: {
|
||||
"yes": function(button) {
|
||||
Backend.clients.remember(client)
|
||||
py.callCoro("save_account", [userId])
|
||||
pageStack.showPage("Default")
|
||||
},
|
||||
"no": function(button) {
|
||||
py.callCoro("forget_account", [userId])
|
||||
pageStack.showPage("Default")
|
||||
},
|
||||
"no": function(button) { pageStack.showPage("Default") },
|
||||
}
|
||||
|
||||
HLabel {
|
||||
|
|
|
@ -4,7 +4,7 @@ import "../Base"
|
|||
|
||||
Item {
|
||||
property string loginWith: "username"
|
||||
onFocusChanged: identifierField.forceActiveFocus()
|
||||
onFocusChanged: idField.forceActiveFocus()
|
||||
|
||||
HInterfaceBox {
|
||||
id: signInBox
|
||||
|
@ -23,15 +23,15 @@ Item {
|
|||
"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) {
|
||||
button.loading = true
|
||||
var args = [idField.text, passwordField.text]
|
||||
|
||||
py.callCoro("login_client", args, {}, function(user_id) {
|
||||
pageStack.showPage(
|
||||
"RememberAccount",
|
||||
{"loginWith": loginWith, "client": client}
|
||||
{"loginWith": loginWith, "userId": user_id}
|
||||
)
|
||||
button.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -58,7 +58,7 @@ Item {
|
|||
}
|
||||
|
||||
HTextField {
|
||||
id: identifierField
|
||||
id: idField
|
||||
placeholderText: qsTr(
|
||||
loginWith === "email" ? "Email" :
|
||||
loginWith === "phone" ? "Phone" :
|
||||
|
|
|
@ -9,6 +9,7 @@ Python {
|
|||
property bool ready: false
|
||||
property var pendingCoroutines: ({})
|
||||
|
||||
signal willLoadAccounts(bool will)
|
||||
property bool loadingAccounts: false
|
||||
|
||||
function callCoro(name, args, kwargs, callback) {
|
||||
|
@ -32,18 +33,20 @@ Python {
|
|||
}
|
||||
}
|
||||
|
||||
addImportPath("../..")
|
||||
importNames("src", ["APP"], function() {
|
||||
call("APP.start", [Qt.application.arguments], function(debug_on) {
|
||||
window.debug = debug_on
|
||||
addImportPath("src")
|
||||
addImportPath("qrc:/")
|
||||
importNames("python", ["APP"], function() {
|
||||
call("APP.is_debug_on", [Qt.application.arguments], function(on) {
|
||||
window.debug = on
|
||||
|
||||
callCoro("has_saved_accounts", [], {}, function(has) {
|
||||
loadingAccounts = has
|
||||
py.ready = true
|
||||
willLoadAccounts(has)
|
||||
|
||||
if (has) {
|
||||
py.loadingAccounts = true
|
||||
py.callCoro("load_saved_accounts", [], {}, function() {
|
||||
loadingAccounts = false
|
||||
py.loadingAccounts = false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -20,7 +20,7 @@ Column {
|
|||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: user.displayName
|
||||
name: user.displayName || stripUserId(user.userId)
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
|
|
|
@ -4,6 +4,8 @@ import "../Base"
|
|||
HRowLayout {
|
||||
id: toolBar
|
||||
|
||||
property alias roomFilter: filterField.text
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
|
||||
|
@ -17,8 +19,6 @@ HRowLayout {
|
|||
placeholderText: qsTr("Filter rooms")
|
||||
backgroundColor: HStyle.sidePane.filterRooms.background
|
||||
|
||||
onTextChanged: Backend.setRoomFilter(text)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
property string userId: ""
|
||||
|
||||
id: roomCategoriesList
|
||||
model: Backend.accounts.get(userId).roomCategories
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: models.roomCategories
|
||||
filters: ValueFilter {
|
||||
roleName: "userId"
|
||||
value: userId
|
||||
}
|
||||
}
|
||||
|
||||
delegate: RoomCategoryDelegate {}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,11 @@ MouseArea {
|
|||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
spacing: roomList.spacing
|
||||
spacing: sidePane.normalSpacing
|
||||
|
||||
HAvatar {
|
||||
id: roomAvatar
|
||||
name: displayName
|
||||
name: stripRoomName(displayName) || qsTr("Empty room")
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
|
@ -35,27 +35,27 @@ MouseArea {
|
|||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
|
||||
HLabel {
|
||||
function getText() {
|
||||
return SidePaneJS.getLastRoomEventText(
|
||||
roomId, roomList.userId
|
||||
)
|
||||
}
|
||||
//HLabel {
|
||||
//function getText() {
|
||||
//return SidePaneJS.getLastRoomEventText(
|
||||
//roomId, roomList.userId
|
||||
//)
|
||||
//}
|
||||
|
||||
property var lastEvTime: lastEventDateTime
|
||||
onLastEvTimeChanged: subtitleLabel.text = getText()
|
||||
//property var lastEvTime: lastEventDateTime
|
||||
//onLastEvTimeChanged: subtitleLabel.text = getText()
|
||||
|
||||
id: subtitleLabel
|
||||
visible: text !== ""
|
||||
text: getText()
|
||||
textFormat: Text.StyledText
|
||||
//id: subtitleLabel
|
||||
//visible: text !== ""
|
||||
//text: getText()
|
||||
//textFormat: Text.StyledText
|
||||
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
//font.pixelSize: HStyle.fontSize.small
|
||||
//elide: Text.ElideRight
|
||||
//maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
//Layout.maximumWidth: parent.width
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../Base"
|
||||
|
||||
HListView {
|
||||
|
@ -7,8 +8,37 @@ HListView {
|
|||
property string category: ""
|
||||
|
||||
id: roomList
|
||||
spacing: accountList.spacing
|
||||
model:
|
||||
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
|
||||
spacing: sidePane.normalSpacing
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: models.rooms
|
||||
filters: AllOf {
|
||||
ValueFilter {
|
||||
roleName: "category"
|
||||
value: category
|
||||
}
|
||||
|
||||
ValueFilter {
|
||||
roleName: "userId"
|
||||
value: userId
|
||||
}
|
||||
|
||||
ExpressionFilter {
|
||||
expression: {
|
||||
var filter = paneToolBar.roomFilter.toLowerCase()
|
||||
var words = filter.split(" ")
|
||||
var room_name = displayName.toLowerCase()
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
if (words[i] && room_name.indexOf(words[i]) == -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: RoomDelegate {}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,18 @@ HRectangle {
|
|||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: collapsed ? 0 : normalSpacing
|
||||
topMargin: spacing
|
||||
bottomMargin: spacing
|
||||
Layout.leftMargin: spacing
|
||||
spacing: collapsed ? 0 : normalSpacing * 3
|
||||
topMargin: normalSpacing
|
||||
bottomMargin: normalSpacing
|
||||
Layout.leftMargin: normalSpacing
|
||||
|
||||
Behavior on spacing {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
}
|
||||
|
||||
PaneToolBar {}
|
||||
PaneToolBar {
|
||||
id: paneToolBar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,15 @@ import "SidePane"
|
|||
Item {
|
||||
id: mainUI
|
||||
|
||||
Connections {
|
||||
target: py
|
||||
onWillLoadAccounts: function(will) {
|
||||
pageStack.showPage(will ? "Default" : "SignIn")
|
||||
}
|
||||
}
|
||||
|
||||
property bool accountsPresent:
|
||||
models.accounts.count > 0 || py.loadingAccounts
|
||||
onAccountsPresentChanged:
|
||||
pageStack.showPage(accountsPresent ? "Default" : "SignIn")
|
||||
|
||||
HImage {
|
||||
id: mainUIBackground
|
||||
|
|
1
submodules/SortFilterProxyModel
Submodule
1
submodules/SortFilterProxyModel
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 770789ee484abf69c230cbf1b64f39823e79a181
|
Loading…
Reference in New Issue
Block a user