Really reenable editing messages, don't just do half a job.
This commit is contained in:
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,6 @@ DelegateChooser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
|
||||||
roleValue: MessageComponentType.ChatBar
|
|
||||||
delegate: ChatBarComponent {}
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageComponentType.ReplyButton
|
roleValue: MessageComponentType.ReplyButton
|
||||||
delegate: ReplyButtonComponent {}
|
delegate: ReplyButtonComponent {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user