Add ChatBarMessageContentModel and hook up

This commit is contained in:
James Graham
2025-08-04 18:11:21 +01:00
parent 9cbe9f7280
commit c128450cf5
20 changed files with 1825 additions and 648 deletions

View File

@@ -27,7 +27,7 @@ target_sources(LibNeoChat PRIVATE
utils.cpp
voicerecorder.cpp
enums/chatbartype.h
enums/messagecomponenttype.h
enums/messagecomponenttype.cpp
enums/messagetype.h
enums/powerlevel.cpp
enums/pushrule.h

View File

@@ -11,6 +11,7 @@
#include <QSyntaxHighlighter>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextDocumentFragment>
#include <QTextList>
#include <QTextTable>
#include <QTimer>
@@ -20,17 +21,21 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include <qlogging.h>
#include <qnamespace.h>
#include <qtextcursor.h>
#include <sched.h>
#include "chatbartype.h"
#include "chatdocumenthandler_logging.h"
#include "eventhandler.h"
#include "utils.h"
using namespace Qt::StringLiterals;
class SyntaxHighlighter : public QSyntaxHighlighter
{
public:
QPointer<NeoChatRoom> room;
QTextCharFormat mentionFormat;
QTextCharFormat errorFormat;
Sonnet::BackgroundChecker checker;
@@ -82,11 +87,10 @@ public:
if (!room) {
return;
}
const auto chatchache = handler->chatBarCache();
if (!chatchache) {
if (!room) {
return;
}
auto mentions = chatchache->mentions();
auto mentions = room->cacheForType(handler->type())->mentions();
mentions->erase(std::remove_if(mentions->begin(),
mentions->end(),
[this](auto &mention) {
@@ -127,18 +131,8 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
{
}
void ChatDocumentHandler::updateCompletion() const
{
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
}
int ChatDocumentHandler::completionStartIndex() const
{
if (!m_room) {
return 0;
}
const qsizetype cursor = cursorPosition();
const auto &text = getText();
@@ -189,21 +183,108 @@ void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
m_highlighter->setDocument(document());
if (m_textItem) {
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCompletion()));
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
if (document()) {
connect(document(), &QTextDocument::contentsChanged, this, &ChatDocumentHandler::contentsChanged);
connect(document(), &QTextDocument::contentsChanged, this, [this]() {
if (m_room) {
m_room->cacheForType(m_type)->setText(getText());
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
updateCursor();
}
});
initializeChars();
}
}
Q_EMIT textItemChanged();
}
ChatDocumentHandler *ChatDocumentHandler::previousDocumentHandler() const
{
return m_previousDocumentHandler;
}
void ChatDocumentHandler::setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler)
{
m_previousDocumentHandler = previousDocumentHandler;
}
ChatDocumentHandler *ChatDocumentHandler::nextDocumentHandler() const
{
return m_nextDocumentHandler;
}
void ChatDocumentHandler::setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler)
{
m_nextDocumentHandler = nextDocumentHandler;
}
QString ChatDocumentHandler::fixedStartChars() const
{
return m_fixedStartChars;
}
void ChatDocumentHandler::setFixedStartChars(const QString &chars)
{
if (chars == m_fixedStartChars) {
return;
}
m_fixedStartChars = chars;
}
QString ChatDocumentHandler::fixedEndChars() const
{
return m_fixedEndChars;
;
}
void ChatDocumentHandler::setFixedEndChars(const QString &chars)
{
if (chars == m_fixedEndChars) {
return;
}
m_fixedEndChars = chars;
}
QString ChatDocumentHandler::initialText() const
{
return m_initialText;
}
void ChatDocumentHandler::setInitialText(const QString &text)
{
if (text == m_initialText) {
return;
}
m_initialText = text;
}
void ChatDocumentHandler::initializeChars()
{
const auto doc = document();
if (!doc) {
return;
}
QTextCursor cursor = QTextCursor(doc);
if (cursor.isNull()) {
return;
}
if (doc->isEmpty() && !m_initialText.isEmpty()) {
cursor.insertText(m_initialText);
}
if (!m_fixedStartChars.isEmpty() && doc->characterAt(0) != m_fixedStartChars) {
cursor.movePosition(QTextCursor::Start);
cursor.insertText(m_fixedEndChars);
}
if (!m_fixedStartChars.isEmpty() && doc->characterAt(doc->characterCount()) != m_fixedStartChars) {
cursor.movePosition(QTextCursor::End);
cursor.insertText(m_fixedEndChars);
}
}
QTextDocument *ChatDocumentHandler::document() const
{
if (!m_textItem) {
@@ -221,6 +302,16 @@ int ChatDocumentHandler::cursorPosition() const
return m_textItem->property("cursorPosition").toInt();
}
void ChatDocumentHandler::updateCursor()
{
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
Q_EMIT formatChanged();
Q_EMIT atFirstLineChanged();
Q_EMIT atLastLineChanged();
}
int ChatDocumentHandler::selectionStart() const
{
if (!m_textItem) {
@@ -248,46 +339,191 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
return;
}
if (m_room && m_type != ChatBarType::None) {
m_room->cacheForType(m_type)->disconnect(this);
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
m_room->mainCache()->setSavedText(document()->toPlainText());
}
}
m_room = room;
m_completionModel->setRoom(m_room);
if (m_room && m_type != ChatBarType::None) {
connect(m_room->cacheForType(m_type), &ChatBarCache::textChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
Q_EMIT fontFamilyChanged();
Q_EMIT textColorChanged();
Q_EMIT alignmentChanged();
Q_EMIT boldChanged();
Q_EMIT italicChanged();
Q_EMIT underlineChanged();
Q_EMIT checkableChanged();
Q_EMIT strikethroughChanged();
Q_EMIT fontSizeChanged();
Q_EMIT fileUrlChanged();
});
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
document()->setPlainText(room->mainCache()->savedText());
m_room->mainCache()->setText(room->mainCache()->savedText());
}
}
Q_EMIT roomChanged();
}
ChatBarCache *ChatDocumentHandler::chatBarCache() const
bool ChatDocumentHandler::isEmpty() const
{
if (!m_room || m_type == ChatBarType::None) {
return nullptr;
return htmlText().length() == 0;
}
bool ChatDocumentHandler::atFirstLine() const
{
const auto cursor = textCursor();
if (cursor.isNull()) {
return false;
}
return cursor.blockNumber() == 0 && cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == 0;
}
bool ChatDocumentHandler::atLastLine() const
{
const auto cursor = textCursor();
const auto doc = document();
if (cursor.isNull() || !doc) {
return false;
}
return cursor.blockNumber() == doc->blockCount() - 1
&& cursor.block().layout()->lineForTextPosition(cursor.positionInBlock()).lineNumber() == (cursor.block().layout()->lineCount() - 1);
}
void ChatDocumentHandler::setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition)
{
const auto doc = document();
const auto item = textItem();
if (!doc || !item) {
return;
}
item->forceActiveFocus();
if (!previousDocumentHandler) {
const auto docLastBlockLayout = doc->lastBlock().layout();
item->setProperty("cursorPosition", infront ? defaultPosition : docLastBlockLayout->lineAt(docLastBlockLayout->lineCount() - 1).textStart());
item->setProperty("cursorVisible", true);
return;
}
const auto previousLinePosition = previousDocumentHandler->cursorPositionInLine();
const auto newMaxLineLength = lineLength(infront ? 0 : lineCount() - 1);
item->setProperty("cursorPosition",
std::min(previousLinePosition, newMaxLineLength ? *newMaxLineLength : defaultPosition) + (infront ? 0 : doc->lastBlock().position()));
item->setProperty("cursorVisible", true);
}
int ChatDocumentHandler::lineCount() const
{
if (const auto doc = document()) {
return doc->lineCount();
}
return 0;
}
std::optional<int> ChatDocumentHandler::lineLength(int lineNumber) const
{
const auto doc = document();
if (!doc || lineNumber < 0 || lineNumber >= doc->lineCount()) {
return std::nullopt;
}
const auto block = doc->findBlockByLineNumber(lineNumber);
const auto lineNumInBlock = lineNumber - block.firstLineNumber();
return block.layout()->lineAt(lineNumInBlock).textLength();
}
int ChatDocumentHandler::cursorPositionInLine() const
{
const auto cursor = textCursor();
if (cursor.isNull()) {
return false;
}
return cursor.positionInBlock();
}
QTextDocumentFragment ChatDocumentHandler::takeFirstBlock()
{
auto cursor = textCursor();
if (cursor.isNull()) {
return {};
}
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::Start);
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
if (document()->blockCount() <= 1) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
}
const auto block = cursor.selection();
cursor.removeSelectedText();
cursor.endEditBlock();
if (document()->characterCount() - 1 <= (m_fixedStartChars.length() + m_fixedEndChars.length())) {
Q_EMIT removeMe(this);
}
return block;
}
void ChatDocumentHandler::fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment)
{
auto cursor = textCursor();
if (cursor.isNull()) {
return;
}
if (cursor.blockNumber() > 0) {
hasBefore = true;
}
auto afterBlock = cursor.blockNumber() < document()->blockCount() - 1;
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::StartOfBlock);
if (!hasBefore) {
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_fixedStartChars.length());
}
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
if (!afterBlock) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, m_fixedEndChars.length());
}
cursor.endEditBlock();
midFragment = cursor.selection();
if (!midFragment.isEmpty()) {
cursor.removeSelectedText();
}
cursor.deletePreviousChar();
if (afterBlock) {
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
afterFragment = cursor.selection();
cursor.removeSelectedText();
}
}
void ChatDocumentHandler::insertFragment(const QTextDocumentFragment fragment, InsertPosition position, bool keepPosition)
{
auto cursor = textCursor();
if (cursor.isNull()) {
return;
}
int currentPosition;
switch (position) {
case Start:
currentPosition = 0;
break;
case End:
currentPosition = document()->characterCount() - 1;
break;
case Cursor:
currentPosition = cursor.position();
break;
}
cursor.setPosition(currentPosition);
if (textFormat() && textFormat() == Qt::PlainText) {
const auto wasEmpty = isEmpty();
auto text = fragment.toPlainText();
while (text.startsWith(u"\n"_s)) {
text.removeFirst();
}
while (text.endsWith(u"\n"_s)) {
text.removeLast();
}
cursor.insertText(fragment.toPlainText());
if (wasEmpty) {
cursor.movePosition(QTextCursor::StartOfBlock);
cursor.deletePreviousChar();
cursor.movePosition(QTextCursor::EndOfBlock);
cursor.deleteChar();
}
} else {
cursor.insertMarkdown(trim(fragment.toMarkdown()));
}
if (keepPosition) {
cursor.setPosition(currentPosition);
}
if (textItem()) {
textItem()->setProperty("cursorPosition", cursor.position());
}
return m_room->cacheForType(m_type);
}
void ChatDocumentHandler::complete(int index)
@@ -436,7 +672,7 @@ void ChatDocumentHandler::setStrikethrough(bool strikethrough)
QTextCharFormat format;
format.setFontStrikeOut(strikethrough);
mergeFormatOnWordOrSelection(format);
Q_EMIT underlineChanged();
Q_EMIT formatChanged();
}
void ChatDocumentHandler::setTextColor(const QColor &color)
@@ -478,7 +714,7 @@ void ChatDocumentHandler::setBold(bool bold)
QTextCharFormat format;
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
mergeFormatOnWordOrSelection(format);
Q_EMIT boldChanged();
Q_EMIT formatChanged();
}
bool ChatDocumentHandler::italic() const
@@ -494,7 +730,7 @@ void ChatDocumentHandler::setItalic(bool italic)
QTextCharFormat format;
format.setFontItalic(italic);
mergeFormatOnWordOrSelection(format);
Q_EMIT italicChanged();
Q_EMIT formatChanged();
}
bool ChatDocumentHandler::underline() const
@@ -510,7 +746,7 @@ void ChatDocumentHandler::setUnderline(bool underline)
QTextCharFormat format;
format.setFontUnderline(underline);
mergeFormatOnWordOrSelection(format);
Q_EMIT underlineChanged();
Q_EMIT formatChanged();
}
bool ChatDocumentHandler::strikethrough() const
@@ -549,11 +785,11 @@ QColor ChatDocumentHandler::textColor() const
QTextCursor ChatDocumentHandler::textCursor() const
{
QTextDocument *doc = document();
if (!doc)
if (!document()) {
return QTextCursor();
}
QTextCursor cursor = QTextCursor(doc);
QTextCursor cursor = QTextCursor(document());
if (selectionStart() != selectionEnd()) {
cursor.setPosition(selectionStart());
cursor.setPosition(selectionEnd(), QTextCursor::KeepAnchor);
@@ -563,6 +799,15 @@ QTextCursor ChatDocumentHandler::textCursor() const
return cursor;
}
std::optional<Qt::TextFormat> ChatDocumentHandler::textFormat() const
{
if (!m_textItem) {
return std::nullopt;
}
return static_cast<Qt::TextFormat>(m_textItem->property("textFormat").toInt());
}
void ChatDocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
{
QTextCursor cursor = textCursor();
@@ -747,11 +992,6 @@ void ChatDocumentHandler::regenerateColorScheme()
// TODO update existing link
}
int ChatDocumentHandler::currentHeadingLevel() const
{
return textCursor().blockFormat().headingLevel();
}
void ChatDocumentHandler::indentListMore()
{
m_nestedListHelper.handleOnIndentMore(textCursor());
@@ -768,22 +1008,46 @@ void ChatDocumentHandler::setListStyle(int styleIndex)
Q_EMIT currentListStyleChanged();
}
void ChatDocumentHandler::setHeadingLevel(int level)
bool ChatDocumentHandler::canIndentList() const
{
const int boundedLevel = qBound(0, 6, level);
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
bool ChatDocumentHandler::canDedentList() const
{
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
int ChatDocumentHandler::currentListStyle() const
{
if (!textCursor().currentList()) {
return 0;
}
return -textCursor().currentList()->format().style();
}
ChatDocumentHandler::Style ChatDocumentHandler::style() const
{
return static_cast<Style>(textCursor().blockFormat().headingLevel());
}
void ChatDocumentHandler::setStyle(ChatDocumentHandler::Style style)
{
const int headingLevel = style <= 6 ? style : 0;
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
// level=2 look the same
const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0;
QTextCursor cursor = textCursor();
cursor.beginEditBlock();
QTextBlockFormat blkfmt;
blkfmt.setHeadingLevel(boundedLevel);
blkfmt.setHeadingLevel(headingLevel);
cursor.mergeBlockFormat(blkfmt);
QTextCharFormat chrfmt;
chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
chrfmt.setFontWeight(headingLevel > 0 ? QFont::Bold : QFont::Normal);
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
// Applying style to the current line or selection
QTextCursor selectCursor = cursor;
@@ -805,28 +1069,8 @@ void ChatDocumentHandler::setHeadingLevel(int level)
cursor.mergeBlockCharFormat(chrfmt);
cursor.endEditBlock();
// richTextComposer()->setTextCursor(cursor);
// richTextComposer()->setFocus();
// richTextComposer()->activateRichText();
}
bool ChatDocumentHandler::canIndentList() const
{
return m_nestedListHelper.canIndent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
bool ChatDocumentHandler::canDedentList() const
{
return m_nestedListHelper.canDedent(textCursor()) && textCursor().blockFormat().headingLevel() == 0;
}
int ChatDocumentHandler::currentListStyle() const
{
if (!textCursor().currentList()) {
return 0;
}
return -textCursor().currentList()->format().style();
Q_EMIT styleChanged();
}
int ChatDocumentHandler::fontSize() const
@@ -857,6 +1101,38 @@ QUrl ChatDocumentHandler::fileUrl() const
return m_fileUrl;
}
void ChatDocumentHandler::deleteChar()
{
QTextCursor cursor = textCursor();
if (cursor.isNull()) {
return;
}
if (cursor.position() >= document()->characterCount() - m_fixedEndChars.length() - 1) {
if (const auto nextHandler = nextDocumentHandler()) {
insertFragment(nextHandler->takeFirstBlock(), Cursor, true);
}
return;
}
cursor.deleteChar();
}
void ChatDocumentHandler::backspace()
{
QTextCursor cursor = textCursor();
if (cursor.isNull()) {
return;
}
if (cursor.position() <= m_fixedStartChars.length()) {
if (const auto previousHandler = previousDocumentHandler()) {
previousHandler->insertFragment(takeFirstBlock(), End, true);
} else {
Q_EMIT unhandledBackspaceAtBeginning(this);
}
return;
}
cursor.deletePreviousChar();
}
void ChatDocumentHandler::insertText(const QString &text)
{
textCursor().insertText(text);
@@ -872,16 +1148,24 @@ void ChatDocumentHandler::dumpHtml()
qWarning() << htmlText();
}
QString ChatDocumentHandler::htmlText()
QString ChatDocumentHandler::htmlText() const
{
auto text = document()->toMarkdown();
while (text.startsWith(u"\n"_s)) {
text.remove(0, 1);
const auto doc = document();
if (!doc) {
return {};
}
while (text.endsWith(u"\n"_s)) {
text.remove(text.size() - 1, text.size());
return trim(doc->toMarkdown());
}
QString ChatDocumentHandler::trim(QString string) const
{
while (string.startsWith(u"\n"_s)) {
string.removeFirst();
}
return text;
while (string.endsWith(u"\n"_s)) {
string.removeLast();
}
return string;
}
#include "moc_chatdocumenthandler.cpp"

View File

@@ -7,6 +7,8 @@
#include <QObject>
#include <QQmlEngine>
#include <QTextCursor>
#include <qnamespace.h>
#include <qtextdocumentfragment.h>
#include "chatbarcache.h"
#include "enums/chatbartype.h"
@@ -71,6 +73,11 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
/**
* @brief The current room that the text document is being handled for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The QML text Item the ChatDocumentHandler is handling.
*/
@@ -85,23 +92,30 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/**
* @brief The current room that the text document is being handled for.
* @brief Whether the cursor is cuurently on the first line.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(bool atFirstLine READ atFirstLine NOTIFY atFirstLineChanged)
/**
* @brief Whether the cursor is cuurently on the last line.
*/
Q_PROPERTY(bool atLastLine READ atLastLine NOTIFY atLastLineChanged)
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontFamilyChanged)
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY underlineChanged)
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY strikethroughChanged)
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY formatChanged)
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY formatChanged)
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY formatChanged)
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY formatChanged)
Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
Q_PROPERTY(ChatDocumentHandler::Style style READ style WRITE setStyle NOTIFY styleChanged)
// Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
// Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
Q_PROPERTY(int currentListStyle READ currentListStyle NOTIFY currentListStyleChanged)
Q_PROPERTY(int currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
// Q_PROPERTY(int currentHeadingLevel READ currentHeadingLevel NOTIFY cursorPositionChanged)
// Q_PROPERTY(bool list READ list WRITE setList NOTIFY listChanged)
@@ -112,18 +126,64 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(QUrl fileUrl READ fileUrl NOTIFY fileUrlChanged)
public:
enum InsertPosition {
Cursor,
Start,
End,
};
/**
* @brief Enum to define available styles.
*
* @note The Paragraph and Heading values are intentially fixed to match heading
* level values returned by QTextBlockFormat::headingLevel().
*
* @sa QTextBlockFormat::headingLevel()
*/
enum Style {
Paragraph = 0,
Heading1 = 1,
Heading2 = 2,
Heading3 = 3,
Heading4 = 4,
Heading5 = 5,
Heading6 = 6,
};
Q_ENUM(Style);
explicit ChatDocumentHandler(QObject *parent = nullptr);
ChatBarType::Type type() const;
void setType(ChatBarType::Type type);
QQuickItem *textItem() const;
void setTextItem(QQuickItem *textItem);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
ChatBarCache *chatBarCache() const;
QQuickItem *textItem() const;
void setTextItem(QQuickItem *textItem);
ChatDocumentHandler *previousDocumentHandler() const;
void setPreviousDocumentHandler(ChatDocumentHandler *previousDocumentHandler);
ChatDocumentHandler *nextDocumentHandler() const;
void setNextDocumentHandler(ChatDocumentHandler *nextDocumentHandler);
QString fixedStartChars() const;
void setFixedStartChars(const QString &chars);
QString fixedEndChars() const;
void setFixedEndChars(const QString &chars);
QString initialText() const;
void setInitialText(const QString &text);
bool isEmpty() const;
bool atFirstLine() const;
bool atLastLine() const;
void setCursorFromDocumentHandler(ChatDocumentHandler *previousDocumentHandler, bool infront, int defaultPosition = 0);
int lineCount() const;
std::optional<int> lineLength(int lineNumber) const;
int cursorPositionInLine() const;
QTextDocumentFragment takeFirstBlock();
void fillFragments(bool &hasBefore, QTextDocumentFragment &midFragment, std::optional<QTextDocumentFragment> &afterFragment);
Q_INVOKABLE void complete(int index);
@@ -145,7 +205,6 @@ public:
bool bold() const;
void setBold(bool bold);
bool italic() const;
void setItalic(bool italic);
@@ -159,7 +218,8 @@ public:
bool canDedentList() const;
int currentListStyle() const;
int currentHeadingLevel() const;
Style style() const;
void setStyle(Style style);
// bool list() const;
// void setList(bool list);
@@ -171,7 +231,10 @@ public:
QString fileType() const;
QUrl fileUrl() const;
Q_INVOKABLE void deleteChar();
Q_INVOKABLE void backspace();
Q_INVOKABLE void insertText(const QString &text);
void insertFragment(const QTextDocumentFragment fragment, InsertPosition position = Cursor, bool keepPosition = false);
Q_INVOKABLE QString currentLinkUrl() const;
Q_INVOKABLE QString currentLinkText() const;
Q_INVOKABLE void updateLink(const QString &linkUrl, const QString &linkText);
@@ -182,16 +245,18 @@ public:
Q_INVOKABLE void indentListMore();
Q_INVOKABLE void setListStyle(int styleIndex);
Q_INVOKABLE void setHeadingLevel(int level);
Q_INVOKABLE void dumpHtml();
Q_INVOKABLE QString htmlText();
Q_INVOKABLE QString htmlText() const;
Q_SIGNALS:
void typeChanged();
void textItemChanged();
void roomChanged();
void atFirstLineChanged();
void atLastLineChanged();
void fontFamilyChanged();
void textColorChanged();
void alignmentChanged();
@@ -205,11 +270,27 @@ Q_SIGNALS:
void fontSizeChanged();
void fileUrlChanged();
void formatChanged();
void styleChanged();
void contentsChanged();
void unhandledBackspaceAtBeginning(ChatDocumentHandler *self);
void removeMe(ChatDocumentHandler *self);
private:
ChatBarType::Type m_type = ChatBarType::None;
QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
QPointer<ChatDocumentHandler> m_previousDocumentHandler;
QPointer<ChatDocumentHandler> m_nextDocumentHandler;
QString m_fixedStartChars = {};
QString m_fixedEndChars = {};
QString m_initialText = {};
void initializeChars();
int completionStartIndex() const;
QPointer<NeoChatRoom> m_room;
@@ -226,6 +307,7 @@ private:
CompletionModel *m_completionModel = nullptr;
QTextCursor textCursor() const;
std::optional<Qt::TextFormat> textFormat() const;
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
void selectLinkText(QTextCursor *cursor) const;
NestedListHelper m_nestedListHelper;
@@ -233,4 +315,9 @@ private:
QColor mLinkColor;
void regenerateColorScheme();
QUrl m_fileUrl;
QString trim(QString string) const;
private Q_SLOTS:
void updateCursor();
};

View File

@@ -0,0 +1,132 @@
// 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 "messagecomponenttype.h"
#include <QMimeDatabase>
#include <Quotient/events/encryptedevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include "events/pollevent.h"
const QList<MessageComponentType::Type> MessageComponentType::textTypes = {
Text,
Code,
Quote,
};
const QList<MessageComponentType::Type> MessageComponentType::fileTypes = {
File,
Image,
Video,
Audio,
};
MessageComponentType::Type MessageComponentType::typeForEvent(const Quotient::RoomEvent &event, bool isInReply)
{
using namespace Quotient;
if (event.isRedacted()) {
return MessageComponentType::Text;
}
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
return MessageComponentType::Verification;
}
switch (e->msgtype()) {
case MessageEventType::Emote:
return MessageComponentType::Text;
case MessageEventType::Notice:
return MessageComponentType::Text;
case MessageEventType::Image:
return MessageComponentType::Image;
case MessageEventType::Audio:
return MessageComponentType::Audio;
case MessageEventType::Video:
return MessageComponentType::Video;
case MessageEventType::Location:
return MessageComponentType::Location;
case MessageEventType::File:
return MessageComponentType::File;
default:
return MessageComponentType::Text;
}
}
if (is<const StickerEvent>(event)) {
return MessageComponentType::Image;
}
if (event.isStateEvent()) {
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return MessageComponentType::LiveLocation;
}
// In the (unlikely) case that this is a reply to a state event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
}
if (is<const EncryptedEvent>(event)) {
return MessageComponentType::Encrypted;
}
if (is<PollStartEvent>(event)) {
const auto pollEvent = eventCast<const PollStartEvent>(&event);
if (pollEvent->isRedacted()) {
return MessageComponentType::Text;
}
return MessageComponentType::Poll;
}
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
}
MessageComponentType::Type MessageComponentType::typeForString(const QString &string)
{
if (string.isEmpty()) {
return Text;
}
if (string.startsWith(u'>')) {
return Quote;
}
if (string.startsWith(u"```"_s) && string.endsWith(u"```"_s)) {
return Code;
}
return Text;
}
MessageComponentType::Type MessageComponentType::typeForTag(const QString &tag)
{
if (tag == u"pre"_s || tag == u"pre"_s) {
return Code;
}
if (tag == u"blockquote"_s) {
return Quote;
}
return Text;
}
MessageComponentType::Type MessageComponentType::typeForPath(const QUrl &path)
{
auto mime = QMimeDatabase().mimeTypeForUrl(path);
if (mime.name().startsWith("image/"_L1)) {
return Image;
} else if (mime.name().startsWith("audio/"_L1)) {
return Audio;
} else if (mime.name().startsWith("video/"_L1)) {
return Video;
}
return File;
}
bool MessageComponentType::isTextType(const MessageComponentType::Type &type)
{
return textTypes.contains(type);
}
bool MessageComponentType::isFileType(const MessageComponentType::Type &type)
{
return fileTypes.contains(type);
}

View File

@@ -6,12 +6,10 @@
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/encryptedevent.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include "events/pollevent.h"
namespace Quotient
{
class RoomEvent;
}
using namespace Qt::StringLiterals;
@@ -74,62 +72,14 @@ public:
*
* @sa Type
*/
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false)
{
using namespace Quotient;
static Type typeForEvent(const Quotient::RoomEvent &event, bool isInReply = false);
if (event.isRedacted()) {
return MessageComponentType::Text;
}
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
if (e->rawMsgtype() == u"m.key.verification.request"_s) {
return MessageComponentType::Verification;
}
switch (e->msgtype()) {
case MessageEventType::Emote:
return MessageComponentType::Text;
case MessageEventType::Notice:
return MessageComponentType::Text;
case MessageEventType::Image:
return MessageComponentType::Image;
case MessageEventType::Audio:
return MessageComponentType::Audio;
case MessageEventType::Video:
return MessageComponentType::Video;
case MessageEventType::Location:
return MessageComponentType::Location;
case MessageEventType::File:
return MessageComponentType::File;
default:
return MessageComponentType::Text;
}
}
if (is<const StickerEvent>(event)) {
return MessageComponentType::Image;
}
if (event.isStateEvent()) {
if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return MessageComponentType::LiveLocation;
}
// In the (unlikely) case that this is a reply to a state event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
}
if (is<const EncryptedEvent>(event)) {
return MessageComponentType::Encrypted;
}
if (is<PollStartEvent>(event)) {
const auto pollEvent = eventCast<const PollStartEvent>(&event);
if (pollEvent->isRedacted()) {
return MessageComponentType::Text;
}
return MessageComponentType::Poll;
}
// In the (unlikely) case that this is a reply to an unusual event, we do want to show something
return isInReply ? MessageComponentType::Text : MessageComponentType::Other;
}
/**
* @brief Return MessageComponentType for the given string.
*
* @sa Type
*/
static Type typeForString(const QString &string);
/**
* @brief Return MessageComponentType for the given html tag.
@@ -138,14 +88,30 @@ public:
*
* @sa Type
*/
static Type typeForTag(const QString &tag)
{
if (tag == u"pre"_s || tag == u"pre"_s) {
return Code;
}
if (tag == u"blockquote"_s) {
return Quote;
}
return Text;
}
static Type typeForTag(const QString &tag);
/**
* @brief Return MessageComponentType for the file with the given path.
*
* @sa Type
*/
static Type typeForPath(const QUrl &path);
/**
* @brief Return if the given MessageComponentType is a text type.
*
* @sa Type
*/
static bool isTextType(const MessageComponentType::Type &type);
/**
* @brief Return if the given MessageComponentType is a file type.
*
* @sa Type
*/
static bool isFileType(const MessageComponentType::Type &type);
private:
static const QList<MessageComponentType::Type> textTypes;
static const QList<MessageComponentType::Type> fileTypes;
};

View File

@@ -8,7 +8,7 @@
struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other;
QString display;
QVariantMap attributes;
QVariantMap attributes = {};
bool operator==(const MessageComponent &right) const
{