Split up TimelineView into separate components

This commit is contained in:
Nicolas Werner 2020-10-26 14:57:54 +01:00
parent 3a0e55e97b
commit 3d64df41da
8 changed files with 531 additions and 472 deletions

View file

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

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

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

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

View file

@ -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,278 +187,20 @@ 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 {
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;
}
}
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 Layout.fillWidth: true
Layout.fillHeight: 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
} }
TypingIndicator {
} }
} }
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
}
} }
ActiveCallBar { ActiveCallBar {
@ -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
View 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()
}
}
}
}
}

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

View file

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