Refactor and fix ChatBox layouting

BUG: 474616
This commit is contained in:
Tobias Fella
2023-10-15 19:30:28 +02:00
parent 0beb5df08d
commit 84cad630cd
12 changed files with 544 additions and 660 deletions

View File

@@ -173,7 +173,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/TypingPane.qml
qml/QuickSwitcher.qml
qml/HoverActions.qml
qml/ChatBox.qml
qml/ChatBar.qml
qml/AttachmentPane.qml
qml/ReplyPane.qml
@@ -290,6 +289,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
qml/AttachDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png

View File

@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
fetchEmojis();
});
});
CustomEmojiModel::fetchEmojis();
}
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const

66
src/qml/AttachDialog.qml Normal file
View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtCore
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
QQC2.Popup {
id: root
padding: 16
signal chosen(string path)
contentItem: RowLayout {
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
icon.name: 'mail-attachment'
text: i18n("Choose local file")
onClicked: {
root.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect(path => root.chosen(path))
fileDialog.open()
}
}
Kirigami.Separator {}
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
icon.name: 'insert-image'
text: i18n("Clipboard image")
onClicked: {
const path = StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + "/screenshots/" + (new Date()).getTime() + ".png"
if (!Clipboard.saveImage(path)) {
return;
}
root.chosen(path)
root.close();
}
}
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
}

View File

@@ -2,35 +2,25 @@
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import QtQuick.Window
import Qt.labs.platform as Platform
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.config
/**
* @brief The component which handles the message sending.
* @brief A component for typing and sending chat messages.
*
* The ChatBox deals with laying out the visual elements with the ChatBar handling
* the core functionality of displaying the current message composition before sending.
* This is designed to go to the bottom of the timeline and provides all the functionality
* required for the user to send messages to the room.
*
* This includes support for the following message types:
* - text
* - media (video, image, file)
* - emojis/stickers
* - location
*
* In addition, when replying, this component supports showing the message that is being
* In addition when replying this component supports showing the message that is being
* replied to.
*
* @note There is no edit functionality here this, is handled inline by the timeline
* text delegate.
*
* @sa ChatBox
* @sa ChatBar
*/
QQC2.Control {
id: root
@@ -39,17 +29,13 @@ QQC2.Control {
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
onActiveFocusChanged: textField.forceActiveFocus()
onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache
/**
* @brief The QQC2.TextArea object.
*
* @sa QQC2.TextArea
*/
property alias textField: textField
property NeoChatConnection connection
/**
* @brief The ActionsHandler object to use.
*
@@ -64,31 +50,22 @@ QQC2.Control {
* Each of these will be visualised in the ChatBar so new actions can be added
* by appending to this list.
*/
property list<Kirigami.Action> actions : [
property list<Kirigami.Action> actions: [
Kirigami.Action {
id: attachmentAction
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
// Matrix does not allow sending attachments in replies
visible: _private.chatBarCache.isReplying && _private.chatBarCache.attachmentPath.length === 0
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: {
if (Clipboard.hasImage) {
attachDialog.open()
} else {
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) {
return;
}
_private.chatBarCache.attachmentPath = path;
})
fileDialog.open()
}
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(applicationWindow().overlay)
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path)
dialog.open()
}
tooltip: text
@@ -105,10 +82,10 @@ QQC2.Control {
checkable: true
onTriggered: {
if (emojiDialog.item.visible) {
emojiDialog.item.close()
if (emojiDialog.visible) {
emojiDialog.close()
} else {
emojiDialog.item.open()
emojiDialog.open()
}
}
tooltip: text
@@ -121,7 +98,7 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
}
tooltip: text
},
@@ -136,7 +113,7 @@ QQC2.Control {
checkable: true
onTriggered: {
root.postMessage()
_private.postMessage()
}
tooltip: text
@@ -148,294 +125,308 @@ QQC2.Control {
*/
signal messageSent()
leftPadding: 0
rightPadding: 0
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
anchors.left: parent.left
anchors.right:parent.right
anchors.top: parent.top
}
}
leftPadding: rightPadding
rightPadding: (root.width - chatBarSizeHelper.currentWidth) / 2
topPadding: 0
bottomPadding: 0
contentItem: QQC2.ScrollView {
id: chatBarScrollView
property var textFieldHeight: textField.height
// 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
}
contentItem: ColumnLayout {
spacing: 0
Item { // Required to adjust for the top separator
Layout.preferredHeight: 1
Layout.fillWidth: true
}
Loader {
id: paneLoader
QQC2.TextArea {
id: textField
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
x: Math.round((root.width - chatBarSizeHelper.currentWidth) / 2) - (root.width > chatBarSizeHelper.currentWidth + Kirigami.Units.largeSpacing * 2.5 ? Kirigami.Units.largeSpacing * 1.5 : 0)
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: LayoutMirroring.enabled ? actionsRow.width : Kirigami.Units.largeSpacing
rightPadding: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : actionsRow.width + x * 2 + Kirigami.Units.largeSpacing * 2
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : _private.chatBarCache.attachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
Accessible.description: placeholderText
// opt-out of whatever spell checker a styled TextArea might come with
Kirigami.SpellCheck.enabled: false
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
root.currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
_private.chatBarCache.text = text
}
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
onSelectedTextChanged: {
if (selectedText.length > 0) {
quickFormatBar.selectionStart = selectionStart
quickFormatBar.selectionEnd = selectionEnd
quickFormatBar.open()
}
}
QuickFormatBar {
id: quickFormatBar
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd)
} else {
remove(cursorPosition, cursorPosition + 1)
}
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible) {
quickFormatBar.close()
}
}
Keys.onEnterPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
root.postMessage();
}
}
Keys.onReturnPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
root.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
event.accepted = root.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
root.currentRoom.replyLastMessage();
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
root.currentRoom.editLastMessage();
} 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) {
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible && selectedText.length > 0) {
quickFormatBar.close()
}
}
}
Keys.onShortcutOverride: event => {
// Accept the event only when there was something to cancel. Otherwise, let the event go to the RoomPage.
if (cancelButton.visible && event.key === Qt.Key_Escape) {
cancelButton.action.trigger();
event.accepted = true;
}
}
Loader {
id: paneLoader
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.right: parent.right
anchors.rightMargin: root.width > chatBarSizeHelper.currentWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
active: visible
visible: _private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0
sourceComponent: _private.chatBarCache.isReplying ? replyPane : attachmentPane
}
Component {
id: replyPane
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarSource
text: _private.chatBarCache.relationMessage
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: _private.chatBarCache.attachmentPath
onAttachmentCancelled: {
_private.chatBarCache.attachmentPath = "";
root.forceActiveFocus()
}
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
active: visible
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
sourceComponent: root.currentRoom.mainCache.replyId.length > 0 ? replyPane : attachmentPane
}
RowLayout {
QQC2.ScrollView {
id: chatBarScrollView
/**
* 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];
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
// 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
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18n("Set an attachment caption…") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
Accessible.description: placeholderText
Kirigami.SpellCheck.enabled: false
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
root.currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
_private.chatBarCache.text = text
}
onSelectedTextChanged: {
if (selectedText.length > 0) {
quickFormatBar.selectionStart = selectionStart
quickFormatBar.selectionEnd = selectionEnd
quickFormatBar.open()
}
}
QuickFormatBar {
id: quickFormatBar
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd)
} else {
remove(cursorPosition, cursorPosition + 1)
}
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible) {
quickFormatBar.close()
}
}
Keys.onEnterPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
_private.postMessage();
}
}
Keys.onReturnPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
_private.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
event.accepted = _private.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
root.currentRoom.replyLastMessage();
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
root.currentRoom.editLastMessage();
} 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) {
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible && selectedText.length > 0) {
quickFormatBar.close()
}
}
}
Keys.onShortcutOverride: event => {
if (completionMenu.visible) {
completionMenu.close()
} else if ((_private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0) && event.key === Qt.Key_Escape) {
_private.chatBarCache.attachmentPath = ""
_private.chatBarCache.replyId = ""
_private.chatBarCache.threadId = ""
event.accepted = true;
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
}
}
RowLayout {
id: actionsRow
spacing: 0
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
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;
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: modelData.tooltip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
PieProgressBar {
visible: modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
}
}
}
}
}
QQC2.ToolButton {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
visible: _private.chatBarCache.isReplying
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onTriggered: {
parentWidth: root.width
}
Component {
id: replyPane
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarSource
text: _private.chatBarCache.relationMessage
onCancel: {
_private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
}
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: _private.chatBarCache.attachmentPath
onAttachmentCancelled: {
_private.chatBarCache.attachmentPath = "";
root.forceActiveFocus()
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
RowLayout {
id: actionsRow
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
anchors.bottomMargin: Kirigami.Units.smallSpacing
spacing: 0
property var requiredMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
QtObject {
id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: modelData.tooltip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
function postMessage() {
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
repeatTimer.stop()
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
messageSent()
}
PieProgressBar {
visible: modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
function formatText(format, selectionStart, selectionEnd) {
let index = textField.cursorPosition;
/*
* There cannot be white space at the beginning or end of the string for the
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
*/
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
if (innerText.charAt(innerText.length - 1) === " ") {
let trimmedRightString = innerText.replace(/\s*$/,"");
let trimDifference = innerText.length - trimmedRightString.length;
selectionEnd -= trimDifference;
}
if (innerText.charAt(0) === " ") {
let trimmedLeftString = innerText.replace(/^\s*/,"");
let trimDifference = innerText.length - trimmedLeftString.length;
selectionStart = selectionStart + trimDifference;
}
let startText = textField.text.substr(0, selectionStart);
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
let endText = textField.text.substr(selectionEnd);
textField.text = "";
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
/*
* Put the cursor where it was when the popup was opened accounting for the
* new markup.
*
* The exception is for a hyperlink where it is placed ready to start typing
* the url.
*/
if (format.extra !== "") {
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
} else if (index == selectionStart) {
textField.cursorPosition = index;
} else {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
}
Loader {
id: emojiDialog
active: !Kirigami.Settings.isMobile
sourceComponent: EmojiDialog {
x: root.width - width
y: -implicitHeight // - Kirigami.Units.smallSpacing
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) emojiAction.checked = false
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
CompletionMenu {
id: completionMenu
height: implicitHeight
y: -height - 5
z: 1
chatDocumentHandler: documentHandler
connection: root.connection
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
function pasteImage() {
let localPath = Clipboard.saveImage();
if (localPath.length === 0) {
return false;
}
_private.chatBarCache.attachmentPath = localPath;
return true;
}
}
@@ -452,21 +443,59 @@ QQC2.Control {
}
}
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
Component {
id: openFileDialog
parentWidth: root.width
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
function forceActiveFocus() {
textField.forceActiveFocus();
// set the cursor to the end of the text
textField.cursorPosition = textField.length;
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
Component {
id: locationChooser
LocationChooser {}
}
CompletionMenu {
id: completionMenu
chatDocumentHandler: documentHandler
connection: root.connection
x: 1
y: -height
width: parent.width - 1
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
EmojiDialog {
id: emojiDialog
x: root.width - width
y: -implicitHeight
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) emojiAction.checked = false
}
function insertText(text) {
@@ -475,139 +504,4 @@ QQC2.Control {
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
textField.cursorPosition = initialCursorPosition + text.length
}
function pasteImage() {
let localPath = Clipboard.saveImage();
if (localPath.length === 0) {
return false;
}
_private.chatBarCache.attachmentPath = localPath;
return true;
}
function postMessage() {
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
repeatTimer.stop()
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
messageSent()
}
function formatText(format, selectionStart, selectionEnd) {
let index = textField.cursorPosition;
/*
* There cannot be white space at the beginning or end of the string for the
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
*/
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
if (innerText.charAt(innerText.length - 1) === " ") {
let trimmedRightString = innerText.replace(/\s*$/,"");
let trimDifference = innerText.length - trimmedRightString.length;
selectionEnd -= trimDifference;
}
if (innerText.charAt(0) === " ") {
let trimmedLeftString = innerText.replace(/^\s*/,"");
let trimDifference = innerText.length - trimmedLeftString.length;
selectionStart = selectionStart + trimDifference;
}
let startText = textField.text.substr(0, selectionStart);
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
let endText = textField.text.substr(selectionEnd);
textField.text = "";
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
/*
* Put the cursor where it was when the popup was opened accounting for the
* new markup.
*
* The exception is for a hyperlink where it is placed ready to start typing
* the url.
*/
if (format.extra !== "") {
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
} else if (index == selectionStart) {
textField.cursorPosition = index;
} else {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
Component {
id: locationChooserComponent
LocationChooser {}
}
QQC2.Popup {
anchors.centerIn: parent
id: attachDialog
padding: 16
contentItem: RowLayout {
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
icon.name: 'mail-attachment'
text: i18n("Choose local file")
onClicked: {
attachDialog.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect(function (path) {
if (!path) {
return;
}
_private.chatBarCache.attachmentPath = path;
})
fileDialog.open()
}
}
Kirigami.Separator {
}
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
icon.name: 'insert-image'
text: i18n("Clipboard image")
onClicked: {
const localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
if (!Clipboard.saveImage(localPath)) {
return;
}
_private.chatBarCache.attachmentPath = localPath;
attachDialog.close();
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
}
}
QtObject {
id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
}
}

View File

@@ -1,99 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component for typing and sending chat messages.
*
* This is designed to go to the bottom of the timeline and provides all the functionality
* required for the user to send messages to the room.
*
* This includes support for the following message types:
* - text
* - media (video, image, file)
* - emojis/stickers
* - location
*
* In addition when replying this component supports showing the message that is being
* replied to.
*
* @note The main role of this component is to layout the elements. The main functionality
* is handled by ChatBar
*
* @sa ChatBar
*/
ColumnLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
/**
* @brief The ActionsHandler object to use.
*
* This is expected to have the correct room set otherwise messages will be sent
* to the wrong room.
*/
required property ActionsHandler actionsHandler
/**
* @brief A message has been sent from the chat bar.
*/
signal messageSent()
/**
* @brief Insert the given text into the ChatBar.
*
* The text is inserted at the current cursor location.
*/
function insertText(text) {
chatBar.insertText(text)
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.Separator {
Layout.fillWidth: true
}
ChatBar {
id: chatBar
connection: root.connection
visible: root.currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true
Layout.minimumHeight: Math.max(Kirigami.Units.gridUnit * 2, Math.round(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
Layout.preferredHeight: Math.round(implicitHeight)
currentRoom: root.currentRoom
actionsHandler: root.actionsHandler
FontMetrics {
id: chatBarFontMetrics
font: chatBar.textField.font
}
onMessageSent: {
root.messageSent();
}
}
onActiveFocusChanged: chatBar.forceActiveFocus()
}

View File

@@ -15,18 +15,19 @@ import org.kde.neochat
QQC2.Popup {
id: root
width: parent.width
required property NeoChatConnection connection
required property var chatDocumentHandler
visible: completions.count > 0
onVisibleChanged: if (visible) root.open()
RoomListModel {
id: roomListModel
connection: root.connection
}
property var chatDocumentHandler
Component.onCompleted: {
chatDocumentHandler.completionModel.roomListModel = roomListModel;
}
@@ -50,40 +51,56 @@ QQC2.Popup {
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
contentItem: ListView {
id: completions
contentItem: ColumnLayout {
spacing: 0
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
anchors.fill: parent
model: root.chatDocumentHandler.completionModel
currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
delegate: Delegates.RoundedItemDelegate {
id: completionDelegate
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
required property int index
required property string displayName
required property string subtitle
required property string iconName
ListView {
id: completions
text: displayName
model: root.chatDocumentHandler.completionModel
currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
onCountChanged: currentIndex = 0
delegate: Delegates.RoundedItemDelegate {
id: completionDelegate
contentItem: RowLayout {
KirigamiComponents.Avatar {
visible: completionDelegate.iconName !== "invalid"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
name: completionDelegate.text
}
Delegates.SubtitleContentItem {
itemDelegate: completionDelegate
labelItem.textFormat: Text.PlainText
subtitle: completionDelegate.subtitle ?? ""
subtitleItem.textFormat: Text.PlainText
required property int index
required property string displayName
required property string subtitle
required property string iconName
text: displayName
contentItem: RowLayout {
KirigamiComponents.Avatar {
visible: completionDelegate.iconName !== "invalid"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
name: completionDelegate.text
}
Delegates.SubtitleContentItem {
itemDelegate: completionDelegate
labelItem.textFormat: Text.PlainText
subtitle: completionDelegate.subtitle ?? ""
subtitleItem.textFormat: Text.PlainText
}
}
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
}
}
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
}
}

View File

@@ -132,7 +132,7 @@ FormCard.FormCardPage {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
parentWindow: root.Window.window
}
}

View File

@@ -1,15 +1,14 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import Qt.labs.platform
import QtQuick.Dialogs
FileDialog {
signal chosen(string path)
id: root
title: i18n("Please choose a file")
signal chosen(string path)
onAccepted: chosen(file)
title: i18n("Select a File")
onAccepted: root.chosen(selectedFile)
}

View File

@@ -11,82 +11,88 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
GridLayout {
RowLayout {
id: root
property string userName
property color userColor: Kirigami.Theme.highlightColor
property color userColor
property url userAvatar: ""
property var text
rows: 3
columns: 3
rowSpacing: Kirigami.Units.smallSpacing
columnSpacing: Kirigami.Units.largeSpacing
signal cancel
QQC2.Label {
id: replyLabel
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 3
topPadding: Kirigami.Units.smallSpacing
text: i18n("Replying to:")
}
Rectangle {
id: verticalBorder
Layout.fillHeight: true
Layout.rowSpan: 2
implicitWidth: Kirigami.Units.smallSpacing
color: userColor
}
KirigamiComponents.Avatar {
id: replyAvatar
ColumnLayout {
RowLayout {
KirigamiComponents.Avatar {
id: replyAvatar
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
source: userAvatar
name: userName
color: userColor
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
source: userAvatar
name: userName
color: userColor
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
color: userColor
text: userName
elide: Text.ElideRight
}
QQC2.TextArea {
id: textArea
Layout.fillWidth: true
Layout.columnSpan: 2
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
selectByMouse: true
selectByKeyboard: true
readOnly: true
wrapMode: QQC2.Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
color: userColor
text: userName
elide: Text.ElideRight
}
}
QQC2.TextArea {
id: textArea
TextMetrics {
id: replyTextMetrics
Layout.fillWidth: true
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
selectByMouse: true
selectByKeyboard: true
readOnly: true
wrapMode: QQC2.Label.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TextMetrics {
id: replyTextMetrics
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
}
}
}
QQC2.ToolButton {
id: cancelButton
Layout.alignment: Qt.AlignVCenter
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
root.cancel()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}

View File

@@ -88,8 +88,8 @@ Kirigami.Page {
onCurrentRoomChanged: {
banner.visible = false;
if (!Kirigami.Settings.isMobile && chatBoxLoader.item) {
chatBoxLoader.item.forceActiveFocus();
if (!Kirigami.Settings.isMobile && chatBarLoader.item) {
chatBarLoader.item.forceActiveFocus();
}
}
@@ -123,9 +123,9 @@ Kirigami.Page {
messageEventModel: root.messageEventModel
messageFilterModel: root.messageFilterModel
actionsHandler: root.actionsHandler
onFocusChatBox: {
if (chatBoxLoader.item) {
chatBoxLoader.item.forceActiveFocus()
onFocusChatBar: {
if (chatBarLoader.item) {
chatBarLoader.item.forceActiveFocus()
}
}
connection: root.connection
@@ -157,10 +157,10 @@ Kirigami.Page {
}
footer: Loader {
id: chatBoxLoader
active: timelineViewLoader.active
sourceComponent: ChatBox {
id: chatBox
id: chatBarLoader
active: timelineViewLoader.active && root.currentRoom.canSendEvent("m.room.message") // TODO make this update in real time
sourceComponent: ChatBar {
id: chatBar
width: parent.width
currentRoom: root.currentRoom
connection: root.connection
@@ -215,8 +215,8 @@ Kirigami.Page {
Keys.onPressed: event => {
if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true;
chatBoxLoader.item.insertText(event.text);
chatBoxLoader.item.forceActiveFocus();
chatBarLoader.item.insertText(event.text);
chatBarLoader.item.forceActiveFocus();
return;
} else if (event.key === Qt.Key_PageUp) {
event.accepted = true;
@@ -228,7 +228,7 @@ Kirigami.Page {
}
Connections {
target: currentRoom
target: root.currentRoom
function onShowMessage(messageType, message) {
banner.text = message;
banner.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;

View File

@@ -55,7 +55,7 @@ QQC2.ScrollView {
/// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false;
signal focusChatBox()
signal focusChatBar()
ListView {
id: messageListView
@@ -166,7 +166,7 @@ QQC2.ScrollView {
action: Kirigami.Action {
onTriggered: {
if (!Kirigami.Settings.isMobile) {
root.focusChatBox();
root.focusChatBar();
}
messageListView.goToEvent(root.currentRoom.readMarkerEventId)
}
@@ -258,7 +258,7 @@ QQC2.ScrollView {
HoverActions {
id: hoverActions
currentRoom: root.currentRoom
onFocusChatBar: root.focusChatBox()
onFocusChatBar: root.focusChatBar()
}
onContentYChanged: {

View File

@@ -25,7 +25,7 @@ RowLayout {
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: bottomEdge ? Kirigami.Units.gridUnit * 2 - 2 : -1 // HACK: -2 here is to ensure the ChatBox and the UserInfo have the same height
Layout.minimumHeight: bottomEdge ? Kirigami.Units.gridUnit * 2 : -1
onVisibleChanged: {
if (!visible) {