First attempt.

This commit is contained in:
Black
2020-02-12 00:37:35 -08:00
parent 29e6933b4f
commit 5c4d0a969d
17 changed files with 561 additions and 1663 deletions

View File

@@ -86,6 +86,8 @@ if (NOT USE_INTREE_LIBQMC)
endif () endif ()
endif () endif ()
find_package(KF5Kirigami2 REQUIRED)
find_package(Qt5Keychain REQUIRED) find_package(Qt5Keychain REQUIRED)
find_package(cmark REQUIRED) find_package(cmark REQUIRED)
@@ -209,6 +211,7 @@ add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
Qt5::Widgets Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::Svg Qt5::QuickControls2 Qt5::Widgets Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::Svg Qt5::QuickControls2
KF5::Kirigami2
Quotient Quotient
cmark::cmark cmark::cmark
${QTKEYCHAIN_LIBRARIES} ${QTKEYCHAIN_LIBRARIES}

Binary file not shown.

View File

@@ -14,185 +14,130 @@ import Spectral.Menu.Timeline 2.0
import Spectral.Effect 2.0 import Spectral.Effect 2.0
import Spectral.Font 0.1 import Spectral.Font 0.1
RowLayout { Image {
readonly property bool avatarVisible: showAuthor && !sentByMe
readonly property bool sentByMe: author.isLocalUser
readonly property bool isAnimated: contentType === "image/gif" readonly property bool isAnimated: contentType === "image/gif"
property bool openOnFinished: false property bool openOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed
id: root readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null)
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
spacing: 4 id: img
z: -5 source: "image://mxc/" + mediaId
onDownloadedChanged: { sourceSize.width: info.w
if (downloaded && openOnFinished) { sourceSize.height: info.h
openSavedFile()
openOnFinished = false fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: img.width
height: img.height
radius: 18
} }
} }
Avatar { Control {
Layout.preferredWidth: 36 anchors.bottom: parent.bottom
Layout.preferredHeight: 36 anchors.bottomMargin: 8
Layout.alignment: Qt.AlignBottom anchors.right: parent.right
anchors.rightMargin: 8
visible: avatarVisible horizontalPadding: 8
hint: author.displayName verticalPadding: 4
source: author.avatarMediaId
color: author.color contentItem: RowLayout {
Label {
text: Qt.formatTime(time)
color: "white"
font.pixelSize: 12
}
Label {
text: author.displayName
color: "white"
font.pixelSize: 12
}
}
background: Rectangle {
radius: height / 2
color: "black"
opacity: 0.3
}
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
RippleEffect {
anchors.fill: parent
id: messageMouseArea
onPrimaryClicked: fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
onSecondaryClicked: {
var contextMenu = imageDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Component { Component {
id: userDetailDialog id: messageSourceDialog
UserDetailDialog {} MessageSourceDialog {}
} }
RippleEffect { Component {
anchors.fill: parent id: openFolderDialog
circular: true OpenFolderDialog {}
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
}
Item {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: !(sentByMe || avatarVisible)
}
Image {
readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null)
readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
readonly property int maxWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48
Layout.minimumWidth: 256
Layout.minimumHeight: 64
Layout.preferredWidth: info.w > maxWidth ? maxWidth : info.w
Layout.preferredHeight: info.w > maxWidth ? (info.h * maxWidth / info.w) : info.h
id: img
source: "image://mxc/" + mediaId
sourceSize.width: info.w
sourceSize.height: info.h
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: img.width
height: img.height
radius: 18
}
} }
Control { Component {
anchors.bottom: parent.bottom id: imageDelegateContextMenu
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
horizontalPadding: 8 FileDelegateContextMenu {}
verticalPadding: 4
contentItem: RowLayout {
Label {
text: Qt.formatTime(time)
color: "white"
font.pixelSize: 12
}
Label {
text: author.displayName
color: "white"
font.pixelSize: 12
}
}
background: Rectangle {
radius: height / 2
color: "black"
opacity: 0.3
}
} }
Rectangle { Component {
anchors.fill: parent id: fullScreenImage
visible: progressInfo.active && !downloaded FullScreenImage {}
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
RippleEffect {
anchors.fill: parent
id: messageMouseArea
onPrimaryClicked: fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen()
onSecondaryClicked: {
var contextMenu = imageDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Component {
id: messageSourceDialog
MessageSourceDialog {}
}
Component {
id: openFolderDialog
OpenFolderDialog {}
}
Component {
id: imageDelegateContextMenu
FileDelegateContextMenu {}
}
Component {
id: fullScreenImage
FullScreenImage {}
}
} }
} }

View File

@@ -1,309 +1,108 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import org.kde.kirigami 2.4 as Kirigami
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu.Timeline 2.0
import Spectral.Effect 2.0
ColumnLayout { RowLayout {
readonly property bool avatarVisible: !sentByMe && showAuthor default property alias innerObject : column.children
readonly property bool sentByMe: author.isLocalUser readonly property bool sentByMe: author.isLocalUser
readonly property bool darkBackground: !sentByMe
readonly property bool replyVisible: reply || false readonly property bool replyVisible: reply || false
readonly property bool failed: marks === EventStatus.SendingFailed readonly property bool failed: marks === EventStatus.SendingFailed
readonly property color authorColor: eventType === "notice" ? MPalette.primary : author.color
readonly property color replyAuthorColor: replyVisible ? reply.author.color : MPalette.accent
signal saveFileAs()
signal openExternally()
id: root id: root
z: -5 spacing: Kirigami.Units.largeSpacing
spacing: 0 Avatar {
Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5
RowLayout { Layout.alignment: Qt.AlignTop
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
id: messageRow visible: showAuthor
hint: author.displayName
source: author.avatarMediaId
color: author.color
}
spacing: 4 Item {
Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5
Layout.preferredHeight: 1
Avatar { visible: !showAuthor
Layout.preferredWidth: 36 }
Layout.preferredHeight: 36
Layout.alignment: Qt.AlignBottom
visible: avatarVisible ColumnLayout {
hint: author.displayName Layout.fillWidth: true
source: author.avatarMediaId
color: author.color
Component { id: column
id: userDetailDialog
UserDetailDialog {} spacing: Kirigami.Units.smallSpacing
}
RippleEffect { Controls.Label {
anchors.fill: parent Layout.fillWidth: true
circular: true visible: showAuthor
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() text: author.displayName
} font.bold: true
color: Kirigami.Theme.activeTextColor
wrapMode: Text.Wrap
} }
Item { RowLayout {
Layout.preferredWidth: 36 Layout.fillWidth: true
Layout.preferredHeight: 36
visible: !(sentByMe || avatarVisible) visible: replyVisible
}
Control { Rectangle {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48 Layout.preferredWidth: 4
Layout.minimumHeight: 36 Layout.fillHeight: true
padding: 0 color: Kirigami.Theme.highlightColor
background: AutoRectangle {
readonly property int minorRadius: 2
id: bubbleBackground
color: sentByMe ? MPalette.background : authorColor
radius: 18
topLeftVisible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2)
topRightVisible: sentByMe && (bubbleShape == 3 || bubbleShape == 2)
bottomLeftVisible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2)
bottomRightVisible: sentByMe && (bubbleShape == 1 || bubbleShape == 2)
topLeftRadius: minorRadius
topRightRadius: minorRadius
bottomLeftRadius: minorRadius
bottomRightRadius: minorRadius
AutoMouseArea {
anchors.fill: parent
id: messageMouseArea
onSecondaryClicked: {
var contextMenu = messageDelegateContextMenu.createObject(root)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.reply.connect(function() {
roomPanelInput.replyModel = Object.assign({}, model)
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {}
}
Component {
id: messageSourceDialog
MessageSourceDialog {}
}
}
} }
contentItem: ColumnLayout { Avatar {
spacing: 0 Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5
Layout.alignment: Qt.AlignTop
Control { source: replyVisible ? reply.author.avatarMediaId : ""
hint: replyVisible ? reply.author.displayName : "H"
color: replyVisible ? reply.author.color : MPalette.accent
}
ColumnLayout {
Layout.fillWidth: true
Controls.Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 text: replyVisible ? reply.author.displayName : ""
Layout.leftMargin: 8 color: Kirigami.Theme.activeTextColor
Layout.rightMargin: 8 wrapMode: Text.Wrap
id: replyControl
padding: 4
rightPadding: 12
visible: replyVisible
contentItem: RowLayout {
Avatar {
Layout.preferredWidth: 28
Layout.preferredHeight: 28
Layout.alignment: Qt.AlignTop
source: replyVisible ? reply.author.avatarMediaId : ""
hint: replyVisible ? reply.author.displayName : "H"
color: replyVisible ? reply.author.color : MPalette.accent
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": reply.author.object, "displayName": reply.author.displayName, "avatarMediaId": reply.author.avatarMediaId, "avatarUrl": reply.author.avatarUrl}).open()
}
}
TextEdit {
Layout.fillWidth: true
color: !sentByMe ? MPalette.foreground : "white"
text: "<style>pre {white-space: pre-wrap} a{color: " + color + ";} .user-pill{}</style>" + (replyVisible ? reply.display : "")
font.family: window.font.family
selectByMouse: true
readOnly: true
wrapMode: Label.Wrap
selectedTextColor: darkBackground ? "white" : replyAuthorColor
selectionColor: darkBackground ? replyAuthorColor : "white"
textFormat: Text.RichText
}
}
background: Rectangle {
color: sentByMe ? replyAuthorColor : MPalette.background
radius: 18
AutoMouseArea {
anchors.fill: parent
onClicked: goToEvent(reply.eventId)
}
}
} }
TextEdit { TextEdit {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 text: "<style>pre {white-space: pre-wrap} a{color: " + color + ";} .user-pill{}</style>" + (replyVisible ? reply.display : "")
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 8
id: contentLabel color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
text: "<style>pre {white-space: pre-wrap} a{color: " + color + ";} .user-pill{}</style>" + display
color: darkBackground ? "white" : MPalette.foreground
font.family: window.font.family
font.pixelSize: (message.length === 2 && /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g.test(message)) ? 48 : 14
selectByMouse: true selectByMouse: true
readOnly: true readOnly: true
wrapMode: Label.Wrap wrapMode: Text.Wrap
selectedTextColor: darkBackground ? authorColor : "white"
selectionColor: darkBackground ? "white" : authorColor
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: {
if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (!result || result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
} }
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
}
}
}
}
RowLayout {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: sentByMe ? undefined : 36 + messageRow.spacing + 12
Layout.rightMargin: sentByMe ? 12 : undefined
Layout.bottomMargin: 4
visible: showAuthor && !failed
Label {
visible: !sentByMe
text: author.displayName
color: MPalette.lighter
}
Label {
text: Qt.formatTime(time)
color: MPalette.lighter
}
}
RowLayout {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: sentByMe ? undefined : 36 + messageRow.spacing + 12
Layout.rightMargin: sentByMe ? 12 : undefined
Layout.bottomMargin: 4
visible: failed
Label {
text: "Send failed:"
color: MPalette.lighter
}
Label {
text: "Resend"
color: MPalette.lighter
MouseArea {
anchors.fill: parent
onClicked: currentRoom.retryMessage(eventId)
}
}
Label {
text: "|"
color: MPalette.lighter
}
Label {
text: "Discard"
color: MPalette.lighter
MouseArea {
anchors.fill: parent
onClicked: currentRoom.discardMessage(eventId)
} }
} }
} }

View File

@@ -1,11 +1,10 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12 as Controls
import Spectral.Setting 0.1
Label { import org.kde.kirigami 2.4 as Kirigami
Controls.Label {
text: section + " • " + Qt.formatTime(time) text: section + " • " + Qt.formatTime(time)
color: MPalette.foreground
font.pixelSize: 13
font.weight: Font.Medium font.weight: Font.Medium
font.capitalization: Font.AllUppercase font.capitalization: Font.AllUppercase
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter

View File

@@ -0,0 +1,23 @@
import QtQuick 2.12
import org.kde.kirigami 2.4 as Kirigami
TextEdit {
text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + display
font {
pixelSize: Kirigami.Theme.defaultFont.pixelSize * 1.2
family: Kirigami.Theme.defaultFont.family
}
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectByMouse: true
readOnly: true
wrapMode: Text.Wrap
textFormat: Text.RichText
}

View File

@@ -0,0 +1,23 @@
import QtQuick 2.12
import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.4 as Kirigami
Controls.Control {
default property alias innerObject : column.children
horizontalPadding: Kirigami.Units.largeSpacing
verticalPadding: Kirigami.Units.smallSpacing
contentItem: Column {
id: column
SectionDelegate {
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(implicitWidth, parent.width)
visible: showSection
}
}
}

View File

@@ -1,5 +1,7 @@
module Spectral.Component.Timeline module Spectral.Component.Timeline
TimelineContainer 2.0 TimelineContainer.qml
MessageDelegate 2.0 MessageDelegate.qml MessageDelegate 2.0 MessageDelegate.qml
TextDelegate 2.0 TextDelegate.qml
StateDelegate 2.0 StateDelegate.qml StateDelegate 2.0 StateDelegate.qml
SectionDelegate 2.0 SectionDelegate.qml SectionDelegate 2.0 SectionDelegate.qml
ImageDelegate 2.0 ImageDelegate.qml ImageDelegate 2.0 ImageDelegate.qml

View File

@@ -1,434 +1,141 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0 import org.kde.kirigami 2.4 as Kirigami
import Spectral.Dialog 2.0
import Spectral.Menu 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { import Spectral.Component 2.0
property var connection: null import Spectral 0.1
readonly property var user: connection ? connection.localUser : null
property int filter: 0 Kirigami.ScrollablePage {
property var enteredRoom: null property var roomListModel
property var enteredRoom
signal enterRoom(var room) signal enterRoom(var room)
signal leaveRoom(var room) signal leaveRoom(var room)
id: root title: "Spectral"
actions {
RoomListModel { main: Kirigami.Action {
id: roomListModel iconName: "document-edit"
connection: root.connection
onNewMessage: if (!window.active && MSettings.showNotification) notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon)
}
Binding {
target: trayIcon
property: "notificationCount"
value: roomListModel.notificationCount
}
SortFilterProxyModel {
id: sortedRoomListModel
sourceModel: roomListModel
proxyRoles: ExpressionRole {
name: "display"
expression: {
switch (category) {
case 1: return "Invited"
case 2: return "Favorites"
case 3: return "People"
case 4: return "Rooms"
case 5: return "Low Priority"
}
}
} }
contextualActions: []
sorters: [
RoleSorter { roleName: "category" },
ExpressionSorter {
expression: {
return modelLeft.highlightCount > 0;
}
},
ExpressionSorter {
expression: {
return modelLeft.notificationCount > 0;
}
},
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
}
]
filters: [
ExpressionFilter {
expression: joinState != "upgraded"
},
RegExpFilter {
roleName: "name"
pattern: searchField.text
caseSensitivity: Qt.CaseInsensitive
},
ExpressionFilter {
enabled: filter === 0
expression: category !== 5 && notificationCount > 0 || currentRoom === enteredRoom
},
ExpressionFilter {
enabled: filter === 1
expression: category === 1 || category === 3
},
ExpressionFilter {
enabled: filter === 2
expression: category !== 3
}
]
} }
Shortcut { ListView {
sequence: "Ctrl+F" id: messageListView
onActivated: searchField.forceActiveFocus()
}
ColumnLayout { model: SortFilterProxyModel {
anchors.fill: parent id: sortedRoomListModel
spacing: 0
Control { sourceModel: roomListModel
Layout.fillWidth: true
Layout.preferredHeight: 64
id: roomListHeader proxyRoles: ExpressionRole {
name: "display"
topPadding: 12 expression: {
bottomPadding: 12 switch (category) {
leftPadding: 12 case 1: return "Invited"
rightPadding: 18 case 2: return "Favorites"
case 3: return "People"
contentItem: RowLayout { case 4: return "Rooms"
ToolButton { case 5: return "Low Priority"
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !searchField.active
contentItem: MaterialIcon {
icon: "\ue8b6"
} }
} }
}
ToolButton { sorters: [
Layout.preferredWidth: height RoleSorter { roleName: "category" },
Layout.fillHeight: true ExpressionSorter {
expression: {
visible: searchField.active return modelLeft.highlightCount > 0;
}
contentItem: MaterialIcon { icon: "\ue5cd" } },
ExpressionSorter {
onClicked: searchField.clear() expression: {
return modelLeft.notificationCount > 0;
}
},
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
} }
]
AutoTextField { filters: [
readonly property bool active: text ExpressionFilter {
expression: joinState != "upgraded"
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
id: searchField
placeholderText: "Search..."
color: MPalette.lighter
} }
]
}
delegate: Kirigami.SwipeListItem {
padding: Kirigami.Units.largeSpacing
actions: [
Kirigami.Action {
text:"Action for buttons"
iconName: "bookmarks"
onTriggered: print("Action 1 clicked")
},
Kirigami.Action {
text:"Action 2"
iconName: "folder"
enabled: false
}
]
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Avatar { Avatar {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignRight
visible: !searchField.active source: avatar
hint: name || "No Name"
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
RippleEffect {
anchors.fill: parent
circular: true
onClicked: accountDetailDialog.createObject(ApplicationWindow.overlay).open()
}
}
}
background: Rectangle {
color: Material.background
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
}
}
AutoListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: listView
z: -1
spacing: 0
model: sortedRoomListModel
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {}
delegate: Item {
width: listView.width
height: 64
Rectangle {
anchors.fill: parent
visible: currentRoom === enteredRoom
color: Material.accent
opacity: 0.1
} }
RowLayout { ColumnLayout {
anchors.fill: parent Layout.fillWidth: true
anchors.margins: 12 Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
spacing: 12 spacing: Kirigami.Units.smallSpacing
Avatar { Controls.Label {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name || "No Name"
}
ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Label { text: name || "No Name"
Layout.fillWidth: true font.pixelSize: 16
Layout.fillHeight: true font.bold: unreadCount >= 0
elide: Text.ElideRight
text: name || "No Name" wrapMode: Text.NoWrap
color: MPalette.foreground
font.pixelSize: 16
font.bold: unreadCount >= 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
color: MPalette.lighter
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
} }
Label { Controls.Label {
visible: notificationCount > 0 && highlightCount == 0 Layout.fillWidth: true
color: MPalette.background Layout.fillHeight: true
text: notificationCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle { text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
radius: height / 2 font.pixelSize: 13
color: MPalette.lighter elide: Text.ElideRight
} wrapMode: Text.NoWrap
} }
Label {
visible: highlightCount > 0
color: "white"
text: highlightCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
background: Rectangle {
radius: height / 2
color: MPalette.accent
}
}
}
AutoMouseArea {
anchors.fill: parent
onPrimaryClicked: {
if (category === RoomType.Invited) {
acceptInvitationDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom}).open()
} else {
joinRoom(currentRoom)
}
}
onSecondaryClicked: roomListContextMenu.createObject(parent, {"room": currentRoom}).popup()
}
Component {
id: roomListContextMenu
RoomListContextMenu {}
} }
} }
section.property: "display" onClicked: {
section.criteria: ViewSection.FullString if (enteredRoom) {
section.delegate: Label { leaveRoom(enteredRoom)
width: parent.width
height: 24
text: section
color: MPalette.lighter
leftPadding: 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
ColumnLayout {
anchors.centerIn: parent
visible: sortedRoomListModel.count == 0
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
icon: "\ue5ca"
font.pixelSize: 48
color: MPalette.lighter
} }
Label { enteredRoom = currentRoom
Layout.alignment: Qt.AlignHCenter
text: "You're all caught up!" enterRoom(enteredRoom)
color: MPalette.foreground
}
} }
} }
Control {
Layout.fillWidth: true
Layout.preferredHeight: 48
Layout.margins: 16
padding: 8
contentItem: RowLayout {
id: tabBar
MaterialIcon {
Layout.fillWidth: true
icon: "\ue7f5"
color: filter == 0 ? MPalette.accent : MPalette.lighter
MouseArea {
anchors.fill: parent
onClicked: filter = 0
}
}
MaterialIcon {
Layout.fillWidth: true
icon: "\ue7ff"
color: filter == 1 ? MPalette.accent : MPalette.lighter
MouseArea {
anchors.fill: parent
onClicked: filter = 1
}
}
MaterialIcon {
Layout.fillWidth: true
icon: "\ue7fc"
color: filter == 2 ? MPalette.accent : MPalette.lighter
MouseArea {
anchors.fill: parent
onClicked: filter = 2
}
}
}
background: AutoRectangle {
color: MPalette.background
radius: 24
topLeftRadius: 8
topRightRadius: 8
topLeftVisible: true
topRightVisible: true
bottomLeftVisible: false
bottomRightVisible: false
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 1
}
}
}
}
Component {
id: acceptInvitationDialog
AcceptInvitationDialog {}
}
function joinRoom(room) {
if (enteredRoom) {
leaveRoom(enteredRoom)
enteredRoom.displayed = false
}
enterRoom(room)
enteredRoom = room
room.displayed = true
} }
} }

View File

@@ -1,658 +1,236 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Qt.labs.qmlmodels 1.0 import Qt.labs.qmlmodels 1.0
import Qt.labs.platform 1.0
import QtGraphicalEffects 1.0
import Spectral.Component 2.0 import org.kde.kirigami 2.4 as Kirigami
import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { import Spectral.Component 2.0
property var currentRoom: null import Spectral.Component.Timeline 2.0
import Spectral 0.1
id: root Kirigami.ScrollablePage {
property var currentRoom
id: page
title: "Messages"
actions {
main: Kirigami.Action {
iconName: "document-edit"
}
contextualActions: []
}
MessageEventModel { MessageEventModel {
id: messageEventModel id: messageEventModel
room: currentRoom room: currentRoom
} }
DropArea { ListView {
anchors.fill: parent readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
enabled: currentRoom id: messageListView
onDropped: { spacing: Kirigami.Units.smallSpacing
if (!drop.hasUrls) return
roomPanelInput.attach(drop.urls[0]) displayMarginBeginning: 100
} displayMarginEnd: 100
} verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
ImageClipboard { model: SortFilterProxyModel {
id: imageClipboard id: sortedMessageEventModel
}
Popup { sourceModel: messageEventModel
anchors.centerIn: parent
id: attachDialog filters: [
ExpressionFilter {
padding: 16 expression: marks !== 0x10 && eventType !== "other"
contentItem: RowLayout {
Control {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
contentItem: ColumnLayout {
spacing: 16
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
icon: "\ue2c8"
font.pixelSize: 64
color: MPalette.lighter
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Choose local file"
color: MPalette.foreground
}
} }
]
background: RippleEffect { onModelReset: {
onClicked: { if (currentRoom) {
attachDialog.close() if (currentRoom.timelineSize < 20)
currentRoom.getPreviousContent(50)
}
}
}
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) onContentYChanged: {
if(!noNeedMoreContent && contentY - 5000 < originY)
currentRoom.getPreviousContent(20);
}
fileDialog.chosen.connect(function(path) { populate: Transition {
if (!path) return NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: 200
}
}
roomPanelInput.attach(path) add: Transition {
}) NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: 200
}
}
fileDialog.open() move: Transition {
NumberAnimation {
property: "y"; duration: 200
}
NumberAnimation {
property: "opacity"; to: 1
}
}
displaced: Transition {
NumberAnimation {
property: "y"; duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
property: "opacity"; to: 1
}
}
delegate: DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "state"
delegate: TimelineContainer {
width: page.width
StateDelegate {}
}
}
DelegateChoice {
roleValue: "emote"
delegate: TimelineContainer {
width: page.width
innerObject: StateDelegate {}
}
}
DelegateChoice {
roleValue: "message"
delegate: TimelineContainer {
width: page.width
innerObject: MessageDelegate {
width: parent.width
innerObject: TextDelegate {
Layout.fillWidth: true
}
} }
} }
} }
Rectangle { DelegateChoice {
Layout.preferredWidth: 1 roleValue: "notice"
Layout.fillHeight: true delegate: TimelineContainer {
width: page.width
color: MPalette.banner innerObject: MessageDelegate {
width: parent.width
innerObject: TextDelegate {
Layout.fillWidth: true
}
}
}
} }
Control { DelegateChoice {
Layout.preferredWidth: 160 roleValue: "image"
Layout.fillHeight: true delegate: TimelineContainer {
width: page.width
padding: 16 innerObject: MessageDelegate {
width: parent.width
contentItem: ColumnLayout { innerObject: ImageDelegate {
spacing: 16 Layout.fillWidth: true
Layout.preferredHeight: info.h
MaterialIcon { }
Layout.alignment: Qt.AlignHCenter
icon: "\ue410"
font.pixelSize: 64
color: MPalette.lighter
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Clipboard image"
color: MPalette.foreground
} }
} }
}
background: RippleEffect { DelegateChoice {
onClicked: { roleValue: "audio"
var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png" delegate: TimelineContainer {
if (!imageClipboard.saveImage(localPath)) return width: page.width
roomPanelInput.attach(localPath)
attachDialog.close() innerObject: MessageDelegate {
width: parent.width
innerObject: AudioDelegate {
Layout.fillWidth: true
}
} }
} }
} }
DelegateChoice {
roleValue: "video"
delegate: TimelineContainer {
width: page.width
innerObject: MessageDelegate {
width: parent.width
innerObject: AudioDelegate {
Layout.fillWidth: true
}
}
}
}
DelegateChoice {
roleValue: "file"
delegate: TimelineContainer {
width: page.width
innerObject: MessageDelegate {
width: parent.width
innerObject: FileDelegate {
Layout.fillWidth: true
}
}
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
} }
} }
Component { footer: RowLayout {
id: openFileDialog Controls.ToolButton {
contentItem: Kirigami.Icon {
OpenFileDialog {} source: "mail-attachment"
} }
Column {
anchors.centerIn: parent
spacing: 16
visible: !currentRoom
Image {
anchors.horizontalCenter: parent.horizontalCenter
width: 240
fillMode: Image.PreserveAspectFit
source: "qrc:/assets/img/matrix.svg"
} }
Label { Controls.TextField {
anchors.horizontalCenter: parent.horizontalCenter
text: "Welcome to Matrix, a new era of instant messaging."
wrapMode: Label.Wrap
}
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: "To start chatting, select a room from the room list."
wrapMode: Label.Wrap
}
}
Rectangle {
anchors.fill: parent
visible: currentRoom
color: MSettings.darkTheme ? "#242424" : "#EBEFF2"
}
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: currentRoom
RoomHeader {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 64
z: 10
id: roomHeader
onClicked: roomDrawer.visible ? roomDrawer.close() : roomDrawer.open()
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: 960
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
width: Math.min(parent.width - 32, 960)
spacing: 16
AutoListView {
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
Layout.fillWidth: true
Layout.fillHeight: true
id: messageListView
spacing: 2
displayMarginBeginning: 100
displayMarginEnd: 100
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: [
ExpressionFilter {
expression: marks !== 0x10 && eventType !== "other"
}
]
onModelReset: {
movingTimer.stop()
if (currentRoom) {
movingTimer.restart()
// var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex())
// if (lastScrollPosition === 0) {
// messageListView.positionViewAtBeginning()
// } else {
// messageListView.currentIndex = lastScrollPosition
// }
if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20)
currentRoom.getPreviousContent(50)
messageListView.positionViewAtBeginning()
}
}
}
onContentYChanged: {
if(!noNeedMoreContent && contentY - 5000 < originY)
currentRoom.getPreviousContent(20);
}
populate: Transition {
NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: 200
}
}
add: Transition {
NumberAnimation {
property: "opacity"; from: 0; to: 1
duration: 200
}
}
move: Transition {
NumberAnimation {
property: "y"; duration: 200
}
NumberAnimation {
property: "opacity"; to: 1
}
}
displaced: Transition {
NumberAnimation {
property: "y"; duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
property: "opacity"; to: 1
}
}
delegate: DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "state"
delegate: StateDelegate {
anchors.horizontalCenter: parent.horizontalCenter
}
}
DelegateChoice {
roleValue: "emote"
delegate: StateDelegate {
anchors.horizontalCenter: parent.horizontalCenter
}
}
DelegateChoice {
roleValue: "message"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
MessageDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "notice"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
MessageDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "image"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
ImageDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "audio"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
AudioDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "video"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
VideoDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "file"
delegate: ColumnLayout {
width: messageListView.width
SectionDelegate {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
visible: showSection
}
FileDelegate {
Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 2
visible: readMarker
color: MPalette.primary
}
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
}
Control {
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 16
padding: 8
id: goReadMarkerFab
visible: currentRoom && currentRoom.hasUnreadMessages
contentItem: MaterialIcon {
icon: "\ue316"
font.pixelSize: 28
}
background: Rectangle {
color: MPalette.background
radius: height / 2
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: goToEvent(currentRoom.readMarkerEventId)
}
}
}
Control {
anchors.right: parent.right
anchors.bottom: parent.bottom
padding: 8
id: goTopFab
visible: !messageListView.atYEnd
contentItem: MaterialIcon {
icon: "\ue313"
font.pixelSize: 28
}
background: Rectangle {
color: MPalette.background
radius: height / 2
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: {
currentRoom.markAllMessagesAsRead()
messageListView.positionViewAtBeginning()
}
}
}
}
Control {
anchors.left: parent.left
anchors.bottom: parent.bottom
visible: currentRoom && currentRoom.usersTyping.length > 0
padding: 4
contentItem: RowLayout {
spacing: 0
RowLayout {
spacing: -8
Repeater {
model: currentRoom && currentRoom.usersTyping.length > 0 ? currentRoom.usersTyping : null
delegate: Rectangle {
Layout.preferredWidth: 28
Layout.preferredHeight: 28
color: "white"
radius: 14
Avatar {
anchors.fill: parent
anchors.margins: 2
source: modelData.avatarMediaId
hint: modelData.displayName
color: modelData.color
}
}
}
}
Item {
Layout.preferredWidth: 28
Layout.preferredHeight: 28
BusyIndicator {
anchors.centerIn: parent
width: 32
height: 32
}
}
}
background: Rectangle {
color: MPalette.background
radius: height / 2
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 1
}
}
}
Keys.onUpPressed: scrollBar.decrease()
Keys.onDownPressed: scrollBar.increase()
ScrollBar.vertical: ScrollBar {
id: scrollBar
}
}
RoomPanelInput {
Layout.fillWidth: true
id: roomPanelInput
}
} }
} }
Timer { background: Item {}
id: movingTimer
interval: 10000
repeat: true
running: false
onTriggered: saveReadMarker()
}
function goToEvent(eventID) {
var index = messageEventModel.eventIDToIndex(eventID)
if (index === -1) return
messageListView.positionViewAtIndex(sortedMessageEventModel.mapFromSource(index), ListView.Contain)
}
function saveViewport() {
currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX + (messageListView.width / 2), messageListView.contentY)), sortedMessageEventModel.mapToSource(messageListView.largestVisibleIndex))
}
function saveReadMarker() {
var readMarker = sortedMessageEventModel.get(messageListView.largestVisibleIndex).eventId
if (!readMarker) return
currentRoom.readMarkerEventId = readMarker
}
} }

View File

@@ -1,204 +1,51 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Qt.labs.settings 1.1
import Qt.labs.platform 1.1 as Platform
import Spectral.Panel 2.0 import org.kde.kirigami 2.4 as Kirigami
import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Component 2.0
import Spectral.Panel 2.0
ApplicationWindow { Kirigami.ApplicationWindow {
readonly property bool inPortrait: window.width < 640 id: root
Material.theme: MPalette.theme globalDrawer: Kirigami.GlobalDrawer {
Material.background: MPalette.background title: "Hello App"
titleIcon: "applications-graphics"
width: 960 actions: [
height: 640 Kirigami.Action {
minimumWidth: 480 text: "View"
minimumHeight: 360 iconName: "view-list-icons"
Kirigami.Action {
id: window text: "action 1"
}
visible: true Kirigami.Action {
title: qsTr("Spectral") text: "action 2"
}
font.family: MSettings.fontFamily Kirigami.Action {
text: "action 3"
background: Rectangle { }
color: MSettings.darkTheme ? "#303030" : "#FFFFFF" },
} Kirigami.Action {
text: "action 3"
TrayIcon {
id: trayIcon
visible: MSettings.showTray
iconSource: ":/assets/img/icon.png"
isOnline: spectralController.isOnline
onShowWindow: window.showWindow()
}
Platform.MenuBar {
id: menuBar
Platform.Menu {
id: fileMenu
title: "File"
Platform.MenuItem {
text: "Preferences"
shortcut: StandardKey.Preferences
role: Platform.MenuItem.PreferencesRole
onTriggered: accountDetailDialog.createObject(window).open()
} }
]
Platform.MenuItem {
text: "Quit"
shortcut: StandardKey.Quit
role: Platform.MenuItem.QuitRole
onTriggered: Qt.quit()
}
}
} }
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
pageStack.initialPage: roomListPanelComponent
Controller { Controller {
id: spectralController id: spectralController
quitOnLastWindowClosed: !MSettings.showTray quitOnLastWindowClosed: true
onErrorOccured: errorControl.show(error + ": " + detail, 3000) onErrorOccured: showPassiveNotification(error + ": " + detail)
}
NotificationsManager {
id: notificationsManager
onNotificationClicked: {
roomListForm.enteredRoom = spectralController.connection.room(roomId)
roomForm.goToEvent(eventId)
showWindow()
}
}
Shortcut {
sequence: "Ctrl+Q"
context: Qt.ApplicationShortcut
onActivated: Qt.quit()
}
ToolTip {
id: busyIndicator
parent: ApplicationWindow.overlay
visible: spectralController.busy
text: "Loading, please wait"
font.pixelSize: 14
}
ToolTip {
id: errorControl
parent: ApplicationWindow.overlay
font.pixelSize: 14
}
Component {
id: accountDetailDialog
AccountDetailDialog {}
}
Component {
id: loginDialog
LoginDialog {}
}
Component {
id: joinRoomDialog
JoinRoomDialog {}
}
Component {
id: startChatDialog
StartChatDialog {}
}
Component {
id: createRoomDialog
CreateRoomDialog {}
}
Component {
id: fontFamilyDialog
FontFamilyDialog {}
}
Drawer {
width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360)
height: window.height
modal: inPortrait
interactive: inPortrait
position: inPortrait ? 0 : 1
visible: !inPortrait
id: roomListDrawer
RoomListPanel {
anchors.fill: parent
id: roomListForm
clip: true
connection: spectralController.connection
onLeaveRoom: roomForm.saveViewport()
}
}
RoomPanel {
anchors.fill: parent
anchors.leftMargin: !inPortrait ? roomListDrawer.width : undefined
anchors.rightMargin: !inPortrait && roomDrawer.visible ? roomDrawer.width : undefined
id: roomForm
clip: true
currentRoom: roomListForm.enteredRoom
}
RoomDrawer {
width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360)
height: window.height
modal: inPortrait
interactive: inPortrait
edge: Qt.RightEdge
id: roomDrawer
room: roomListForm.enteredRoom
} }
Binding { Binding {
@@ -207,19 +54,34 @@ ApplicationWindow {
value: spectralController.connection value: spectralController.connection
} }
function showWindow() { RoomListModel {
window.show() id: spectralRoomListModel
window.raise()
window.requestActivate() connection: spectralController.connection
} }
function hideWindow() { Component {
window.hide() id: roomPanelComponent
RoomPanel {
currentRoom: root.currentRoom
}
} }
Component.onCompleted: { Component {
spectralController.initiated.connect(function() { id: roomListPanelComponent
if (spectralController.accountCount == 0) loginDialog.createObject(window).open()
}) RoomListPanel {
roomListModel: spectralRoomListModel
onEnterRoom: {
applicationWindow().pageStack.push(roomPanelComponent, {"currentRoom": room})
}
onLeaveRoom: {
var stack = applicationWindow().pageStack;
stack.removePage(stack.lastItem)
}
}
} }
} }

View File

@@ -1,12 +0,0 @@
; This file can be edited to change the style of the application
; Read "Qt Quick Controls 2 Configuration File" for details:
; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html
[Controls]
Style=Material
[Material]
Theme=Light
Variant=Dense
Primary=#344955
Accent=#4286F5

View File

@@ -1,6 +1,5 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>qtquickcontrols2.conf</file>
<file>qml/main.qml</file> <file>qml/main.qml</file>
<file>imports/Spectral/Component/Emoji/EmojiPicker.qml</file> <file>imports/Spectral/Component/Emoji/EmojiPicker.qml</file>
<file>imports/Spectral/Component/Emoji/qmldir</file> <file>imports/Spectral/Component/Emoji/qmldir</file>
@@ -12,7 +11,6 @@
<file>imports/Spectral/Component/qmldir</file> <file>imports/Spectral/Component/qmldir</file>
<file>imports/Spectral/Effect/ElevationEffect.qml</file> <file>imports/Spectral/Effect/ElevationEffect.qml</file>
<file>imports/Spectral/Effect/qmldir</file> <file>imports/Spectral/Effect/qmldir</file>
<file>assets/font/material.ttf</file>
<file>assets/img/icon.png</file> <file>assets/img/icon.png</file>
<file>imports/Spectral/Setting/Setting.qml</file> <file>imports/Spectral/Setting/Setting.qml</file>
<file>imports/Spectral/Font/MaterialFont.qml</file> <file>imports/Spectral/Font/MaterialFont.qml</file>
@@ -59,5 +57,7 @@
<file>imports/Spectral/Component/Timeline/ReactionDelegate.qml</file> <file>imports/Spectral/Component/Timeline/ReactionDelegate.qml</file>
<file>imports/Spectral/Component/Timeline/AudioDelegate.qml</file> <file>imports/Spectral/Component/Timeline/AudioDelegate.qml</file>
<file>imports/Spectral/Dialog/StartChatDialog.qml</file> <file>imports/Spectral/Dialog/StartChatDialog.qml</file>
<file>imports/Spectral/Component/Timeline/TextDelegate.qml</file>
<file>imports/Spectral/Component/Timeline/TimelineContainer.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -21,6 +21,9 @@ ThumbnailResponse::ThumbnailResponse(Quotient::Connection* c,
QString::number(requestedSize.width()), QString::number(requestedSize.width()),
QString::number(requestedSize.height()))), QString::number(requestedSize.height()))),
errorStr("Image request hasn't started") { errorStr("Image request hasn't started") {
if (!c) {
return;
}
if (requestedSize.isEmpty()) { if (requestedSize.isEmpty()) {
errorStr.clear(); errorStr.clear();
emit finished(); emit finished();

View File

@@ -34,7 +34,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
roles[UserMarkerRole] = "userMarker"; roles[UserMarkerRole] = "userMarker";
roles[ShowAuthorRole] = "showAuthor"; roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection"; roles[ShowSectionRole] = "showSection";
roles[BubbleShapeRole] = "bubbleShape";
roles[ReactionRole] = "reaction"; roles[ReactionRole] = "reaction";
return roles; return roles;
} }
@@ -83,7 +82,7 @@ void MessageEventModel::setRoom(SpectralRoom* room) {
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - auto rowBelowInserted = m_currentRoom->maxTimelineIndex() -
biggest + timelineBaseIndex() - 1; biggest + timelineBaseIndex() - 1;
refreshEventRoles(rowBelowInserted, refreshEventRoles(rowBelowInserted,
{ShowAuthorRole, BubbleShapeRole}); {ShowAuthorRole});
} }
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)
@@ -91,10 +90,7 @@ void MessageEventModel::setRoom(SpectralRoom* room) {
}); });
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, connect(m_currentRoom, &Room::pendingEventAboutToAdd, this,
[this] { beginInsertRows({}, 0, 0); }); [this] { beginInsertRows({}, 0, 0); });
connect(m_currentRoom, &Room::pendingEventAdded, this, [=] { connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
endInsertRows();
refreshEventRoles(1, {ShowAuthorRole, BubbleShapeRole});
});
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, connect(m_currentRoom, &Room::pendingEventAboutToMerge, this,
[this](RoomEvent*, int i) { [this](RoomEvent*, int i) {
if (i == 0) if (i == 0)
@@ -116,7 +112,7 @@ void MessageEventModel::setRoom(SpectralRoom* 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,
{ShowAuthorRole, BubbleShapeRole}); {ShowAuthorRole});
}); });
connect(m_currentRoom, &Room::pendingEventChanged, this, connect(m_currentRoom, &Room::pendingEventChanged, this,
&MessageEventModel::refreshRow); &MessageEventModel::refreshRow);
@@ -463,7 +459,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
} }
if (role == ShowAuthorRole) { if (role == ShowAuthorRole) {
for (auto r = row - 1; r >= 0; --r) { for (auto r = row + 1; r < 0; ++r) {
auto i = index(r); auto i = index(r);
if (data(i, SpecialMarksRole) != EventStatus::Hidden) { if (data(i, SpecialMarksRole) != EventStatus::Hidden) {
return data(i, AuthorRole) != data(idx, AuthorRole) || return data(i, AuthorRole) != data(idx, AuthorRole) ||
@@ -490,34 +486,6 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
return true; return true;
} }
if (role == BubbleShapeRole) { // TODO: Convoluted logic.
int aboveRow = -1; // Invalid
for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r);
if (data(i, SpecialMarksRole) != EventStatus::Hidden) {
aboveRow = r;
break;
}
}
bool aboveShow, belowShow;
if (aboveRow == -1) {
aboveShow = true;
} else {
aboveShow = data(index(aboveRow), ShowAuthorRole).toBool();
}
belowShow = data(idx, ShowAuthorRole).toBool();
if (aboveShow && belowShow)
return BubbleShapes::NoShape;
if (aboveShow && !belowShow)
return BubbleShapes::BeginShape;
if (belowShow)
return BubbleShapes::EndShape;
return BubbleShapes::MiddleShape;
}
if (role == ReactionRole) { if (role == ReactionRole) {
const auto& annotations = const auto& annotations =
m_currentRoom->relatedEvents(evt, EventRelation::Annotation()); m_currentRoom->relatedEvents(evt, EventRelation::Annotation());

View File

@@ -32,8 +32,6 @@ class MessageEventModel : public QAbstractListModel {
ShowAuthorRole, ShowAuthorRole,
ShowSectionRole, ShowSectionRole,
BubbleShapeRole,
ReactionRole, ReactionRole,
// For debugging // For debugging

View File

@@ -244,7 +244,7 @@ QString SpectralRoom::eventToString(const RoomEvent& evt,
if (removeReply) { if (removeReply) {
htmlBody.remove(utils::removeRichReplyRegex); htmlBody.remove(utils::removeRichReplyRegex);
} }
htmlBody.replace(utils::userPillRegExp, "<b>\\1</b>"); htmlBody.replace(utils::userPillRegExp, "<b class=\"user-pill\">\\1</b>");
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>"); htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
return htmlBody; return htmlBody;