Compare commits

..

4 Commits

Author SHA1 Message Date
Joshua Goins
c9c6db8e04 Prevent users from trying to search in encrypted rooms
I forgot that this has to be handled client-side (duh, we're the only
one who can read the messages!) This has already tripped up at least one
user, and probably more. So until we support that, our UI shouldn't lie
to you.
2026-02-27 15:24:18 -05:00
James Graham
7550b2b000 Apply 1 suggestion(s) to 1 file(s)
Co-authored-by: Tobias Fella <fella@posteo.de>
2026-02-27 14:15:30 +00:00
James Graham
daa66c4abc Treat all text in a code block as plain, i.e. don't format based on markdown. 2026-02-27 14:15:30 +00:00
James Graham
e0b229e040 Focus the charbar when clicking the reply button. 2026-02-27 13:29:04 +00:00
10 changed files with 28 additions and 142 deletions

View File

@@ -109,34 +109,6 @@ 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,23 +135,7 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto [event, _] = m_room->getEvent(m_relationId); event != nullptr) {
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 EventHandler::markdownBody(event);
}
return {};
}

View File

@@ -15,7 +15,6 @@ namespace Quotient
class RoomMember;
}
struct MessageComponent;
class NeoChatRoom;
/**
@@ -92,6 +91,13 @@ 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.
*/
@@ -141,7 +147,6 @@ public:
bool relationAuthorIsPresent() const;
QString relationMessage() const;
QList<MessageComponent> relationComponents() const;
bool isThreaded() const;
QString threadId() const;

View File

@@ -188,7 +188,7 @@ void ChatMarkdownHelper::checkMarkdown(int position, int charsRemoved, int chars
// This can happen when formatting is applied.
if (charsAdded == charsRemoved) {
return;
} else if (m_textItem->isCompleting || charsRemoved > charsAdded || charsAdded - charsRemoved > 1) {
} else if ((m_textItem->textFormat() && m_textItem->textFormat() == Qt::TextFormat::PlainText) || m_textItem->isCompleting || charsRemoved > charsAdded || charsAdded - charsRemoved > 1) {
updatePosition(std::max(0, position - charsRemoved + charsAdded));
return;
}

View File

@@ -9,6 +9,7 @@
#include <QTextCursor>
#include <Kirigami/Platform/PlatformTheme>
#include <qtextdocument.h>
#include "chatbarsyntaxhighlighter.h"
#include "neochatroom.h"
@@ -164,52 +165,23 @@ 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()) {
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
cursor.movePosition(QTextCursor::Start);
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();
}
cursor.insertText(m_fixedStartChars);
finalCursorPos += m_fixedStartChars.length();
}
if (!m_fixedStartChars.isEmpty()) {
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
cursor.movePosition(QTextCursor::End);
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
if (cursor.selectedText() != m_fixedEndChars) {
cursor.movePosition(QTextCursor::End);
cursor.insertText(m_fixedEndChars);
}
cursor.keepPositionOnInsert();
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"\""_s);
string.insert(startQuotationIndex, u'');
// This is U+201D
string.insert(endQuotationIndex, u"\""_s);
string.insert(endQuotationIndex, u'');
}
return string;

View File

@@ -13,10 +13,8 @@
#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
{
@@ -53,18 +51,6 @@ 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)) {
@@ -73,15 +59,9 @@ 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);
@@ -110,8 +90,9 @@ void ChatBarMessageContentModel::connectCache(ChatBarCache *oldCache)
}
const auto currentCache = m_room->cacheForType(m_type);
updateReplyModel();
refocusCurrentComponent();
if (currentCache->isEditing()) {
initializeEdit();
initializeFromCache();
}
});
connect(m_room->cacheForType(m_type), &ChatBarCache::attachmentPathChanged, this, [this]() {
@@ -171,38 +152,6 @@ 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,12 +130,10 @@ 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;

View File

@@ -30,6 +30,9 @@ SearchPage {
*/
property string senderId
// This requires client-side search we don't implement yet
readonly property bool canSearch: !room.usesEncryption
title: i18nc("@action:title", "Search Messages")
model: SearchModel {
@@ -45,6 +48,9 @@ SearchPage {
searchFieldPlaceholder: i18n("Find messages…")
noSearchPlaceholderMessage: i18n("Enter text to start searching")
noResultPlaceholderMessage: i18n("No messages found")
customPlaceholderIcon: "lock-symbolic"
customPlaceholderText: !canSearch ? i18n("Cannot search in encrypted rooms") : ""
enableSearch: canSearch
listVerticalLayoutDirection: ListView.BottomToTop