Merge branch 'multilogin' into 'master'

Multilogin

See merge request b0/matrique!17
This commit is contained in:
Black Hat 2018-09-13 05:38:50 +00:00
commit 816380e9d0
35 changed files with 901 additions and 467 deletions

@ -1 +1 @@
Subproject commit d9ff200ff62fb7f5b6b51082dc3979d5454a1bec Subproject commit 875514ee865b00be67542849f94d2c2561ba4137

View File

@ -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}; 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 markdown_parser = function(str){
var rules = [ var rules = [

25
js/util.js Normal file
View File

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

View File

@ -7,7 +7,13 @@ CONFIG += object_parallel_to_source
TARGET = matrique TARGET = matrique
packagesExist(QMatrixClient) {
message("Found libQMatrixClient via pkg-config.")
CONFIG += link_pkgconfig
PKGCONFIG += QMatrixClient
} else {
include(include/libqmatrixclient/libqmatrixclient.pri) include(include/libqmatrixclient/libqmatrixclient.pri)
}
include(include/SortFilterProxyModel/SortFilterProxyModel.pri) include(include/SortFilterProxyModel/SortFilterProxyModel.pri)
# The following define makes your compiler emit warnings if you use # The following define makes your compiler emit warnings if you use
@ -29,7 +35,9 @@ SOURCES += src/main.cpp \
src/emojimodel.cpp \ src/emojimodel.cpp \
src/matriqueroom.cpp \ src/matriqueroom.cpp \
src/userlistmodel.cpp \ src/userlistmodel.cpp \
src/imageitem.cpp src/imageitem.cpp \
src/accountlistmodel.cpp \
src/matriqueuser.cpp
RESOURCES += \ RESOURCES += \
res.qrc res.qrc
@ -67,19 +75,19 @@ mac {
ICON = asset/img/icon.icns ICON = asset/img/icon.icns
} }
DISTFILES += \ #DISTFILES += \
ChatForm.qml \ # ChatForm.qml \
LoginForm.qml \ # LoginForm.qml \
main.qml \ # main.qml \
Home.qml \ # Home.qml \
Login.qml \ # Login.qml \
ImageStatus.qml \ # ImageStatus.qml \
ButtonDelegate.qml \ # ButtonDelegate.qml \
SideNav.qml \ # SideNav.qml \
RoomListForm.qml \ # RoomListForm.qml \
Room.qml \ # Room.qml \
Setting.qml \ # Setting.qml \
qml/js/md.js \ # qml/js/md.js \
HEADERS += \ HEADERS += \
src/controller.h \ src/controller.h \
@ -89,4 +97,6 @@ HEADERS += \
src/emojimodel.h \ src/emojimodel.h \
src/matriqueroom.h \ src/matriqueroom.h \
src/userlistmodel.h \ src/userlistmodel.h \
src/imageitem.h src/imageitem.h \
src/accountlistmodel.h \
src/matriqueuser.h

View File

@ -151,12 +151,9 @@ Page {
return 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.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
controller.connectionAdded.connect(function() { stackView.pop() })
} }
} }
} }

View File

@ -4,7 +4,7 @@ import Qt.labs.settings 1.0
Settings { Settings {
property bool lazyLoad: true property bool lazyLoad: true
property bool richText property bool richText: true
property bool pressAndHold property bool pressAndHold
property bool rearrangeByActivity property bool rearrangeByActivity

View File

@ -1,6 +1,7 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Matrique 0.1 import Matrique 0.1
import Matrique.Settings 0.1 import Matrique.Settings 0.1

View File

@ -2,56 +2,184 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Matrique 0.1
import Matrique.Settings 0.1 import Matrique.Settings 0.1
import "component" import "component"
import "form" import "form"
import "qrc:/js/util.js" as Util
Page { Page {
property var connection property alias listModel: accountSettingsListView.model
Page { Page {
id: accountForm id: accountForm
parent: null parent: null
padding: 64 padding: 64
ColumnLayout { ColumnLayout {
RowLayout { anchors.fill: parent
Layout.preferredHeight: 60
ImageStatus { ListView {
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 {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Label { id: accountSettingsListView
font.pointSize: 18
text: matriqueController.isLogin ? connection.localUser.displayName : "" 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 { Label {
font.pointSize: 12 text: user.displayName
text: matriqueController.isLogin ? connection.localUser.id : "" }
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 { Button {
text: "Logout" Layout.fillWidth: true
text: "Add Account"
flat: true
highlighted: true highlighted: true
onClicked: { onClicked: stackView.push(loginPage)
matriqueController.logout()
Qt.quit()
}
} }
} }
} }
@ -61,6 +189,8 @@ Page {
parent: null parent: null
padding: 64
Column { Column {
Switch { Switch {
text: "Lazy load at initial sync" text: "Lazy load at initial sync"
@ -90,6 +220,8 @@ Page {
parent: null parent: null
padding: 64
Column { Column {
Switch { Switch {
text: "Dark theme" text: "Dark theme"
@ -116,6 +248,7 @@ Page {
Page { Page {
id: aboutForm id: aboutForm
parent: null parent: null
padding: 64 padding: 64
@ -133,49 +266,53 @@ Page {
} }
} }
RowLayout { Rectangle {
ColumnLayout { width: 240
Layout.preferredWidth: 240 height: parent.height
Layout.fillHeight: true
spacing: 0 id: settingDrawer
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
Column {
anchors.fill: parent
ItemDelegate { ItemDelegate {
Layout.fillWidth: true width: parent.width
text: "Account" text: "Account"
onClicked: pushToStack(accountForm) onClicked: pushToStack(accountForm)
} }
ItemDelegate { ItemDelegate {
Layout.fillWidth: true width: parent.width
text: "General" text: "General"
onClicked: pushToStack(generalForm) onClicked: pushToStack(generalForm)
} }
ItemDelegate { ItemDelegate {
Layout.fillWidth: true width: parent.width
text: "Appearance" text: "Appearance"
onClicked: pushToStack(appearanceForm) onClicked: pushToStack(appearanceForm)
} }
ItemDelegate { ItemDelegate {
Layout.fillWidth: true width: parent.width
text: "About" text: "About"
onClicked: pushToStack(aboutForm) onClicked: pushToStack(aboutForm)
} }
} }
}
StackView { StackView {
Layout.fillWidth: true anchors.fill: parent
Layout.fillHeight: true anchors.leftMargin: settingDrawer.width
id: settingStackView id: settingStackView
} }
}
function pushToStack(item) { function pushToStack(item) {
settingStackView.clear() settingStackView.clear()

View File

@ -15,7 +15,11 @@ Control {
AutoMouseArea { AutoMouseArea {
anchors.fill: parent 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 } background: Rectangle { color: colored ? Material.accent : highlighted ? Material.primary : backgroundColor }

View File

@ -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;
}
}

View File

@ -3,20 +3,13 @@ import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Matrique.Settings 0.1 import Matrique.Settings 0.1
Item {
property alias icon: iconText.text
property var color: MSettings.darkTheme ? "white" : "black"
id: item
Text { Text {
anchors.fill: parent property alias icon: materialLabel.text
id: materialLabel
id: iconText
font.pointSize: 16 font.pointSize: 16
font.family: materialFont.name font.family: materialFont.name
color: item.color
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
}

View File

@ -4,9 +4,10 @@ import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import Matrique 0.1 import Matrique 0.1
import Matrique.Settings 0.1 import Matrique.Settings 0.1
import "qrc:/js/util.js" as Util
RowLayout { 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 highlighted: !(sentByMe || eventType === "notice" )
readonly property bool sentByMe: author === currentRoom.localUser readonly property bool sentByMe: author === currentRoom.localUser
readonly property bool isText: eventType === "notice" || eventType === "message" readonly property bool isText: eventType === "notice" || eventType === "message"
@ -22,15 +23,16 @@ RowLayout {
spacing: 6 spacing: 6
ImageStatus { ImageItem {
Layout.preferredWidth: 40 Layout.preferredWidth: 40
Layout.preferredHeight: 40 Layout.preferredHeight: 40
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
round: false round: false
visible: avatarVisible visible: avatarVisible
source: author.avatarUrl != "" ? "image://mxc/" + author.avatarUrl : null hint: author.displayName
displayText: author.displayName defaultColor: Util.stringToColor(author.displayName)
image: author.avatar
} }
Rectangle { Rectangle {
@ -61,6 +63,11 @@ RowLayout {
Material.foreground: Material.accent Material.foreground: Material.accent
coloredBackground: highlighted coloredBackground: highlighted
font.bold: true font.bold: true
MouseArea {
anchors.fill: parent
onClicked: inputField.insert(inputField.cursorPosition, author.displayName)
}
} }
AutoLabel { AutoLabel {
@ -86,14 +93,30 @@ RowLayout {
active: eventType === "image" || eventType === "file" || eventType === "audio" active: eventType === "image" || eventType === "file" || eventType === "audio"
} }
AutoLabel { Row {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
spacing: 8
AutoLabel {
id: timeLabel
visible: Math.abs(time - aboveTime) > 600000 || index == 0 visible: Math.abs(time - aboveTime) > 600000 || index == 0
text: Qt.formatTime(time, "hh:mm") text: Qt.formatTime(time, "hh:mm")
coloredBackground: highlighted coloredBackground: highlighted
Material.foreground: "grey" Material.foreground: "grey"
font.pointSize: 8 font.pointSize: 8
} }
MaterialIcon {
height: timeLabel.height
visible: userMarker.length > 0
icon: "\ue5ca"
color: highlighted ? "white": Material.foreground
font.pointSize: 12
}
}
} }
Component { Component {

View File

@ -4,6 +4,8 @@ import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Matrique 0.1 import Matrique 0.1
import "qrc:/js/util.js" as Util
Drawer { Drawer {
property var room property var room
@ -22,13 +24,14 @@ Drawer {
anchors.fill: parent anchors.fill: parent
anchors.margins: 32 anchors.margins: 32
ImageStatus { ImageItem {
Layout.preferredWidth: 64 Layout.preferredWidth: 64
Layout.preferredHeight: 64 Layout.preferredHeight: 64
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
source: room && room.avatarUrl != "" ? "image://mxc/" + room.avatarUrl : null hint: room ? room.displayName : "No name"
displayText: room ? room.displayName : "" defaultColor: Util.stringToColor(room ? room.displayName : "No name")
image: matriqueController.safeImage(room ? room.avatar : null)
} }
Label { Label {
@ -45,6 +48,13 @@ Drawer {
text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias" 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 { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -53,6 +63,7 @@ Drawer {
id: roomNameField id: roomNameField
text: room && room.name ? room.name : "" text: room && room.name ? room.name : ""
selectByMouse: true
} }
ItemDelegate { ItemDelegate {
@ -74,6 +85,7 @@ Drawer {
id: roomTopicField id: roomTopicField
text: room && room.topic ? room.topic : "" text: room && room.topic ? room.topic : ""
selectByMouse: true
} }
ItemDelegate { ItemDelegate {
@ -94,7 +106,11 @@ Drawer {
boundsBehavior: Flickable.DragOverBounds boundsBehavior: Flickable.DragOverBounds
delegate: ItemDelegate { model: UserListModel {
room: roomDrawer.room
}
delegate: SwipeDelegate {
width: parent.width width: parent.width
height: 48 height: 48
@ -103,12 +119,13 @@ Drawer {
anchors.margins: 8 anchors.margins: 8
spacing: 12 spacing: 12
ImageStatus { ImageItem {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
source: avatar != "" ? "image://mxc/" + avatar : "" defaultColor: Util.stringToColor(name)
displayText: name image: avatar
hint: name
} }
Label { Label {
@ -117,12 +134,25 @@ Drawer {
text: name 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 { SwipeDelegate.onClicked: room.kickMember(userId)
id: userListModel }
room: roomDrawer.room onClicked: inputField.insert(inputField.cursorPosition, name)
} }
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {}

View File

@ -3,29 +3,23 @@ import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import "qrc:/js/util.js" as Util
ItemDelegate { ItemDelegate {
property var page property var page
readonly property bool selected: stackView.currentItem === page property bool selected: stackView.currentItem === page
property color highlightColor: Material.accent
Rectangle { Rectangle {
width: selected ? 4 : 0 width: selected ? 4 : 0
height: parent.height height: parent.height
color: Material.accent color: highlightColor
Behavior on width { Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 } PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
} }
} }
onClicked: { onClicked: Util.pushToStack(stackView, page)
if(page && stackView.currentItem !== page) {
if(stackView.depth === 1) {
stackView.replace(page)
} else {
stackView.clear()
stackView.push(page)
}
}
}
} }

View File

@ -1,14 +1,15 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import Matrique 0.1 import Matrique 0.1
import Matrique.Settings 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/md.js" as Markdown
import "qrc:/js/util.js" as Util
Item { Item {
property var currentRoom: null property var currentRoom: null
@ -57,12 +58,13 @@ Item {
spacing: 12 spacing: 12
ImageStatus { ImageItem {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
source: currentRoom && currentRoom.avatarUrl != "" ? "image://mxc/" + currentRoom.avatarUrl : null hint: currentRoom ? currentRoom.displayName : "No name"
displayText: currentRoom ? currentRoom.displayName : "" defaultColor: Util.stringToColor(currentRoom ? currentRoom.displayName : "No name")
image: matriqueController.safeImage(currentRoom ? currentRoom.avatar : null)
} }
ColumnLayout { ColumnLayout {
@ -87,7 +89,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
text: currentRoom ? currentRoom.topic : "" text: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
color: "white" color: "white"
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
@ -223,6 +225,36 @@ Item {
Behavior on opacity { NumberAnimation { duration: 200 } } 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 { ScrollBar {

View File

@ -8,7 +8,9 @@ import Matrique 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import Matrique.Settings 0.1 import Matrique.Settings 0.1
import "../component" import "qrc:/qml/component"
import "qrc:/qml/menu"
import "qrc:/js/util.js" as Util
Item { Item {
property alias listModel: roomListProxyModel.sourceModel property alias listModel: roomListProxyModel.sourceModel
@ -38,7 +40,7 @@ Item {
bottomPadding: 0 bottomPadding: 0
placeholderText: "Search..." placeholderText: "Search..."
background: Rectangle { color: MSettings.darkTheme ? "#282828" : "#fafafa" } background: Rectangle { color: MSettings.darkTheme ? "#303030" : "#fafafa" }
Shortcut { Shortcut {
sequence: StandardKey.Find sequence: StandardKey.Find
@ -106,15 +108,25 @@ Item {
width: parent.width width: parent.width
height: 64 height: 64
color: MSettings.darkTheme ? "#282828" : "#fafafa" color: MSettings.darkTheme ? "#303030" : "#fafafa"
AutoMouseArea { AutoMouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: MSettings.miniMode hoverEnabled: MSettings.miniMode
onSecondaryClicked: Qt.createComponent("qrc:/qml/menu/RoomContextMenu.qml").createObject(this) onSecondaryClicked: {
onPrimaryClicked: category === RoomType.Invited ? inviteDialog.open() : enteredRoom = currentRoom roomContextMenu.model = model
roomContextMenu.popup()
}
onPrimaryClicked: {
if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom
inviteDialog.open()
} else {
enteredRoom = currentRoom
}
}
ToolTip.visible: MSettings.miniMode && containsMouse ToolTip.visible: MSettings.miniMode && containsMouse
ToolTip.text: name ToolTip.text: name
@ -129,11 +141,14 @@ Item {
} }
Rectangle { Rectangle {
width: 4 width: unreadCount > 0 || highlighted ? 4 : 0
height: parent.height height: parent.height
color: Material.accent color: Material.accent
visible: unreadCount > 0 || highlighted
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
} }
RowLayout { RowLayout {
@ -142,14 +157,6 @@ Item {
spacing: 12 spacing: 12
// ImageStatus {
// Layout.preferredWidth: height
// Layout.fillHeight: true
// source: avatar ? "image://mxc/" + avatar : ""
// displayText: name
// }
ImageItem { ImageItem {
id: imageItem id: imageItem
@ -157,7 +164,7 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
hint: name || "No Name" hint: name || "No Name"
defaultColor: stringToColor(name || "No Name") defaultColor: Util.stringToColor(name || "No Name")
image: avatar image: avatar
} }
@ -183,7 +190,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: 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 elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
} }
@ -205,7 +212,11 @@ Item {
horizontalAlignment: MSettings.miniMode ? Text.AlignHCenter : undefined horizontalAlignment: MSettings.miniMode ? Text.AlignHCenter : undefined
} }
RoomContextMenu { id: roomContextMenu }
Dialog { Dialog {
property var currentRoom
id: inviteDialog id: inviteDialog
parent: ApplicationWindow.overlay 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;
}
} }

View File

@ -9,9 +9,10 @@ import Matrique.Settings 0.1
import "component" import "component"
import "form" import "form"
import "qrc:/js/util.js" as Util
ApplicationWindow { ApplicationWindow {
readonly property var connection: matriqueController.connection readonly property var currentConnection: accountListView.currentConnection ? accountListView.currentConnection : null
width: 960 width: 960
height: 640 height: 640
@ -25,11 +26,7 @@ ApplicationWindow {
Material.theme: MSettings.darkTheme ? Material.Dark : Material.Light Material.theme: MSettings.darkTheme ? Material.Dark : Material.Light
Settings { Material.accent: matriqueController.color(currentConnection ? currentConnection.localUserId : "")
property alias homeserver: matriqueController.homeserver
property alias userID: matriqueController.userID
property alias token: matriqueController.token
}
FontLoader { id: materialFont; source: "qrc:/asset/font/material.ttf" } FontLoader { id: materialFont; source: "qrc:/asset/font/material.ttf" }
@ -48,6 +45,11 @@ ApplicationWindow {
} }
} }
AccountListModel {
id: accountListModel
controller: matriqueController
}
Popup { Popup {
property bool busy: matriqueController.busy property bool busy: matriqueController.busy
@ -77,7 +79,7 @@ ApplicationWindow {
parent: null parent: null
connection: window.connection connection: currentConnection
} }
Setting { Setting {
@ -85,7 +87,7 @@ ApplicationWindow {
parent: null parent: null
connection: window.connection listModel: accountListModel
} }
RowLayout { RowLayout {
@ -104,25 +106,41 @@ ApplicationWindow {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
SideNavButton { ListView {
Layout.fillWidth: true property var currentConnection: null
Layout.preferredHeight: width
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.fill: parent
anchors.margins: 12 anchors.margins: 12
source: matriqueController.isLogin ? connection.localUser && connection.localUser.avatarUrl ? "image://mxc/" + connection.localUser.avatarUrl : "" : "qrc:/asset/img/avatar.png" hint: user.displayName
displayText: matriqueController.isLogin && connection.localUser.displayName ? connection.localUser.displayName : "" image: user.avatar
defaultColor: Util.stringToColor(user.displayName)
} }
highlightColor: matriqueController.color(user.id)
page: roomPage page: roomPage
onClicked: accountListView.currentConnection = connection
} }
Rectangle {
Layout.fillHeight: true
color: "transparent"
} }
SideNavButton { SideNavButton {
@ -174,7 +192,7 @@ ApplicationWindow {
} }
} }
onAccepted: matriqueController.createRoom(addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text) onAccepted: matriqueController.createRoom(currentConnection, addRoomDialogNameTextField.text, addRoomDialogTopicTextField.text)
} }
} }
MenuItem { MenuItem {
@ -200,7 +218,7 @@ ApplicationWindow {
placeholderText: "#matrix:matrix.org" placeholderText: "#matrix:matrix.org"
} }
onAccepted: matriqueController.joinRoom(joinRoomDialogTextField.text) onAccepted: matriqueController.joinRoom(currentConnection, joinRoomDialogTextField.text)
} }
} }
@ -227,7 +245,7 @@ ApplicationWindow {
placeholderText: "@bot:matrix.org" placeholderText: "@bot:matrix.org"
} }
onAccepted: matriqueController.createDirectChat(directChatDialogTextField.text) onAccepted: currentConnection.createDirectChat(directChatDialogTextField.text)
} }
} }
} }
@ -241,7 +259,7 @@ ApplicationWindow {
anchors.fill: parent anchors.fill: parent
icon: "\ue8b8" icon: "\ue8b8"
color: parent.highlighted ? Material.accent : "white" color: "white"
} }
page: settingPage page: settingPage
} }
@ -272,13 +290,15 @@ ApplicationWindow {
} }
} }
Component.onCompleted: { Binding {
imageProvider.connection = matriqueController.connection target: imageProvider
property: "connection"
value: currentConnection
}
if (matriqueController.userID && matriqueController.token) { Component.onCompleted: {
matriqueController.login(); matriqueController.initiated.connect(function() {
} else { if (matriqueController.accountCount == 0) stackView.push(loginPage)
stackView.replace(loginPage); })
}
} }
} }

View File

@ -2,42 +2,45 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
Menu { 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 id: messageContextMenu
MenuItem { MenuItem {
text: "Copy" text: "Copy"
onTriggered: matriqueController.copyToClipboard(plainText) onTriggered: matriqueController.copyToClipboard(model.plainText)
} }
MenuItem { MenuItem {
text: "Copy Source" text: "View Source"
onTriggered: matriqueController.copyToClipboard(toolTip) onTriggered: {
sourceDialog.sourceText = model.toolTip
sourceDialog.open()
}
} }
MenuItem { MenuItem {
visible: isFile visible: isFile
height: visible ? undefined : 0 height: visible ? undefined : 0
text: "Open Externally" text: "Open Externally"
onTriggered: messageRow.openExternally() onTriggered: row.openExternally()
} }
MenuItem { MenuItem {
visible: isFile visible: isFile
height: visible ? undefined : 0 height: visible ? undefined : 0
text: "Save As" text: "Save As"
onTriggered: messageRow.saveFileAs() onTriggered: row.saveFileAs()
} }
MenuItem { MenuItem {
visible: sentByMe visible: model && model.author === currentRoom.localUser
height: visible ? undefined : 0 height: visible ? undefined : 0
text: "Redact" text: "Redact"
onTriggered: currentRoom.redactEvent(eventId) onTriggered: currentRoom.redactEvent(model.eventId)
} }
Component.onCompleted: popup()
onClosed: messageContextMenu.destroy()
} }

View File

@ -1,35 +1,35 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import Matrique 0.1
Menu { Menu {
property var model: null
id: roomListMenu id: roomListMenu
MenuItem { MenuItem {
text: "Favourite" text: "Favourite"
checkable: true 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 { MenuItem {
text: "Deprioritize" text: "Deprioritize"
checkable: true 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 {} MenuSeparator {}
MenuItem { MenuItem {
text: "Mark as Read" text: "Mark as Read"
onTriggered: currentRoom.markAllMessagesAsRead() onTriggered: model.currentRoom.markAllMessagesAsRead()
} }
MenuItem { MenuItem {
text: "Leave Room" text: "Leave Room"
onTriggered: currentRoom.forget() onTriggered: model.currentRoom.forget()
} }
Component.onCompleted: popup()
onClosed: roomListMenu.destroy()
} }

View File

@ -6,7 +6,6 @@
<file>asset/font/material.ttf</file> <file>asset/font/material.ttf</file>
<file>qml/Login.qml</file> <file>qml/Login.qml</file>
<file>qml/main.qml</file> <file>qml/main.qml</file>
<file>qml/component/ImageStatus.qml</file>
<file>qml/form/RoomForm.qml</file> <file>qml/form/RoomForm.qml</file>
<file>qml/Room.qml</file> <file>qml/Room.qml</file>
<file>qml/component/SideNavButton.qml</file> <file>qml/component/SideNavButton.qml</file>
@ -30,5 +29,6 @@
<file>qml/component/StateDelegate.qml</file> <file>qml/component/StateDelegate.qml</file>
<file>qml/component/AutoLabel.qml</file> <file>qml/component/AutoLabel.qml</file>
<file>qml/component/RoomDrawer.qml</file> <file>qml/component/RoomDrawer.qml</file>
<file>js/util.js</file>
</qresource> </qresource>
</RCC> </RCC>

77
src/accountlistmodel.cpp Normal file
View File

@ -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;
}

34
src/accountlistmodel.h Normal file
View File

@ -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

View File

@ -1,6 +1,8 @@
#include "controller.h" #include "controller.h"
#include "matriqueroom.h" #include "matriqueroom.h"
#include "matriqueuser.h"
#include "settings.h"
#include "events/eventcontent.h" #include "events/eventcontent.h"
#include "events/roommessageevent.h" #include "events/roommessageevent.h"
@ -8,7 +10,21 @@
#include "csapi/joining.h" #include "csapi/joining.h"
#include <QClipboard> #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) { Controller::Controller(QObject* parent) : QObject(parent) {
tray->setIcon(QIcon(":/asset/img/icon.png")); tray->setIcon(QIcon(":/asset/img/icon.png"));
@ -24,87 +40,169 @@ Controller::Controller(QObject* parent) : QObject(parent) {
tray->show(); tray->show();
Connection::setRoomType<MatriqueRoom>(); Connection::setRoomType<MatriqueRoom>();
Connection::setUserType<MatriqueUser>();
connect(m_connection, &Connection::connected, this, &Controller::connected); QTimer::singleShot(0, this, SLOT(invokeLogin()));
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); });
} }
Controller::~Controller() { Controller::~Controller() {}
m_connection->saveState();
m_connection->stopSync();
m_connection->deleteLater();
}
void Controller::login() { inline QString accessTokenFileName(const AccountSettings& account) {
if (!m_isLogin) { QString fileName = account.userId();
m_connection->setHomeserver(QUrl(m_homeserver)); fileName.replace(':', '_');
m_connection->connectWithToken(m_userID, m_token, ""); return QStandardPaths::writableLocation(
} QStandardPaths::AppLocalDataLocation) +
'/' + fileName;
} }
void Controller::loginWithCredentials(QString serverAddr, QString user, void Controller::loginWithCredentials(QString serverAddr, QString user,
QString pass) { QString pass) {
if (!m_isLogin) {
if (!user.isEmpty() && !pass.isEmpty()) { if (!user.isEmpty() && !pass.isEmpty()) {
Connection* m_connection = new Connection(this);
m_connection->setHomeserver(QUrl(serverAddr)); m_connection->setHomeserver(QUrl(serverAddr));
m_connection->connectToServer(user, pass, ""); 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 { } 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;
} }
} }
return false;
void Controller::logout() {
m_connection->logout();
setUserID("");
setToken("");
setIsLogin(false);
} }
void Controller::connected() { void Controller::joinRoom(Connection* c, const QString& alias) {
setHomeserver(m_connection->homeserver().toString()); JoinRoomJob* joinRoomJob = c->joinRoom(alias);
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);
setBusy(true); setBusy(true);
joinRoomJob->connect(joinRoomJob, &JoinRoomJob::finished, joinRoomJob->connect(joinRoomJob, &JoinRoomJob::finished,
[=] { setBusy(false); }); [=] { setBusy(false); });
} }
void Controller::createRoom(const QString& name, const QString& topic) { void Controller::createRoom(Connection* c, const QString& name,
const QString& topic) {
CreateRoomJob* createRoomJob = CreateRoomJob* createRoomJob =
((Connection*)m_connection) c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
setBusy(true); setBusy(true);
createRoomJob->connect(createRoomJob, &CreateRoomJob::finished, createRoomJob->connect(createRoomJob, &CreateRoomJob::finished,
[=] { setBusy(false); }); [=] { setBusy(false); });
} }
void Controller::createDirectChat(const QString& userID) {
m_connection->requestDirectChat(userID);
}
void Controller::copyToClipboard(const QString& text) { void Controller::copyToClipboard(const QString& text) {
m_clipboard->setText(text); m_clipboard->setText(text);
} }
@ -120,3 +218,16 @@ void Controller::showMessage(const QString& title, const QString& msg,
const QIcon& icon) { const QIcon& icon) {
tray->showMessage(title, msg, 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());
}

View File

@ -2,6 +2,7 @@
#define CONTROLLER_H #define CONTROLLER_H
#include "connection.h" #include "connection.h"
#include "settings.h"
#include "user.h" #include "user.h"
#include <QApplication> #include <QApplication>
@ -15,101 +16,71 @@ using namespace QMatrixClient;
class Controller : public QObject { class Controller : public QObject {
Q_OBJECT 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(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
Q_PROPERTY(int accountCount READ accountCount NOTIFY connectionAdded NOTIFY
connectionDropped)
public: public:
explicit Controller(QObject* parent = nullptr); explicit Controller(QObject* parent = nullptr);
~Controller(); ~Controller();
// All the Q_INVOKABLEs. // All the Q_INVOKABLEs.
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithCredentials(QString, QString, QString); Q_INVOKABLE void loginWithCredentials(QString, QString, QString);
Q_INVOKABLE void logout();
QVector<Connection*> connections() { return m_connections; }
// All the non-Q_INVOKABLE functions. // All the non-Q_INVOKABLE functions.
void addConnection(Connection* c);
void dropConnection(Connection* c);
// All the Q_PROPERTYs. // 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; } bool busy() { return m_busy; }
void setBusy(bool b) { void setBusy(bool value) {
if (b != m_busy) { if (value != m_busy) {
m_busy = b; m_busy = value;
emit busyChanged(); emit busyChanged();
} }
} }
int accountCount() { return m_connections.count(); }
Q_INVOKABLE QColor color(QString userId);
Q_INVOKABLE void setColor(QString userId, QColor newColor);
private: private:
QClipboard* m_clipboard = QApplication::clipboard(); QClipboard* m_clipboard = QApplication::clipboard();
QSystemTrayIcon* tray = new QSystemTrayIcon(); QSystemTrayIcon* tray = new QSystemTrayIcon();
QMenu* trayMenu = new QMenu(); 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; bool m_busy = false;
void connected(); QByteArray loadAccessToken(const AccountSettings& account);
void resync(); bool saveAccessToken(const AccountSettings& account,
void reconnect(); const QByteArray& accessToken);
void loadSettings();
void saveSettings() const;
private slots:
void invokeLogin();
signals: signals:
void connectionChanged();
void isLoginChanged();
void userIDChanged();
void tokenChanged();
void homeserverChanged();
void busyChanged(); void busyChanged();
void errorOccured(); void errorOccured();
void toggleWindow(); void toggleWindow();
void connectionAdded(Connection* conn);
void connectionDropped(Connection* conn);
void initiated();
public slots: public slots:
void joinRoom(const QString& alias); void logout(Connection* conn);
void createRoom(const QString& name, const QString& topic); void joinRoom(Connection* c, const QString& alias);
void createDirectChat(const QString& userID); void createRoom(Connection* c, const QString& name, const QString& topic);
void copyToClipboard(const QString& text); void copyToClipboard(const QString& text);
void playAudio(QUrl localFile); void playAudio(QUrl localFile);
void showMessage(const QString& title, const QString& msg, const QIcon& icon); void showMessage(const QString& title, const QString& msg, const QIcon& icon);
static QImage safeImage(QImage image);
}; };
#endif // CONTROLLER_H #endif // CONTROLLER_H

View File

@ -14,8 +14,12 @@ void ImageItem::paint(QPainter *painter) {
if (m_image.isNull()) { if (m_image.isNull()) {
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->setBrush(QColor(m_color)); painter->setBrush(QColor(m_color));
if (m_round)
painter->drawEllipse(0, 0, int(bounding_rect.width()), painter->drawEllipse(0, 0, int(bounding_rect.width()),
int(bounding_rect.height())); int(bounding_rect.height()));
else
painter->drawRect(0, 0, int(bounding_rect.width()),
int(bounding_rect.height()));
painter->setPen(QPen(Qt::white, 2)); painter->setPen(QPen(Qt::white, 2));
QFont font; QFont font;
font.setPixelSize(22); font.setPixelSize(22);
@ -33,11 +37,13 @@ void ImageItem::paint(QPainter *painter) {
QPointF center = bounding_rect.center() - scaled.rect().center(); QPointF center = bounding_rect.center() - scaled.rect().center();
if (m_round) {
QPainterPath clip; QPainterPath clip;
clip.addEllipse( clip.addEllipse(
0, 0, bounding_rect.width(), 0, 0, bounding_rect.width(),
bounding_rect.height()); // this is the shape we want to clip to bounding_rect.height()); // this is the shape we want to clip to
painter->setClipPath(clip); painter->setClipPath(clip);
}
if (center.x() < 0) center.setX(0); if (center.x() < 0) center.setX(0);
if (center.y() < 0) center.setY(0); if (center.y() < 0) center.setY(0);
@ -66,3 +72,11 @@ void ImageItem::setDefaultColor(QString color) {
update(); update();
} }
} }
void ImageItem::setRound(bool value) {
if (m_round != value) {
m_round = value;
emit roundChanged();
update();
}
}

View File

@ -13,6 +13,7 @@ class ImageItem : public QQuickPaintedItem {
Q_PROPERTY(QString hint READ hint WRITE setHint NOTIFY hintChanged) Q_PROPERTY(QString hint READ hint WRITE setHint NOTIFY hintChanged)
Q_PROPERTY(QString defaultColor READ defaultColor WRITE setDefaultColor NOTIFY Q_PROPERTY(QString defaultColor READ defaultColor WRITE setDefaultColor NOTIFY
defaultColorChanged) defaultColorChanged)
Q_PROPERTY(bool round READ round WRITE setRound NOTIFY roundChanged)
public: public:
ImageItem(QQuickItem *parent = nullptr); ImageItem(QQuickItem *parent = nullptr);
@ -28,15 +29,20 @@ class ImageItem : public QQuickPaintedItem {
QString defaultColor() { return m_color; } QString defaultColor() { return m_color; }
void setDefaultColor(QString color); void setDefaultColor(QString color);
bool round() { return m_round; }
void setRound(bool value);
signals: signals:
void imageChanged(); void imageChanged();
void hintChanged(); void hintChanged();
void defaultColorChanged(); void defaultColorChanged();
void roundChanged();
private: private:
QImage m_image; QImage m_image;
QString m_hint; QString m_hint = "H";
QString m_color = "#000000"; QString m_color = "#000000";
bool m_round = true;
}; };
#endif // IMAGEITEM_H #endif // IMAGEITEM_H

View File

@ -3,6 +3,7 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include "accountlistmodel.h"
#include "controller.h" #include "controller.h"
#include "emojimodel.h" #include "emojimodel.h"
#include "imageitem.h" #include "imageitem.h"
@ -38,6 +39,7 @@ int main(int argc, char *argv[]) {
qmlRegisterType<ImageItem>("Matrique", 0, 1, "ImageItem"); qmlRegisterType<ImageItem>("Matrique", 0, 1, "ImageItem");
qmlRegisterType<Controller>("Matrique", 0, 1, "Controller"); qmlRegisterType<Controller>("Matrique", 0, 1, "Controller");
qmlRegisterType<AccountListModel>("Matrique", 0, 1, "AccountListModel");
qmlRegisterType<RoomListModel>("Matrique", 0, 1, "RoomListModel"); qmlRegisterType<RoomListModel>("Matrique", 0, 1, "RoomListModel");
qmlRegisterType<UserListModel>("Matrique", 0, 1, "UserListModel"); qmlRegisterType<UserListModel>("Matrique", 0, 1, "UserListModel");
qmlRegisterType<MessageEventModel>("Matrique", 0, 1, "MessageEventModel"); qmlRegisterType<MessageEventModel>("Matrique", 0, 1, "MessageEventModel");

View File

@ -10,6 +10,7 @@ using namespace QMatrixClient;
class MatriqueRoom : public Room { class MatriqueRoom : public Room {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QImage avatar READ getAvatar NOTIFY avatarChanged)
Q_PROPERTY(bool hasUsersTyping READ hasUsersTyping NOTIFY typingChanged) Q_PROPERTY(bool hasUsersTyping READ hasUsersTyping NOTIFY typingChanged)
Q_PROPERTY(QString usersTyping READ getUsersTyping NOTIFY typingChanged) Q_PROPERTY(QString usersTyping READ getUsersTyping NOTIFY typingChanged)
Q_PROPERTY(QString cachedInput READ cachedInput WRITE setCachedInput NOTIFY Q_PROPERTY(QString cachedInput READ cachedInput WRITE setCachedInput NOTIFY
@ -18,6 +19,8 @@ class MatriqueRoom : public Room {
explicit MatriqueRoom(Connection* connection, QString roomId, explicit MatriqueRoom(Connection* connection, QString roomId,
JoinState joinState = {}); JoinState joinState = {});
QImage getAvatar() { return avatar(128); }
const QString& cachedInput() const { return m_cachedInput; } const QString& cachedInput() const { return m_cachedInput; }
void setCachedInput(const QString& input) { void setCachedInput(const QString& input) {
if (input != m_cachedInput) { if (input != m_cachedInput) {

6
src/matriqueuser.cpp Normal file
View File

@ -0,0 +1,6 @@
#include "matriqueuser.h"
MatriqueUser::MatriqueUser(QString userId, Connection* connection)
: User(userId, connection) {
connect(this, &User::avatarChanged, this, &MatriqueUser::inheritedAvatarChanged);
}

23
src/matriqueuser.h Normal file
View File

@ -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

View File

@ -15,6 +15,7 @@
QHash<int, QByteArray> MessageEventModel::roleNames() const { QHash<int, QByteArray> MessageEventModel::roleNames() const {
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[EventTypeRole] = "eventType"; roles[EventTypeRole] = "eventType";
roles[AboveEventTypeRole] = "aboveEventType";
roles[EventIdRole] = "eventId"; roles[EventIdRole] = "eventId";
roles[TimeRole] = "time"; roles[TimeRole] = "time";
roles[AboveTimeRole] = "aboveTime"; roles[AboveTimeRole] = "aboveTime";
@ -31,6 +32,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
roles[AnnotationRole] = "annotation"; roles[AnnotationRole] = "annotation";
roles[EventResolvedTypeRole] = "eventResolvedType"; roles[EventResolvedTypeRole] = "eventResolvedType";
roles[PlainTextRole] = "plainText"; roles[PlainTextRole] = "plainText";
roles[UserMarkerRole] = "userMarker";
return roles; return roles;
} }
@ -51,7 +53,6 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
beginResetModel(); beginResetModel();
if (m_currentRoom) { if (m_currentRoom) {
m_currentRoom->disconnect(this); m_currentRoom->disconnect(this);
qDebug() << "Disconnected from" << m_currentRoom->id();
} }
m_currentRoom = room; m_currentRoom = room;
@ -78,7 +79,8 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - auto rowBelowInserted = m_currentRoom->maxTimelineIndex() -
biggest + timelineBaseIndex() - 1; biggest + timelineBaseIndex() - 1;
refreshEventRoles(rowBelowInserted, refreshEventRoles(rowBelowInserted,
{AboveAuthorRole, AboveSectionRole}); {AboveEventTypeRole, AboveAuthorRole,
AboveSectionRole, AboveTimeRole});
} }
for (auto i = m_currentRoom->maxTimelineIndex() - biggest; for (auto i = m_currentRoom->maxTimelineIndex() - biggest;
i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) i <= m_currentRoom->maxTimelineIndex() - lowest; ++i)
@ -108,7 +110,8 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole}); refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole});
if (timelineBaseIndex() > 0) // Refresh below, see #312 if (timelineBaseIndex() > 0) // Refresh below, see #312
refreshEventRoles(timelineBaseIndex() - 1, refreshEventRoles(timelineBaseIndex() - 1,
{AboveAuthorRole, AboveSectionRole}); {AboveEventTypeRole, AboveAuthorRole,
AboveSectionRole, AboveTimeRole});
}); });
connect(m_currentRoom, &Room::pendingEventChanged, this, connect(m_currentRoom, &Room::pendingEventChanged, this,
&MessageEventModel::refreshRow); &MessageEventModel::refreshRow);
@ -135,6 +138,11 @@ void MessageEventModel::setRoom(MatriqueRoom* room) {
&MessageEventModel::refreshEvent); &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferCancelled, this, connect(m_currentRoom, &Room::fileTransferCancelled, this,
&MessageEventModel::refreshEvent); &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" qDebug() << "Connected to room" << room->id() << "as"
<< room->localUser()->id(); << room->localUser()->id();
} else } else
@ -371,7 +379,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
: tr("self-unbanned"); : tr("self-unbanned");
} }
return (e.senderId() != e.userId()) 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"); : tr("left the room");
case MembershipType::Ban: case MembershipType::Ban:
return (e.senderId() != e.userId()) return (e.senderId() != e.userId())
@ -470,7 +478,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
: tr("self-unbanned"); : tr("self-unbanned");
} }
return (e.senderId() != e.userId()) 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"); : tr("left the room");
case MembershipType::Ban: case MembershipType::Ban:
return (e.senderId() != e.userId()) 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); return role == TimeRole ? QVariant(ts) : renderDate(ts);
} }
if (role == AboveSectionRole || role == AboveAuthorRole || if (role == UserMarkerRole) {
role == AboveTimeRole) 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) { for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r); auto i = index(r);
if (data(i, SpecialMarksRole) != EventStatus::Hidden) if (data(i, SpecialMarksRole) != EventStatus::Hidden) switch (role) {
return data(i, role == AboveSectionRole case AboveEventTypeRole:
? SectionRole return data(i, EventTypeRole);
: role == AboveAuthorRole ? AuthorRole : TimeRole); case AboveSectionRole:
return data(i, SectionRole);
case AboveAuthorRole:
return data(i, AuthorRole);
case AboveTimeRole:
return data(i, TimeRole);
}
} }
return {}; return {};

View File

@ -1,19 +1,19 @@
#ifndef MESSAGEEVENTMODEL_H #ifndef MESSAGEEVENTMODEL_H
#define MESSAGEEVENTMODEL_H #define MESSAGEEVENTMODEL_H
#include "room.h"
#include "matriqueroom.h" #include "matriqueroom.h"
#include "room.h"
#include <QtCore/QAbstractListModel> #include <QtCore/QAbstractListModel>
class MessageEventModel : public QAbstractListModel { class MessageEventModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
Q_PROPERTY( Q_PROPERTY(MatriqueRoom* room READ getRoom WRITE setRoom NOTIFY roomChanged)
MatriqueRoom* room READ getRoom WRITE setRoom NOTIFY roomChanged)
public: public:
enum EventRoles { enum EventRoles {
EventTypeRole = Qt::UserRole + 1, EventTypeRole = Qt::UserRole + 1,
AboveEventTypeRole,
EventIdRole, EventIdRole,
TimeRole, TimeRole,
AboveTimeRole, AboveTimeRole,
@ -29,6 +29,7 @@ class MessageEventModel : public QAbstractListModel {
LongOperationRole, LongOperationRole,
AnnotationRole, AnnotationRole,
PlainTextRole, PlainTextRole,
UserMarkerRole,
// For debugging // For debugging
EventResolvedTypeRole, EventResolvedTypeRole,
}; };
@ -42,7 +43,7 @@ class MessageEventModel : public QAbstractListModel {
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, QVariant data(const QModelIndex& index,
int role = Qt::DisplayRole) const override; int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const; QHash<int, QByteArray> roleNames() const override;
private slots: private slots:
int refreshEvent(const QString& eventId); int refreshEvent(const QString& eventId);

View File

@ -14,12 +14,20 @@ RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) {}
RoomListModel::~RoomListModel() {} RoomListModel::~RoomListModel() {}
void RoomListModel::setConnection(Connection* connection) { 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; m_connection = connection;
doResetModel(); for (MatriqueRoom* room : m_rooms) room->disconnect(this);
connect(connection, &Connection::connected, this, connect(connection, &Connection::connected, this,
&RoomListModel::doResetModel); &RoomListModel::doResetModel);
@ -30,6 +38,8 @@ void RoomListModel::setConnection(Connection* connection) {
connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::aboutToDeleteRoom, this, connect(connection, &Connection::aboutToDeleteRoom, this,
&RoomListModel::deleteRoom); &RoomListModel::deleteRoom);
doResetModel();
} }
void RoomListModel::doResetModel() { void RoomListModel::doResetModel() {
@ -64,7 +74,7 @@ void RoomListModel::connectRoomSignals(MatriqueRoom* room) {
[=] { refresh(room, {AvatarRole}); }); [=] { refresh(room, {AvatarRole}); });
connect(room, &Room::addedMessages, this, connect(room, &Room::addedMessages, this,
[=] { refresh(room, {LastEventRole}); }); [=] { refresh(room, {LastEventRole}); });
connect(room, &QMatrixClient::Room::aboutToAddNewMessages, this, connect(room, &Room::aboutToAddNewMessages, this,
[=](QMatrixClient::RoomEventsRange eventsRange) { [=](QMatrixClient::RoomEventsRange eventsRange) {
RoomEvent* event = (eventsRange.end() - 1)->get(); RoomEvent* event = (eventsRange.end() - 1)->get();
if (event->isStateEvent()) return; if (event->isStateEvent()) return;
@ -139,7 +149,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const {
MatriqueRoom* room = m_rooms.at(index.row()); MatriqueRoom* room = m_rooms.at(index.row());
if (role == NameRole) return room->displayName(); if (role == NameRole) return room->displayName();
if (role == AvatarRole) { if (role == AvatarRole) {
if (room->avatarUrl().toString() != "") return room->avatar(64, 64); if (!room->avatarUrl().isEmpty()) return room->avatar(64, 64);
return QImage(); return QImage();
} }
if (role == TopicRole) return room->topic(); if (role == TopicRole) return room->topic();

View File

@ -67,8 +67,13 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const {
if (role == NameRole) { if (role == NameRole) {
return user->displayname(m_currentRoom); return user->displayname(m_currentRoom);
} }
if (role == UserIDRole) {
return user->id();
}
if (role == AvatarRole) { if (role == AvatarRole) {
return user->avatarUrl(m_currentRoom); if (!user->avatarUrl(m_currentRoom).isEmpty())
return user->avatar(64, m_currentRoom);
return QImage();
} }
return QVariant(); return QVariant();
@ -110,7 +115,7 @@ void UserListModel::refresh(QMatrixClient::User* user, QVector<int> roles) {
void UserListModel::avatarChanged(QMatrixClient::User* user, void UserListModel::avatarChanged(QMatrixClient::User* user,
const QMatrixClient::Room* context) { 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 { 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> UserListModel::roleNames() const {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[UserIDRole] = "userId";
roles[AvatarRole] = "avatar"; roles[AvatarRole] = "avatar";
return roles; return roles;
} }

View File

@ -17,10 +17,7 @@ class UserListModel : public QAbstractListModel {
Q_PROPERTY( Q_PROPERTY(
QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged) QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged)
public: public:
enum EventRoles { enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole };
NameRole = Qt::UserRole + 1,
AvatarRole
};
using User = QMatrixClient::User; using User = QMatrixClient::User;
@ -30,8 +27,7 @@ class UserListModel : public QAbstractListModel {
void setRoom(QMatrixClient::Room* room); void setRoom(QMatrixClient::Room* room);
User* userAt(QModelIndex index); User* userAt(QModelIndex index);
QVariant data(const QModelIndex& index, QVariant data(const QModelIndex& index, int role = NameRole) const override;
int role = NameRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;