Add ChatBarMessageContentModel and hook up
This commit is contained in:
@@ -46,6 +46,7 @@ ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE
|
||||
contentprovider.cpp
|
||||
mediasizehelper.cpp
|
||||
pollhandler.cpp
|
||||
models/chatbarmessagecontentmodel.cpp
|
||||
models/itinerarymodel.cpp
|
||||
models/linemodel.cpp
|
||||
models/messagecontentmodel.cpp
|
||||
|
||||
@@ -13,6 +13,11 @@ import org.kde.neochat
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
@@ -37,10 +42,29 @@ QQC2.Control {
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief Whether the component should be editable.
|
||||
*/
|
||||
required property bool editable
|
||||
|
||||
/**
|
||||
* @brief The attributes of the component.
|
||||
*/
|
||||
required property var componentAttributes
|
||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||
chatDocumentHandler.type = ChatBarType.Room;
|
||||
chatDocumentHandler.room = root.Message.room;
|
||||
chatDocumentHandler.textItem = codeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the component is currently focussed.
|
||||
*/
|
||||
required property bool currentFocus
|
||||
onCurrentFocusChanged: if (currentFocus && !codeText.focus) {
|
||||
codeText.forceActiveFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
@@ -52,6 +76,8 @@ QQC2.Control {
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
|
||||
|
||||
width: ListView.view?.width ?? -1
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
@@ -66,12 +92,44 @@ QQC2.Control {
|
||||
|
||||
QQC2.TextArea {
|
||||
id: codeText
|
||||
|
||||
Keys.onUpPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (root.chatDocumentHandler.atFirstLine) {
|
||||
Message.contentModel.focusRow = root.index - 1
|
||||
}
|
||||
}
|
||||
Keys.onDownPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (root.chatDocumentHandler.atLastLine) {
|
||||
Message.contentModel.focusRow = root.index + 1
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: (event) => {
|
||||
event.accepted = true;
|
||||
root.chatDocumentHandler.deleteChar();
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||
event.accepted = true;
|
||||
root.chatDocumentHandler.backspace();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
onFocusChanged: if (focus && !root.currentFocus) {
|
||||
Message.contentModel.setFocusRow(root.index, true)
|
||||
}
|
||||
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
text: root.display
|
||||
readOnly: true
|
||||
text: root.editable ? "" : root.display
|
||||
readOnly: !root.editable
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
@@ -149,7 +207,7 @@ QQC2.Control {
|
||||
right: parent.right
|
||||
rightMargin: (codeScrollView.QQC2.ScrollBar.vertical.visible ? codeScrollView.QQC2.ScrollBar.vertical.width : 0) + Kirigami.Units.smallSpacing
|
||||
}
|
||||
visible: root.hovered
|
||||
visible: root.hovered && !root.editable
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Button {
|
||||
|
||||
@@ -9,9 +9,14 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Control {
|
||||
QQC2.TextArea {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
@@ -31,50 +36,117 @@ QQC2.Control {
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief Whether the component should be editable.
|
||||
*/
|
||||
required property bool editable
|
||||
|
||||
/**
|
||||
* @brief The attributes of the component.
|
||||
*/
|
||||
required property var componentAttributes
|
||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||
chatDocumentHandler.type = ChatBarType.Room;
|
||||
chatDocumentHandler.room = root.Message.room;
|
||||
chatDocumentHandler.textItem = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the component is currently focussed.
|
||||
*/
|
||||
required property bool currentFocus
|
||||
onCurrentFocusChanged: if (currentFocus && !focus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
Keys.onUpPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (root.chatDocumentHandler.atFirstLine) {
|
||||
Message.contentModel.focusRow = root.index - 1
|
||||
}
|
||||
}
|
||||
Keys.onDownPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (root.chatDocumentHandler.atLastLine) {
|
||||
Message.contentModel.focusRow = root.index + 1
|
||||
}
|
||||
}
|
||||
Keys.onLeftPressed: (event) => {
|
||||
if (cursorPosition == 1) {
|
||||
event.accepted = true;
|
||||
} else {
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
Keys.onRightPressed: (event) => {
|
||||
if (cursorPosition == (length - 1)) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: (event) => {
|
||||
event.accepted = true;
|
||||
chatDocumentHandler.deleteChar();
|
||||
}
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key == Qt.Key_Backspace) {
|
||||
event.accepted = true;
|
||||
chatDocumentHandler.backspace();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
contentItem: TextEdit {
|
||||
id: quoteText
|
||||
Layout.fillWidth: true
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
text: root.editable ? "" : root.display
|
||||
selectByMouse: true
|
||||
persistentSelection: true
|
||||
readOnly: !root.editable
|
||||
textFormat: TextEdit.RichText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font.italic: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||
|
||||
text: root.display
|
||||
readOnly: true
|
||||
textFormat: TextEdit.RichText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||
|
||||
font.italic: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||
onFocusChanged: if (focus && !currentFocus) {
|
||||
Message.contentModel.setFocusRow(root.index, true)
|
||||
}
|
||||
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||
onCursorPositionChanged: if (cursorPosition == 0) {
|
||||
cursorPosition = 1;
|
||||
} else if (cursorPosition == length) {
|
||||
cursorPosition = length - 1;
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: !quoteText.hoveredLink
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !root.hoveredLink
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: {
|
||||
const event = root.Message.room.findEvent(root.eventId);
|
||||
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ RowLayout {
|
||||
*/
|
||||
required property var replyContentModel
|
||||
|
||||
/**
|
||||
* @brief Whether the component should be editable.
|
||||
*/
|
||||
required property bool editable
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
@@ -55,6 +60,21 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: cancelButton
|
||||
|
||||
anchors.top: root.top
|
||||
anchors.right: root.right
|
||||
|
||||
visible: root.editable
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Cancel reply")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: root.Message.room.mainCache.replyId = ""
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
@@ -40,10 +40,29 @@ TextEdit {
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief Whether the component should be editable.
|
||||
*/
|
||||
required property bool editable
|
||||
|
||||
/**
|
||||
* @brief The attributes of the component.
|
||||
*/
|
||||
required property var componentAttributes
|
||||
readonly property ChatDocumentHandler chatDocumentHandler: componentAttributes?.chatDocumentHandler ?? null
|
||||
onChatDocumentHandlerChanged: if (chatDocumentHandler) {
|
||||
chatDocumentHandler.type = ChatBarType.Room;
|
||||
chatDocumentHandler.room = root.Message.room;
|
||||
chatDocumentHandler.textItem = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the component is currently focussed.
|
||||
*/
|
||||
required property bool currentFocus
|
||||
onCurrentFocusChanged: if (currentFocus && !focus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the message contains a spoiler
|
||||
@@ -56,12 +75,46 @@ TextEdit {
|
||||
property bool isReply: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
Keys.onUpPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (chatDocumentHandler.atFirstLine) {
|
||||
Message.contentModel.focusRow = root.index - 1
|
||||
}
|
||||
}
|
||||
Keys.onDownPressed: (event) => {
|
||||
event.accepted = false;
|
||||
if (chatDocumentHandler.atLastLine) {
|
||||
Message.contentModel.focusRow = root.index + 1
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: (event) => {
|
||||
event.accepted = true;
|
||||
chatDocumentHandler.deleteChar();
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key == Qt.Key_Backspace && cursorPosition == 0) {
|
||||
event.accepted = true;
|
||||
chatDocumentHandler.backspace();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
onFocusChanged: if (focus && !root.currentFocus) {
|
||||
Message.contentModel.setFocusRow(root.index, true)
|
||||
}
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
|
||||
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
persistentSelection: true
|
||||
|
||||
text: display
|
||||
text: root.editable ? "" : display
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
@@ -73,7 +126,7 @@ TextEdit {
|
||||
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
|
||||
}
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
readOnly: !root.editable
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
|
||||
490
src/messagecontent/models/chatbarmessagecontentmodel.cpp
Normal file
490
src/messagecontent/models/chatbarmessagecontentmodel.cpp
Normal file
@@ -0,0 +1,490 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "chatbarmessagecontentmodel.h"
|
||||
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "enums/chatbartype.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "messagecontentmodel.h"
|
||||
|
||||
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
|
||||
: MessageContentModel(parent)
|
||||
{
|
||||
m_editableActive = true;
|
||||
initializeModel();
|
||||
|
||||
connect(this, &ChatBarMessageContentModel::roomChanged, this, [this]() {
|
||||
if (m_type == ChatBarType::None || !m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_room->cacheForType(m_type), &ChatBarCache::relationIdChanged, this, &ChatBarMessageContentModel::updateReplyModel);
|
||||
clearModel();
|
||||
|
||||
beginResetModel();
|
||||
|
||||
if (m_room->cacheForType(m_type)->attachmentPath().length() > 0) {
|
||||
addAttachment(QUrl(m_room->cacheForType(m_type)->attachmentPath()));
|
||||
}
|
||||
|
||||
const auto textSections = m_room->cacheForType(m_type)->text().split(u"\n\n"_s);
|
||||
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);
|
||||
}
|
||||
m_currentFocusComponent = QPersistentModelIndex(index(rowCount() - 1));
|
||||
endResetModel();
|
||||
|
||||
Q_EMIT focusRowChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::initializeModel()
|
||||
{
|
||||
beginInsertRows({}, rowCount(), rowCount());
|
||||
const auto documentHandler = new ChatDocumentHandler();
|
||||
connectHandler(documentHandler);
|
||||
m_components += MessageComponent{
|
||||
.type = MessageComponentType::Text,
|
||||
.display = {},
|
||||
.attributes = {{"chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler)}},
|
||||
};
|
||||
m_currentFocusComponent = QPersistentModelIndex(index(0));
|
||||
endInsertRows();
|
||||
|
||||
Q_EMIT focusRowChanged();
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::connectHandler(ChatDocumentHandler *handler)
|
||||
{
|
||||
connect(handler, &ChatDocumentHandler::contentsChanged, this, &ChatBarMessageContentModel::updateCache);
|
||||
connect(handler, &ChatDocumentHandler::unhandledBackspaceAtBeginning, this, [this](ChatDocumentHandler *handler) {
|
||||
const auto index = indexForDocumentHandler(handler);
|
||||
if (index.isValid()) {
|
||||
if (index.row() > 0 && MessageComponentType::isFileType(m_components[index.row() - 1].type)) {
|
||||
removeAttachment();
|
||||
} else if (m_components[index.row()].type == MessageComponentType::Code || m_components[index.row()].type == MessageComponentType::Quote) {
|
||||
insertComponentAtCursor(MessageComponentType::Text);
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(handler, &ChatDocumentHandler::removeMe, this, [this](ChatDocumentHandler *handler) {
|
||||
removeComponent(handler);
|
||||
});
|
||||
}
|
||||
|
||||
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForComponent(const MessageComponent &component) const
|
||||
{
|
||||
if (const auto chatDocumentHandler = qvariant_cast<ChatDocumentHandler *>(component.attributes["chatDocumentHandler"_L1])) {
|
||||
return chatDocumentHandler;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ChatDocumentHandler *ChatBarMessageContentModel::documentHandlerForIndex(const QModelIndex &index) const
|
||||
{
|
||||
return documentHandlerForComponent(m_components[index.row()]);
|
||||
}
|
||||
|
||||
QModelIndex ChatBarMessageContentModel::indexForDocumentHandler(ChatDocumentHandler *handler) const
|
||||
{
|
||||
for (auto it = m_components.begin(); it != m_components.end(); ++it) {
|
||||
const auto currentIndex = index(it - m_components.begin());
|
||||
if (documentHandlerForIndex(currentIndex) == handler) {
|
||||
return currentIndex;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::updateDocumentHandlerRefs(const ComponentIt &it)
|
||||
{
|
||||
if (it == m_components.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto handler = documentHandlerForComponent(*it);
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (it != m_components.begin()) {
|
||||
if (const auto beforeHandler = documentHandlerForComponent(*(it - 1))) {
|
||||
beforeHandler->setNextDocumentHandler(handler);
|
||||
handler->setPreviousDocumentHandler(beforeHandler);
|
||||
}
|
||||
}
|
||||
if (it + 1 != m_components.end()) {
|
||||
if (const auto afterHandler = documentHandlerForComponent(*(it + 1))) {
|
||||
afterHandler->setPreviousDocumentHandler(handler);
|
||||
handler->setNextDocumentHandler(afterHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatBarType::Type ChatBarMessageContentModel::type() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::setType(ChatBarType::Type type)
|
||||
{
|
||||
if (type == m_type) {
|
||||
return;
|
||||
}
|
||||
m_type = type;
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
|
||||
int ChatBarMessageContentModel::focusRow() const
|
||||
{
|
||||
return m_currentFocusComponent.row();
|
||||
}
|
||||
|
||||
MessageComponentType::Type ChatBarMessageContentModel::focusType() const
|
||||
{
|
||||
return static_cast<MessageComponentType::Type>(m_currentFocusComponent.data(ComponentTypeRole).toInt());
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::setFocusRow(int focusRow, bool mouse)
|
||||
{
|
||||
if (focusRow == m_currentFocusComponent.row() || focusRow < 0 || focusRow >= rowCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusIndex(index(focusRow), mouse);
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::setFocusIndex(const QModelIndex &index, bool mouse)
|
||||
{
|
||||
const auto oldIndex = std::exchange(m_currentFocusComponent, QPersistentModelIndex(index));
|
||||
|
||||
if (m_currentFocusComponent.isValid()) {
|
||||
if (!mouse) {
|
||||
focusCurrentComponent(oldIndex, m_currentFocusComponent.row() > oldIndex.row());
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT focusRowChanged();
|
||||
emitFocusChangeSignals();
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::focusCurrentComponent(const QModelIndex &previousIndex, bool down)
|
||||
{
|
||||
const auto chatDocumentHandler = focusedDocumentHandler();
|
||||
if (!chatDocumentHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatDocumentHandler->setCursorFromDocumentHandler(documentHandlerForIndex(previousIndex), down, MessageComponentType::Quote ? 1 : 0);
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::refocusCurrentComponent() const
|
||||
{
|
||||
const auto chatDocumentHandler = focusedDocumentHandler();
|
||||
if (!chatDocumentHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatDocumentHandler->textItem()->forceActiveFocus();
|
||||
}
|
||||
|
||||
ChatDocumentHandler *ChatBarMessageContentModel::focusedDocumentHandler() const
|
||||
{
|
||||
if (!m_currentFocusComponent.isValid()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const auto chatDocumentHandler = documentHandlerForIndex(m_currentFocusComponent)) {
|
||||
return chatDocumentHandler;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::emitFocusChangeSignals()
|
||||
{
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {CurrentFocusRole});
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::addAttachment(const QUrl &path)
|
||||
{
|
||||
if (m_type == ChatBarType::None || !m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = insertComponent(m_components.first().type == MessageComponentType::Reply ? 1 : 0,
|
||||
MessageComponentType::typeForPath(path),
|
||||
{
|
||||
{"filename"_L1, path.fileName()},
|
||||
{"source"_L1, path},
|
||||
{"animated"_L1, false},
|
||||
});
|
||||
it->display = path.fileName();
|
||||
++it;
|
||||
Q_EMIT dataChanged(index(std::distance(m_components.begin(), it)), index(std::distance(m_components.begin(), it)), {DisplayRole});
|
||||
|
||||
bool textKept = false;
|
||||
while (it != m_components.end()) {
|
||||
if (it->type != MessageComponentType::Text || textKept) {
|
||||
it = removeComponent(it);
|
||||
} else {
|
||||
textKept = true;
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
m_room->cacheForType(m_type)->setAttachmentPath(path.toString());
|
||||
}
|
||||
|
||||
ChatBarMessageContentModel::ComponentIt
|
||||
ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes, const QString &intialText)
|
||||
{
|
||||
if (row < 0 || row > rowCount()) {
|
||||
return m_components.end();
|
||||
}
|
||||
|
||||
if (MessageComponentType::isTextType(type)) {
|
||||
const auto documentHandler = new ChatDocumentHandler();
|
||||
documentHandler->setInitialText(intialText);
|
||||
if (type == MessageComponentType::Quote) {
|
||||
documentHandler->setFixedStartChars(u"\""_s);
|
||||
documentHandler->setFixedEndChars(u"\""_s);
|
||||
}
|
||||
|
||||
attributes.insert("chatDocumentHandler"_L1, QVariant::fromValue<ChatDocumentHandler *>(documentHandler));
|
||||
connectHandler(documentHandler);
|
||||
}
|
||||
beginInsertRows({}, row, row);
|
||||
const auto it = m_components.insert(row,
|
||||
MessageComponent{
|
||||
.type = type,
|
||||
.display = {},
|
||||
.attributes = attributes,
|
||||
});
|
||||
updateDocumentHandlerRefs(it);
|
||||
endInsertRows();
|
||||
return it;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::Type type)
|
||||
{
|
||||
if (m_components[m_currentFocusComponent.row()].type == type) {
|
||||
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
|
||||
focusedDocumentHandler()->setStyle(ChatDocumentHandler::Paragraph);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasBefore = false;
|
||||
QTextDocumentFragment midFragment;
|
||||
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
||||
|
||||
if (const auto currentChatDocumentHandler = focusedDocumentHandler()) {
|
||||
currentChatDocumentHandler->fillFragments(hasBefore, midFragment, afterFragment);
|
||||
}
|
||||
|
||||
const auto currentType = m_components[m_currentFocusComponent.row()].type;
|
||||
int insertRow = m_currentFocusComponent.row() + (hasBefore ? 1 : 0);
|
||||
|
||||
if (!hasBefore) {
|
||||
removeComponent(insertRow, true);
|
||||
}
|
||||
|
||||
const auto insertIt = insertComponent(insertRow, type);
|
||||
if (insertIt != m_components.end()) {
|
||||
if (const auto insertChatDocumentHandler = documentHandlerForComponent(*insertIt)) {
|
||||
insertChatDocumentHandler->insertFragment(midFragment);
|
||||
}
|
||||
m_currentFocusComponent = QPersistentModelIndex(index(insertIt - m_components.begin()));
|
||||
Q_EMIT focusRowChanged();
|
||||
emitFocusChangeSignals();
|
||||
}
|
||||
|
||||
if (afterFragment) {
|
||||
const auto afterIt = insertComponent(insertRow + 1, currentType);
|
||||
if (afterIt != m_components.end()) {
|
||||
if (const auto afterChatDocumentHandler = documentHandlerForComponent(*afterIt)) {
|
||||
afterChatDocumentHandler->insertFragment(*afterFragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::removeComponent(int row, bool removeLast)
|
||||
{
|
||||
if (row < 0 || row >= rowCount() || (rowCount() == 1 && !removeLast)) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeComponent(m_components.begin() + row);
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::removeAttachment()
|
||||
{
|
||||
if (!hasComponentType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video})) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto mediaRow = 0;
|
||||
if (MessageComponentType::isFileType(m_components[1].type)) {
|
||||
mediaRow = 1;
|
||||
}
|
||||
removeComponent(mediaRow);
|
||||
if (m_room) {
|
||||
m_room->cacheForType(m_type)->setAttachmentPath({});
|
||||
}
|
||||
}
|
||||
|
||||
ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeComponent(ComponentIt it)
|
||||
{
|
||||
if (it == m_components.end()) {
|
||||
return it;
|
||||
}
|
||||
|
||||
const auto row = std::distance(m_components.begin(), it);
|
||||
beginRemoveRows({}, row, row);
|
||||
if (rowCount() == 1) {
|
||||
setFocusIndex({});
|
||||
} else if (m_currentFocusComponent.row() == row) {
|
||||
int newFocusRow;
|
||||
if (row > 0) {
|
||||
newFocusRow = row - 1;
|
||||
} else {
|
||||
newFocusRow = row + 1;
|
||||
}
|
||||
setFocusRow(newFocusRow);
|
||||
}
|
||||
|
||||
if (const auto chatDocumentHandler = documentHandlerForIndex(index(row))) {
|
||||
const auto beforeHandler = chatDocumentHandler->previousDocumentHandler();
|
||||
const auto afterHandler = chatDocumentHandler->nextDocumentHandler();
|
||||
if (beforeHandler && afterHandler) {
|
||||
beforeHandler->setNextDocumentHandler(afterHandler);
|
||||
afterHandler->setPreviousDocumentHandler(beforeHandler);
|
||||
} else if (beforeHandler) {
|
||||
beforeHandler->setNextDocumentHandler(nullptr);
|
||||
} else if (afterHandler) {
|
||||
afterHandler->setPreviousDocumentHandler(nullptr);
|
||||
}
|
||||
|
||||
m_components[row].attributes.remove("chatDocumentHandler"_L1);
|
||||
chatDocumentHandler->disconnect(this);
|
||||
chatDocumentHandler->deleteLater();
|
||||
}
|
||||
it = m_components.erase(it);
|
||||
endRemoveRows();
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::removeComponent(ChatDocumentHandler *handler)
|
||||
{
|
||||
const auto index = indexForDocumentHandler(handler);
|
||||
if (index.isValid()) {
|
||||
removeComponent(index.row());
|
||||
}
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::updateCache() const
|
||||
{
|
||||
if (m_type == ChatBarType::None || !m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_room->cacheForType(m_type)->setText(messageText());
|
||||
}
|
||||
|
||||
inline QString formatQuote(const QString &input)
|
||||
{
|
||||
QString stringOut;
|
||||
auto splitString = input.split(u"\n\n"_s, Qt::SkipEmptyParts);
|
||||
for (auto &string : splitString) {
|
||||
if (string.startsWith(u'*')) {
|
||||
string.removeFirst();
|
||||
}
|
||||
if (string.startsWith(u'\"')) {
|
||||
string.removeFirst();
|
||||
}
|
||||
if (string.endsWith(u'*')) {
|
||||
string.removeLast();
|
||||
}
|
||||
if (string.endsWith(u'\"')) {
|
||||
string.removeLast();
|
||||
}
|
||||
if (!stringOut.isEmpty()) {
|
||||
stringOut += u"\n\n"_s;
|
||||
}
|
||||
stringOut += u"> "_s + string;
|
||||
}
|
||||
return stringOut;
|
||||
}
|
||||
|
||||
inline QString formatCode(const QString &input)
|
||||
{
|
||||
return u"```\n%1\n```"_s.arg(input).replace(u"\n\n"_s, u"\n"_s);
|
||||
}
|
||||
|
||||
QString ChatBarMessageContentModel::messageText() const
|
||||
{
|
||||
QString text;
|
||||
for (const auto &component : m_components) {
|
||||
if (MessageComponentType::isTextType(component.type)) {
|
||||
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
||||
auto newText = chatDocumentHandler->htmlText();
|
||||
if (component.type == MessageComponentType::Quote) {
|
||||
newText = formatQuote(newText);
|
||||
} else if (component.type == MessageComponentType::Code) {
|
||||
newText = formatCode(newText);
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
text += u"\n\n"_s;
|
||||
}
|
||||
text += newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::postMessage()
|
||||
{
|
||||
if (m_type == ChatBarType::None || !m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << m_room->cacheForType(m_type)->text();
|
||||
m_room->cacheForType(m_type)->postMessage();
|
||||
clearModel();
|
||||
initializeModel();
|
||||
}
|
||||
|
||||
std::optional<QString> ChatBarMessageContentModel::getReplyEventId()
|
||||
{
|
||||
return m_room->mainCache()->isReplying() ? std::make_optional(m_room->mainCache()->replyId()) : std::nullopt;
|
||||
}
|
||||
|
||||
void ChatBarMessageContentModel::clearModel()
|
||||
{
|
||||
beginResetModel();
|
||||
for (const auto &component : m_components) {
|
||||
if (const auto chatDocumentHandler = documentHandlerForComponent(component)) {
|
||||
chatDocumentHandler->disconnect(this);
|
||||
chatDocumentHandler->deleteLater();
|
||||
}
|
||||
}
|
||||
m_components.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
#include "moc_chatbarmessagecontentmodel.cpp"
|
||||
96
src/messagecontent/models/chatbarmessagecontentmodel.h
Normal file
96
src/messagecontent/models/chatbarmessagecontentmodel.h
Normal file
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: 2025 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 once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
#include <qabstractitemmodel.h>
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "messagecomponent.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
|
||||
/**
|
||||
* @class ChatBarMessageContentModel
|
||||
*
|
||||
* Inherited from MessageContentModel this visulaises the content of a Quotient::RoomMessageEvent.
|
||||
*/
|
||||
class ChatBarMessageContentModel : public MessageContentModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QQuickTextDocument that is being handled.
|
||||
*/
|
||||
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
||||
|
||||
/**
|
||||
* @brief The row of the model component that currently has focus.
|
||||
*/
|
||||
Q_PROPERTY(int focusRow READ focusRow WRITE setFocusRow NOTIFY focusRowChanged)
|
||||
|
||||
/**
|
||||
* @brief The MessageComponentType of the focussed row.
|
||||
*/
|
||||
Q_PROPERTY(MessageComponentType::Type focusType READ focusType NOTIFY focusRowChanged)
|
||||
|
||||
/**
|
||||
* @brief The ChatDocumentHandler of the model component that currently has focus.
|
||||
*/
|
||||
Q_PROPERTY(ChatDocumentHandler *focusedDocumentHandler READ focusedDocumentHandler NOTIFY focusRowChanged)
|
||||
|
||||
public:
|
||||
explicit ChatBarMessageContentModel(QObject *parent = nullptr);
|
||||
|
||||
ChatBarType::Type type() const;
|
||||
void setType(ChatBarType::Type type);
|
||||
|
||||
int focusRow() const;
|
||||
MessageComponentType::Type focusType() const;
|
||||
Q_INVOKABLE void setFocusRow(int focusRow, bool mouse = false);
|
||||
void setFocusIndex(const QModelIndex &index, bool mouse = false);
|
||||
Q_INVOKABLE void refocusCurrentComponent() const;
|
||||
ChatDocumentHandler *focusedDocumentHandler() const;
|
||||
|
||||
Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type);
|
||||
|
||||
Q_INVOKABLE void addAttachment(const QUrl &path);
|
||||
|
||||
Q_INVOKABLE void removeComponent(int row, bool removeLast = false);
|
||||
|
||||
Q_INVOKABLE void removeAttachment();
|
||||
|
||||
Q_INVOKABLE void postMessage();
|
||||
|
||||
Q_SIGNALS:
|
||||
void typeChanged();
|
||||
void focusRowChanged();
|
||||
|
||||
private:
|
||||
ChatBarType::Type m_type = ChatBarType::None;
|
||||
|
||||
void initializeModel();
|
||||
|
||||
std::optional<QString> getReplyEventId() override;
|
||||
|
||||
void connectHandler(ChatDocumentHandler *handler);
|
||||
ChatDocumentHandler *documentHandlerForComponent(const MessageComponent &component) const;
|
||||
ChatDocumentHandler *documentHandlerForIndex(const QModelIndex &index) const;
|
||||
QModelIndex indexForDocumentHandler(ChatDocumentHandler *handler) const;
|
||||
void updateDocumentHandlerRefs(const ComponentIt &it);
|
||||
|
||||
ComponentIt insertComponent(int row, MessageComponentType::Type type, QVariantMap attributes = {}, const QString &intialText = {});
|
||||
ComponentIt removeComponent(ComponentIt it);
|
||||
void removeComponent(ChatDocumentHandler *handler);
|
||||
|
||||
void focusCurrentComponent(const QModelIndex &previousIndex, bool down);
|
||||
void emitFocusChangeSignals();
|
||||
|
||||
void updateCache() const;
|
||||
QString messageText() const;
|
||||
|
||||
void clearModel();
|
||||
};
|
||||
@@ -25,7 +25,7 @@ using namespace Quotient;
|
||||
bool EventMessageContentModel::m_threadsEnabled = false;
|
||||
|
||||
EventMessageContentModel::EventMessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
|
||||
: MessageContentModel(room, parent, eventId)
|
||||
: MessageContentModel(room, eventId, parent)
|
||||
, m_currentState(isPending ? Pending : Unknown)
|
||||
, m_isReply(isReply)
|
||||
{
|
||||
@@ -313,44 +313,23 @@ QList<MessageComponent> EventMessageContentModel::messageContentComponents(bool
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
void EventMessageContentModel::updateReplyModel()
|
||||
std::optional<QString> EventMessageContentModel::getReplyEventId()
|
||||
{
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr || m_isReply) {
|
||||
return;
|
||||
if (m_isReply) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(m_room->getEvent(m_eventId).first);
|
||||
if (roomMessageEvent == nullptr) {
|
||||
return;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!roomMessageEvent->isReply(!m_threadsEnabled)) {
|
||||
if (m_replyModel) {
|
||||
m_replyModel->disconnect(this);
|
||||
m_replyModel->deleteLater();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_replyModel = new EventMessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this);
|
||||
|
||||
bool hasModel = hasComponentType(MessageComponentType::Reply);
|
||||
if (m_replyModel && !hasModel) {
|
||||
int insertRow = 0;
|
||||
if (m_components.first().type == MessageComponentType::Author) {
|
||||
insertRow = 1;
|
||||
}
|
||||
beginInsertRows({}, insertRow, insertRow);
|
||||
m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}});
|
||||
} else if (!m_replyModel && hasModel) {
|
||||
int removeRow = 0;
|
||||
if (m_components.first().type == MessageComponentType::Author) {
|
||||
removeRow = 1;
|
||||
}
|
||||
beginRemoveRows({}, removeRow, removeRow);
|
||||
m_components.removeAt(removeRow);
|
||||
endRemoveRows();
|
||||
return std::nullopt;
|
||||
}
|
||||
return roomMessageEvent->isReply(!m_threadsEnabled) ? std::make_optional(roomMessageEvent->replyEventId(!m_threadsEnabled)) : std::nullopt;
|
||||
}
|
||||
|
||||
QList<MessageComponent> EventMessageContentModel::componentsForType(MessageComponentType::Type type)
|
||||
|
||||
@@ -67,7 +67,7 @@ private:
|
||||
void resetContent(bool isEditing = false, bool isThreading = false);
|
||||
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
|
||||
|
||||
void updateReplyModel();
|
||||
std::optional<QString> getReplyEventId() override;
|
||||
|
||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||
|
||||
|
||||
@@ -15,21 +15,29 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, MessageContentModel *parent, const QString &eventId)
|
||||
MessageContentModel::MessageContentModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
initializeModel();
|
||||
}
|
||||
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, MessageContentModel *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_room(room)
|
||||
, m_eventId(eventId)
|
||||
{
|
||||
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &MessageContentModel::updateSpoilers);
|
||||
|
||||
setRoom(room);
|
||||
initializeModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::initializeModel()
|
||||
{
|
||||
Q_ASSERT(m_room != nullptr);
|
||||
|
||||
connect(this, &MessageContentModel::componentsUpdated, this, [this]() {
|
||||
if (!m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
forEachComponentOfType({MessageComponentType::Text, MessageComponentType::Quote}, m_linkPreviewAddFunction);
|
||||
} else {
|
||||
@@ -42,37 +50,60 @@ void MessageContentModel::initializeModel()
|
||||
forEachComponentOfType(MessageComponentType::File, m_fileFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr && eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
if (errorMessage.isEmpty()) {
|
||||
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
|
||||
} else {
|
||||
Q_EMIT m_room->showMessage(MessageType::Error,
|
||||
i18nc("@info Failed to download file: [error message]", "Failed to download file:<br />%1", errorMessage));
|
||||
}
|
||||
|
||||
NeoChatRoom *MessageContentModel::room() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
void MessageContentModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_room) {
|
||||
m_room->disconnect(this);
|
||||
}
|
||||
|
||||
m_room = room;
|
||||
|
||||
if (m_room) {
|
||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated);
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr && eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId, const QString &errorMessage) {
|
||||
if (eventId == m_eventId) {
|
||||
forEachComponentOfType({MessageComponentType::File, MessageComponentType::Audio, MessageComponentType::Image, MessageComponentType::Video},
|
||||
m_fileInfoFunction);
|
||||
if (errorMessage.isEmpty()) {
|
||||
Q_EMIT m_room->showMessage(MessageType::Error, i18nc("@info", "Failed to download file."));
|
||||
} else {
|
||||
Q_EMIT m_room->showMessage(MessageType::Error,
|
||||
i18nc("@info Failed to download file: [error message]", "Failed to download file:<br />%1", errorMessage));
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, &MessageContentModel::componentsUpdated);
|
||||
}
|
||||
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
QString MessageContentModel::eventId() const
|
||||
@@ -170,6 +201,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
|
||||
}
|
||||
if (role == Editable) {
|
||||
return m_editableActive;
|
||||
}
|
||||
if (role == CurrentFocusRole) {
|
||||
return index.row() == m_currentFocusComponent.row();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -202,6 +239,8 @@ QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
|
||||
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
|
||||
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
|
||||
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
|
||||
roles[MessageContentModel::Editable] = "editable";
|
||||
roles[MessageContentModel::CurrentFocusRole] = "currentFocus";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -215,6 +254,16 @@ bool MessageContentModel::hasComponentType(MessageComponentType::Type type)
|
||||
!= m_components.cend();
|
||||
}
|
||||
|
||||
bool MessageContentModel::hasComponentType(QList<MessageComponentType::Type> types)
|
||||
{
|
||||
for (const auto &type : types) {
|
||||
if (hasComponentType(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MessageContentModel::forEachComponentOfType(MessageComponentType::Type type,
|
||||
std::function<MessageContentModel::ComponentIt(MessageContentModel::ComponentIt)> function)
|
||||
{
|
||||
@@ -237,6 +286,54 @@ void MessageContentModel::forEachComponentOfType(QList<MessageComponentType::Typ
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QString> MessageContentModel::getReplyEventId()
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void MessageContentModel::updateReplyModel()
|
||||
{
|
||||
const auto eventId = getReplyEventId();
|
||||
if (!eventId) {
|
||||
if (m_replyModel) {
|
||||
m_replyModel->disconnect(this);
|
||||
m_replyModel->deleteLater();
|
||||
}
|
||||
if (hasComponentType(MessageComponentType::Reply)) {
|
||||
forEachComponentOfType(MessageComponentType::Reply, [this](ComponentIt it) {
|
||||
beginRemoveRows({}, std::distance(m_components.begin(), it), std::distance(m_components.begin(), it));
|
||||
it = m_components.erase(it);
|
||||
endRemoveRows();
|
||||
return it;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_replyModel && m_replyModel->eventId() == eventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_replyModel = new EventMessageContentModel(m_room, *eventId, true, false, this);
|
||||
|
||||
bool hasModel = hasComponentType(MessageComponentType::Reply);
|
||||
if (!hasModel) {
|
||||
int insertRow = 0;
|
||||
if (m_components.first().type == MessageComponentType::Author) {
|
||||
insertRow = 1;
|
||||
}
|
||||
beginInsertRows({}, insertRow, insertRow);
|
||||
m_components.insert(insertRow, MessageComponent{MessageComponentType::Reply, QString(), {}});
|
||||
endInsertRows();
|
||||
} else {
|
||||
forEachComponentOfType(MessageComponentType::Reply, [this](ComponentIt it) {
|
||||
const auto replyIndex = index(std::distance(m_components.begin(), it));
|
||||
dataChanged(replyIndex, replyIndex, {ReplyContentModelRole});
|
||||
return ++it;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||
{
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
#include <QImageReader>
|
||||
#include <QQmlEngine>
|
||||
#include <optional>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KSyntaxHighlighting/Definition>
|
||||
@@ -35,8 +36,15 @@ class MessageContentModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The room the chat bar is for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The author if the message.
|
||||
*/
|
||||
Q_PROPERTY(NeochatRoomMember *author READ author NOTIFY authorChanged)
|
||||
Q_PROPERTY(QString eventId READ eventId CONSTANT)
|
||||
|
||||
@@ -59,10 +67,16 @@ public:
|
||||
ThreadRootRole, /**< The thread root event ID for the event. */
|
||||
LinkPreviewerRole, /**< The link preview details. */
|
||||
ChatBarCacheRole, /**< The ChatBarCache to use. */
|
||||
Editable, /**< Whether the component can be edited. */
|
||||
CurrentFocusRole, /**< Whteher the delegate should have focus. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit MessageContentModel(NeoChatRoom *room, MessageContentModel *parent = nullptr, const QString &eventId = {});
|
||||
explicit MessageContentModel(QObject *parent = nullptr);
|
||||
explicit MessageContentModel(NeoChatRoom *room, const QString &eventId, MessageContentModel *parent = nullptr);
|
||||
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
@@ -109,6 +123,7 @@ public:
|
||||
Q_INVOKABLE void toggleSpoiler(QModelIndex index);
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void authorChanged();
|
||||
|
||||
/**
|
||||
@@ -123,7 +138,7 @@ Q_SIGNALS:
|
||||
|
||||
protected:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QString m_eventId;
|
||||
QString m_eventId = {};
|
||||
|
||||
/**
|
||||
* @brief NeoChatDateTime for the message.
|
||||
@@ -150,14 +165,25 @@ protected:
|
||||
|
||||
QList<MessageComponent> m_components;
|
||||
bool hasComponentType(MessageComponentType::Type type);
|
||||
bool hasComponentType(QList<MessageComponentType::Type> types);
|
||||
void forEachComponentOfType(MessageComponentType::Type type, std::function<ComponentIt(ComponentIt)> function);
|
||||
void forEachComponentOfType(QList<MessageComponentType::Type> types, std::function<ComponentIt(ComponentIt)> function);
|
||||
|
||||
/**
|
||||
* @brief The ID for the event that the message is replying to, if any.
|
||||
*
|
||||
* The default implementation returns a std::nullopt.
|
||||
*/
|
||||
virtual std::optional<QString> getReplyEventId();
|
||||
void updateReplyModel();
|
||||
QPointer<MessageContentModel> m_replyModel;
|
||||
QPointer<ReactionModel> m_reactionModel = nullptr;
|
||||
QPointer<ItineraryModel> m_itineraryModel = nullptr;
|
||||
bool m_emptyItinerary = false;
|
||||
|
||||
bool m_editableActive = false;
|
||||
QPersistentModelIndex m_currentFocusComponent = {};
|
||||
|
||||
private:
|
||||
void initializeModel();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user