Compare commits

...

2 Commits

Author SHA1 Message Date
James Graham
341c439edf Fix tests 2026-02-28 10:21:11 +00:00
James Graham
1d5ec4d239 Plumb the adding of message edit text back in.
This should also improve edits where there is code or quotes.
2026-02-27 17:18:19 +00:00
8 changed files with 141 additions and 20 deletions

View File

@@ -109,6 +109,34 @@ TestCase {
compare(spyCursor.count, 5);
}
function test_longFixedChars(): void {
textEdit.forceActiveFocus();
testHelper.setFixedChars("111", "222");
compare(textEdit.text, "111222");
compare(textEdit.cursorPosition, 3);
compare(spyCursor.count, 0);
keyClick("b");
compare(textEdit.text, "111b222");
compare(textEdit.cursorPosition, 4);
compare(spyCursor.count, 1);
keyClick(Qt.Key_Left);
compare(textEdit.text, "111b222");
compare(textEdit.cursorPosition, 3);
compare(spyCursor.count, 2);
keyClick(Qt.Key_Left);
compare(textEdit.text, "111b222");
compare(textEdit.cursorPosition, 3);
compare(spyCursor.count, 3);
keyClick(Qt.Key_Right);
compare(textEdit.text, "111b222");
compare(textEdit.cursorPosition, 4);
compare(spyCursor.count, 4);
keyClick(Qt.Key_Right);
compare(textEdit.text, "111b222");
compare(textEdit.cursorPosition, 4);
compare(spyCursor.count, 5);
}
function test_document(): void {
// We can't get to the QTextDocument from QML so we have to use a helper function.
compare(testHelper.compareDocuments(textEdit.textDocument), true);

View File

@@ -626,10 +626,10 @@ void TextHandlerTest::componentOutput_data()
MessageComponent{MessageComponentType::Code, u"Some code"_s, QVariantMap{{u"class"_s, u"html"_s}}}};
QTest::newRow("quote") << u"<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Quote, u"blockquote"_s, {}}};
MessageComponent{MessageComponentType::Quote, u"\"blockquote\""_s, {}}};
QTest::newRow("multiple paragraph quote") << u"<blockquote>\n<p>blockquote</p>\n<p>next paragraph</p>\n</blockquote>"_s
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Quote, u"<p>blockquote</p>\n<p>next paragraph</p>"_s, {}}};
MessageComponent{MessageComponentType::Quote, u"<p>\"blockquote</p>\n<p>next paragraph\"</p>"_s, {}}};
QTest::newRow("no tag first paragraph") << u"Text\n<p>Text</p>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Text, u"Text"_s, {}}};

View File

@@ -135,7 +135,23 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto [event, _] = m_room->getEvent(m_relationId); event != nullptr) {
return EventHandler::markdownBody(event);
return EventHandler::rawMessageBody(*event);
}
return {};
}
QList<MessageComponent> ChatBarCache::relationComponents() const
{
if (!m_room) {
qCWarning(ChatBar) << "ChatBarCache:" << __FUNCTION__ << "called after room was deleted";
return {};
}
if (m_relationId.isEmpty()) {
return {};
}
if (auto [event, _] = m_room->getEvent(m_relationId); event != nullptr) {
TextHandler handler;
return TextHandler().textComponents(EventHandler::rawMessageBody(*event), EventHandler::messageBodyInputFormat(*event), m_room, event);
}
return {};
}

View File

@@ -15,6 +15,7 @@ namespace Quotient
class RoomMember;
}
struct MessageComponent;
class NeoChatRoom;
/**
@@ -91,13 +92,6 @@ class ChatBarCache : public QObject
*/
Q_PROPERTY(bool relationAuthorIsPresent READ relationAuthorIsPresent NOTIFY relationAuthorIsPresentChanged)
/**
* @brief The content of the related message.
*
* Will be QString() if no related message.
*/
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is replying in a thread.
*/
@@ -147,6 +141,7 @@ public:
bool relationAuthorIsPresent() const;
QString relationMessage() const;
QList<MessageComponent> relationComponents() const;
bool isThreaded() const;
QString threadId() const;

View File

@@ -9,7 +9,6 @@
#include <QTextCursor>
#include <Kirigami/Platform/PlatformTheme>
#include <qtextdocument.h>
#include "chatbarsyntaxhighlighter.h"
#include "neochatroom.h"
@@ -165,23 +164,52 @@ void ChatTextItemHelper::initialize()
int finalCursorPos = cursor.position();
if (doc->isEmpty() && !m_initialFragment.isEmpty()) {
cursor.insertFragment(m_initialFragment);
if (cursor.blockFormat().bottomMargin() > 0) {
auto blockFormat = cursor.blockFormat();
blockFormat.setBottomMargin(0);
cursor.setBlockFormat(blockFormat);
}
finalCursorPos = cursor.position();
}
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
if (!m_fixedStartChars.isEmpty()) {
cursor.movePosition(QTextCursor::Start);
cursor.insertText(m_fixedStartChars);
finalCursorPos += m_fixedStartChars.length();
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_fixedStartChars.length());
if (cursor.selectedText() != m_fixedStartChars) {
cursor.movePosition(QTextCursor::Start);
cursor.insertText(m_fixedStartChars);
finalCursorPos += m_fixedStartChars.length();
}
}
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
if (!m_fixedStartChars.isEmpty()) {
cursor.movePosition(QTextCursor::End);
cursor.keepPositionOnInsert();
cursor.insertText(m_fixedEndChars);
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
if (cursor.selectedText() != m_fixedEndChars) {
cursor.movePosition(QTextCursor::End);
cursor.insertText(m_fixedEndChars);
}
}
setCursorPosition(finalCursorPos);
cursor.endEditBlock();
qWarning() << doc->toRawText();
const auto blockProperties = cursor.blockFormat().properties();
for (const auto &property : blockProperties.keys()) {
qWarning() << static_cast<QTextFormat::Property>(property) << blockProperties[property];
}
const auto textProperties = cursor.charFormat().properties();
for (const auto &property : textProperties.keys()) {
qWarning() << static_cast<QTextFormat::Property>(property) << textProperties[property];
}
const auto currentList = cursor.currentList();
if (currentList) {
const auto listProperties = currentList->format().properties();
for (const auto &property : listProperties.keys()) {
qWarning() << static_cast<QTextFormat::Property>(property) << listProperties[property];
}
}
m_initializingChars = false;
}

View File

@@ -402,9 +402,9 @@ QString TextHandler::stripBlockTags(QString string, const QString &tagType) cons
}
// This is not a normal quotation mark but U+201C
string.insert(startQuotationIndex, u'');
string.insert(startQuotationIndex, u"\""_s);
// This is U+201D
string.insert(endQuotationIndex, u'');
string.insert(endQuotationIndex, u"\""_s);
}
return string;

View File

@@ -13,8 +13,10 @@
#include "enums/chatbartype.h"
#include "enums/messagecomponenttype.h"
#include "enums/richformat.h"
#include "messagecomponent.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
#include "texthandler.h"
namespace
{
@@ -51,6 +53,18 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
textItem->setRoom(m_room);
}
}
// We can't guarantee whether room or type is intialised first so we have to handle.
if (!m_room || !unhandledTypeChange) {
return;
}
connectCache(m_room->cacheForType(*unhandledTypeChange));
unhandledTypeChange = std::nullopt;
const auto newCache = m_room->cacheForType(m_type);
if (newCache && newCache->isEditing()) {
initializeEdit();
return;
}
initializeFromCache();
});
connect(this, &ChatBarMessageContentModel::typeChanged, this, [this](ChatBarType::Type oldType) {
for (const auto &component : std::as_const(m_components)) {
@@ -59,9 +73,15 @@ ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
}
}
if (!m_room) {
unhandledTypeChange = oldType;
return;
}
connectCache(m_room->cacheForType(oldType));
const auto newCache = m_room->cacheForType(m_type);
if (newCache && newCache->isEditing()) {
initializeEdit();
return;
}
initializeFromCache();
});
connect(m_markdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatBarMessageContentModel::insertStyleAtCursor);
@@ -91,7 +111,7 @@ void ChatBarMessageContentModel::connectCache(ChatBarCache *oldCache)
const auto currentCache = m_room->cacheForType(m_type);
updateReplyModel();
if (currentCache->isEditing()) {
initializeFromCache();
initializeEdit();
}
});
connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() {
@@ -151,6 +171,38 @@ void ChatBarMessageContentModel::initializeFromCache()
Q_EMIT focusRowChanged();
}
void ChatBarMessageContentModel::initializeEdit()
{
clearModel();
const auto currentCache = m_room->cacheForType(m_type);
auto components = currentCache->relationComponents();
if (components.isEmpty()) {
initializeModel();
return;
}
beginResetModel();
std::ranges::for_each(components, [this](MessageComponent component) {
if (MessageComponentType::isTextType(component.type)) {
const auto textItemWrapper = new ChatTextItemHelper(this);
const auto initialFragment = component.type == MessageComponentType::Code ? QTextDocumentFragment::fromPlainText(component.display)
: QTextDocumentFragment::fromHtml(component.display);
textItemWrapper->setInitialFragment(initialFragment);
textItemWrapper->setRoom(m_room);
textItemWrapper->setType(m_type);
if (component.type == MessageComponentType::Quote) {
textItemWrapper->setFixedChars(u"\""_s, u"\""_s);
}
component.attributes.insert(TextItemKey, QVariant::fromValue<ChatTextItemHelper *>(textItemWrapper));
connectTextItem(textItemWrapper);
}
m_components += component;
});
endResetModel();
}
ChatBarType::Type ChatBarMessageContentModel::type() const
{
return m_type;

View File

@@ -130,10 +130,12 @@ Q_SIGNALS:
private:
ChatBarType::Type m_type = ChatBarType::None;
std::optional<ChatBarType::Type> unhandledTypeChange = std::nullopt;
void connectCache(ChatBarCache *oldCache = nullptr);
void initializeModel(const QString &initialText = {});
void initializeFromCache();
void initializeEdit();
std::optional<QString> getReplyEventId() override;