Start rewriting backend with pyotherside+asyncio

This commit is contained in:
miruka
2019-06-27 02:31:03 -04:00
parent f530f51937
commit 3344debbbf
128 changed files with 715 additions and 2941 deletions

45
src/qml/Base/HAvatar.qml Normal file
View File

@@ -0,0 +1,45 @@
import QtQuick 2.7
import "../Base"
Rectangle {
property var name: null
property var imageUrl: null
property int dimension: HStyle.avatar.size
property bool hidden: false
width: dimension
height: hidden ? 1 : dimension
implicitWidth: dimension
implicitHeight: hidden ? 1 : dimension
opacity: hidden ? 0 : 1
color: name ?
Qt.hsla(
Backend.hueFromString(name),
HStyle.avatar.background.saturation,
HStyle.avatar.background.lightness,
HStyle.avatar.background.alpha
) :
HStyle.avatar.background.unknown
HLabel {
z: 1
anchors.centerIn: parent
visible: ! hidden
text: name ? name.charAt(0) : "?"
color: HStyle.avatar.letter
font.pixelSize: parent.height / 1.4
}
HImage {
z: 2
anchors.fill: parent
visible: ! hidden && imageUrl
Component.onCompleted: if (imageUrl) { source = imageUrl }
fillMode: Image.PreserveAspectCrop
sourceSize.width: dimension
}
}

138
src/qml/Base/HButton.qml Normal file
View File

@@ -0,0 +1,138 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
Button {
property int horizontalMargin: 0
property int verticalMargin: 0
property string iconName: ""
property var iconDimension: null
property var iconTransform: null
property bool circle: false
property int fontSize: HStyle.fontSize.normal
property color backgroundColor: HStyle.controls.button.background
property alias overlayOpacity: buttonBackgroundOverlay.opacity
property bool checkedLightens: false
property bool loading: false
property int contentWidth: 0
readonly property alias visibility: button.visible
onVisibilityChanged: if (! visibility) { loading = false }
signal canceled
signal clicked
signal doubleClicked
signal entered
signal exited
signal pressAndHold
signal pressed
signal released
function loadingUntilFutureDone(future) {
loading = true
future.onGotResult.connect(function() { loading = false })
}
id: button
background: Rectangle {
id: buttonBackground
color: Qt.lighter(
backgroundColor, checked ? (checkedLightens ? 1.3 : 0.7) : 1.0
)
radius: circle ? height : 0
Behavior on color {
ColorAnimation { duration: HStyle.animationDuration / 2 }
}
Rectangle {
id: buttonBackgroundOverlay
anchors.fill: parent
radius: parent.radius
color: "black"
opacity: 0
Behavior on opacity {
NumberAnimation { duration: HStyle.animationDuration / 2 }
}
}
}
Component {
id: buttonContent
HRowLayout {
id: contentLayout
spacing: button.text && iconName ? 5 : 0
Component.onCompleted: contentWidth = implicitWidth
HIcon {
svgName: loading ? "hourglass" : iconName
dimension: iconDimension || contentLayout.height
transform: iconTransform
Layout.topMargin: verticalMargin
Layout.bottomMargin: verticalMargin
Layout.leftMargin: horizontalMargin
Layout.rightMargin: horizontalMargin
}
HLabel {
text: button.text
font.pixelSize: fontSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
Component {
id: loadingOverlay
HRowLayout {
HIcon {
svgName: "hourglass"
Layout.preferredWidth: contentWidth || -1
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
contentItem: Loader {
sourceComponent:
loading && ! iconName ? loadingOverlay : buttonContent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onCanceled: button.canceled()
onClicked: button.clicked()
onDoubleClicked: button.doubleClicked()
onEntered: {
overlayOpacity = checked ? 0 : 0.15
button.entered()
}
onExited: {
overlayOpacity = 0
button.exited()
}
onPressAndHold: button.pressAndHold()
onPressed: {
overlayOpacity += 0.15
button.pressed()
}
onReleased: {
if (checkable) { checked = ! checked }
overlayOpacity = checked ? 0 : 0.15
button.released()
}
}
}

View File

@@ -0,0 +1,11 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ColumnLayout {
id: columnLayout
spacing: 0
property int totalSpacing:
spacing * Math.max(0, (columnLayout.visibleChildren.length - 1))
}

10
src/qml/Base/HIcon.qml Normal file
View File

@@ -0,0 +1,10 @@
import QtQuick 2.7
HImage {
property var svgName: null
property int dimension: 20
source: "../../icons/" + (svgName || "none") + ".svg"
sourceSize.width: svgName ? dimension : 0
sourceSize.height: svgName ? dimension : 0
}

8
src/qml/Base/HImage.qml Normal file
View File

@@ -0,0 +1,8 @@
import QtQuick 2.7
Image {
asynchronous: true
cache: true
mipmap: true
fillMode: Image.PreserveAspectFit
}

View File

@@ -0,0 +1,60 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
HScalingBox {
id: interfaceBox
property alias title: interfaceTitle.text
property alias buttonModel: interfaceButtonsRepeater.model
property var buttonCallbacks: []
property string enterButtonTarget: ""
default property alias body: interfaceBody.children
function clickEnterButtonTarget() {
for (var i = 0; i < buttonModel.length; i++) {
var btn = interfaceButtonsRepeater.itemAt(i)
if (btn.name === enterButtonTarget) { btn.clicked() }
}
}
HColumnLayout {
anchors.fill: parent
id: mainColumn
HRowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.margins: interfaceBox.margins
HLabel {
id: interfaceTitle
font.pixelSize: HStyle.fontSize.big
}
}
HSpacer {}
HColumnLayout { id: interfaceBody }
HSpacer {}
HRowLayout {
Repeater {
id: interfaceButtonsRepeater
model: []
HButton {
property string name: modelData.name
id: button
text: modelData.text
iconName: modelData.iconName || ""
onClicked: buttonCallbacks[modelData.name](button)
Layout.fillWidth: true
Layout.preferredHeight: HStyle.avatar.size
}
}
}
}
}

11
src/qml/Base/HLabel.qml Normal file
View File

@@ -0,0 +1,11 @@
import QtQuick.Controls 2.2
Label {
font.family: HStyle.fontFamily.sans
font.pixelSize: HStyle.fontSize.normal
textFormat: Label.PlainText
color: HStyle.colors.foreground
style: Label.Outline
styleColor: HStyle.colors.textBorder
}

View File

@@ -0,0 +1,57 @@
import QtQuick 2.7
ListModel {
// To initialize a HListModel with items,
// use `Component.onCompleted: extend([{"foo": 1, "bar": 2}, ...])`
id: listModel
function extend(new_items) {
for (var i = 0; i < new_items.length; i++) {
listModel.append(new_items[i])
}
}
function getIndices(where_role, is, max) { // max: undefined or int
var results = []
for (var i = 0; i < listModel.count; i++) {
if (listModel.get(i)[where_role] == is) {
results.push(i)
if (max && results.length >= max) {
break
}
}
}
return results
}
function getWhere(where_role, is, max) {
var indices = getIndices(where_role, is, max)
var results = []
for (var i = 0; i < indices.length; i++) {
results.push(listModel.get(indices[i]))
}
return results
}
function upsert(where_role, is, new_item) {
// new_item can contain only the keys we're interested in updating
var indices = getIndices(where_role, is, 1)
if (indices.length == 0) {
listModel.append(new_item)
} else {
listModel.set(indices[0], new_item)
}
}
function pop(index) {
var item = listModel.get(index)
listModel.remove(index)
return item
}
}

View File

@@ -0,0 +1,24 @@
import QtQuick 2.7
ListView {
property int duration: HStyle.animationDuration
add: Transition {
NumberAnimation { properties: "x,y"; from: 100; duration: duration }
}
move: Transition {
NumberAnimation { properties: "x,y"; duration: duration }
}
displaced: Transition {
NumberAnimation { properties: "x,y"; duration: duration }
}
remove: Transition {
ParallelAnimation {
NumberAnimation { property: "opacity"; to: 0; duration: duration }
NumberAnimation { properties: "x,y"; to: 100; duration: duration }
}
}
}

View File

@@ -0,0 +1,34 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HRowLayout {
property alias label: noticeLabel
property alias text: noticeLabel.text
property alias color: noticeLabel.color
property alias font: noticeLabel.font
property alias backgroundColor: noticeLabelBackground.color
property alias radius: noticeLabelBackground.radius
HLabel {
id: noticeLabel
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
padding: 3
leftPadding: 10
rightPadding: 10
Layout.margins: 10
Layout.alignment: Qt.AlignCenter
Layout.maximumWidth:
parent.width - Layout.leftMargin - Layout.rightMargin
opacity: width > Layout.leftMargin + Layout.rightMargin ? 1 : 0
background: Rectangle {
id: noticeLabelBackground
color: HStyle.box.background
radius: HStyle.box.radius
}
}
}

View File

@@ -0,0 +1,6 @@
import QtQuick 2.7
Rectangle {
id: rectangle
color: HStyle.sidePane.background
}

View File

@@ -0,0 +1,22 @@
import QtQuick 2.7
HLabel {
id: label
textFormat: Text.RichText
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPositionChanged: function (event) {
cursorShape = label.linkAt(event.x, event.y) ?
Qt.PointingHandCursor : Qt.ArrowCursor
}
onClicked: function(event) {
var link = label.linkAt(event.x, event.y)
if (link) { Qt.openUrlExternally(link) }
}
}
}

View File

@@ -0,0 +1,10 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
RowLayout {
id: rowLayout
spacing: 0
property int totalSpacing:
spacing * Math.max(0, (rowLayout.visibleChildren.length - 1))
}

View File

@@ -0,0 +1,15 @@
import QtQuick 2.7
HRectangle {
property real widthForHeight: 0.75
property int baseHeight: 300
property int startScalingUpAboveHeight: 1080
readonly property int baseWidth: baseHeight * widthForHeight
readonly property int margins: baseHeight * 0.03
color: HStyle.box.background
height: Math.min(parent.height, baseHeight)
width: Math.min(parent.width, baseWidth)
scale: Math.max(1, parent.height / startScalingUpAboveHeight)
}

View File

@@ -0,0 +1,33 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
ScrollView {
property alias backgroundColor: textAreaBackground.color
property alias placeholderText: textArea.placeholderText
property alias text: textArea.text
property alias area: textArea
default property alias textAreaData: textArea.data
id: scrollView
clip: true
TextArea {
id: textArea
readOnly: ! visible
selectByMouse: true
wrapMode: TextEdit.Wrap
font.family: HStyle.fontFamily.sans
font.pixelSize: HStyle.fontSize.normal
color: HStyle.colors.foreground
background: Rectangle {
id: textAreaBackground
color: HStyle.controls.textArea.background
}
Keys.forwardTo: [scrollView]
}
}

7
src/qml/Base/HSpacer.qml Normal file
View File

@@ -0,0 +1,7 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}

View File

@@ -0,0 +1,24 @@
import QtQuick 2.7
import QtQuick.Controls 1.4 as Controls1
//https://doc.qt.io/qt-5/qml-qtquick-controls-splitview.html
Controls1.SplitView {
id: splitView
property bool anyHovered: false
property bool anyPressed: false
property bool anyResizing: false
property bool canAutoSize: true
onAnyPressedChanged: canAutoSize = false
handleDelegate: Item {
readonly property bool hovered: styleData.hovered
readonly property bool pressed: styleData.pressed
readonly property bool resizing: styleData.resizing
onHoveredChanged: splitView.anyHovered = hovered
onPressedChanged: splitView.anyPressed = pressed
onResizingChanged: splitView.anyResizing = resizing
}
}

View File

@@ -0,0 +1,11 @@
import QtQuick 2.7
HAvatar {
HImage {
id: status
anchors.right: parent.right
anchors.bottom: parent.bottom
source: "../../icons/status.svg"
sourceSize.width: 12
}
}

139
src/qml/Base/HStyle.qml Normal file
View File

@@ -0,0 +1,139 @@
pragma Singleton
import QtQuick 2.7
QtObject {
id: style
property int animationDuration: 100
readonly property QtObject fontSize: QtObject {
property int smallest: 6
property int smaller: 8
property int small: 12
property int normal: 16
property int big: 24
property int bigger: 32
property int biggest: 48
}
readonly property QtObject fontFamily: QtObject {
property string sans: "SFNS Display"
property string serif: "Roboto Slab"
property string mono: "Hack"
}
property int radius: 5
readonly property QtObject colors: QtObject {
property color background0: Qt.hsla(0, 0, 0.8, 0.5)
property color background1: Qt.hsla(0, 0, 0.8, 0.7)
property color foreground: "black"
property color foregroundDim: Qt.hsla(0, 0, 0.2, 1)
property color foregroundError: Qt.hsla(0.95, 0.64, 0.32, 1)
property color textBorder: Qt.hsla(0, 0, 0, 0.07)
}
readonly property QtObject controls: QtObject {
readonly property QtObject button: QtObject {
property color background: colors.background1
}
readonly property QtObject textField: QtObject {
property color background: colors.background1
}
readonly property QtObject textArea: QtObject {
property color background: colors.background1
}
}
readonly property QtObject sidePane: QtObject {
property color background: colors.background1
readonly property QtObject settingsButton: QtObject {
property color background: colors.background1
}
readonly property QtObject filterRooms: QtObject {
property color background: colors.background1
}
}
readonly property QtObject chat: QtObject {
readonly property QtObject selectViewBar: QtObject {
property color background: colors.background1
}
readonly property QtObject roomHeader: QtObject {
property color background: colors.background1
}
readonly property QtObject roomEventList: QtObject {
property color background: "transparent"
}
readonly property QtObject message: QtObject {
property color background: colors.background1
property color body: colors.foreground
property color date: colors.foregroundDim
}
readonly property QtObject event: QtObject {
property color background: colors.background1
property real saturation: 0.22
property real lightness: 0.24
property color date: colors.foregroundDim
}
readonly property QtObject daybreak: QtObject {
property color background: colors.background1
property color foreground: colors.foreground
property int radius: style.radius
}
readonly property QtObject inviteBanner: QtObject {
property color background: colors.background1
}
readonly property QtObject leftBanner: QtObject {
property color background: colors.background1
}
readonly property QtObject unknownDevices: QtObject {
property color background: colors.background1
}
readonly property QtObject typingMembers: QtObject {
property color background: colors.background0
}
readonly property QtObject sendBox: QtObject {
property color background: colors.background1
}
}
readonly property QtObject box: QtObject {
property color background: colors.background0
property int radius: style.radius
}
readonly property QtObject avatar: QtObject {
property int size: 36
property int radius: style.radius
property color letter: "white"
readonly property QtObject background: QtObject {
property real saturation: 0.22
property real lightness: 0.5
property real alpha: 1
property color unknown: Qt.hsla(0, 0, 0.22, 1)
}
}
readonly property QtObject displayName: QtObject {
property real saturation: 0.32
property real lightness: 0.3
}
property int bottomElementsHeight: 32
}

View File

@@ -0,0 +1,17 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
TextField {
property alias backgroundColor: textFieldBackground.color
font.family: HStyle.fontFamily.sans
font.pixelSize: HStyle.fontSize.normal
color: HStyle.colors.foreground
background: Rectangle {
id: textFieldBackground
color: HStyle.controls.textField.background
}
selectByMouse: true
}

1
src/qml/Base/qmldir Normal file
View File

@@ -0,0 +1 @@
singleton HStyle 1.0 HStyle.qml

View 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
}
}
}
}
}

View 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)
}
}
}

View 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()
},
}
}

View 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
View 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
}
}
}

View 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
}

View 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) +
"&nbsp;&nbsp;" +
"<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
}
}

View 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) +
"&nbsp;&nbsp;<font size=" + HStyle.fontSize.small +
"px color=" + HStyle.chat.message.date + ">" +
Qt.formatDateTime(dateTime, "hh:mm:ss") +
"</font>" +
(isLocalEcho ?
"&nbsp;<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
}
}
}
}

View 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
}
}

View 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
View 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 }
}
}
}

View 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
}
}
}
}

View 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
}
}

View 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
View 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)
}
}
}

View 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
View 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…"
}

View File

@@ -0,0 +1,2 @@
// FIXME: Obsolete method, but need Qt 5.12+ for standard JS modules import
Qt.include("system.js")

View File

@@ -0,0 +1,8 @@
function onAppExitRequested(exit_code) {
Qt.exit(exit_code)
}
function onCoroutineDone(uuid, result) {
py.pendingCoroutines[uuid](result)
delete pendingCoroutines[uuid]
}

10
src/qml/LoadingScreen.qml Normal file
View File

@@ -0,0 +1,10 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
Rectangle {
color: "lightgray"
BusyIndicator {
anchors.centerIn: parent
}
}

View File

@@ -0,0 +1,5 @@
import "../Base"
HNoticePage {
text: "Select or add a room to start."
}

View File

@@ -0,0 +1,43 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
Item {
property string loginWith: "username"
property var client: null
HInterfaceBox {
id: rememberBox
title: "Sign in"
anchors.centerIn: parent
enterButtonTarget: "yes"
buttonModel: [
{ name: "yes", text: qsTr("Yes") },
{ name: "no", text: qsTr("No") },
]
buttonCallbacks: {
"yes": function(button) {
Backend.clients.remember(client)
pageStack.showPage("Default")
},
"no": function(button) { pageStack.showPage("Default") },
}
HLabel {
text: qsTr(
"Do you want to remember this account?\n\n" +
"If yes, the " + loginWith + " and an access token will be " +
"stored to automatically sign in on this device."
)
wrapMode: Text.Wrap
Layout.margins: rememberBox.margins
Layout.maximumWidth: rememberBox.width - Layout.margins * 2
}
HSpacer {}
}
}

83
src/qml/Pages/SignIn.qml Normal file
View File

@@ -0,0 +1,83 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
Item {
property string loginWith: "username"
onFocusChanged: identifierField.forceActiveFocus()
HInterfaceBox {
id: signInBox
title: "Sign in"
anchors.centerIn: parent
enterButtonTarget: "login"
buttonModel: [
{ name: "register", text: qsTr("Register") },
{ name: "login", text: qsTr("Login") },
{ name: "forgot", text: qsTr("Forgot?") }
]
buttonCallbacks: {
"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) {
pageStack.showPage(
"RememberAccount",
{"loginWith": loginWith, "client": client}
)
})
},
"forgot": function(button) {}
}
HRowLayout {
spacing: signInBox.margins * 1.25
Layout.margins: signInBox.margins
Layout.alignment: Qt.AlignHCenter
Repeater {
model: ["username", "email", "phone"]
HButton {
iconName: modelData
circle: true
checked: loginWith == modelData
autoExclusive: true
checkedLightens: true
onClicked: loginWith = modelData
}
}
}
HTextField {
id: identifierField
placeholderText: qsTr(
loginWith === "email" ? "Email" :
loginWith === "phone" ? "Phone" :
"Username"
)
onAccepted: signInBox.clickEnterButtonTarget()
Layout.fillWidth: true
Layout.margins: signInBox.margins
}
HTextField {
id: passwordField
placeholderText: qsTr("Password")
echoMode: HTextField.Password
onAccepted: signInBox.clickEnterButtonTarget()
Layout.fillWidth: true
Layout.margins: signInBox.margins
}
}
}

38
src/qml/Python.qml Normal file
View File

@@ -0,0 +1,38 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
import io.thp.pyotherside 1.5
import "EventHandlers/includes.js" as EventHandlers
Python {
id: py
signal ready(bool accountsToLoad)
property var pendingCoroutines: ({})
function callCoro(name, args, kwargs, callback) {
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
pendingCoroutines[uuid] = callback || function() {}
})
}
Component.onCompleted: {
for (var func in EventHandlers) {
if (EventHandlers.hasOwnProperty(func)) {
setHandler(func.replace(/^on/, ""), EventHandlers[func])
}
}
addImportPath("../..")
importNames("src", ["APP"], function() {
call("APP.start", [Qt.application.arguments], function(debug_on) {
window.debug = debug_on
callCoro("has_saved_accounts", [], {}, function(has) {
print(has)
py.ready(has)
})
})
})
}
}

View File

@@ -0,0 +1,80 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
Column {
id: accountDelegate
width: parent.width
property var user: Backend.users.get(userId)
property string roomCategoriesListUserId: userId
property bool expanded: true
HRowLayout {
width: parent.width
height: childrenRect.height
id: row
HAvatar {
id: avatar
name: user.displayName.value
}
HColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
HLabel {
id: accountLabel
text: user.displayName.value
elide: HLabel.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
leftPadding: 6
rightPadding: leftPadding
}
HTextField {
id: statusEdit
text: user.statusMessage || ""
placeholderText: qsTr("Set status message")
font.pixelSize: HStyle.fontSize.small
background: null
padding: 0
leftPadding: accountLabel.leftPadding
rightPadding: leftPadding
Layout.fillWidth: true
onEditingFinished: {
//Backend.setStatusMessage(userId, text)
pageStack.forceActiveFocus()
}
}
}
ExpandButton {
expandableItem: accountDelegate
Layout.preferredHeight: row.height
}
}
RoomCategoriesList {
id: roomCategoriesList
interactive: false // no scrolling
visible: height > 0
width: parent.width
height: childrenRect.height * (accountDelegate.expanded ? 1 : 0)
clip: heightAnimation.running
userId: roomCategoriesListUserId
Behavior on height {
NumberAnimation {
id: heightAnimation;
duration: HStyle.animationDuration
}
}
}
}

View File

@@ -0,0 +1,11 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HListView {
id: accountList
clip: true
model: Backend.accounts
delegate: AccountDelegate {}
}

View File

@@ -0,0 +1,22 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HButton {
property var expandableItem: null
id: expandButton
iconName: "expand"
iconDimension: 16
backgroundColor: "transparent"
onClicked: expandableItem.expanded = ! expandableItem.expanded
iconTransform: Rotation {
origin.x: expandButton.iconDimension / 2
origin.y: expandButton.iconDimension / 2
angle: expandableItem.expanded ? 90 : 180
Behavior on angle {
NumberAnimation { duration: HStyle.animationDuration }
}
}
}

View File

@@ -0,0 +1,25 @@
import QtQuick.Layouts 1.3
import "../Base"
HRowLayout {
id: toolBar
Layout.fillWidth: true
Layout.preferredHeight: HStyle.bottomElementsHeight
HButton {
iconName: "settings"
backgroundColor: HStyle.sidePane.settingsButton.background
}
HTextField {
id: filterField
placeholderText: qsTr("Filter rooms")
backgroundColor: HStyle.sidePane.filterRooms.background
onTextChanged: Backend.setRoomFilter(text)
Layout.fillWidth: true
Layout.preferredHeight: parent.height
}
}

View File

@@ -0,0 +1,11 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HListView {
property string userId: ""
id: roomCategoriesList
model: Backend.accounts.get(userId).roomCategories
delegate: RoomCategoryDelegate {}
}

View File

@@ -0,0 +1,60 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
Column {
id: roomCategoryDelegate
width: roomCategoriesList.width
property int normalHeight: childrenRect.height // avoid binding loop
opacity: roomList.model.count > 0 ? 1 : 0
height: normalHeight * opacity
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: HStyle.animationDuration }
}
property string roomListUserId: userId
property bool expanded: true
HRowLayout {
width: parent.width
HLabel {
id: roomCategoryLabel
text: name
font.weight: Font.DemiBold
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
}
ExpandButton {
expandableItem: roomCategoryDelegate
iconDimension: 12
}
}
RoomList {
id: roomList
interactive: false // no scrolling
visible: height > 0
width: roomCategoriesList.width - accountList.Layout.leftMargin
opacity: roomCategoryDelegate.expanded ? 1 : 0
height: childrenRect.height * opacity
clip: listHeightAnimation.running
userId: roomListUserId
category: name
Behavior on opacity {
NumberAnimation {
id: listHeightAnimation
duration: HStyle.animationDuration
}
}
}
}

View File

@@ -0,0 +1,61 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
import "utils.js" as SidePaneJS
MouseArea {
id: roomDelegate
width: roomList.width
height: childrenRect.height
onClicked: pageStack.showRoom(roomList.userId, roomList.category, roomId)
HRowLayout {
width: parent.width
spacing: roomList.spacing
HAvatar {
id: roomAvatar
name: displayName
}
HColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth:
parent.width - parent.totalSpacing - roomAvatar.width
HLabel {
id: roomLabel
text: displayName ? displayName : "<i>Empty room</i>"
textFormat: Text.StyledText
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Qt.AlignVCenter
Layout.maximumWidth: parent.width
}
HLabel {
function getText() {
return SidePaneJS.getLastRoomEventText(
roomId, roomList.userId
)
}
property var lastEvTime: lastEventDateTime
onLastEvTimeChanged: subtitleLabel.text = getText()
id: subtitleLabel
visible: text !== ""
text: getText()
textFormat: Text.StyledText
font.pixelSize: HStyle.fontSize.small
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: parent.width
}
}
}
}

View File

@@ -0,0 +1,14 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HListView {
property string userId: ""
property string category: ""
id: roomList
spacing: accountList.spacing
model:
Backend.accounts.get(userId).roomCategories.get(category).sortedRooms
delegate: RoomDelegate {}
}

View File

@@ -0,0 +1,30 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HRectangle {
id: sidePane
property int normalSpacing: 8
property bool collapsed: false
HColumnLayout {
anchors.fill: parent
AccountList {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: collapsed ? 0 : normalSpacing
topMargin: spacing
bottomMargin: spacing
Layout.leftMargin: spacing
Behavior on spacing {
NumberAnimation { duration: HStyle.animationDuration }
}
}
PaneToolBar {}
}
}

29
src/qml/SidePane/utils.js Normal file
View File

@@ -0,0 +1,29 @@
.import "../Chat/utils.js" as ChatJS
function getLastRoomEventText(roomId, accountId) {
var eventsModel = Backend.roomEvents.get(roomId)
if (eventsModel.count < 1) { return "" }
var ev = eventsModel.get(0)
var name = Backend.users.get(ev.dict.sender).displayName.value
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
if (undecryptable || ev.type.startsWith("RoomMessage")) {
var color = Qt.hsla(Backend.hueFromString(name), 0.32, 0.3, 1)
return "<font color='" + color + "'>" +
name +
":</font> " +
(undecryptable ?
"<font color='darkred'>" + qsTr("Undecryptable") + "<font>" :
ev.dict.body)
} else {
return "<font color='" + (undecryptable ? "darkred" : "#444") + "'>" +
name +
" " +
ChatJS.getEventText(ev.type, ev.dict) +
"</font>"
}
}

106
src/qml/UI.qml Normal file
View File

@@ -0,0 +1,106 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.7
import "Base"
import "SidePane"
Item {
id: mainUI
property bool accountsLoggedIn: Backend.clients.count > 0
HImage {
id: mainUIBackground
fillMode: Image.PreserveAspectCrop
source: "../images/login_background.jpg"
sourceSize.width: Screen.width
sourceSize.height: Screen.height
anchors.fill: parent
asynchronous: false
}
HSplitView {
id: uiSplitView
anchors.fill: parent
SidePane {
id: sidePane
visible: accountsLoggedIn
collapsed: width < Layout.minimumWidth + normalSpacing
property int parentWidth: parent.width
property int collapseBelow: 120
function set_width() {
width = parent.width * 0.3 < collapseBelow ?
Layout.minimumWidth : Math.min(parent.width * 0.3, 300)
}
onParentWidthChanged: if (uiSplitView.canAutoSize) { set_width() }
width: set_width() // Initial width
Layout.minimumWidth: HStyle.avatar.size
Layout.maximumWidth: parent.width
Behavior on width {
NumberAnimation {
// Don't slow down the user manually resizing
duration:
(uiSplitView.canAutoSize &&
parent.width * 0.3 < sidePane.collapseBelow * 1.2) ?
HStyle.animationDuration : 0
}
}
}
StackView {
id: pageStack
function showPage(name, properties) {
pageStack.replace("Pages/" + name + ".qml", properties || {})
}
function showRoom(userId, category, roomId) {
pageStack.replace(
"Chat/Chat.qml",
{ userId: userId, category: category, roomId: roomId }
)
}
Connections {
target: py
onReady: function(accountsToLoad) {
pageStack.showPage(accountsToLoad ? "Default" : "SignIn")
if (accountsToLoad) {
py.callCoro("load_saved_accounts")
// initialRoomTimer.start()
}
}
}
Timer {
// TODO: remove this, debug
id: initialRoomTimer
interval: 5000
repeat: false
onTriggered: pageStack.showRoom(
"@test_mary:matrix.org",
"Rooms",
"!TSXGsbBbdwsdylIOJZ:matrix.org"
)
}
onCurrentItemChanged: if (currentItem) {
currentItem.forceActiveFocus()
}
// Buggy
replaceExit: null
popExit: null
pushExit: null
}
Keys.onEscapePressed: if (window.debug) { py.call("APP.pdb") }
}
}

44
src/qml/Window.qml Normal file
View File

@@ -0,0 +1,44 @@
import QtQuick 2.7
import QtQuick.Controls 2.2
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
color: "black"
title: "Test"
property bool debug: false
property bool ready: false
Component.onCompleted: {
Qt.application.name = "harmonyqml"
Qt.application.displayName = "Harmony QML"
Qt.application.version = "0.1.0"
window.ready = true
}
Python {
id: py
}
LoadingScreen {
id: loadingScreen
anchors.fill: parent
visible: uiLoader.scale < 1
}
Loader {
id: uiLoader
anchors.fill: parent
property bool ready: window.ready && py.ready
scale: uiLoader.ready ? 1 : 0.5
source: uiLoader.ready ? "UI.qml" : ""
Behavior on scale {
NumberAnimation { duration: 100 }
}
}
}