Support serializing / deserializing custom emoji as tags.
This commit is contained in:
parent
32cf0968a1
commit
e6b3951b40
3 changed files with 165 additions and 12 deletions
|
|
@ -2052,7 +2052,9 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
|
||||||
const auto processState = [&](State nextState) {
|
const auto processState = [&](State nextState) {
|
||||||
const auto linkChanged = (nextState.link != state.link);
|
const auto linkChanged = (nextState.link != state.link);
|
||||||
if (linkChanged) {
|
if (linkChanged) {
|
||||||
if (IsMentionLink(state.link)) {
|
if (Ui::InputField::IsCustomEmojiLink(state.link)) {
|
||||||
|
closeType(EntityType::CustomEmoji);
|
||||||
|
} else if (IsMentionLink(state.link)) {
|
||||||
closeType(EntityType::MentionName);
|
closeType(EntityType::MentionName);
|
||||||
} else {
|
} else {
|
||||||
closeType(EntityType::CustomUrl);
|
closeType(EntityType::CustomUrl);
|
||||||
|
|
@ -2064,7 +2066,13 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (linkChanged && !nextState.link.isEmpty()) {
|
if (linkChanged && !nextState.link.isEmpty()) {
|
||||||
if (IsMentionLink(nextState.link)) {
|
if (Ui::InputField::IsCustomEmojiLink(nextState.link)) {
|
||||||
|
const auto data = Ui::InputField::CustomEmojiEntityData(
|
||||||
|
nextState.link);
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
openType(EntityType::CustomEmoji, data);
|
||||||
|
}
|
||||||
|
} else if (IsMentionLink(nextState.link)) {
|
||||||
const auto match = qthelp::regex_match(
|
const auto match = qthelp::regex_match(
|
||||||
"^(\\d+\\.\\d+)(/|$)",
|
"^(\\d+\\.\\d+)(/|$)",
|
||||||
base::StringViewMid(nextState.link, kMentionTagStart.size()));
|
base::StringViewMid(nextState.link, kMentionTagStart.size()));
|
||||||
|
|
@ -2175,6 +2183,9 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
|
||||||
push(url);
|
push(url);
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
case EntityType::CustomEmoji: {
|
||||||
|
push(Ui::InputField::CustomEmojiLink(entity.data()));
|
||||||
|
} break;
|
||||||
case EntityType::Bold: push(Ui::InputField::kTagBold); break;
|
case EntityType::Bold: push(Ui::InputField::kTagBold); break;
|
||||||
//case EntityType::Semibold: // Semibold is for UI parts only.
|
//case EntityType::Semibold: // Semibold is for UI parts only.
|
||||||
// push(Ui::InputField::kTagSemibold);
|
// push(Ui::InputField::kTagSemibold);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,9 @@
|
||||||
#include "base/random.h"
|
#include "base/random.h"
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
#include "emoji_suggestions_helper.h"
|
#include "emoji_suggestions_helper.h"
|
||||||
#include "styles/palette.h"
|
#include "base/qthelp_regex.h"
|
||||||
#include "base/qt/qt_common_adapters.h"
|
#include "base/qt/qt_common_adapters.h"
|
||||||
|
#include "styles/palette.h"
|
||||||
|
|
||||||
#include <QtWidgets/QCommonStyle>
|
#include <QtWidgets/QCommonStyle>
|
||||||
#include <QtWidgets/QScrollBar>
|
#include <QtWidgets/QScrollBar>
|
||||||
|
|
@ -34,6 +35,9 @@ constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1;
|
||||||
constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
|
constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
|
||||||
constexpr auto kReplaceTagId = QTextFormat::UserProperty + 3;
|
constexpr auto kReplaceTagId = QTextFormat::UserProperty + 3;
|
||||||
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
|
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
|
||||||
|
constexpr auto kCustomEmojiFormat = QTextFormat::UserObject + 1;
|
||||||
|
constexpr auto kCustomEmojiText = QTextFormat::UserProperty + 5;
|
||||||
|
constexpr auto kCustomEmojiLink = QTextFormat::UserProperty + 6;
|
||||||
const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
|
const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
|
||||||
const auto kObjectReplacement = QString::fromRawData(
|
const auto kObjectReplacement = QString::fromRawData(
|
||||||
&kObjectReplacementCh,
|
&kObjectReplacementCh,
|
||||||
|
|
@ -45,13 +49,17 @@ const auto &kTagStrikeOut = InputField::kTagStrikeOut;
|
||||||
const auto &kTagCode = InputField::kTagCode;
|
const auto &kTagCode = InputField::kTagCode;
|
||||||
const auto &kTagPre = InputField::kTagPre;
|
const auto &kTagPre = InputField::kTagPre;
|
||||||
const auto &kTagSpoiler = InputField::kTagSpoiler;
|
const auto &kTagSpoiler = InputField::kTagSpoiler;
|
||||||
const auto kTagCheckLinkMeta = QString("^:/:/:^");
|
const auto kTagCheckLinkMeta = u"^:/:/:^"_q;
|
||||||
const auto kNewlineChars = QString("\r\n")
|
const auto kNewlineChars = QString("\r\n")
|
||||||
+ QChar(0xfdd0) // QTextBeginningOfFrame
|
+ QChar(0xfdd0) // QTextBeginningOfFrame
|
||||||
+ QChar(0xfdd1) // QTextEndOfFrame
|
+ QChar(0xfdd1) // QTextEndOfFrame
|
||||||
+ QChar(QChar::ParagraphSeparator)
|
+ QChar(QChar::ParagraphSeparator)
|
||||||
+ QChar(QChar::LineSeparator);
|
+ QChar(QChar::LineSeparator);
|
||||||
|
|
||||||
|
// We need unique tags otherwise same custom emoji would join in a single
|
||||||
|
// QTextCharFormat with the same properties, including kCustomEmojiText.
|
||||||
|
auto GlobalCustomEmojiCounter = 0;
|
||||||
|
|
||||||
class InputDocument : public QTextDocument {
|
class InputDocument : public QTextDocument {
|
||||||
public:
|
public:
|
||||||
InputDocument(QObject *parent, const style::InputField &st);
|
InputDocument(QObject *parent, const style::InputField &st);
|
||||||
|
|
@ -105,6 +113,20 @@ bool IsNewline(QChar ch) {
|
||||||
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
|
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsCustomEmojiLink(QStringView link) {
|
||||||
|
return link.startsWith(Ui::InputField::kCustomEmojiTagStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString MakeUniqueCustomEmojiLink(QStringView link) {
|
||||||
|
if (!IsCustomEmojiLink(link)) {
|
||||||
|
return link.toString();
|
||||||
|
}
|
||||||
|
const auto index = link.indexOf('?');
|
||||||
|
return u"%1?%2"_q
|
||||||
|
.arg((index < 0) ? link : base::StringViewMid(link, 0, index))
|
||||||
|
.arg(++GlobalCustomEmojiCounter);
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString CheckFullTextTag(
|
[[nodiscard]] QString CheckFullTextTag(
|
||||||
const TextWithTags &textWithTags,
|
const TextWithTags &textWithTags,
|
||||||
const QString &tag) {
|
const QString &tag) {
|
||||||
|
|
@ -703,8 +725,15 @@ QTextCharFormat PrepareTagFormat(
|
||||||
auto font = st.font;
|
auto font = st.font;
|
||||||
auto color = std::optional<style::color>();
|
auto color = std::optional<style::color>();
|
||||||
auto bg = std::optional<style::color>();
|
auto bg = std::optional<style::color>();
|
||||||
|
auto replaceWhat = QString();
|
||||||
|
auto replaceWith = QString();
|
||||||
const auto applyOne = [&](QStringView tag) {
|
const auto applyOne = [&](QStringView tag) {
|
||||||
if (IsValidMarkdownLink(tag)) {
|
if (IsCustomEmojiLink(tag)) {
|
||||||
|
replaceWhat = tag.toString();
|
||||||
|
replaceWith = MakeUniqueCustomEmojiLink(tag);
|
||||||
|
result.setObjectType(kCustomEmojiFormat);
|
||||||
|
result.setProperty(kCustomEmojiLink, replaceWith);
|
||||||
|
} else if (IsValidMarkdownLink(tag)) {
|
||||||
color = st::defaultTextPalette.linkFg;
|
color = st::defaultTextPalette.linkFg;
|
||||||
} else if (tag == kTagBold) {
|
} else if (tag == kTagBold) {
|
||||||
font = font->bold();
|
font = font->bold();
|
||||||
|
|
@ -726,7 +755,11 @@ QTextCharFormat PrepareTagFormat(
|
||||||
}
|
}
|
||||||
result.setFont(font);
|
result.setFont(font);
|
||||||
result.setForeground(color.value_or(st.textFg));
|
result.setForeground(color.value_or(st.textFg));
|
||||||
result.setProperty(kTagProperty, tag);
|
result.setProperty(
|
||||||
|
kTagProperty,
|
||||||
|
(replaceWhat.isEmpty()
|
||||||
|
? tag
|
||||||
|
: std::move(tag).replace(replaceWhat, replaceWith)));
|
||||||
if (bg) {
|
if (bg) {
|
||||||
result.setBackground(*bg);
|
result.setBackground(*bg);
|
||||||
}
|
}
|
||||||
|
|
@ -772,7 +805,6 @@ int ProcessInsertedTags(
|
||||||
QTextCursor c(document);
|
QTextCursor c(document);
|
||||||
c.setPosition(tagFrom);
|
c.setPosition(tagFrom);
|
||||||
c.setPosition(tagTo, QTextCursor::KeepAnchor);
|
c.setPosition(tagTo, QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
c.mergeCharFormat(PrepareTagFormat(st, tagId));
|
c.mergeCharFormat(PrepareTagFormat(st, tagId));
|
||||||
|
|
||||||
applyNoTagFrom = tagTo;
|
applyNoTagFrom = tagTo;
|
||||||
|
|
@ -824,6 +856,7 @@ struct FormattingAction {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
Invalid,
|
Invalid,
|
||||||
InsertEmoji,
|
InsertEmoji,
|
||||||
|
InsertCustomEmoji,
|
||||||
TildeFont,
|
TildeFont,
|
||||||
RemoveTag,
|
RemoveTag,
|
||||||
RemoveNewline,
|
RemoveNewline,
|
||||||
|
|
@ -834,6 +867,8 @@ struct FormattingAction {
|
||||||
EmojiPtr emoji = nullptr;
|
EmojiPtr emoji = nullptr;
|
||||||
bool isTilde = false;
|
bool isTilde = false;
|
||||||
QString tildeTag;
|
QString tildeTag;
|
||||||
|
QString customEmojiText;
|
||||||
|
QString customEmojiLink;
|
||||||
int intervalStart = 0;
|
int intervalStart = 0;
|
||||||
int intervalEnd = 0;
|
int intervalEnd = 0;
|
||||||
|
|
||||||
|
|
@ -850,6 +885,7 @@ const QString InputField::kTagStrikeOut = QStringLiteral("~~");
|
||||||
const QString InputField::kTagCode = QStringLiteral("`");
|
const QString InputField::kTagCode = QStringLiteral("`");
|
||||||
const QString InputField::kTagPre = QStringLiteral("```");
|
const QString InputField::kTagPre = QStringLiteral("```");
|
||||||
const QString InputField::kTagSpoiler = QStringLiteral("||");
|
const QString InputField::kTagSpoiler = QStringLiteral("||");
|
||||||
|
const QString InputField::kCustomEmojiTagStart = u"custom-emoji://"_q;
|
||||||
|
|
||||||
class InputField::Inner final : public QTextEdit {
|
class InputField::Inner final : public QTextEdit {
|
||||||
public:
|
public:
|
||||||
|
|
@ -904,6 +940,20 @@ void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) {
|
||||||
cursor.insertText(kObjectReplacement, format);
|
cursor.insertText(kObjectReplacement, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InsertCustomEmojiAtCursor(
|
||||||
|
QTextCursor cursor,
|
||||||
|
const QString &text,
|
||||||
|
const QString &link) {
|
||||||
|
const auto currentFormat = cursor.charFormat();
|
||||||
|
auto format = QTextCharFormat();
|
||||||
|
format.setObjectType(kCustomEmojiFormat);
|
||||||
|
format.setProperty(kCustomEmojiText, text);
|
||||||
|
format.setProperty(kCustomEmojiLink, MakeUniqueCustomEmojiLink(link));
|
||||||
|
format.setVerticalAlignment(QTextCharFormat::AlignBottom);
|
||||||
|
ApplyTagFormat(format, currentFormat);
|
||||||
|
cursor.insertText(kObjectReplacement, format);
|
||||||
|
}
|
||||||
|
|
||||||
void InstantReplaces::add(const QString &what, const QString &with) {
|
void InstantReplaces::add(const QString &what, const QString &with) {
|
||||||
auto node = &reverseMap;
|
auto node = &reverseMap;
|
||||||
for (auto i = what.end(), b = what.begin(); i != b;) {
|
for (auto i = what.end(), b = what.begin(); i != b;) {
|
||||||
|
|
@ -1265,6 +1315,30 @@ void FlatInput::onTextChange(const QString &text) {
|
||||||
Integration::Instance().textActionsUpdated();
|
Integration::Instance().textActionsUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomEmojiObject::CustomEmojiObject(QObject *parent) : QObject(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
QSizeF CustomEmojiObject::intrinsicSize(
|
||||||
|
QTextDocument *doc,
|
||||||
|
int posInDocument,
|
||||||
|
const QTextFormat &format) {
|
||||||
|
const auto factor = style::DevicePixelRatio() * 1.;
|
||||||
|
const auto size = Emoji::GetSizeNormal() / factor;
|
||||||
|
const auto width = size + st::emojiPadding * 2.;
|
||||||
|
const auto font = format.toCharFormat().font();
|
||||||
|
const auto height = std::max(QFontMetrics(font).height() * 1., size);
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomEmojiObject::drawObject(
|
||||||
|
QPainter *painter,
|
||||||
|
const QRectF &rect,
|
||||||
|
QTextDocument *doc,
|
||||||
|
int posInDocument,
|
||||||
|
const QTextFormat &format) {
|
||||||
|
painter->fillRect(rect, QColor(0, 128, 0, 128));
|
||||||
|
}
|
||||||
|
|
||||||
InputField::InputField(
|
InputField::InputField(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
const style::InputField &st,
|
const style::InputField &st,
|
||||||
|
|
@ -1314,6 +1388,10 @@ InputField::InputField(
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_inner->document()->documentLayout()->registerHandler(
|
||||||
|
kCustomEmojiFormat,
|
||||||
|
new CustomEmojiObject(this));
|
||||||
|
|
||||||
_inner->setFont(_st.font->f);
|
_inner->setFont(_st.font->f);
|
||||||
_inner->setAlignment(_st.textAlign);
|
_inner->setAlignment(_st.textAlign);
|
||||||
if (_mode == Mode::SingleLine) {
|
if (_mode == Mode::SingleLine) {
|
||||||
|
|
@ -1940,7 +2018,7 @@ QString InputField::getTextPart(
|
||||||
return emoji->text();
|
return emoji->text();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return QString();
|
return format.property(kCustomEmojiText).toString();
|
||||||
}();
|
}();
|
||||||
auto text = [&] {
|
auto text = [&] {
|
||||||
const auto result = fragment.text();
|
const auto result = fragment.text();
|
||||||
|
|
@ -2086,6 +2164,20 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
|
||||||
auto *textStart = fragmentText.constData();
|
auto *textStart = fragmentText.constData();
|
||||||
auto *textEnd = textStart + fragmentText.size();
|
auto *textEnd = textStart + fragmentText.size();
|
||||||
|
|
||||||
|
if (format.objectType() == kCustomEmojiFormat) {
|
||||||
|
if (fragmentText == kObjectReplacement) {
|
||||||
|
checkedTill = fragmentEnd;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
action.type = ActionType::InsertCustomEmoji;
|
||||||
|
action.intervalStart = fragmentPosition;
|
||||||
|
action.intervalEnd = fragmentPosition
|
||||||
|
+ fragmentText.size();
|
||||||
|
action.customEmojiText = fragmentText;
|
||||||
|
action.customEmojiLink = format.property(
|
||||||
|
kCustomEmojiLink).toString();
|
||||||
|
}
|
||||||
|
|
||||||
const auto with = format.property(kInstantReplaceWithId);
|
const auto with = format.property(kInstantReplaceWithId);
|
||||||
if (with.isValid()) {
|
if (with.isValid()) {
|
||||||
const auto string = with.toString();
|
const auto string = with.toString();
|
||||||
|
|
@ -2205,8 +2297,16 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
|
||||||
auto cursor = QTextCursor(document);
|
auto cursor = QTextCursor(document);
|
||||||
cursor.setPosition(action.intervalStart);
|
cursor.setPosition(action.intervalStart);
|
||||||
cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
|
cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
|
||||||
|
if (action.type == ActionType::InsertEmoji
|
||||||
|
|| action.type == ActionType::InsertCustomEmoji) {
|
||||||
if (action.type == ActionType::InsertEmoji) {
|
if (action.type == ActionType::InsertEmoji) {
|
||||||
InsertEmojiAtCursor(cursor, action.emoji);
|
InsertEmojiAtCursor(cursor, action.emoji);
|
||||||
|
} else {
|
||||||
|
InsertCustomEmojiAtCursor(
|
||||||
|
cursor,
|
||||||
|
action.customEmojiText,
|
||||||
|
action.customEmojiLink);
|
||||||
|
}
|
||||||
insertPosition = action.intervalStart + 1;
|
insertPosition = action.intervalStart + 1;
|
||||||
if (insertEnd >= action.intervalEnd) {
|
if (insertEnd >= action.intervalEnd) {
|
||||||
insertEnd -= action.intervalEnd
|
insertEnd -= action.intervalEnd
|
||||||
|
|
@ -3348,6 +3448,23 @@ bool InputField::IsValidMarkdownLink(QStringView link) {
|
||||||
return ::Ui::IsValidMarkdownLink(link);
|
return ::Ui::IsValidMarkdownLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool InputField::IsCustomEmojiLink(QStringView link) {
|
||||||
|
return ::Ui::IsCustomEmojiLink(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputField::CustomEmojiLink(QStringView entityData) {
|
||||||
|
return MakeUniqueCustomEmojiLink(u"%1%2"_q
|
||||||
|
.arg(kCustomEmojiTagStart)
|
||||||
|
.arg(entityData));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputField::CustomEmojiEntityData(QStringView link) {
|
||||||
|
const auto match = qthelp::regex_match(
|
||||||
|
"^(\\d+\\.\\d+/\\d+)(\\?|$)",
|
||||||
|
base::StringViewMid(link, kCustomEmojiTagStart.size()));
|
||||||
|
return match ? match->captured(1) : QString();
|
||||||
|
}
|
||||||
|
|
||||||
void InputField::commitMarkdownLinkEdit(
|
void InputField::commitMarkdownLinkEdit(
|
||||||
EditLinkSelection selection,
|
EditLinkSelection selection,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include <QContextMenuEvent>
|
#include <QContextMenuEvent>
|
||||||
#include <QtWidgets/QLineEdit>
|
#include <QtWidgets/QLineEdit>
|
||||||
#include <QtWidgets/QTextEdit>
|
#include <QtWidgets/QTextEdit>
|
||||||
|
#include <QtGui/QTextObjectInterface>
|
||||||
#include <QtCore/QTimer>
|
#include <QtCore/QTimer>
|
||||||
|
|
||||||
#include <rpl/variable.h>
|
#include <rpl/variable.h>
|
||||||
|
|
@ -143,6 +144,26 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CustomEmojiObject : public QObject, public QTextObjectInterface {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_INTERFACES(QTextObjectInterface)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CustomEmojiObject(QObject *parent);
|
||||||
|
|
||||||
|
QSizeF intrinsicSize(
|
||||||
|
QTextDocument *doc,
|
||||||
|
int posInDocument,
|
||||||
|
const QTextFormat &format) override;
|
||||||
|
void drawObject(
|
||||||
|
QPainter *painter,
|
||||||
|
const QRectF &rect,
|
||||||
|
QTextDocument *doc,
|
||||||
|
int posInDocument,
|
||||||
|
const QTextFormat &format) override;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
class InputField : public RpWidget {
|
class InputField : public RpWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
|
@ -173,6 +194,7 @@ public:
|
||||||
static const QString kTagCode;
|
static const QString kTagCode;
|
||||||
static const QString kTagPre;
|
static const QString kTagPre;
|
||||||
static const QString kTagSpoiler;
|
static const QString kTagSpoiler;
|
||||||
|
static const QString kCustomEmojiTagStart;
|
||||||
|
|
||||||
InputField(
|
InputField(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
|
|
@ -261,7 +283,10 @@ public:
|
||||||
EditLinkSelection selection,
|
EditLinkSelection selection,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
const QString &link);
|
const QString &link);
|
||||||
static bool IsValidMarkdownLink(QStringView link);
|
[[nodiscard]] static bool IsValidMarkdownLink(QStringView link);
|
||||||
|
[[nodiscard]] static bool IsCustomEmojiLink(QStringView link);
|
||||||
|
[[nodiscard]] static QString CustomEmojiLink(QStringView entityData);
|
||||||
|
[[nodiscard]] static QString CustomEmojiEntityData(QStringView link);
|
||||||
|
|
||||||
const QString &getLastText() const {
|
const QString &getLastText() const {
|
||||||
return _lastTextWithTags.text;
|
return _lastTextWithTags.text;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue