Start rewriting backend with pyotherside+asyncio
This commit is contained in:
90
src/qml/Chat/Banners/Banner.qml
Normal file
90
src/qml/Chat/Banners/Banner.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
id: banner
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
|
||||
property alias avatar: bannerAvatar
|
||||
property alias icon: bannerIcon
|
||||
property alias labelText: bannerLabel.text
|
||||
property alias buttonModel: bannerRepeater.model
|
||||
property var buttonCallbacks: []
|
||||
|
||||
HRowLayout {
|
||||
id: bannerRow
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: bannerAvatar
|
||||
dimension: banner.Layout.preferredHeight
|
||||
}
|
||||
|
||||
HIcon {
|
||||
id: bannerIcon
|
||||
dimension: bannerLabel.implicitHeight
|
||||
visible: Boolean(svgName)
|
||||
|
||||
Layout.leftMargin: 5
|
||||
}
|
||||
|
||||
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: 5
|
||||
Layout.rightMargin: Layout.leftMargin
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
HRowLayout {
|
||||
id: bannerButtons
|
||||
|
||||
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()
|
||||
|
||||
Repeater {
|
||||
id: bannerRepeater
|
||||
model: []
|
||||
|
||||
HButton {
|
||||
id: button
|
||||
text: modelData.text
|
||||
iconName: modelData.iconName
|
||||
onClicked: buttonCallbacks[modelData.name](button)
|
||||
|
||||
Layout.maximumWidth: bannerButtons.compact ? height : -1
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
src/qml/Chat/Banners/InviteBanner.qml
Normal file
41
src/qml/Chat/Banners/InviteBanner.qml
Normal file
@@ -0,0 +1,41 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
Banner {
|
||||
property var inviter: null
|
||||
|
||||
color: HStyle.chat.inviteBanner.background
|
||||
|
||||
avatar.name: inviter ? inviter.displayname : ""
|
||||
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
|
||||
|
||||
labelText:
|
||||
(inviter ?
|
||||
("<b>" + inviter.displayname + "</b>") : qsTr("Someone")) +
|
||||
" " + qsTr("invited you to join the room.")
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "accept",
|
||||
text: qsTr("Accept"),
|
||||
iconName: "invite_accept",
|
||||
},
|
||||
{
|
||||
name: "decline",
|
||||
text: qsTr("Decline"),
|
||||
iconName: "invite_decline",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"accept": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).joinRoom(chatPage.roomId)
|
||||
},
|
||||
|
||||
"decline": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).leaveRoom(chatPage.roomId)
|
||||
}
|
||||
}
|
||||
}
|
28
src/qml/Chat/Banners/LeftBanner.qml
Normal file
28
src/qml/Chat/Banners/LeftBanner.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Banner {
|
||||
property var leftEvent: null
|
||||
|
||||
color: HStyle.chat.leftBanner.background
|
||||
|
||||
avatar.name: ChatJS.getLeftBannerAvatarName(leftEvent, chatPage.userId)
|
||||
labelText: ChatJS.getLeftBannerText(leftEvent)
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "forget",
|
||||
text: qsTr("Forget"),
|
||||
iconName: "forget_room",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"forget": function(button) {
|
||||
button.loading = true
|
||||
Backend.clients.get(chatPage.userId).forgetRoom(chatPage.roomId)
|
||||
pageStack.clear()
|
||||
},
|
||||
}
|
||||
}
|
25
src/qml/Chat/Banners/UnknownDevicesBanner.qml
Normal file
25
src/qml/Chat/Banners/UnknownDevicesBanner.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Banner {
|
||||
color: HStyle.chat.unknownDevices.background
|
||||
|
||||
avatar.visible: false
|
||||
icon.svgName: "unknown_devices_warning"
|
||||
labelText: "Unknown devices are present in this encrypted room."
|
||||
|
||||
buttonModel: [
|
||||
{
|
||||
name: "inspect",
|
||||
text: qsTr("Inspect"),
|
||||
iconName: "unknown_devices_inspect",
|
||||
}
|
||||
]
|
||||
|
||||
buttonCallbacks: {
|
||||
"inspect": function(button) {
|
||||
print("show")
|
||||
},
|
||||
}
|
||||
}
|
148
src/qml/Chat/Chat.qml
Normal file
148
src/qml/Chat/Chat.qml
Normal file
@@ -0,0 +1,148 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
import "Banners"
|
||||
import "RoomEventList"
|
||||
import "RoomSidePane"
|
||||
|
||||
HColumnLayout {
|
||||
property string userId: ""
|
||||
property string category: ""
|
||||
property string roomId: ""
|
||||
|
||||
readonly property var roomInfo:
|
||||
Backend.accounts.get(userId)
|
||||
.roomCategories.get(category)
|
||||
.rooms.get(roomId)
|
||||
|
||||
readonly property var sender: Backend.users.get(userId)
|
||||
|
||||
readonly property bool hasUnknownDevices:
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
RoomHeader {
|
||||
id: roomHeader
|
||||
displayName: roomInfo.displayName
|
||||
topic: roomInfo.topic || ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.avatar.size
|
||||
}
|
||||
|
||||
|
||||
HSplitView {
|
||||
id: chatSplitView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RoomEventList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
TypingMembersBar {}
|
||||
|
||||
InviteBanner {
|
||||
visible: category === "Invites"
|
||||
inviter: roomInfo.inviter
|
||||
}
|
||||
|
||||
UnknownDevicesBanner {
|
||||
visible: category == "Rooms" && hasUnknownDevices
|
||||
}
|
||||
|
||||
SendBox {
|
||||
id: sendBox
|
||||
visible: category == "Rooms" && ! hasUnknownDevices
|
||||
}
|
||||
|
||||
LeftBanner {
|
||||
visible: category === "Left"
|
||||
leftEvent: roomInfo.leftEvent
|
||||
}
|
||||
}
|
||||
|
||||
RoomSidePane {
|
||||
id: roomSidePane
|
||||
|
||||
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
|
||||
|
||||
onStarted: {
|
||||
target.oldWidth = target.width
|
||||
target.Layout.minimumWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
9
src/qml/Chat/RoomEventList/Daybreak.qml
Normal file
9
src/qml/Chat/RoomEventList/Daybreak.qml
Normal file
@@ -0,0 +1,9 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: dateTime.toLocaleDateString()
|
||||
color: HStyle.chat.daybreak.foreground
|
||||
backgroundColor: HStyle.chat.daybreak.background
|
||||
radius: HStyle.chat.daybreak.radius
|
||||
}
|
53
src/qml/Chat/RoomEventList/EventContent.qml
Normal file
53
src/qml/Chat/RoomEventList/EventContent.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Row {
|
||||
id: eventContent
|
||||
spacing: standardSpacing / 2
|
||||
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
width: Math.min(
|
||||
roomEventListView.width - avatar.width - eventContent.spacing,
|
||||
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||
contentLabel.implicitWidth
|
||||
)
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: sender.displayName.value
|
||||
hidden: combine
|
||||
dimension: 28
|
||||
}
|
||||
|
||||
HLabel {
|
||||
width: parent.width
|
||||
|
||||
id: contentLabel
|
||||
text: "<font color='" +
|
||||
Qt.hsla(Backend.hueFromString(sender.displayName.value),
|
||||
HStyle.chat.event.saturation,
|
||||
HStyle.chat.event.lightness,
|
||||
1) +
|
||||
"'>" +
|
||||
sender.displayName.value + " " +
|
||||
ChatJS.getEventText(type, dict) +
|
||||
|
||||
" " +
|
||||
"<font size=" + HStyle.fontSize.small + "px " +
|
||||
"color=" + HStyle.chat.event.date + ">" +
|
||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||
"</font> " +
|
||||
"</font>"
|
||||
|
||||
textFormat: Text.RichText
|
||||
background: Rectangle {color: HStyle.chat.event.background}
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: verticalPadding
|
||||
bottomPadding: verticalPadding
|
||||
}
|
||||
}
|
80
src/qml/Chat/RoomEventList/MessageContent.qml
Normal file
80
src/qml/Chat/RoomEventList/MessageContent.qml
Normal file
@@ -0,0 +1,80 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
Row {
|
||||
id: messageContent
|
||||
spacing: standardSpacing / 2
|
||||
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
hidden: combine
|
||||
name: sender.displayName.value
|
||||
dimension: 48
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: HStyle.chat.message.background
|
||||
|
||||
//width: nameLabel.implicitWidth
|
||||
width: Math.min(
|
||||
roomEventListView.width - avatar.width - messageContent.spacing,
|
||||
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||
Math.max(
|
||||
nameLabel.visible ? nameLabel.implicitWidth : 0,
|
||||
contentLabel.implicitWidth
|
||||
)
|
||||
)
|
||||
height: nameLabel.height + contentLabel.implicitHeight
|
||||
|
||||
Column {
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
|
||||
HLabel {
|
||||
height: combine ? 0 : implicitHeight
|
||||
width: parent.width
|
||||
visible: height > 0
|
||||
|
||||
id: nameLabel
|
||||
text: sender.displayName.value
|
||||
color: Qt.hsla(Backend.hueFromString(text),
|
||||
HStyle.displayName.saturation,
|
||||
HStyle.displayName.lightness,
|
||||
1)
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: isOwn ? Text.AlignRight : Text.AlignLeft
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: verticalPadding
|
||||
}
|
||||
|
||||
HRichLabel {
|
||||
width: parent.width
|
||||
|
||||
id: contentLabel
|
||||
text: (dict.formatted_body ?
|
||||
Backend.htmlFilter.filter(dict.formatted_body) :
|
||||
dict.body) +
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px color=" + HStyle.chat.message.date + ">" +
|
||||
Qt.formatDateTime(dateTime, "hh:mm:ss") +
|
||||
"</font>" +
|
||||
(isLocalEcho ?
|
||||
" <font size=" + HStyle.fontSize.small +
|
||||
"px>⏳</font>" : "")
|
||||
textFormat: Text.RichText
|
||||
color: HStyle.chat.message.body
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
leftPadding: horizontalPadding
|
||||
rightPadding: horizontalPadding
|
||||
topPadding: nameLabel.visible ? 0 : verticalPadding
|
||||
bottomPadding: verticalPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
src/qml/Chat/RoomEventList/RoomEventDelegate.qml
Normal file
88
src/qml/Chat/RoomEventList/RoomEventDelegate.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
import "../utils.js" as ChatJS
|
||||
|
||||
Column {
|
||||
id: roomEventDelegate
|
||||
|
||||
function minsBetween(date1, date2) {
|
||||
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
|
||||
}
|
||||
|
||||
property var previousItem: getPreviousItem()
|
||||
signal reloadPreviousItem()
|
||||
onReloadPreviousItem: previousItem = getPreviousItem()
|
||||
|
||||
readonly property bool isMessage: getIsMessage(type)
|
||||
|
||||
readonly property bool isUndecryptableEvent:
|
||||
type === "OlmEvent" || type === "MegolmEvent"
|
||||
|
||||
readonly property var sender: Backend.users.get(dict.sender)
|
||||
|
||||
readonly property bool isOwn:
|
||||
chatPage.userId === dict.sender
|
||||
|
||||
readonly property bool isFirstEvent: type == "RoomCreateEvent"
|
||||
|
||||
readonly property bool combine:
|
||||
previousItem &&
|
||||
! talkBreak &&
|
||||
! dayBreak &&
|
||||
getIsMessage(previousItem.type) === isMessage &&
|
||||
previousItem.dict.sender === dict.sender &&
|
||||
minsBetween(previousItem.dateTime, dateTime) <= 5
|
||||
|
||||
readonly property bool dayBreak:
|
||||
isFirstEvent ||
|
||||
previousItem &&
|
||||
dateTime.getDate() != previousItem.dateTime.getDate()
|
||||
|
||||
readonly property bool talkBreak:
|
||||
previousItem &&
|
||||
! dayBreak &&
|
||||
minsBetween(previousItem.dateTime, dateTime) >= 20
|
||||
|
||||
|
||||
property int standardSpacing: 16
|
||||
property int horizontalPadding: 6
|
||||
property int verticalPadding: 4
|
||||
|
||||
ListView.onAdd: {
|
||||
var nextDelegate = roomEventListView.contentItem.children[index]
|
||||
if (nextDelegate) { nextDelegate.reloadPreviousItem() }
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
|
||||
topPadding:
|
||||
isFirstEvent ? 0 :
|
||||
dayBreak ? standardSpacing * 2 :
|
||||
talkBreak ? standardSpacing * 3 :
|
||||
combine ? standardSpacing / 4 :
|
||||
standardSpacing
|
||||
|
||||
Loader {
|
||||
source: dayBreak ? "Daybreak.qml" : ""
|
||||
width: roomEventDelegate.width
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: dayBreak
|
||||
width: parent.width
|
||||
height: topPadding
|
||||
}
|
||||
|
||||
Loader {
|
||||
source: isMessage ? "MessageContent.qml" : "EventContent.qml"
|
||||
anchors.right: isOwn ? parent.right : undefined
|
||||
}
|
||||
}
|
43
src/qml/Chat/RoomEventList/RoomEventList.qml
Normal file
43
src/qml/Chat/RoomEventList/RoomEventList.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
import QtQuick 2.7
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
property int space: 8
|
||||
|
||||
color: HStyle.chat.roomEventList.background
|
||||
|
||||
HListView {
|
||||
id: roomEventListView
|
||||
delegate: RoomEventDelegate {}
|
||||
model: Backend.roomEvents.get(chatPage.roomId)
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: space
|
||||
anchors.rightMargin: space
|
||||
|
||||
topMargin: space
|
||||
bottomMargin: space
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
// Keep x scroll pages cached, to limit images having to be
|
||||
// reloaded from network.
|
||||
cacheBuffer: height * 6
|
||||
|
||||
// Declaring this "alias" provides the on... signal
|
||||
property real yPos: visibleArea.yPosition
|
||||
|
||||
onYPosChanged: {
|
||||
if (chatPage.category != "Invites" && yPos <= 0.1) {
|
||||
Backend.loadPastEvents(chatPage.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HNoticePage {
|
||||
text: qsTr("Nothing to show here yet...")
|
||||
|
||||
visible: roomEventListView.model.count < 1
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
102
src/qml/Chat/RoomHeader.qml
Normal file
102
src/qml/Chat/RoomHeader.qml
Normal file
@@ -0,0 +1,102 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
property string displayName: ""
|
||||
property string topic: ""
|
||||
|
||||
property alias buttonsImplicitWidth: viewButtons.implicitWidth
|
||||
property int buttonsWidth: viewButtons.Layout.preferredWidth
|
||||
property var activeButton: "members"
|
||||
|
||||
property bool collapseButtons: width < 400
|
||||
|
||||
id: roomHeader
|
||||
color: HStyle.chat.roomHeader.background
|
||||
|
||||
HRowLayout {
|
||||
id: row
|
||||
spacing: 8
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: displayName
|
||||
dimension: roomHeader.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: roomName
|
||||
text: displayName
|
||||
font.pixelSize: HStyle.fontSize.big
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: Math.max(
|
||||
0,
|
||||
row.width - row.totalSpacing - avatar.width -
|
||||
viewButtons.width -
|
||||
(expandButton.visible ? expandButton.width : 0)
|
||||
)
|
||||
}
|
||||
|
||||
HLabel {
|
||||
id: roomTopic
|
||||
text: topic
|
||||
font.pixelSize: HStyle.fontSize.small
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Layout.maximumWidth: Math.max(
|
||||
0,
|
||||
row.width - row.totalSpacing - avatar.width -
|
||||
roomName.width - viewButtons.width -
|
||||
(expandButton.visible ? expandButton.width : 0)
|
||||
)
|
||||
}
|
||||
|
||||
HSpacer {}
|
||||
|
||||
Row {
|
||||
id: viewButtons
|
||||
Layout.preferredWidth: collapseButtons ? 0 : implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
"members", "files", "notifications", "history", "settings"
|
||||
]
|
||||
HButton {
|
||||
iconName: "room_view_" + modelData
|
||||
iconDimension: 22
|
||||
autoExclusive: true
|
||||
checked: activeButton == modelData
|
||||
onClicked: activeButton = activeButton == modelData ?
|
||||
null : modelData
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
id: buttonsAnimation
|
||||
duration: HStyle.animationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HButton {
|
||||
id: expandButton
|
||||
z: 1
|
||||
anchors.right: parent.right
|
||||
opacity: collapseButtons ? 1 : 0
|
||||
visible: opacity > 0
|
||||
iconName: "reduced_room_buttons"
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: buttonsAnimation.duration * 2 }
|
||||
}
|
||||
}
|
||||
}
|
37
src/qml/Chat/RoomSidePane/MemberDelegate.qml
Normal file
37
src/qml/Chat/RoomSidePane/MemberDelegate.qml
Normal file
@@ -0,0 +1,37 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
MouseArea {
|
||||
id: memberDelegate
|
||||
width: memberList.width
|
||||
height: childrenRect.height
|
||||
|
||||
property var member: Backend.users.get(userId)
|
||||
|
||||
HRowLayout {
|
||||
width: parent.width
|
||||
spacing: memberList.spacing
|
||||
|
||||
HAvatar {
|
||||
id: memberAvatar
|
||||
name: member.displayName.value
|
||||
}
|
||||
|
||||
HColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth:
|
||||
parent.width - parent.totalSpacing - memberAvatar.width
|
||||
|
||||
HLabel {
|
||||
id: memberName
|
||||
text: member.displayName.value
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
Layout.maximumWidth: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
src/qml/Chat/RoomSidePane/MembersView.qml
Normal file
49
src/qml/Chat/RoomSidePane/MembersView.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HColumnLayout {
|
||||
property bool collapsed: false
|
||||
property int normalSpacing: collapsed ? 0 : 8
|
||||
|
||||
Behavior on normalSpacing {
|
||||
NumberAnimation { duration: HStyle.animationDuration }
|
||||
}
|
||||
|
||||
HListView {
|
||||
id: memberList
|
||||
|
||||
spacing: normalSpacing
|
||||
topMargin: normalSpacing
|
||||
bottomMargin: normalSpacing
|
||||
Layout.leftMargin: normalSpacing
|
||||
Layout.rightMargin: normalSpacing
|
||||
|
||||
model: chatPage.roomInfo.sortedMembers
|
||||
delegate: MemberDelegate {}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
}
|
||||
|
||||
HTextField {
|
||||
id: filterField
|
||||
placeholderText: qsTr("Filter members")
|
||||
backgroundColor: HStyle.sidePane.filterRooms.background
|
||||
|
||||
// Without this, if the user types in the field, changes of room, then
|
||||
// comes back, the field will be empty but the filter still applied.
|
||||
Component.onCompleted:
|
||||
text = Backend.clients.get(chatPage.userId).getMemberFilter(
|
||||
chatPage.category, chatPage.roomId
|
||||
)
|
||||
|
||||
onTextChanged: Backend.clients.get(chatPage.userId).setMemberFilter(
|
||||
chatPage.category, chatPage.roomId, text
|
||||
)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: HStyle.bottomElementsHeight
|
||||
}
|
||||
}
|
15
src/qml/Chat/RoomSidePane/RoomSidePane.qml
Normal file
15
src/qml/Chat/RoomSidePane/RoomSidePane.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../Base"
|
||||
|
||||
HRectangle {
|
||||
id: roomSidePane
|
||||
|
||||
property bool collapsed: false
|
||||
property var activeView: null
|
||||
|
||||
MembersView {
|
||||
anchors.fill: parent
|
||||
collapsed: parent.collapsed
|
||||
}
|
||||
}
|
72
src/qml/Chat/SendBox.qml
Normal file
72
src/qml/Chat/SendBox.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
|
||||
HRectangle {
|
||||
function setFocus() { textArea.forceActiveFocus() }
|
||||
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: HStyle.bottomElementsHeight
|
||||
Layout.preferredHeight: textArea.implicitHeight
|
||||
// parent.height / 2 causes binding loop?
|
||||
Layout.maximumHeight: pageStack.height / 2
|
||||
color: HStyle.chat.sendBox.background
|
||||
|
||||
HRowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
HAvatar {
|
||||
id: avatar
|
||||
name: chatPage.sender.displayName.value
|
||||
dimension: root.Layout.minimumHeight
|
||||
}
|
||||
|
||||
HScrollableTextArea {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: textArea
|
||||
placeholderText: qsTr("Type a message...")
|
||||
backgroundColor: "transparent"
|
||||
area.focus: true
|
||||
|
||||
property bool textChangedSinceLostFocus: false
|
||||
|
||||
function setTyping(typing) {
|
||||
Backend.clients.get(chatPage.userId)
|
||||
.setTypingState(chatPage.roomId, typing)
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
setTyping(Boolean(text))
|
||||
textChangedSinceLostFocus = true
|
||||
}
|
||||
area.onEditingFinished: { // when lost focus
|
||||
if (text && textChangedSinceLostFocus) {
|
||||
setTyping(false)
|
||||
textChangedSinceLostFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
event.accepted = true
|
||||
|
||||
if (event.modifiers & Qt.ShiftModifier ||
|
||||
event.modifiers & Qt.ControlModifier ||
|
||||
event.modifiers & Qt.AltModifier) {
|
||||
textArea.insert(textArea.cursorPosition, "\n")
|
||||
return
|
||||
}
|
||||
|
||||
if (textArea.text === "") { return }
|
||||
Backend.clients.get(chatPage.userId)
|
||||
.sendMarkdown(chatPage.roomId, textArea.text)
|
||||
area.clear()
|
||||
}
|
||||
|
||||
// Numpad enter
|
||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||
}
|
||||
}
|
||||
}
|
23
src/qml/Chat/TypingMembersBar.qml
Normal file
23
src/qml/Chat/TypingMembersBar.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../Base"
|
||||
import "utils.js" as ChatJS
|
||||
|
||||
HRectangle {
|
||||
property var typingMembers: chatPage.roomInfo.typingMembers
|
||||
|
||||
color: HStyle.chat.typingMembers.background
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0
|
||||
Layout.maximumHeight: Layout.minimumHeight
|
||||
|
||||
HLabel {
|
||||
id: usersLabel
|
||||
anchors.fill: parent
|
||||
|
||||
text: ChatJS.getTypingMembersText(typingMembers, chatPage.userId)
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
210
src/qml/Chat/utils.js
Normal file
210
src/qml/Chat/utils.js
Normal file
@@ -0,0 +1,210 @@
|
||||
function getEventText(type, dict) {
|
||||
switch (type) {
|
||||
case "RoomCreateEvent":
|
||||
return (dict.federate ? "allowed" : "blocked") +
|
||||
" users on other matrix servers " +
|
||||
(dict.federate ? "to join" : "from joining") +
|
||||
" this room."
|
||||
break
|
||||
|
||||
case "RoomGuestAccessEvent":
|
||||
return (dict.guest_access === "can_join" ? "allowed " : "forbad") +
|
||||
"guests to join the room."
|
||||
break
|
||||
|
||||
case "RoomJoinRulesEvent":
|
||||
return "made the room " +
|
||||
(dict.join_rule === "public." ? "public" : "invite only.")
|
||||
break
|
||||
|
||||
case "RoomHistoryVisibilityEvent":
|
||||
return getHistoryVisibilityEventText(dict)
|
||||
break
|
||||
|
||||
case "PowerLevelsEvent":
|
||||
return "changed the room's permissions."
|
||||
|
||||
case "RoomMemberEvent":
|
||||
return getMemberEventText(dict)
|
||||
break
|
||||
|
||||
case "RoomAliasEvent":
|
||||
return "set the room's main address to " +
|
||||
dict.canonical_alias + "."
|
||||
break
|
||||
|
||||
case "RoomNameEvent":
|
||||
return "changed the room's name to \"" + dict.name + "\"."
|
||||
break
|
||||
|
||||
case "RoomTopicEvent":
|
||||
return "changed the room's topic to \"" + dict.topic + "\"."
|
||||
break
|
||||
|
||||
case "RoomEncryptionEvent":
|
||||
return "turned on encryption for this room."
|
||||
break
|
||||
|
||||
case "OlmEvent":
|
||||
case "MegolmEvent":
|
||||
return "hasn't sent your device the keys to decrypt this message."
|
||||
|
||||
default:
|
||||
console.log(type + "\n" + JSON.stringify(dict, null, 4) + "\n")
|
||||
return "did something this client does not understand."
|
||||
|
||||
//case "CallEvent": TODO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getHistoryVisibilityEventText(dict) {
|
||||
switch (dict.history_visibility) {
|
||||
case "shared":
|
||||
var end = "all room members."
|
||||
break
|
||||
|
||||
case "world_readable":
|
||||
var end = "any member or outsider."
|
||||
break
|
||||
|
||||
case "joined":
|
||||
var end = "all room members, since the point they joined."
|
||||
break
|
||||
|
||||
case "invited":
|
||||
var end = "all room members, since the point they were invited."
|
||||
break
|
||||
}
|
||||
|
||||
return "made future history visible to " + end
|
||||
}
|
||||
|
||||
|
||||
function getStateDisplayName(dict) {
|
||||
// The dict.content.displayname may be outdated, prefer
|
||||
// retrieving it fresh
|
||||
return Backend.users.get(dict.state_key).displayName.value
|
||||
}
|
||||
|
||||
|
||||
function getMemberEventText(dict) {
|
||||
var info = dict.content, prev = dict.prev_content
|
||||
|
||||
if (! prev || (info.membership != prev.membership)) {
|
||||
var reason = info.reason ? (" Reason: " + info.reason) : ""
|
||||
|
||||
switch (info.membership) {
|
||||
case "join":
|
||||
return prev && prev.membership === "invite" ?
|
||||
"accepted the invitation." : "joined the room."
|
||||
break
|
||||
|
||||
case "invite":
|
||||
return "invited " + getStateDisplayName(dict) + " to the room."
|
||||
break
|
||||
|
||||
case "leave":
|
||||
if (dict.state_key === dict.sender) {
|
||||
return (prev && prev.membership === "invite" ?
|
||||
"declined the invitation." : "left the room.") +
|
||||
reason
|
||||
}
|
||||
|
||||
var name = getStateDisplayName(dict)
|
||||
return (prev && prev.membership === "invite" ?
|
||||
"withdrew " + name + "'s invitation." :
|
||||
|
||||
prev && prev.membership == "ban" ?
|
||||
"unbanned " + name + " from the room." :
|
||||
|
||||
"kicked out " + name + " from the room.") +
|
||||
reason
|
||||
break
|
||||
|
||||
case "ban":
|
||||
var name = getStateDisplayName(dict)
|
||||
return "banned " + name + " from the room." + reason
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var changed = []
|
||||
|
||||
if (prev && (info.avatar_url != prev.avatar_url)) {
|
||||
changed.push("profile picture")
|
||||
}
|
||||
|
||||
if (prev && (info.displayname != prev.displayname)) {
|
||||
changed.push("display name from \"" +
|
||||
(prev.displayname || dict.state_key) + '" to "' +
|
||||
(info.displayname || dict.state_key) + '"')
|
||||
}
|
||||
|
||||
if (changed.length > 0) {
|
||||
return "changed their " + changed.join(" and ") + "."
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
function getLeftBannerText(leftEvent) {
|
||||
if (! leftEvent) {
|
||||
return "You are not member of this room."
|
||||
}
|
||||
|
||||
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 the room." : "You left the room.") +
|
||||
reason
|
||||
}
|
||||
|
||||
if (info.membership)
|
||||
|
||||
var name = Backend.users.get(leftEvent.sender).displayName.value
|
||||
|
||||
return "<b>" + name + "</b> " +
|
||||
(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.users.get(accountId).displayName.value
|
||||
}
|
||||
|
||||
return Backend.users.get(leftEvent.sender).displayName.value
|
||||
}
|
||||
|
||||
|
||||
function getTypingMembersText(users, ourAccountId) {
|
||||
var names = []
|
||||
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
if (users[i] !== ourAccountId) {
|
||||
names.push(Backend.users.get(users[i]).displayName.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (names.length < 1) { return "" }
|
||||
|
||||
return "🖋 " +
|
||||
[names.slice(0, -1).join(", "), names.slice(-1)[0]]
|
||||
.join(names.length < 2 ? "" : " and ") +
|
||||
(names.length > 1 ? " are" : " is") + " typing…"
|
||||
}
|
Reference in New Issue
Block a user