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__
|
__pycache__
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
build
|
|
||||||
dist
|
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
|
|
||||||
.pylintrc
|
|
||||||
|
|
||||||
tmp-*
|
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 asyncio
|
||||||
|
import signal
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
@ -15,31 +16,16 @@ class App:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.appdirs = AppDirs(appname=__about__.__pkg_name__, roaming=True)
|
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 = asyncio.get_event_loop()
|
||||||
self.loop_thread = Thread(target=self._loop_starter)
|
self.loop_thread = Thread(target=self._loop_starter)
|
||||||
self.loop_thread.start()
|
self.loop_thread.start()
|
||||||
|
|
||||||
|
|
||||||
def start(self, cli_flags: Sequence[str] = ()) -> bool:
|
def is_debug_on(self, cli_flags: Sequence[str] = ()) -> bool:
|
||||||
debug = False
|
return "-d" in cli_flags or "--debug" in cli_flags
|
||||||
|
|
||||||
if "-d" in cli_flags or "--debug" in cli_flags:
|
|
||||||
self.run_in_loop(self._exit_on_app_file_change())
|
|
||||||
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 _loop_starter(self) -> None:
|
def _loop_starter(self) -> None:
|
||||||
|
@ -74,7 +60,7 @@ class App:
|
||||||
name: str,
|
name: str,
|
||||||
args: Optional[List[str]] = None,
|
args: Optional[List[str]] = None,
|
||||||
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
kwargs: Optional[Dict[str, Any]] = None) -> str:
|
||||||
client = self.backend.clients[account_id] # type: ignore
|
client = self.backend.clients[account_id]
|
||||||
return self._call_coro(
|
return self._call_coro(
|
||||||
getattr(client, name)(*args or [], **kwargs or {})
|
getattr(client, name)(*args or [], **kwargs or {})
|
||||||
)
|
)
|
||||||
|
@ -85,14 +71,19 @@ class App:
|
||||||
ad = additional_data
|
ad = additional_data
|
||||||
rl = self.run_in_loop
|
rl = self.run_in_loop
|
||||||
ba = self.backend
|
ba = self.backend
|
||||||
cl = self.backend.clients # type: ignore
|
cl = self.backend.clients
|
||||||
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
tcl = lambda user: cl[f"@test_{user}:matrix.org"]
|
||||||
|
|
||||||
import json
|
import json
|
||||||
jd = lambda obj: print(json.dumps(obj, indent=4, ensure_ascii=False))
|
jd = lambda obj: print(json.dumps(obj, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
import pdb
|
print("\n=> Run `socat readline tcp:127.0.0.1:4444` in a terminal "
|
||||||
pdb.set_trace()
|
"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()
|
APP = App()
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from atomicfile import AtomicFile
|
from atomicfile import AtomicFile
|
||||||
|
|
||||||
|
@ -29,13 +29,14 @@ class Backend:
|
||||||
user: str,
|
user: str,
|
||||||
password: str,
|
password: str,
|
||||||
device_id: Optional[str] = None,
|
device_id: Optional[str] = None,
|
||||||
homeserver: str = "https://matrix.org") -> None:
|
homeserver: str = "https://matrix.org") -> str:
|
||||||
client = MatrixClient(
|
client = MatrixClient(
|
||||||
user=user, homeserver=homeserver, device_id=device_id
|
user=user, homeserver=homeserver, device_id=device_id
|
||||||
)
|
)
|
||||||
await client.login(password)
|
await client.login(password)
|
||||||
self.clients[client.user_id] = client
|
self.clients[client.user_id] = client
|
||||||
users.AccountUpdated(client.user_id)
|
users.AccountUpdated(client.user_id)
|
||||||
|
return client.user_id
|
||||||
|
|
||||||
|
|
||||||
async def resume_client(self,
|
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({
|
await self._write_config({
|
||||||
**self.saved_accounts,
|
**self.saved_accounts,
|
||||||
client.userId: {
|
client.user_id: {
|
||||||
"hostname": client.nio.host,
|
"homeserver": client.homeserver,
|
||||||
"token": client.nio.access_token,
|
"token": client.access_token,
|
||||||
"device_id": client.nio.device_id,
|
"device_id": client.device_id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -119,7 +122,7 @@ class Backend:
|
||||||
async def _write_config(self, accounts: SavedAccounts) -> None:
|
async def _write_config(self, accounts: SavedAccounts) -> None:
|
||||||
js = json.dumps(accounts, indent=4, ensure_ascii=False, sort_keys=True)
|
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)
|
self.saved_accounts_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with AtomicFile(self.saved_accounts_path, "w") as new:
|
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 int dimension: HStyle.avatar.size
|
||||||
property bool hidden: false
|
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
|
var hue = 0
|
||||||
for (var i = 0; i < name.length; i++) {
|
for (var i = 0; i < name.length; i++) {
|
||||||
hue += name.charCodeAt(i) * 99
|
hue += name.charCodeAt(i) * 99
|
||||||
|
@ -24,7 +31,7 @@ Rectangle {
|
||||||
|
|
||||||
color: name ?
|
color: name ?
|
||||||
Qt.hsla(
|
Qt.hsla(
|
||||||
hue_from_name(name),
|
hueFromName(name),
|
||||||
HStyle.avatar.background.saturation,
|
HStyle.avatar.background.saturation,
|
||||||
HStyle.avatar.background.lightness,
|
HStyle.avatar.background.lightness,
|
||||||
HStyle.avatar.background.alpha
|
HStyle.avatar.background.alpha
|
||||||
|
|
|
@ -32,11 +32,6 @@ Button {
|
||||||
signal pressed
|
signal pressed
|
||||||
signal released
|
signal released
|
||||||
|
|
||||||
function loadingUntilFutureDone(future) {
|
|
||||||
loading = true
|
|
||||||
future.onGotResult.connect(function() { loading = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
id: button
|
id: button
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
|
|
|
@ -1,85 +1,112 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
ListModel {
|
SortFilterProxyModel {
|
||||||
// To initialize a HListModel with items,
|
// To initialize a HListModel with items,
|
||||||
// use `Component.onCompleted: extend([{"foo": 1, "bar": 2}, ...])`
|
// 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) {
|
function extend(new_items) {
|
||||||
for (var i = 0; i < new_items.length; i++) {
|
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 = []
|
var results = []
|
||||||
|
|
||||||
for (var i = 0; i < listModel.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
if (listModel.get(i)[where_role] == is) {
|
var item = model.get(i)
|
||||||
results.push(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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include) {
|
||||||
|
results.push(i)
|
||||||
|
if (max_results && results.length >= max_results) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max_tries && i >= max_tries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhere(where_role, is, max) {
|
function getWhere(roles_are, max_results, max_tries) {
|
||||||
var indices = getIndices(where_role, is, max)
|
var indices = getIndices(roles_are, max_results, max_tries)
|
||||||
var results = []
|
var items = []
|
||||||
|
|
||||||
for (var i = 0; i < indices.length; i++) {
|
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) {
|
function forEachWhere(roles_are, func, max_results, max_tries) {
|
||||||
var items = getWhere(where_role, is, max)
|
var items = getWhere(roles_are, max_results, max_tries)
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
func(item)
|
func(items[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(where_role, is, new_item, update_if_exist) {
|
function upsert(where_roles_are, new_item, update_if_exist, max_tries) {
|
||||||
// new_item can contain only the keys we're interested in updating
|
var indices = getIndices(where_roles_are, 1, max_tries)
|
||||||
|
|
||||||
var indices = getIndices(where_role, is, 1)
|
|
||||||
|
|
||||||
if (indices.length == 0) {
|
if (indices.length == 0) {
|
||||||
listModel.append(new_item)
|
model.append(new_item)
|
||||||
return listModel.get(listModel.count)
|
return model.get(model.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update_if_exist != false) {
|
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) {
|
function pop(index) {
|
||||||
var item = listModel.get(index)
|
var item = model.get(index)
|
||||||
listModel.remove(index)
|
model.remove(index)
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
function popWhere(where_role, is, max) {
|
function popWhere(roles_are, max_results, max_tries) {
|
||||||
var indices = getIndices(where_role, is, max)
|
var indices = getIndices(roles_are, max_results, max_tries)
|
||||||
var results = []
|
var items = []
|
||||||
|
|
||||||
for (var i = 0; i < indices.length; i++) {
|
for (var i = 0; i < indices.length; i++) {
|
||||||
results.push(listModel.get(indices[i]))
|
items.push(model.get(indices[i]))
|
||||||
listModel.remove(indices[i])
|
model.remove(indices[i])
|
||||||
}
|
}
|
||||||
return results
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toObject(item_list) {
|
function toObject(item_list) {
|
||||||
item_list = item_list || listModel
|
item_list = item_list || sortFilteredModel
|
||||||
var obj_list = []
|
var obj_list = []
|
||||||
|
|
||||||
for (var i = 0; i < item_list.count; i++) {
|
for (var i = 0; i < item_list.count; i++) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ Banner {
|
||||||
|
|
||||||
color: HStyle.chat.inviteBanner.background
|
color: HStyle.chat.inviteBanner.background
|
||||||
|
|
||||||
|
// TODO: get disp name from models.users, inviter = userid now
|
||||||
avatar.name: inviter ? inviter.displayname : ""
|
avatar.name: inviter ? inviter.displayname : ""
|
||||||
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
||||||
|
|
||||||
|
|
|
@ -10,27 +10,27 @@ HColumnLayout {
|
||||||
property string category: ""
|
property string category: ""
|
||||||
property string roomId: ""
|
property string roomId: ""
|
||||||
|
|
||||||
readonly property var roomInfo:
|
readonly property var roomInfo: models.rooms.getWhere(
|
||||||
Backend.accounts.get(userId)
|
{"userId": userId, "roomId": roomId, "category": category}, 1
|
||||||
.roomCategories.get(category)
|
)[0]
|
||||||
.rooms.get(roomId)
|
|
||||||
|
|
||||||
readonly property var sender: Backend.users.get(userId)
|
readonly property var sender:
|
||||||
|
models.users.getWhere({"userId": userId}, 1)[0]
|
||||||
|
|
||||||
readonly property bool hasUnknownDevices:
|
readonly property bool hasUnknownDevices: false
|
||||||
category == "Rooms" ?
|
//category == "Rooms" ?
|
||||||
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
|
//Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
|
||||||
|
|
||||||
id: chatPage
|
id: chatPage
|
||||||
onFocusChanged: sendBox.setFocus()
|
onFocusChanged: sendBox.setFocus()
|
||||||
|
|
||||||
Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
|
//Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
|
||||||
function(forUserId, forRoomId, previous, now) {
|
//function(forUserId, forRoomId, previous, now) {
|
||||||
if (chatPage && forUserId == userId && forRoomId == roomId) {
|
//if (chatPage && forUserId == userId && forRoomId == roomId) {
|
||||||
chatPage.category = now
|
//chatPage.category = now
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
)
|
//)
|
||||||
|
|
||||||
RoomHeader {
|
RoomHeader {
|
||||||
id: roomHeader
|
id: roomHeader
|
||||||
|
@ -77,72 +77,72 @@ HColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomSidePane {
|
// RoomSidePane {
|
||||||
id: roomSidePane
|
//id: roomSidePane
|
||||||
|
|
||||||
activeView: roomHeader.activeButton
|
//activeView: roomHeader.activeButton
|
||||||
property int oldWidth: width
|
//property int oldWidth: width
|
||||||
onActiveViewChanged:
|
//onActiveViewChanged:
|
||||||
activeView ? restoreAnimation.start() : hideAnimation.start()
|
//activeView ? restoreAnimation.start() : hideAnimation.start()
|
||||||
|
|
||||||
NumberAnimation {
|
//NumberAnimation {
|
||||||
id: hideAnimation
|
//id: hideAnimation
|
||||||
target: roomSidePane
|
//target: roomSidePane
|
||||||
properties: "width"
|
//properties: "width"
|
||||||
duration: HStyle.animationDuration
|
//duration: HStyle.animationDuration
|
||||||
from: target.width
|
//from: target.width
|
||||||
to: 0
|
//to: 0
|
||||||
|
|
||||||
onStarted: {
|
//onStarted: {
|
||||||
target.oldWidth = target.width
|
//target.oldWidth = target.width
|
||||||
target.Layout.minimumWidth = 0
|
//target.Layout.minimumWidth = 0
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
|
|
||||||
NumberAnimation {
|
//NumberAnimation {
|
||||||
id: restoreAnimation
|
//id: restoreAnimation
|
||||||
target: roomSidePane
|
//target: roomSidePane
|
||||||
properties: "width"
|
//properties: "width"
|
||||||
duration: HStyle.animationDuration
|
//duration: HStyle.animationDuration
|
||||||
from: 0
|
//from: 0
|
||||||
to: target.oldWidth
|
//to: target.oldWidth
|
||||||
|
|
||||||
onStopped: target.Layout.minimumWidth = Qt.binding(
|
//onStopped: target.Layout.minimumWidth = Qt.binding(
|
||||||
function() { return HStyle.avatar.size }
|
//function() { return HStyle.avatar.size }
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
|
|
||||||
collapsed: width < HStyle.avatar.size + 8
|
//collapsed: width < HStyle.avatar.size + 8
|
||||||
|
|
||||||
property bool wasSnapped: false
|
//property bool wasSnapped: false
|
||||||
property int referenceWidth: roomHeader.buttonsWidth
|
//property int referenceWidth: roomHeader.buttonsWidth
|
||||||
onReferenceWidthChanged: {
|
//onReferenceWidthChanged: {
|
||||||
if (chatSplitView.canAutoSize || wasSnapped) {
|
//if (chatSplitView.canAutoSize || wasSnapped) {
|
||||||
if (wasSnapped) { chatSplitView.canAutoSize = true }
|
//if (wasSnapped) { chatSplitView.canAutoSize = true }
|
||||||
width = referenceWidth
|
//width = referenceWidth
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
|
|
||||||
property int currentWidth: width
|
//property int currentWidth: width
|
||||||
onCurrentWidthChanged: {
|
//onCurrentWidthChanged: {
|
||||||
if (referenceWidth != width &&
|
//if (referenceWidth != width &&
|
||||||
referenceWidth - 15 < width &&
|
//referenceWidth - 15 < width &&
|
||||||
width < referenceWidth + 15)
|
//width < referenceWidth + 15)
|
||||||
{
|
//{
|
||||||
currentWidth = referenceWidth
|
//currentWidth = referenceWidth
|
||||||
width = referenceWidth
|
//width = referenceWidth
|
||||||
wasSnapped = true
|
//wasSnapped = true
|
||||||
currentWidth = Qt.binding(
|
//currentWidth = Qt.binding(
|
||||||
function() { return roomSidePane.width }
|
//function() { return roomSidePane.width }
|
||||||
)
|
//)
|
||||||
} else {
|
//} else {
|
||||||
wasSnapped = false
|
//wasSnapped = false
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
|
|
||||||
width: referenceWidth // Initial width
|
//width: referenceWidth // Initial width
|
||||||
Layout.minimumWidth: HStyle.avatar.size
|
//Layout.minimumWidth: HStyle.avatar.size
|
||||||
Layout.maximumWidth: parent.width
|
//Layout.maximumWidth: parent.width
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import QtQuick 2.7
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HNoticePage {
|
HNoticePage {
|
||||||
text: dateTime.toLocaleDateString()
|
text: model.date.toLocaleDateString()
|
||||||
color: HStyle.chat.daybreak.foreground
|
color: HStyle.chat.daybreak.foreground
|
||||||
backgroundColor: HStyle.chat.daybreak.background
|
backgroundColor: HStyle.chat.daybreak.background
|
||||||
radius: HStyle.chat.daybreak.radius
|
radius: HStyle.chat.daybreak.radius
|
||||||
|
|
|
@ -16,7 +16,7 @@ Row {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
name: sender.displayName.value
|
name: sender.displayName || stripUserId(sender.userId)
|
||||||
hidden: combine
|
hidden: combine
|
||||||
dimension: 28
|
dimension: 28
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ Row {
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
hidden: combine
|
hidden: combine
|
||||||
name: sender.displayName.value
|
name: senderInfo.displayName || stripUserId(model.senderId)
|
||||||
dimension: 48
|
dimension: 48
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ Row {
|
||||||
visible: height > 0
|
visible: height > 0
|
||||||
|
|
||||||
id: nameLabel
|
id: nameLabel
|
||||||
text: sender.displayName.value
|
text: senderInfo.displayName || model.senderId
|
||||||
color: Qt.hsla(Backend.hueFromString(text),
|
color: Qt.hsla(avatar.hueFromName(avatar.name),
|
||||||
HStyle.displayName.saturation,
|
HStyle.displayName.saturation,
|
||||||
HStyle.displayName.lightness,
|
HStyle.displayName.lightness,
|
||||||
1)
|
1)
|
||||||
|
@ -56,17 +56,16 @@ Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
id: contentLabel
|
id: contentLabel
|
||||||
text: (dict.formatted_body ?
|
text: model.content +
|
||||||
Backend.htmlFilter.filter(dict.formatted_body) :
|
|
||||||
dict.body) +
|
|
||||||
" <font size=" + HStyle.fontSize.small +
|
" <font size=" + HStyle.fontSize.small +
|
||||||
"px color=" + HStyle.chat.message.date + ">" +
|
"px color=" + HStyle.chat.message.date + ">" +
|
||||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
Qt.formatDateTime(model.date, "hh:mm:ss") +
|
||||||
"</font>" +
|
"</font>" +
|
||||||
(isLocalEcho ?
|
(model.isLocalEcho ?
|
||||||
" <font size=" + HStyle.fontSize.small +
|
" <font size=" + HStyle.fontSize.small +
|
||||||
"px>⏳</font>" : "")
|
"px>⏳</font>" : "")
|
||||||
textFormat: Text.RichText
|
textFormat: model.type == "text" ?
|
||||||
|
Text.PlainText : Text.RichText
|
||||||
color: HStyle.chat.message.body
|
color: HStyle.chat.message.body
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
import "../utils.js" as ChatJS
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: roomEventDelegate
|
id: roomEventDelegate
|
||||||
|
@ -10,46 +9,47 @@ Column {
|
||||||
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
|
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIsMessage(type_) { return type_.startsWith("RoomMessage") }
|
|
||||||
|
|
||||||
function getPreviousItem() {
|
function getPreviousItem() {
|
||||||
return index < roomEventListView.model.count - 1 ?
|
return index < roomEventListView.model.count - 1 ?
|
||||||
roomEventListView.model.get(index + 1) : null
|
roomEventListView.model.get(index + 1) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIsMessage(type) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
property var previousItem: getPreviousItem()
|
property var previousItem: getPreviousItem()
|
||||||
signal reloadPreviousItem()
|
signal reloadPreviousItem()
|
||||||
onReloadPreviousItem: previousItem = getPreviousItem()
|
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:
|
//readonly property bool isMessage: ! model.type.match(/^event.*/)
|
||||||
type === "OlmEvent" || type === "MegolmEvent"
|
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:
|
readonly property bool isFirstEvent: model.type == "eventCreate"
|
||||||
chatPage.userId === dict.sender
|
|
||||||
|
|
||||||
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
|
||||||
|
|
||||||
readonly property bool combine:
|
readonly property bool combine:
|
||||||
previousItem &&
|
previousItem &&
|
||||||
! talkBreak &&
|
! talkBreak &&
|
||||||
! dayBreak &&
|
! dayBreak &&
|
||||||
getIsMessage(previousItem.type) === isMessage &&
|
getIsMessage(previousItem.type) === isMessage &&
|
||||||
previousItem.dict.sender === dict.sender &&
|
previousItem.senderId === senderId &&
|
||||||
minsBetween(previousItem.dateTime, dateTime) <= 5
|
minsBetween(previousItem.date, model.date) <= 5
|
||||||
|
|
||||||
readonly property bool dayBreak:
|
readonly property bool dayBreak:
|
||||||
isFirstEvent ||
|
isFirstEvent ||
|
||||||
previousItem &&
|
previousItem &&
|
||||||
dateTime.getDate() != previousItem.dateTime.getDate()
|
model.date.getDate() != previousItem.date.getDate()
|
||||||
|
|
||||||
readonly property bool talkBreak:
|
readonly property bool talkBreak:
|
||||||
previousItem &&
|
previousItem &&
|
||||||
! dayBreak &&
|
! dayBreak &&
|
||||||
minsBetween(previousItem.dateTime, dateTime) >= 20
|
minsBetween(previousItem.date, model.date) >= 20
|
||||||
|
|
||||||
|
|
||||||
property int standardSpacing: 16
|
property int standardSpacing: 16
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HRectangle {
|
HRectangle {
|
||||||
|
@ -8,10 +9,19 @@ HRectangle {
|
||||||
|
|
||||||
HListView {
|
HListView {
|
||||||
id: roomEventListView
|
id: roomEventListView
|
||||||
delegate: RoomEventDelegate {}
|
|
||||||
model: Backend.roomEvents.get(chatPage.roomId)
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
model: HListModel {
|
||||||
|
sourceModel: models.timelines
|
||||||
|
|
||||||
|
filters: ValueFilter {
|
||||||
|
roleName: "roomId"
|
||||||
|
value: chatPage.roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: RoomEventDelegate {}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: space
|
anchors.leftMargin: space
|
||||||
anchors.rightMargin: space
|
anchors.rightMargin: space
|
||||||
|
@ -29,7 +39,7 @@ HRectangle {
|
||||||
|
|
||||||
onYPosChanged: {
|
onYPosChanged: {
|
||||||
if (chatPage.category != "Invites" && yPos <= 0.1) {
|
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"
|
import "../Base"
|
||||||
|
|
||||||
HRectangle {
|
HRectangle {
|
||||||
property string displayName: ""
|
property var displayName: ""
|
||||||
property string topic: ""
|
property string topic: ""
|
||||||
|
|
||||||
property alias buttonsImplicitWidth: viewButtons.implicitWidth
|
property alias buttonsImplicitWidth: viewButtons.implicitWidth
|
||||||
|
@ -22,7 +22,7 @@ HRectangle {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
name: displayName
|
name: stripRoomName(displayName) || qsTr("Empty room")
|
||||||
dimension: roomHeader.height
|
dimension: roomHeader.height
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ MouseArea {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: memberAvatar
|
id: memberAvatar
|
||||||
name: member.displayName.value
|
name: member.displayName || stripUserId(member.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
|
|
|
@ -18,7 +18,7 @@ HRectangle {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
name: chatPage.sender.displayName.value
|
name: chatPage.sender.displayName || stripUserId(chatPage.userId)
|
||||||
dimension: root.Layout.minimumHeight
|
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,
|
function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
|
||||||
topic, last_event_date, inviter, left_event) {
|
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
|
var rooms = models.rooms
|
||||||
|
|
||||||
|
function roles(for_category) {
|
||||||
|
return {"userId": user_id, "roomId": room_id, "category": for_category}
|
||||||
|
}
|
||||||
|
|
||||||
if (category == "Invites") {
|
if (category == "Invites") {
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id))
|
rooms.popWhere(roles("Rooms"), 1)
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
rooms.popWhere(roles("Left"), 1)
|
||||||
}
|
}
|
||||||
else if (category == "Rooms") {
|
else if (category == "Rooms") {
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
rooms.popWhere(roles("Invites"), 1)
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
|
rooms.popWhere(roles("Left"), 1)
|
||||||
}
|
}
|
||||||
else if (category == "Left") {
|
else if (category == "Left") {
|
||||||
var old_room =
|
var old_room = rooms.popWhere(roles("Invites"), 1)[0] ||
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id)) ||
|
rooms.popWhere(roles("Rooms"), 1)[0]
|
||||||
rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
|
|
||||||
|
|
||||||
if (old_room) {
|
if (old_room) {
|
||||||
display_name = old_room.displayName
|
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 , {
|
rooms.upsert(roles(category), {
|
||||||
"clientId": client_id,
|
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"category": category,
|
"category": category,
|
||||||
"roomId": room_id,
|
"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) {
|
function onRoomDeleted(user_id, category, room_id) {
|
||||||
var client_id = clientId(user_id, category, room_id)
|
var roles = {"userId": user_id, "roomId": room_id, "category": category}
|
||||||
return models.rooms.popWhere("clientId", client_id, 1)
|
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})
|
models.accounts.append({"userId": user_id})
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountDeleted(user_id) {
|
function onAccountDeleted(user_id) {
|
||||||
models.accounts.popWhere("userId", user_id, 1)
|
models.accounts.popWhere({"userId": user_id}, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserUpdated(user_id, display_name, avatar_url, status_message) {
|
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,
|
"userId": user_id,
|
||||||
"displayName": display_name,
|
"displayName": display_name,
|
||||||
"avatarUrl": avatar_url,
|
"avatarUrl": avatar_url,
|
||||||
"statusMessage": status_message
|
"statusMessage": status_message
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDeviceUpdated(user_id, device_id, ed25519_key, trust, display_name,
|
function onDeviceUpdated(user_id, device_id, ed25519_key, trust, display_name,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
import "Base"
|
import "Base"
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
|
@ -8,7 +9,7 @@ QtObject {
|
||||||
function getUser(as_account_id, wanted_user_id) {
|
function getUser(as_account_id, wanted_user_id) {
|
||||||
wanted_user_id = wanted_user_id || as_account_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] }
|
if (found.length > 0) { return found[0] }
|
||||||
|
|
||||||
users.append({
|
users.append({
|
||||||
|
@ -22,13 +23,20 @@ QtObject {
|
||||||
as_account_id, "request_user_update_event", [wanted_user_id]
|
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 devices: HListModel {}
|
||||||
|
|
||||||
|
property HListModel roomCategories: HListModel {}
|
||||||
|
|
||||||
property HListModel rooms: 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 {
|
Item {
|
||||||
property string loginWith: "username"
|
property string loginWith: "username"
|
||||||
property var client: null
|
property string userId: ""
|
||||||
|
|
||||||
HInterfaceBox {
|
HInterfaceBox {
|
||||||
id: rememberBox
|
id: rememberBox
|
||||||
|
@ -20,10 +20,13 @@ Item {
|
||||||
|
|
||||||
buttonCallbacks: {
|
buttonCallbacks: {
|
||||||
"yes": function(button) {
|
"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")
|
pageStack.showPage("Default")
|
||||||
},
|
},
|
||||||
"no": function(button) { pageStack.showPage("Default") },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import "../Base"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property string loginWith: "username"
|
property string loginWith: "username"
|
||||||
onFocusChanged: identifierField.forceActiveFocus()
|
onFocusChanged: idField.forceActiveFocus()
|
||||||
|
|
||||||
HInterfaceBox {
|
HInterfaceBox {
|
||||||
id: signInBox
|
id: signInBox
|
||||||
|
@ -23,15 +23,15 @@ Item {
|
||||||
"register": function(button) {},
|
"register": function(button) {},
|
||||||
|
|
||||||
"login": function(button) {
|
"login": function(button) {
|
||||||
var future = Backend.clients.new(
|
button.loading = true
|
||||||
"matrix.org", identifierField.text, passwordField.text
|
var args = [idField.text, passwordField.text]
|
||||||
)
|
|
||||||
button.loadingUntilFutureDone(future)
|
py.callCoro("login_client", args, {}, function(user_id) {
|
||||||
future.onGotResult.connect(function(client) {
|
|
||||||
pageStack.showPage(
|
pageStack.showPage(
|
||||||
"RememberAccount",
|
"RememberAccount",
|
||||||
{"loginWith": loginWith, "client": client}
|
{"loginWith": loginWith, "userId": user_id}
|
||||||
)
|
)
|
||||||
|
button.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
HTextField {
|
HTextField {
|
||||||
id: identifierField
|
id: idField
|
||||||
placeholderText: qsTr(
|
placeholderText: qsTr(
|
||||||
loginWith === "email" ? "Email" :
|
loginWith === "email" ? "Email" :
|
||||||
loginWith === "phone" ? "Phone" :
|
loginWith === "phone" ? "Phone" :
|
||||||
|
|
|
@ -9,6 +9,7 @@ Python {
|
||||||
property bool ready: false
|
property bool ready: false
|
||||||
property var pendingCoroutines: ({})
|
property var pendingCoroutines: ({})
|
||||||
|
|
||||||
|
signal willLoadAccounts(bool will)
|
||||||
property bool loadingAccounts: false
|
property bool loadingAccounts: false
|
||||||
|
|
||||||
function callCoro(name, args, kwargs, callback) {
|
function callCoro(name, args, kwargs, callback) {
|
||||||
|
@ -32,18 +33,20 @@ Python {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addImportPath("../..")
|
addImportPath("src")
|
||||||
importNames("src", ["APP"], function() {
|
addImportPath("qrc:/")
|
||||||
call("APP.start", [Qt.application.arguments], function(debug_on) {
|
importNames("python", ["APP"], function() {
|
||||||
window.debug = debug_on
|
call("APP.is_debug_on", [Qt.application.arguments], function(on) {
|
||||||
|
window.debug = on
|
||||||
|
|
||||||
callCoro("has_saved_accounts", [], {}, function(has) {
|
callCoro("has_saved_accounts", [], {}, function(has) {
|
||||||
loadingAccounts = has
|
|
||||||
py.ready = true
|
py.ready = true
|
||||||
|
willLoadAccounts(has)
|
||||||
|
|
||||||
if (has) {
|
if (has) {
|
||||||
|
py.loadingAccounts = true
|
||||||
py.callCoro("load_saved_accounts", [], {}, function() {
|
py.callCoro("load_saved_accounts", [], {}, function() {
|
||||||
loadingAccounts = false
|
py.loadingAccounts = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,7 +20,7 @@ Column {
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
name: user.displayName
|
name: user.displayName || stripUserId(user.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import "../Base"
|
||||||
HRowLayout {
|
HRowLayout {
|
||||||
id: toolBar
|
id: toolBar
|
||||||
|
|
||||||
|
property alias roomFilter: filterField.text
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||||
|
|
||||||
|
@ -17,8 +19,6 @@ HRowLayout {
|
||||||
placeholderText: qsTr("Filter rooms")
|
placeholderText: qsTr("Filter rooms")
|
||||||
backgroundColor: HStyle.sidePane.filterRooms.background
|
backgroundColor: HStyle.sidePane.filterRooms.background
|
||||||
|
|
||||||
onTextChanged: Backend.setRoomFilter(text)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: parent.height
|
Layout.preferredHeight: parent.height
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
|
||||||
HListView {
|
HListView {
|
||||||
property string userId: ""
|
property string userId: ""
|
||||||
|
|
||||||
id: roomCategoriesList
|
id: roomCategoriesList
|
||||||
model: Backend.accounts.get(userId).roomCategories
|
|
||||||
|
model: SortFilterProxyModel {
|
||||||
|
sourceModel: models.roomCategories
|
||||||
|
filters: ValueFilter {
|
||||||
|
roleName: "userId"
|
||||||
|
value: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: RoomCategoryDelegate {}
|
delegate: RoomCategoryDelegate {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ MouseArea {
|
||||||
|
|
||||||
HRowLayout {
|
HRowLayout {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: roomList.spacing
|
spacing: sidePane.normalSpacing
|
||||||
|
|
||||||
HAvatar {
|
HAvatar {
|
||||||
id: roomAvatar
|
id: roomAvatar
|
||||||
name: displayName
|
name: stripRoomName(displayName) || qsTr("Empty room")
|
||||||
}
|
}
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
|
@ -35,27 +35,27 @@ MouseArea {
|
||||||
Layout.maximumWidth: parent.width
|
Layout.maximumWidth: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabel {
|
//HLabel {
|
||||||
function getText() {
|
//function getText() {
|
||||||
return SidePaneJS.getLastRoomEventText(
|
//return SidePaneJS.getLastRoomEventText(
|
||||||
roomId, roomList.userId
|
//roomId, roomList.userId
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
|
|
||||||
property var lastEvTime: lastEventDateTime
|
//property var lastEvTime: lastEventDateTime
|
||||||
onLastEvTimeChanged: subtitleLabel.text = getText()
|
//onLastEvTimeChanged: subtitleLabel.text = getText()
|
||||||
|
|
||||||
id: subtitleLabel
|
//id: subtitleLabel
|
||||||
visible: text !== ""
|
//visible: text !== ""
|
||||||
text: getText()
|
//text: getText()
|
||||||
textFormat: Text.StyledText
|
//textFormat: Text.StyledText
|
||||||
|
|
||||||
font.pixelSize: HStyle.fontSize.small
|
//font.pixelSize: HStyle.fontSize.small
|
||||||
elide: Text.ElideRight
|
//elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
//maximumLineCount: 1
|
||||||
|
|
||||||
Layout.maximumWidth: parent.width
|
//Layout.maximumWidth: parent.width
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
|
||||||
HListView {
|
HListView {
|
||||||
|
@ -7,8 +8,37 @@ HListView {
|
||||||
property string category: ""
|
property string category: ""
|
||||||
|
|
||||||
id: roomList
|
id: roomList
|
||||||
spacing: accountList.spacing
|
spacing: sidePane.normalSpacing
|
||||||
model:
|
|
||||||
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
|
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 {}
|
delegate: RoomDelegate {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,18 @@ HRectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
spacing: collapsed ? 0 : normalSpacing
|
spacing: collapsed ? 0 : normalSpacing * 3
|
||||||
topMargin: spacing
|
topMargin: normalSpacing
|
||||||
bottomMargin: spacing
|
bottomMargin: normalSpacing
|
||||||
Layout.leftMargin: spacing
|
Layout.leftMargin: normalSpacing
|
||||||
|
|
||||||
Behavior on spacing {
|
Behavior on spacing {
|
||||||
NumberAnimation { duration: HStyle.animationDuration }
|
NumberAnimation { duration: HStyle.animationDuration }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaneToolBar {}
|
PaneToolBar {
|
||||||
|
id: paneToolBar
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,15 @@ import "SidePane"
|
||||||
Item {
|
Item {
|
||||||
id: mainUI
|
id: mainUI
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: py
|
||||||
|
onWillLoadAccounts: function(will) {
|
||||||
|
pageStack.showPage(will ? "Default" : "SignIn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property bool accountsPresent:
|
property bool accountsPresent:
|
||||||
models.accounts.count > 0 || py.loadingAccounts
|
models.accounts.count > 0 || py.loadingAccounts
|
||||||
onAccountsPresentChanged:
|
|
||||||
pageStack.showPage(accountsPresent ? "Default" : "SignIn")
|
|
||||||
|
|
||||||
HImage {
|
HImage {
|
||||||
id: mainUIBackground
|
id: mainUIBackground
|
||||||
|
|
1
submodules/SortFilterProxyModel
Submodule
1
submodules/SortFilterProxyModel
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 770789ee484abf69c230cbf1b64f39823e79a181
|
Loading…
Reference in New Issue
Block a user