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:
miruka 2019-04-20 17:36:21 -04:00
parent b33f5f1d34
commit 8f35e60801
34 changed files with 304 additions and 250 deletions

View File

@ -6,12 +6,14 @@ PKG_DIR = harmonyqml
PYTHON = python3
PIP = pip3
PYLINT = pylint
MYPY = mypy
VULTURE = vulture
CLOC = cloc
ARCHIVE_FORMATS = gztar
INSTALL_FLAGS = --user --editable
PYLINT_FLAGS = --output-format colorized
MYPY_FLAGS = --ignore-missing-imports
VULTURE_FLAGS = --min-confidence 100
CLOC_FLAGS = --ignore-whitespace
@ -47,6 +49,8 @@ upload: dist
test:
- ${PYLINT} ${PYLINT_FLAGS} ${PKG_DIR} *.py
@echo
- ${MYPY} ${PKG_DIR} ${MYPY_FLAGS}
@echo
- ${VULTURE} ${PKG_DIR} ${VULTURE_FLAGS}
@echo
${CLOC} ${CLOC_FLAGS} ${PKG_DIR}

View File

@ -1,6 +1,5 @@
- Separate categories for invited, group and direct rooms
- Invited → Accept/Deny dialog
- Keep the room header name and topic updated
- Merge login page
- 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
- 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
@ -40,3 +39,5 @@
- Verify E2E working
- Multiaccount aliases
- Fix tooltip hide()

View File

@ -3,7 +3,7 @@
import hashlib
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set
from typing import Dict, Sequence, Set
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
@ -96,3 +96,15 @@ class Backend(QObject):
break
else:
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()

View File

@ -67,11 +67,11 @@ class Client(QObject):
def __repr__(self) -> str:
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)
def userID(self) -> str:
def userId(self) -> str:
return self.nio.user_id

View File

@ -63,7 +63,7 @@ class ClientManager(QObject):
def _on_connected(self, client: Client) -> None:
self.clients[client.userID] = client
self.clients[client.userId] = client
self.clientAdded.emit(client)
client.startSyncing()
@ -130,7 +130,7 @@ class ClientManager(QObject):
def configAdd(self, client: Client) -> None:
self._write_config({
**self.configAccounts(),
**{client.userID: {
**{client.userId: {
"hostname": client.nio.host,
"token": client.nio.access_token,
"device_id": client.nio.device_id,

View File

@ -1,7 +1,7 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from . import items
from .list_model import ListModel
from .list_model_map import ListModelMap
from .qml_models import QMLModels
from . import enums, items

View File

@ -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"

View File

@ -1,29 +1,83 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from typing import Any, Callable, Optional, Tuple, Union
from typing import Dict, List, NamedTuple, Optional
from PyQt5.QtCore import QDateTime
from ..pyqt_future import PyQtFuture
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
class User(NamedTuple):
user_id: str
display_name: PyQtFuture
avatar_url: Optional[str] = None
status_message: Optional[str] = None
class ListItem(QObject):
roles: Tuple[str, ...] = ()
def __init__(self, *args, **kwargs):
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):
room_id: str
display_name: Optional[str]
description: str = ""
typing_users: List[str] = []
def __repr__(self) -> str:
return "%s(%s)" % (
type(self).__name__,
", ".join((f"{r}={getattr(self, r)!r}" for r in self.roles)),
)
class RoomEvent(NamedTuple):
type: str
date_time: QDateTime
dict: Dict[str, str]
is_local_echo: bool = False
@pyqtProperty(str, constant=True)
def repr(self) -> str:
return repr(self)
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)

View File

@ -4,27 +4,25 @@ from typing import (
Sequence, Tuple, Union
)
from namedlist import namedlist
from PyQt5.QtCore import (
QAbstractListModel, QModelIndex, QObject, Qt, pyqtProperty, pyqtSignal,
pyqtSlot
)
NewValue = Union[Mapping[str, Any], Sequence]
ReturnItem = Dict[str, Any]
from .items import ListItem
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
class ListModel(QAbstractListModel):
changed = pyqtSignal()
def __init__(self,
initial_data: Optional[List[NewValue]] = None,
initial_data: Optional[List[NewItem]] = None,
container: Callable[..., MutableSequence] = list,
parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._ref_namedlist = None
self._roles: Tuple[str, ...] = ()
self._data: MutableSequence = container()
self._data: MutableSequence[ListItem] = container()
if initial_data:
self.extend(initial_data)
@ -50,57 +48,64 @@ class ListModel(QAbstractListModel):
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]:
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:
if role <= Qt.UserRole:
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:
return len(self._data)
def _convert_new_value(self, value: NewValue) -> Any:
if isinstance(value, Mapping):
if not self._ref_namedlist:
self._ref_namedlist = namedlist("ListItem", value.keys())
self._roles = tuple(value.keys())
def _convert_new_value(self, value: NewItem) -> ListItem:
def convert() -> ListItem:
if self._data and isinstance(value, Mapping):
assert set(value.keys()) <= set(self.roles), \
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._ref_namedlist:
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."
)
if not self._data and isinstance(value, Mapping):
raise NotImplementedError("First item must be set from Python")
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)
def count(self) -> int: # pylint: disable=arguments-differ
def count(self) -> int:
return self.rowCount()
@pyqtSlot(int, result="QVariantMap")
def get(self, index: int) -> ReturnItem:
return self._data[index]._asdict()
@pyqtSlot(int, result="QVariant")
def get(self, index: int) -> ListItem:
return self._data[index]
@pyqtSlot(str, "QVariant", result=int)
@ -109,17 +114,17 @@ class ListModel(QAbstractListModel):
if getattr(item, prop) == is_value:
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}.")
@pyqtSlot(str, "QVariant", result="QVariantMap")
def getWhere(self, prop: str, is_value: Any) -> ReturnItem:
@pyqtSlot(str, "QVariant", result="QVariant")
def getWhere(self, prop: str, is_value: Any) -> ListItem:
return self.get(self.indexWhere(prop, is_value))
@pyqtSlot(int, list)
def insert(self, index: int, value: NewValue) -> None:
@pyqtSlot(int, "QVariantMap")
def insert(self, index: int, value: NewItem) -> None:
value = self._convert_new_value(value)
self.beginInsertRows(QModelIndex(), index, index)
self._data.insert(index, value)
@ -127,19 +132,44 @@ class ListModel(QAbstractListModel):
self.changed.emit()
@pyqtSlot(list)
def append(self, value: NewValue) -> None:
@pyqtSlot("QVariantMap")
def append(self, value: NewItem) -> None:
self.insert(self.rowCount(), value)
@pyqtSlot(list)
def extend(self, values: Iterable[NewValue]) -> None:
def extend(self, values: Iterable[NewItem]) -> None:
for val in values:
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)
def set(self, index: int, value: NewValue) -> None:
def set(self, index: int, value: NewItem) -> None:
qidx = QAbstractListModel.index(self, index, 0)
value = self._convert_new_value(value)
self._data[index] = value
@ -149,16 +179,16 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int, str, "QVariant")
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)
self.dataChanged.emit(qidx, qidx, self.roleNames())
self.changed.emit()
# pylint: disable=invalid-name
@pyqtSlot(int, int)
@pyqtSlot(int, int, int)
def move(self, from_: int, to: int, n: int = 1) -> None:
# pylint: disable=invalid-name
qlast = from_ + n - 1
if (n <= 0) or (from_ == to) or (qlast == to) or \
@ -186,7 +216,7 @@ class ListModel(QAbstractListModel):
@pyqtSlot(int)
def remove(self, index: int) -> None: # pylint: disable=arguments-differ
def remove(self, index: int) -> None:
self.beginRemoveRows(QModelIndex(), index, index)
del self._data[index]
self.endRemoveRows()

View File

@ -132,8 +132,6 @@ class NetworkManager:
response = self.read(sock)
except (OSError, RemoteTransportError) as err:
logging.error("Connection error for %s: %s",
nio_func.__name__, str(err))
self._close_socket(sock)
self.http_disconnect()
retry.sleep(max_time=2)

View File

@ -31,14 +31,14 @@ class SignalManager(QObject):
def onClientAdded(self, client: Client) -> None:
self.connectClient(client)
self.backend.models.accounts.append(User(
user_id = client.userID,
display_name = self.backend.getUserDisplayName(client.userID),
userId = client.userId,
displayName = self.backend.getUserDisplayName(client.userId),
))
def onClientDeleted(self, user_id: str) -> None:
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:
@ -58,7 +58,7 @@ class SignalManager(QObject):
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]
def group_name() -> Optional[str]:
@ -66,22 +66,17 @@ class SignalManager(QObject):
return None if name == "Empty room?" else name
item = Room(
room_id = room_id,
display_name = room.name or room.canonical_alias or group_name(),
description = room.topic,
roomId = room_id,
displayName = room.name or room.canonical_alias or group_name(),
topic = room.topic,
)
try:
index = model.indexWhere("room_id", room_id)
except ValueError:
model.append(item)
else:
model[index] = item
model.updateOrAppendWhere("roomId", room_id, item)
def onRoomLeft(self, client: Client, room_id: str) -> None:
rooms = self.backend.models.rooms[client.userID]
del rooms[rooms.indexWhere("room_id", room_id)]
rooms = self.backend.models.rooms[client.userId]
del rooms[rooms.indexWhere("roomId", room_id)]
def onRoomSyncPrevBatchTokenReceived(
@ -116,15 +111,15 @@ class SignalManager(QObject):
model = self.backend.models.roomEvents[room_id]
date_time = QDateTime\
.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:
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
for i, event in enumerate(model):
if not event.is_local_echo:
if not event.isLocalEcho:
continue
sb = (event.dict["sender"], event.dict["body"])
@ -132,23 +127,23 @@ class SignalManager(QObject):
if sb == new_sb:
# 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
if local_echoes_met >= self._events_in_transfer:
break
if replace_at is not None:
model[replace_at] = new_event
if update_at is not None:
model.update(update_at, new_event)
self._events_in_transfer -= 1
return
for i, event in enumerate(model):
if event.is_local_echo:
if event.isLocalEcho:
continue
# 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)
return
@ -159,8 +154,8 @@ class SignalManager(QObject):
self, client: Client, room_id: str, users: List[str]
) -> None:
rooms = self.backend.models.rooms[client.userID]
rooms[rooms.indexWhere("room_id", room_id)].typing_users = users
rooms = self.backend.models.rooms[client.userId]
rooms[rooms.indexWhere("roomId", room_id)].typingUsers = users
def onMessageAboutToBeSent(
@ -171,15 +166,15 @@ class SignalManager(QObject):
model = self.backend.models.roomEvents[room_id]
nio_event = nio.events.RoomMessage.parse_event({
"event_id": "",
"sender": client.userID,
"sender": client.userId,
"origin_server_ts": timestamp,
"content": content,
})
event = RoomEvent(
type = type(nio_event).__name__,
date_time = QDateTime.fromMSecsSinceEpoch(timestamp),
dateTime = QDateTime.fromMSecsSinceEpoch(timestamp),
dict = nio_event.__dict__,
is_local_echo = True,
isLocalEcho = True,
)
model.insert(0, event)
self._events_in_transfer += 1

View File

@ -1,7 +1,8 @@
import QtQuick 2.7
import QtQuick.Controls 1.4 as Controls1
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.4
import "side_pane" as SidePane
import "sidePane" as SidePane
import "chat" as Chat
//https://doc.qt.io/qt-5/qml-qtquick-controls-splitview.html
@ -14,12 +15,9 @@ Controls1.SplitView {
}
StackView {
function show_page(componentName) {
pageStack.replace(componentName + ".qml")
}
function show_room(user_id, room_obj) {
function showRoom(userId, roomId) {
pageStack.replace(
"chat/Root.qml", { user_id: user_id, room: room_obj }
"chat/Root.qml", { userId: userId, roomId: roomId }
)
console.log("replaced")
}
@ -28,6 +26,12 @@ Controls1.SplitView {
onCurrentItemChanged: currentItem.forceActiveFocus()
initialItem: MouseArea { // TODO: (test, remove)
onClicked: pageStack.showRoom(
"@test_mary:matrix.org", "!VDSsFIzQnXARSCVNxS:matrix.org"
)
}
// Buggy
replaceExit: null
popExit: null

View File

@ -8,7 +8,7 @@ Item {
property var imageSource: null
property int dimmension: 48
readonly property string resolved_name:
readonly property string resolvedName:
! name ? "?" :
typeof(name) == "string" ? name :
(name.value ? name.value : "?")
@ -21,13 +21,13 @@ Item {
id: "letterRectangle"
anchors.fill: parent
visible: ! invisible && imageSource === null
color: resolved_name === "?" ?
color: resolvedName === "?" ?
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 {
anchors.centerIn: parent
text: resolved_name.charAt(0)
text: resolvedName.charAt(0)
color: "white"
font.pixelSize: letterRectangle.height / 1.4
}

View File

@ -14,7 +14,7 @@ ToolButton {
onClicked: toolTip.hide()
ToolTip {
id: "toolTip"
id: toolTip
text: tooltip
delay: Qt.styleHints.mousePressAndHoldInterval
visible: text ? toolTipZone.containsMouse : false

View File

@ -2,11 +2,14 @@ import QtQuick 2.7
import QtQuick.Controls 2.0
HLabel {
property string toolTipText: ""
id: text
ToolTip {
delay: Qt.styleHints.mousePressAndHoldInterval
visible: text ? toolTipZone.containsMouse : false
text: user_id
text: toolTipText
}
MouseArea {
id: toolTipZone

View File

@ -2,12 +2,13 @@ import QtQuick 2.7
import "../base" as Base
Base.HLabel {
text: date_time.toLocaleDateString()
width: messageDelegate.width
horizontalAlignment: Text.AlignHCenter
topPadding: messageDelegate.isFirstMessage ?
0 : messageDelegate.standardSpacing
bottomPadding: messageDelegate.standardSpacing
text: dateTime.toLocaleDateString()
horizontalAlignment: Text.AlignHCenter
font.pixelSize: normalSize * 1.1
color: "darkolivegreen"
}

View File

@ -11,7 +11,7 @@ RowLayout {
anchors.right: isOwn ? parent.right : undefined
readonly property string contentText:
isMessage ? "" : ChatJS.get_event_text(type, dict)
isMessage ? "" : ChatJS.getEventText(type, dict)
Base.Avatar {
id: avatar
@ -26,7 +26,7 @@ RowLayout {
(isUndecryptableEvent ? "darkred" : "gray") + "'>" +
(displayName.value || dict.sender) + " " + contentText +
"&nbsp;&nbsp;<font size=" + smallSize + "px color='gray'>" +
Qt.formatDateTime(date_time, "hh:mm:ss") +
Qt.formatDateTime(dateTime, "hh:mm:ss") +
"</font></font>"
textFormat: Text.RichText
background: Rectangle {color: "#DDD"}

View File

@ -32,19 +32,13 @@ Row {
Base.RichLabel {
id: contentLabel
//text: (isOwn ? "" : content + "&nbsp;&nbsp;") +
//"<font size=" + smallSize + "px color=gray>" +
//Qt.formatDateTime(date_time, "hh:mm:ss") +
//"</font>" +
// (isOwn ? "&nbsp;&nbsp;" + content : "")
//
text: (dict.formatted_body ?
Backend.htmlFilter.filter(dict.formatted_body) :
dict.body) +
"&nbsp;&nbsp;<font size=" + smallSize + "px color=gray>" +
Qt.formatDateTime(date_time, "hh:mm:ss") +
Qt.formatDateTime(dateTime, "hh:mm:ss") +
"</font>" +
(is_local_echo ?
(isLocalEcho ?
"&nbsp;<font size=" + smallSize + "px>⏳</font>" : "")
textFormat: Text.RichText
background: Rectangle {color: "#DDD"}

View File

@ -7,22 +7,22 @@ import "utils.js" as ChatJS
Column {
id: "messageDelegate"
function mins_between(date1, date2) {
function minsBetween(date1, date2) {
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 ?
messageListView.model.get(index + 1) : null
}
property var previousItem: get_previous_item()
property var previousItem: getPreviousItem()
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:
type === "OlmEvent" || type === "MegolmEvent"
@ -31,7 +31,7 @@ Column {
Backend.getUserDisplayName(dict.sender)
readonly property bool isOwn:
chatPage.user_id === dict.sender
chatPage.userId === dict.sender
readonly property bool isFirstEvent: type == "RoomCreateEvent"
@ -39,19 +39,19 @@ Column {
previousItem &&
! talkBreak &&
! dayBreak &&
is_message(previousItem.type) === isMessage &&
getIsMessage(previousItem.type) === isMessage &&
previousItem.dict.sender === dict.sender &&
mins_between(previousItem.date_time, date_time) <= 5
minsBetween(previousItem.dateTime, dateTime) <= 5
readonly property bool dayBreak:
isFirstEvent ||
previousItem &&
date_time.getDay() != previousItem.date_time.getDay()
dateTime.getDay() != previousItem.dateTime.getDay()
readonly property bool talkBreak:
previousItem &&
! dayBreak &&
mins_between(previousItem.date_time, date_time) >= 20
minsBetween(previousItem.dateTime, dateTime) >= 20
property int standardSpacing: 16
@ -59,8 +59,8 @@ Column {
property int verticalPadding: 5
ListView.onAdd: {
var next_delegate = messageListView.contentItem.children[index]
if (next_delegate) { next_delegate.reloadPreviousItem() }
var nextDelegate = messageListView.contentItem.children[index]
if (nextDelegate) { nextDelegate.reloadPreviousItem() }
}
width: parent.width

View File

@ -14,8 +14,7 @@ Rectangle {
id: messageListView
anchors.fill: parent
delegate: MessageDelegate {}
model: Backend.models.roomEvents.get(chatPage.room.room_id)
//highlight: Rectangle {color: "lightsteelblue"; radius: 5}
model: Backend.models.roomEvents.get(chatPage.roomId)
clip: true
topMargin: space
@ -31,7 +30,7 @@ Rectangle {
onYPosChanged: {
if (yPos <= 0.1) {
Backend.loadPastEvents(chatPage.room.room_id)
Backend.loadPastEvents(chatPage.roomId)
}
}
}

View File

@ -4,7 +4,10 @@ import QtQuick.Layouts 1.4
import "../base" as Base
Rectangle {
id: root
property string displayName: ""
property string topic: ""
id: "root"
Layout.fillWidth: true
Layout.minimumHeight: 36
Layout.maximumHeight: Layout.minimumHeight
@ -19,21 +22,23 @@ Rectangle {
id: "avatar"
Layout.alignment: Qt.AlignTop
dimmension: root.Layout.minimumHeight
name: chatPage.room.display_name
name: displayName
}
Base.HLabel {
id: "roomName"
text: chatPage.room.display_name
text: displayName
font.pixelSize: bigSize
elide: Text.ElideRight
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 {
id: "roomDescription"
text: chatPage.room.description || ""
id: "roomTopic"
text: topic
font.pixelSize: smallSize
elide: Text.ElideRight
maximumLineCount: 1

View File

@ -3,16 +3,23 @@ import QtQuick.Controls 2.2
import QtQuick.Layouts 1.4
ColumnLayout {
property var user_id: null
property var room: null
property var userId: null
property var roomId: null
property var roomInfo:
Backend.models.rooms.get(userId).getWhere("roomId", roomId)
id: chatPage
id: "chatPage"
spacing: 0
onFocusChanged: sendBox.setFocus()
RoomHeader {}
RoomHeader {
id: "roomHeader"
displayName: roomInfo.displayName
topic: roomInfo.topic
}
MessageList {}
TypingUsersBar {}
SendBox { id: sendBox }
SendBox { id: "sendBox" }
}

View File

@ -20,7 +20,7 @@ Rectangle {
Base.Avatar {
id: "avatar"
name: Backend.getUserDisplayName(chatPage.user_id)
name: Backend.getUserDisplayName(chatPage.userId)
dimmension: root.Layout.minimumHeight
//visible: textArea.text === ""
visible: textArea.height <= root.Layout.minimumHeight
@ -43,13 +43,13 @@ Rectangle {
font.pixelSize: 16
focus: true
function set_typing(typing) {
Backend.clientManager.clients[chatPage.user_id]
.setTypingState(chatPage.room.room_id, typing)
function setTyping(typing) {
Backend.clientManager.clients[chatPage.userId]
.setTypingState(chatPage.roomId, typing)
}
onTypedTextChanged: set_typing(Boolean(text))
onEditingFinished: set_typing(false) // when lost focus
onTypedTextChanged: setTyping(Boolean(text))
onEditingFinished: setTyping(false) // when lost focus
Keys.onReturnPressed: {
event.accepted = true
@ -62,8 +62,8 @@ Rectangle {
}
if (textArea.text === "") { return }
Backend.clientManager.clients[chatPage.user_id]
.sendMarkdown(chatPage.room.room_id, textArea.text)
Backend.clientManager.clients[chatPage.userId]
.sendMarkdown(chatPage.roomId, textArea.text)
textArea.clear()
}

View File

@ -11,19 +11,12 @@ Rectangle {
Layout.maximumHeight: Layout.minimumHeight
color: "#BBB"
property var typingUsers: chatPage.roomInfo.typingUsers
Base.HLabel {
id: "usersLabel"
anchors.fill: parent
Timer {
interval: 500
repeat: true
running: true
triggeredOnStart: true
onTriggered: usersLabel.text = ChatJS.get_typing_users_text(
chatPage.user_id, chatPage.room.room_id
)
}
text: ChatJS.getTypingUsersText(typingUsers, chatPage.userId)
elide: Text.ElideMiddle
maximumLineCount: 1

View File

@ -1,4 +1,4 @@
function get_event_text(type, dict) {
function getEventText(type, dict) {
switch (type) {
case "RoomCreateEvent":
return (dict.federate ? "allowed" : "blocked") +
@ -18,14 +18,14 @@ function get_event_text(type, dict) {
break
case "RoomHistoryVisibilityEvent":
return get_history_visibility_event_text(dict)
return getHistoryVisibilityEventText(dict)
break
case "PowerLevelsEvent":
return "changed the room's permissions."
case "RoomMemberEvent":
return get_member_event_text(dict)
return getMemberEventText(dict)
break
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) {
case "shared":
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
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 room = Backend.models.rooms.get(account_id)
.getWhere("room_id", room_id)
for (var i = 0; i < room.typing_users.length; i++) {
if (room.typing_users[i] !== account_id) {
names.push(Backend.getUserDisplayName(room.typing_users[i], false)
.result())
for (var i = 0; i < users.length; i++) {
if (users[i] !== ourAccountId) {
names.push(Backend.getUserDisplayName(users[i], false).result())
}
}

View File

@ -1,9 +0,0 @@
import QtQuick 2.7
import "../base" as Base
Rectangle {
Base.HLabel {
anchors.centerIn: parent
text: "Home page"
}
}

View File

@ -12,7 +12,7 @@ ColumnLayout {
id: "row"
spacing: 0
Base.Avatar { id: "avatar"; name: display_name; dimmension: 36 }
Base.Avatar { id: "avatar"; name: displayName; dimmension: 36 }
ColumnLayout {
Layout.fillWidth: true
@ -21,7 +21,7 @@ ColumnLayout {
Base.HLabel {
id: "accountLabel"
text: display_name.value || user_id
text: displayName.value || userId
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
@ -31,7 +31,7 @@ ColumnLayout {
TextField {
id: "statusEdit"
text: status_message || ""
text: statusMessage || ""
placeholderText: qsTr("Set status message")
background: null
color: "black"
@ -44,7 +44,7 @@ ColumnLayout {
rightPadding: leftPadding
onEditingFinished: {
Backend.setStatusMessage(user_id, text)
Backend.setStatusMessage(userId, text)
pageStack.forceActiveFocus()
}
}
@ -67,7 +67,7 @@ ColumnLayout {
id: "roomList"
visible: true
interactive: false // no scrolling
for_user_id: user_id
forUserId: userId
Layout.minimumHeight:
roomList.visible ?

View File

@ -9,24 +9,21 @@ MouseArea {
width: roomList.width
height: roomList.childrenHeight
onClicked: pageStack.show_room(
roomList.for_user_id,
roomList.model.get(index)
)
onClicked: pageStack.showRoom(roomList.forUserId, roomId)
RowLayout {
anchors.fill: parent
id: row
spacing: 1
Base.Avatar { id: avatar; name: display_name; dimmension: root.height }
Base.Avatar { id: avatar; name: displayName; dimmension: root.height }
ColumnLayout {
spacing: 0
Base.HLabel {
id: roomLabel
text: display_name ? display_name : "<i>Empty room</i>"
text: displayName ? displayName : "<i>Empty room</i>"
textFormat: Text.StyledText
elide: Text.ElideRight
maximumLineCount: 1
@ -39,18 +36,18 @@ MouseArea {
rightPadding: leftPadding
}
Base.HLabel {
function get_text() {
return SidePaneJS.get_last_room_event_text(room_id)
function getText() {
return SidePaneJS.getLastRoomEventText(roomId)
}
Connections {
target: Backend.models.roomEvents.get(room_id)
onChanged: subtitleLabel.text = subtitleLabel.get_text()
target: Backend.models.roomEvents.get(roomId)
onChanged: subtitleLabel.text = subtitleLabel.getText()
}
id: subtitleLabel
visible: text !== ""
text: get_text()
text: getText()
textFormat: Text.StyledText
font.pixelSize: smallSize

View File

@ -4,7 +4,7 @@ import QtQuick.Layouts 1.4
import "../base" as Base
ListView {
property var for_user_id: null
property var forUserId: null
property int childrenHeight: 36
property int contentHeight: 0
@ -16,6 +16,6 @@ ListView {
id: "roomList"
spacing: 8
model: Backend.models.rooms.get(for_user_id)
model: Backend.models.rooms.get(forUserId)
delegate: RoomDelegate {}
}

View File

@ -1,8 +1,8 @@
.import "../chat/utils.js" as ChatJS
function get_last_room_event_text(room_id) {
var eventsModel = Backend.models.roomEvents.get(room_id)
function getLastRoomEventText(roomId) {
var eventsModel = Backend.models.roomEvents.get(roomId)
for (var i = 0; i < eventsModel.count; 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"
if (undecryptable || ev.type.startsWith("RoomMessage")) {
var color = ev.dict.sender === roomList.for_user_id ?
var color = ev.dict.sender === roomList.forUserId ?
"darkblue" : "purple"
return "<font color='" +
@ -36,7 +36,7 @@ function get_last_room_event_text(room_id) {
"'>" +
name +
" " +
ChatJS.get_event_text(ev.type, ev.dict) +
ChatJS.getEventText(ev.type, ev.dict) +
"</font>"
}
}