Start rewriting backend with pyotherside+asyncio
This commit is contained in:
45
src/qml/Base/HAvatar.qml
Normal file
45
src/qml/Base/HAvatar.qml
Normal 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
138
src/qml/Base/HButton.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
11
src/qml/Base/HColumnLayout.qml
Normal file
11
src/qml/Base/HColumnLayout.qml
Normal 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
10
src/qml/Base/HIcon.qml
Normal 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
8
src/qml/Base/HImage.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
cache: true
|
||||
mipmap: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
60
src/qml/Base/HInterfaceBox.qml
Normal file
60
src/qml/Base/HInterfaceBox.qml
Normal 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
11
src/qml/Base/HLabel.qml
Normal 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
|
||||
}
|
57
src/qml/Base/HListModel.qml
Normal file
57
src/qml/Base/HListModel.qml
Normal 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
|
||||
}
|
||||
}
|
24
src/qml/Base/HListView.qml
Normal file
24
src/qml/Base/HListView.qml
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
34
src/qml/Base/HNoticePage.qml
Normal file
34
src/qml/Base/HNoticePage.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
6
src/qml/Base/HRectangle.qml
Normal file
6
src/qml/Base/HRectangle.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
import QtQuick 2.7
|
||||
|
||||
Rectangle {
|
||||
id: rectangle
|
||||
color: HStyle.sidePane.background
|
||||
}
|
22
src/qml/Base/HRichLabel.qml
Normal file
22
src/qml/Base/HRichLabel.qml
Normal 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) }
|
||||
}
|
||||
}
|
||||
}
|
10
src/qml/Base/HRowLayout.qml
Normal file
10
src/qml/Base/HRowLayout.qml
Normal 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))
|
||||
}
|
15
src/qml/Base/HScalingBox.qml
Normal file
15
src/qml/Base/HScalingBox.qml
Normal 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)
|
||||
}
|
33
src/qml/Base/HScrollableTextArea.qml
Normal file
33
src/qml/Base/HScrollableTextArea.qml
Normal 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
7
src/qml/Base/HSpacer.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
24
src/qml/Base/HSplitView.qml
Normal file
24
src/qml/Base/HSplitView.qml
Normal 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
|
||||
}
|
||||
}
|
11
src/qml/Base/HStatusAvatar.qml
Normal file
11
src/qml/Base/HStatusAvatar.qml
Normal 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
139
src/qml/Base/HStyle.qml
Normal 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
|
||||
}
|
17
src/qml/Base/HTextField.qml
Normal file
17
src/qml/Base/HTextField.qml
Normal 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
1
src/qml/Base/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton HStyle 1.0 HStyle.qml
|
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…"
|
||||
}
|
2
src/qml/EventHandlers/includes.js
Normal file
2
src/qml/EventHandlers/includes.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// FIXME: Obsolete method, but need Qt 5.12+ for standard JS modules import
|
||||
Qt.include("system.js")
|
8
src/qml/EventHandlers/system.js
Normal file
8
src/qml/EventHandlers/system.js
Normal 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
10
src/qml/LoadingScreen.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Rectangle {
|
||||
color: "lightgray"
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
5
src/qml/Pages/Default.qml
Normal file
5
src/qml/Pages/Default.qml
Normal file
@@ -0,0 +1,5 @@
|
||||
import "../Base"
|
||||
|
||||
HNoticePage {
|
||||
text: "Select or add a room to start."
|
||||
}
|
43
src/qml/Pages/RememberAccount.qml
Normal file
43
src/qml/Pages/RememberAccount.qml
Normal 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
83
src/qml/Pages/SignIn.qml
Normal 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
38
src/qml/Python.qml
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
80
src/qml/SidePane/AccountDelegate.qml
Normal file
80
src/qml/SidePane/AccountDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
src/qml/SidePane/AccountList.qml
Normal file
11
src/qml/SidePane/AccountList.qml
Normal 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 {}
|
||||
}
|
22
src/qml/SidePane/ExpandButton.qml
Normal file
22
src/qml/SidePane/ExpandButton.qml
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
25
src/qml/SidePane/PaneToolBar.qml
Normal file
25
src/qml/SidePane/PaneToolBar.qml
Normal 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
|
||||
}
|
||||
}
|
11
src/qml/SidePane/RoomCategoriesList.qml
Normal file
11
src/qml/SidePane/RoomCategoriesList.qml
Normal 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 {}
|
||||
}
|
60
src/qml/SidePane/RoomCategoryDelegate.qml
Normal file
60
src/qml/SidePane/RoomCategoryDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
src/qml/SidePane/RoomDelegate.qml
Normal file
61
src/qml/SidePane/RoomDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/qml/SidePane/RoomList.qml
Normal file
14
src/qml/SidePane/RoomList.qml
Normal 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 {}
|
||||
}
|
30
src/qml/SidePane/SidePane.qml
Normal file
30
src/qml/SidePane/SidePane.qml
Normal 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
29
src/qml/SidePane/utils.js
Normal 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
106
src/qml/UI.qml
Normal 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
44
src/qml/Window.qml
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user