Capitalization, list model and room header work
- Standardized capitalization for variables and file names everywhere in QML and JS, get rid of mixed camelCase/snakeCase, use camelCase like everywhere in Qt - ListModel items are now stored and returned as real QObjects with PyQt properties and signals. This makes dynamic property binding a lot easier and eliminates the need for many hacks. - New update(), updateOrAppendWhere() methods and roles property for ListModel - RoomHeader now properly updates when the room title or topic changes - Add Backend.pdb(), to make it easier to start the debugger from QML
This commit is contained in:
parent
b33f5f1d34
commit
8f35e60801
4
Makefile
4
Makefile
|
@ -6,12 +6,14 @@ PKG_DIR = harmonyqml
|
||||||
PYTHON = python3
|
PYTHON = python3
|
||||||
PIP = pip3
|
PIP = pip3
|
||||||
PYLINT = pylint
|
PYLINT = pylint
|
||||||
|
MYPY = mypy
|
||||||
VULTURE = vulture
|
VULTURE = vulture
|
||||||
CLOC = cloc
|
CLOC = cloc
|
||||||
|
|
||||||
ARCHIVE_FORMATS = gztar
|
ARCHIVE_FORMATS = gztar
|
||||||
INSTALL_FLAGS = --user --editable
|
INSTALL_FLAGS = --user --editable
|
||||||
PYLINT_FLAGS = --output-format colorized
|
PYLINT_FLAGS = --output-format colorized
|
||||||
|
MYPY_FLAGS = --ignore-missing-imports
|
||||||
VULTURE_FLAGS = --min-confidence 100
|
VULTURE_FLAGS = --min-confidence 100
|
||||||
CLOC_FLAGS = --ignore-whitespace
|
CLOC_FLAGS = --ignore-whitespace
|
||||||
|
|
||||||
|
@ -47,6 +49,8 @@ upload: dist
|
||||||
test:
|
test:
|
||||||
- ${PYLINT} ${PYLINT_FLAGS} ${PKG_DIR} *.py
|
- ${PYLINT} ${PYLINT_FLAGS} ${PKG_DIR} *.py
|
||||||
@echo
|
@echo
|
||||||
|
- ${MYPY} ${PKG_DIR} ${MYPY_FLAGS}
|
||||||
|
@echo
|
||||||
- ${VULTURE} ${PKG_DIR} ${VULTURE_FLAGS}
|
- ${VULTURE} ${PKG_DIR} ${VULTURE_FLAGS}
|
||||||
@echo
|
@echo
|
||||||
${CLOC} ${CLOC_FLAGS} ${PKG_DIR}
|
${CLOC} ${CLOC_FLAGS} ${PKG_DIR}
|
||||||
|
|
5
TODO.md
5
TODO.md
|
@ -1,6 +1,5 @@
|
||||||
- Separate categories for invited, group and direct rooms
|
- Separate categories for invited, group and direct rooms
|
||||||
- Invited → Accept/Deny dialog
|
- Invited → Accept/Deny dialog
|
||||||
- Keep the room header name and topic updated
|
|
||||||
- Merge login page
|
- Merge login page
|
||||||
|
|
||||||
- When inviting someone to direct chat, room is "Empty room" until accepted,
|
- When inviting someone to direct chat, room is "Empty room" until accepted,
|
||||||
|
@ -14,7 +13,7 @@
|
||||||
- Use Loader? for MessageDelegate to show sub-components based on condition
|
- Use Loader? for MessageDelegate to show sub-components based on condition
|
||||||
- Better names and organization for the Message components
|
- Better names and organization for the Message components
|
||||||
|
|
||||||
- Migrate more JS functions to their own files
|
- Migrate more JS functions to their own files / Implement in Python instead
|
||||||
|
|
||||||
- Set Qt parents for all QObject
|
- Set Qt parents for all QObject
|
||||||
|
|
||||||
|
@ -40,3 +39,5 @@
|
||||||
- Verify E2E working
|
- Verify E2E working
|
||||||
|
|
||||||
- Multiaccount aliases
|
- Multiaccount aliases
|
||||||
|
|
||||||
|
- Fix tooltip hide()
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Dict, Set
|
from typing import Dict, Sequence, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
|
@ -96,3 +96,15 @@ class Backend(QObject):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Room not found in any client: {room_id}")
|
raise ValueError(f"Room not found in any client: {room_id}")
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
@pyqtSlot(list)
|
||||||
|
def pdb(self, additional_data: Sequence = ()) -> None:
|
||||||
|
# pylint: disable=all
|
||||||
|
ad = additional_data
|
||||||
|
re = self.models.roomEvents.get(ad[1])
|
||||||
|
import pdb
|
||||||
|
from PyQt5.QtCore import pyqtRemoveInputHook
|
||||||
|
pyqtRemoveInputHook()
|
||||||
|
pdb.set_trace()
|
||||||
|
|
|
@ -67,11 +67,11 @@ class Client(QObject):
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "%s(host=%r, port=%r, user_id=%r)" % \
|
return "%s(host=%r, port=%r, user_id=%r)" % \
|
||||||
(type(self).__name__, self.host, self.port, self.userID)
|
(type(self).__name__, self.host, self.port, self.userId)
|
||||||
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant=True)
|
||||||
def userID(self) -> str:
|
def userId(self) -> str:
|
||||||
return self.nio.user_id
|
return self.nio.user_id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class ClientManager(QObject):
|
||||||
|
|
||||||
|
|
||||||
def _on_connected(self, client: Client) -> None:
|
def _on_connected(self, client: Client) -> None:
|
||||||
self.clients[client.userID] = client
|
self.clients[client.userId] = client
|
||||||
self.clientAdded.emit(client)
|
self.clientAdded.emit(client)
|
||||||
client.startSyncing()
|
client.startSyncing()
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ class ClientManager(QObject):
|
||||||
def configAdd(self, client: Client) -> None:
|
def configAdd(self, client: Client) -> None:
|
||||||
self._write_config({
|
self._write_config({
|
||||||
**self.configAccounts(),
|
**self.configAccounts(),
|
||||||
**{client.userID: {
|
**{client.userId: {
|
||||||
"hostname": client.nio.host,
|
"hostname": client.nio.host,
|
||||||
"token": client.nio.access_token,
|
"token": client.nio.access_token,
|
||||||
"device_id": client.nio.device_id,
|
"device_id": client.nio.device_id,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright 2019 miruka
|
# Copyright 2019 miruka
|
||||||
# This file is part of harmonyqml, licensed under GPLv3.
|
# This file is part of harmonyqml, licensed under GPLv3.
|
||||||
|
|
||||||
|
from . import items
|
||||||
from .list_model import ListModel
|
from .list_model import ListModel
|
||||||
from .list_model_map import ListModelMap
|
from .list_model_map import ListModelMap
|
||||||
from .qml_models import QMLModels
|
from .qml_models import QMLModels
|
||||||
from . import enums, items
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Copyright 2019 miruka
|
|
||||||
# This file is part of harmonyqml, licensed under GPLv3.
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class Activity(Enum):
|
|
||||||
none = 0
|
|
||||||
focus = 1
|
|
||||||
paused_typing = 2
|
|
||||||
typing = 3
|
|
||||||
|
|
||||||
|
|
||||||
class Presence(Enum):
|
|
||||||
none = 0
|
|
||||||
offline = 1
|
|
||||||
invisible = 2
|
|
||||||
away = 3
|
|
||||||
busy = 4
|
|
||||||
online = 5
|
|
||||||
|
|
||||||
|
|
||||||
class MessageKind(Enum):
|
|
||||||
audio = "m.audio"
|
|
||||||
emote = "m.emote"
|
|
||||||
file = "m.file"
|
|
||||||
image = "m.image"
|
|
||||||
location = "m.location"
|
|
||||||
notice = "m.notice"
|
|
||||||
text = "m.text"
|
|
||||||
video = "m.video"
|
|
|
@ -1,29 +1,83 @@
|
||||||
# Copyright 2019 miruka
|
from typing import Any, Callable, Optional, Tuple, Union
|
||||||
# This file is part of harmonyqml, licensed under GPLv3.
|
|
||||||
|
|
||||||
from typing import Dict, List, NamedTuple, Optional
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
from PyQt5.QtCore import QDateTime
|
|
||||||
|
|
||||||
from ..pyqt_future import PyQtFuture
|
|
||||||
|
|
||||||
|
|
||||||
class User(NamedTuple):
|
class ListItem(QObject):
|
||||||
user_id: str
|
roles: Tuple[str, ...] = ()
|
||||||
display_name: PyQtFuture
|
|
||||||
avatar_url: Optional[str] = None
|
def __init__(self, *args, **kwargs):
|
||||||
status_message: Optional[str] = None
|
super().__init__()
|
||||||
|
|
||||||
|
for role, value in zip(self.roles, args):
|
||||||
|
setattr(self, role, value)
|
||||||
|
|
||||||
|
for role, value in kwargs.items():
|
||||||
|
setattr(self, role, value)
|
||||||
|
|
||||||
|
|
||||||
class Room(NamedTuple):
|
def __repr__(self) -> str:
|
||||||
room_id: str
|
return "%s(%s)" % (
|
||||||
display_name: Optional[str]
|
type(self).__name__,
|
||||||
description: str = ""
|
", ".join((f"{r}={getattr(self, r)!r}" for r in self.roles)),
|
||||||
typing_users: List[str] = []
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomEvent(NamedTuple):
|
@pyqtProperty(str, constant=True)
|
||||||
type: str
|
def repr(self) -> str:
|
||||||
date_time: QDateTime
|
return repr(self)
|
||||||
dict: Dict[str, str]
|
|
||||||
is_local_echo: bool = False
|
|
||||||
|
def prop(qt_type: Union[str, Callable],
|
||||||
|
name: str,
|
||||||
|
signal: Optional[pyqtSignal] = None,
|
||||||
|
default_value: Any = None) -> pyqtProperty:
|
||||||
|
|
||||||
|
def fget(self, name=name, default_value=default_value):
|
||||||
|
if not hasattr(self, f"_{name}"):
|
||||||
|
setattr(self, f"_{name}", default_value)
|
||||||
|
return getattr(self, f"_{name}")
|
||||||
|
|
||||||
|
def fset(self, value, name=name, signal=signal):
|
||||||
|
setattr(self, f"_{name}", value)
|
||||||
|
if signal:
|
||||||
|
getattr(self, f"{name}Changed").emit(value)
|
||||||
|
|
||||||
|
kws = {"notify": signal} if signal else {"constant": True}
|
||||||
|
|
||||||
|
return pyqtProperty(qt_type, fget=fget, fset=fset, **kws)
|
||||||
|
|
||||||
|
|
||||||
|
class User(ListItem):
|
||||||
|
roles = ("userId", "displayName", "avatarUrl", "statusMessage")
|
||||||
|
|
||||||
|
displayNameChanged = pyqtSignal("QVariant")
|
||||||
|
avatarUrlChanged = pyqtSignal("QVariant")
|
||||||
|
statusMessageChanged = pyqtSignal(str)
|
||||||
|
|
||||||
|
userId = prop(str, "userId")
|
||||||
|
displayName = prop("QVariant", "displayName", displayNameChanged)
|
||||||
|
avatarUrl = prop(str, "avatarUrl", avatarUrlChanged)
|
||||||
|
statusMessage = prop(str, "statusMessage", statusMessageChanged, "")
|
||||||
|
|
||||||
|
|
||||||
|
class Room(ListItem):
|
||||||
|
roles = ("roomId", "displayName", "topic", "typingUsers")
|
||||||
|
|
||||||
|
displayNameChanged = pyqtSignal("QVariant")
|
||||||
|
topicChanged = pyqtSignal(str)
|
||||||
|
typingUsersChanged = pyqtSignal("QVariantList")
|
||||||
|
|
||||||
|
roomId = prop(str, "roomId")
|
||||||
|
displayName = prop(str, "displayName", displayNameChanged)
|
||||||
|
topic = prop(str, "topic", topicChanged, "")
|
||||||
|
typingUsers = prop(list, "typingUsers", typingUsersChanged, [])
|
||||||
|
|
||||||
|
|
||||||
|
class RoomEvent(ListItem):
|
||||||
|
roles = ("type", "dateTime", "dict", "isLocalEcho")
|
||||||
|
|
||||||
|
type = prop(str, "type")
|
||||||
|
dateTime = prop("QVariant", "dateTime")
|
||||||
|
dict = prop("QVariantMap", "dict")
|
||||||
|
isLocalEcho = prop(bool, "isLocalEcho", None, False)
|
||||||
|
|
|
@ -4,27 +4,25 @@ from typing import (
|
||||||
Sequence, Tuple, Union
|
Sequence, Tuple, Union
|
||||||
)
|
)
|
||||||
|
|
||||||
from namedlist import namedlist
|
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QAbstractListModel, QModelIndex, QObject, Qt, pyqtProperty, pyqtSignal,
|
QAbstractListModel, QModelIndex, QObject, Qt, pyqtProperty, pyqtSignal,
|
||||||
pyqtSlot
|
pyqtSlot
|
||||||
)
|
)
|
||||||
|
|
||||||
NewValue = Union[Mapping[str, Any], Sequence]
|
from .items import ListItem
|
||||||
ReturnItem = Dict[str, Any]
|
|
||||||
|
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
|
||||||
|
|
||||||
|
|
||||||
class ListModel(QAbstractListModel):
|
class ListModel(QAbstractListModel):
|
||||||
changed = pyqtSignal()
|
changed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
initial_data: Optional[List[NewValue]] = None,
|
initial_data: Optional[List[NewItem]] = None,
|
||||||
container: Callable[..., MutableSequence] = list,
|
container: Callable[..., MutableSequence] = list,
|
||||||
parent: Optional[QObject] = None) -> None:
|
parent: Optional[QObject] = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._ref_namedlist = None
|
self._data: MutableSequence[ListItem] = container()
|
||||||
self._roles: Tuple[str, ...] = ()
|
|
||||||
self._data: MutableSequence = container()
|
|
||||||
|
|
||||||
if initial_data:
|
if initial_data:
|
||||||
self.extend(initial_data)
|
self.extend(initial_data)
|
||||||
|
@ -50,57 +48,64 @@ class ListModel(QAbstractListModel):
|
||||||
return self.rowCount()
|
return self.rowCount()
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtProperty(list)
|
||||||
|
def roles(self) -> Tuple[str, ...]:
|
||||||
|
return self._data[0].roles if self._data else () # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def roleNames(self) -> Dict[int, bytes]:
|
def roleNames(self) -> Dict[int, bytes]:
|
||||||
return {Qt.UserRole + i: bytes(f, "utf-8")
|
return {Qt.UserRole + i: bytes(f, "utf-8")
|
||||||
for i, f in enumerate(self._roles, 1)}
|
for i, f in enumerate(self.roles, 1)} \
|
||||||
|
if self._data else {}
|
||||||
|
|
||||||
|
|
||||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||||
if role <= Qt.UserRole:
|
if role <= Qt.UserRole:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._data[index.row()][role - Qt.UserRole - 1]
|
return getattr(self._data[index.row()],
|
||||||
|
str(self.roleNames()[role], "utf8"))
|
||||||
|
|
||||||
|
|
||||||
def rowCount(self, _: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, _: QModelIndex = QModelIndex()) -> int:
|
||||||
return len(self._data)
|
return len(self._data)
|
||||||
|
|
||||||
|
|
||||||
def _convert_new_value(self, value: NewValue) -> Any:
|
def _convert_new_value(self, value: NewItem) -> ListItem:
|
||||||
if isinstance(value, Mapping):
|
def convert() -> ListItem:
|
||||||
if not self._ref_namedlist:
|
if self._data and isinstance(value, Mapping):
|
||||||
self._ref_namedlist = namedlist("ListItem", value.keys())
|
assert set(value.keys()) <= set(self.roles), \
|
||||||
self._roles = tuple(value.keys())
|
f"{value}: must have all these keys: {self.roles}"
|
||||||
|
|
||||||
return self._ref_namedlist(**value) # type: ignore
|
return type(self._data[0])(**value)
|
||||||
|
|
||||||
if isinstance(value, Sequence):
|
if not self._data and isinstance(value, Mapping):
|
||||||
if not self._ref_namedlist:
|
raise NotImplementedError("First item must be set from Python")
|
||||||
try:
|
|
||||||
self._ref_namedlist = namedlist(
|
|
||||||
value.__class__.__name__, value._fields # type: ignore
|
|
||||||
)
|
|
||||||
self._roles = tuple(value._fields) # type: ignore
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError(
|
|
||||||
"Need a mapping/dict, namedtuple or namedlist as "
|
|
||||||
"first value to set allowed keys/fields."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._ref_namedlist(*value) # type: ignore
|
if self._data and isinstance(value, type(self._data[0])):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if not self._data and isinstance(value, ListItem):
|
||||||
|
return value
|
||||||
|
|
||||||
raise TypeError("Value must be a mapping or sequence.")
|
raise TypeError("%r: must be mapping or %s" % (
|
||||||
|
value,
|
||||||
|
type(self._data[0]).__name__ if self._data else "ListItem"
|
||||||
|
))
|
||||||
|
|
||||||
|
value = convert()
|
||||||
|
value.setParent(self)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
@pyqtProperty(int, constant=True)
|
||||||
def count(self) -> int: # pylint: disable=arguments-differ
|
def count(self) -> int:
|
||||||
return self.rowCount()
|
return self.rowCount()
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(int, result="QVariantMap")
|
@pyqtSlot(int, result="QVariant")
|
||||||
def get(self, index: int) -> ReturnItem:
|
def get(self, index: int) -> ListItem:
|
||||||
return self._data[index]._asdict()
|
return self._data[index]
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariant", result=int)
|
@pyqtSlot(str, "QVariant", result=int)
|
||||||
|
@ -109,17 +114,17 @@ class ListModel(QAbstractListModel):
|
||||||
if getattr(item, prop) == is_value:
|
if getattr(item, prop) == is_value:
|
||||||
return i
|
return i
|
||||||
|
|
||||||
raise ValueError(f"No {type(self._ref_namedlist)} in list with "
|
raise ValueError(f"No item in model data with "
|
||||||
f"property {prop!r} set to {is_value!r}.")
|
f"property {prop!r} set to {is_value!r}.")
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariant", result="QVariantMap")
|
@pyqtSlot(str, "QVariant", result="QVariant")
|
||||||
def getWhere(self, prop: str, is_value: Any) -> ReturnItem:
|
def getWhere(self, prop: str, is_value: Any) -> ListItem:
|
||||||
return self.get(self.indexWhere(prop, is_value))
|
return self.get(self.indexWhere(prop, is_value))
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(int, list)
|
@pyqtSlot(int, "QVariantMap")
|
||||||
def insert(self, index: int, value: NewValue) -> None:
|
def insert(self, index: int, value: NewItem) -> None:
|
||||||
value = self._convert_new_value(value)
|
value = self._convert_new_value(value)
|
||||||
self.beginInsertRows(QModelIndex(), index, index)
|
self.beginInsertRows(QModelIndex(), index, index)
|
||||||
self._data.insert(index, value)
|
self._data.insert(index, value)
|
||||||
|
@ -127,19 +132,44 @@ class ListModel(QAbstractListModel):
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(list)
|
@pyqtSlot("QVariantMap")
|
||||||
def append(self, value: NewValue) -> None:
|
def append(self, value: NewItem) -> None:
|
||||||
self.insert(self.rowCount(), value)
|
self.insert(self.rowCount(), value)
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(list)
|
@pyqtSlot(list)
|
||||||
def extend(self, values: Iterable[NewValue]) -> None:
|
def extend(self, values: Iterable[NewItem]) -> None:
|
||||||
for val in values:
|
for val in values:
|
||||||
self.append(val)
|
self.append(val)
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtSlot("QVariantMap")
|
||||||
|
def update(self, index: int, value: NewItem) -> None:
|
||||||
|
value = self._convert_new_value(value)
|
||||||
|
|
||||||
|
for role in self.roles:
|
||||||
|
setattr(self._data[index], role, getattr(value, role))
|
||||||
|
|
||||||
|
qidx = QAbstractListModel.index(self, index, 0)
|
||||||
|
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||||
|
self.changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
@pyqtSlot(str, "QVariant", "QVariantMap")
|
||||||
|
def updateOrAppendWhere(
|
||||||
|
self, prop: str, is_value: Any, update_with: NewItem
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
index = self.indexWhere(prop, is_value)
|
||||||
|
self.update(index, update_with)
|
||||||
|
except ValueError:
|
||||||
|
index = self.rowCount()
|
||||||
|
self.append(update_with)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(int, list)
|
@pyqtSlot(int, list)
|
||||||
def set(self, index: int, value: NewValue) -> None:
|
def set(self, index: int, value: NewItem) -> None:
|
||||||
qidx = QAbstractListModel.index(self, index, 0)
|
qidx = QAbstractListModel.index(self, index, 0)
|
||||||
value = self._convert_new_value(value)
|
value = self._convert_new_value(value)
|
||||||
self._data[index] = value
|
self._data[index] = value
|
||||||
|
@ -149,16 +179,16 @@ class ListModel(QAbstractListModel):
|
||||||
|
|
||||||
@pyqtSlot(int, str, "QVariant")
|
@pyqtSlot(int, str, "QVariant")
|
||||||
def setProperty(self, index: int, prop: str, value: Any) -> None:
|
def setProperty(self, index: int, prop: str, value: Any) -> None:
|
||||||
self._data[index][self._roles.index(prop)] = value
|
setattr(self._data[index], prop, value)
|
||||||
qidx = QAbstractListModel.index(self, index, 0)
|
qidx = QAbstractListModel.index(self, index, 0)
|
||||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
@pyqtSlot(int, int)
|
@pyqtSlot(int, int)
|
||||||
@pyqtSlot(int, int, int)
|
@pyqtSlot(int, int, int)
|
||||||
def move(self, from_: int, to: int, n: int = 1) -> None:
|
def move(self, from_: int, to: int, n: int = 1) -> None:
|
||||||
|
# pylint: disable=invalid-name
|
||||||
qlast = from_ + n - 1
|
qlast = from_ + n - 1
|
||||||
|
|
||||||
if (n <= 0) or (from_ == to) or (qlast == to) or \
|
if (n <= 0) or (from_ == to) or (qlast == to) or \
|
||||||
|
@ -186,7 +216,7 @@ class ListModel(QAbstractListModel):
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def remove(self, index: int) -> None: # pylint: disable=arguments-differ
|
def remove(self, index: int) -> None:
|
||||||
self.beginRemoveRows(QModelIndex(), index, index)
|
self.beginRemoveRows(QModelIndex(), index, index)
|
||||||
del self._data[index]
|
del self._data[index]
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
|
|
|
@ -132,8 +132,6 @@ class NetworkManager:
|
||||||
response = self.read(sock)
|
response = self.read(sock)
|
||||||
|
|
||||||
except (OSError, RemoteTransportError) as err:
|
except (OSError, RemoteTransportError) as err:
|
||||||
logging.error("Connection error for %s: %s",
|
|
||||||
nio_func.__name__, str(err))
|
|
||||||
self._close_socket(sock)
|
self._close_socket(sock)
|
||||||
self.http_disconnect()
|
self.http_disconnect()
|
||||||
retry.sleep(max_time=2)
|
retry.sleep(max_time=2)
|
||||||
|
|
|
@ -31,14 +31,14 @@ class SignalManager(QObject):
|
||||||
def onClientAdded(self, client: Client) -> None:
|
def onClientAdded(self, client: Client) -> None:
|
||||||
self.connectClient(client)
|
self.connectClient(client)
|
||||||
self.backend.models.accounts.append(User(
|
self.backend.models.accounts.append(User(
|
||||||
user_id = client.userID,
|
userId = client.userId,
|
||||||
display_name = self.backend.getUserDisplayName(client.userID),
|
displayName = self.backend.getUserDisplayName(client.userId),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def onClientDeleted(self, user_id: str) -> None:
|
def onClientDeleted(self, user_id: str) -> None:
|
||||||
accs = self.backend.models.accounts
|
accs = self.backend.models.accounts
|
||||||
del accs[accs.indexWhere("user_id", user_id)]
|
del accs[accs.indexWhere("userId", user_id)]
|
||||||
|
|
||||||
|
|
||||||
def connectClient(self, client: Client) -> None:
|
def connectClient(self, client: Client) -> None:
|
||||||
|
@ -58,7 +58,7 @@ class SignalManager(QObject):
|
||||||
|
|
||||||
|
|
||||||
def onRoomJoined(self, client: Client, room_id: str) -> None:
|
def onRoomJoined(self, client: Client, room_id: str) -> None:
|
||||||
model = self.backend.models.rooms[client.userID]
|
model = self.backend.models.rooms[client.userId]
|
||||||
room = client.nio.rooms[room_id]
|
room = client.nio.rooms[room_id]
|
||||||
|
|
||||||
def group_name() -> Optional[str]:
|
def group_name() -> Optional[str]:
|
||||||
|
@ -66,22 +66,17 @@ class SignalManager(QObject):
|
||||||
return None if name == "Empty room?" else name
|
return None if name == "Empty room?" else name
|
||||||
|
|
||||||
item = Room(
|
item = Room(
|
||||||
room_id = room_id,
|
roomId = room_id,
|
||||||
display_name = room.name or room.canonical_alias or group_name(),
|
displayName = room.name or room.canonical_alias or group_name(),
|
||||||
description = room.topic,
|
topic = room.topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
model.updateOrAppendWhere("roomId", room_id, item)
|
||||||
index = model.indexWhere("room_id", room_id)
|
|
||||||
except ValueError:
|
|
||||||
model.append(item)
|
|
||||||
else:
|
|
||||||
model[index] = item
|
|
||||||
|
|
||||||
|
|
||||||
def onRoomLeft(self, client: Client, room_id: str) -> None:
|
def onRoomLeft(self, client: Client, room_id: str) -> None:
|
||||||
rooms = self.backend.models.rooms[client.userID]
|
rooms = self.backend.models.rooms[client.userId]
|
||||||
del rooms[rooms.indexWhere("room_id", room_id)]
|
del rooms[rooms.indexWhere("roomId", room_id)]
|
||||||
|
|
||||||
|
|
||||||
def onRoomSyncPrevBatchTokenReceived(
|
def onRoomSyncPrevBatchTokenReceived(
|
||||||
|
@ -116,15 +111,15 @@ class SignalManager(QObject):
|
||||||
model = self.backend.models.roomEvents[room_id]
|
model = self.backend.models.roomEvents[room_id]
|
||||||
date_time = QDateTime\
|
date_time = QDateTime\
|
||||||
.fromMSecsSinceEpoch(edict["server_timestamp"])
|
.fromMSecsSinceEpoch(edict["server_timestamp"])
|
||||||
new_event = RoomEvent(type=etype, date_time=date_time, dict=edict)
|
new_event = RoomEvent(type=etype, dateTime=date_time, dict=edict)
|
||||||
|
|
||||||
if self._events_in_transfer:
|
if self._events_in_transfer:
|
||||||
local_echoes_met: int = 0
|
local_echoes_met: int = 0
|
||||||
replace_at: Optional[int] = None
|
update_at: Optional[int] = None
|
||||||
|
|
||||||
# Find if any locally echoed event corresponds to new_event
|
# Find if any locally echoed event corresponds to new_event
|
||||||
for i, event in enumerate(model):
|
for i, event in enumerate(model):
|
||||||
if not event.is_local_echo:
|
if not event.isLocalEcho:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sb = (event.dict["sender"], event.dict["body"])
|
sb = (event.dict["sender"], event.dict["body"])
|
||||||
|
@ -132,23 +127,23 @@ class SignalManager(QObject):
|
||||||
|
|
||||||
if sb == new_sb:
|
if sb == new_sb:
|
||||||
# The oldest matching local echo shall be replaced
|
# The oldest matching local echo shall be replaced
|
||||||
replace_at = max(replace_at or 0, i)
|
update_at = max(update_at or 0, i)
|
||||||
|
|
||||||
local_echoes_met += 1
|
local_echoes_met += 1
|
||||||
if local_echoes_met >= self._events_in_transfer:
|
if local_echoes_met >= self._events_in_transfer:
|
||||||
break
|
break
|
||||||
|
|
||||||
if replace_at is not None:
|
if update_at is not None:
|
||||||
model[replace_at] = new_event
|
model.update(update_at, new_event)
|
||||||
self._events_in_transfer -= 1
|
self._events_in_transfer -= 1
|
||||||
return
|
return
|
||||||
|
|
||||||
for i, event in enumerate(model):
|
for i, event in enumerate(model):
|
||||||
if event.is_local_echo:
|
if event.isLocalEcho:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Model is sorted from newest to oldest message
|
# Model is sorted from newest to oldest message
|
||||||
if new_event.date_time > event.date_time:
|
if new_event.dateTime > event.dateTime:
|
||||||
model.insert(i, new_event)
|
model.insert(i, new_event)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -159,8 +154,8 @@ class SignalManager(QObject):
|
||||||
self, client: Client, room_id: str, users: List[str]
|
self, client: Client, room_id: str, users: List[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
rooms = self.backend.models.rooms[client.userID]
|
rooms = self.backend.models.rooms[client.userId]
|
||||||
rooms[rooms.indexWhere("room_id", room_id)].typing_users = users
|
rooms[rooms.indexWhere("roomId", room_id)].typingUsers = users
|
||||||
|
|
||||||
|
|
||||||
def onMessageAboutToBeSent(
|
def onMessageAboutToBeSent(
|
||||||
|
@ -171,15 +166,15 @@ class SignalManager(QObject):
|
||||||
model = self.backend.models.roomEvents[room_id]
|
model = self.backend.models.roomEvents[room_id]
|
||||||
nio_event = nio.events.RoomMessage.parse_event({
|
nio_event = nio.events.RoomMessage.parse_event({
|
||||||
"event_id": "",
|
"event_id": "",
|
||||||
"sender": client.userID,
|
"sender": client.userId,
|
||||||
"origin_server_ts": timestamp,
|
"origin_server_ts": timestamp,
|
||||||
"content": content,
|
"content": content,
|
||||||
})
|
})
|
||||||
event = RoomEvent(
|
event = RoomEvent(
|
||||||
type = type(nio_event).__name__,
|
type = type(nio_event).__name__,
|
||||||
date_time = QDateTime.fromMSecsSinceEpoch(timestamp),
|
dateTime = QDateTime.fromMSecsSinceEpoch(timestamp),
|
||||||
dict = nio_event.__dict__,
|
dict = nio_event.__dict__,
|
||||||
is_local_echo = True,
|
isLocalEcho = True,
|
||||||
)
|
)
|
||||||
model.insert(0, event)
|
model.insert(0, event)
|
||||||
self._events_in_transfer += 1
|
self._events_in_transfer += 1
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 1.4 as Controls1
|
import QtQuick.Controls 1.4 as Controls1
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.4
|
import QtQuick.Layouts 1.4
|
||||||
import "side_pane" as SidePane
|
import "sidePane" as SidePane
|
||||||
import "chat" as Chat
|
import "chat" as Chat
|
||||||
|
|
||||||
//https://doc.qt.io/qt-5/qml-qtquick-controls-splitview.html
|
//https://doc.qt.io/qt-5/qml-qtquick-controls-splitview.html
|
||||||
|
@ -14,12 +15,9 @@ Controls1.SplitView {
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
function show_page(componentName) {
|
function showRoom(userId, roomId) {
|
||||||
pageStack.replace(componentName + ".qml")
|
|
||||||
}
|
|
||||||
function show_room(user_id, room_obj) {
|
|
||||||
pageStack.replace(
|
pageStack.replace(
|
||||||
"chat/Root.qml", { user_id: user_id, room: room_obj }
|
"chat/Root.qml", { userId: userId, roomId: roomId }
|
||||||
)
|
)
|
||||||
console.log("replaced")
|
console.log("replaced")
|
||||||
}
|
}
|
||||||
|
@ -28,6 +26,12 @@ Controls1.SplitView {
|
||||||
|
|
||||||
onCurrentItemChanged: currentItem.forceActiveFocus()
|
onCurrentItemChanged: currentItem.forceActiveFocus()
|
||||||
|
|
||||||
|
initialItem: MouseArea { // TODO: (test, remove)
|
||||||
|
onClicked: pageStack.showRoom(
|
||||||
|
"@test_mary:matrix.org", "!VDSsFIzQnXARSCVNxS:matrix.org"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Buggy
|
// Buggy
|
||||||
replaceExit: null
|
replaceExit: null
|
||||||
popExit: null
|
popExit: null
|
||||||
|
|
|
@ -8,7 +8,7 @@ Item {
|
||||||
property var imageSource: null
|
property var imageSource: null
|
||||||
property int dimmension: 48
|
property int dimmension: 48
|
||||||
|
|
||||||
readonly property string resolved_name:
|
readonly property string resolvedName:
|
||||||
! name ? "?" :
|
! name ? "?" :
|
||||||
typeof(name) == "string" ? name :
|
typeof(name) == "string" ? name :
|
||||||
(name.value ? name.value : "?")
|
(name.value ? name.value : "?")
|
||||||
|
@ -21,13 +21,13 @@ Item {
|
||||||
id: "letterRectangle"
|
id: "letterRectangle"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: ! invisible && imageSource === null
|
visible: ! invisible && imageSource === null
|
||||||
color: resolved_name === "?" ?
|
color: resolvedName === "?" ?
|
||||||
Qt.hsla(0, 0, 0.22, 1) :
|
Qt.hsla(0, 0, 0.22, 1) :
|
||||||
Qt.hsla(Backend.hueFromString(resolved_name), 0.22, 0.5, 1)
|
Qt.hsla(Backend.hueFromString(resolvedName), 0.22, 0.5, 1)
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: resolved_name.charAt(0)
|
text: resolvedName.charAt(0)
|
||||||
color: "white"
|
color: "white"
|
||||||
font.pixelSize: letterRectangle.height / 1.4
|
font.pixelSize: letterRectangle.height / 1.4
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ ToolButton {
|
||||||
onClicked: toolTip.hide()
|
onClicked: toolTip.hide()
|
||||||
|
|
||||||
ToolTip {
|
ToolTip {
|
||||||
id: "toolTip"
|
id: toolTip
|
||||||
text: tooltip
|
text: tooltip
|
||||||
delay: Qt.styleHints.mousePressAndHoldInterval
|
delay: Qt.styleHints.mousePressAndHoldInterval
|
||||||
visible: text ? toolTipZone.containsMouse : false
|
visible: text ? toolTipZone.containsMouse : false
|
||||||
|
|
|
@ -2,11 +2,14 @@ import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.0
|
import QtQuick.Controls 2.0
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
|
property string toolTipText: ""
|
||||||
|
|
||||||
id: text
|
id: text
|
||||||
|
|
||||||
ToolTip {
|
ToolTip {
|
||||||
delay: Qt.styleHints.mousePressAndHoldInterval
|
delay: Qt.styleHints.mousePressAndHoldInterval
|
||||||
visible: text ? toolTipZone.containsMouse : false
|
visible: text ? toolTipZone.containsMouse : false
|
||||||
text: user_id
|
text: toolTipText
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: toolTipZone
|
id: toolTipZone
|
||||||
|
|
|
@ -2,12 +2,13 @@ import QtQuick 2.7
|
||||||
import "../base" as Base
|
import "../base" as Base
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
text: date_time.toLocaleDateString()
|
|
||||||
width: messageDelegate.width
|
width: messageDelegate.width
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
topPadding: messageDelegate.isFirstMessage ?
|
topPadding: messageDelegate.isFirstMessage ?
|
||||||
0 : messageDelegate.standardSpacing
|
0 : messageDelegate.standardSpacing
|
||||||
bottomPadding: messageDelegate.standardSpacing
|
bottomPadding: messageDelegate.standardSpacing
|
||||||
|
|
||||||
|
text: dateTime.toLocaleDateString()
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
font.pixelSize: normalSize * 1.1
|
font.pixelSize: normalSize * 1.1
|
||||||
color: "darkolivegreen"
|
color: "darkolivegreen"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ RowLayout {
|
||||||
anchors.right: isOwn ? parent.right : undefined
|
anchors.right: isOwn ? parent.right : undefined
|
||||||
|
|
||||||
readonly property string contentText:
|
readonly property string contentText:
|
||||||
isMessage ? "" : ChatJS.get_event_text(type, dict)
|
isMessage ? "" : ChatJS.getEventText(type, dict)
|
||||||
|
|
||||||
Base.Avatar {
|
Base.Avatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
|
@ -26,7 +26,7 @@ RowLayout {
|
||||||
(isUndecryptableEvent ? "darkred" : "gray") + "'>" +
|
(isUndecryptableEvent ? "darkred" : "gray") + "'>" +
|
||||||
(displayName.value || dict.sender) + " " + contentText +
|
(displayName.value || dict.sender) + " " + contentText +
|
||||||
" <font size=" + smallSize + "px color='gray'>" +
|
" <font size=" + smallSize + "px color='gray'>" +
|
||||||
Qt.formatDateTime(date_time, "hh:mm:ss") +
|
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||||
"</font></font>"
|
"</font></font>"
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
background: Rectangle {color: "#DDD"}
|
background: Rectangle {color: "#DDD"}
|
||||||
|
|
|
@ -32,19 +32,13 @@ Row {
|
||||||
|
|
||||||
Base.RichLabel {
|
Base.RichLabel {
|
||||||
id: contentLabel
|
id: contentLabel
|
||||||
//text: (isOwn ? "" : content + " ") +
|
|
||||||
//"<font size=" + smallSize + "px color=gray>" +
|
|
||||||
//Qt.formatDateTime(date_time, "hh:mm:ss") +
|
|
||||||
//"</font>" +
|
|
||||||
// (isOwn ? " " + content : "")
|
|
||||||
//
|
|
||||||
text: (dict.formatted_body ?
|
text: (dict.formatted_body ?
|
||||||
Backend.htmlFilter.filter(dict.formatted_body) :
|
Backend.htmlFilter.filter(dict.formatted_body) :
|
||||||
dict.body) +
|
dict.body) +
|
||||||
" <font size=" + smallSize + "px color=gray>" +
|
" <font size=" + smallSize + "px color=gray>" +
|
||||||
Qt.formatDateTime(date_time, "hh:mm:ss") +
|
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||||
"</font>" +
|
"</font>" +
|
||||||
(is_local_echo ?
|
(isLocalEcho ?
|
||||||
" <font size=" + smallSize + "px>⏳</font>" : "")
|
" <font size=" + smallSize + "px>⏳</font>" : "")
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
background: Rectangle {color: "#DDD"}
|
background: Rectangle {color: "#DDD"}
|
||||||
|
|
|
@ -7,22 +7,22 @@ import "utils.js" as ChatJS
|
||||||
Column {
|
Column {
|
||||||
id: "messageDelegate"
|
id: "messageDelegate"
|
||||||
|
|
||||||
function mins_between(date1, date2) {
|
function minsBetween(date1, date2) {
|
||||||
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
|
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_message(type_) { return type_.startsWith("RoomMessage") }
|
function getIsMessage(type_) { return type_.startsWith("RoomMessage") }
|
||||||
|
|
||||||
function get_previous_item() {
|
function getPreviousItem() {
|
||||||
return index < messageListView.model.count - 1 ?
|
return index < messageListView.model.count - 1 ?
|
||||||
messageListView.model.get(index + 1) : null
|
messageListView.model.get(index + 1) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
property var previousItem: get_previous_item()
|
property var previousItem: getPreviousItem()
|
||||||
signal reloadPreviousItem()
|
signal reloadPreviousItem()
|
||||||
onReloadPreviousItem: previousItem = get_previous_item()
|
onReloadPreviousItem: previousItem = getPreviousItem()
|
||||||
|
|
||||||
readonly property bool isMessage: is_message(type)
|
readonly property bool isMessage: getIsMessage(type)
|
||||||
|
|
||||||
readonly property bool isUndecryptableEvent:
|
readonly property bool isUndecryptableEvent:
|
||||||
type === "OlmEvent" || type === "MegolmEvent"
|
type === "OlmEvent" || type === "MegolmEvent"
|
||||||
|
@ -31,7 +31,7 @@ Column {
|
||||||
Backend.getUserDisplayName(dict.sender)
|
Backend.getUserDisplayName(dict.sender)
|
||||||
|
|
||||||
readonly property bool isOwn:
|
readonly property bool isOwn:
|
||||||
chatPage.user_id === dict.sender
|
chatPage.userId === dict.sender
|
||||||
|
|
||||||
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
||||||
|
|
||||||
|
@ -39,19 +39,19 @@ Column {
|
||||||
previousItem &&
|
previousItem &&
|
||||||
! talkBreak &&
|
! talkBreak &&
|
||||||
! dayBreak &&
|
! dayBreak &&
|
||||||
is_message(previousItem.type) === isMessage &&
|
getIsMessage(previousItem.type) === isMessage &&
|
||||||
previousItem.dict.sender === dict.sender &&
|
previousItem.dict.sender === dict.sender &&
|
||||||
mins_between(previousItem.date_time, date_time) <= 5
|
minsBetween(previousItem.dateTime, dateTime) <= 5
|
||||||
|
|
||||||
readonly property bool dayBreak:
|
readonly property bool dayBreak:
|
||||||
isFirstEvent ||
|
isFirstEvent ||
|
||||||
previousItem &&
|
previousItem &&
|
||||||
date_time.getDay() != previousItem.date_time.getDay()
|
dateTime.getDay() != previousItem.dateTime.getDay()
|
||||||
|
|
||||||
readonly property bool talkBreak:
|
readonly property bool talkBreak:
|
||||||
previousItem &&
|
previousItem &&
|
||||||
! dayBreak &&
|
! dayBreak &&
|
||||||
mins_between(previousItem.date_time, date_time) >= 20
|
minsBetween(previousItem.dateTime, dateTime) >= 20
|
||||||
|
|
||||||
|
|
||||||
property int standardSpacing: 16
|
property int standardSpacing: 16
|
||||||
|
@ -59,8 +59,8 @@ Column {
|
||||||
property int verticalPadding: 5
|
property int verticalPadding: 5
|
||||||
|
|
||||||
ListView.onAdd: {
|
ListView.onAdd: {
|
||||||
var next_delegate = messageListView.contentItem.children[index]
|
var nextDelegate = messageListView.contentItem.children[index]
|
||||||
if (next_delegate) { next_delegate.reloadPreviousItem() }
|
if (nextDelegate) { nextDelegate.reloadPreviousItem() }
|
||||||
}
|
}
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
|
@ -14,8 +14,7 @@ Rectangle {
|
||||||
id: messageListView
|
id: messageListView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
delegate: MessageDelegate {}
|
delegate: MessageDelegate {}
|
||||||
model: Backend.models.roomEvents.get(chatPage.room.room_id)
|
model: Backend.models.roomEvents.get(chatPage.roomId)
|
||||||
//highlight: Rectangle {color: "lightsteelblue"; radius: 5}
|
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
topMargin: space
|
topMargin: space
|
||||||
|
@ -31,7 +30,7 @@ Rectangle {
|
||||||
|
|
||||||
onYPosChanged: {
|
onYPosChanged: {
|
||||||
if (yPos <= 0.1) {
|
if (yPos <= 0.1) {
|
||||||
Backend.loadPastEvents(chatPage.room.room_id)
|
Backend.loadPastEvents(chatPage.roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@ import QtQuick.Layouts 1.4
|
||||||
import "../base" as Base
|
import "../base" as Base
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
property string displayName: ""
|
||||||
|
property string topic: ""
|
||||||
|
|
||||||
|
id: "root"
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 36
|
Layout.minimumHeight: 36
|
||||||
Layout.maximumHeight: Layout.minimumHeight
|
Layout.maximumHeight: Layout.minimumHeight
|
||||||
|
@ -19,21 +22,23 @@ Rectangle {
|
||||||
id: "avatar"
|
id: "avatar"
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
dimmension: root.Layout.minimumHeight
|
dimmension: root.Layout.minimumHeight
|
||||||
name: chatPage.room.display_name
|
name: displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
id: "roomName"
|
id: "roomName"
|
||||||
text: chatPage.room.display_name
|
text: displayName
|
||||||
font.pixelSize: bigSize
|
font.pixelSize: bigSize
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: row.width - row.spacing * (row.children.length - 1) - avatar.width
|
Layout.maximumWidth:
|
||||||
|
row.width - row.spacing * (row.children.length - 1) -
|
||||||
|
avatar.width
|
||||||
}
|
}
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
id: "roomDescription"
|
id: "roomTopic"
|
||||||
text: chatPage.room.description || ""
|
text: topic
|
||||||
font.pixelSize: smallSize
|
font.pixelSize: smallSize
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
|
|
@ -3,16 +3,23 @@ import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.4
|
import QtQuick.Layouts 1.4
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
property var user_id: null
|
property var userId: null
|
||||||
property var room: null
|
property var roomId: null
|
||||||
|
|
||||||
|
property var roomInfo:
|
||||||
|
Backend.models.rooms.get(userId).getWhere("roomId", roomId)
|
||||||
|
|
||||||
id: chatPage
|
id: "chatPage"
|
||||||
spacing: 0
|
spacing: 0
|
||||||
onFocusChanged: sendBox.setFocus()
|
onFocusChanged: sendBox.setFocus()
|
||||||
|
|
||||||
RoomHeader {}
|
RoomHeader {
|
||||||
|
id: "roomHeader"
|
||||||
|
displayName: roomInfo.displayName
|
||||||
|
topic: roomInfo.topic
|
||||||
|
}
|
||||||
|
|
||||||
MessageList {}
|
MessageList {}
|
||||||
TypingUsersBar {}
|
TypingUsersBar {}
|
||||||
SendBox { id: sendBox }
|
SendBox { id: "sendBox" }
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ Rectangle {
|
||||||
|
|
||||||
Base.Avatar {
|
Base.Avatar {
|
||||||
id: "avatar"
|
id: "avatar"
|
||||||
name: Backend.getUserDisplayName(chatPage.user_id)
|
name: Backend.getUserDisplayName(chatPage.userId)
|
||||||
dimmension: root.Layout.minimumHeight
|
dimmension: root.Layout.minimumHeight
|
||||||
//visible: textArea.text === ""
|
//visible: textArea.text === ""
|
||||||
visible: textArea.height <= root.Layout.minimumHeight
|
visible: textArea.height <= root.Layout.minimumHeight
|
||||||
|
@ -43,13 +43,13 @@ Rectangle {
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
function set_typing(typing) {
|
function setTyping(typing) {
|
||||||
Backend.clientManager.clients[chatPage.user_id]
|
Backend.clientManager.clients[chatPage.userId]
|
||||||
.setTypingState(chatPage.room.room_id, typing)
|
.setTypingState(chatPage.roomId, typing)
|
||||||
}
|
}
|
||||||
|
|
||||||
onTypedTextChanged: set_typing(Boolean(text))
|
onTypedTextChanged: setTyping(Boolean(text))
|
||||||
onEditingFinished: set_typing(false) // when lost focus
|
onEditingFinished: setTyping(false) // when lost focus
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
|
@ -62,8 +62,8 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textArea.text === "") { return }
|
if (textArea.text === "") { return }
|
||||||
Backend.clientManager.clients[chatPage.user_id]
|
Backend.clientManager.clients[chatPage.userId]
|
||||||
.sendMarkdown(chatPage.room.room_id, textArea.text)
|
.sendMarkdown(chatPage.roomId, textArea.text)
|
||||||
textArea.clear()
|
textArea.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,19 +11,12 @@ Rectangle {
|
||||||
Layout.maximumHeight: Layout.minimumHeight
|
Layout.maximumHeight: Layout.minimumHeight
|
||||||
color: "#BBB"
|
color: "#BBB"
|
||||||
|
|
||||||
|
property var typingUsers: chatPage.roomInfo.typingUsers
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
id: "usersLabel"
|
id: "usersLabel"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
text: ChatJS.getTypingUsersText(typingUsers, chatPage.userId)
|
||||||
Timer {
|
|
||||||
interval: 500
|
|
||||||
repeat: true
|
|
||||||
running: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: usersLabel.text = ChatJS.get_typing_users_text(
|
|
||||||
chatPage.user_id, chatPage.room.room_id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
elide: Text.ElideMiddle
|
elide: Text.ElideMiddle
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
function get_event_text(type, dict) {
|
function getEventText(type, dict) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "RoomCreateEvent":
|
case "RoomCreateEvent":
|
||||||
return (dict.federate ? "allowed" : "blocked") +
|
return (dict.federate ? "allowed" : "blocked") +
|
||||||
|
@ -18,14 +18,14 @@ function get_event_text(type, dict) {
|
||||||
break
|
break
|
||||||
|
|
||||||
case "RoomHistoryVisibilityEvent":
|
case "RoomHistoryVisibilityEvent":
|
||||||
return get_history_visibility_event_text(dict)
|
return getHistoryVisibilityEventText(dict)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "PowerLevelsEvent":
|
case "PowerLevelsEvent":
|
||||||
return "changed the room's permissions."
|
return "changed the room's permissions."
|
||||||
|
|
||||||
case "RoomMemberEvent":
|
case "RoomMemberEvent":
|
||||||
return get_member_event_text(dict)
|
return getMemberEventText(dict)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "RoomAliasEvent":
|
case "RoomAliasEvent":
|
||||||
|
@ -58,7 +58,7 @@ function get_event_text(type, dict) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_history_visibility_event_text(dict) {
|
function getHistoryVisibilityEventText(dict) {
|
||||||
switch (dict.history_visibility) {
|
switch (dict.history_visibility) {
|
||||||
case "shared":
|
case "shared":
|
||||||
var end = "all room members."
|
var end = "all room members."
|
||||||
|
@ -81,7 +81,7 @@ function get_history_visibility_event_text(dict) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_member_event_text(dict) {
|
function getMemberEventText(dict) {
|
||||||
var info = dict.content, prev = dict.prev_content
|
var info = dict.content, prev = dict.prev_content
|
||||||
|
|
||||||
if (! prev || (info.membership != prev.membership)) {
|
if (! prev || (info.membership != prev.membership)) {
|
||||||
|
@ -127,15 +127,12 @@ function get_member_event_text(dict) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_typing_users_text(account_id, room_id) {
|
function getTypingUsersText(users, ourAccountId) {
|
||||||
var names = []
|
var names = []
|
||||||
var room = Backend.models.rooms.get(account_id)
|
|
||||||
.getWhere("room_id", room_id)
|
|
||||||
|
|
||||||
for (var i = 0; i < room.typing_users.length; i++) {
|
for (var i = 0; i < users.length; i++) {
|
||||||
if (room.typing_users[i] !== account_id) {
|
if (users[i] !== ourAccountId) {
|
||||||
names.push(Backend.getUserDisplayName(room.typing_users[i], false)
|
names.push(Backend.getUserDisplayName(users[i], false).result())
|
||||||
.result())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import QtQuick 2.7
|
|
||||||
import "../base" as Base
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Base.HLabel {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Home page"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ ColumnLayout {
|
||||||
id: "row"
|
id: "row"
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Base.Avatar { id: "avatar"; name: display_name; dimmension: 36 }
|
Base.Avatar { id: "avatar"; name: displayName; dimmension: 36 }
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
@ -21,7 +21,7 @@ ColumnLayout {
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
id: "accountLabel"
|
id: "accountLabel"
|
||||||
text: display_name.value || user_id
|
text: displayName.value || userId
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
@ -31,7 +31,7 @@ ColumnLayout {
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: "statusEdit"
|
id: "statusEdit"
|
||||||
text: status_message || ""
|
text: statusMessage || ""
|
||||||
placeholderText: qsTr("Set status message")
|
placeholderText: qsTr("Set status message")
|
||||||
background: null
|
background: null
|
||||||
color: "black"
|
color: "black"
|
||||||
|
@ -44,7 +44,7 @@ ColumnLayout {
|
||||||
rightPadding: leftPadding
|
rightPadding: leftPadding
|
||||||
|
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
Backend.setStatusMessage(user_id, text)
|
Backend.setStatusMessage(userId, text)
|
||||||
pageStack.forceActiveFocus()
|
pageStack.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ ColumnLayout {
|
||||||
id: "roomList"
|
id: "roomList"
|
||||||
visible: true
|
visible: true
|
||||||
interactive: false // no scrolling
|
interactive: false // no scrolling
|
||||||
for_user_id: user_id
|
forUserId: userId
|
||||||
|
|
||||||
Layout.minimumHeight:
|
Layout.minimumHeight:
|
||||||
roomList.visible ?
|
roomList.visible ?
|
|
@ -9,24 +9,21 @@ MouseArea {
|
||||||
width: roomList.width
|
width: roomList.width
|
||||||
height: roomList.childrenHeight
|
height: roomList.childrenHeight
|
||||||
|
|
||||||
onClicked: pageStack.show_room(
|
onClicked: pageStack.showRoom(roomList.forUserId, roomId)
|
||||||
roomList.for_user_id,
|
|
||||||
roomList.model.get(index)
|
|
||||||
)
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
id: row
|
id: row
|
||||||
spacing: 1
|
spacing: 1
|
||||||
|
|
||||||
Base.Avatar { id: avatar; name: display_name; dimmension: root.height }
|
Base.Avatar { id: avatar; name: displayName; dimmension: root.height }
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
id: roomLabel
|
id: roomLabel
|
||||||
text: display_name ? display_name : "<i>Empty room</i>"
|
text: displayName ? displayName : "<i>Empty room</i>"
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
@ -39,18 +36,18 @@ MouseArea {
|
||||||
rightPadding: leftPadding
|
rightPadding: leftPadding
|
||||||
}
|
}
|
||||||
Base.HLabel {
|
Base.HLabel {
|
||||||
function get_text() {
|
function getText() {
|
||||||
return SidePaneJS.get_last_room_event_text(room_id)
|
return SidePaneJS.getLastRoomEventText(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend.models.roomEvents.get(room_id)
|
target: Backend.models.roomEvents.get(roomId)
|
||||||
onChanged: subtitleLabel.text = subtitleLabel.get_text()
|
onChanged: subtitleLabel.text = subtitleLabel.getText()
|
||||||
}
|
}
|
||||||
|
|
||||||
id: subtitleLabel
|
id: subtitleLabel
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
text: get_text()
|
text: getText()
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
|
|
||||||
font.pixelSize: smallSize
|
font.pixelSize: smallSize
|
|
@ -4,7 +4,7 @@ import QtQuick.Layouts 1.4
|
||||||
import "../base" as Base
|
import "../base" as Base
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
property var for_user_id: null
|
property var forUserId: null
|
||||||
|
|
||||||
property int childrenHeight: 36
|
property int childrenHeight: 36
|
||||||
property int contentHeight: 0
|
property int contentHeight: 0
|
||||||
|
@ -16,6 +16,6 @@ ListView {
|
||||||
|
|
||||||
id: "roomList"
|
id: "roomList"
|
||||||
spacing: 8
|
spacing: 8
|
||||||
model: Backend.models.rooms.get(for_user_id)
|
model: Backend.models.rooms.get(forUserId)
|
||||||
delegate: RoomDelegate {}
|
delegate: RoomDelegate {}
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
.import "../chat/utils.js" as ChatJS
|
.import "../chat/utils.js" as ChatJS
|
||||||
|
|
||||||
|
|
||||||
function get_last_room_event_text(room_id) {
|
function getLastRoomEventText(roomId) {
|
||||||
var eventsModel = Backend.models.roomEvents.get(room_id)
|
var eventsModel = Backend.models.roomEvents.get(roomId)
|
||||||
|
|
||||||
for (var i = 0; i < eventsModel.count; i++) {
|
for (var i = 0; i < eventsModel.count; i++) {
|
||||||
var ev = eventsModel.get(i)
|
var ev = eventsModel.get(i)
|
||||||
|
@ -19,7 +19,7 @@ function get_last_room_event_text(room_id) {
|
||||||
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
|
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
|
||||||
|
|
||||||
if (undecryptable || ev.type.startsWith("RoomMessage")) {
|
if (undecryptable || ev.type.startsWith("RoomMessage")) {
|
||||||
var color = ev.dict.sender === roomList.for_user_id ?
|
var color = ev.dict.sender === roomList.forUserId ?
|
||||||
"darkblue" : "purple"
|
"darkblue" : "purple"
|
||||||
|
|
||||||
return "<font color='" +
|
return "<font color='" +
|
||||||
|
@ -36,7 +36,7 @@ function get_last_room_event_text(room_id) {
|
||||||
"'>" +
|
"'>" +
|
||||||
name +
|
name +
|
||||||
" " +
|
" " +
|
||||||
ChatJS.get_event_text(ev.type, ev.dict) +
|
ChatJS.getEventText(ev.type, ev.dict) +
|
||||||
"</font>"
|
"</font>"
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user