Somewhat better hierarchy.

Init separate .qml files.
square-messages
Black Hat 2018-10-02 10:07:47 +08:00
parent 91fc03aba5
commit ae538a7c45
28 changed files with 1468 additions and 1321 deletions

View File

@ -0,0 +1,113 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
TextArea {
property real progress: 0
wrapMode: Text.Wrap
placeholderText: "Send a Message"
leftPadding: 16
topPadding: 0
bottomPadding: 0
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
text: currentRoom ? currentRoom.cachedInput : ""
background: Item {
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
}
}
Timer {
id: repeatTimer
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
ToolTip.visible: currentRoom
&& currentRoom.hasUsersTyping
ToolTip.text: currentRoom ? currentRoom.usersTyping : ""
Keys.onReturnPressed: {
if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n")
} else {
postMessage(text)
text = ""
}
}
onTextChanged: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
}
function postMessage(text) {
if (text.trim().length === 0) { return }
if(!currentRoom) { return }
var PREFIX_ME = '/me '
var PREFIX_NOTICE = '/notice '
var PREFIX_RAINBOW = '/rainbow '
var PREFIX_HTML = '/html '
var PREFIX_MARKDOWN = '/md '
var replyRe = new RegExp("^> <(.*)><(.*)> (.*)\n\n(.*)")
if (text.match(replyRe)) {
var matches = text.match(replyRe)
currentRoom.sendReply(matches[1], matches[2], matches[3], matches[4])
return
}
if (text.indexOf(PREFIX_ME) === 0) {
text = text.substr(PREFIX_ME.length)
currentRoom.postMessage(text, RoomMessageEvent.Emote)
return
}
if (text.indexOf(PREFIX_NOTICE) === 0) {
text = text.substr(PREFIX_NOTICE.length)
currentRoom.postMessage(text, RoomMessageEvent.Notice)
return
}
if (text.indexOf(PREFIX_RAINBOW) === 0) {
text = text.substr(PREFIX_RAINBOW.length)
var parsedText = ""
var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"]
for (var i = 0; i < text.length; i++) {
parsedText = parsedText + "<font color='" + rainbowColor[i % rainbowColor.length] + "'>" + text.charAt(i) + "</font>"
}
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
return
}
if (text.indexOf(PREFIX_HTML) === 0) {
text = text.substr(PREFIX_HTML.length)
var re = new RegExp("<.*?>")
var plainText = text.replace(re, "")
currentRoom.postHtmlMessage(plainText, text, RoomMessageEvent.Text)
return
}
if (text.indexOf(PREFIX_MARKDOWN) === 0) {
text = text.substr(PREFIX_MARKDOWN.length)
var parsedText = Markdown.markdown_parser(text)
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
return
}
currentRoom.postPlainText(text)
}
}

View File

@ -6,6 +6,8 @@ import QtQuick.Controls.Material 2.2
import Spectral 0.1
import Spectral.Setting 0.1
import Spectral.Component 2.0
import "qrc:/js/util.js" as Util
RowLayout {

View File

@ -2,3 +2,6 @@ module Spectral.Component
AutoMouseArea 2.0 AutoMouseArea.qml
MaterialIcon 2.0 MaterialIcon.qml
SideNavButton 2.0 SideNavButton.qml
AutoImage 2.0 AutoImage.qml
AutoLabel 2.0 AutoLabel.qml
AutoTextArea 2.0 AutoTextArea.qml

View File

@ -1,539 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 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 "qrc:/js/md.js" as Markdown
import "qrc:/js/util.js" as Util
Item {
property var currentRoom: null
id: item
MessageEventModel {
id: messageEventModel
room: currentRoom
}
RoomDrawer {
width: Math.min(item.width * 0.7, 480)
height: item.height
id: roomDrawer
room: currentRoom
}
Label {
anchors.centerIn: parent
visible: !currentRoom
text: "Please choose a room."
}
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: currentRoom
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 64
z: 10
color: Material.accent
ItemDelegate {
anchors.fill: parent
onClicked: roomDrawer.open()
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
ImageItem {
Layout.preferredWidth: height
Layout.fillHeight: true
hint: currentRoom ? currentRoom.displayName : "No name"
image: spectralController.safeImage(currentRoom ? currentRoom.avatar : null)
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: parent.width > 64
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: currentRoom ? currentRoom.displayName : ""
color: "white"
font.pointSize: 12
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
color: "white"
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
ProgressBar {
Layout.fillWidth: true
z: 10
visible: currentRoom && currentRoom.busy
indeterminate: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 16
Layout.rightMargin: 16
id: messageListView
displayMarginBeginning: 40
displayMarginEnd: 40
verticalLayoutDirection: ListView.BottomToTop
spacing: 8
cacheBuffer: 200
boundsBehavior: Flickable.DragOverBounds
property int largestVisibleIndex: count > 0 ? indexAt(contentX, contentY + height - 1) : -1
onContentYChanged: {
if(verticalVelocity < 0 && contentY - 5000 < originY)
currentRoom.getPreviousContent(50);
}
onMovementEnded: {
currentRoom.saveViewport(sortedMessageEventModel.mapToSource(indexAt(contentX, contentY)), sortedMessageEventModel.mapToSource(largestVisibleIndex))
var newReadMarker = sortedMessageEventModel.get(largestVisibleIndex).eventId
if (newReadMarker) currentRoom.readMarkerEventId = newReadMarker
}
displaced: Transition {
NumberAnimation {
property: "y"; duration: 200
easing.type: Easing.OutQuad
}
}
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: ExpressionFilter {
expression: marks !== 0x08 && marks !== 0x10
}
onModelReset: {
if (currentRoom)
{
var lastScrollPosition = mapFromSource(currentRoom.savedTopVisibleIndex())
if (lastScrollPosition === 0)
messageListView.positionViewAtBeginning()
else
{
console.log("Scrolling to position", lastScrollPosition)
messageListView.currentIndex = lastScrollPosition
}
if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize === 0)
currentRoom.getPreviousContent(100)
}
console.log("Model timeline reset")
}
}
delegate: ColumnLayout {
width: parent.width
id: delegateColumn
spacing: 8
Label {
Layout.alignment: Qt.AlignHCenter
visible: section !== aboveSection
text: section
color: "white"
verticalAlignment: Text.AlignVCenter
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
background: Rectangle {
color: MSettings.darkTheme ? "#484848" : "grey"
}
}
MessageDelegate {
visible: eventType === "notice" || eventType === "message" || eventType === "image" || eventType === "video" || eventType === "audio" || eventType === "file"
}
StateDelegate {
Layout.maximumWidth: messageListView.width * 0.8
visible: eventType === "emote" || eventType === "state"
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: eventType === "other"
text: display
color: "grey"
font.italic: true
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: readMarker === true && index !== 0
text: "And Now"
color: "white"
verticalAlignment: Text.AlignVCenter
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
background: Rectangle { color: MSettings.darkTheme ? "#484848" : "grey" }
}
}
RoundButton {
width: 64
height: 64
id: goTopFab
visible: !(parent.atYEnd || messageListView.moving)
anchors.right: parent.right
anchors.bottom: parent.bottom
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue313"
color: "white"
}
Material.background: Material.accent
onClicked: parent.positionViewAtBeginning()
Behavior on opacity { NumberAnimation { duration: 200 } }
}
MessageContextMenu { id: messageContextMenu }
Popup {
property string sourceText
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 480
id: sourceDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ScrollView {
TextArea {
readOnly: true
selectByMouse: true
text: sourceDialog.sourceText
}
}
}
Popup {
property alias listModel: readMarkerListView.model
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 320
id: readMarkerDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ListView {
implicitHeight: Math.min(window.height - 64, readMarkerListView.contentHeight)
id: readMarkerListView
clip: true
boundsBehavior: Flickable.DragOverBounds
delegate: ItemDelegate {
width: parent.width
height: 48
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
ImageItem {
Layout.preferredWidth: height
Layout.fillHeight: true
image: modelData.avatar
hint: modelData.displayName
}
Label {
Layout.fillWidth: true
text: modelData.displayName
}
}
}
ScrollBar.vertical: ScrollBar {}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 40
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.leftMargin: 16
Layout.rightMargin: 16
color: Material.background
Rectangle {
anchors.verticalCenter: parent.top
width: parent.width
height: 48
color: MSettings.darkTheme ? "#303030" : "#fafafa"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
RowLayout {
anchors.fill: parent
spacing: 0
ItemDelegate {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
contentItem: MaterialIcon { icon: "\ue226" }
onClicked: currentRoom.chooseAndUploadFile()
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 48
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
clip: true
TextArea {
property real progress: 0
id: inputField
wrapMode: Text.Wrap
placeholderText: "Send a Message"
leftPadding: 16
topPadding: 0
bottomPadding: 0
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
text: currentRoom ? currentRoom.cachedInput : ""
background: Item {}
onTextChanged: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
}
ToolTip.visible: currentRoom && currentRoom.hasUsersTyping
ToolTip.text: currentRoom ? currentRoom.usersTyping : ""
Keys.onReturnPressed: {
if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(inputField.cursorPosition, "\n")
} else {
inputField.postMessage(inputField.text)
inputField.text = ""
}
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
}
}
Timer {
id: repeatTimer
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
function postMessage(text) {
if (text.trim().length === 0) { return }
if(!currentRoom) { return }
var PREFIX_ME = '/me '
var PREFIX_NOTICE = '/notice '
var PREFIX_RAINBOW = '/rainbow '
var PREFIX_HTML = '/html '
var PREFIX_MARKDOWN = '/md '
var replyRe = new RegExp("^> <(.*)><(.*)> (.*)\n\n(.*)")
if (text.match(replyRe)) {
var matches = text.match(replyRe)
currentRoom.sendReply(matches[1], matches[2], matches[3], matches[4])
return
}
if (text.indexOf(PREFIX_ME) === 0) {
text = text.substr(PREFIX_ME.length)
currentRoom.postMessage(text, RoomMessageEvent.Emote)
return
}
if (text.indexOf(PREFIX_NOTICE) === 0) {
text = text.substr(PREFIX_NOTICE.length)
currentRoom.postMessage(text, RoomMessageEvent.Notice)
return
}
if (text.indexOf(PREFIX_RAINBOW) === 0) {
text = text.substr(PREFIX_RAINBOW.length)
var parsedText = ""
var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"]
for (var i = 0; i < text.length; i++) {
parsedText = parsedText + "<font color='" + rainbowColor[i % rainbowColor.length] + "'>" + text.charAt(i) + "</font>"
}
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
return
}
if (text.indexOf(PREFIX_HTML) === 0) {
text = text.substr(PREFIX_HTML.length)
var re = new RegExp("<.*?>")
var plainText = text.replace(re, "")
currentRoom.postHtmlMessage(plainText, text, RoomMessageEvent.Text)
return
}
if (text.indexOf(PREFIX_MARKDOWN) === 0) {
text = text.substr(PREFIX_MARKDOWN.length)
var parsedText = Markdown.markdown_parser(text)
currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text)
return
}
currentRoom.postPlainText(text)
}
}
}
ItemDelegate {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
id: emojiButton
contentItem: MaterialIcon { icon: "\ue24e" }
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open()
EmojiPicker {
x: window.width - 370
y: window.height - 400
width: 360
height: 320
id: emojiPicker
parent: ApplicationWindow.overlay
Material.elevation: 2
textArea: inputField
}
}
}
}
}
}
}

View File

@ -1,266 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls.Material 2.2
import QtQml.Models 2.3
import Spectral.Component 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 "qrc:/js/util.js" as Util
Rectangle {
property alias listModel: sortedRoomListModel.sourceModel
property int filter: 0
property var enteredRoom: null
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
Label {
text: MSettings.miniMode ? "Empty" : "Here? No, not here."
anchors.centerIn: parent
visible: listView.count === 0
}
ColumnLayout {
anchors.fill: parent
spacing: 0
TextField {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.margins: 12
id: searchField
leftPadding: MSettings.miniMode ? 4 : 32
topPadding: 0
bottomPadding: 0
placeholderText: "Search..."
background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#fafafa"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: searchField.focus ? 2 : 1
}
}
Shortcut {
sequence: StandardKey.Find
onActivated: searchField.forceActiveFocus()
}
}
SortFilterProxyModel {
id: sortedRoomListModel
proxyRoles: ExpressionRole {
name: "display"
expression: {
switch (category) {
case 1: return "Invited"
case 2: return "Favorites"
case 3: return "Rooms"
case 4: return "People"
case 5: return "Low Priority"
}
}
}
sorters: [
RoleSorter { roleName: "category" },
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
}
]
}
SortFilterProxyModel {
id: roomListProxyModel
sourceModel: sortedRoomListModel
filters: [
RegExpFilter {
roleName: "name"
pattern: searchField.text
caseSensitivity: Qt.CaseInsensitive
},
ExpressionFilter {
enabled: filter === 1
expression: unreadCount > 0
},
ExpressionFilter {
enabled: filter === 2
expression: category === 1 || category === 2 || category === 4
},
ExpressionFilter {
enabled: filter === 3
expression: category === 3 || category === 5
}
]
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: listView
spacing: 1
clip: true
model: roomListProxyModel
currentIndex: -1
highlightFollowsCurrentItem: true
highlightMoveDuration: 200
highlightResizeDuration: 0
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {}
delegate: Rectangle {
readonly property bool highlighted: currentRoom === enteredRoom
width: parent.width
height: 64
color: MSettings.darkTheme ? "#303030" : "#fafafa"
AutoMouseArea {
anchors.fill: parent
hoverEnabled: MSettings.miniMode
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
}
Rectangle {
anchors.fill: parent
visible: highlightCount > 0 || highlighted
color: Material.accent
opacity: 0.1
}
Rectangle {
width: unreadCount > 0 || highlighted ? 4 : 0
height: parent.height
color: Material.accent
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
ImageItem {
id: imageItem
Layout.preferredWidth: height
Layout.fillHeight: true
hint: name || "No Name"
image: avatar
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: parent.width > 64
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
font.pointSize: 12
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,"")
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
section.property: "display"
section.criteria: ViewSection.FullString
section.delegate: Label {
width: parent.width
height: 24
text: section
color: "grey"
leftPadding: MSettings.miniMode ? undefined : 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: MSettings.miniMode ? Text.AlignHCenter : undefined
}
RoomContextMenu { id: roomContextMenu }
Dialog {
property var currentRoom
id: inviteDialog
parent: ApplicationWindow.overlay
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
title: "Action Required"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: Label { text: "Accept this invitation?" }
onAccepted: currentRoom.acceptInvitation()
onRejected: currentRoom.forget()
}
}
}
}

View File

@ -1,4 +0,0 @@
module Spectral.Form
RoomForm 2.0 RoomForm.qml
RoomListForm 2.0 RoomListForm.qml

View File

@ -1,4 +1,3 @@
module Spectral.Menu
MessageContextMenu 2.0 MessageContextMenu.qml
RoomContextMenu 2.0 RoomContextMenu.qml

View File

@ -1,162 +1,20 @@
import QtQuick 2.9
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Setting 0.1
Page {
property var controller
Row {
anchors.fill: parent
Pane {
width: parent.width / 2
height: parent.height
background: Item {
Image {
id: background
anchors.fill: parent
source: "qrc:/assets/img/background.jpg"
fillMode: Image.PreserveAspectCrop
cache: false
}
ColorOverlay {
anchors.fill: background
source: background
color: Material.accent
opacity: 0.7
}
}
Column {
x: 32
anchors.verticalCenter: parent.verticalCenter
Label {
text: "MATRIX LOGIN."
font.pointSize: 28
font.bold: true
color: "white"
}
Label {
text: "A NEW METHOD OF MESSAGING"
font.pointSize: 12
color: "white"
}
}
LoginForm {
loginButton.onClicked: {
if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) {
loginButtonTooltip.text = "Server address should start with http(s)://"
loginButtonTooltip.open()
return
}
if (!(usernameField.text.startsWith("@") && usernameField.text.includes(":"))) {
loginButtonTooltip.text = "Username should be in format of @example:example.com"
loginButtonTooltip.open()
return
}
Pane {
width: parent.width / 2
height: parent.height
controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
padding: 64
ColumnLayout {
width: parent.width
id: mainCol
TextField {
Layout.fillWidth: true
id: serverField
leftPadding: 16
topPadding: 0
bottomPadding: 0
text: "https://matrix.org"
placeholderText: "Server"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
TextField {
Layout.fillWidth: true
id: usernameField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Username"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
TextField {
Layout.fillWidth: true
id: passwordField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Password"
echoMode: TextInput.Password
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
Button {
Layout.fillWidth: true
id: loginButton
text: "LOGIN"
highlighted: true
ToolTip {
id: loginButtonTooltip
}
onClicked: {
if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) {
loginButtonTooltip.text = "Server address should start with http(s)://"
loginButtonTooltip.open()
return
}
if (!(usernameField.text.startsWith("@") && usernameField.text.includes(":"))) {
loginButtonTooltip.text = "Username should be in format of @example:example.com"
loginButtonTooltip.open()
return
}
controller.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
controller.connectionAdded.connect(function() { stackView.pop() })
}
}
}
}
controller.connectionAdded.connect(function() { stackView.pop() })
}
}

View File

@ -0,0 +1,147 @@
import QtQuick 2.9
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Setting 0.1
Page {
property var controller
property alias loginButton: loginButton
Row {
anchors.fill: parent
Pane {
width: parent.width / 2
height: parent.height
background: Item {
Image {
id: background
anchors.fill: parent
source: "qrc:/assets/img/background.jpg"
fillMode: Image.PreserveAspectCrop
cache: false
}
ColorOverlay {
anchors.fill: background
source: background
color: Material.accent
opacity: 0.7
}
}
Column {
x: 32
anchors.verticalCenter: parent.verticalCenter
Label {
text: "MATRIX LOGIN."
font.pointSize: 28
font.bold: true
color: "white"
}
Label {
text: "A NEW METHOD OF MESSAGING"
font.pointSize: 12
color: "white"
}
}
}
Pane {
width: parent.width / 2
height: parent.height
padding: 64
ColumnLayout {
width: parent.width
id: mainCol
TextField {
Layout.fillWidth: true
id: serverField
leftPadding: 16
topPadding: 0
bottomPadding: 0
text: "https://matrix.org"
placeholderText: "Server"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
TextField {
Layout.fillWidth: true
id: usernameField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Username"
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
TextField {
Layout.fillWidth: true
id: passwordField
leftPadding: 16
topPadding: 0
bottomPadding: 0
placeholderText: "Password"
echoMode: TextInput.Password
background: Rectangle {
implicitHeight: 48
color: MSettings.darkTheme ? "#242424" : "#eaeaea"
border.color: parent.activeFocus ? Material.accent : "transparent"
border.width: 2
}
}
Button {
Layout.fillWidth: true
id: loginButton
text: "LOGIN"
highlighted: true
ToolTip {
id: loginButtonTooltip
}
}
}
}
}
}

View File

@ -1,55 +1,5 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Form 2.0
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Page {
property alias connection: roomListModel.connection
property alias filter: roomListForm.filter
id: page
RoomListModel {
id: roomListModel
onNewMessage: if (!window.active) spectralController.showMessage(roomName, content, icon)
}
RowLayout {
anchors.fill: parent
spacing: 0
RoomListForm {
Layout.fillHeight: true
Layout.preferredWidth: MSettings.miniMode ? 64 : page.width * 0.35
Layout.minimumWidth: 64
Layout.maximumWidth: 360
id: roomListForm
listModel: roomListModel
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
}
RoomForm {
Layout.fillWidth: true
Layout.fillHeight: true
id: roomForm
currentRoom: roomListForm.enteredRoom
}
}
RoomForm {
roomListModel.onNewMessage: if (!window.active) spectralController.showMessage(roomName, content, icon)
}

View File

@ -0,0 +1,61 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Panel 2.0
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Page {
property alias connection: roomListModel.connection
property alias filter: roomListForm.filter
property alias roomListModel: roomListModel
id: page
RoomListModel {
id: roomListModel
}
RowLayout {
anchors.fill: parent
spacing: 0
RoomListPanel {
Layout.fillHeight: true
Layout.preferredWidth: MSettings.miniMode ? 64 : page.width * 0.35
Layout.minimumWidth: 64
Layout.maximumWidth: 360
id: roomListForm
listModel: roomListModel
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
}
RoomPanel {
Layout.fillWidth: true
Layout.fillHeight: true
id: roomForm
currentRoom: roomListForm.enteredRoom
}
}
}
/*##^## Designer {
D{i:0;autoSize:false;height:480;width:640}
}
##^##*/

View File

@ -1,297 +1,5 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
import "qrc:/js/util.js" as Util
Page {
property alias listModel: accountSettingsListView.model
Page {
id: accountForm
parent: null
padding: 64
ColumnLayout {
anchors.fill: parent
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: accountSettingsListView
boundsBehavior: Flickable.DragOverBounds
clip: true
delegate: Column {
property bool expanded: false
spacing: 8
ItemDelegate {
width: accountSettingsListView.width
height: 64
Row {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ImageItem {
width: parent.height
height: parent.height
hint: user.displayName
image: user.avatar
}
ColumnLayout {
Label {
text: user.displayName
}
Label {
text: user.id
}
}
}
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: spectralController.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
}
}
Button {
Layout.fillWidth: true
highlighted: true
text: "Logout"
onClicked: spectralController.logout(connection)
}
Behavior on height {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
}
}
Button {
Layout.fillWidth: true
text: "Add Account"
flat: true
highlighted: true
onClicked: stackView.push(loginPage)
}
}
}
Page {
id: generalForm
parent: null
padding: 64
Column {
Switch {
text: "Use press and hold instead of right click"
checked: MSettings.pressAndHold
onCheckedChanged: MSettings.pressAndHold = checked
}
}
}
Page {
id: appearanceForm
parent: null
padding: 64
Column {
Switch {
text: "Dark theme"
checked: MSettings.darkTheme
onCheckedChanged: MSettings.darkTheme = checked
}
Switch {
text: "Mini Room List"
checked: MSettings.miniMode
onCheckedChanged: MSettings.miniMode = checked
}
}
}
Page {
id: aboutForm
parent: null
padding: 64
ColumnLayout {
spacing: 16
Image {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
source: "qrc:/assets/img/icon.png"
}
Label { text: "Spectral, an IM client for the Matrix protocol." }
Label { text: "Released under GNU General Public License, version 3." }
}
}
Rectangle {
width: 240
height: parent.height
z: 10
id: settingDrawer
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 4
}
Column {
anchors.fill: parent
ItemDelegate {
width: parent.width
text: "Account"
onClicked: pushToStack(accountForm)
}
ItemDelegate {
width: parent.width
text: "General"
onClicked: pushToStack(generalForm)
}
ItemDelegate {
width: parent.width
text: "Appearance"
onClicked: pushToStack(appearanceForm)
}
ItemDelegate {
width: parent.width
text: "About"
onClicked: pushToStack(aboutForm)
}
}
}
StackView {
anchors.fill: parent
anchors.leftMargin: settingDrawer.width
id: settingStackView
}
function pushToStack(item) {
settingStackView.clear()
settingStackView.push(item)
}
SettingForm {
addAccountButton.onClicked: stackView.push(loginPage)
}

View File

@ -0,0 +1,136 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Spectral.Component 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Column {
property bool expanded: false
spacing: 8
ItemDelegate {
width: accountSettingsListView.width
height: 64
Row {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ImageItem {
width: parent.height
height: parent.height
hint: user.displayName
image: user.avatar
}
ColumnLayout {
Label {
text: user.displayName
}
Label {
text: user.id
}
}
}
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: spectralController.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
}
}
Button {
Layout.fillWidth: true
highlighted: true
text: "Logout"
}
}
}

View File

@ -0,0 +1,11 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
ItemDelegate {
text: category
onClicked: {
settingStackView.clear()
settingStackView.push([accountForm, generalForm, appearanceForm, aboutForm][form])
}
}

View File

@ -0,0 +1,178 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3
import Spectral.Component 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
Page {
property alias listModel: accountSettingsListView.model
property alias addAccountButton: addAccountButton
implicitWidth: 400
implicitHeight: 300
Page {
id: accountForm
parent: null
padding: 64
ColumnLayout {
anchors.fill: parent
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: accountSettingsListView
boundsBehavior: Flickable.DragOverBounds
clip: true
delegate: SettingAccountDelegate {}
}
Button {
Layout.fillWidth: true
id: addAccountButton
text: "Add Account"
flat: true
highlighted: true
}
}
}
Page {
id: generalForm
parent: null
padding: 64
Column {
Switch {
text: "Use press and hold instead of right click"
checked: MSettings.pressAndHold
onCheckedChanged: MSettings.pressAndHold = checked
}
}
}
Page {
id: appearanceForm
parent: null
padding: 64
Column {
Switch {
text: "Dark theme"
checked: MSettings.darkTheme
onCheckedChanged: MSettings.darkTheme = checked
}
Switch {
text: "Mini Room List"
checked: MSettings.miniMode
onCheckedChanged: MSettings.miniMode = checked
}
}
}
Page {
id: aboutForm
parent: null
padding: 64
ColumnLayout {
spacing: 16
Image {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
source: "qrc:/assets/img/icon.png"
}
Label {
text: "Spectral, an IM client for the Matrix protocol."
}
Label {
text: "Released under GNU General Public License, version 3."
}
}
}
Rectangle {
width: 240
height: parent.height
z: 10
id: settingDrawer
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 4
}
Column {
anchors.fill: parent
Repeater {
model: ListModel {
ListElement {
category: "Account"
form: 0
}
ListElement {
category: "General"
form: 1
}
ListElement {
category: "Appearance"
form: 2
}
ListElement {
category: "About"
form: 3
}
}
delegate: SettingCategoryDelegate {
width: parent.width
}
}
}
}
StackView {
anchors.fill: parent
anchors.leftMargin: settingDrawer.width
id: settingStackView
initialItem: aboutForm
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -2,4 +2,3 @@ module Spectral.Page
Login 2.0 Login.qml
Room 2.0 Room.qml
Setting 2.0 Setting.qml

View File

@ -0,0 +1,69 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral 0.1
Rectangle {
property alias image: headerImage.image
property alias topic: headerTopicLabel.text
signal clicked()
id: header
color: Material.accent
ItemDelegate {
anchors.fill: parent
id: roomHeader
onClicked: header.clicked()
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
ImageItem {
Layout.preferredWidth: height
Layout.fillHeight: true
id: headerImage
hint: currentRoom ? currentRoom.displayName : "No name"
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: parent.width > 64
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: currentRoom ? currentRoom.displayName : ""
color: "white"
font.pointSize: 12
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
id: headerTopicLabel
color: "white"
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}

View File

@ -0,0 +1,101 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral 0.1
import Spectral.Setting 0.1
import Spectral.Component 2.0
Rectangle {
readonly property bool highlighted: currentRoom === enteredRoom
color: MSettings.darkTheme ? "#303030" : "#fafafa"
AutoMouseArea {
anchors.fill: parent
hoverEnabled: MSettings.miniMode
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
}
Rectangle {
anchors.fill: parent
visible: highlightCount > 0 || highlighted
color: Material.accent
opacity: 0.1
}
Rectangle {
width: unreadCount > 0 || highlighted ? 4 : 0
height: parent.height
color: Material.accent
Behavior on width {
PropertyAnimation { easing.type: Easing.InOutCubic; duration: 200 }
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
ImageItem {
id: imageItem
Layout.preferredWidth: height
Layout.fillHeight: true
hint: name || "No Name"
image: avatar
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: parent.width > 64
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
font.pointSize: 12
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,"")
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}

View File

@ -0,0 +1,44 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import SortFilterProxyModel 0.2
RoomListPanelForm {
sortedRoomListModel.proxyRoles: ExpressionRole {
name: "display"
expression: {
switch (category) {
case 1: return "Invited"
case 2: return "Favorites"
case 3: return "Rooms"
case 4: return "People"
case 5: return "Low Priority"
}
}
}
Shortcut {
sequence: StandardKey.Find
onActivated: searchField.forceActiveFocus()
}
Dialog {
property var currentRoom
id: inviteDialog
parent: ApplicationWindow.overlay
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
title: "Action Required"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: Label { text: "Accept this invitation?" }
onAccepted: currentRoom.acceptInvitation()
onRejected: currentRoom.forget()
}
}

View File

@ -0,0 +1,141 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls.Material 2.2
import QtQml.Models 2.3
import Spectral.Component 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 "qrc:/js/util.js" as Util
Rectangle {
property alias listModel: sortedRoomListModel.sourceModel
property int filter: 0
property var enteredRoom: null
property alias searchField: searchField
property alias sortedRoomListModel: sortedRoomListModel
color: MSettings.darkTheme ? "#323232" : "#f3f3f3"
Label {
text: MSettings.miniMode ? "Empty" : "Here? No, not here."
anchors.centerIn: parent
visible: listView.count === 0
}
ColumnLayout {
anchors.fill: parent
spacing: 0
TextField {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.margins: 12
id: searchField
leftPadding: MSettings.miniMode ? 4 : 32
topPadding: 0
bottomPadding: 0
placeholderText: "Search..."
background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#fafafa"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: searchField.focus ? 2 : 1
}
}
}
SortFilterProxyModel {
id: sortedRoomListModel
sorters: [
RoleSorter { roleName: "category" },
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
}
]
}
SortFilterProxyModel {
id: roomListProxyModel
sourceModel: sortedRoomListModel
filters: [
RegExpFilter {
roleName: "name"
pattern: searchField.text
caseSensitivity: Qt.CaseInsensitive
},
ExpressionFilter {
enabled: filter === 1
expression: unreadCount > 0
},
ExpressionFilter {
enabled: filter === 2
expression: category === 1 || category === 2 || category === 4
},
ExpressionFilter {
enabled: filter === 3
expression: category === 3 || category === 5
}
]
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
id: listView
spacing: 1
clip: true
model: roomListProxyModel
currentIndex: -1
highlightFollowsCurrentItem: true
highlightMoveDuration: 200
highlightResizeDuration: 0
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: ScrollBar {}
delegate: RoomListDelegate {
width: parent.width
height: 64
}
section.property: "display"
section.criteria: ViewSection.FullString
section.delegate: Label {
width: parent.width
height: 24
text: section
color: "grey"
leftPadding: MSettings.miniMode ? undefined : 16
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: MSettings.miniMode ? Text.AlignHCenter : undefined
}
RoomContextMenu { id: roomContextMenu }
}
}
}

View File

@ -0,0 +1,56 @@
import QtQuick 2.9
RoomPanelForm {
roomHeader.onClicked: roomDrawer.open()
roomHeader.image: spectralController.safeImage(currentRoom ? currentRoom.avatar : null)
roomHeader.topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
sortedMessageEventModel.onModelReset: {
if (currentRoom)
{
var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex())
if (lastScrollPosition === 0)
messageListView.positionViewAtBeginning()
else
{
console.log("Scrolling to position", lastScrollPosition)
messageListView.currentIndex = lastScrollPosition
}
if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize === 0)
currentRoom.getPreviousContent(100)
}
console.log("Model timeline reset")
}
messageListView {
property int largestVisibleIndex: messageListView.count > 0 ? messageListView.indexAt(messageListView.contentX, messageListView.contentY + height - 1) : -1
onContentYChanged: {
if(messageListView.verticalVelocity < 0 && messageListView.contentY - 5000 < messageListView.originY)
currentRoom.getPreviousContent(50);
}
onMovementEnded: {
currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX, messageListView.contentY)), sortedMessageEventModel.mapToSource(largestVisibleIndex))
var newReadMarker = sortedMessageEventModel.get(largestVisibleIndex).eventId
if (newReadMarker) currentRoom.readMarkerEventId = newReadMarker
}
displaced: Transition {
NumberAnimation {
property: "y"; duration: 200
easing.type: Easing.OutQuad
}
}
}
goTopFab {
onClicked: messageListView.positionViewAtBeginning()
Behavior on opacity { NumberAnimation { duration: 200 } }
}
uploadButton.onClicked: currentRoom.chooseAndUploadFile()
emojiButton.onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open()
}

View File

@ -0,0 +1,364 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 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 "qrc:/js/md.js" as Markdown
import "qrc:/js/util.js" as Util
Item {
property var currentRoom: null
property alias roomHeader: roomHeader
property alias messageListView: messageListView
property alias goTopFab: goTopFab
property alias uploadButton: uploadButton
property alias emojiButton: emojiButton
property alias emojiPicker: emojiPicker
property alias sortedMessageEventModel: sortedMessageEventModel
property alias inputField: inputField
property alias roomDrawer: roomDrawer
id: root
MessageEventModel {
id: messageEventModel
room: currentRoom
}
RoomDrawer {
width: Math.min(root.width * 0.7, 480)
height: root.height
id: roomDrawer
room: currentRoom
}
Label {
anchors.centerIn: parent
visible: !currentRoom
text: "Please choose a room."
}
ColumnLayout {
anchors.fill: parent
spacing: 0
visible: currentRoom
RoomHeader {
Layout.fillWidth: true
Layout.preferredHeight: 64
z: 10
id: roomHeader
}
ProgressBar {
Layout.fillWidth: true
z: 10
visible: currentRoom && currentRoom.busy
indeterminate: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 16
Layout.rightMargin: 16
id: messageListView
displayMarginBeginning: 40
displayMarginEnd: 40
verticalLayoutDirection: ListView.BottomToTop
spacing: 8
boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: ExpressionFilter {
expression: marks !== 0x08 && marks !== 0x10
}
}
delegate: ColumnLayout {
width: parent.width
id: delegateColumn
spacing: 8
Label {
Layout.alignment: Qt.AlignHCenter
visible: section !== aboveSection
text: section
color: "white"
verticalAlignment: Text.AlignVCenter
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
background: Rectangle {
color: MSettings.darkTheme ? "#484848" : "grey"
}
}
MessageDelegate {
visible: eventType === "notice" || eventType === "message"
|| eventType === "image" || eventType === "video"
|| eventType === "audio" || eventType === "file"
}
StateDelegate {
Layout.maximumWidth: messageListView.width * 0.8
visible: eventType === "emote" || eventType === "state"
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: eventType === "other"
text: display
color: "grey"
font.italic: true
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: readMarker === true && index !== 0
text: "And Now"
color: "white"
verticalAlignment: Text.AlignVCenter
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
background: Rectangle {
color: MSettings.darkTheme ? "#484848" : "grey"
}
}
}
RoundButton {
width: 64
height: 64
id: goTopFab
visible: !(parent.atYEnd || messageListView.moving)
anchors.right: parent.right
anchors.bottom: parent.bottom
contentItem: MaterialIcon {
anchors.fill: parent
icon: "\ue313"
color: "white"
}
Material.background: Material.accent
}
MessageContextMenu {
id: messageContextMenu
}
Popup {
property string sourceText
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 480
id: sourceDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ScrollView {
TextArea {
readOnly: true
selectByMouse: true
text: sourceDialog.sourceText
}
}
}
Popup {
property alias listModel: readMarkerListView.model
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 320
id: readMarkerDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ListView {
implicitHeight: Math.min(window.height - 64,
readMarkerListView.contentHeight)
id: readMarkerListView
clip: true
boundsBehavior: Flickable.DragOverBounds
delegate: ItemDelegate {
width: parent.width
height: 48
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
ImageItem {
Layout.preferredWidth: height
Layout.fillHeight: true
image: modelData.avatar
hint: modelData.displayName
}
Label {
Layout.fillWidth: true
text: modelData.displayName
}
}
}
ScrollBar.vertical: ScrollBar {}
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 40
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.leftMargin: 16
Layout.rightMargin: 16
color: Material.background
Rectangle {
anchors.verticalCenter: parent.top
width: parent.width
height: 48
color: MSettings.darkTheme ? "#303030" : "#fafafa"
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 2
}
RowLayout {
anchors.fill: parent
spacing: 0
ItemDelegate {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
id: uploadButton
contentItem: MaterialIcon {
icon: "\ue226"
}
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 48
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
clip: true
AutoTextArea {
id: inputField
}
}
ItemDelegate {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
id: emojiButton
contentItem: MaterialIcon {
icon: "\ue24e"
}
EmojiPicker {
x: window.width - 370
y: window.height - 400
width: 360
height: 320
id: emojiPicker
parent: ApplicationWindow.overlay
Material.elevation: 2
textArea: inputField
}
}
}
}
}
}
}
/*##^## Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -0,0 +1,3 @@
module Spectral.Panel
RoomPanel 2.0 RoomPanel.qml
RoomListPanel 2.0 RoomListPanel.qml

24
res.qrc
View File

@ -7,8 +7,6 @@
<file>imports/Spectral/Component/Emoji/EmojiButton.qml</file>
<file>imports/Spectral/Component/Emoji/EmojiPicker.qml</file>
<file>imports/Spectral/Component/Emoji/qmldir</file>
<file>imports/Spectral/Component/Timeline/AutoImage.qml</file>
<file>imports/Spectral/Component/Timeline/AutoLabel.qml</file>
<file>imports/Spectral/Component/Timeline/DownloadableContent.qml</file>
<file>imports/Spectral/Component/Timeline/GenericBubble.qml</file>
<file>imports/Spectral/Component/Timeline/MessageDelegate.qml</file>
@ -20,17 +18,12 @@
<file>imports/Spectral/Component/SideNavButton.qml</file>
<file>imports/Spectral/Effect/ElevationEffect.qml</file>
<file>imports/Spectral/Effect/qmldir</file>
<file>imports/Spectral/Form/qmldir</file>
<file>imports/Spectral/Form/RoomDrawer.qml</file>
<file>imports/Spectral/Form/RoomForm.qml</file>
<file>imports/Spectral/Form/RoomListForm.qml</file>
<file>imports/Spectral/Menu/MessageContextMenu.qml</file>
<file>imports/Spectral/Menu/qmldir</file>
<file>imports/Spectral/Menu/RoomContextMenu.qml</file>
<file>imports/Spectral/Page/Login.qml</file>
<file>imports/Spectral/Page/qmldir</file>
<file>imports/Spectral/Page/Room.qml</file>
<file>imports/Spectral/Page/Setting.qml</file>
<file>assets/font/material.ttf</file>
<file>assets/img/avatar.png</file>
<file>assets/img/background.jpg</file>
@ -41,5 +34,22 @@
<file>imports/Spectral/Font/MaterialFont.qml</file>
<file>imports/Spectral/Font/qmldir</file>
<file>imports/Spectral/Setting/qmldir</file>
<file>imports/Spectral/Page/Setting.qml</file>
<file>imports/Spectral/Page/SettingForm.ui.qml</file>
<file>imports/Spectral/Page/SettingCategoryDelegate.qml</file>
<file>imports/Spectral/Page/SettingAccountDelegate.qml</file>
<file>imports/Spectral/Page/RoomForm.ui.qml</file>
<file>imports/Spectral/Page/LoginForm.ui.qml</file>
<file>imports/Spectral/Panel/qmldir</file>
<file>imports/Spectral/Panel/RoomDrawer.qml</file>
<file>imports/Spectral/Panel/RoomListPanel.qml</file>
<file>imports/Spectral/Panel/RoomListPanelForm.ui.qml</file>
<file>imports/Spectral/Panel/RoomPanel.qml</file>
<file>imports/Spectral/Panel/RoomPanelForm.ui.qml</file>
<file>imports/Spectral/Panel/RoomHeader.qml</file>
<file>imports/Spectral/Panel/RoomListDelegate.qml</file>
<file>imports/Spectral/Component/AutoImage.qml</file>
<file>imports/Spectral/Component/AutoLabel.qml</file>
<file>imports/Spectral/Component/AutoTextArea.qml</file>
</qresource>
</RCC>

View File

@ -9,6 +9,7 @@
#include <QFileDialog>
#include <QMimeDatabase>
#include <QMetaObject>
SpectralRoom::SpectralRoom(Connection* connection, QString roomId,
JoinState joinState)
@ -180,5 +181,8 @@ void SpectralRoom::saveViewport(int topIndex, int bottomIndex) {
void SpectralRoom::getPreviousContent(int limit) {
setBusy(true);
Room::getPreviousContent(limit);
QMetaObject::invokeMethod(
this,
[=] { Room::getPreviousContent(limit); },
Qt::QueuedConnection);
}