diff --git a/TODO.md b/TODO.md
index 95441728..0f82857f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,6 +13,7 @@
- Graphic bug when resizing window vertically for side pane?
- Fix tooltip hide()
- ![A picture](https://picsum.photos/256/256) not clickable?
+ - Icons aren't reloaded
- UI
- Use HRowLayout and its totalSpacing wherever possible
@@ -48,6 +49,7 @@
it should be the peer's display name instead.
- Missing nio support
+ - Left room events
- `org.matrix.room.preview_urls` event
- `m.room.aliases` event
- Avatars
diff --git a/harmonyqml/backend/client.py b/harmonyqml/backend/client.py
index 5e33cb14..a5363f57 100644
--- a/harmonyqml/backend/client.py
+++ b/harmonyqml/backend/client.py
@@ -22,15 +22,16 @@ _POOLS: DefaultDict[str, ThreadPoolExecutor] = \
class Client(QObject):
- roomInvited = pyqtSignal(str)
- roomInvited = pyqtSignal(str, dict)
- roomJoined = pyqtSignal(str)
- roomLeft = pyqtSignal(str)
+ roomInvited = pyqtSignal([str, dict], [str])
+ roomJoined = pyqtSignal(str)
+ roomLeft = pyqtSignal([str, dict], [str])
+
roomSyncPrevBatchTokenReceived = pyqtSignal(str, str)
roomPastPrevBatchTokenReceived = pyqtSignal(str, str)
roomEventReceived = pyqtSignal(str, str, dict)
roomTypingUsersUpdated = pyqtSignal(str, list)
- messageAboutToBeSent = pyqtSignal(str, dict)
+
+ messageAboutToBeSent = pyqtSignal(str, dict)
def __init__(self,
@@ -120,13 +121,13 @@ class Client(QObject):
for room_id, room_info in response.rooms.invite.items():
for ev in room_info.invite_state:
- member_event = isinstance(ev, ne.InviteMemberEvent)
+ member_ev = isinstance(ev, ne.InviteMemberEvent)
- if member_event and ev.content["membership"] == "join":
+ if member_ev and ev.content["membership"] == "join":
self.roomInvited.emit(room_id, ev.content)
break
else:
- self.roomInvited.emit(room_id)
+ self.roomInvited[str].emit(room_id)
for room_id, room_info in response.rooms.join.items():
self.roomJoined.emit(room_id)
@@ -146,8 +147,15 @@ class Client(QObject):
else:
print("ephemeral event: ", ev)
- for room_id in response.rooms.leave:
- self.roomLeft.emit(room_id)
+ for room_id, room_info in response.rooms.leave.items():
+ for ev in room_info.timeline.events:
+ member_ev = isinstance(ev, ne.RoomMemberEvent)
+
+ if member_ev and ev.content["membership"] in ("leave", "ban"):
+ self.roomLeft.emit(room_id, ev.__dict__)
+ break
+ else:
+ self.roomLeft[str].emit(room_id)
@futurize()
diff --git a/harmonyqml/backend/model/items.py b/harmonyqml/backend/model/items.py
index 1f5c5bfe..31fb934a 100644
--- a/harmonyqml/backend/model/items.py
+++ b/harmonyqml/backend/model/items.py
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Optional, Tuple, Union
+from typing import Any, Callable, Optional, Sequence, Tuple, Union
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
@@ -6,8 +6,9 @@ from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
class ListItem(QObject):
roles: Tuple[str, ...] = ()
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, no_update: Sequence[str] = (), **kwargs):
super().__init__()
+ self.no_update = no_update
for role, value in zip(self.roles, args):
setattr(self, role, value)
@@ -17,8 +18,9 @@ class ListItem(QObject):
def __repr__(self) -> str:
- return "%s(%s)" % (
+ return "%s(no_update=%s, %s)" % (
type(self).__name__,
+ self.no_update,
", ".join((f"{r}={getattr(self, r)!r}" for r in self.roles)),
)
@@ -63,7 +65,7 @@ class User(ListItem):
class Room(ListItem):
roles = ("roomId", "category", "displayName", "topic", "typingUsers",
- "inviter")
+ "inviter", "leftEvent")
categoryChanged = pyqtSignal(str)
displayNameChanged = pyqtSignal("QVariant")
@@ -75,7 +77,8 @@ class Room(ListItem):
displayName = prop(str, "displayName", displayNameChanged)
topic = prop(str, "topic", topicChanged, "")
typingUsers = prop(list, "typingUsers", typingUsersChanged, [])
- inviter = prop("QVariantMap", "inviter")
+ inviter = prop("QVariant", "inviter")
+ leftEvent = prop("QVariant", "leftEvent")
class RoomEvent(ListItem):
diff --git a/harmonyqml/backend/model/list_model.py b/harmonyqml/backend/model/list_model.py
index 4d7e7d6e..a283476a 100644
--- a/harmonyqml/backend/model/list_model.py
+++ b/harmonyqml/backend/model/list_model.py
@@ -148,6 +148,9 @@ class ListModel(QAbstractListModel):
value = self._convert_new_value(value)
for role in self.roles:
+ if role in value.no_update:
+ continue
+
setattr(self._data[index], role, getattr(value, role))
qidx = QAbstractListModel.index(self, index, 0)
diff --git a/harmonyqml/backend/signal_manager.py b/harmonyqml/backend/signal_manager.py
index 06d11c67..20e1d1dd 100644
--- a/harmonyqml/backend/signal_manager.py
+++ b/harmonyqml/backend/signal_manager.py
@@ -13,7 +13,8 @@ from .backend import Backend
from .client import Client
from .model.items import Room, RoomEvent, User
-Inviter = Optional[Dict[str, str]]
+Inviter = Optional[Dict[str, str]]
+LeftEvent = Optional[Dict[str, str]]
class SignalManager(QObject):
@@ -23,8 +24,8 @@ class SignalManager(QObject):
super().__init__(parent=backend)
self.backend = backend
- self.last_room_events: Deque[str] = Deque(maxlen=1000)
- self._events_in_transfer: int = 0
+ self.last_room_events: Deque[str] = Deque(maxlen=1000)
+ self._events_in_transfer: int = 0
cm = self.backend.clientManager
cm.clientAdded.connect(self.onClientAdded)
@@ -60,45 +61,60 @@ class SignalManager(QObject):
client: Client,
room_id: str,
inviter: Inviter = None) -> None:
- self._add_room(
- client, client.nio.invited_rooms[room_id], "Invites", inviter
- )
+
+ self._add_room(client, room_id, client.nio.invited_rooms[room_id],
+ "Invites", inviter=inviter)
def onRoomJoined(self, client: Client, room_id: str) -> None:
- self._add_room(client, client.nio.rooms[room_id], "Rooms")
+ self._add_room(client, room_id, client.nio.rooms[room_id], "Rooms")
+
+
+ def onRoomLeft(self,
+ client: Client,
+ room_id: str,
+ left_event: LeftEvent = None) -> None:
+
+ self._add_room(client, room_id, client.nio.rooms.get(room_id), "Left",
+ left_event=left_event)
def _add_room(self,
- client: Client,
- room: MatrixRoom,
- category: str,
- inviter: Inviter = None) -> None:
- model = self.backend.models.rooms[client.userId]
+ client: Client,
+ room_id: str,
+ room: MatrixRoom,
+ category: str,
+ inviter: Inviter = None,
+ left_event: LeftEvent = None) -> None:
+
+ assert not (inviter and left_event)
+
+ model = self.backend.models.rooms[client.userId]
+ no_update = []
+
+ def get_displayname() -> Optional[str]:
+ if not room:
+ no_update.append("displayName")
+ return room_id
+
+ name = room.name or room.canonical_alias
+ if name:
+ return name
- def group_name() -> Optional[str]:
name = room.group_name()
return None if name == "Empty room?" else name
item = Room(
- roomId = room.room_id,
+ roomId = room_id,
category = category,
- displayName = room.name or room.canonical_alias or group_name(),
- topic = room.topic,
+ displayName = get_displayname(),
+ topic = room.topic if room else "",
inviter = inviter,
+ leftEvent = left_event,
+ no_update = no_update,
)
- model.updateOrAppendWhere("roomId", room.room_id, item)
-
-
- def onRoomLeft(self, client: Client, room_id: str) -> None:
- rooms = self.backend.models.rooms[client.userId]
- try:
- index = rooms.indexWhere("roomId", room_id)
- except ValueError:
- pass
- else:
- del rooms[index]
+ model.updateOrAppendWhere("roomId", room_id, item)
def onRoomSyncPrevBatchTokenReceived(
diff --git a/harmonyqml/components/UI.qml b/harmonyqml/components/UI.qml
index 738dd2c1..b391ee78 100644
--- a/harmonyqml/components/UI.qml
+++ b/harmonyqml/components/UI.qml
@@ -15,7 +15,7 @@ Controls1.SplitView {
}
StackView {
- function showRoom(userId, roomId, isInvite) {
+ function showRoom(userId, roomId) {
pageStack.replace(
"chat/Root.qml", { userId: userId, roomId: roomId }
)
@@ -29,6 +29,7 @@ Controls1.SplitView {
initialItem: Item { // TODO: (test, remove)
Keys.onPressed: pageStack.showRoom(
"@test_mary:matrix.org", "!TSXGsbBbdwsdylIOJZ:matrix.org"
+ //"@test_mary:matrix.org", "!TEXkdeErtVCMqClNfb:matrix.org"
)
}
diff --git a/harmonyqml/components/chat/Banner.qml b/harmonyqml/components/chat/Banner.qml
new file mode 100644
index 00000000..597a1d4a
--- /dev/null
+++ b/harmonyqml/components/chat/Banner.qml
@@ -0,0 +1,89 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.4
+import "../base" as Base
+
+Rectangle {
+ id: banner
+ Layout.fillWidth: true
+ Layout.preferredHeight: 32
+ color: "#BBB"
+
+ property alias avatarName: bannerAvatar.name
+ property alias avatarSource: bannerAvatar.imageSource
+ property alias labelText: bannerLabel.text
+ property alias buttonModel: bannerRepeater.model
+
+ Base.HRowLayout {
+ id: bannerRow
+ anchors.fill: parent
+
+ Base.Avatar {
+ id: bannerAvatar
+ dimmension: banner.Layout.preferredHeight
+ }
+
+ Base.HLabel {
+ id: bannerLabel
+ textFormat: Text.StyledText
+ maximumLineCount: 1
+ elide: Text.ElideRight
+
+ visible:
+ bannerRow.width - bannerAvatar.width - bannerButtons.width > 30
+
+ Layout.maximumWidth:
+ bannerRow.width -
+ bannerAvatar.width - bannerButtons.width -
+ Layout.leftMargin - Layout.rightMargin
+
+ Layout.leftMargin: 10
+ Layout.rightMargin: Layout.leftMargin
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Base.HRowLayout {
+ id: bannerButtons
+ spacing: 0
+
+ function getButtonsWidth() {
+ var total = 0
+
+ for (var i = 0; i < bannerRepeater.count; i++) {
+ total += bannerRepeater.itemAt(i).implicitWidth
+ }
+
+ return total
+ }
+
+ property bool compact:
+ bannerRow.width <
+ bannerAvatar.width +
+ bannerLabel.implicitWidth +
+ bannerLabel.Layout.leftMargin +
+ bannerLabel.Layout.rightMargin +
+ getButtonsWidth()
+
+ property int displayMode:
+ compact ? Button.IconOnly : Button.TextBesideIcon
+
+ Repeater {
+ id: bannerRepeater
+ model: []
+
+ Base.HButton {
+ id: declineButton
+ text: modelData.text
+ iconName: modelData.iconName
+ icon.color: modelData.iconColor
+ icon.width: 32
+ display: bannerButtons.displayMode
+
+ Layout.maximumWidth: bannerButtons.compact ? height : -1
+ Layout.fillHeight: true
+ }
+ }
+ }
+ }
+}
diff --git a/harmonyqml/components/chat/InviteBanner.qml b/harmonyqml/components/chat/InviteBanner.qml
new file mode 100644
index 00000000..ff17d659
--- /dev/null
+++ b/harmonyqml/components/chat/InviteBanner.qml
@@ -0,0 +1,29 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.4
+import "../base" as Base
+
+Banner {
+ property var inviter: null
+
+ avatarName: inviter ? inviter.displayname : ""
+ //avatarSource: inviter ? inviter.avatar_url : ""
+
+ labelText:
+ (inviter ?
+ ("" + inviter.displayname + "") : qsTr("Someone")) +
+ " " + qsTr("invited you to join the room.")
+
+ buttonModel: [
+ {
+ text: "Accept",
+ iconName: "accept",
+ iconColor: Qt.hsla(0.45, 0.9, 0.3, 1),
+ },
+ {
+ text: "Decline",
+ iconName: "decline",
+ iconColor: Qt.hsla(0.95, 0.9, 0.35, 1),
+ }
+ ]
+}
diff --git a/harmonyqml/components/chat/InviteOffer.qml b/harmonyqml/components/chat/InviteOffer.qml
deleted file mode 100644
index 3a548198..00000000
--- a/harmonyqml/components/chat/InviteOffer.qml
+++ /dev/null
@@ -1,84 +0,0 @@
-import QtQuick 2.7
-import QtQuick.Controls 2.2
-import QtQuick.Layouts 1.4
-import "../base" as Base
-
-Rectangle {
- id: inviteOffer
- Layout.fillWidth: true
- Layout.preferredHeight: 32
- color: "#BBB"
-
- property var inviter: null
-
- Base.HRowLayout {
- id: inviteRow
- anchors.fill: parent
-
- Base.Avatar {
- id: inviteAvatar
- name: inviter ? inviter.displayname : ""
- dimmension: inviteOffer.Layout.preferredHeight
- //imageSource: inviter ? inviter.avatar_url : ""
- }
-
- Base.HLabel {
- id: inviteLabel
- text: (inviter ?
- ("" + inviter.displayname + "") : qsTr("Someone")) +
- " " + qsTr("invited you to join the room.")
- textFormat: Text.StyledText
- maximumLineCount: 1
- elide: Text.ElideRight
-
- visible:
- inviteRow.width - inviteAvatar.width - inviteButtons.width > 30
-
- Layout.maximumWidth:
- inviteRow.width -
- inviteAvatar.width - inviteButtons.width -
- Layout.leftMargin - Layout.rightMargin
-
- Layout.leftMargin: 10
- Layout.rightMargin: Layout.leftMargin
- }
-
- Item { Layout.fillWidth: true }
-
- Base.HRowLayout {
- id: inviteButtons
- spacing: 0
-
- property bool compact:
- inviteRow.width <
- inviteAvatar.width + inviteLabel.implicitWidth +
- acceptButton.implicitWidth + declineButton.implicitWidth
-
- property int displayMode:
- compact ? Button.IconOnly : Button.TextBesideIcon
-
- Base.HButton {
- id: acceptButton
- text: qsTr("Accept")
- iconName: "accept"
- icon.color: Qt.hsla(0.45, 0.9, 0.3, 1)
- display: inviteButtons.displayMode
-
- Layout.maximumWidth: inviteButtons.compact ? height : -1
- Layout.fillHeight: true
- }
-
- Base.HButton {
- id: declineButton
- text: qsTr("Decline")
- iconName: "decline"
- icon.color: Qt.hsla(0.95, 0.9, 0.35, 1)
- icon.width: 32
- display: inviteButtons.displayMode
-
- Layout.maximumWidth: inviteButtons.compact ? height : -1
- Layout.fillHeight: true
- }
- }
- }
-}
diff --git a/harmonyqml/components/chat/LeftBanner.qml b/harmonyqml/components/chat/LeftBanner.qml
new file mode 100644
index 00000000..65351d12
--- /dev/null
+++ b/harmonyqml/components/chat/LeftBanner.qml
@@ -0,0 +1,25 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.4
+import "../base" as Base
+import "utils.js" as ChatJS
+
+Banner {
+ property var leftEvent: null
+
+ avatarName: ChatJS.getLeftBannerAvatarName(leftEvent, chatPage.userId)
+ labelText: ChatJS.getLeftBannerText(leftEvent)
+
+ buttonModel: [
+ {
+ text: "Rejoin",
+ iconName: "join",
+ iconColor: Qt.hsla(0.13, 0.9, 0.35, 1),
+ },
+ {
+ text: "Forget",
+ iconName: "trash_can",
+ iconColor: Qt.hsla(0.95, 0.9, 0.35, 1),
+ }
+ ]
+}
diff --git a/harmonyqml/components/chat/Root.qml b/harmonyqml/components/chat/Root.qml
index cb75df4a..70206c51 100644
--- a/harmonyqml/components/chat/Root.qml
+++ b/harmonyqml/components/chat/Root.qml
@@ -9,8 +9,6 @@ ColumnLayout {
readonly property var roomInfo:
Backend.models.rooms.get(userId).getWhere("roomId", roomId)
- property bool isInvite: roomInfo.category === "Invites"
-
id: chatPage
spacing: 0
onFocusChanged: sendBox.setFocus()
@@ -20,19 +18,22 @@ ColumnLayout {
topic: roomInfo.topic
}
-
MessageList {}
-
TypingUsersBar {}
- InviteOffer {
- visible: isInvite
+ InviteBanner {
+ visible: roomInfo.category === "Invites"
inviter: roomInfo.inviter
}
SendBox {
id: sendBox
- visible: ! isInvite
+ visible: roomInfo.category === "Rooms"
+ }
+
+ LeftBanner {
+ visible: roomInfo.category === "Left"
+ leftEvent: roomInfo.leftEvent
}
}
diff --git a/harmonyqml/components/chat/utils.js b/harmonyqml/components/chat/utils.js
index 81e65bc7..50aab78d 100644
--- a/harmonyqml/components/chat/utils.js
+++ b/harmonyqml/components/chat/utils.js
@@ -151,6 +151,51 @@ function getMemberEventText(dict) {
}
+function getLeftBannerText(leftEvent) {
+ if (! leftEvent) {
+ return "You are not member of this room."
+ }
+
+ console.log(JSON.stringify(leftEvent, null, 4))
+
+ var info = leftEvent.content
+ var prev = leftEvent.prev_content
+ var reason = info.reason ? (" Reason: " + info.reason) : ""
+
+ if (leftEvent.state_key === leftEvent.sender) {
+ return (prev && prev.membership === "invite" ?
+ "You declined to join this room." : "You left the room.") +
+ reason
+ }
+
+ if (info.membership)
+
+ var name = Backend.getUserDisplayName(leftEvent.sender, false).result()
+
+ return "" + name + " " +
+ (info.membership == "ban" ?
+ "banned you from the room." :
+
+ prev && prev.membership === "invite" ?
+ "canceled your invitation." :
+
+ prev && prev.membership == "ban" ?
+ "unbanned you from the room." :
+
+ "kicked you out of the room.") +
+ reason
+}
+
+
+function getLeftBannerAvatarName(leftEvent, accountId) {
+ if (! leftEvent || leftEvent.state_key == leftEvent.sender) {
+ return Backend.getUserDisplayName(accountId, false).result()
+ }
+
+ return Backend.getUserDisplayName(leftEvent.sender, false).result()
+}
+
+
function getTypingUsersText(users, ourAccountId) {
var names = []
diff --git a/harmonyqml/icons/join.svg b/harmonyqml/icons/join.svg
new file mode 100644
index 00000000..c1224e13
--- /dev/null
+++ b/harmonyqml/icons/join.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/harmonyqml/icons/trash_can.svg b/harmonyqml/icons/trash_can.svg
new file mode 100644
index 00000000..107485c1
--- /dev/null
+++ b/harmonyqml/icons/trash_can.svg
@@ -0,0 +1 @@
+
\ No newline at end of file