Apply all the required styling in cpp

Apply all the required styling to show links, table, spoilers, etc in cpp. This also updates the method of revealing spoilers so now you can click to reveal then click again to hide.
This commit is contained in:
James Graham
2025-08-20 17:10:44 +01:00
parent 4498d4457b
commit 0d63fce59a
9 changed files with 297 additions and 107 deletions

View File

@@ -34,6 +34,10 @@ private Q_SLOTS:
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void addStyle_data();
void addStyle();
void dontAddStyle_data();
void dontAddStyle();
void sendSimpleStringCase();
void sendSingleParaMarkup();
@@ -71,6 +75,9 @@ private Q_SLOTS:
void componentOutput_data();
void componentOutput();
void updateSpoiler_data();
void updateSpoiler();
};
void TextHandlerTest::initTestCase()
@@ -89,21 +96,26 @@ void TextHandlerTest::initTestCase()
void TextHandlerTest::allowedAttributes()
{
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1S = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1R = u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
// Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2S = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2R =
u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a><a href='https://kde.org' style=\"text-decoration: none;\">link</a>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R);
}
void TextHandlerTest::stripDisallowedTags()
@@ -146,6 +158,56 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::addStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\"><tr><th style=\"border: 1px solid black; padding: 3px;\">Company</th><th style=\"border: 1px solid black; padding: 3px;\">Contact</th><th style=\"border: 1px solid black; padding: 3px;\">Country</th></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Alfreds Futterkiste</td><td style=\"border: 1px solid black; padding: 3px;\">Maria Anders</td><td style=\"border: 1px solid black; padding: 3px;\">Germany</td></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Centro comercial Moctezuma</td><td style=\"border: 1px solid black; padding: 3px;\">Francisco Chang</td><td style=\"border: 1px solid black; padding: 3px;\">Mexico</td></tr></table>"_s;
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
}
void TextHandlerTest::addStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::dontAddStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s;
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
}
void TextHandlerTest::dontAddStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = u"This data should just be left alone."_s;
@@ -338,7 +400,8 @@ void TextHandlerTest::receiveRichInPlainOut()
void TextHandlerTest::receivePlainTextIn()
{
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
const QString testOutputStringRich = u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>."_s;
const QString testOutputStringRich =
u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
// Make sure quotes are maintained in a plain string.
@@ -408,7 +471,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill()
{
const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s;
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>"_s;
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\" style=\"text-decoration: none;\">@alice:example.org</a></b>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -460,21 +523,23 @@ void TextHandlerTest::receiveRichPlainUrl_data()
// so we can confirm consistent behaviour for complex urls.
QTest::addRow("link 1")
<< u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s;
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">Link already rich</a>"_s;
// Another real case. The linkification wasn't handling it when a single link
// contains what looks like and email. It was broken into 3 but needs to
// be just single link.
QTest::addRow("link 2")
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\" style=\"text-decoration: none;\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
QTest::addRow("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< uR"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)"_s;
QTest::addRow("email")
<< uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< uR"(<a href="mailto:email@example.com" style="text-decoration: none;">email@example.com</a> <a href="mailto:email@example.com" style="text-decoration: none;">Link already rich</a>)"_s;
QTest::addRow("mxid")
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>"_s;
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s << u"a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b"_s;
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">Link already rich</a></b>"_s;
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;
}
/**
@@ -596,5 +661,35 @@ void TextHandlerTest::componentOutput()
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents);
}
void TextHandlerTest::updateSpoiler_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::addColumn<bool>("spoilerRevealed");
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("same length") << u"<span data-mx-spoiler style=\"color: #123456; background: #123456;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("different length") << u"<span data-mx-spoiler style=\"color: short; background: looooooooooong;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("spoiler revealed")
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(theme->alternateBackgroundColor().name())
<< u"<span data-mx-spoiler style=\"color: %1; background: %2;\">Test<span>"_s.arg(theme->textColor().name(), theme->alternateBackgroundColor().name())
<< true;
}
void TextHandlerTest::updateSpoiler()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
QFETCH(bool, spoilerRevealed);
QCOMPARE(TextHandler::updateSpoilerText(this, testInputString, spoilerRevealed), testOutputString);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

View File

@@ -93,8 +93,12 @@ QString TextHandler::handleSendText()
return outputString;
}
QString
TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines, bool isEdited)
QString TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat,
const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool stripNewlines,
bool isEdited,
bool spoilerRevealed)
{
m_pos = 0;
m_dataBuffer = m_data;
@@ -151,7 +155,7 @@ TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat, const NeoChatRoom
} else if ((getTagType(m_nextToken) == u"br"_s && stripNewlines)) {
nextTokenBuffer = u' ';
}
nextTokenBuffer = cleanAttributes(getTagType(m_nextToken), nextTokenBuffer);
nextTokenBuffer = cleanAttributes(getTagType(m_nextToken), nextTokenBuffer, true, spoilerRevealed);
}
outputString.append(nextTokenBuffer);
@@ -333,7 +337,8 @@ MessageComponent TextHandler::nextBlock(const QString &string,
Qt::TextFormat inputFormat,
const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool isEdited)
bool isEdited,
bool spoilerRevealed)
{
if (string.isEmpty()) {
return {};
@@ -355,7 +360,11 @@ MessageComponent TextHandler::nextBlock(const QString &string,
content = unescapeHtml(content);
break;
default:
content = handleRecieveRichText(inputFormat, room, event, false, isEdited);
content = handleRecieveRichText(inputFormat, room, event, false, isEdited, spoilerRevealed);
}
if (content.contains(u"data-mx-spoiler"_s)) {
attributes[u"hasSpoiler"_s] = true;
}
return MessageComponent{messageComponentType, content, attributes};
}
@@ -462,8 +471,11 @@ bool TextHandler::isAllowedLink(const QString &link, bool isImg)
}
}
QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString)
QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString, bool addStyle, bool spoilerRevealed)
{
if (!tagString.contains(u'<') || !tagString.contains(u'>')) {
return tagString;
}
int nextAttributeIndex = tagString.indexOf(u' ', 1);
if (nextAttributeIndex != -1) {
@@ -518,11 +530,33 @@ QString TextHandler::cleanAttributes(const QString &tag, const QString &tagStrin
nextAttributeIndex = nextSpaceIndex + 1;
}
outputString += u'>';
return outputString;
return addStyle ? this->addStyle(tag, outputString, spoilerRevealed) : outputString + u'>';
}
return tagString;
return addStyle ? this->addStyle(tag, tagString) : tagString;
}
QString TextHandler::addStyle(const QString &tag, QString cleanTagString, bool spoilerRevealed)
{
if (cleanTagString.endsWith(u'>')) {
cleanTagString.removeLast();
}
if (!cleanTagString.startsWith(u"</"_s)) {
if (tag == u"a"_s) {
cleanTagString += u" style=\"text-decoration: none;\""_s;
} else if (tag == u"table"_s) {
cleanTagString += u" style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\""_s;
} else if (tag == u"th"_s || tag == u"td"_s) {
cleanTagString += u" style=\"border: 1px solid black; padding: 3px;\""_s;
} else if (tag == u"span"_s && cleanTagString.contains(u"data-mx-spoiler"_s)) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
cleanTagString += u" style=\"color: %1; background: %2;\""_s.arg(spoilerRevealed ? theme->highlightedTextColor().name() : u"transparent"_s,
theme->alternateBackgroundColor().name());
}
}
return cleanTagString + u'>';
}
QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagString)
@@ -567,8 +601,12 @@ QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagStr
return attributes;
}
QList<MessageComponent>
TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isEdited)
QList<MessageComponent> TextHandler::textComponents(QString string,
Qt::TextFormat inputFormat,
const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool isEdited,
bool spoilerRevealed)
{
if (string.trimmed().isEmpty()) {
return {MessageComponent{MessageComponentType::Text, i18n("<i>This event does not have any content.</i>"), {}}};
@@ -580,7 +618,8 @@ TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const Ne
QList<MessageComponent> components;
while (!string.isEmpty()) {
const auto nextBlockPos = this->nextBlockPos(string);
const auto nextBlock = this->nextBlock(string, nextBlockPos, inputFormat, room, event, nextBlockPos == string.size() ? isEdited : false);
const auto nextBlock =
this->nextBlock(string, nextBlockPos, inputFormat, room, event, nextBlockPos == string.size() ? isEdited : false, spoilerRevealed);
components += nextBlock;
string.remove(0, nextBlockPos);
@@ -798,4 +837,20 @@ QString TextHandler::convertCodeLanguageString(const QString &languageString)
return languageString.right(languageString.length() - equalsPos - 1);
}
QString TextHandler::updateSpoilerText(QObject *object, QString string, bool spoilerRevealed)
{
auto it = QRegularExpression(u"<span[^>]*data-mx-spoiler[^>]*style=\"color: (.*?); background: (.*?);\">"_s).globalMatch(string);
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(object, true));
int offset = 0;
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
const auto newColor = spoilerRevealed ? theme->textColor().name() : u"transparent"_s;
string.replace(match.capturedStart(2) + offset, match.capturedLength(2), theme->alternateBackgroundColor().name());
string.replace(match.capturedStart(1) + offset, match.capturedLength(1), newColor);
offset = newColor.length() - match.capturedLength(1);
}
return string;
}
#include "moc_texthandler.cpp"

View File

@@ -75,7 +75,8 @@ public:
const NeoChatRoom *room = nullptr,
const Quotient::RoomEvent *event = nullptr,
bool stripNewlines = false,
bool isEdited = false);
bool isEdited = false,
bool spoilerRevealed = false);
/**
* @brief Handle the text as a plain output for a message being received.
@@ -104,7 +105,13 @@ public:
Qt::TextFormat inputFormat = Qt::RichText,
const NeoChatRoom *room = nullptr,
const Quotient::RoomEvent *event = nullptr,
bool isEdited = false);
bool isEdited = false,
bool spoilerRevealed = false);
/**
* @brief Modify the style parameters of the spoilers to reveal or hide the text.
*/
static QString updateSpoilerText(QObject *object, QString string, bool spoilerRevealed);
private:
QString m_data;
@@ -123,7 +130,8 @@ private:
Qt::TextFormat inputFormat = Qt::RichText,
const NeoChatRoom *room = nullptr,
const Quotient::RoomEvent *event = nullptr,
bool isEdited = false);
bool isEdited = false,
bool spoilerRevealed = false);
QString stripBlockTags(QString string, const QString &tagType) const;
QString getTagType(const QString &tagToken) const;
@@ -133,7 +141,8 @@ private:
bool isAllowedTag(const QString &type);
bool isAllowedAttribute(const QString &tag, const QString &attribute);
bool isAllowedLink(const QString &link, bool isImg = false);
QString cleanAttributes(const QString &tag, const QString &tagString);
QString cleanAttributes(const QString &tag, const QString &tagString, bool addStyle = false, bool spoilerRevealed = false);
QString addStyle(const QString &tag, QString cleanTagString, bool spoilerRevealed = false);
QVariantMap getAttributes(const QString &tag, const QString &tagString);
QString markdownToHTML(const QString &markdown);

View File

@@ -16,6 +16,11 @@ import org.kde.neochat
TextEdit {
id: root
/**
* @brief The index of the delegate in the model.
*/
required property int index
/**
* @brief The matrix ID of the message event.
*/
@@ -35,90 +40,28 @@ TextEdit {
*/
required property string display
/**
* @brief The attributes of the component.
*/
required property var componentAttributes
/**
* @brief Whether the message contains a spoiler
*/
readonly property var hasSpoiler: root.componentAttributes?.hasSpoiler ?? false
/**
* @brief Whether this message is replying to another.
*/
property bool isReply: false
/**
* @brief Regex for detecting a message with a spoiler.
*/
readonly property var hasSpoiler: /data-mx-spoiler/g
/**
* @brief Whether a spoiler should be revealed.
*/
property bool spoilerRevealed: !hasSpoiler.test(display)
/**
* @brief The color of spoiler blocks, to be theme-agnostic.
*/
property color spoilerBlockColor: Kirigami.ColorUtils.tintWithAlpha("#232629", Kirigami.Theme.textColor, 0.15)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: Message.maxContentWidth
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
persistentSelection: true
text: "<style>
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
code {
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
blockquote {
margin: 0;
}
blockquote table {
width: 100%;
border-width: 0;
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
blockquote td {
width: 100%;
padding: " + Kirigami.Units.largeSpacing + ";
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
[data-mx-spoiler] a {
background: " + root.spoilerBlockColor + ";
}
[data-mx-spoiler] {
background: " + root.spoilerBlockColor + ";
}
" + (!spoilerRevealed ? "
[data-mx-spoiler] a {
color: transparent;
}
[data-mx-spoiler] {
color: transparent;
}
" : "
[data-mx-spoiler] a {
color: white;
}
[data-mx-spoiler] {
color: white;
}
") + "
</style>" + display
text: display
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
@@ -133,7 +76,6 @@ a{
textFormat: Text.RichText
onLinkActivated: link => {
spoilerRevealed = true;
RoomManager.resolveResource(link, "join");
}
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
@@ -143,11 +85,11 @@ a{
}
HoverHandler {
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
cursorShape: root.hoveredLink || (!(root.componentAttributes?.spoilerRevealed ?? false) && root.hasSpoiler) ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TapHandler {
enabled: !root.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true
enabled: !root.hoveredLink && root.hasSpoiler
onTapped: root.Message.contentModel.toggleSpoiler(root.Message.contentFilterModel.mapToSource(root.Message.contentFilterModel.index(root.index, 0)))
}
TapHandler {
enabled: !root.hoveredLink

View File

@@ -9,6 +9,7 @@
#include "contentprovider.h"
#include "enums/messagecomponenttype.h"
#include "neochatconnection.h"
#include "texthandler.h"
using namespace Quotient;
@@ -17,6 +18,11 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, MessageContentModel
, m_room(room)
, m_eventId(eventId)
{
m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
if (m_theme) {
connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, &MessageContentModel::updateSpoilers);
}
initializeModel();
}
@@ -277,4 +283,40 @@ void MessageContentModel::closeLinkPreview(int row)
}
}
void MessageContentModel::updateSpoilers()
{
for (auto it = m_components.begin(); it != m_components.end(); ++it) {
updateSpoiler(index(it - m_components.begin()));
}
}
void MessageContentModel::updateSpoiler(const QModelIndex &index)
{
const auto row = index.row();
if (row < 0 || row >= rowCount()) {
qWarning() << __FUNCTION__ << "called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
return;
}
const auto spoilerRevealed = m_components[row].attributes.value("spoilerRevealed"_L1, false).toBool();
m_components[row].display = TextHandler::updateSpoilerText(this, m_components[row].display, spoilerRevealed);
Q_EMIT dataChanged(index, index, {DisplayRole});
}
void MessageContentModel::toggleSpoiler(QModelIndex index)
{
const auto row = index.row();
if (row < 0 || row >= rowCount()) {
qWarning() << __FUNCTION__ << "called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
return;
}
if (m_components[row].type != MessageComponentType::Text) {
return;
}
const auto spoilerRevealed = !m_components[row].attributes.value("spoilerRevealed"_L1, false).toBool();
m_components[row].attributes["spoilerRevealed"_L1] = spoilerRevealed;
Q_EMIT dataChanged(index, index, {ComponentAttributesRole});
updateSpoiler(index);
}
#include "moc_messagecontentmodel.cpp"

View File

@@ -7,6 +7,7 @@
#include <QQmlEngine>
#include <QImageReader>
#include <Kirigami/Platform/PlatformTheme>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
@@ -101,6 +102,11 @@ public:
*/
Q_INVOKABLE void closeLinkPreview(int row);
/**
* @brief Toggle spoiler for the component at the given row.
*/
Q_INVOKABLE void toggleSpoiler(QModelIndex index);
Q_SIGNALS:
void authorChanged();
@@ -232,4 +238,8 @@ private:
QList<QUrl> m_removedLinkPreviews;
MessageComponent linkPreviewComponent(const QUrl &link);
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
void updateSpoilers();
void updateSpoiler(const QModelIndex &index);
};

View File

@@ -73,6 +73,8 @@ QQC2.Control {
*/
signal showMessageMenu
Message.contentFilterModel: messageContentFilterModel
contentItem: RowLayout {
Kirigami.Icon {
source: "content-loading-symbolic"
@@ -87,6 +89,7 @@ QQC2.Control {
Repeater {
id: contentRepeater
model: MessageContentFilterModel {
id: messageContentFilterModel
showAuthor: root.showAuthor
sourceModel: root.contentModel
}

View File

@@ -66,6 +66,22 @@ void MessageAttached::setContentModel(MessageContentModel *contentModel)
Q_EMIT contentModelChanged();
}
MessageContentFilterModel *MessageAttached::contentFilterModel() const
{
return m_contentFilterModel;
}
void MessageAttached::setContentFilterModel(MessageContentFilterModel *contentFilterModel)
{
m_explicitContentFilterModel = true;
if (m_contentFilterModel == contentFilterModel) {
return;
}
m_contentFilterModel = contentFilterModel;
propagateMessage(this);
Q_EMIT contentFilterModelChanged();
}
int MessageAttached::index() const
{
return m_index;
@@ -147,6 +163,11 @@ void MessageAttached::propagateMessage(MessageAttached *message)
Q_EMIT contentModelChanged();
}
if (!m_explicitContentFilterModel && m_contentFilterModel != message->contentFilterModel()) {
m_contentFilterModel = message->contentFilterModel();
Q_EMIT contentFilterModelChanged();
}
if (m_explicitIndex || m_index != message->index()) {
m_index = message->index();
Q_EMIT indexChanged();

View File

@@ -7,6 +7,7 @@
#include <QQuickAttachedPropertyPropagator>
#include <QQuickItem>
#include "models/messagecontentfiltermodel.h"
#include "models/messagecontentmodel.h"
#include "neochatroom.h"
@@ -32,6 +33,11 @@ class MessageAttached : public QQuickAttachedPropertyPropagator
*/
Q_PROPERTY(MessageContentModel *contentModel READ contentModel WRITE setContentModel NOTIFY contentModelChanged FINAL)
/**
* @brief The content model for the current message.
*/
Q_PROPERTY(MessageContentFilterModel *contentFilterModel READ contentFilterModel WRITE setContentFilterModel NOTIFY contentFilterModelChanged FINAL)
/**
* @brief The index of the message in the timeline
*/
@@ -66,6 +72,9 @@ public:
MessageContentModel *contentModel() const;
void setContentModel(MessageContentModel *contentModel);
MessageContentFilterModel *contentFilterModel() const;
void setContentFilterModel(MessageContentFilterModel *contentFilterModel);
int index() const;
void setIndex(int index);
@@ -82,6 +91,7 @@ Q_SIGNALS:
void roomChanged();
void timelineChanged();
void contentModelChanged();
void contentFilterModelChanged();
void indexChanged();
void maxContentWidthChanged();
void selectedTextChanged();
@@ -101,6 +111,9 @@ private:
QPointer<MessageContentModel> m_contentModel;
bool m_explicitContentModel = false;
QPointer<MessageContentFilterModel> m_contentFilterModel;
bool m_explicitContentFilterModel = false;
int m_index = -1;
bool m_explicitIndex = false;