Really reenable editing messages, don't just do half a job.

This commit is contained in:
James Graham
2026-02-02 15:11:03 +00:00
parent a3cd0c0e8d
commit 234d823366
17 changed files with 132 additions and 421 deletions

View File

@@ -9,19 +9,13 @@ import org.kde.neochat.libneochat
import NeoChatTestUtils import NeoChatTestUtils
TestCase { TestCase {
name: "ChatTextItemHelperTest" name: "ChatKeyHelperTest"
TextEdit { TextEdit {
id: textEdit id: textEdit
Keys.onUpPressed: (event) => { Keys.onPressed: (event) => {
event.accepted = true; event.accepted = testHelper.keyHelper.handleKey(event.key, event.modifiers);
testHelper.keyHelper.up();
}
Keys.onDownPressed: (event) => {
event.accepted = true;
testHelper.keyHelper.down();
} }
} }

View File

@@ -29,6 +29,7 @@
#include "models/livelocationsmodel.h" #include "models/livelocationsmodel.h"
#include "models/locationsmodel.h" #include "models/locationsmodel.h"
#include "models/messagecontentfiltermodel.h" #include "models/messagecontentfiltermodel.h"
#include "models/messagecontentmodel.h"
#include "models/notificationsmodel.h" #include "models/notificationsmodel.h"
#include "models/permissionsmodel.h" #include "models/permissionsmodel.h"
#include "models/pinnedmessagemodel.h" #include "models/pinnedmessagemodel.h"
@@ -179,8 +180,8 @@ void ModelTest::testRoomTreeModel()
void ModelTest::testMessageContentModel() void ModelTest::testMessageContentModel()
{ {
auto contentModel = new MessageContentModel(room, eventId); auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
auto tester = new QAbstractItemModelTester(contentModel); auto tester = new QAbstractItemModelTester(contentModel.get());
tester->setUseFetchMore(true); tester->setUseFetchMore(true);
} }

View File

@@ -8,6 +8,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
QML_FILES QML_FILES
AttachDialog.qml AttachDialog.qml
ChatBar.qml ChatBar.qml
ChatBarCore.qml
RichEditBar.qml RichEditBar.qml
SendBar.qml SendBar.qml
CompletionMenu.qml CompletionMenu.qml

View File

@@ -42,9 +42,9 @@ Item {
} }
} }
property int chatBarType: LibNeoChat.ChatBarType.Room onActiveFocusChanged: if (activeFocus) {
core.forceActiveFocus();
onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus() }
Connections { Connections {
target: ShareHandler target: ShareHandler
@@ -67,97 +67,16 @@ Item {
} }
} }
Message.room: root.currentRoom implicitHeight: core.implicitHeight + Kirigami.Units.largeSpacing
Message.contentModel: contentModel
implicitHeight: chatBar.implicitHeight + Kirigami.Units.largeSpacing
QQC2.Control {
id: chatBar
ChatBarCore {
id: core
anchors.top: root.top anchors.top: root.top
anchors.horizontalCenter: root.horizontalCenter anchors.horizontalCenter: root.horizontalCenter
spacing: 0 Message.room: root.currentRoom
room: root.currentRoom
width: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2 maxAvailableWidth: chatBarSizeHelper.availableWidth
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: ColumnLayout {
RichEditBar {
id: richEditBar
visible: NeoChatConfig.sendMessageWith === 1
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
room: root.currentRoom
contentModel: chatContentView.model
onClicked: contentModel.refocusCurrentComponent()
}
Kirigami.Separator {
Layout.fillWidth: true
visible: NeoChatConfig.sendMessageWith === 1
}
RowLayout {
spacing: 0
QQC2.ScrollView {
id: chatScrollView
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
clip: true
ColumnLayout {
readonly property real visibleTop: chatScrollView.QQC2.ScrollBar.vertical.position * chatScrollView.contentHeight
readonly property real visibleBottom: chatScrollView.QQC2.ScrollBar.vertical.position * chatScrollView.contentHeight + chatScrollView.QQC2.ScrollBar.vertical.size * chatScrollView.contentHeight
readonly property rect cursorRectInColumn: mapFromItem(contentModel.focusedTextItem.textItem, contentModel.focusedTextItem.cursorRectangle);
onCursorRectInColumnChanged: {
if (chatScrollView.QQC2.ScrollBar.vertical.visible) {
if (cursorRectInColumn.y < visibleTop) {
chatScrollView.QQC2.ScrollBar.vertical.position = cursorRectInColumn.y / chatScrollView.contentHeight
} else if (cursorRectInColumn.y + cursorRectInColumn.height > visibleBottom) {
chatScrollView.QQC2.ScrollBar.vertical.position = (cursorRectInColumn.y + cursorRectInColumn.height - (chatScrollView.QQC2.ScrollBar.vertical.size * chatScrollView.contentHeight)) / chatScrollView.contentHeight
}
}
}
width: chatScrollView.width
spacing: Kirigami.Units.smallSpacing
Repeater {
id: chatContentView
model: ChatBarMessageContentModel {
id: contentModel
type: root.chatBarType
room: root.currentRoom
sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0
}
delegate: MessageComponentChooser {
rightAnchorMargin: chatScrollView.QQC2.ScrollBar.vertical.visible ? chatScrollView.QQC2.ScrollBar.vertical.width : 0
}
}
}
}
SendBar {
room: root.currentRoom
contentModel: chatContentView.model
}
}
}
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
width: 1
}
}
} }
MouseArea { MouseArea {
id: hoverArea id: hoverArea
@@ -165,7 +84,7 @@ Item {
top: chatModeButton.top top: chatModeButton.top
left: root.left left: root.left
right: root.right right: root.right
bottom: chatBar.top bottom: core.top
} }
propagateComposedEvents: true propagateComposedEvents: true
hoverEnabled: true hoverEnabled: true
@@ -174,12 +93,12 @@ Item {
QQC2.Button { QQC2.Button {
id: chatModeButton id: chatModeButton
anchors { anchors {
bottom: chatBar.top bottom: core.top
bottomMargin: Kirigami.Units.smallSpacing bottomMargin: Kirigami.Units.smallSpacing
horizontalCenter: root.horizontalCenter horizontalCenter: root.horizontalCenter
} }
visible: hoverArea.containsMouse || hovered || chatBar.hovered visible: hoverArea.containsMouse || hovered || core.hovered
width: Kirigami.Units.iconSizes.enormous width: Kirigami.Units.iconSizes.enormous
height: Kirigami.Units.iconSizes.smallMedium height: Kirigami.Units.iconSizes.smallMedium
@@ -197,27 +116,4 @@ Item {
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85 endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
maxWidth: NeoChatConfig.compactLayout ? root.width - Kirigami.Units.largeSpacing * 2 : Kirigami.Units.gridUnit * 60 maxWidth: NeoChatConfig.compactLayout ? root.width - Kirigami.Units.largeSpacing * 2 : Kirigami.Units.gridUnit * 60
} }
QtObject {
id: _private
property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel {
room: root.currentRoom
type: root.chatBarType
textItem: contentModel.focusedTextItem
roomListModel: RoomManager.roomListModel
userListModel: RoomManager.userListModel
onIsCompletingChanged: {
if (!isCompleting) {
return;
}
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'CompletionMenu').createObject(contentModel.focusedTextItem.textItem, {
model: _private.completionModel,
keyHelper: contentModel.keyHelper
}).open();
}
}
}
} }

View File

@@ -8,14 +8,26 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.libneochat as LibNeoChat import org.kde.neochat.libneochat as LibNeoChat
QQC2.Control { QQC2.Control {
id: root id: root
required property real availableWidth /**
* @brief The current room that user is viewing.
*/
required property LibNeoChat.NeoChatRoom room
width: root.availableWidth - Kirigami.Units.largeSpacing * 2 property int chatBarType: LibNeoChat.ChatBarType.Room
required property real maxAvailableWidth
Message.contentModel: contentModel
onActiveFocusChanged: contentModel.refocusCurrentComponent()
implicitWidth: root.maxAvailableWidth - (root.maxAvailableWidth >= (parent?.width ?? 0) ? Kirigami.Units.largeSpacing * 2 : 0)
topPadding: Kirigami.Units.smallSpacing topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing
@@ -23,9 +35,9 @@ QQC2.Control {
RichEditBar { RichEditBar {
id: richEditBar id: richEditBar
visible: NeoChatConfig.sendMessageWith === 1 visible: NeoChatConfig.sendMessageWith === 1
maxAvailableWidth: root.availableWidth - Kirigami.Units.largeSpacing * 2 maxAvailableWidth: root.maxAvailableWidth - Kirigami.Units.largeSpacing * 2
room: root.currentRoom room: root.room
contentModel: chatContentView.model contentModel: chatContentView.model
onClicked: contentModel.refocusCurrentComponent() onClicked: contentModel.refocusCurrentComponent()
@@ -65,18 +77,18 @@ QQC2.Control {
model: ChatBarMessageContentModel { model: ChatBarMessageContentModel {
id: contentModel id: contentModel
type: root.chatBarType type: root.chatBarType
room: root.currentRoom room: root.room
sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0 sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0
} }
delegate: MessageComponentChooser { delegate: BaseMessageComponentChooser {
rightAnchorMargin: chatScrollView.QQC2.ScrollBar.vertical.visible ? chatScrollView.QQC2.ScrollBar.vertical.width : 0 rightAnchorMargin: chatScrollView.QQC2.ScrollBar.vertical.visible ? chatScrollView.QQC2.ScrollBar.vertical.width : 0
} }
} }
} }
} }
SendBar { SendBar {
room: root.currentRoom room: root.room
contentModel: chatContentView.model contentModel: chatContentView.model
} }
} }
@@ -93,4 +105,27 @@ QQC2.Control {
width: 1 width: 1
} }
} }
QtObject {
id: _private
property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel {
room: root.room
type: root.chatBarType
textItem: contentModel.focusedTextItem
roomListModel: RoomManager.roomListModel
userListModel: RoomManager.userListModel
onIsCompletingChanged: {
if (!isCompleting) {
return;
}
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'CompletionMenu').createObject(contentModel.focusedTextItem.textItem, {
model: _private.completionModel,
keyHelper: contentModel.keyHelper
}).open();
}
}
}
} }

View File

@@ -31,7 +31,7 @@ RowLayout {
property bool isBusy: root.room && root.room.hasFileUploading property bool isBusy: root.room && root.room.hasFileUploading
visible: root.chatBarCache.attachmentPath.length === 0 visible: !root.contentModel.hasAttachment && (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room
icon.name: "mail-attachment" icon.name: "mail-attachment"
text: i18n("Attach an image or file") text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
@@ -84,6 +84,7 @@ RowLayout {
} }
QQC2.ToolButton { QQC2.ToolButton {
id: mapButton id: mapButton
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room
icon.name: "globe" icon.name: "globe"
property bool isBusy: false property bool isBusy: false
text: i18n("Send a Location") text: i18n("Send a Location")
@@ -100,6 +101,7 @@ RowLayout {
} }
QQC2.ToolButton { QQC2.ToolButton {
id: pollButton id: pollButton
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Room
icon.name: "amarok_playcount" icon.name: "amarok_playcount"
property bool isBusy: false property bool isBusy: false
text: i18nc("@action:button", "Create a Poll") text: i18nc("@action:button", "Create a Poll")
@@ -128,4 +130,21 @@ RowLayout {
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text QQC2.ToolTip.text: text
} }
QQC2.ToolButton {
id: cancelButton
visible: (root.contentModel?.type ?? true) === LibNeoChat.ChatBarType.Edit
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onClicked: root.room.cacheForType(contentModel.type).clearRelations()
Kirigami.Action {
shortcut: "Escape"
onTriggered: cancelButton.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
} }

View File

@@ -30,7 +30,7 @@ struct Mention {
* A class to cache data from a chat bar. * A class to cache data from a chat bar.
* *
* A chat bar can be anything that allows users to compose or edit message, it doesn't * A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. ChatBarComponent. * necessarily have to use the ChatBar component.
* *
* This object is intended to allow the current contents of a chat bar to be cached * This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could * between different rooms, i.e. there is an expectation that each NeoChatRoom could
@@ -40,7 +40,7 @@ struct Mention {
* as it's parent. This is necessary for certain functions which need to get * as it's parent. This is necessary for certain functions which need to get
* relevant room information. * relevant room information.
* *
* @sa ChatBar, ChatBarComponent, NeoChatRoom * @sa ChatBar, NeoChatRoom
*/ */
class ChatBarCache : public QObject class ChatBarCache : public QObject
{ {

View File

@@ -512,7 +512,7 @@ public:
ChatBarCache *threadCache() const; ChatBarCache *threadCache() const;
ChatBarCache *cacheForType(ChatBarType::Type type) const; Q_INVOKABLE ChatBarCache *cacheForType(ChatBarType::Type type) const;
/** /**
* @brief Reply to the last message sent in the timeline. * @brief Reply to the last message sent in the timeline.

View File

@@ -52,8 +52,8 @@ void TextHandler::setData(const QString &string)
QString TextHandler::handleSendText() QString TextHandler::handleSendText()
{ {
m_pos = 0; m_pos = 0;
m_dataBuffer = customMarkdownToHtml(m_data); m_dataBuffer = markdownToHTML(m_data);
m_dataBuffer = markdownToHTML(m_dataBuffer); m_dataBuffer = customMarkdownToHtml(m_dataBuffer);
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType); m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
@@ -805,6 +805,8 @@ QString TextHandler::customMarkdownToHtml(const QString &stringIn)
// underline // underline
processSyntax(u"_"_s, u"<u>"_s, u"</u>"_s); processSyntax(u"_"_s, u"<u>"_s, u"</u>"_s);
qWarning() << buffer;
return buffer; return buffer;
} }

View File

@@ -151,11 +151,6 @@ DelegateChooser {
} }
} }
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {}
}
DelegateChoice { DelegateChoice {
roleValue: MessageComponentType.ReplyButton roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {} delegate: ReplyButtonComponent {}

View File

@@ -11,7 +11,6 @@ ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE
ReplyMessageComponentChooser.qml ReplyMessageComponentChooser.qml
AuthorComponent.qml AuthorComponent.qml
AudioComponent.qml AudioComponent.qml
ChatBarComponent.qml
CodeComponent.qml CodeComponent.qml
EncryptedComponent.qml EncryptedComponent.qml
FetchButtonComponent.qml FetchButtonComponent.qml

View File

@@ -1,269 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.chatbar
/**
* @brief A component to show a chat bar in a message bubble.
*/
QQC2.Control {
id: root
/**
* @brief The ChatBarCache to use.
*/
required property ChatBarCache chatBarCache
readonly property bool isBusy: root.Message.room && root.Message.room.hasFileUploading
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
implicitWidth: Message.maxContentWidth
contentItem: ColumnLayout {
Loader {
id: paneLoader
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
active: visible
visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0
sourceComponent: root.chatBarCache.replyId.length > 0 ? replyPane : attachmentPane
}
RowLayout {
Layout.fillWidth: true
spacing: 0
QQC2.ScrollView {
id: chatBarScrollView
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
QQC2.TextArea {
id: textArea
// Work around for BUG: 503846
// Seems to crash when we try to access textArea's text here. Even though we add a slight delay it's still instantaneous in the UI.
Component.onCompleted: Qt.callLater(() => _private.updateText())
Layout.fillWidth: true
color: Kirigami.Theme.textColor
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
onTextChanged: {
root.chatBarCache.text = text;
}
Keys.onEnterPressed: event => {
if (completionMenu.visible) {
completionMenu.completeCurrent();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onReturnPressed: event => {
if (completionMenu.visible) {
completionMenu.completeCurrent();
} else if (event.modifiers & Qt.ShiftModifier) {
textArea.insert(cursorPosition, "\n");
} else {
_private.post();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.completeCurrent();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex();
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex();
}
}
// CompletionMenu {
// id: completionMenu
// width: Math.max(350, root.width - 1)
// height: implicitHeight
// y: -height - 5
// z: 10
// margins: 0
// Behavior on height {
// NumberAnimation {
// property: "height"
// duration: Kirigami.Units.shortDuration
// easing.type: Easing.OutCubic
// }
// }
// }
// opt-out of whatever spell checker a styled TextArea might come with
Kirigami.SpellCheck.enabled: false
TextMetrics {
id: textMetrics
text: textArea.text
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
background: null
}
}
PieProgressBar {
visible: root.isBusy
progress: root.Message.room.fileUploadingProgress
}
QQC2.ToolButton {
visible: !root.isBusy
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Attach an image or file")
icon.name: "mail-attachment"
onClicked: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay) as QQC2.Dialog;
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send"
onClicked: _private.post()
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: cancelButton
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onClicked: {
root.chatBarCache.clearRelations();
}
Kirigami.Action {
shortcut: "Escape"
onTriggered: cancelButton.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
}
}
Component {
id: replyPane
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent
replyContentModel: ContentProvider.contentModelForEvent(root.Message.room, root.chatBarCache.replyId, true)
Message.maxContentWidth: paneLoader.item.width
}
QQC2.Button {
anchors.top: parent.top
anchors.right: parent.right
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
root.chatBarCache.replyId = "";
root.chatBarCache.attachmentPath = "";
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: root.chatBarCache.attachmentPath
onAttachmentCancelled: {
root.chatBarCache.attachmentPath = "";
root.forceActiveFocus();
}
}
}
QtObject {
id: _private
function updateText() {
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
documentHandler.document;
if (root.chatBarCache?.isEditing && root.chatBarCache.relationMessage.length > 0) {
textArea.text = root.chatBarCache.relationMessage;
documentHandler.updateMentions(root.chatBarCache.editId);
textArea.forceActiveFocus();
textArea.cursorPosition = textArea.text.length;
}
}
function post() {
root.chatBarCache.postMessage();
textArea.clear();
root.chatBarCache.clearRelations();
}
}
}

View File

@@ -2,9 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick import QtQuick
import QtQuick.Layouts
import Qt.labs.qmlmodels import Qt.labs.qmlmodels
import org.kde.neochat import org.kde.neochat
import org.kde.neochat.libneochat as LibNeoChat
/** /**
* @brief Select a message component based on a MessageComponentType. * @brief Select a message component based on a MessageComponentType.
@@ -23,4 +25,15 @@ BaseMessageComponentChooser {
} }
} }
} }
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarCore {
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
room: Message.room
chatBarType: LibNeoChat.ChatBarType.Edit
maxAvailableWidth: Message.maxContentWidth
}
}
} }

View File

@@ -77,7 +77,17 @@ void ChatBarMessageContentModel::connectCache(ChatBarCache *oldCache)
oldCache->disconnect(this); oldCache->disconnect(this);
} }
connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, &ChatBarMessageContentModel::updateReplyModel); connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, [this]() {
if (!m_room || m_type == ChatBarType::None) {
return;
}
const auto currentCache = m_room->cacheForType(m_type);
if (currentCache->isReplying()) {
updateReplyModel();
} else if (currentCache->isEditing()) {
initializeFromCache();
}
});
connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() { connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() {
if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) { if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) {
addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath())); addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath()));
@@ -114,7 +124,8 @@ void ChatBarMessageContentModel::initializeFromCache()
clearModel(); clearModel();
const auto textSections = m_room->cacheForType(m_type)->text().split(u"\n\n"_s); const auto currentCache = m_room->cacheForType(m_type);
const auto textSections = (m_type == ChatBarType::Room ? currentCache->text() : currentCache->relationMessage()).split(u"\n\n"_s);
if (textSections.length() == 1 && textSections[0].isEmpty()) { if (textSections.length() == 1 && textSections[0].isEmpty()) {
initializeModel(); initializeModel();
return; return;
@@ -334,6 +345,11 @@ bool ChatBarMessageContentModel::hasRichFormatting() const
return false; return false;
} }
bool ChatBarMessageContentModel::hasAttachment() const
{
return hasComponentType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video});
}
void ChatBarMessageContentModel::addAttachment(const QUrl &path) void ChatBarMessageContentModel::addAttachment(const QUrl &path)
{ {
if (m_type == ChatBarType::None || !m_room) { if (m_type == ChatBarType::None || !m_room) {
@@ -360,6 +376,7 @@ void ChatBarMessageContentModel::addAttachment(const QUrl &path)
it->display = path.fileName(); it->display = path.fileName();
Q_EMIT dataChanged(index(std::distance(m_components.begin(), it)), index(std::distance(m_components.begin(), it)), {DisplayRole}); Q_EMIT dataChanged(index(std::distance(m_components.begin(), it)), index(std::distance(m_components.begin(), it)), {DisplayRole});
m_room->cacheForType(m_type)->setAttachmentPath(path.toString()); m_room->cacheForType(m_type)->setAttachmentPath(path.toString());
Q_EMIT hasAttachmentChanged();
} }
ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::ComponentIt
@@ -477,6 +494,7 @@ void ChatBarMessageContentModel::removeAttachment()
if (m_room) { if (m_room) {
m_room->cacheForType(m_type)->setAttachmentPath({}); m_room->cacheForType(m_type)->setAttachmentPath({});
} }
Q_EMIT hasAttachmentChanged();
} }
bool ChatBarMessageContentModel::sendMessageWithEnter() const bool ChatBarMessageContentModel::sendMessageWithEnter() const

View File

@@ -67,6 +67,11 @@ class ChatBarMessageContentModel : public MessageContentModel
*/ */
Q_PROPERTY(bool hasRichFormatting READ hasRichFormatting NOTIFY hasRichFormattingChanged) Q_PROPERTY(bool hasRichFormatting READ hasRichFormatting NOTIFY hasRichFormattingChanged)
/**
* @brief Whether the model has an attachment..
*/
Q_PROPERTY(bool hasAttachment READ hasAttachment NOTIFY hasAttachmentChanged)
/** /**
* @brief The UserListModel to be used for room completions. * @brief The UserListModel to be used for room completions.
*/ */
@@ -89,6 +94,7 @@ public:
Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type); Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type);
bool hasRichFormatting() const; bool hasRichFormatting() const;
bool hasAttachment() const;
Q_INVOKABLE void addAttachment(const QUrl &path); Q_INVOKABLE void addAttachment(const QUrl &path);
Q_INVOKABLE void removeComponent(int row, bool removeLast = false); Q_INVOKABLE void removeComponent(int row, bool removeLast = false);
@@ -104,6 +110,7 @@ Q_SIGNALS:
void typeChanged(ChatBarType::Type oldType, ChatBarType::Type newType); void typeChanged(ChatBarType::Type oldType, ChatBarType::Type newType);
void focusRowChanged(); void focusRowChanged();
void hasRichFormattingChanged(); void hasRichFormattingChanged();
void hasAttachmentChanged();
void sendMessageWithEnterChanged(); void sendMessageWithEnterChanged();
private: private:

View File

@@ -244,7 +244,7 @@ QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
return roles; return roles;
} }
bool MessageContentModel::hasComponentType(MessageComponentType::Type type) bool MessageContentModel::hasComponentType(MessageComponentType::Type type) const
{ {
return std::find_if(m_components.cbegin(), return std::find_if(m_components.cbegin(),
m_components.cend(), m_components.cend(),
@@ -254,7 +254,7 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
!= m_components.cend(); != m_components.cend();
} }
bool MessageContentModel::hasComponentType(QList<MessageComponentType::Type> types) bool MessageContentModel::hasComponentType(QList<MessageComponentType::Type> types) const
{ {
for (const auto &type : types) { for (const auto &type : types) {
if (hasComponentType(type)) { if (hasComponentType(type)) {

View File

@@ -164,8 +164,8 @@ protected:
using ComponentIt = QList<MessageComponent>::iterator; using ComponentIt = QList<MessageComponent>::iterator;
QList<MessageComponent> m_components; QList<MessageComponent> m_components;
bool hasComponentType(MessageComponentType::Type type); bool hasComponentType(MessageComponentType::Type type) const;
bool hasComponentType(QList<MessageComponentType::Type> types); bool hasComponentType(QList<MessageComponentType::Type> types) const;
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function); void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function); void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);