Refactor and fix ChatBox layouting
BUG: 474616
This commit is contained in:
@@ -173,7 +173,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/TypingPane.qml
|
qml/TypingPane.qml
|
||||||
qml/QuickSwitcher.qml
|
qml/QuickSwitcher.qml
|
||||||
qml/HoverActions.qml
|
qml/HoverActions.qml
|
||||||
qml/ChatBox.qml
|
|
||||||
qml/ChatBar.qml
|
qml/ChatBar.qml
|
||||||
qml/AttachmentPane.qml
|
qml/AttachmentPane.qml
|
||||||
qml/ReplyPane.qml
|
qml/ReplyPane.qml
|
||||||
@@ -290,6 +289,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/Security.qml
|
qml/Security.qml
|
||||||
qml/QrCodeMaximizeComponent.qml
|
qml/QrCodeMaximizeComponent.qml
|
||||||
qml/SelectSpacesDialog.qml
|
qml/SelectSpacesDialog.qml
|
||||||
|
qml/AttachDialog.qml
|
||||||
RESOURCES
|
RESOURCES
|
||||||
qml/confetti.png
|
qml/confetti.png
|
||||||
qml/glowdot.png
|
qml/glowdot.png
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
|||||||
fetchEmojis();
|
fetchEmojis();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
CustomEmojiModel::fetchEmojis();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||||
|
|||||||
66
src/qml/AttachDialog.qml
Normal file
66
src/qml/AttachDialog.qml
Normal 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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,35 +2,25 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Window
|
import QtQuick.Layouts
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
import org.kde.neochat.config
|
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
|
* This is designed to go to the bottom of the timeline and provides all the functionality
|
||||||
* the core functionality of displaying the current message composition before sending.
|
* required for the user to send messages to the room.
|
||||||
*
|
*
|
||||||
* This includes support for the following message types:
|
* In addition when replying this component supports showing the message that is being
|
||||||
* - text
|
|
||||||
* - media (video, image, file)
|
|
||||||
* - emojis/stickers
|
|
||||||
* - location
|
|
||||||
*
|
|
||||||
* In addition, when replying, this component supports showing the message that is being
|
|
||||||
* replied to.
|
* replied to.
|
||||||
*
|
*
|
||||||
* @note There is no edit functionality here this, is handled inline by the timeline
|
* @sa ChatBar
|
||||||
* text delegate.
|
|
||||||
*
|
|
||||||
* @sa ChatBox
|
|
||||||
*/
|
*/
|
||||||
QQC2.Control {
|
QQC2.Control {
|
||||||
id: root
|
id: root
|
||||||
@@ -39,17 +29,13 @@ QQC2.Control {
|
|||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
required property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
|
|
||||||
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
|
onActiveFocusChanged: textField.forceActiveFocus()
|
||||||
|
|
||||||
onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache
|
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.
|
* @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
|
* Each of these will be visualised in the ChatBar so new actions can be added
|
||||||
* by appending to this list.
|
* by appending to this list.
|
||||||
*/
|
*/
|
||||||
property list<Kirigami.Action> actions : [
|
property list<Kirigami.Action> actions: [
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: attachmentAction
|
id: attachmentAction
|
||||||
|
|
||||||
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||||
|
|
||||||
// Matrix does not allow sending attachments in replies
|
// 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"
|
icon.name: "mail-attachment"
|
||||||
text: i18n("Attach an image or file")
|
text: i18n("Attach an image or file")
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (Clipboard.hasImage) {
|
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(applicationWindow().overlay)
|
||||||
attachDialog.open()
|
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path)
|
||||||
} else {
|
dialog.open()
|
||||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
|
||||||
fileDialog.chosen.connect((path) => {
|
|
||||||
if (!path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_private.chatBarCache.attachmentPath = path;
|
|
||||||
})
|
|
||||||
fileDialog.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -105,10 +82,10 @@ QQC2.Control {
|
|||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (emojiDialog.item.visible) {
|
if (emojiDialog.visible) {
|
||||||
emojiDialog.item.close()
|
emojiDialog.close()
|
||||||
} else {
|
} else {
|
||||||
emojiDialog.item.open()
|
emojiDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -121,7 +98,7 @@ QQC2.Control {
|
|||||||
displayHint: QQC2.AbstractButton.IconOnly
|
displayHint: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
|
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
|
||||||
}
|
}
|
||||||
tooltip: text
|
tooltip: text
|
||||||
},
|
},
|
||||||
@@ -136,7 +113,7 @@ QQC2.Control {
|
|||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.postMessage()
|
_private.postMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -148,294 +125,308 @@ QQC2.Control {
|
|||||||
*/
|
*/
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
|
|
||||||
leftPadding: 0
|
spacing: 0
|
||||||
rightPadding: 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
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
|
||||||
contentItem: QQC2.ScrollView {
|
contentItem: ColumnLayout {
|
||||||
id: chatBarScrollView
|
spacing: 0
|
||||||
|
Item { // Required to adjust for the top separator
|
||||||
property var textFieldHeight: textField.height
|
Layout.preferredHeight: 1
|
||||||
|
Layout.fillWidth: true
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loader {
|
||||||
|
id: paneLoader
|
||||||
|
|
||||||
QQC2.TextArea {
|
Layout.fillWidth: true
|
||||||
id: textField
|
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)
|
active: visible
|
||||||
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
|
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
|
||||||
bottomPadding: Kirigami.Units.largeSpacing
|
sourceComponent: root.currentRoom.mainCache.replyId.length > 0 ? replyPane : attachmentPane
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
RowLayout {
|
||||||
|
QQC2.ScrollView {
|
||||||
|
id: chatBarScrollView
|
||||||
|
|
||||||
/**
|
Layout.fillWidth: true
|
||||||
* Because of the paneLoader we have to manage the scroll
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
||||||
* position manually or it doesn't keep the cursor visible properly all the time.
|
|
||||||
*/
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
function ensureVisible(r) {
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
// Find the child that is the Flickable created by ScrollView.
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
|
||||||
let flickable = undefined;
|
|
||||||
for (var index in children) {
|
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
||||||
if (children[index] instanceof Flickable) {
|
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||||
flickable = children[index];
|
|
||||||
|
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) {
|
Repeater {
|
||||||
if (flickable.contentX >= r.x) {
|
model: root.actions
|
||||||
flickable.contentX = r.x;
|
delegate: QQC2.ToolButton {
|
||||||
} else if (flickable.contentX + width <= r.x + r.width) {
|
Layout.alignment: Qt.AlignVCenter
|
||||||
flickable.contentX = r.x + r.width - width;
|
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||||
} if (flickable.contentY >= r.y) {
|
onClicked: modelData.trigger()
|
||||||
flickable.contentY = r.y;
|
|
||||||
} else if (flickable.contentY + height <= r.y + r.height) {
|
QQC2.ToolTip.visible: hovered
|
||||||
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
|
QQC2.ToolTip.text: modelData.tooltip
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
PieProgressBar {
|
||||||
|
visible: modelData.isBusy
|
||||||
|
progress: root.currentRoom.fileUploadingProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolButton {
|
DelegateSizeHelper {
|
||||||
id: cancelButton
|
id: chatBarSizeHelper
|
||||||
anchors.top: parent.top
|
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||||
anchors.right: parent.right
|
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||||
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)
|
startPercentWidth: 100
|
||||||
|
endPercentWidth: Config.compactLayout ? 100 : 85
|
||||||
|
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
|
||||||
|
|
||||||
visible: _private.chatBarCache.isReplying
|
parentWidth: root.width
|
||||||
display: QQC2.AbstractButton.IconOnly
|
}
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Cancel reply")
|
Component {
|
||||||
icon.name: "dialog-close"
|
id: replyPane
|
||||||
onTriggered: {
|
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.replyId = "";
|
||||||
_private.chatBarCache.attachmentPath = "";
|
_private.chatBarCache.attachmentPath = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: attachmentPane
|
||||||
|
AttachmentPane {
|
||||||
|
attachmentPath: _private.chatBarCache.attachmentPath
|
||||||
|
|
||||||
|
onAttachmentCancelled: {
|
||||||
|
_private.chatBarCache.attachmentPath = "";
|
||||||
root.forceActiveFocus()
|
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 {
|
QtObject {
|
||||||
model: root.actions
|
id: _private
|
||||||
delegate: QQC2.ToolButton {
|
property ChatBarCache chatBarCache
|
||||||
Layout.alignment: Qt.AlignVCenter
|
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
|
||||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
|
||||||
onClicked: modelData.trigger()
|
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
function postMessage() {
|
||||||
QQC2.ToolTip.text: modelData.tooltip
|
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
repeatTimer.stop()
|
||||||
|
root.currentRoom.markAllMessagesAsRead();
|
||||||
|
textField.clear();
|
||||||
|
_private.chatBarCache.replyId = "";
|
||||||
|
messageSent()
|
||||||
|
}
|
||||||
|
|
||||||
PieProgressBar {
|
function formatText(format, selectionStart, selectionEnd) {
|
||||||
visible: modelData.isBusy
|
let index = textField.cursorPosition;
|
||||||
progress: root.currentRoom.fileUploadingProgress
|
|
||||||
}
|
/*
|
||||||
|
* 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 {
|
function pasteImage() {
|
||||||
id: emojiDialog
|
let localPath = Clipboard.saveImage();
|
||||||
active: !Kirigami.Settings.isMobile
|
if (localPath.length === 0) {
|
||||||
sourceComponent: EmojiDialog {
|
return false;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
_private.chatBarCache.attachmentPath = localPath;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,21 +443,59 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateSizeHelper {
|
Component {
|
||||||
id: chatBarSizeHelper
|
id: openFileDialog
|
||||||
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
|
|
||||||
|
|
||||||
parentWidth: root.width
|
OpenFileDialog {
|
||||||
|
parentWindow: Window.window
|
||||||
|
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceActiveFocus() {
|
Component {
|
||||||
textField.forceActiveFocus();
|
id: attachDialog
|
||||||
// set the cursor to the end of the text
|
|
||||||
textField.cursorPosition = textField.length;
|
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) {
|
function insertText(text) {
|
||||||
@@ -475,139 +504,4 @@ QQC2.Control {
|
|||||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
|
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
|
||||||
textField.cursorPosition = initialCursorPosition + text.length
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -15,18 +15,19 @@ import org.kde.neochat
|
|||||||
|
|
||||||
QQC2.Popup {
|
QQC2.Popup {
|
||||||
id: root
|
id: root
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
required property NeoChatConnection connection
|
required property NeoChatConnection connection
|
||||||
|
required property var chatDocumentHandler
|
||||||
|
|
||||||
visible: completions.count > 0
|
visible: completions.count > 0
|
||||||
|
|
||||||
|
onVisibleChanged: if (visible) root.open()
|
||||||
|
|
||||||
RoomListModel {
|
RoomListModel {
|
||||||
id: roomListModel
|
id: roomListModel
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
}
|
}
|
||||||
|
|
||||||
property var chatDocumentHandler
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
chatDocumentHandler.completionModel.roomListModel = roomListModel;
|
chatDocumentHandler.completionModel.roomListModel = roomListModel;
|
||||||
}
|
}
|
||||||
@@ -50,40 +51,56 @@ QQC2.Popup {
|
|||||||
|
|
||||||
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||||
|
|
||||||
contentItem: ListView {
|
contentItem: ColumnLayout {
|
||||||
id: completions
|
spacing: 0
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
QQC2.ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: contentHeight
|
||||||
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
|
||||||
|
|
||||||
anchors.fill: parent
|
background: Rectangle {
|
||||||
model: root.chatDocumentHandler.completionModel
|
color: Kirigami.Theme.backgroundColor
|
||||||
currentIndex: 0
|
}
|
||||||
keyNavigationWraps: true
|
|
||||||
highlightMoveDuration: 100
|
|
||||||
delegate: Delegates.RoundedItemDelegate {
|
|
||||||
id: completionDelegate
|
|
||||||
|
|
||||||
required property int index
|
ListView {
|
||||||
required property string displayName
|
id: completions
|
||||||
required property string subtitle
|
|
||||||
required property string iconName
|
|
||||||
|
|
||||||
text: displayName
|
model: root.chatDocumentHandler.completionModel
|
||||||
|
currentIndex: 0
|
||||||
|
keyNavigationWraps: true
|
||||||
|
highlightMoveDuration: 100
|
||||||
|
onCountChanged: currentIndex = 0
|
||||||
|
delegate: Delegates.RoundedItemDelegate {
|
||||||
|
id: completionDelegate
|
||||||
|
|
||||||
contentItem: RowLayout {
|
required property int index
|
||||||
KirigamiComponents.Avatar {
|
required property string displayName
|
||||||
visible: completionDelegate.iconName !== "invalid"
|
required property string subtitle
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
required property string iconName
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
|
||||||
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
|
text: displayName
|
||||||
name: completionDelegate.text
|
|
||||||
}
|
contentItem: RowLayout {
|
||||||
Delegates.SubtitleContentItem {
|
KirigamiComponents.Avatar {
|
||||||
itemDelegate: completionDelegate
|
visible: completionDelegate.iconName !== "invalid"
|
||||||
labelItem.textFormat: Text.PlainText
|
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||||
subtitle: completionDelegate.subtitle ?? ""
|
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||||
subtitleItem.textFormat: Text.PlainText
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ FormCard.FormCardPage {
|
|||||||
id: openFileDialog
|
id: openFileDialog
|
||||||
|
|
||||||
OpenFileDialog {
|
OpenFileDialog {
|
||||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||||
parentWindow: root.Window.window
|
parentWindow: root.Window.window
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
FileDialog {
|
FileDialog {
|
||||||
signal chosen(string path)
|
|
||||||
|
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
title: i18n("Please choose a file")
|
signal chosen(string path)
|
||||||
|
|
||||||
onAccepted: chosen(file)
|
title: i18n("Select a File")
|
||||||
|
onAccepted: root.chosen(selectedFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,82 +11,88 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
|||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
|
||||||
GridLayout {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string userName
|
property string userName
|
||||||
property color userColor: Kirigami.Theme.highlightColor
|
property color userColor
|
||||||
property url userAvatar: ""
|
property url userAvatar: ""
|
||||||
property var text
|
property var text
|
||||||
|
|
||||||
rows: 3
|
signal cancel
|
||||||
columns: 3
|
|
||||||
rowSpacing: Kirigami.Units.smallSpacing
|
|
||||||
columnSpacing: Kirigami.Units.largeSpacing
|
|
||||||
|
|
||||||
QQC2.Label {
|
|
||||||
id: replyLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignLeft
|
|
||||||
Layout.columnSpan: 3
|
|
||||||
topPadding: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
text: i18n("Replying to:")
|
|
||||||
}
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: verticalBorder
|
id: verticalBorder
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.rowSpan: 2
|
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.smallSpacing
|
implicitWidth: Kirigami.Units.smallSpacing
|
||||||
color: userColor
|
color: userColor
|
||||||
}
|
}
|
||||||
KirigamiComponents.Avatar {
|
ColumnLayout {
|
||||||
id: replyAvatar
|
RowLayout {
|
||||||
|
KirigamiComponents.Avatar {
|
||||||
|
id: replyAvatar
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.iconSizes.small
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
implicitHeight: Kirigami.Units.iconSizes.small
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
source: userAvatar
|
source: userAvatar
|
||||||
name: userName
|
name: userName
|
||||||
color: userColor
|
color: userColor
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignLeft
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
|
||||||
color: userColor
|
color: userColor
|
||||||
text: userName
|
text: userName
|
||||||
elide: Text.ElideRight
|
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
|
|
||||||
}
|
}
|
||||||
|
QQC2.TextArea {
|
||||||
|
id: textArea
|
||||||
|
|
||||||
TextMetrics {
|
Layout.fillWidth: true
|
||||||
id: replyTextMetrics
|
|
||||||
|
|
||||||
text: root.text
|
leftPadding: 0
|
||||||
font: textArea.font
|
rightPadding: 0
|
||||||
elide: Qt.ElideRight
|
topPadding: 0
|
||||||
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ Kirigami.Page {
|
|||||||
|
|
||||||
onCurrentRoomChanged: {
|
onCurrentRoomChanged: {
|
||||||
banner.visible = false;
|
banner.visible = false;
|
||||||
if (!Kirigami.Settings.isMobile && chatBoxLoader.item) {
|
if (!Kirigami.Settings.isMobile && chatBarLoader.item) {
|
||||||
chatBoxLoader.item.forceActiveFocus();
|
chatBarLoader.item.forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +123,9 @@ Kirigami.Page {
|
|||||||
messageEventModel: root.messageEventModel
|
messageEventModel: root.messageEventModel
|
||||||
messageFilterModel: root.messageFilterModel
|
messageFilterModel: root.messageFilterModel
|
||||||
actionsHandler: root.actionsHandler
|
actionsHandler: root.actionsHandler
|
||||||
onFocusChatBox: {
|
onFocusChatBar: {
|
||||||
if (chatBoxLoader.item) {
|
if (chatBarLoader.item) {
|
||||||
chatBoxLoader.item.forceActiveFocus()
|
chatBarLoader.item.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
@@ -157,10 +157,10 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer: Loader {
|
footer: Loader {
|
||||||
id: chatBoxLoader
|
id: chatBarLoader
|
||||||
active: timelineViewLoader.active
|
active: timelineViewLoader.active && root.currentRoom.canSendEvent("m.room.message") // TODO make this update in real time
|
||||||
sourceComponent: ChatBox {
|
sourceComponent: ChatBar {
|
||||||
id: chatBox
|
id: chatBar
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
@@ -215,8 +215,8 @@ Kirigami.Page {
|
|||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
|
if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
chatBoxLoader.item.insertText(event.text);
|
chatBarLoader.item.insertText(event.text);
|
||||||
chatBoxLoader.item.forceActiveFocus();
|
chatBarLoader.item.forceActiveFocus();
|
||||||
return;
|
return;
|
||||||
} else if (event.key === Qt.Key_PageUp) {
|
} else if (event.key === Qt.Key_PageUp) {
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
@@ -228,7 +228,7 @@ Kirigami.Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: currentRoom
|
target: root.currentRoom
|
||||||
function onShowMessage(messageType, message) {
|
function onShowMessage(messageType, message) {
|
||||||
banner.text = message;
|
banner.text = message;
|
||||||
banner.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;
|
banner.type = messageType === ActionsHandler.Error ? Kirigami.MessageType.Error : messageType === ActionsHandler.Positive ? Kirigami.MessageType.Positive : Kirigami.MessageType.Information;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ QQC2.ScrollView {
|
|||||||
/// Used to determine if scrolling to the bottom should mark the message as unread
|
/// Used to determine if scrolling to the bottom should mark the message as unread
|
||||||
property bool hasScrolledUpBefore: false;
|
property bool hasScrolledUpBefore: false;
|
||||||
|
|
||||||
signal focusChatBox()
|
signal focusChatBar()
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: messageListView
|
id: messageListView
|
||||||
@@ -166,7 +166,7 @@ QQC2.ScrollView {
|
|||||||
action: Kirigami.Action {
|
action: Kirigami.Action {
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!Kirigami.Settings.isMobile) {
|
if (!Kirigami.Settings.isMobile) {
|
||||||
root.focusChatBox();
|
root.focusChatBar();
|
||||||
}
|
}
|
||||||
messageListView.goToEvent(root.currentRoom.readMarkerEventId)
|
messageListView.goToEvent(root.currentRoom.readMarkerEventId)
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ QQC2.ScrollView {
|
|||||||
HoverActions {
|
HoverActions {
|
||||||
id: hoverActions
|
id: hoverActions
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
onFocusChatBar: root.focusChatBox()
|
onFocusChatBar: root.focusChatBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentYChanged: {
|
onContentYChanged: {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ RowLayout {
|
|||||||
|
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
Layout.bottomMargin: 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: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
|
|||||||
Reference in New Issue
Block a user