diff --git a/src/chatbar/ChatBar.qml b/src/chatbar/ChatBar.qml index 4722e5e99..11f31f8e3 100644 --- a/src/chatbar/ChatBar.qml +++ b/src/chatbar/ChatBar.qml @@ -42,6 +42,8 @@ Item { } } + property int chatBarType: LibNeoChat.ChatBarType.Room + onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus() Connections { @@ -127,7 +129,7 @@ Item { id: chatContentView model: ChatBarMessageContentModel { id: contentModel - type: ChatBarType.Room + type: root.chatBarType room: root.currentRoom sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0 } @@ -201,7 +203,7 @@ Item { property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel { room: root.currentRoom - type: LibNeoChat.ChatBarType.Room + type: root.chatBarType textItem: contentModel.focusedTextItem roomListModel: RoomManager.roomListModel userListModel: RoomManager.userListModel diff --git a/src/chatbar/ChatBarCore.qml b/src/chatbar/ChatBarCore.qml new file mode 100644 index 000000000..e7d9eee03 --- /dev/null +++ b/src/chatbar/ChatBarCore.qml @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtCore +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat.libneochat as LibNeoChat + +QQC2.Control { + id: root + + required property real availableWidth + + width: root.availableWidth - Kirigami.Units.largeSpacing * 2 + topPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + contentItem: ColumnLayout { + RichEditBar { + id: richEditBar + visible: NeoChatConfig.sendMessageWith === 1 + maxAvailableWidth: root.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 + } + } +} diff --git a/src/messagecontent/ChatBarComponent.qml b/src/messagecontent/ChatBarComponent.qml index 9b6b03454..c0ce9de4d 100644 --- a/src/messagecontent/ChatBarComponent.qml +++ b/src/messagecontent/ChatBarComponent.qml @@ -104,25 +104,21 @@ QQC2.Control { } } - CompletionMenu { - id: completionMenu - width: Math.max(350, root.width - 1) - height: implicitHeight - y: -height - 5 - z: 10 - - room: root.Message.room - type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread - // textItem: textArea - margins: 0 - Behavior on height { - NumberAnimation { - property: "height" - duration: Kirigami.Units.shortDuration - easing.type: Easing.OutCubic - } - } - } + // 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 diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index 0e24ec76e..7cc34464b 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -14,6 +14,7 @@ #include "enums/messagecomponenttype.h" #include "enums/richformat.h" #include "messagecontentmodel.h" +#include "neochatroom.h" namespace { @@ -27,46 +28,13 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent) { m_editableActive = true; - connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() { + connect(this, &ChatBarMessageContentModel::roomChanged, this, [this](NeoChatRoom *oldRoom) { if (m_type == ChatBarType::None || !m_room) { return; } - connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, &ChatBarMessageContentModel::updateReplyModel); - connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() { - if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) { - addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath())); - } - }); - - if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) { - addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath())); - } - - clearModel(); - - const auto textSections = m_room->cacheForType(m_type)->text().split(u"\n\n"_s); - if (textSections.length() == 1 && textSections[0].isEmpty()) { - initializeModel(); - return; - } - - beginResetModel(); - for (const auto §ion : textSections) { - const auto type = MessageComponentType::typeForString(section); - auto cleanText = section; - if (type == MessageComponentType::Code) { - cleanText.remove(0, 4); - cleanText.remove(cleanText.length() - 4, 4); - } else if (type == MessageComponentType::Quote) { - cleanText.remove(0, 2); - } - insertComponent(rowCount(), type, {}, cleanText); - } - endResetModel(); - - m_currentFocusComponent = QPersistentModelIndex(index(rowCount() - 1)); - Q_EMIT focusRowChanged(); + connectCache(oldRoom ? oldRoom->cacheForType(m_type) : nullptr); + initializeFromCache(); }); connect(this, &ChatBarMessageContentModel::focusRowChanged, this, [this]() { m_markdownHelper->setTextItem(focusedTextItem()); @@ -80,19 +48,43 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent) } m_keyHelper->room = m_room; }); - connect(this, &ChatBarMessageContentModel::typeChanged, this, [this]() { + connect(this, &ChatBarMessageContentModel::typeChanged, this, [this](ChatBarType::Type oldType) { for (const auto &component : m_components) { if (const auto textItem = textItemForComponent(component)) { textItem->setType(m_type); } } + if (!m_room) { + return; + } + connectCache(m_room->cacheForType(oldType)); + initializeFromCache(); }); connect(m_markdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatBarMessageContentModel::insertStyleAtCursor); + connectCache(); connectKeyHelper(); initializeModel(); } +void ChatBarMessageContentModel::connectCache(ChatBarCache *oldCache) +{ + if (m_type == ChatBarType::None || !m_room) { + return; + } + + if (oldCache) { + oldCache->disconnect(this); + } + + connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, &ChatBarMessageContentModel::updateReplyModel); + connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() { + if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) { + addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath())); + } + }); +} + void ChatBarMessageContentModel::initializeModel(const QString &initialText) { updateReplyModel(); @@ -114,6 +106,38 @@ void ChatBarMessageContentModel::initializeModel(const QString &initialText) Q_EMIT focusRowChanged(); } +void ChatBarMessageContentModel::initializeFromCache() +{ + if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) { + addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath())); + } + + clearModel(); + + const auto textSections = m_room->cacheForType(m_type)->text().split(u"\n\n"_s); + if (textSections.length() == 1 && textSections[0].isEmpty()) { + initializeModel(); + return; + } + + beginResetModel(); + for (const auto §ion : textSections) { + const auto type = MessageComponentType::typeForString(section); + auto cleanText = section; + if (type == MessageComponentType::Code) { + cleanText.remove(0, 4); + cleanText.remove(cleanText.length() - 4, 4); + } else if (type == MessageComponentType::Quote) { + cleanText.remove(0, 2); + } + insertComponent(rowCount(), type, {}, cleanText); + } + endResetModel(); + + m_currentFocusComponent = QPersistentModelIndex(index(rowCount() - 1)); + Q_EMIT focusRowChanged(); +} + ChatBarType::Type ChatBarMessageContentModel::type() const { return m_type; @@ -124,8 +148,8 @@ void ChatBarMessageContentModel::setType(ChatBarType::Type type) if (type == m_type) { return; } - m_type = type; - Q_EMIT typeChanged(); + const auto oldType = std::exchange(m_type, type); + Q_EMIT typeChanged(oldType, m_type); } ChatKeyHelper *ChatBarMessageContentModel::keyHelper() const diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h index 6b3cbab8a..f43d97d7b 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.h +++ b/src/messagecontent/models/chatbarmessagecontentmodel.h @@ -6,6 +6,7 @@ #include #include +#include "chatbarcache.h" #include "chatkeyhelper.h" #include "chatmarkdownhelper.h" #include "chattextitemhelper.h" @@ -100,15 +101,17 @@ public: Q_INVOKABLE void postMessage(); Q_SIGNALS: - void typeChanged(); + void typeChanged(ChatBarType::Type oldType, ChatBarType::Type newType); void focusRowChanged(); void hasRichFormattingChanged(); void sendMessageWithEnterChanged(); private: ChatBarType::Type m_type = ChatBarType::None; + void connectCache(ChatBarCache *oldCache = nullptr); void initializeModel(const QString &initialText = {}); + void initializeFromCache(); std::optional getReplyEventId() override; diff --git a/src/messagecontent/models/messagecontentmodel.cpp b/src/messagecontent/models/messagecontentmodel.cpp index d0a3af03a..984394adf 100644 --- a/src/messagecontent/models/messagecontentmodel.cpp +++ b/src/messagecontent/models/messagecontentmodel.cpp @@ -67,7 +67,7 @@ void MessageContentModel::setRoom(NeoChatRoom *room) m_room->disconnect(this); } - m_room = room; + const auto oldRoom = std::exchange(m_room, room); if (m_room) { connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) { @@ -103,7 +103,7 @@ void MessageContentModel::setRoom(NeoChatRoom *room) connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated); } - Q_EMIT roomChanged(); + Q_EMIT roomChanged(oldRoom, m_room); } QString MessageContentModel::eventId() const diff --git a/src/messagecontent/models/messagecontentmodel.h b/src/messagecontent/models/messagecontentmodel.h index 4d26e6941..3602a512e 100644 --- a/src/messagecontent/models/messagecontentmodel.h +++ b/src/messagecontent/models/messagecontentmodel.h @@ -123,7 +123,7 @@ public: Q_INVOKABLE void toggleSpoiler(QModelIndex index); Q_SIGNALS: - void roomChanged(); + void roomChanged(NeoChatRoom *oldRoom, NeoChatRoom *newRoom); void authorChanged(); /**