mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-12-01 22:58:49 +03:00
Split up TimelineView into separate components
This commit is contained in:
parent
3a0e55e97b
commit
3d64df41da
8 changed files with 531 additions and 472 deletions
|
@ -8,7 +8,7 @@ Rectangle {
|
||||||
|
|
||||||
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
|
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
|
||||||
color: "#2ECC71"
|
color: "#2ECC71"
|
||||||
implicitHeight: rowLayout.height + 8
|
implicitHeight: visible ? rowLayout.height + 8 : 0
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: rowLayout
|
id: rowLayout
|
||||||
|
|
90
resources/qml/MessageInput.qml
Normal file
90
resources/qml/MessageInput.qml
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: colors.window
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: textInput.height
|
||||||
|
Layout.minimumHeight: 40
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: inputBar
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignBottom
|
||||||
|
hoverEnabled: true
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
image: ":/icons/icons/ui/place-call.png"
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignBottom
|
||||||
|
hoverEnabled: true
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
image: ":/icons/icons/ui/paper-clip-outline.png"
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: textInput
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignBottom
|
||||||
|
Layout.maximumHeight: Window.height / 4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
placeholderText: qsTr("Write a message...")
|
||||||
|
placeholderTextColor: colors.buttonText
|
||||||
|
color: colors.text
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
// workaround for wrong cursor shape on some platforms
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: colors.window
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||||
|
hoverEnabled: true
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
image: ":/icons/icons/ui/smile.png"
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
||||||
|
hoverEnabled: true
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
image: ":/icons/icons/ui/cursor.png"
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
202
resources/qml/MessageView.qml
Normal file
202
resources/qml/MessageView.qml
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import "./delegates"
|
||||||
|
import QtGraphicalEffects 1.0
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: chat
|
||||||
|
|
||||||
|
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
cacheBuffer: 400
|
||||||
|
model: TimelineManager.timeline
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
pixelAligned: true
|
||||||
|
spacing: 4
|
||||||
|
verticalLayoutDirection: ListView.BottomToTop
|
||||||
|
onCountChanged: {
|
||||||
|
if (atYEnd)
|
||||||
|
model.currentIndex = 0;
|
||||||
|
|
||||||
|
} // Mark last event as read, since we are at the bottom
|
||||||
|
|
||||||
|
ScrollHelper {
|
||||||
|
flickable: parent
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: StandardKey.MoveToPreviousPage
|
||||||
|
onActivated: {
|
||||||
|
chat.contentY = chat.contentY - chat.height / 2;
|
||||||
|
chat.returnToBounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: StandardKey.MoveToNextPage
|
||||||
|
onActivated: {
|
||||||
|
chat.contentY = chat.contentY + chat.height / 2;
|
||||||
|
chat.returnToBounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: StandardKey.Cancel
|
||||||
|
onActivated: chat.model.reply = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Alt+Up"
|
||||||
|
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Alt+Down"
|
||||||
|
onActivated: {
|
||||||
|
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
|
||||||
|
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
property: "section"
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: sectionHeader
|
||||||
|
|
||||||
|
Column {
|
||||||
|
property var modelData
|
||||||
|
property string section
|
||||||
|
property string nextSection
|
||||||
|
|
||||||
|
topPadding: 4
|
||||||
|
bottomPadding: 4
|
||||||
|
spacing: 8
|
||||||
|
visible: !!modelData
|
||||||
|
width: parent.width
|
||||||
|
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: dateBubble
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||||
|
visible: section.includes(" ")
|
||||||
|
text: chat.model.formatDateSeparator(modelData.timestamp)
|
||||||
|
color: colors.text
|
||||||
|
height: fontMetrics.height * 1.4
|
||||||
|
width: contentWidth * 1.2
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: parent.height / 2
|
||||||
|
color: colors.window
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
height: userName.height
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
width: avatarSize
|
||||||
|
height: avatarSize
|
||||||
|
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
|
||||||
|
displayName: modelData.userName
|
||||||
|
userid: modelData.userId
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
propagateComposedEvents: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: userName
|
||||||
|
|
||||||
|
text: TimelineManager.escapeEmoji(modelData.userName)
|
||||||
|
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
|
textFormat: Text.RichText
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
propagateComposedEvents: true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
id: scrollbar
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: wrapper
|
||||||
|
|
||||||
|
// This would normally be previousSection, but our model's order is inverted.
|
||||||
|
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
|
||||||
|
property Item section
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||||
|
width: chat.delegateMaxWidth
|
||||||
|
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||||
|
onSectionBoundaryChanged: {
|
||||||
|
if (sectionBoundary) {
|
||||||
|
var properties = {
|
||||||
|
"modelData": model.dump,
|
||||||
|
"section": ListView.section,
|
||||||
|
"nextSection": ListView.nextSection
|
||||||
|
};
|
||||||
|
section = sectionHeader.createObject(wrapper, properties);
|
||||||
|
} else {
|
||||||
|
section.destroy();
|
||||||
|
section = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineRow {
|
||||||
|
id: timelinerow
|
||||||
|
|
||||||
|
y: section ? section.y + section.height : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onMovementEnded() {
|
||||||
|
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
|
||||||
|
chat.model.currentIndex = index;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
target: chat
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: BusyIndicator {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
running: chat.model && chat.model.paginationInProgress
|
||||||
|
height: 50
|
||||||
|
width: 50
|
||||||
|
z: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
resources/qml/ReplyPopup.qml
Normal file
47
resources/qml/ReplyPopup.qml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import "./delegates/"
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: replyPopup
|
||||||
|
|
||||||
|
property var room: TimelineManager.timeline
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: room && room.reply
|
||||||
|
// Height of child, plus margins, plus border
|
||||||
|
implicitHeight: replyPreview.height + 10
|
||||||
|
color: colors.window
|
||||||
|
z: 3
|
||||||
|
|
||||||
|
Reply {
|
||||||
|
id: replyPreview
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 2 * 22 + 3 * 16
|
||||||
|
anchors.right: closeReplyButton.left
|
||||||
|
anchors.rightMargin: 2 * 22 + 3 * 16
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
modelData: room ? room.getDump(room.reply, room.id) : {
|
||||||
|
}
|
||||||
|
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
id: closeReplyButton
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 15
|
||||||
|
anchors.top: replyPreview.top
|
||||||
|
hoverEnabled: true
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
image: ":/icons/icons/ui/remove-symbol.png"
|
||||||
|
ToolTip.visible: closeReplyButton.hovered
|
||||||
|
ToolTip.text: qsTr("Close")
|
||||||
|
onClicked: room.reply = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -43,12 +43,21 @@ Page {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: userProfileComponent
|
||||||
|
|
||||||
|
UserProfile {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: messageContextMenu
|
id: messageContextMenu
|
||||||
|
|
||||||
property string eventId
|
property string eventId
|
||||||
property int eventType
|
property int eventType
|
||||||
property bool isEncrypted
|
property bool isEncrypted
|
||||||
|
property var room: TimelineManager.timeline
|
||||||
|
|
||||||
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
|
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
|
||||||
eventId = eventId_;
|
eventId = eventId_;
|
||||||
|
@ -69,12 +78,12 @@ Page {
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Reply")
|
text: qsTr("Reply")
|
||||||
onClicked: chat.model.replyAction(messageContextMenu.eventId)
|
onClicked: room.replyAction(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Read receipts")
|
text: qsTr("Read receipts")
|
||||||
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
|
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
@ -83,19 +92,19 @@ Page {
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("View raw message")
|
text: qsTr("View raw message")
|
||||||
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
|
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
visible: messageContextMenu.isEncrypted
|
visible: messageContextMenu.isEncrypted
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
text: qsTr("View decrypted raw message")
|
text: qsTr("View decrypted raw message")
|
||||||
onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
|
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Redact message")
|
text: qsTr("Redact message")
|
||||||
onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
|
onTriggered: room.redactEvent(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
@ -163,125 +172,7 @@ Page {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Rectangle {
|
TopBar {
|
||||||
id: topBar
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: topLayout.height + 16
|
|
||||||
z: 3
|
|
||||||
color: colors.window
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: TimelineManager.openRoomSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
GridLayout {
|
|
||||||
//Layout.margins: 8
|
|
||||||
|
|
||||||
id: topLayout
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 8
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
id: backToRoomsButton
|
|
||||||
|
|
||||||
Layout.column: 0
|
|
||||||
Layout.row: 0
|
|
||||||
Layout.rowSpan: 2
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
visible: TimelineManager.isNarrowView
|
|
||||||
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Back to room list")
|
|
||||||
onClicked: TimelineManager.backToRooms()
|
|
||||||
}
|
|
||||||
|
|
||||||
Avatar {
|
|
||||||
Layout.column: 1
|
|
||||||
Layout.row: 0
|
|
||||||
Layout.rowSpan: 2
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
width: avatarSize
|
|
||||||
height: avatarSize
|
|
||||||
url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
|
|
||||||
displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: TimelineManager.openRoomSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.column: 2
|
|
||||||
Layout.row: 0
|
|
||||||
color: colors.text
|
|
||||||
font.pointSize: fontMetrics.font.pointSize * 1.1
|
|
||||||
text: chat.model ? chat.model.roomName : qsTr("No room selected")
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: TimelineManager.openRoomSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.column: 2
|
|
||||||
Layout.row: 1
|
|
||||||
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
|
|
||||||
clip: true
|
|
||||||
text: chat.model ? chat.model.roomTopic : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
id: roomOptionsButton
|
|
||||||
|
|
||||||
Layout.column: 3
|
|
||||||
Layout.row: 0
|
|
||||||
Layout.rowSpan: 2
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
image: ":/icons/icons/ui/vertical-ellipsis.png"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Room options")
|
|
||||||
onClicked: roomOptionsMenu.popup(roomOptionsButton)
|
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: roomOptionsMenu
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Invite users")
|
|
||||||
onTriggered: TimelineManager.openInviteUsersDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Members")
|
|
||||||
onTriggered: TimelineManager.openMemberListDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Leave room")
|
|
||||||
onTriggered: TimelineManager.openLeaveRoomDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Settings")
|
|
||||||
onTriggered: TimelineManager.openRoomSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -296,276 +187,18 @@ Page {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
color: colors.base
|
color: colors.base
|
||||||
|
|
||||||
ListView {
|
ColumnLayout {
|
||||||
id: chat
|
|
||||||
|
|
||||||
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
|
|
||||||
|
|
||||||
cacheBuffer: 400
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
model: TimelineManager.timeline
|
spacing: 0
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
pixelAligned: true
|
|
||||||
spacing: 4
|
|
||||||
verticalLayoutDirection: ListView.BottomToTop
|
|
||||||
onCountChanged: {
|
|
||||||
if (atYEnd)
|
|
||||||
model.currentIndex = 0;
|
|
||||||
|
|
||||||
} // Mark last event as read, since we are at the bottom
|
MessageView {
|
||||||
|
Layout.fillWidth: true
|
||||||
ScrollHelper {
|
Layout.fillHeight: true
|
||||||
flickable: parent
|
|
||||||
anchors.fill: parent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortcut {
|
TypingIndicator {
|
||||||
sequence: StandardKey.MoveToPreviousPage
|
|
||||||
onActivated: {
|
|
||||||
chat.contentY = chat.contentY - chat.height / 2;
|
|
||||||
chat.returnToBounds();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
sequence: StandardKey.MoveToNextPage
|
|
||||||
onActivated: {
|
|
||||||
chat.contentY = chat.contentY + chat.height / 2;
|
|
||||||
chat.returnToBounds();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
sequence: StandardKey.Cancel
|
|
||||||
onActivated: chat.model.reply = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
sequence: "Alt+Up"
|
|
||||||
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
sequence: "Alt+Down"
|
|
||||||
onActivated: {
|
|
||||||
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
|
|
||||||
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: userProfileComponent
|
|
||||||
|
|
||||||
UserProfile {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
property: "section"
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: sectionHeader
|
|
||||||
|
|
||||||
Column {
|
|
||||||
property var modelData
|
|
||||||
property string section
|
|
||||||
property string nextSection
|
|
||||||
|
|
||||||
topPadding: 4
|
|
||||||
bottomPadding: 4
|
|
||||||
spacing: 8
|
|
||||||
visible: !!modelData
|
|
||||||
width: parent.width
|
|
||||||
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: dateBubble
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
|
||||||
visible: section.includes(" ")
|
|
||||||
text: chat.model.formatDateSeparator(modelData.timestamp)
|
|
||||||
color: colors.text
|
|
||||||
height: fontMetrics.height * 1.4
|
|
||||||
width: contentWidth * 1.2
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: parent.height / 2
|
|
||||||
color: colors.window
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
height: userName.height
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Avatar {
|
|
||||||
width: avatarSize
|
|
||||||
height: avatarSize
|
|
||||||
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
|
|
||||||
displayName: modelData.userName
|
|
||||||
userid: modelData.userId
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: userName
|
|
||||||
|
|
||||||
text: TimelineManager.escapeEmoji(modelData.userName)
|
|
||||||
color: TimelineManager.userColor(modelData.userId, colors.window)
|
|
||||||
textFormat: Text.RichText
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
id: scrollbar
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: wrapper
|
|
||||||
|
|
||||||
// This would normally be previousSection, but our model's order is inverted.
|
|
||||||
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
|
|
||||||
property Item section
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
|
||||||
width: chat.delegateMaxWidth
|
|
||||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
|
||||||
onSectionBoundaryChanged: {
|
|
||||||
if (sectionBoundary) {
|
|
||||||
var properties = {
|
|
||||||
"modelData": model.dump,
|
|
||||||
"section": ListView.section,
|
|
||||||
"nextSection": ListView.nextSection
|
|
||||||
};
|
|
||||||
section = sectionHeader.createObject(wrapper, properties);
|
|
||||||
} else {
|
|
||||||
section.destroy();
|
|
||||||
section = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineRow {
|
|
||||||
id: timelinerow
|
|
||||||
|
|
||||||
y: section ? section.y + section.height : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onMovementEnded() {
|
|
||||||
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
|
|
||||||
chat.model.currentIndex = index;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
target: chat
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
footer: BusyIndicator {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
running: chat.model && chat.model.paginationInProgress
|
|
||||||
height: 50
|
|
||||||
width: 50
|
|
||||||
z: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: typingRect
|
|
||||||
|
|
||||||
color: colors.base
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: (chat.model && chat.model.typingUsers.length > 0) ? typingDisplay.height : 0
|
|
||||||
z: 3
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: typingDisplay
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 10
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
color: colors.text
|
|
||||||
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.base) : ""
|
|
||||||
textFormat: Text.RichText
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: replyPopup
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: chat.model && chat.model.reply
|
|
||||||
// Height of child, plus margins, plus border
|
|
||||||
implicitHeight: replyPreview.height + 10
|
|
||||||
color: colors.window
|
|
||||||
z: 3
|
|
||||||
|
|
||||||
Reply {
|
|
||||||
id: replyPreview
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.right: closeReplyButton.left
|
|
||||||
anchors.rightMargin: 20
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {
|
|
||||||
}
|
|
||||||
userColor: TimelineManager.userColor(modelData.userId, colors.window)
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
id: closeReplyButton
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 15
|
|
||||||
anchors.top: replyPreview.top
|
|
||||||
hoverEnabled: true
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
image: ":/icons/icons/ui/remove-symbol.png"
|
|
||||||
ToolTip.visible: closeReplyButton.hovered
|
|
||||||
ToolTip.text: qsTr("Close")
|
|
||||||
onClicked: chat.model.reply = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -582,90 +215,10 @@ Page {
|
||||||
color: colors.mid
|
color: colors.mid
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
ReplyPopup {
|
||||||
color: colors.window
|
}
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: textInput.height
|
|
||||||
Layout.minimumHeight: 40
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: inputBar
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
hoverEnabled: true
|
|
||||||
width: 22
|
|
||||||
height: 22
|
|
||||||
image: ":/icons/icons/ui/place-call.png"
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
Layout.leftMargin: 16
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
hoverEnabled: true
|
|
||||||
width: 22
|
|
||||||
height: 22
|
|
||||||
image: ":/icons/icons/ui/paper-clip-outline.png"
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
id: textInput
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
Layout.maximumHeight: Window.height / 4
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
TextArea {
|
|
||||||
placeholderText: qsTr("Write a message...")
|
|
||||||
placeholderTextColor: colors.buttonText
|
|
||||||
color: colors.text
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
// workaround for wrong cursor shape on some platforms
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: Qt.IBeamCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: colors.window
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
|
||||||
hoverEnabled: true
|
|
||||||
width: 22
|
|
||||||
height: 22
|
|
||||||
image: ":/icons/icons/ui/smile.png"
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton {
|
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
|
|
||||||
hoverEnabled: true
|
|
||||||
width: 22
|
|
||||||
height: 22
|
|
||||||
image: ":/icons/icons/ui/cursor.png"
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 8
|
|
||||||
Layout.rightMargin: 16
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
MessageInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
127
resources/qml/TopBar.qml
Normal file
127
resources/qml/TopBar.qml
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: topBar
|
||||||
|
|
||||||
|
property var room: TimelineManager.timeline
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: topLayout.height + 16
|
||||||
|
z: 3
|
||||||
|
color: colors.window
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: TimelineManager.openRoomSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
//Layout.margins: 8
|
||||||
|
|
||||||
|
id: topLayout
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
id: backToRoomsButton
|
||||||
|
|
||||||
|
Layout.column: 0
|
||||||
|
Layout.row: 0
|
||||||
|
Layout.rowSpan: 2
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
visible: TimelineManager.isNarrowView
|
||||||
|
image: ":/icons/icons/ui/angle-pointing-to-left.png"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Back to room list")
|
||||||
|
onClicked: TimelineManager.backToRooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
Avatar {
|
||||||
|
Layout.column: 1
|
||||||
|
Layout.row: 0
|
||||||
|
Layout.rowSpan: 2
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
width: avatarSize
|
||||||
|
height: avatarSize
|
||||||
|
url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
|
||||||
|
displayName: room ? room.roomName : qsTr("No room selected")
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: TimelineManager.openRoomSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.column: 2
|
||||||
|
Layout.row: 0
|
||||||
|
color: colors.text
|
||||||
|
font.pointSize: fontMetrics.font.pointSize * 1.1
|
||||||
|
text: room ? room.roomName : qsTr("No room selected")
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: TimelineManager.openRoomSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.column: 2
|
||||||
|
Layout.row: 1
|
||||||
|
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
|
||||||
|
clip: true
|
||||||
|
text: room ? room.roomTopic : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageButton {
|
||||||
|
id: roomOptionsButton
|
||||||
|
|
||||||
|
Layout.column: 3
|
||||||
|
Layout.row: 0
|
||||||
|
Layout.rowSpan: 2
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
image: ":/icons/icons/ui/vertical-ellipsis.png"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Room options")
|
||||||
|
onClicked: roomOptionsMenu.popup(roomOptionsButton)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: roomOptionsMenu
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Invite users")
|
||||||
|
onTriggered: TimelineManager.openInviteUsersDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Members")
|
||||||
|
onTriggered: TimelineManager.openMemberListDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Leave room")
|
||||||
|
onTriggered: TimelineManager.openLeaveRoomDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Settings")
|
||||||
|
onTriggered: TimelineManager.openRoomSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
resources/qml/TypingIndicator.qml
Normal file
35
resources/qml/TypingIndicator.qml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import im.nheko 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property var room: TimelineManager.timeline
|
||||||
|
|
||||||
|
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: typingRect
|
||||||
|
|
||||||
|
visible: (room && room.typingUsers.length > 0)
|
||||||
|
color: colors.base
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 3
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: typingDisplay
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
color: colors.text
|
||||||
|
text: room ? room.formatTypingUsers(room.typingUsers, colors.base) : ""
|
||||||
|
textFormat: Text.RichText
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -121,6 +121,11 @@
|
||||||
<file>qtquickcontrols2.conf</file>
|
<file>qtquickcontrols2.conf</file>
|
||||||
|
|
||||||
<file>qml/TimelineView.qml</file>
|
<file>qml/TimelineView.qml</file>
|
||||||
|
<file>qml/TopBar.qml</file>
|
||||||
|
<file>qml/MessageView.qml</file>
|
||||||
|
<file>qml/MessageInput.qml</file>
|
||||||
|
<file>qml/TypingIndicator.qml</file>
|
||||||
|
<file>qml/ReplyPopup.qml</file>
|
||||||
<file>qml/ActiveCallBar.qml</file>
|
<file>qml/ActiveCallBar.qml</file>
|
||||||
<file>qml/Avatar.qml</file>
|
<file>qml/Avatar.qml</file>
|
||||||
<file>qml/ImageButton.qml</file>
|
<file>qml/ImageButton.qml</file>
|
||||||
|
|
Loading…
Reference in a new issue