)",
+ ]]
+
+
+ def __init__(self) -> None:
+ self._sanitizer = sanitizer.Sanitizer(self.sanitizer_settings)
+
+ # The whitespace remover doesn't take 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
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 already has an
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()
diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py
new file mode 100644
index 00000000..63127afd
--- /dev/null
+++ b/src/python/matrix_client.py
@@ -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,
+ )
diff --git a/src/qml/Base/HAvatar.qml b/src/qml/Base/HAvatar.qml
index be97d8cd..7c3205fc 100644
--- a/src/qml/Base/HAvatar.qml
+++ b/src/qml/Base/HAvatar.qml
@@ -7,7 +7,14 @@ Rectangle {
property int dimension: HStyle.avatar.size
property bool hidden: false
- function hue_from_name(name) {
+ function stripUserId(user_id) {
+ return user_id.substring(1) // Remove leading @
+ }
+ function stripRoomName(name) {
+ return name[0] == "#" ? name.substring(1) : name
+ }
+
+ function hueFromName(name) {
var hue = 0
for (var i = 0; i < name.length; i++) {
hue += name.charCodeAt(i) * 99
@@ -24,7 +31,7 @@ Rectangle {
color: name ?
Qt.hsla(
- hue_from_name(name),
+ hueFromName(name),
HStyle.avatar.background.saturation,
HStyle.avatar.background.lightness,
HStyle.avatar.background.alpha
diff --git a/src/qml/Base/HButton.qml b/src/qml/Base/HButton.qml
index 755b7214..cbb00ad2 100644
--- a/src/qml/Base/HButton.qml
+++ b/src/qml/Base/HButton.qml
@@ -32,11 +32,6 @@ Button {
signal pressed
signal released
- function loadingUntilFutureDone(future) {
- loading = true
- future.onGotResult.connect(function() { loading = false })
- }
-
id: button
background: Rectangle {
diff --git a/src/qml/Base/HListModel.qml b/src/qml/Base/HListModel.qml
index 44536ea9..18384ece 100644
--- a/src/qml/Base/HListModel.qml
+++ b/src/qml/Base/HListModel.qml
@@ -1,85 +1,112 @@
import QtQuick 2.7
+import SortFilterProxyModel 0.2
-ListModel {
+SortFilterProxyModel {
// To initialize a HListModel with items,
// use `Component.onCompleted: extend([{"foo": 1, "bar": 2}, ...])`
- id: listModel
+ id: sortFilteredModel
+
+ property var model: ListModel {}
+ sourceModel: model // Can't assign a "ListModel {}" directly here
+
+ function append(dict) { return model.append(dict) }
+ function clear() { return model.clear() }
+ function insert(index, dict) { return model.inset(index, dict) }
+ function move(from, to, n) { return model.move(from, to, n) }
+ function remove(index, count) { return model.remove(index, count) }
+ function set(index, dict) { return model.set(index, dict) }
+ function sync() { return model.sync() }
+ function setProperty(index, prop, value) {
+ return model.setProperty(index, prop, value)
+ }
function extend(new_items) {
for (var i = 0; i < new_items.length; i++) {
- listModel.append(new_items[i])
+ model.append(new_items[i])
}
}
- function getIndices(where_role, is, max) { // max: undefined or int
+ function getIndices(where_roles_are, max_results, max_tries) {
+ // max arguments: unefined or int
var results = []
- for (var i = 0; i < listModel.count; i++) {
- if (listModel.get(i)[where_role] == is) {
- results.push(i)
+ for (var i = 0; i < model.count; i++) {
+ var item = model.get(i)
+ var include = true
- if (max && results.length >= max) {
+ for (var role in where_roles_are) {
+ if (item[role] != where_roles_are[role]) {
+ include = false
break
}
}
+
+ if (include) {
+ results.push(i)
+ if (max_results && results.length >= max_results) {
+ break
+ }
+ }
+
+ if (max_tries && i >= max_tries) {
+ break
+ }
}
return results
}
- function getWhere(where_role, is, max) {
- var indices = getIndices(where_role, is, max)
- var results = []
+ function getWhere(roles_are, max_results, max_tries) {
+ var indices = getIndices(roles_are, max_results, max_tries)
+ var items = []
for (var i = 0; i < indices.length; i++) {
- results.push(listModel.get(indices[i]))
+ items.push(model.get(indices[i]))
}
- return results
+ return items
}
- function forEachWhere(where_role, is, max, func) {
- var items = getWhere(where_role, is, max)
+ function forEachWhere(roles_are, func, max_results, max_tries) {
+ var items = getWhere(roles_are, max_results, max_tries)
for (var i = 0; i < items.length; i++) {
- func(item)
+ func(items[i])
}
}
- function upsert(where_role, is, new_item, update_if_exist) {
- // new_item can contain only the keys we're interested in updating
-
- var indices = getIndices(where_role, is, 1)
+ function upsert(where_roles_are, new_item, update_if_exist, max_tries) {
+ var indices = getIndices(where_roles_are, 1, max_tries)
if (indices.length == 0) {
- listModel.append(new_item)
- return listModel.get(listModel.count)
+ model.append(new_item)
+ return model.get(model.count)
}
if (update_if_exist != false) {
- listModel.set(indices[0], new_item)
+ model.set(indices[0], new_item)
}
- return listModel.get(indices[0])
+ return model.get(indices[0])
}
function pop(index) {
- var item = listModel.get(index)
- listModel.remove(index)
+ var item = model.get(index)
+ model.remove(index)
return item
}
- function popWhere(where_role, is, max) {
- var indices = getIndices(where_role, is, max)
- var results = []
+ function popWhere(roles_are, max_results, max_tries) {
+ var indices = getIndices(roles_are, max_results, max_tries)
+ var items = []
for (var i = 0; i < indices.length; i++) {
- results.push(listModel.get(indices[i]))
- listModel.remove(indices[i])
+ items.push(model.get(indices[i]))
+ model.remove(indices[i])
}
- return results
+ return items
}
function toObject(item_list) {
- item_list = item_list || listModel
+ item_list = item_list || sortFilteredModel
var obj_list = []
for (var i = 0; i < item_list.count; i++) {
diff --git a/src/qml/Chat/Banners/InviteBanner.qml b/src/qml/Chat/Banners/InviteBanner.qml
index ba522031..37811ae4 100644
--- a/src/qml/Chat/Banners/InviteBanner.qml
+++ b/src/qml/Chat/Banners/InviteBanner.qml
@@ -6,6 +6,7 @@ Banner {
color: HStyle.chat.inviteBanner.background
+ // TODO: get disp name from models.users, inviter = userid now
avatar.name: inviter ? inviter.displayname : ""
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
diff --git a/src/qml/Chat/Chat.qml b/src/qml/Chat/Chat.qml
index 6bc5cdb0..ccaec67d 100644
--- a/src/qml/Chat/Chat.qml
+++ b/src/qml/Chat/Chat.qml
@@ -10,27 +10,27 @@ HColumnLayout {
property string category: ""
property string roomId: ""
- readonly property var roomInfo:
- Backend.accounts.get(userId)
- .roomCategories.get(category)
- .rooms.get(roomId)
+ readonly property var roomInfo: models.rooms.getWhere(
+ {"userId": userId, "roomId": roomId, "category": category}, 1
+ )[0]
- readonly property var sender: Backend.users.get(userId)
+ readonly property var sender:
+ models.users.getWhere({"userId": userId}, 1)[0]
- readonly property bool hasUnknownDevices:
- category == "Rooms" ?
- Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
+ readonly property bool hasUnknownDevices: false
+ //category == "Rooms" ?
+ //Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
id: chatPage
onFocusChanged: sendBox.setFocus()
- Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
- function(forUserId, forRoomId, previous, now) {
- if (chatPage && forUserId == userId && forRoomId == roomId) {
- chatPage.category = now
- }
- }
- )
+ //Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
+ //function(forUserId, forRoomId, previous, now) {
+ //if (chatPage && forUserId == userId && forRoomId == roomId) {
+ //chatPage.category = now
+ //}
+ //}
+ //)
RoomHeader {
id: roomHeader
@@ -77,72 +77,72 @@ HColumnLayout {
}
}
- RoomSidePane {
- id: roomSidePane
+// RoomSidePane {
+ //id: roomSidePane
- activeView: roomHeader.activeButton
- property int oldWidth: width
- onActiveViewChanged:
- activeView ? restoreAnimation.start() : hideAnimation.start()
+ //activeView: roomHeader.activeButton
+ //property int oldWidth: width
+ //onActiveViewChanged:
+ //activeView ? restoreAnimation.start() : hideAnimation.start()
- NumberAnimation {
- id: hideAnimation
- target: roomSidePane
- properties: "width"
- duration: HStyle.animationDuration
- from: target.width
- to: 0
+ //NumberAnimation {
+ //id: hideAnimation
+ //target: roomSidePane
+ //properties: "width"
+ //duration: HStyle.animationDuration
+ //from: target.width
+ //to: 0
- onStarted: {
- target.oldWidth = target.width
- target.Layout.minimumWidth = 0
- }
- }
+ //onStarted: {
+ //target.oldWidth = target.width
+ //target.Layout.minimumWidth = 0
+ //}
+ //}
- NumberAnimation {
- id: restoreAnimation
- target: roomSidePane
- properties: "width"
- duration: HStyle.animationDuration
- from: 0
- to: target.oldWidth
+ //NumberAnimation {
+ //id: restoreAnimation
+ //target: roomSidePane
+ //properties: "width"
+ //duration: HStyle.animationDuration
+ //from: 0
+ //to: target.oldWidth
- onStopped: target.Layout.minimumWidth = Qt.binding(
- function() { return HStyle.avatar.size }
- )
- }
+ //onStopped: target.Layout.minimumWidth = Qt.binding(
+ //function() { return HStyle.avatar.size }
+ //)
+ //}
- collapsed: width < HStyle.avatar.size + 8
+ //collapsed: width < HStyle.avatar.size + 8
- property bool wasSnapped: false
- property int referenceWidth: roomHeader.buttonsWidth
- onReferenceWidthChanged: {
- if (chatSplitView.canAutoSize || wasSnapped) {
- if (wasSnapped) { chatSplitView.canAutoSize = true }
- width = referenceWidth
- }
- }
+ //property bool wasSnapped: false
+ //property int referenceWidth: roomHeader.buttonsWidth
+ //onReferenceWidthChanged: {
+ //if (chatSplitView.canAutoSize || wasSnapped) {
+ //if (wasSnapped) { chatSplitView.canAutoSize = true }
+ //width = referenceWidth
+ //}
+ //}
- property int currentWidth: width
- onCurrentWidthChanged: {
- if (referenceWidth != width &&
- referenceWidth - 15 < width &&
- width < referenceWidth + 15)
- {
- currentWidth = referenceWidth
- width = referenceWidth
- wasSnapped = true
- currentWidth = Qt.binding(
- function() { return roomSidePane.width }
- )
- } else {
- wasSnapped = false
- }
- }
+ //property int currentWidth: width
+ //onCurrentWidthChanged: {
+ //if (referenceWidth != width &&
+ //referenceWidth - 15 < width &&
+ //width < referenceWidth + 15)
+ //{
+ //currentWidth = referenceWidth
+ //width = referenceWidth
+ //wasSnapped = true
+ //currentWidth = Qt.binding(
+ //function() { return roomSidePane.width }
+ //)
+ //} else {
+ //wasSnapped = false
+ //}
+ //}
- width: referenceWidth // Initial width
- Layout.minimumWidth: HStyle.avatar.size
- Layout.maximumWidth: parent.width
- }
+ //width: referenceWidth // Initial width
+ //Layout.minimumWidth: HStyle.avatar.size
+ //Layout.maximumWidth: parent.width
+ //}
}
}
diff --git a/src/qml/Chat/RoomEventList/Daybreak.qml b/src/qml/Chat/RoomEventList/Daybreak.qml
index fd1da83a..71319dbd 100644
--- a/src/qml/Chat/RoomEventList/Daybreak.qml
+++ b/src/qml/Chat/RoomEventList/Daybreak.qml
@@ -2,7 +2,7 @@ import QtQuick 2.7
import "../../Base"
HNoticePage {
- text: dateTime.toLocaleDateString()
+ text: model.date.toLocaleDateString()
color: HStyle.chat.daybreak.foreground
backgroundColor: HStyle.chat.daybreak.background
radius: HStyle.chat.daybreak.radius
diff --git a/src/qml/Chat/RoomEventList/EventContent.qml b/src/qml/Chat/RoomEventList/EventContent.qml
index 5b3fdd6e..3ecb92a5 100644
--- a/src/qml/Chat/RoomEventList/EventContent.qml
+++ b/src/qml/Chat/RoomEventList/EventContent.qml
@@ -16,7 +16,7 @@ Row {
HAvatar {
id: avatar
- name: sender.displayName.value
+ name: sender.displayName || stripUserId(sender.userId)
hidden: combine
dimension: 28
}
diff --git a/src/qml/Chat/RoomEventList/MessageContent.qml b/src/qml/Chat/RoomEventList/MessageContent.qml
index 41f3bd17..c03f3579 100644
--- a/src/qml/Chat/RoomEventList/MessageContent.qml
+++ b/src/qml/Chat/RoomEventList/MessageContent.qml
@@ -10,7 +10,7 @@ Row {
HAvatar {
id: avatar
hidden: combine
- name: sender.displayName.value
+ name: senderInfo.displayName || stripUserId(model.senderId)
dimension: 48
}
@@ -38,8 +38,8 @@ Row {
visible: height > 0
id: nameLabel
- text: sender.displayName.value
- color: Qt.hsla(Backend.hueFromString(text),
+ text: senderInfo.displayName || model.senderId
+ color: Qt.hsla(avatar.hueFromName(avatar.name),
HStyle.displayName.saturation,
HStyle.displayName.lightness,
1)
@@ -56,17 +56,16 @@ Row {
width: parent.width
id: contentLabel
- text: (dict.formatted_body ?
- Backend.htmlFilter.filter(dict.formatted_body) :
- dict.body) +
+ text: model.content +
" " +
- Qt.formatDateTime(dateTime, "hh:mm:ss") +
+ Qt.formatDateTime(model.date, "hh:mm:ss") +
"" +
- (isLocalEcho ?
+ (model.isLocalEcho ?
" ⏳" : "")
- textFormat: Text.RichText
+ textFormat: model.type == "text" ?
+ Text.PlainText : Text.RichText
color: HStyle.chat.message.body
wrapMode: Text.Wrap
diff --git a/src/qml/Chat/RoomEventList/RoomEventDelegate.qml b/src/qml/Chat/RoomEventList/RoomEventDelegate.qml
index 3fb19ade..b81d4556 100644
--- a/src/qml/Chat/RoomEventList/RoomEventDelegate.qml
+++ b/src/qml/Chat/RoomEventList/RoomEventDelegate.qml
@@ -1,7 +1,6 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
-import "../utils.js" as ChatJS
Column {
id: roomEventDelegate
@@ -10,46 +9,47 @@ Column {
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
}
- function getIsMessage(type_) { return type_.startsWith("RoomMessage") }
-
function getPreviousItem() {
return index < roomEventListView.model.count - 1 ?
roomEventListView.model.get(index + 1) : null
}
+ function getIsMessage(type) {
+ return true
+ }
+
property var previousItem: getPreviousItem()
signal reloadPreviousItem()
onReloadPreviousItem: previousItem = getPreviousItem()
- readonly property bool isMessage: getIsMessage(type)
+ property var senderInfo: null
+ Component.onCompleted:
+ senderInfo = models.users.getUser(chatPage.userId, senderId)
- readonly property bool isUndecryptableEvent:
- type === "OlmEvent" || type === "MegolmEvent"
+ //readonly property bool isMessage: ! model.type.match(/^event.*/)
+ readonly property bool isMessage: getIsMessage(model.type)
- readonly property var sender: Backend.users.get(dict.sender)
+ readonly property bool isOwn: chatPage.userId === senderId
- readonly property bool isOwn:
- chatPage.userId === dict.sender
-
- readonly property bool isFirstEvent: type == "RoomCreateEvent"
+ readonly property bool isFirstEvent: model.type == "eventCreate"
readonly property bool combine:
previousItem &&
! talkBreak &&
! dayBreak &&
getIsMessage(previousItem.type) === isMessage &&
- previousItem.dict.sender === dict.sender &&
- minsBetween(previousItem.dateTime, dateTime) <= 5
+ previousItem.senderId === senderId &&
+ minsBetween(previousItem.date, model.date) <= 5
readonly property bool dayBreak:
isFirstEvent ||
previousItem &&
- dateTime.getDate() != previousItem.dateTime.getDate()
+ model.date.getDate() != previousItem.date.getDate()
readonly property bool talkBreak:
previousItem &&
! dayBreak &&
- minsBetween(previousItem.dateTime, dateTime) >= 20
+ minsBetween(previousItem.date, model.date) >= 20
property int standardSpacing: 16
diff --git a/src/qml/Chat/RoomEventList/RoomEventList.qml b/src/qml/Chat/RoomEventList/RoomEventList.qml
index a467d0e8..a26eaafe 100644
--- a/src/qml/Chat/RoomEventList/RoomEventList.qml
+++ b/src/qml/Chat/RoomEventList/RoomEventList.qml
@@ -1,4 +1,5 @@
import QtQuick 2.7
+import SortFilterProxyModel 0.2
import "../../Base"
HRectangle {
@@ -8,10 +9,19 @@ HRectangle {
HListView {
id: roomEventListView
- delegate: RoomEventDelegate {}
- model: Backend.roomEvents.get(chatPage.roomId)
clip: true
+ model: HListModel {
+ sourceModel: models.timelines
+
+ filters: ValueFilter {
+ roleName: "roomId"
+ value: chatPage.roomId
+ }
+ }
+
+ delegate: RoomEventDelegate {}
+
anchors.fill: parent
anchors.leftMargin: space
anchors.rightMargin: space
@@ -29,7 +39,7 @@ HRectangle {
onYPosChanged: {
if (chatPage.category != "Invites" && yPos <= 0.1) {
- Backend.loadPastEvents(chatPage.roomId)
+ //Backend.loadPastEvents(chatPage.roomId)
}
}
}
diff --git a/src/qml/Chat/RoomHeader.qml b/src/qml/Chat/RoomHeader.qml
index 82265393..720570f7 100644
--- a/src/qml/Chat/RoomHeader.qml
+++ b/src/qml/Chat/RoomHeader.qml
@@ -3,7 +3,7 @@ import QtQuick.Layouts 1.3
import "../Base"
HRectangle {
- property string displayName: ""
+ property var displayName: ""
property string topic: ""
property alias buttonsImplicitWidth: viewButtons.implicitWidth
@@ -22,7 +22,7 @@ HRectangle {
HAvatar {
id: avatar
- name: displayName
+ name: stripRoomName(displayName) || qsTr("Empty room")
dimension: roomHeader.height
Layout.alignment: Qt.AlignTop
}
diff --git a/src/qml/Chat/RoomSidePane/MemberDelegate.qml b/src/qml/Chat/RoomSidePane/MemberDelegate.qml
index 3803a525..bab34cb7 100644
--- a/src/qml/Chat/RoomSidePane/MemberDelegate.qml
+++ b/src/qml/Chat/RoomSidePane/MemberDelegate.qml
@@ -15,7 +15,7 @@ MouseArea {
HAvatar {
id: memberAvatar
- name: member.displayName.value
+ name: member.displayName || stripUserId(member.userId)
}
HColumnLayout {
diff --git a/src/qml/Chat/SendBox.qml b/src/qml/Chat/SendBox.qml
index 0fadaede..3a70b92d 100644
--- a/src/qml/Chat/SendBox.qml
+++ b/src/qml/Chat/SendBox.qml
@@ -18,7 +18,7 @@ HRectangle {
HAvatar {
id: avatar
- name: chatPage.sender.displayName.value
+ name: chatPage.sender.displayName || stripUserId(chatPage.userId)
dimension: root.Layout.minimumHeight
}
diff --git a/src/qml/EventHandlers/rooms.js b/src/qml/EventHandlers/rooms.js
index 5e137781..1d19410f 100644
--- a/src/qml/EventHandlers/rooms.js
+++ b/src/qml/EventHandlers/rooms.js
@@ -1,26 +1,28 @@
-function clientId(user_id, category, room_id) {
- return user_id + " " + room_id + " " + category
-}
-
-
function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
topic, last_event_date, inviter, left_event) {
- var client_id = clientId(user_id, category, room_id)
- var rooms = models.rooms
+ models.roomCategories.upsert({"userId": user_id, "name": category}, {
+ "userId": user_id,
+ "name": category
+ })
+
+ var rooms = models.rooms
+
+ function roles(for_category) {
+ return {"userId": user_id, "roomId": room_id, "category": for_category}
+ }
if (category == "Invites") {
- rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id))
- rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
+ rooms.popWhere(roles("Rooms"), 1)
+ rooms.popWhere(roles("Left"), 1)
}
else if (category == "Rooms") {
- rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
- rooms.popWhere("clientId", clientId(user_id, "Left", room_id))
+ rooms.popWhere(roles("Invites"), 1)
+ rooms.popWhere(roles("Left"), 1)
}
else if (category == "Left") {
- var old_room =
- rooms.popWhere("clientId", clientId(user_id, "Rooms", room_id)) ||
- rooms.popWhere("clientId", clientId(user_id, "Invites", room_id))
+ var old_room = rooms.popWhere(roles("Invites"), 1)[0] ||
+ rooms.popWhere(roles("Rooms"), 1)[0]
if (old_room) {
display_name = old_room.displayName
@@ -30,8 +32,7 @@ function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
}
}
- rooms.upsert("clientId", client_id , {
- "clientId": client_id,
+ rooms.upsert(roles(category), {
"userId": user_id,
"category": category,
"roomId": room_id,
@@ -47,8 +48,8 @@ function onRoomUpdated(user_id, category, room_id, display_name, avatar_url,
function onRoomDeleted(user_id, category, room_id) {
- var client_id = clientId(user_id, category, room_id)
- return models.rooms.popWhere("clientId", client_id, 1)
+ var roles = {"userId": user_id, "roomId": room_id, "category": category}
+ models.rooms.popWhere(roles, 1)
}
diff --git a/src/qml/EventHandlers/rooms_timeline.js b/src/qml/EventHandlers/rooms_timeline.js
index e69de29b..f06889c7 100644
--- a/src/qml/EventHandlers/rooms_timeline.js
+++ b/src/qml/EventHandlers/rooms_timeline.js
@@ -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)
+}
diff --git a/src/qml/EventHandlers/users.js b/src/qml/EventHandlers/users.js
index 9322a97d..a0a6d658 100644
--- a/src/qml/EventHandlers/users.js
+++ b/src/qml/EventHandlers/users.js
@@ -2,18 +2,17 @@ function onAccountUpdated(user_id) {
models.accounts.append({"userId": user_id})
}
-function AccountDeleted(user_id) {
- models.accounts.popWhere("userId", user_id, 1)
+function onAccountDeleted(user_id) {
+ models.accounts.popWhere({"userId": user_id}, 1)
}
function onUserUpdated(user_id, display_name, avatar_url, status_message) {
- models.users.upsert("userId", user_id, {
+ models.users.upsert({"userId": user_id}, {
"userId": user_id,
"displayName": display_name,
"avatarUrl": avatar_url,
"statusMessage": status_message
})
-
}
function onDeviceUpdated(user_id, device_id, ed25519_key, trust, display_name,
diff --git a/src/qml/Models.qml b/src/qml/Models.qml
index 2aaac983..7fd22e4c 100644
--- a/src/qml/Models.qml
+++ b/src/qml/Models.qml
@@ -1,4 +1,5 @@
import QtQuick 2.7
+import SortFilterProxyModel 0.2
import "Base"
QtObject {
@@ -8,7 +9,7 @@ QtObject {
function getUser(as_account_id, wanted_user_id) {
wanted_user_id = wanted_user_id || as_account_id
- var found = users.getWhere("userId", wanted_user_id, 1)
+ var found = users.getWhere({"userId": wanted_user_id}, 1)
if (found.length > 0) { return found[0] }
users.append({
@@ -22,13 +23,20 @@ QtObject {
as_account_id, "request_user_update_event", [wanted_user_id]
)
- return users.getWhere("userId", wanted_user_id, 1)[0]
+ return users.getWhere({"userId": wanted_user_id}, 1)[0]
}
}
property HListModel devices: HListModel {}
+ property HListModel roomCategories: HListModel {}
+
property HListModel rooms: HListModel {}
- property HListModel timelines: HListModel {}
+ property HListModel timelines: HListModel {
+ sorters: RoleSorter {
+ roleName: "date"
+ sortOrder: Qt.DescendingOrder
+ }
+ }
}
diff --git a/src/qml/Pages/RememberAccount.qml b/src/qml/Pages/RememberAccount.qml
index a1a5f2ca..7542b0c8 100644
--- a/src/qml/Pages/RememberAccount.qml
+++ b/src/qml/Pages/RememberAccount.qml
@@ -4,7 +4,7 @@ import "../Base"
Item {
property string loginWith: "username"
- property var client: null
+ property string userId: ""
HInterfaceBox {
id: rememberBox
@@ -20,10 +20,13 @@ Item {
buttonCallbacks: {
"yes": function(button) {
- Backend.clients.remember(client)
+ py.callCoro("save_account", [userId])
+ pageStack.showPage("Default")
+ },
+ "no": function(button) {
+ py.callCoro("forget_account", [userId])
pageStack.showPage("Default")
},
- "no": function(button) { pageStack.showPage("Default") },
}
HLabel {
diff --git a/src/qml/Pages/SignIn.qml b/src/qml/Pages/SignIn.qml
index f29fbafb..2a9e2347 100644
--- a/src/qml/Pages/SignIn.qml
+++ b/src/qml/Pages/SignIn.qml
@@ -4,7 +4,7 @@ import "../Base"
Item {
property string loginWith: "username"
- onFocusChanged: identifierField.forceActiveFocus()
+ onFocusChanged: idField.forceActiveFocus()
HInterfaceBox {
id: signInBox
@@ -23,15 +23,15 @@ Item {
"register": function(button) {},
"login": function(button) {
- var future = Backend.clients.new(
- "matrix.org", identifierField.text, passwordField.text
- )
- button.loadingUntilFutureDone(future)
- future.onGotResult.connect(function(client) {
+ button.loading = true
+ var args = [idField.text, passwordField.text]
+
+ py.callCoro("login_client", args, {}, function(user_id) {
pageStack.showPage(
"RememberAccount",
- {"loginWith": loginWith, "client": client}
+ {"loginWith": loginWith, "userId": user_id}
)
+ button.loading = false
})
},
@@ -58,7 +58,7 @@ Item {
}
HTextField {
- id: identifierField
+ id: idField
placeholderText: qsTr(
loginWith === "email" ? "Email" :
loginWith === "phone" ? "Phone" :
diff --git a/src/qml/Python.qml b/src/qml/Python.qml
index 2273f01e..c6810442 100644
--- a/src/qml/Python.qml
+++ b/src/qml/Python.qml
@@ -9,6 +9,7 @@ Python {
property bool ready: false
property var pendingCoroutines: ({})
+ signal willLoadAccounts(bool will)
property bool loadingAccounts: false
function callCoro(name, args, kwargs, callback) {
@@ -32,18 +33,20 @@ Python {
}
}
- addImportPath("../..")
- importNames("src", ["APP"], function() {
- call("APP.start", [Qt.application.arguments], function(debug_on) {
- window.debug = debug_on
+ addImportPath("src")
+ addImportPath("qrc:/")
+ importNames("python", ["APP"], function() {
+ call("APP.is_debug_on", [Qt.application.arguments], function(on) {
+ window.debug = on
callCoro("has_saved_accounts", [], {}, function(has) {
- loadingAccounts = has
py.ready = true
+ willLoadAccounts(has)
if (has) {
+ py.loadingAccounts = true
py.callCoro("load_saved_accounts", [], {}, function() {
- loadingAccounts = false
+ py.loadingAccounts = false
})
}
})
diff --git a/src/qml/SidePane/AccountDelegate.qml b/src/qml/SidePane/AccountDelegate.qml
index 3a9ef467..8efa6bb2 100644
--- a/src/qml/SidePane/AccountDelegate.qml
+++ b/src/qml/SidePane/AccountDelegate.qml
@@ -20,7 +20,7 @@ Column {
HAvatar {
id: avatar
- name: user.displayName
+ name: user.displayName || stripUserId(user.userId)
}
HColumnLayout {
diff --git a/src/qml/SidePane/PaneToolBar.qml b/src/qml/SidePane/PaneToolBar.qml
index 4cecb7f5..bdbb4ab2 100644
--- a/src/qml/SidePane/PaneToolBar.qml
+++ b/src/qml/SidePane/PaneToolBar.qml
@@ -4,6 +4,8 @@ import "../Base"
HRowLayout {
id: toolBar
+ property alias roomFilter: filterField.text
+
Layout.fillWidth: true
Layout.preferredHeight: HStyle.bottomElementsHeight
@@ -17,8 +19,6 @@ HRowLayout {
placeholderText: qsTr("Filter rooms")
backgroundColor: HStyle.sidePane.filterRooms.background
- onTextChanged: Backend.setRoomFilter(text)
-
Layout.fillWidth: true
Layout.preferredHeight: parent.height
}
diff --git a/src/qml/SidePane/RoomCategoriesList.qml b/src/qml/SidePane/RoomCategoriesList.qml
index 0efd465c..35ad0755 100644
--- a/src/qml/SidePane/RoomCategoriesList.qml
+++ b/src/qml/SidePane/RoomCategoriesList.qml
@@ -1,11 +1,20 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
+import SortFilterProxyModel 0.2
import "../Base"
HListView {
property string userId: ""
id: roomCategoriesList
- model: Backend.accounts.get(userId).roomCategories
+
+ model: SortFilterProxyModel {
+ sourceModel: models.roomCategories
+ filters: ValueFilter {
+ roleName: "userId"
+ value: userId
+ }
+ }
+
delegate: RoomCategoryDelegate {}
}
diff --git a/src/qml/SidePane/RoomDelegate.qml b/src/qml/SidePane/RoomDelegate.qml
index 5d6dee7a..3cabce06 100644
--- a/src/qml/SidePane/RoomDelegate.qml
+++ b/src/qml/SidePane/RoomDelegate.qml
@@ -12,11 +12,11 @@ MouseArea {
HRowLayout {
width: parent.width
- spacing: roomList.spacing
+ spacing: sidePane.normalSpacing
HAvatar {
id: roomAvatar
- name: displayName
+ name: stripRoomName(displayName) || qsTr("Empty room")
}
HColumnLayout {
@@ -35,27 +35,27 @@ MouseArea {
Layout.maximumWidth: parent.width
}
- HLabel {
- function getText() {
- return SidePaneJS.getLastRoomEventText(
- roomId, roomList.userId
- )
- }
+ //HLabel {
+ //function getText() {
+ //return SidePaneJS.getLastRoomEventText(
+ //roomId, roomList.userId
+ //)
+ //}
- property var lastEvTime: lastEventDateTime
- onLastEvTimeChanged: subtitleLabel.text = getText()
+ //property var lastEvTime: lastEventDateTime
+ //onLastEvTimeChanged: subtitleLabel.text = getText()
- id: subtitleLabel
- visible: text !== ""
- text: getText()
- textFormat: Text.StyledText
+ //id: subtitleLabel
+ //visible: text !== ""
+ //text: getText()
+ //textFormat: Text.StyledText
- font.pixelSize: HStyle.fontSize.small
- elide: Text.ElideRight
- maximumLineCount: 1
+ //font.pixelSize: HStyle.fontSize.small
+ //elide: Text.ElideRight
+ //maximumLineCount: 1
- Layout.maximumWidth: parent.width
- }
+ //Layout.maximumWidth: parent.width
+ //}
}
}
}
diff --git a/src/qml/SidePane/RoomList.qml b/src/qml/SidePane/RoomList.qml
index 2e663933..cdfa6668 100644
--- a/src/qml/SidePane/RoomList.qml
+++ b/src/qml/SidePane/RoomList.qml
@@ -1,5 +1,6 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
+import SortFilterProxyModel 0.2
import "../Base"
HListView {
@@ -7,8 +8,37 @@ HListView {
property string category: ""
id: roomList
- spacing: accountList.spacing
- model:
- Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
+ spacing: sidePane.normalSpacing
+
+ model: SortFilterProxyModel {
+ sourceModel: models.rooms
+ filters: AllOf {
+ ValueFilter {
+ roleName: "category"
+ value: category
+ }
+
+ ValueFilter {
+ roleName: "userId"
+ value: userId
+ }
+
+ ExpressionFilter {
+ expression: {
+ var filter = paneToolBar.roomFilter.toLowerCase()
+ var words = filter.split(" ")
+ var room_name = displayName.toLowerCase()
+
+ for (var i = 0; i < words.length; i++) {
+ if (words[i] && room_name.indexOf(words[i]) == -1) {
+ return false
+ }
+ }
+ return true
+ }
+ }
+ }
+ }
+
delegate: RoomDelegate {}
}
diff --git a/src/qml/SidePane/SidePane.qml b/src/qml/SidePane/SidePane.qml
index c6f9a20b..7ae95211 100644
--- a/src/qml/SidePane/SidePane.qml
+++ b/src/qml/SidePane/SidePane.qml
@@ -15,16 +15,18 @@ HRectangle {
Layout.fillWidth: true
Layout.fillHeight: true
- spacing: collapsed ? 0 : normalSpacing
- topMargin: spacing
- bottomMargin: spacing
- Layout.leftMargin: spacing
+ spacing: collapsed ? 0 : normalSpacing * 3
+ topMargin: normalSpacing
+ bottomMargin: normalSpacing
+ Layout.leftMargin: normalSpacing
Behavior on spacing {
NumberAnimation { duration: HStyle.animationDuration }
}
}
- PaneToolBar {}
+ PaneToolBar {
+ id: paneToolBar
+ }
}
}
diff --git a/src/qml/UI.qml b/src/qml/UI.qml
index fc07b5f9..f6288ed5 100644
--- a/src/qml/UI.qml
+++ b/src/qml/UI.qml
@@ -8,10 +8,15 @@ import "SidePane"
Item {
id: mainUI
+ Connections {
+ target: py
+ onWillLoadAccounts: function(will) {
+ pageStack.showPage(will ? "Default" : "SignIn")
+ }
+ }
+
property bool accountsPresent:
models.accounts.count > 0 || py.loadingAccounts
- onAccountsPresentChanged:
- pageStack.showPage(accountsPresent ? "Default" : "SignIn")
HImage {
id: mainUIBackground
diff --git a/submodules/SortFilterProxyModel b/submodules/SortFilterProxyModel
new file mode 160000
index 00000000..770789ee
--- /dev/null
+++ b/submodules/SortFilterProxyModel
@@ -0,0 +1 @@
+Subproject commit 770789ee484abf69c230cbf1b64f39823e79a181