Merge branch 'multilogin' into 'master'
Multilogin See merge request b0/matrique!17
This commit is contained in:
commit
816380e9d0
|
@ -1 +1 @@
|
|||
Subproject commit d9ff200ff62fb7f5b6b51082dc3979d5454a1bec
|
||||
Subproject commit 875514ee865b00be67542849f94d2c2561ba4137
|
39
js/md.js
39
js/md.js
|
@ -1,44 +1,7 @@
|
|||
/* jshint browser: true, devel: true */
|
||||
.pragma library
|
||||
|
||||
/**
|
||||
* preg_replace (from PHP) in JavaScript!
|
||||
*
|
||||
* This is basically a pattern replace. You can use a regex pattern to search and
|
||||
* another for the replace. For more information see the PHP docs on the original
|
||||
* function (http://php.net/manual/en/function.preg-replace.php), and for more on
|
||||
* JavaScript flavour regex visit http://www.regular-expressions.info/javascript.html
|
||||
*
|
||||
* NOTE: Unlike the PHP version, this function only deals with string inputs. No arrays.
|
||||
*
|
||||
* @author William Duyck <fuzzyfox0@gmail.com>
|
||||
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License 2.0
|
||||
*
|
||||
* @param {String} pattern The pattern to search for.
|
||||
* @param {String} replace The string to replace.
|
||||
* @param {String} subject The string to search and replace.
|
||||
* @param {Integer} limit The maximum possible replacements.
|
||||
* @return {String} If matches are found, the new subject will be returned.
|
||||
*/
|
||||
var preg_replace=function(a,b,c,d){void 0===d&&(d=-1);var e=a.substr(a.lastIndexOf(a[0])+1),f=a.substr(1,a.lastIndexOf(a[0])-1),g=RegExp(f,e),i=[],j=0,k=0,l=c,m=[];if(-1===d){do m=g.exec(c),null!==m&&i.push(m);while(null!==m&&-1!==e.indexOf("g"))}else i.push(g.exec(c));for(j=i.length-1;j>-1;j--){for(m=b,k=i[j].length;k>-1;k--)m=m.replace("${"+k+"}",i[j][k]).replace("$"+k,i[j][k]).replace("\\"+k,i[j][k]);l=l.replace(i[j][0],m)}return l};
|
||||
|
||||
/**
|
||||
* Basic Markdown Parser
|
||||
*
|
||||
* This function parses a small subset of the Markdown language as defined by
|
||||
* [John Gruber](http://daringfireball.net/projects/markdown). It's very basic
|
||||
* and needs to be refactored a little, and there are plans to add more support
|
||||
* for the rest of the language in the near future.
|
||||
*
|
||||
* This implimentation is based loosely on
|
||||
* [slimdown.php](https://gist.github.com/jbroadway/2836900) by Johnny Broadway.
|
||||
*
|
||||
* @version 0.1
|
||||
* @author William Duyck <fuzzyfox0@gmail.com>
|
||||
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License 2.0
|
||||
*
|
||||
* @param {String} str A Markdown string to be converted to HTML.
|
||||
* @return {String} The HTML for the given Markdown.
|
||||
*/
|
||||
var markdown_parser = function(str){
|
||||
|
||||
var rules = [
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.pragma library
|
||||
|
||||
function stringToColor(str) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
var colour = '#';
|
||||
for (var j = 0; j < 3; j++) {
|
||||
var value = (hash >> (j * 8)) & 0xFF;
|
||||
colour += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return colour;
|
||||
}
|
||||
|
||||
function pushToStack(stack, page) {
|
||||
if(page && stack.currentItem !== page) {
|
||||
if(stack.depth === 1) {
|
||||
stack.replace(page)
|
||||
} else {
|
||||
stack.pop(null)
|
||||
stack.replace(page)
|
||||
}
|
||||
}
|
||||
}
|
40
matrique.pro
40
matrique.pro
|
@ -7,7 +7,13 @@ CONFIG += object_parallel_to_source
|
|||
|
||||
TARGET = matrique
|
||||
|
||||
packagesExist(QMatrixClient) {
|
||||
message("Found libQMatrixClient via pkg-config.")
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += QMatrixClient
|
||||
} else {
|
||||
include(include/libqmatrixclient/libqmatrixclient.pri)
|
||||
}
|
||||
include(include/SortFilterProxyModel/SortFilterProxyModel.pri)
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
|
@ -29,7 +35,9 @@ SOURCES += src/main.cpp \
|
|||
src/emojimodel.cpp \
|
||||
src/matriqueroom.cpp \
|
||||
src/userlistmodel.cpp \
|
||||
src/imageitem.cpp
|
||||
src/imageitem.cpp \
|
||||
src/accountlistmodel.cpp \
|
||||
src/matriqueuser.cpp
|
||||
|
||||
RESOURCES += \
|
||||
res.qrc
|
||||
|
@ -67,19 +75,19 @@ mac {
|
|||
ICON = asset/img/icon.icns
|
||||
}
|
||||
|
||||
DISTFILES += \
|
||||
ChatForm.qml \
|
||||
LoginForm.qml \
|
||||
main.qml \
|
||||
Home.qml \
|
||||
Login.qml \
|
||||
ImageStatus.qml \
|
||||
ButtonDelegate.qml \
|
||||
SideNav.qml \
|
||||
RoomListForm.qml \
|
||||
Room.qml \
|
||||
Setting.qml \
|
||||
qml/js/md.js \
|
||||
#DISTFILES += \
|
||||
# ChatForm.qml \
|
||||
# LoginForm.qml \
|
||||
# main.qml \
|
||||
# Home.qml \
|
||||
# Login.qml \
|
||||
# ImageStatus.qml \
|
||||
# ButtonDelegate.qml \
|
||||
# SideNav.qml \
|
||||
# RoomListForm.qml \
|
||||
# Room.qml \
|
||||
# Setting.qml \
|
||||
# qml/js/md.js \
|
||||
|
||||
HEADERS += \
|
||||
src/controller.h \
|
||||
|
@ -89,4 +97,6 @@ HEADERS += \
|
|||
src/emojimodel.h \
|
||||
src/matriqueroom.h \
|
||||
src/userlistmodel.h \
|
||||
src/imageitem.h
|
||||
src/imageitem.h \
|
||||
src/accountlistmodel.h \
|
||||
src/matriqueuser.h
|
||||
|
|
|
@ -151,12 +151,9 @@ Page {
|
|||
return
|
||||
}
|
||||
|
||||
var replaceViewFunction = function() {
|
||||
if (matriqueController.isLogin) stackView.replace(roomPage)
|
||||
matriqueController.isLoginChanged.disconnect(replaceViewFunction)
|
||||
}
|
||||
matriqueController.isLoginChanged.connect(replaceViewFunction)
|
||||
controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
|
||||
|
||||
controller.connectionAdded.connect(function() { stackView.pop() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import Qt.labs.settings 1.0
|
|||
|
||||
Settings {
|
||||
property bool lazyLoad: true
|
||||
property bool richText
|
||||
property bool richText: true
|
||||
property bool pressAndHold
|
||||
property bool rearrangeByActivity
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import Matrique 0.1
|
||||
import Matrique.Settings 0.1
|
||||
|
||||
|
|
205
qml/Setting.qml
205
qml/Setting.qml
|
@ -2,56 +2,184 @@ import QtQuick 2.9
|
|||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import Matrique 0.1
|
||||
import Matrique.Settings 0.1
|
||||
|
||||
import "component"
|
||||
import "form"
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
Page {
|
||||
property var connection
|
||||
property alias listModel: accountSettingsListView.model
|
||||
|
||||
Page {
|
||||
id: accountForm
|
||||
|
||||
parent: null
|
||||
|
||||
padding: 64
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.preferredHeight: 60
|
||||
anchors.fill: parent
|
||||
|
||||
ImageStatus {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: matriqueController.isLogin ? connection.localUser && connection.localUser.avatarUrl ? "image://mxc/" + connection.localUser.avatarUrl : "" : "qrc:/asset/img/avatar.png"
|
||||
displayText: matriqueController.isLogin && connection.localUser.displayName ? connection.localUser.displayName : ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Label {
|
||||
font.pointSize: 18
|
||||
text: matriqueController.isLogin ? connection.localUser.displayName : ""
|
||||
id: accountSettingsListView
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
clip: true
|
||||
|
||||
delegate: Column {
|
||||
property bool expanded: false
|
||||
|
||||
spacing: 8
|
||||
|
||||
SwipeDelegate {
|
||||
width: accountSettingsListView.width
|
||||
height: 64
|
||||
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
spacing: 8
|
||||
|
||||
ImageItem {
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
|
||||
hint: user.displayName
|
||||
defaultColor: Util.stringToColor(user.displayName)
|
||||
image: user.avatar
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
font.pointSize: 12
|
||||
text: matriqueController.isLogin ? connection.localUser.id : ""
|
||||
text: user.displayName
|
||||
}
|
||||
Label {
|
||||
text: user.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
anchors.right: parent.right
|
||||
|
||||
color: Material.accent
|
||||
|
||||
MaterialIcon {
|
||||
anchors.fill: parent
|
||||
|
||||
icon: "\ue879"
|
||||
color: "white"
|
||||
}
|
||||
|
||||
SwipeDelegate.onClicked: matriqueController.logout(connection)
|
||||
}
|
||||
|
||||
onClicked: expanded = !expanded
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width - 32
|
||||
height: expanded ? implicitHeight : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
spacing: 0
|
||||
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 24
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
spacing: 8
|
||||
|
||||
model: ["#498882", "#42a5f5", "#5c6bc0", "#7e57c2", "#ab47bc", "#ff7043"]
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
radius: width / 2
|
||||
|
||||
color: modelData
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: matriqueController.setColor(connection.localUserId, modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label { text: "Homeserver:" }
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: connection.homeserver
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Label { text: "Device ID:" }
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: connection.deviceId
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
|
||||
Label { text: "Access Token:" }
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: connection.accessToken
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Logout"
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Add Account"
|
||||
flat: true
|
||||
highlighted: true
|
||||
|
||||
onClicked: {
|
||||
matriqueController.logout()
|
||||
Qt.quit()
|
||||
}
|
||||
onClicked: stackView.push(loginPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +189,8 @@ Page {
|
|||
|
||||
parent: null
|
||||
|
||||
padding: 64
|
||||
|
||||
Column {
|
||||
Switch {
|
||||
text: "Lazy load at initial sync"
|
||||
|
@ -90,6 +220,8 @@ Page {
|
|||
|
||||
parent: null
|
||||
|
||||
padding: 64
|
||||
|
||||
Column {
|
||||
Switch {
|
||||
text: "Dark theme"
|
||||
|
@ -116,6 +248,7 @@ Page {
|
|||
|
||||
Page {
|
||||
id: aboutForm
|
||||
|
||||
parent: null
|
||||
|
||||
padding: 64
|
||||
|
@ -133,49 +266,53 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: 240
|
||||
Layout.fillHeight: true
|
||||
Rectangle {
|
||||
width: 240
|
||||
height: parent.height
|
||||
|
||||
spacing: 0
|
||||
id: settingDrawer
|
||||
|
||||
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
|
||||
ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
|
||||
text: "Account"
|
||||
onClicked: pushToStack(accountForm)
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
|
||||
text: "General"
|
||||
onClicked: pushToStack(generalForm)
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
|
||||
text: "Appearance"
|
||||
onClicked: pushToStack(appearanceForm)
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
|
||||
text: "About"
|
||||
onClicked: pushToStack(aboutForm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: settingDrawer.width
|
||||
|
||||
id: settingStackView
|
||||
}
|
||||
}
|
||||
|
||||
function pushToStack(item) {
|
||||
settingStackView.clear()
|
||||
|
|
|
@ -15,7 +15,11 @@ Control {
|
|||
AutoMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onSecondaryClicked: Qt.createComponent("qrc:/qml/menu/MessageContextMenu.qml").createObject(this)
|
||||
onSecondaryClicked: {
|
||||
messageContextMenu.row = messageRow
|
||||
messageContextMenu.model = model
|
||||
messageContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle { color: colored ? Material.accent : highlighted ? Material.primary : backgroundColor }
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
Item {
|
||||
property bool round: true
|
||||
property string source: ""
|
||||
property string displayText: ""
|
||||
readonly property bool showImage: source
|
||||
readonly property bool showInitial: !showImage && displayText || avatar.status != Image.Ready
|
||||
|
||||
id: item
|
||||
|
||||
Image {
|
||||
width: item.width
|
||||
height: item.width
|
||||
|
||||
id: avatar
|
||||
|
||||
visible: showImage
|
||||
source: item.source
|
||||
|
||||
mipmap: true
|
||||
layer.enabled: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: item.width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Item {
|
||||
width: avatar.width
|
||||
height: avatar.width
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: avatar.width
|
||||
height: avatar.width
|
||||
radius: round? avatar.width / 2 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
|
||||
color: "white"
|
||||
visible: showInitial
|
||||
text: showInitial ? getInitials(displayText)[0] : ""
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: round? width / 2 : 0
|
||||
color: showInitial ? stringToColor(displayText) : Material.accent
|
||||
}
|
||||
}
|
||||
|
||||
function getInitials(text) {
|
||||
if (!text) return "N"
|
||||
var initial = text.toUpperCase().replace(/[^a-zA-Z- ]/g, "").match(/\b\w/g);
|
||||
if (!initial) return "N"
|
||||
return initial
|
||||
}
|
||||
|
||||
function stringToColor(str) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
var colour = '#';
|
||||
for (var j = 0; j < 3; j++) {
|
||||
var value = (hash >> (j * 8)) & 0xFF;
|
||||
colour += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return colour;
|
||||
}
|
||||
}
|
|
@ -3,20 +3,13 @@ import QtQuick.Controls 2.2
|
|||
import QtQuick.Layouts 1.3
|
||||
import Matrique.Settings 0.1
|
||||
|
||||
Item {
|
||||
property alias icon: iconText.text
|
||||
property var color: MSettings.darkTheme ? "white" : "black"
|
||||
|
||||
id: item
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
property alias icon: materialLabel.text
|
||||
|
||||
id: materialLabel
|
||||
|
||||
id: iconText
|
||||
font.pointSize: 16
|
||||
font.family: materialFont.name
|
||||
color: item.color
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import QtQuick.Layouts 1.3
|
|||
import QtQuick.Controls.Material 2.2
|
||||
import Matrique 0.1
|
||||
import Matrique.Settings 0.1
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
RowLayout {
|
||||
readonly property bool avatarVisible: !(sentByMe || (aboveAuthor === author && section === aboveSection))
|
||||
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote")
|
||||
readonly property bool highlighted: !(sentByMe || eventType === "notice" )
|
||||
readonly property bool sentByMe: author === currentRoom.localUser
|
||||
readonly property bool isText: eventType === "notice" || eventType === "message"
|
||||
|
@ -22,15 +23,16 @@ RowLayout {
|
|||
|
||||
spacing: 6
|
||||
|
||||
ImageStatus {
|
||||
ImageItem {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
round: false
|
||||
visible: avatarVisible
|
||||
source: author.avatarUrl != "" ? "image://mxc/" + author.avatarUrl : null
|
||||
displayText: author.displayName
|
||||
hint: author.displayName
|
||||
defaultColor: Util.stringToColor(author.displayName)
|
||||
image: author.avatar
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -61,6 +63,11 @@ RowLayout {
|
|||
Material.foreground: Material.accent
|
||||
coloredBackground: highlighted
|
||||
font.bold: true
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: inputField.insert(inputField.cursorPosition, author.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
AutoLabel {
|
||||
|
@ -86,14 +93,30 @@ RowLayout {
|
|||
active: eventType === "image" || eventType === "file" || eventType === "audio"
|
||||
}
|
||||
|
||||
AutoLabel {
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
spacing: 8
|
||||
|
||||
AutoLabel {
|
||||
id: timeLabel
|
||||
|
||||
visible: Math.abs(time - aboveTime) > 600000 || index == 0
|
||||
text: Qt.formatTime(time, "hh:mm")
|
||||
coloredBackground: highlighted
|
||||
Material.foreground: "grey"
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
height: timeLabel.height
|
||||
|
||||
visible: userMarker.length > 0
|
||||
icon: "\ue5ca"
|
||||
color: highlighted ? "white": Material.foreground
|
||||
font.pointSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
|
|
|
@ -4,6 +4,8 @@ import QtQuick.Controls.Material 2.2
|
|||
import QtQuick.Layouts 1.3
|
||||
import Matrique 0.1
|
||||
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
Drawer {
|
||||
property var room
|
||||
|
||||
|
@ -22,13 +24,14 @@ Drawer {
|
|||
anchors.fill: parent
|
||||
anchors.margins: 32
|
||||
|
||||
ImageStatus {
|
||||
ImageItem {
|
||||
Layout.preferredWidth: 64
|
||||
Layout.preferredHeight: 64
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
source: room && room.avatarUrl != "" ? "image://mxc/" + room.avatarUrl : null
|
||||
displayText: room ? room.displayName : ""
|
||||
hint: room ? room.displayName : "No name"
|
||||
defaultColor: Util.stringToColor(room ? room.displayName : "No name")
|
||||
image: matriqueController.safeImage(room ? room.avatar : null)
|
||||
}
|
||||
|
||||
Label {
|
||||
|
@ -45,6 +48,13 @@ Drawer {
|
|||
text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias"
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: room ? room.memberCount + " Members" : "No Member Count"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
|
@ -53,6 +63,7 @@ Drawer {
|
|||
|
||||
id: roomNameField
|
||||
text: room && room.name ? room.name : ""
|
||||
selectByMouse: true
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
|
@ -74,6 +85,7 @@ Drawer {
|
|||
id: roomTopicField
|
||||
|
||||
text: room && room.topic ? room.topic : ""
|
||||
selectByMouse: true
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
|
@ -94,7 +106,11 @@ Drawer {
|
|||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
delegate: ItemDelegate {
|
||||
model: UserListModel {
|
||||
room: roomDrawer.room
|
||||
}
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
|
@ -103,12 +119,13 @@ Drawer {
|
|||
anchors.margins: 8
|
||||
spacing: 12
|
||||
|
||||
ImageStatus {
|
||||
ImageItem {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: avatar != "" ? "image://mxc/" + avatar : ""
|
||||
displayText: name
|
||||
defaultColor: Util.stringToColor(name)
|
||||
image: avatar
|
||||
hint: name
|
||||
}
|
||||
|
||||
Label {
|
||||
|
@ -117,12 +134,25 @@ Drawer {
|
|||
text: name
|
||||
}
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
anchors.right: parent.right
|
||||
|
||||
color: Material.accent
|
||||
|
||||
MaterialIcon {
|
||||
anchors.fill: parent
|
||||
|
||||
icon: "\ue8fb"
|
||||
color: "white"
|
||||
}
|
||||
|
||||
model: UserListModel {
|
||||
id: userListModel
|
||||
SwipeDelegate.onClicked: room.kickMember(userId)
|
||||
}
|
||||
|
||||
room: roomDrawer.room
|
||||
onClicked: inputField.insert(inputField.cursorPosition, name)
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
|
|
@ -3,29 +3,23 @@ import QtQuick.Controls 2.2
|
|||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
ItemDelegate {
|
||||
property var page
|
||||
readonly property bool selected: stackView.currentItem === page
|
||||
property bool selected: stackView.currentItem === page
|
||||
property color highlightColor: Material.accent
|
||||
|
||||
Rectangle {
|
||||
width: selected ? 4 : 0
|
||||
height: parent.height
|
||||
|
||||
color: Material.accent
|
||||
color: highlightColor
|
||||
|
||||
Behavior on width {
|
||||
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if(page && stackView.currentItem !== page) {
|
||||
if(stackView.depth === 1) {
|
||||
stackView.replace(page)
|
||||
} else {
|
||||
stackView.clear()
|
||||
stackView.push(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: Util.pushToStack(stackView, page)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import QtGraphicalEffects 1.0
|
||||
import Matrique 0.1
|
||||
import Matrique.Settings 0.1
|
||||
|
||||
import "../component"
|
||||
import "qrc:/qml/component"
|
||||
import "qrc:/qml/menu"
|
||||
import "qrc:/js/md.js" as Markdown
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
Item {
|
||||
property var currentRoom: null
|
||||
|
@ -57,12 +58,13 @@ Item {
|
|||
|
||||
spacing: 12
|
||||
|
||||
ImageStatus {
|
||||
ImageItem {
|
||||
Layout.preferredWidth: height
|
||||
Layout.fillHeight: true
|
||||
|
||||
source: currentRoom && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : null
|
||||
displayText: currentRoom ? currentRoom.displayName : ""
|
||||
hint: currentRoom ? currentRoom.displayName : "No name"
|
||||
defaultColor: Util.stringToColor(currentRoom ? currentRoom.displayName : "No name")
|
||||
image: matriqueController.safeImage(currentRoom ? currentRoom.avatar : null)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
@ -87,7 +89,7 @@ Item {
|
|||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: currentRoom ? currentRoom.topic : ""
|
||||
text: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
|
||||
color: "white"
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
|
@ -223,6 +225,36 @@ Item {
|
|||
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
}
|
||||
|
||||
MessageContextMenu { id: messageContextMenu }
|
||||
|
||||
Dialog {
|
||||
property string sourceText
|
||||
|
||||
x: (window.width - width) / 2
|
||||
y: (window.height - height) / 2
|
||||
width: 480
|
||||
|
||||
id: sourceDialog
|
||||
|
||||
parent: ApplicationWindow.overlay
|
||||
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
padding: 16
|
||||
|
||||
title: "View Source"
|
||||
|
||||
contentItem: ScrollView {
|
||||
TextArea {
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
|
||||
text: sourceDialog.sourceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar {
|
||||
|
|
|
@ -8,7 +8,9 @@ import Matrique 0.1
|
|||
import SortFilterProxyModel 0.2
|
||||
import Matrique.Settings 0.1
|
||||
|
||||
import "../component"
|
||||
import "qrc:/qml/component"
|
||||
import "qrc:/qml/menu"
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
Item {
|
||||
property alias listModel: roomListProxyModel.sourceModel
|
||||
|
@ -38,7 +40,7 @@ Item {
|
|||
bottomPadding: 0
|
||||
placeholderText: "Search..."
|
||||
|
||||
background: Rectangle { color: MSettings.darkTheme ? "#282828" : "#fafafa" }
|
||||
background: Rectangle { color: MSettings.darkTheme ? "#303030" : "#fafafa" }
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Find
|
||||
|
@ -106,15 +108,25 @@ Item {
|
|||
width: parent.width
|
||||
height: 64
|
||||
|
||||
color: MSettings.darkTheme ? "#282828" : "#fafafa"
|
||||
color: MSettings.darkTheme ? "#303030" : "#fafafa"
|
||||
|
||||
AutoMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: MSettings.miniMode
|
||||
|
||||
onSecondaryClicked: Qt.createComponent("qrc:/qml/menu/RoomContextMenu.qml").createObject(this)
|
||||
onPrimaryClicked: category === RoomType.Invited ? inviteDialog.open() : enteredRoom = currentRoom
|
||||
onSecondaryClicked: {
|
||||
roomContextMenu.model = model
|
||||
roomContextMenu.popup()
|
||||
}
|
||||
onPrimaryClicked: {
|
||||
if (category === RoomType.Invited) {
|
||||
inviteDialog.currentRoom = currentRoom
|
||||
inviteDialog.open()
|
||||
} else {
|
||||
enteredRoom = currentRoom
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: MSettings.miniMode && containsMouse
|
||||
ToolTip.text: name
|
||||
|
@ -129,11 +141,14 @@ Item {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
width: unreadCount > 0 || highlighted ? 4 : 0
|
||||
height: parent.height
|
||||
|
||||
color: Material.accent
|
||||
visible: unreadCount > 0 || highlighted
|
||||
|
||||
Behavior on width {
|
||||
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -142,14 +157,6 @@ Item {
|
|||
|
||||
spacing: 12
|
||||
|
||||
// ImageStatus {
|
||||
// Layout.preferredWidth: height
|
||||
// Layout.fillHeight: true
|
||||
|
||||
// source: avatar ? "image://mxc/" + avatar : ""
|
||||
// displayText: name
|
||||
// }
|
||||
|
||||
ImageItem {
|
||||
id: imageItem
|
||||
|
||||
|
@ -157,7 +164,7 @@ Item {
|
|||
Layout.fillHeight: true
|
||||
|
||||
hint: name || "No Name"
|
||||
defaultColor: stringToColor(name || "No Name")
|
||||
defaultColor: Util.stringToColor(name || "No Name")
|
||||
|
||||
image: avatar
|
||||
}
|
||||
|
@ -183,7 +190,7 @@ Item {
|
|||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"");
|
||||
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm,"")
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
@ -205,7 +212,11 @@ Item {
|
|||
horizontalAlignment: MSettings.miniMode ? Text.AlignHCenter : undefined
|
||||
}
|
||||
|
||||
RoomContextMenu { id: roomContextMenu }
|
||||
|
||||
Dialog {
|
||||
property var currentRoom
|
||||
|
||||
id: inviteDialog
|
||||
parent: ApplicationWindow.overlay
|
||||
|
||||
|
@ -224,17 +235,4 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stringToColor(str) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
var colour = '#';
|
||||
for (var j = 0; j < 3; j++) {
|
||||
var value = (hash >> (j * 8)) & 0xFF;
|
||||
colour += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return colour;
|
||||
}
|
||||
}
|
||||
|
|
80
qml/main.qml
80
qml/main.qml
|
@ -9,9 +9,10 @@ import Matrique.Settings 0.1
|
|||
|
||||
import "component"
|
||||
import "form"
|
||||
import "qrc:/js/util.js" as Util
|
||||
|
||||
ApplicationWindow {
|
||||
readonly property var connection: matriqueController.connection
|
||||
readonly property var currentConnection: accountListView.currentConnection ? accountListView.currentConnection : null
|
||||
|
||||
width: 960
|
||||
height: 640
|
||||
|
@ -25,11 +26,7 @@ ApplicationWindow {
|
|||
|
||||
Material.theme: MSettings.darkTheme ? Material.Dark : Material.Light
|
||||
|
||||
Settings {
|
||||
property alias homeserver: matriqueController.homeserver
|
||||
property alias userID: matriqueController.userID
|
||||
property alias token: matriqueController.token
|
||||
}
|
||||
Material.accent: matriqueController.color(currentConnection ? currentConnection.localUserId : "")
|
||||
|
||||
FontLoader { id: materialFont; source: "qrc:/asset/font/material.ttf" }
|
||||
|
||||
|
@ -48,6 +45,11 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
AccountListModel {
|
||||
id: accountListModel
|
||||
controller: matriqueController
|
||||
}
|
||||
|
||||
Popup {
|
||||
property bool busy: matriqueController.busy
|
||||
|
||||
|
@ -77,7 +79,7 @@ ApplicationWindow {
|
|||
|
||||
parent: null
|
||||
|
||||
connection: window.connection
|
||||
connection: currentConnection
|
||||
}
|
||||
|
||||
Setting {
|
||||
|
@ -85,7 +87,7 @@ ApplicationWindow {
|
|||
|
||||
parent: null
|
||||
|
||||
connection: window.connection
|
||||
listModel: accountListModel
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -104,25 +106,41 @@ ApplicationWindow {
|
|||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
SideNavButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
ListView {
|
||||
property var currentConnection: null
|
||||
|
||||
ImageStatus {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: accountListView
|
||||
|
||||
model: accountListModel
|
||||
|
||||
spacing: 0
|
||||
|
||||
clip: true
|
||||
|
||||
delegate: SideNavButton {
|
||||
width: parent.width
|
||||
height: width
|
||||
|
||||
selected: stackView.currentItem === page && currentConnection === connection
|
||||
|
||||
ImageItem {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
|
||||
source: matriqueController.isLogin ? connection.localUser && connection.localUser.avatarUrl ? "image://mxc/" + connection.localUser.avatarUrl : "" : "qrc:/asset/img/avatar.png"
|
||||
displayText: matriqueController.isLogin && connection.localUser.displayName ? connection.localUser.displayName : ""
|
||||
hint: user.displayName
|
||||
image: user.avatar
|
||||
defaultColor: Util.stringToColor(user.displayName)
|
||||
}
|
||||
|
||||
highlightColor: matriqueController.color(user.id)
|
||||
|
||||
page: roomPage
|
||||
|
||||
onClicked: accountListView.currentConnection = connection
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
SideNavButton {
|
||||
|
@ -174,7 +192,7 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
onAccepted: matriqueController.createRoom(addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text)
|
||||
onAccepted: matriqueController.createRoom(currentConnection, addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text)
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
|
@ -200,7 +218,7 @@ ApplicationWindow {
|
|||
placeholderText: "#matrix:matrix.org"
|
||||
}
|
||||
|
||||
onAccepted: matriqueController.joinRoom(joinRoomDialogTextField.text)
|
||||
onAccepted: matriqueController.joinRoom(currentConnection, joinRoomDialogTextField.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +245,7 @@ ApplicationWindow {
|
|||
placeholderText: "@bot:matrix.org"
|
||||
}
|
||||
|
||||
onAccepted: matriqueController.createDirectChat(directChatDialogTextField.text)
|
||||
onAccepted: currentConnection.createDirectChat(directChatDialogTextField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +259,7 @@ ApplicationWindow {
|
|||
anchors.fill: parent
|
||||
|
||||
icon: "\ue8b8"
|
||||
color: parent.highlighted ? Material.accent : "white"
|
||||
color: "white"
|
||||
}
|
||||
page: settingPage
|
||||
}
|
||||
|
@ -272,13 +290,15 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
imageProvider.connection = matriqueController.connection
|
||||
Binding {
|
||||
target: imageProvider
|
||||
property: "connection"
|
||||
value: currentConnection
|
||||
}
|
||||
|
||||
if (matriqueController.userID && matriqueController.token) {
|
||||
matriqueController.login();
|
||||
} else {
|
||||
stackView.replace(loginPage);
|
||||
}
|
||||
Component.onCompleted: {
|
||||
matriqueController.initiated.connect(function() {
|
||||
if (matriqueController.accountCount == 0) stackView.push(loginPage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,42 +2,45 @@ import QtQuick 2.9
|
|||
import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
readonly property bool isFile: eventType === "video" || eventType === "audio" || eventType === "file" || eventType === "image"
|
||||
property var row: null
|
||||
property var model: null
|
||||
|
||||
readonly property bool isFile: model && (model.eventType === "video" || model.eventType === "audio" || model.eventType === "file" || model.eventType === "image")
|
||||
|
||||
id: messageContextMenu
|
||||
|
||||
MenuItem {
|
||||
text: "Copy"
|
||||
|
||||
onTriggered: matriqueController.copyToClipboard(plainText)
|
||||
onTriggered: matriqueController.copyToClipboard(model.plainText)
|
||||
}
|
||||
MenuItem {
|
||||
text: "Copy Source"
|
||||
text: "View Source"
|
||||
|
||||
onTriggered: matriqueController.copyToClipboard(toolTip)
|
||||
onTriggered: {
|
||||
sourceDialog.sourceText = model.toolTip
|
||||
sourceDialog.open()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
visible: isFile
|
||||
height: visible ? undefined : 0
|
||||
text: "Open Externally"
|
||||
|
||||
onTriggered: messageRow.openExternally()
|
||||
onTriggered: row.openExternally()
|
||||
}
|
||||
MenuItem {
|
||||
visible: isFile
|
||||
height: visible ? undefined : 0
|
||||
text: "Save As"
|
||||
|
||||
onTriggered: messageRow.saveFileAs()
|
||||
onTriggered: row.saveFileAs()
|
||||
}
|
||||
MenuItem {
|
||||
visible: sentByMe
|
||||
visible: model && model.author === currentRoom.localUser
|
||||
height: visible ? undefined : 0
|
||||
text: "Redact"
|
||||
|
||||
onTriggered: currentRoom.redactEvent(eventId)
|
||||
onTriggered: currentRoom.redactEvent(model.eventId)
|
||||
}
|
||||
|
||||
Component.onCompleted: popup()
|
||||
onClosed: messageContextMenu.destroy()
|
||||
}
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import Matrique 0.1
|
||||
|
||||
Menu {
|
||||
property var model: null
|
||||
|
||||
id: roomListMenu
|
||||
|
||||
MenuItem {
|
||||
text: "Favourite"
|
||||
checkable: true
|
||||
checked: currentRoom && currentRoom.isFavourite
|
||||
checked: model && model.category === RoomType.Favorite
|
||||
|
||||
onTriggered: currentRoom.isFavourite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", "1")
|
||||
onTriggered: model.category === RoomType.Favorite ? model.currentRoom.removeTag("m.favourite") : model.currentRoom.addTag("m.favourite", "1")
|
||||
}
|
||||
MenuItem {
|
||||
text: "Deprioritize"
|
||||
checkable: true
|
||||
checked: currentRoom && currentRoom.isLowPriority
|
||||
checked: model && model.category === RoomType.Deprioritized
|
||||
|
||||
onTriggered: currentRoom.isLowPriority ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", "1")
|
||||
onTriggered: model.category === RoomType.Deprioritized ? model.currentRoom.removeTag("m.lowpriority") : model.currentRoom.addTag("m.lowpriority", "1")
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: "Mark as Read"
|
||||
|
||||
onTriggered: currentRoom.markAllMessagesAsRead()
|
||||
onTriggered: model.currentRoom.markAllMessagesAsRead()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Leave Room"
|
||||
|
||||
onTriggered: currentRoom.forget()
|
||||
onTriggered: model.currentRoom.forget()
|
||||
}
|
||||
|
||||
Component.onCompleted: popup()
|
||||
onClosed: roomListMenu.destroy()
|
||||
}
|
||||
|
|
2
res.qrc
2
res.qrc
|
@ -6,7 +6,6 @@
|
|||
<file>asset/font/material.ttf</file>
|
||||
<file>qml/Login.qml</file>
|
||||
<file>qml/main.qml</file>
|
||||
<file>qml/component/ImageStatus.qml</file>
|
||||
<file>qml/form/RoomForm.qml</file>
|
||||
<file>qml/Room.qml</file>
|
||||
<file>qml/component/SideNavButton.qml</file>
|
||||
|
@ -30,5 +29,6 @@
|
|||
<file>qml/component/StateDelegate.qml</file>
|
||||
<file>qml/component/AutoLabel.qml</file>
|
||||
<file>qml/component/RoomDrawer.qml</file>
|
||||
<file>js/util.js</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
#include "accountlistmodel.h"
|
||||
|
||||
#include "room.h"
|
||||
|
||||
AccountListModel::AccountListModel(QObject* parent)
|
||||
: QAbstractListModel(parent) {}
|
||||
|
||||
void AccountListModel::setController(Controller* value) {
|
||||
if (m_controller != value) {
|
||||
beginResetModel();
|
||||
m_connections.clear();
|
||||
|
||||
m_controller = value;
|
||||
|
||||
for (auto c : m_controller->connections()) m_connections.append(c);
|
||||
|
||||
connect(m_controller, &Controller::connectionAdded, this,
|
||||
[=](Connection* conn) {
|
||||
if (!conn) {
|
||||
}
|
||||
beginInsertRows(QModelIndex(), m_connections.count(),
|
||||
m_connections.count());
|
||||
m_connections.append(conn);
|
||||
endInsertRows();
|
||||
});
|
||||
connect(m_controller, &Controller::connectionDropped, this,
|
||||
[=](Connection* conn) {
|
||||
qDebug() << "Dropping connection" << conn->userId();
|
||||
if (!conn) {
|
||||
qDebug() << "Trying to remove null connection";
|
||||
return;
|
||||
}
|
||||
conn->disconnect(this);
|
||||
const auto it =
|
||||
std::find(m_connections.begin(), m_connections.end(), conn);
|
||||
if (it == m_connections.end())
|
||||
return; // Already deleted, nothing to do
|
||||
const int row = it - m_connections.begin();
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_connections.erase(it);
|
||||
endRemoveRows();
|
||||
});
|
||||
emit controllerChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant AccountListModel::data(const QModelIndex& index, int role) const {
|
||||
if (!index.isValid()) return QVariant();
|
||||
|
||||
if (index.row() >= m_connections.count()) {
|
||||
qDebug() << "AccountListModel, something's wrong: index.row() >= "
|
||||
"m_users.count()";
|
||||
return QVariant();
|
||||
}
|
||||
auto m_connection = m_connections.at(index.row());
|
||||
if (role == UserRole) {
|
||||
return QVariant::fromValue(m_connection->user());
|
||||
}
|
||||
if (role == ConnectionRole) {
|
||||
return QVariant::fromValue(m_connection);
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
int AccountListModel::rowCount(const QModelIndex& parent) const {
|
||||
if (parent.isValid()) return 0;
|
||||
|
||||
return m_connections.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountListModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[UserRole] = "user";
|
||||
roles[ConnectionRole] = "connection";
|
||||
return roles;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef ACCOUNTLISTMODEL_H
|
||||
#define ACCOUNTLISTMODEL_H
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
|
||||
class AccountListModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(Controller* controller READ controller WRITE setController NOTIFY
|
||||
controllerChanged)
|
||||
public:
|
||||
enum EventRoles { UserRole = Qt::UserRole + 1, ConnectionRole };
|
||||
|
||||
AccountListModel(QObject* parent = nullptr);
|
||||
|
||||
QVariant data(const QModelIndex& index, int role = UserRole) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Controller* controller() { return m_controller; }
|
||||
void setController(Controller* value);
|
||||
|
||||
private:
|
||||
Controller* m_controller;
|
||||
QVector<Connection*> m_connections;
|
||||
|
||||
signals:
|
||||
void controllerChanged();
|
||||
};
|
||||
|
||||
#endif // ACCOUNTLISTMODEL_H
|
|
@ -1,6 +1,8 @@
|
|||
#include "controller.h"
|
||||
|
||||
#include "matriqueroom.h"
|
||||
#include "matriqueuser.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include "events/eventcontent.h"
|
||||
#include "events/roommessageevent.h"
|
||||
|
@ -8,7 +10,21 @@
|
|||
#include "csapi/joining.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QStringBuilder>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtGui/QCloseEvent>
|
||||
#include <QtGui/QDesktopServices>
|
||||
#include <QtGui/QMovie>
|
||||
#include <QtGui/QPixmap>
|
||||
#include <QtNetwork/QAuthenticator>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
Controller::Controller(QObject* parent) : QObject(parent) {
|
||||
tray->setIcon(QIcon(":/asset/img/icon.png"));
|
||||
|
@ -24,87 +40,169 @@ Controller::Controller(QObject* parent) : QObject(parent) {
|
|||
tray->show();
|
||||
|
||||
Connection::setRoomType<MatriqueRoom>();
|
||||
Connection::setUserType<MatriqueUser>();
|
||||
|
||||
connect(m_connection, &Connection::connected, this, &Controller::connected);
|
||||
connect(m_connection, &Connection::resolveError, this,
|
||||
&Controller::reconnect);
|
||||
connect(m_connection, &Connection::syncError, this, &Controller::reconnect);
|
||||
connect(m_connection, &Connection::syncDone, this, &Controller::resync);
|
||||
connect(m_connection, &Connection::connected, this,
|
||||
&Controller::connectionChanged);
|
||||
|
||||
connect(m_connection, &Connection::connected, [=] { setBusy(true); });
|
||||
connect(m_connection, &Connection::syncDone, [=] { setBusy(false); });
|
||||
QTimer::singleShot(0, this, SLOT(invokeLogin()));
|
||||
}
|
||||
|
||||
Controller::~Controller() {
|
||||
m_connection->saveState();
|
||||
m_connection->stopSync();
|
||||
m_connection->deleteLater();
|
||||
}
|
||||
Controller::~Controller() {}
|
||||
|
||||
void Controller::login() {
|
||||
if (!m_isLogin) {
|
||||
m_connection->setHomeserver(QUrl(m_homeserver));
|
||||
m_connection->connectWithToken(m_userID, m_token, "");
|
||||
}
|
||||
inline QString accessTokenFileName(const AccountSettings& account) {
|
||||
QString fileName = account.userId();
|
||||
fileName.replace(':', '_');
|
||||
return QStandardPaths::writableLocation(
|
||||
QStandardPaths::AppLocalDataLocation) +
|
||||
'/' + fileName;
|
||||
}
|
||||
|
||||
void Controller::loginWithCredentials(QString serverAddr, QString user,
|
||||
QString pass) {
|
||||
if (!m_isLogin) {
|
||||
if (!user.isEmpty() && !pass.isEmpty()) {
|
||||
Connection* m_connection = new Connection(this);
|
||||
m_connection->setHomeserver(QUrl(serverAddr));
|
||||
m_connection->connectToServer(user, pass, "");
|
||||
connect(m_connection, &Connection::connected, [=] {
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName("Matrique");
|
||||
if (!saveAccessToken(account, m_connection->accessToken()))
|
||||
qWarning() << "Couldn't save access token";
|
||||
account.sync();
|
||||
addConnection(m_connection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::logout(Connection* conn) {
|
||||
if (!conn) {
|
||||
qCritical() << "Attempt to logout null connection";
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsGroup("Accounts").remove(conn->userId());
|
||||
QFile(accessTokenFileName(AccountSettings(conn->userId()))).remove();
|
||||
|
||||
conn->logout();
|
||||
}
|
||||
|
||||
void Controller::addConnection(Connection* c) {
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
|
||||
m_connections.push_back(c);
|
||||
|
||||
connect(c, &Connection::syncDone, this, [=] {
|
||||
c->saveState();
|
||||
c->sync(30000);
|
||||
});
|
||||
connect(c, &Connection::loggedOut, this, [=] { dropConnection(c); });
|
||||
|
||||
using namespace QMatrixClient;
|
||||
|
||||
c->sync(30000);
|
||||
|
||||
emit connectionAdded(c);
|
||||
}
|
||||
|
||||
void Controller::dropConnection(Connection* c) {
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
|
||||
m_connections.removeOne(c);
|
||||
|
||||
emit connectionDropped(c);
|
||||
c->deleteLater();
|
||||
}
|
||||
|
||||
void Controller::invokeLogin() {
|
||||
using namespace QMatrixClient;
|
||||
const auto accounts = SettingsGroup("Accounts").childGroups();
|
||||
for (const auto& accountId : accounts) {
|
||||
AccountSettings account{accountId};
|
||||
if (!account.homeserver().isEmpty()) {
|
||||
auto accessToken = loadAccessToken(account);
|
||||
if (accessToken.isEmpty()) {
|
||||
// Try to look in the legacy location (QSettings) and if found,
|
||||
// migrate it from there to a file.
|
||||
accessToken = account.accessToken().toLatin1();
|
||||
if (accessToken.isEmpty())
|
||||
continue; // No access token anywhere, no autologin
|
||||
|
||||
saveAccessToken(account, accessToken);
|
||||
account.clearAccessToken(); // Clean the old place
|
||||
}
|
||||
|
||||
auto c = new Connection(account.homeserver(), this);
|
||||
auto deviceName = account.deviceName();
|
||||
connect(c, &Connection::connected, this, [=] {
|
||||
c->loadState();
|
||||
addConnection(c);
|
||||
});
|
||||
c->connectWithToken(account.userId(), accessToken, account.deviceId());
|
||||
}
|
||||
}
|
||||
emit initiated();
|
||||
}
|
||||
|
||||
QByteArray Controller::loadAccessToken(const AccountSettings& account) {
|
||||
QFile accountTokenFile{accessTokenFileName(account)};
|
||||
if (accountTokenFile.open(QFile::ReadOnly)) {
|
||||
if (accountTokenFile.size() < 1024) return accountTokenFile.readAll();
|
||||
|
||||
qWarning() << "File" << accountTokenFile.fileName() << "is"
|
||||
<< accountTokenFile.size()
|
||||
<< "bytes long - too long for a token, ignoring it.";
|
||||
}
|
||||
qWarning() << "Could not open access token file"
|
||||
<< accountTokenFile.fileName();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool Controller::saveAccessToken(const AccountSettings& account,
|
||||
const QByteArray& accessToken) {
|
||||
// (Re-)Make a dedicated file for access_token.
|
||||
QFile accountTokenFile{accessTokenFileName(account)};
|
||||
accountTokenFile.remove(); // Just in case
|
||||
|
||||
auto fileDir = QFileInfo(accountTokenFile).dir();
|
||||
if (!((fileDir.exists() || fileDir.mkpath(".")) &&
|
||||
accountTokenFile.open(QFile::WriteOnly))) {
|
||||
emit errorOccured();
|
||||
} else {
|
||||
qCritical() << "You are already logged in.";
|
||||
// Try to restrict access rights to the file. The below is useless
|
||||
// on Windows: FAT doesn't control access at all and NTFS is
|
||||
// incompatible with the UNIX perms model used by Qt. If the attempt
|
||||
// didn't have the effect, at least ask the user if it's fine to save
|
||||
// the token to a file readable by others.
|
||||
// TODO: use system-specific API to ensure proper access.
|
||||
if ((accountTokenFile.setPermissions(QFile::ReadOwner |
|
||||
QFile::WriteOwner) &&
|
||||
!(accountTokenFile.permissions() &
|
||||
(QFile::ReadGroup | QFile::ReadOther)))) {
|
||||
accountTokenFile.write(accessToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::logout() {
|
||||
m_connection->logout();
|
||||
setUserID("");
|
||||
setToken("");
|
||||
setIsLogin(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void Controller::connected() {
|
||||
setHomeserver(m_connection->homeserver().toString());
|
||||
setUserID(m_connection->userId());
|
||||
setToken(m_connection->accessToken());
|
||||
m_connection->loadState();
|
||||
resync();
|
||||
setIsLogin(true);
|
||||
}
|
||||
|
||||
void Controller::resync() { m_connection->sync(30000); }
|
||||
|
||||
void Controller::reconnect() {
|
||||
qDebug() << "Connection lost. Reconnecting...";
|
||||
m_connection->connectWithToken(m_userID, m_token, "");
|
||||
}
|
||||
|
||||
void Controller::joinRoom(const QString& alias) {
|
||||
JoinRoomJob* joinRoomJob = m_connection->joinRoom(alias);
|
||||
void Controller::joinRoom(Connection* c, const QString& alias) {
|
||||
JoinRoomJob* joinRoomJob = c->joinRoom(alias);
|
||||
setBusy(true);
|
||||
joinRoomJob->connect(joinRoomJob, &JoinRoomJob::finished,
|
||||
[=] { setBusy(false); });
|
||||
}
|
||||
|
||||
void Controller::createRoom(const QString& name, const QString& topic) {
|
||||
void Controller::createRoom(Connection* c, const QString& name,
|
||||
const QString& topic) {
|
||||
CreateRoomJob* createRoomJob =
|
||||
((Connection*)m_connection)
|
||||
->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
setBusy(true);
|
||||
createRoomJob->connect(createRoomJob, &CreateRoomJob::finished,
|
||||
[=] { setBusy(false); });
|
||||
}
|
||||
|
||||
void Controller::createDirectChat(const QString& userID) {
|
||||
m_connection->requestDirectChat(userID);
|
||||
}
|
||||
|
||||
void Controller::copyToClipboard(const QString& text) {
|
||||
m_clipboard->setText(text);
|
||||
}
|
||||
|
@ -120,3 +218,16 @@ void Controller::showMessage(const QString& title, const QString& msg,
|
|||
const QIcon& icon) {
|
||||
tray->showMessage(title, msg, icon);
|
||||
}
|
||||
|
||||
QImage Controller::safeImage(QImage image) {
|
||||
if (image.isNull()) return QImage();
|
||||
return image;
|
||||
}
|
||||
|
||||
QColor Controller::color(QString userId) {
|
||||
return QColor(SettingsGroup("UI/Color").value(userId, "#498882").toString());
|
||||
}
|
||||
|
||||
void Controller::setColor(QString userId, QColor newColor) {
|
||||
SettingsGroup("UI/Color").setValue(userId, newColor.name());
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define CONTROLLER_H
|
||||
|
||||
#include "connection.h"
|
||||
#include "settings.h"
|
||||
#include "user.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
@ -15,101 +16,71 @@ using namespace QMatrixClient;
|
|||
class Controller : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Connection* connection READ connection CONSTANT)
|
||||
Q_PROPERTY(bool isLogin READ isLogin WRITE setIsLogin NOTIFY isLoginChanged)
|
||||
Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY
|
||||
homeserverChanged)
|
||||
Q_PROPERTY(QString userID READ userID WRITE setUserID NOTIFY userIDChanged)
|
||||
Q_PROPERTY(QByteArray token READ token WRITE setToken NOTIFY tokenChanged)
|
||||
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
|
||||
Q_PROPERTY(int accountCount READ accountCount NOTIFY connectionAdded NOTIFY
|
||||
connectionDropped)
|
||||
|
||||
public:
|
||||
explicit Controller(QObject* parent = nullptr);
|
||||
~Controller();
|
||||
|
||||
// All the Q_INVOKABLEs.
|
||||
Q_INVOKABLE void login();
|
||||
Q_INVOKABLE void loginWithCredentials(QString, QString, QString);
|
||||
Q_INVOKABLE void logout();
|
||||
|
||||
QVector<Connection*> connections() { return m_connections; }
|
||||
|
||||
// All the non-Q_INVOKABLE functions.
|
||||
void addConnection(Connection* c);
|
||||
void dropConnection(Connection* c);
|
||||
|
||||
// All the Q_PROPERTYs.
|
||||
Connection* m_connection = new Connection();
|
||||
Connection* connection() { return m_connection; }
|
||||
|
||||
bool isLogin() { return m_isLogin; }
|
||||
void setIsLogin(bool n) {
|
||||
if (n != m_isLogin) {
|
||||
m_isLogin = n;
|
||||
emit isLoginChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString userID() { return m_userID; }
|
||||
void setUserID(QString n) {
|
||||
if (n != m_userID) {
|
||||
m_userID = n;
|
||||
emit userIDChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray token() { return m_token; }
|
||||
void setToken(QByteArray n) {
|
||||
if (n != m_token) {
|
||||
m_token = n;
|
||||
emit tokenChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString homeserver() { return m_homeserver; }
|
||||
void setHomeserver(QString n) {
|
||||
if (n != m_homeserver) {
|
||||
m_homeserver = n;
|
||||
emit homeserverChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool busy() { return m_busy; }
|
||||
void setBusy(bool b) {
|
||||
if (b != m_busy) {
|
||||
m_busy = b;
|
||||
void setBusy(bool value) {
|
||||
if (value != m_busy) {
|
||||
m_busy = value;
|
||||
emit busyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int accountCount() { return m_connections.count(); }
|
||||
|
||||
Q_INVOKABLE QColor color(QString userId);
|
||||
Q_INVOKABLE void setColor(QString userId, QColor newColor);
|
||||
|
||||
private:
|
||||
QClipboard* m_clipboard = QApplication::clipboard();
|
||||
QSystemTrayIcon* tray = new QSystemTrayIcon();
|
||||
QMenu* trayMenu = new QMenu();
|
||||
QVector<Connection*> m_connections;
|
||||
|
||||
bool m_isLogin = false;
|
||||
QString m_userID;
|
||||
QByteArray m_token;
|
||||
QString m_homeserver;
|
||||
bool m_busy = false;
|
||||
|
||||
void connected();
|
||||
void resync();
|
||||
void reconnect();
|
||||
QByteArray loadAccessToken(const AccountSettings& account);
|
||||
bool saveAccessToken(const AccountSettings& account,
|
||||
const QByteArray& accessToken);
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
|
||||
private slots:
|
||||
void invokeLogin();
|
||||
|
||||
signals:
|
||||
void connectionChanged();
|
||||
void isLoginChanged();
|
||||
void userIDChanged();
|
||||
void tokenChanged();
|
||||
void homeserverChanged();
|
||||
void busyChanged();
|
||||
void errorOccured();
|
||||
void toggleWindow();
|
||||
void connectionAdded(Connection* conn);
|
||||
void connectionDropped(Connection* conn);
|
||||
void initiated();
|
||||
|
||||
public slots:
|
||||
void joinRoom(const QString& alias);
|
||||
void createRoom(const QString& name, const QString& topic);
|
||||
void createDirectChat(const QString& userID);
|
||||
void logout(Connection* conn);
|
||||
void joinRoom(Connection* c, const QString& alias);
|
||||
void createRoom(Connection* c, const QString& name, const QString& topic);
|
||||
void copyToClipboard(const QString& text);
|
||||
void playAudio(QUrl localFile);
|
||||
void showMessage(const QString& title, const QString& msg, const QIcon& icon);
|
||||
|
||||
static QImage safeImage(QImage image);
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_H
|
||||
|
|
|
@ -14,8 +14,12 @@ void ImageItem::paint(QPainter *painter) {
|
|||
if (m_image.isNull()) {
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(m_color));
|
||||
if (m_round)
|
||||
painter->drawEllipse(0, 0, int(bounding_rect.width()),
|
||||
int(bounding_rect.height()));
|
||||
else
|
||||
painter->drawRect(0, 0, int(bounding_rect.width()),
|
||||
int(bounding_rect.height()));
|
||||
painter->setPen(QPen(Qt::white, 2));
|
||||
QFont font;
|
||||
font.setPixelSize(22);
|
||||
|
@ -33,11 +37,13 @@ void ImageItem::paint(QPainter *painter) {
|
|||
|
||||
QPointF center = bounding_rect.center() - scaled.rect().center();
|
||||
|
||||
if (m_round) {
|
||||
QPainterPath clip;
|
||||
clip.addEllipse(
|
||||
0, 0, bounding_rect.width(),
|
||||
bounding_rect.height()); // this is the shape we want to clip to
|
||||
painter->setClipPath(clip);
|
||||
}
|
||||
|
||||
if (center.x() < 0) center.setX(0);
|
||||
if (center.y() < 0) center.setY(0);
|
||||
|
@ -66,3 +72,11 @@ void ImageItem::setDefaultColor(QString color) {
|
|||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageItem::setRound(bool value) {
|
||||
if (m_round != value) {
|
||||
m_round = value;
|
||||
emit roundChanged();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ class ImageItem : public QQuickPaintedItem {
|
|||
Q_PROPERTY(QString hint READ hint WRITE setHint NOTIFY hintChanged)
|
||||
Q_PROPERTY(QString defaultColor READ defaultColor WRITE setDefaultColor NOTIFY
|
||||
defaultColorChanged)
|
||||
Q_PROPERTY(bool round READ round WRITE setRound NOTIFY roundChanged)
|
||||
|
||||
public:
|
||||
ImageItem(QQuickItem *parent = nullptr);
|
||||
|
@ -28,15 +29,20 @@ class ImageItem : public QQuickPaintedItem {
|
|||
QString defaultColor() { return m_color; }
|
||||
void setDefaultColor(QString color);
|
||||
|
||||
bool round() { return m_round; }
|
||||
void setRound(bool value);
|
||||
|
||||
signals:
|
||||
void imageChanged();
|
||||
void hintChanged();
|
||||
void defaultColorChanged();
|
||||
void roundChanged();
|
||||
|
||||
private:
|
||||
QImage m_image;
|
||||
QString m_hint;
|
||||
QString m_hint = "H";
|
||||
QString m_color = "#000000";
|
||||
bool m_round = true;
|
||||
};
|
||||
|
||||
#endif // IMAGEITEM_H
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include "accountlistmodel.h"
|
||||
#include "controller.h"
|
||||
#include "emojimodel.h"
|
||||
#include "imageitem.h"
|
||||
|
@ -38,6 +39,7 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
qmlRegisterType<ImageItem>("Matrique", 0, 1, "ImageItem");
|
||||
qmlRegisterType<Controller>("Matrique", 0, 1, "Controller");
|
||||
qmlRegisterType<AccountListModel>("Matrique", 0, 1, "AccountListModel");
|
||||
qmlRegisterType<RoomListModel>("Matrique", 0, 1, "RoomListModel");
|
||||
qmlRegisterType<UserListModel>("Matrique", 0, 1, "UserListModel");
|
||||
qmlRegisterType<MessageEventModel>("Matrique", 0, 1, "MessageEventModel");
|
||||
|
|
|
@ -10,6 +10,7 @@ using namespace QMatrixClient;
|
|||
|
||||
class MatriqueRoom : public Room {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QImage avatar READ getAvatar NOTIFY avatarChanged)
|
||||
Q_PROPERTY(bool hasUsersTyping READ hasUsersTyping NOTIFY typingChanged)
|
||||
Q_PROPERTY(QString usersTyping READ getUsersTyping NOTIFY typingChanged)
|
||||
Q_PROPERTY(QString cachedInput READ cachedInput WRITE setCachedInput NOTIFY
|
||||
|
@ -18,6 +19,8 @@ class MatriqueRoom : public Room {
|
|||
explicit MatriqueRoom(Connection* connection, QString roomId,
|
||||
JoinState joinState = {});
|
||||
|
||||
QImage getAvatar() { return avatar(128); }
|
||||
|
||||
const QString& cachedInput() const { return m_cachedInput; }
|
||||
void setCachedInput(const QString& input) {
|
||||
if (input != m_cachedInput) {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
#include "matriqueuser.h"
|
||||
|
||||
MatriqueUser::MatriqueUser(QString userId, Connection* connection)
|
||||
: User(userId, connection) {
|
||||
connect(this, &User::avatarChanged, this, &MatriqueUser::inheritedAvatarChanged);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
#ifndef MATRIQUEUSER_H
|
||||
#define MATRIQUEUSER_H
|
||||
|
||||
#include "user.h"
|
||||
#include "room.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
using namespace QMatrixClient;
|
||||
|
||||
class MatriqueUser : public User {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QImage avatar READ getAvatar NOTIFY inheritedAvatarChanged)
|
||||
public:
|
||||
MatriqueUser(QString userId, Connection* connection);
|
||||
|
||||
QImage getAvatar() { return avatar(128); }
|
||||
|
||||
signals:
|
||||
void inheritedAvatarChanged(User* user, const Room* roomContext); // https://bugreports.qt.io/browse/QTBUG-7684
|
||||
};
|
||||
|
||||
#endif // MATRIQUEUSER_H
|
|
@ -15,6 +15,7 @@
|
|||
QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||
roles[EventTypeRole] = "eventType";
|
||||
roles[AboveEventTypeRole] = "aboveEventType";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[AboveTimeRole] = "aboveTime";
|
||||
|
@ -31,6 +32,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
|
|||
roles[AnnotationRole] = "annotation";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[PlainTextRole] = "plainText";
|
||||
roles[UserMarkerRole] = "userMarker";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
@ -51,7 +53,6 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
|
|||
beginResetModel();
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
qDebug() << "Disconnected from" << m_currentRoom->id();
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
|
@ -78,7 +79,8 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
|
|||
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() -
|
||||
biggest + timelineBaseIndex() - 1;
|
||||
refreshEventRoles(rowBelowInserted,
|
||||
{AboveAuthorRole, AboveSectionRole});
|
||||
{AboveEventTypeRole, AboveAuthorRole,
|
||||
AboveSectionRole, AboveTimeRole});
|
||||
}
|
||||
for (auto i = m_currentRoom->maxTimelineIndex() - biggest;
|
||||
i <= m_currentRoom->maxTimelineIndex() - lowest; ++i)
|
||||
|
@ -108,7 +110,8 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
|
|||
refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole});
|
||||
if (timelineBaseIndex() > 0) // Refresh below, see #312
|
||||
refreshEventRoles(timelineBaseIndex() - 1,
|
||||
{AboveAuthorRole, AboveSectionRole});
|
||||
{AboveEventTypeRole, AboveAuthorRole,
|
||||
AboveSectionRole, AboveTimeRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventChanged, this,
|
||||
&MessageEventModel::refreshRow);
|
||||
|
@ -135,6 +138,11 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
|
|||
&MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferCancelled, this,
|
||||
&MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::readMarkerForUserMoved, this,
|
||||
[=](User* user, QString fromEventId, QString toEventId) {
|
||||
refreshEventRoles(fromEventId, {UserMarkerRole});
|
||||
refreshEventRoles(toEventId, {UserMarkerRole});
|
||||
});
|
||||
qDebug() << "Connected to room" << room->id() << "as"
|
||||
<< room->localUser()->id();
|
||||
} else
|
||||
|
@ -371,7 +379,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||
: tr("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? tr("has put %1 out of the room").arg(subjectName)
|
||||
? tr("has kicked %1 from the room").arg(subjectName)
|
||||
: tr("left the room");
|
||||
case MembershipType::Ban:
|
||||
return (e.senderId() != e.userId())
|
||||
|
@ -470,7 +478,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||
: tr("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? tr("has put %1 out of the room").arg(subjectName)
|
||||
? tr("has kicked %1 from the room").arg(subjectName)
|
||||
: tr("left the room");
|
||||
case MembershipType::Ban:
|
||||
return (e.senderId() != e.userId())
|
||||
|
@ -622,14 +630,29 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
|
|||
return role == TimeRole ? QVariant(ts) : renderDate(ts);
|
||||
}
|
||||
|
||||
if (role == AboveSectionRole || role == AboveAuthorRole ||
|
||||
role == AboveTimeRole)
|
||||
if (role == UserMarkerRole) {
|
||||
QVariantList variantList;
|
||||
for (User* user : m_currentRoom->usersAtEventId(evt.id())) {
|
||||
if (user == m_currentRoom->localUser()) continue;
|
||||
variantList.append(QVariant::fromValue(user));
|
||||
}
|
||||
return variantList;
|
||||
}
|
||||
|
||||
if (role == AboveEventTypeRole || role == AboveSectionRole ||
|
||||
role == AboveAuthorRole || role == AboveTimeRole)
|
||||
for (auto r = row + 1; r < rowCount(); ++r) {
|
||||
auto i = index(r);
|
||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden)
|
||||
return data(i, role == AboveSectionRole
|
||||
? SectionRole
|
||||
: role == AboveAuthorRole ? AuthorRole : TimeRole);
|
||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden) switch (role) {
|
||||
case AboveEventTypeRole:
|
||||
return data(i, EventTypeRole);
|
||||
case AboveSectionRole:
|
||||
return data(i, SectionRole);
|
||||
case AboveAuthorRole:
|
||||
return data(i, AuthorRole);
|
||||
case AboveTimeRole:
|
||||
return data(i, TimeRole);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
#ifndef MESSAGEEVENTMODEL_H
|
||||
#define MESSAGEEVENTMODEL_H
|
||||
|
||||
#include "room.h"
|
||||
#include "matriqueroom.h"
|
||||
#include "room.h"
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
|
||||
class MessageEventModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(
|
||||
MatriqueRoom* room READ getRoom WRITE setRoom NOTIFY roomChanged)
|
||||
Q_PROPERTY(MatriqueRoom* room READ getRoom WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
public:
|
||||
enum EventRoles {
|
||||
EventTypeRole = Qt::UserRole + 1,
|
||||
AboveEventTypeRole,
|
||||
EventIdRole,
|
||||
TimeRole,
|
||||
AboveTimeRole,
|
||||
|
@ -29,6 +29,7 @@ class MessageEventModel : public QAbstractListModel {
|
|||
LongOperationRole,
|
||||
AnnotationRole,
|
||||
PlainTextRole,
|
||||
UserMarkerRole,
|
||||
// For debugging
|
||||
EventResolvedTypeRole,
|
||||
};
|
||||
|
@ -42,7 +43,7 @@ class MessageEventModel : public QAbstractListModel {
|
|||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index,
|
||||
int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private slots:
|
||||
int refreshEvent(const QString& eventId);
|
||||
|
|
|
@ -14,12 +14,20 @@ RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) {}
|
|||
RoomListModel::~RoomListModel() {}
|
||||
|
||||
void RoomListModel::setConnection(Connection* connection) {
|
||||
if (!connection && connection == m_connection) return;
|
||||
if (connection == m_connection) return;
|
||||
if (m_connection) m_connection->disconnect(this);
|
||||
if (!connection) {
|
||||
qDebug() << "Removing current connection...";
|
||||
m_connection = nullptr;
|
||||
beginResetModel();
|
||||
m_rooms.clear();
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
using QMatrixClient::Room;
|
||||
m_connection = connection;
|
||||
|
||||
doResetModel();
|
||||
for (MatriqueRoom* room : m_rooms) room->disconnect(this);
|
||||
|
||||
connect(connection, &Connection::connected, this,
|
||||
&RoomListModel::doResetModel);
|
||||
|
@ -30,6 +38,8 @@ void RoomListModel::setConnection(Connection* connection) {
|
|||
connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom);
|
||||
connect(connection, &Connection::aboutToDeleteRoom, this,
|
||||
&RoomListModel::deleteRoom);
|
||||
|
||||
doResetModel();
|
||||
}
|
||||
|
||||
void RoomListModel::doResetModel() {
|
||||
|
@ -64,7 +74,7 @@ void RoomListModel::connectRoomSignals(MatriqueRoom* room) {
|
|||
[=] { refresh(room, {AvatarRole}); });
|
||||
connect(room, &Room::addedMessages, this,
|
||||
[=] { refresh(room, {LastEventRole}); });
|
||||
connect(room, &QMatrixClient::Room::aboutToAddNewMessages, this,
|
||||
connect(room, &Room::aboutToAddNewMessages, this,
|
||||
[=](QMatrixClient::RoomEventsRange eventsRange) {
|
||||
RoomEvent* event = (eventsRange.end() - 1)->get();
|
||||
if (event->isStateEvent()) return;
|
||||
|
@ -139,7 +149,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
|
|||
MatriqueRoom* room = m_rooms.at(index.row());
|
||||
if (role == NameRole) return room->displayName();
|
||||
if (role == AvatarRole) {
|
||||
if (room->avatarUrl().toString() != "") return room->avatar(64, 64);
|
||||
if (!room->avatarUrl().isEmpty()) return room->avatar(64, 64);
|
||||
return QImage();
|
||||
}
|
||||
if (role == TopicRole) return room->topic();
|
||||
|
|
|
@ -67,8 +67,13 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const {
|
|||
if (role == NameRole) {
|
||||
return user->displayname(m_currentRoom);
|
||||
}
|
||||
if (role == UserIDRole) {
|
||||
return user->id();
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
return user->avatarUrl(m_currentRoom);
|
||||
if (!user->avatarUrl(m_currentRoom).isEmpty())
|
||||
return user->avatar(64, m_currentRoom);
|
||||
return QImage();
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
|
@ -110,7 +115,7 @@ void UserListModel::refresh(QMatrixClient::User* user, QVector<int> roles) {
|
|||
|
||||
void UserListModel::avatarChanged(QMatrixClient::User* user,
|
||||
const QMatrixClient::Room* context) {
|
||||
if (context == m_currentRoom) refresh(user, {Qt::DecorationRole});
|
||||
if (context == m_currentRoom) refresh(user, {AvatarRole});
|
||||
}
|
||||
|
||||
int UserListModel::findUserPos(User* user) const {
|
||||
|
@ -124,6 +129,7 @@ int UserListModel::findUserPos(const QString& username) const {
|
|||
QHash<int, QByteArray> UserListModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[NameRole] = "name";
|
||||
roles[UserIDRole] = "userId";
|
||||
roles[AvatarRole] = "avatar";
|
||||
return roles;
|
||||
}
|
||||
|
|
|
@ -17,10 +17,7 @@ class UserListModel : public QAbstractListModel {
|
|||
Q_PROPERTY(
|
||||
QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
public:
|
||||
enum EventRoles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
AvatarRole
|
||||
};
|
||||
enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole };
|
||||
|
||||
using User = QMatrixClient::User;
|
||||
|
||||
|
@ -30,8 +27,7 @@ class UserListModel : public QAbstractListModel {
|
|||
void setRoom(QMatrixClient::Room* room);
|
||||
User* userAt(QModelIndex index);
|
||||
|
||||
QVariant data(const QModelIndex& index,
|
||||
int role = NameRole) const override;
|
||||
QVariant data(const QModelIndex& index, int role = NameRole) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
|
Loading…
Reference in New Issue