Chatbar Refactor

This is mostly a stylistic rework of the chatbar but there are some buxfixes / improvements in here as well. The aim was to make the chatbar so it's size was managed in the same manner as the timeline in both bubble and compactmode in the same manner as network/neochat!476.

The other features are:
- Replies, attachments and edits now look like they are inside the chatbar and use a similar styling to edits in message bubbles
- Replies and edits now part of the message so they arte part of the ScrollView and will scroll away when the text is long
- ~~The emoji picker is now a popup so it doesn't mess with the timeline layout when activated~~ (done in network/neochat!697)
- ~~Emoji dialog is now no longer required as the picker itself is a popup now~~ (no longer the case see above)
- The scrollbar now sits on the right of the chatbar actions rather than weirdly to the left
- The action icons will always stay in the same place even as the chatbar gets taller


Updated\
![2022-12-10_14-13-41.mkv](/uploads/ccbe14843834a19bf98ef0028e023eae/2022-12-10_14-13-41.mkv)

Scrollbar behaviour before
![chatbar_refactor_scrollbar_before](/uploads/2f3b91a79eb302ccf83dd35e51004e6a/chatbar_refactor_scrollbar_before.png)

Scrollbar behaviour after
![chatbar_refactor_scrollbar_after](/uploads/fcab044d8a4338ed9bcff6721b65e89c/chatbar_refactor_scrollbar_after.png)
This commit is contained in:
James Graham
2023-01-24 18:02:19 +00:00
parent 826760a55c
commit eee93e0f1f
5 changed files with 498 additions and 540 deletions

View File

@@ -10,188 +10,115 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
Loader { ColumnLayout {
id: attachmentPaneLoader id: root
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPaneLoader.attachmentPath) signal attachmentCancelled()
property string attachmentPath
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix) readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
readonly property string attachmentPath: currentRoom.chatBoxAttachmentPath
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length) readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
active: visible RowLayout {
sourceComponent: Component { spacing: Kirigami.Units.smallSpacing
QQC2.Pane {
id: attachmentPane
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Item { QQC2.Label {
property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing Layout.fillWidth: true
implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth) Layout.alignment: Qt.AlignLeft
implicitHeight: Math.max( text: i18n("Attachment:")
(hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0) horizontalAlignment: Text.AlignLeft
+ fileInfoLayout.implicitHeight, verticalAlignment: Text.AlignVCenter
toolBar.implicitHeight }
) QQC2.ToolButton {
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Image { Component {
id: image id: imageEditorPage
property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8) ImageEditorPage {
height: preferredHeight imagePath: root.attachmentPath
anchors {
horizontalCenter: parent.horizontalCenter
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
width: Math.min(implicitWidth, attachmentPane.availableWidth)
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
smooth: height === preferredHeight && parent.height === parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? attachmentPaneLoader.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
onSourceChanged: {
// Reset source size height, which affect implicitHeight
sourceSize.height = -1
}
onSourceSizeChanged: {
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
// This can save a lot of RAM when loading large images.
// It also improves visual quality for large images.
sourceSize.height = Kirigami.Units.gridUnit * 8
}
}
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
QQC2.BusyIndicator {
id: imageBusyIndicator
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: fileInfoLayout.top
bottomMargin: parent.spacing
}
visible: running
running: image.visible && image.progress < 1
}
RowLayout {
id: fileInfoLayout
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: undefined
anchors.bottom: parent.bottom
spacing: parent.spacing
Kirigami.Icon {
id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight)
implicitWidth: implicitHeight
source: attachmentMimetype.iconName
}
QQC2.Label {
id: fileLabel
text: baseFileName
}
states: State {
when: !hasImage
AnchorChanges {
target: fileInfoLayout
anchors.bottom: undefined
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Using a toolbar to get a button spacing consistent with what the QQC2 style normally has
// Also has some accessibility info
QQC2.ToolBar {
id: toolBar
width: parent.width
anchors.top: parent.top
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
Kirigami.Theme.inherit: true
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: RowLayout {
spacing: parent.spacing
QQC2.Label {
Layout.leftMargin: -attachmentPane.leftPadding
Layout.topMargin: -attachmentPane.topPadding
leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding
rightPadding: cancelAttachmentButton.rightPadding + 1
topPadding: cancelAttachmentButton.topPadding + attachmentPane.topPadding
bottomPadding: cancelAttachmentButton.bottomPadding
text: i18n("Attachment:")
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
background: Kirigami.ShadowedRectangle {
property real cornerRadius: cancelAttachmentButton.background.hasOwnProperty("radius") ?
Math.min(cancelAttachmentButton.background.radius, height/2) : 0
corners.bottomLeftRadius: toolBar.mirrored ? cornerRadius : 0
corners.bottomRightRadius: toolBar.mirrored ? 0 : cornerRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.75
}
}
Item {
Layout.fillWidth: true
}
QQC2.ToolButton {
id: editImageButton
visible: hasImage
icon.name: "document-edit"
text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: attachmentPaneLoader.attachmentPath
}
}
onClicked: {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
attachmentPaneLoader.attachmentPath = newPath;
});
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
id: cancelAttachmentButton
icon.name: "dialog-close"
text: i18n("Cancel sending Image")
display: QQC2.AbstractButton.IconOnly
onClicked: currentRoom.chatBoxAttachmentPath = "";
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
background: null
} }
} }
background: Rectangle { onClicked: {
color: Kirigami.Theme.backgroundColor let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
root.attachmentPath = newPath;
});
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
id: cancelAttachmentButton
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18n("Cancel sending attachment")
icon.name: "dialog-close"
onTriggered: attachmentCancelled();
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
Image {
id: image
Layout.alignment: Qt.AlignHCenter
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
source: hasImage ? root.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
onSourceChanged: {
// Reset source size height, which affect implicitHeight
sourceSize.height = -1
}
onSourceSizeChanged: {
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
// This can save a lot of RAM when loading large images.
// It also improves visual quality for large images.
sourceSize.height = Kirigami.Units.gridUnit * 8
}
}
Behavior on height {
NumberAnimation {
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
} }
} }
} }
QQC2.BusyIndicator {
id: imageBusyIndicator
visible: running
running: image.visible && image.progress < 1
}
RowLayout {
id: fileInfoLayout
Layout.alignment: Qt.AlignHCenter
spacing: parent.spacing
Kirigami.Icon {
id: mimetypeIcon
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: attachmentMimetype.iconName
}
QQC2.Label {
id: fileLabel
text: baseFileName
}
}
} }

View File

@@ -5,205 +5,62 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Window 2.15
import org.kde.kirigami 2.18 as Kirigami import org.kde.kirigami 2.18 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
QQC2.ToolBar { QQC2.Control {
id: chatBar id: root
property alias inputFieldText: inputField.text
property alias textField: inputField property alias textField: textField
property alias cursorPosition: inputField.cursorPosition property bool isReplying: currentRoom.chatBoxReplyId.length > 0
property bool isEditing: currentRoom.chatBoxEditId.length > 0
property bool replyPaneVisible: isReplying || isEditing
property NeoChatUser replyUser: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0
signal inputFieldForceActiveFocusTriggered()
signal messageSent() signal messageSent()
onInputFieldForceActiveFocusTriggered: { property list<Kirigami.Action> actions : [
inputField.forceActiveFocus(); Kirigami.Action {
// set the cursor to the end of the text id: attachmentAction
inputField.cursorPosition = inputField.length;
}
position: QQC2.ToolBar.Footer property bool isBusy: currentRoom && currentRoom.hasFileUploading
Kirigami.Theme.colorSet: Kirigami.Theme.View // Matrix does not allow sending attachments in replies
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
displayHint: Kirigami.DisplayHint.IconOnly
// Using a custom background because some styles like Material onTriggered: {
// or Fusion might have ugly colors for a TextArea placed inside if (Clipboard.hasImage) {
// of a toolbar. ToolBar is otherwise the closest QQC2 component attachDialog.open()
// to what we want because of the padding and spacing values. } else {
background: Rectangle { var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
color: Kirigami.Theme.backgroundColor fileDialog.chosen.connect((path) => {
} if (!path) {
return;
contentItem: RowLayout {
spacing: chatBar.spacing
QQC2.ScrollView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: inputField.implicitHeight
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
+ inputField.topPadding + inputField.bottomPadding
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
FontMetrics {
id: fontMetrics
font: inputField.font
}
QQC2.TextArea {
id: inputField
focus: true
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
* Make sure there is no background since we are using the ToolBar background.
*
* This could cause a problem if the QQC2 style was designed around TextArea
* background colors being very different from the QPalette::Base color.
* Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles.
*/
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
topPadding: 0
bottomPadding: 0
placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
horizontalAlignment: TextEdit.AlignLeft
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
hoverEnabled: !Kirigami.Settings.tabletMode
selectByMouse: !Kirigami.Settings.tabletMode
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
} }
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) { currentRoom.chatBoxAttachmentPath = path;
let editEvent = messageEventModel.getLastLocalUserMessageEventId() })
if (editEvent) { fileDialog.open()
currentRoom.chatBoxEditId = editEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace && inputField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
}
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
currentRoom.chatBoxText = text
} }
} }
}
Item { tooltip: text
visible: currentRoom.chatBoxReplyId.length === 0 && (currentRoom.chatBoxAttachmentPath.length === 0 || uploadingBusySpinner.running) },
implicitWidth: uploadButton.implicitWidth Kirigami.Action {
implicitHeight: uploadButton.implicitHeight id: emojiAction
QQC2.ToolButton {
id: uploadButton
anchors.fill: parent
// Matrix does not allow sending attachments in replies
visible: currentRoom.chatBoxReplyId.length === 0 && currentRoom.chatBoxAttachmentPath.length === 0 && !uploadingBusySpinner.running
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly
onClicked: { property bool isBusy: false
if (Clipboard.hasImage) {
attachDialog.open()
} else {
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) {
return;
}
currentRoom.chatBoxAttachmentPath = path;
})
fileDialog.open()
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.BusyIndicator {
id: uploadingBusySpinner
anchors.fill: parent
visible: running
running: currentRoom && currentRoom.hasFileUploading
}
}
QQC2.ToolButton {
id: emojiButton
icon.name: "smiley" icon.name: "smiley"
text: i18n("Add an Emoji") text: i18n("Add an Emoji")
display: QQC2.AbstractButton.IconOnly displayHint: Kirigami.DisplayHint.IconOnly
checkable: true checkable: true
onClicked: { onTriggered: {
if (emojiDialog.visible) { if (emojiDialog.visible) {
emojiDialog.close() emojiDialog.close()
} else { } else {
@@ -211,29 +68,266 @@ QQC2.ToolBar {
} }
} }
QQC2.ToolTip.text: text tooltip: text
QQC2.ToolTip.visible: hovered },
} Kirigami.Action {
id: sendAction
property bool isBusy: false
QQC2.ToolButton {
id: sendButton
icon.name: "document-send" icon.name: "document-send"
text: i18n("Send message") text: i18n("Send message")
display: QQC2.AbstractButton.IconOnly displayHint: Kirigami.DisplayHint.IconOnly
checkable: true
onClicked: { onTriggered: {
chatBar.postMessage() root.postMessage()
} }
QQC2.ToolTip.text: text tooltip: text
QQC2.ToolTip.visible: hovered }
]
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: QQC2.ScrollView {
id: chatBarScrollView
property var textFieldHeight: textField.height
property var visualLeftPadding: (root.width - chatBoxMaxWidth) / 2 - (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing : 0)
property var visualRightPadding: (root.width - chatBoxMaxWidth) / 2 + (root.width > chatBoxMaxWidth ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0)
leftPadding: LayoutMirroring.enabled ? visualRightPadding : visualLeftPadding
rightPadding: LayoutMirroring.enabled ? visualLeftPadding : visualRightPadding
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
Behavior on implicitHeight {
NumberAnimation {
id: chatBarHeightAnimation
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutCubic
}
}
QQC2.TextArea{
id: textField
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing)
rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width
placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported)
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
currentRoom.chatBoxText = text
}
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
let editEvent = messageEventModel.getLastLocalUserMessageEventId()
if (editEvent) {
currentRoom.chatBoxEditId = editEvent["event_id"]
}
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
}
Loader {
id: paneLoader
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing
anchors.right: parent.right
anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
active: visible
visible: root.replyPaneVisible || root.attachmentPaneVisible
sourceComponent: root.replyPaneVisible ? replyPane : attachmentPane
}
Component {
id: replyPane
ReplyPane {
userName: root.replyUser ? root.replyUser.displayName : ""
userColor: root.replyUser ? root.replyUser.color : ""
userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : ""
isReply: root.isReplying
text: isEditing ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: currentRoom.chatBoxAttachmentPath
onAttachmentCancelled: {
currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus()
}
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
}
/**
* Because of the paneLoader we have to manage the scroll
* position manually or it doesn't keep the cursor visible properly all the time.
*/
function ensureVisible(r) {
// Find the child that is the Flickable created by ScrollView.
let flickable = undefined;
for (var index in children) {
if (children[index] instanceof Flickable) {
flickable = children[index];
}
}
if (flickable) {
if (flickable.contentX >= r.x) {
flickable.contentX = r.x;
} else if (flickable.contentX + width <= r.x + r.width) {
flickable.contentX = r.x + r.width - width;
} if (flickable.contentY >= r.y) {
flickable.contentY = r.y;
} else if (flickable.contentY + height <= r.y + r.height) {
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
}
}
}
}
QQC2.ToolButton {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
visible: root.replyPaneVisible
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus()
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
Row {
id: actionsRow
padding: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
anchors.right: parent.right
property var requiredMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: Kirigami.Units.largeSpacing - 2
Repeater {
model: root.actions
Kirigami.Icon {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
active: actionArea.containsPress
visible: modelData.visible
enabled: modelData.enabled
MouseArea {
id: actionArea
anchors.fill: parent
onClicked: modelData.trigger()
cursorShape: Qt.PointingHandCursor
}
QQC2.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered
QQC2.ToolTip.text: modelData.tooltip
HoverHandler { id: hoverHandler }
QQC2.BusyIndicator {
anchors.fill: parent
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
visible: running
running: modelData.isBusy
}
}
} }
} }
EmojiDialog { EmojiDialog {
id: emojiDialog id: emojiDialog
x: parent.width - implicitWidth x: parent.width - implicitWidth
y: -implicitHeight - Kirigami.Units.smallSpacing y: -implicitHeight // - Kirigami.Units.smallSpacing
modal: false modal: false
includeCustom: true includeCustom: true
@@ -243,6 +337,10 @@ QQC2.ToolBar {
onClosed: if (emojiButton.checked) emojiButton.checked = false onClosed: if (emojiButton.checked) emojiButton.checked = false
} }
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
CompletionMenu { CompletionMenu {
id: completionMenu id: completionMenu
height: implicitHeight height: implicitHeight
@@ -262,22 +360,34 @@ QQC2.ToolBar {
target: currentRoom target: currentRoom
function onChatBoxEditIdChanged() { function onChatBoxEditIdChanged() {
if (currentRoom.chatBoxEditMessage.length > 0) { if (currentRoom.chatBoxEditMessage.length > 0) {
chatBar.inputFieldText = currentRoom.chatBoxEditMessage textField.text = currentRoom.chatBoxEditMessage
} }
} }
} }
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
document: inputField.textDocument document: textField.textDocument
cursorPosition: inputField.cursorPosition cursorPosition: textField.cursorPosition
selectionStart: inputField.selectionStart selectionStart: textField.selectionStart
selectionEnd: inputField.selectionEnd selectionEnd: textField.selectionEnd
Component.onCompleted: { Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler; RoomManager.chatDocumentHandler = documentHandler;
} }
} }
function forceActiveFocus() {
textField.forceActiveFocus();
// set the cursor to the end of the text
textField.cursorPosition = textField.length;
}
function insertText(text) {
let initialCursorPosition = textField.cursorPosition;
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
textField.cursorPosition = initialCursorPosition + text.length
}
function pasteImage() { function pasteImage() {
let localPath = Clipboard.saveImage(); let localPath = Clipboard.saveImage();
@@ -291,7 +401,7 @@ QQC2.ToolBar {
actionsHandler.handleMessage(); actionsHandler.handleMessage();
repeatTimer.stop() repeatTimer.stop()
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
inputField.clear(); textField.clear();
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
messageSent() messageSent()

View File

@@ -5,108 +5,52 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
ColumnLayout { ColumnLayout {
id: chatBox id: chatBox
property alias inputFieldText: chatBar.inputFieldText
signal messageSent() signal messageSent()
property alias chatBar: chatBar
readonly property int extraWidth: width >= Kirigami.Units.gridUnit * 47 ? Math.min((width - Kirigami.Units.gridUnit * 47), Kirigami.Units.gridUnit * 20) : 0
readonly property int chatBoxMaxWidth: Config.compactLayout ? width : Math.min(width, Kirigami.Units.gridUnit * 39 + extraWidth)
spacing: 0 spacing: 0
Kirigami.Separator { Kirigami.InlineMessage {
id: connectionPaneSeparator
visible: connectionPane.visible
Layout.fillWidth: true Layout.fillWidth: true
} Layout.leftMargin: 1 // So we can see the border
Layout.rightMargin: 1 // So we can see the border
QQC2.Pane { text: i18n("NeoChat is offline. Please check your network connection.")
id: connectionPane
padding: fontMetrics.lineSpacing * 0.25
FontMetrics {
id: fontMetrics
font: networkLabel.font
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
visible: !Controller.isOnline visible: !Controller.isOnline
Layout.fillWidth: true
QQC2.Label {
id: networkLabel
width: parent.width
wrapMode: Text.Wrap
text: i18n("NeoChat is offline. Please check your network connection.")
}
} }
Kirigami.Separator { Kirigami.Separator {
id: replySeparator
visible: replyPane.visible
Layout.fillWidth: true
}
ReplyPane {
id: replyPane
visible: currentRoom.chatBoxReplyId.length > 0 || currentRoom.chatBoxEditId.length > 0
Layout.fillWidth: true
onReplyCancelled: {
chatBox.focusInputField()
}
}
Kirigami.Separator {
id: attachmentSeparator
visible: attachmentPane.visible
Layout.fillWidth: true
}
AttachmentPane {
id: attachmentPane
visible: currentRoom.chatBoxAttachmentPath.length > 0
Layout.fillWidth: true
}
Kirigami.Separator {
id: chatBarSeparator
visible: chatBar.visible
Layout.fillWidth: true Layout.fillWidth: true
} }
ChatBar { ChatBar {
id: chatBar id: chatBar
visible: currentRoom.canSendEvent("m.room.message") visible: currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: implicitHeight + Kirigami.Units.largeSpacing
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding
FontMetrics {
id: chatBarFontMetrics
font: chatBar.textField.font
}
onMessageSent: { onMessageSent: {
chatBox.messageSent(); chatBox.messageSent();
} }
Behavior on implicitHeight {
NumberAnimation {
property: "implicitHeight"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
function insertText(str) {
let index = chatBar.cursorPosition;
chatBox.inputFieldText = inputFieldText.substr(0, chatBar.cursorPosition) + str + inputFieldText.substr(chatBar.cursorPosition);
chatBar.cursorPosition = index + str.length;
}
function focusInputField() {
chatBar.inputFieldForceActiveFocusTriggered()
} }
} }

View File

@@ -10,108 +10,83 @@ import org.kde.kirigami 2.14 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
Loader { GridLayout {
id: replyPane id: root
property NeoChatUser user: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser property string userName
property color userColor: Kirigami.Theme.highlightColor
property var userAvatar: ""
property bool isReply
property var text
signal replyCancelled() rows: 3
columns: 3
rowSpacing: Kirigami.Units.smallSpacing
columnSpacing: Kirigami.Units.largeSpacing
active: visible QQC2.Label {
sourceComponent: QQC2.Pane { id: replyLabel
id: replyPane Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 3
topPadding: Kirigami.Units.smallSpacing
Kirigami.Theme.colorSet: Kirigami.Theme.View text: isReply ? i18n("Replying to:") : i18n("Editing message:")
}
Rectangle {
id: verticalBorder
spacing: leftPadding Layout.fillHeight: true
Layout.rowSpan: 2
contentItem: RowLayout { implicitWidth: Kirigami.Units.smallSpacing
Layout.fillWidth: true color: userColor
spacing: replyPane.spacing }
Kirigami.Avatar {
id: replyAvatar
FontMetrics { implicitWidth: Kirigami.Units.iconSizes.small
id: fontMetrics implicitHeight: Kirigami.Units.iconSizes.small
font: textArea.font
}
Kirigami.Avatar { source: userAvatar
id: avatar name: userName
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter color: userColor
Layout.preferredWidth: Layout.preferredHeight }
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading QQC2.Label {
source: user ? "image://mxc/" + currentRoom.getUser(user.id).avatarMediaId : "" Layout.fillWidth: true
name: user ? user.displayName : "" Layout.alignment: Qt.AlignLeft
color: user ? user.color : "transparent"
visible: Boolean(user)
}
ColumnLayout { color: userColor
id: textContentLayout text: userName
Layout.alignment: Qt.AlignCenter elide: Text.ElideRight
Layout.fillWidth: true }
spacing: fontMetrics.leading QQC2.TextArea {
QQC2.Label { id: textArea
Layout.fillWidth: true
textFormat: Text.StyledText
elide: Text.ElideRight
text: {
let heading = "<b>%1</b>"
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
if (currentRoom.chatBoxEditId.length > 0) {
heading = heading.arg(i18n("Editing message:")) + "<br/>"
} else {
heading = heading.arg(i18n("Replying to %1:", userName))
}
return heading Layout.fillWidth: true
} Layout.columnSpan: 2
}
//TODO edit user mentions
QQC2.ScrollView {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) leftPadding: 0
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff rightPadding: 0
topPadding: 0
QQC2.TextArea { bottomPadding: 0
id: textArea text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
leftPadding: 0 selectByMouse: true
rightPadding: 0 selectByKeyboard: true
topPadding: 0 readOnly: true
bottomPadding: 0 wrapMode: QQC2.Label.Wrap
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + (currentRoom.chatBoxEditId.length > 0 ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage) textFormat: TextEdit.RichText
selectByMouse: true background: Item {}
selectByKeyboard: true HoverHandler {
readOnly: true cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
wrapMode: QQC2.Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
} }
background: Rectangle { TextMetrics {
color: Kirigami.Theme.backgroundColor id: replyTextMetrics
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
} }
} }
} }

View File

@@ -50,6 +50,7 @@ Kirigami.ScrollablePage {
applicationWindow().hoverLinkIndicator.text = ""; applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning(); messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false; hasScrolledUpBefore = false;
chatBox.chatBar.forceActiveFocus();
} }
Connections { Connections {
@@ -137,8 +138,8 @@ Kirigami.ScrollablePage {
switchRoomUp(); switchRoomUp();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) { } else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true; event.accepted = true;
chatBox.addText(event.text); chatBox.chatBar.insertText(event.text);
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
return; return;
} }
} }
@@ -349,7 +350,7 @@ Kirigami.ScrollablePage {
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
messageListView.goToEvent(currentRoom.readMarkerEventId) messageListView.goToEvent(currentRoom.readMarkerEventId)
} }
icon.name: "go-up" icon.name: "go-up"
@@ -373,7 +374,7 @@ Kirigami.ScrollablePage {
visible: !messageListView.atYEnd visible: !messageListView.atYEnd
action: Kirigami.Action { action: Kirigami.Action {
onTriggered: { onTriggered: {
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
goToLastMessage(); goToLastMessage();
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
} }
@@ -519,13 +520,14 @@ Kirigami.ScrollablePage {
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
icon.name: "preferences-desktop-emoticons" icon.name: "preferences-desktop-emoticons"
onClicked: emojiDialog.open(); onClicked: emojiDialog.open();
EmojiDialog { EmojiDialog {
id: emojiDialog id: emojiDialog
showQuickReaction: true showQuickReaction: true
onChosen: { onChosen: {
page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji); page.currentRoom.toggleReaction(hoverActions.event.eventId, emoji);
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
} }
@@ -538,7 +540,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxEditId = hoverActions.event.eventId;
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
QQC2.Button { QQC2.Button {
@@ -549,7 +551,7 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxReplyId = hoverActions.event.eventId; currentRoom.chatBoxReplyId = hoverActions.event.eventId;
currentRoom.chatBoxEditId = ""; currentRoom.chatBoxEditId = "";
chatBox.focusInputField(); chatBox.chatBar.forceActiveFocus();
} }
} }
} }