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};
/**
* 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 = [

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 "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());
}

View File

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

View File

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

View File

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

View File

@ -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");

View File

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

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

View File

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

View File

@ -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();

View File

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

View File

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