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

@@ -1,434 +1,141 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0
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 org.kde.kirigami 2.4 as Kirigami
import SortFilterProxyModel 0.2
Item {
property var connection: null
readonly property var user: connection ? connection.localUser : null
import Spectral.Component 2.0
import Spectral 0.1
property int filter: 0
property var enteredRoom: null
Kirigami.ScrollablePage {
property var roomListModel
property var enteredRoom
signal enterRoom(var room)
signal leaveRoom(var room)
id: root
RoomListModel {
id: roomListModel
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"
}
}
title: "Spectral"
actions {
main: Kirigami.Action {
iconName: "document-edit"
}
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
}
]
contextualActions: []
}
Shortcut {
sequence: "Ctrl+F"
onActivated: searchField.forceActiveFocus()
}
ListView {
id: messageListView
ColumnLayout {
anchors.fill: parent
spacing: 0
model: SortFilterProxyModel {
id: sortedRoomListModel
Control {
Layout.fillWidth: true
Layout.preferredHeight: 64
sourceModel: roomListModel
id: roomListHeader
topPadding: 12
bottomPadding: 12
leftPadding: 12
rightPadding: 18
contentItem: RowLayout {
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: !searchField.active
contentItem: MaterialIcon {
icon: "\ue8b6"
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"
}
}
}
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: searchField.active
contentItem: MaterialIcon { icon: "\ue5cd" }
onClicked: searchField.clear()
sorters: [
RoleSorter { roleName: "category" },
ExpressionSorter {
expression: {
return modelLeft.highlightCount > 0;
}
},
ExpressionSorter {
expression: {
return modelLeft.notificationCount > 0;
}
},
RoleSorter {
roleName: "lastActiveTime"
sortOrder: Qt.DescendingOrder
}
]
AutoTextField {
readonly property bool active: text
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
id: searchField
placeholderText: "Search..."
color: MPalette.lighter
filters: [
ExpressionFilter {
expression: joinState != "upgraded"
}
]
}
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 {
Layout.preferredWidth: height
Layout.fillHeight: true
Layout.alignment: Qt.AlignRight
visible: !searchField.active
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
source: avatar
hint: name || "No Name"
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
spacing: 12
spacing: Kirigami.Units.smallSpacing
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: avatar
hint: name || "No Name"
}
ColumnLayout {
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: name || "No Name"
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
}
text: name || "No Name"
font.pixelSize: 16
font.bold: unreadCount >= 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
visible: notificationCount > 0 && highlightCount == 0
color: MPalette.background
text: notificationCount
leftPadding: 12
rightPadding: 12
topPadding: 4
bottomPadding: 4
font.bold: true
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
background: Rectangle {
radius: height / 2
color: MPalette.lighter
}
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
font.pixelSize: 13
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"
section.criteria: ViewSection.FullString
section.delegate: Label {
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
onClicked: {
if (enteredRoom) {
leaveRoom(enteredRoom)
}
Label {
Layout.alignment: Qt.AlignHCenter
enteredRoom = currentRoom
text: "You're all caught up!"
color: MPalette.foreground
}
enterRoom(enteredRoom)
}
}
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.Controls 2.12
import QtQuick.Controls 2.12 as Controls
import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Qt.labs.qmlmodels 1.0
import Qt.labs.platform 1.0
import QtGraphicalEffects 1.0
import Spectral.Component 2.0
import Spectral.Component.Emoji 2.0
import Spectral.Component.Timeline 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0
import org.kde.kirigami 2.4 as Kirigami
import Spectral 0.1
import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
Item {
property var currentRoom: null
import Spectral.Component 2.0
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 {
id: messageEventModel
room: currentRoom
}
DropArea {
anchors.fill: parent
ListView {
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: {
if (!drop.hasUrls) return
spacing: Kirigami.Units.smallSpacing
roomPanelInput.attach(drop.urls[0])
}
}
displayMarginBeginning: 100
displayMarginEnd: 100
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
ImageClipboard {
id: imageClipboard
}
model: SortFilterProxyModel {
id: sortedMessageEventModel
Popup {
anchors.centerIn: parent
sourceModel: messageEventModel
id: attachDialog
padding: 16
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
}
filters: [
ExpressionFilter {
expression: marks !== 0x10 && eventType !== "other"
}
]
background: RippleEffect {
onClicked: {
attachDialog.close()
onModelReset: {
if (currentRoom) {
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) {
if (!path) return
populate: Transition {
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 {
Layout.preferredWidth: 1
Layout.fillHeight: true
DelegateChoice {
roleValue: "notice"
delegate: TimelineContainer {
width: page.width
color: MPalette.banner
innerObject: MessageDelegate {
width: parent.width
innerObject: TextDelegate {
Layout.fillWidth: true
}
}
}
}
Control {
Layout.preferredWidth: 160
Layout.fillHeight: true
DelegateChoice {
roleValue: "image"
delegate: TimelineContainer {
width: page.width
padding: 16
innerObject: MessageDelegate {
width: parent.width
contentItem: ColumnLayout {
spacing: 16
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
icon: "\ue410"
font.pixelSize: 64
color: MPalette.lighter
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "Clipboard image"
color: MPalette.foreground
innerObject: ImageDelegate {
Layout.fillWidth: true
Layout.preferredHeight: info.h
}
}
}
}
background: RippleEffect {
onClicked: {
var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
if (!imageClipboard.saveImage(localPath)) return
roomPanelInput.attach(localPath)
attachDialog.close()
DelegateChoice {
roleValue: "audio"
delegate: TimelineContainer {
width: page.width
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 {
id: openFileDialog
OpenFileDialog {}
}
Column {
anchors.centerIn: parent
spacing: 16
visible: !currentRoom
Image {
anchors.horizontalCenter: parent.horizontalCenter
width: 240
fillMode: Image.PreserveAspectFit
source: "qrc:/assets/img/matrix.svg"
footer: RowLayout {
Controls.ToolButton {
contentItem: Kirigami.Icon {
source: "mail-attachment"
}
}
Label {
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 {
Controls.TextField {
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 {
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
}
background: Item {}
}