Inline Edits

Edit text messages inline instead of in the chatbar
This commit is contained in:
James Graham
2023-02-12 13:46:23 +00:00
parent aaa26571d1
commit 5482aad7ba
14 changed files with 327 additions and 51 deletions

View File

@@ -17,6 +17,7 @@
#include "models/actionsmodel.h" #include "models/actionsmodel.h"
#include "models/customemojimodel.h" #include "models/customemojimodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "roommanager.h" #include "roommanager.h"
@@ -58,9 +59,9 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged(); Q_EMIT roomChanged();
} }
void ActionsHandler::handleMessage() void ActionsHandler::handleNewMessage()
{ {
checkEffects(); checkEffects(m_room->chatBoxText());
if (!m_room->chatBoxAttachmentPath().isEmpty()) { if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath()); QUrl url(m_room->chatBoxAttachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
@@ -69,13 +70,39 @@ void ActionsHandler::handleMessage()
m_room->setChatBoxText({}); m_room->setChatBoxText({});
return; return;
} }
QString handledText = m_room->chatBoxText();
std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool { QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
}
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor(); return a.cursor.anchor() > b.cursor.anchor();
}); });
for (const auto &mention : *m_room->mentions()) { for (const auto &mention : *mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) { if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue; continue;
} }
@@ -83,11 +110,16 @@ void ActionsHandler::handleMessage()
mention.cursor.position() - mention.cursor.anchor(), mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id)); QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
} }
m_room->mentions()->clear(); mentions->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
{
if (NeoChatConfig::allowQuickEdit()) { if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$"); QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
auto match = sed.match(m_room->chatBoxText()); auto match = sed.match(text);
if (match.hasMatch()) { if (match.hasMatch()) {
const QString regex = match.captured(1); const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped(); const QString replacement = match.captured(2).toHtmlEscaped();
@@ -146,13 +178,13 @@ void ActionsHandler::handleMessage()
if (handledText.length() == 0) { if (handledText.length() == 0) {
return; return;
} }
m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId());
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
} }
void ActionsHandler::checkEffects() void ActionsHandler::checkEffects(const QString &text)
{ {
std::optional<QString> effect = std::nullopt; std::optional<QString> effect = std::nullopt;
const auto &text = m_room->chatBoxText();
if (text.contains("\u2744")) { if (text.contains("\u2744")) {
effect = QLatin1String("snowflake"); effect = QLatin1String("snowflake");
} else if (text.contains("\u1F386")) { } else if (text.contains("\u1F386")) {

View File

@@ -33,14 +33,22 @@ Q_SIGNALS:
public Q_SLOTS: public Q_SLOTS:
/// \brief Post a message. /**
/// * @brief Pre-process text and send message.
/// This also interprets commands if any. */
void handleMessage(); void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
private: private:
NeoChatRoom *m_room = nullptr; NeoChatRoom *m_room = nullptr;
void checkEffects(); void checkEffects(const QString &text);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
}; };
QString markdownToHTML(const QString &markdown); QString markdownToHTML(const QString &markdown);

View File

@@ -108,11 +108,16 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
static QPointer<NeoChatRoom> previousRoom = nullptr; static QPointer<NeoChatRoom> previousRoom = nullptr;
if (previousRoom) { if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr); disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
} }
previousRoom = m_room; previousRoom = m_room;
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() { connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex(); int start = completionStartIndex();
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start)); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
}); });
}); });
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() { connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
@@ -123,7 +128,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
return; return;
} }
int start = completionStartIndex(); int start = completionStartIndex();
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start)); m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
}); });
} }
@@ -138,7 +143,7 @@ int ChatDocumentHandler::completionStartIndex() const
#else #else
const auto cursor = cursorPosition(); const auto cursor = cursorPosition();
#endif #endif
const auto &text = m_room->chatBoxText(); const auto &text = getText();
auto start = std::min(cursor, text.size()) - 1; auto start = std::min(cursor, text.size()) - 1;
while (start > -1) { while (start > -1) {
if (text.at(start) == QLatin1Char(' ')) { if (text.at(start) == QLatin1Char(' ')) {
@@ -150,6 +155,20 @@ int ChatDocumentHandler::completionStartIndex() const
return start; return start;
} }
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const QQuickTextDocument *ChatDocumentHandler::document() const
{ {
return m_document; return m_document;
@@ -204,7 +223,7 @@ void ChatDocumentHandler::complete(int index)
if (m_completionModel->autoCompletionType() == CompletionModel::User) { if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString(); auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString(); auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = m_room->chatBoxText(); auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1); auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document()->textDocument());
cursor.setPosition(at); cursor.setPosition(at);
@@ -213,11 +232,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor); cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true); cursor.setKeepPositionOnInsert(true);
m_room->mentions()->push_back({cursor, name, 0, 0, id}); pushMention({cursor, name, 0, 0, id});
m_highlighter->rehighlight(); m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) { } else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString(); auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = m_room->chatBoxText(); auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('/')); auto at = text.lastIndexOf(QLatin1Char('/'));
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document()->textDocument());
cursor.setPosition(at); cursor.setPosition(at);
@@ -225,7 +244,7 @@ void ChatDocumentHandler::complete(int index)
cursor.insertText(QStringLiteral("/%1 ").arg(command)); cursor.insertText(QStringLiteral("/%1 ").arg(command));
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) { } else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString(); auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = m_room->chatBoxText(); auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1); auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document()->textDocument());
cursor.setPosition(at); cursor.setPosition(at);
@@ -234,11 +253,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at); cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor); cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true); cursor.setKeepPositionOnInsert(true);
m_room->mentions()->push_back({cursor, alias, 0, 0, alias}); pushMention({cursor, alias, 0, 0, alias});
m_highlighter->rehighlight(); m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) { } else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString(); auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = m_room->chatBoxText(); auto text = getText();
auto at = text.lastIndexOf(QLatin1Char(':')); auto at = text.lastIndexOf(QLatin1Char(':'));
QTextCursor cursor(document()->textDocument()); QTextCursor cursor(document()->textDocument());
cursor.setPosition(at); cursor.setPosition(at);
@@ -281,3 +300,27 @@ void ChatDocumentHandler::setSelectionEnd(int position)
m_selectionEnd = position; m_selectionEnd = position;
Q_EMIT selectionEndChanged(); Q_EMIT selectionEndChanged();
} }
QString ChatDocumentHandler::getText() const
{
if (!m_room) {
return QString();
}
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
}
}
void ChatDocumentHandler::pushMention(const Mention mention) const
{
if (!m_room) {
return;
}
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
m_room->mentions()->push_back(mention);
}
}

View File

@@ -9,6 +9,7 @@
#include "models/completionmodel.h" #include "models/completionmodel.h"
#include "models/userlistmodel.h" #include "models/userlistmodel.h"
#include "neochatroom.h"
class QTextDocument; class QTextDocument;
class NeoChatRoom; class NeoChatRoom;
@@ -17,6 +18,14 @@ class SyntaxHighlighter;
class ChatDocumentHandler : public QObject class ChatDocumentHandler : public QObject
{ {
Q_OBJECT Q_OBJECT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged) Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged) Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged) Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
@@ -24,11 +33,14 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged) Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged) Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public: public:
explicit ChatDocumentHandler(QObject *parent = nullptr); explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const; [[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document); void setDocument(QQuickTextDocument *document);
@@ -49,6 +61,7 @@ public:
void updateCompletions(); void updateCompletions();
CompletionModel *completionModel() const; CompletionModel *completionModel() const;
Q_SIGNALS: Q_SIGNALS:
void isEditChanged();
void documentChanged(); void documentChanged();
void cursorPositionChanged(); void cursorPositionChanged();
void roomChanged(); void roomChanged();
@@ -59,6 +72,8 @@ Q_SIGNALS:
private: private:
int completionStartIndex() const; int completionStartIndex() const;
bool m_isEdit;
QQuickTextDocument *m_document; QQuickTextDocument *m_document;
NeoChatRoom *m_room = nullptr; NeoChatRoom *m_room = nullptr;
@@ -68,6 +83,9 @@ private:
int m_selectionStart; int m_selectionStart;
int m_selectionEnd; int m_selectionEnd;
QString getText() const;
void pushMention(const Mention mention) const;
SyntaxHighlighter *m_highlighter = nullptr; SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None; CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;

View File

@@ -1626,6 +1626,17 @@ void NeoChatRoom::setChatBoxText(const QString &text)
Q_EMIT chatBoxTextChanged(); Q_EMIT chatBoxTextChanged();
} }
QString NeoChatRoom::editText() const
{
return m_editText;
}
void NeoChatRoom::setEditText(const QString &text)
{
m_editText = text;
Q_EMIT editTextChanged();
}
QString NeoChatRoom::chatBoxReplyId() const QString NeoChatRoom::chatBoxReplyId() const
{ {
return m_chatBoxReplyId; return m_chatBoxReplyId;
@@ -1702,6 +1713,11 @@ QVector<Mention> *NeoChatRoom::mentions()
return &m_mentions; return &m_mentions;
} }
QVector<Mention> *NeoChatRoom::editMentions()
{
return &m_editMentions;
}
QString NeoChatRoom::savedText() const QString NeoChatRoom::savedText() const
{ {
return m_savedText; return m_savedText;

View File

@@ -79,6 +79,11 @@ class NeoChatRoom : public Quotient::Room
// Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms // Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms
Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged) Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged)
/**
* @brief The text for any message currently being edited in the room.
*/
Q_PROPERTY(QString editText READ editText WRITE setEditText NOTIFY editTextChanged)
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged) Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged) Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged) Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
@@ -271,6 +276,9 @@ public:
QString chatBoxText() const; QString chatBoxText() const;
void setChatBoxText(const QString &text); void setChatBoxText(const QString &text);
QString editText() const;
void setEditText(const QString &text);
QString chatBoxReplyId() const; QString chatBoxReplyId() const;
void setChatBoxReplyId(const QString &replyId); void setChatBoxReplyId(const QString &replyId);
@@ -288,6 +296,11 @@ public:
QVector<Mention> *mentions(); QVector<Mention> *mentions();
/**
* @brief Vector of mentions in the current edit text.
*/
QVector<Mention> *editMentions();
QString savedText() const; QString savedText() const;
void setSavedText(const QString &savedText); void setSavedText(const QString &savedText);
@@ -337,10 +350,12 @@ private:
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString()); QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
QString m_chatBoxText; QString m_chatBoxText;
QString m_editText;
QString m_chatBoxReplyId; QString m_chatBoxReplyId;
QString m_chatBoxEditId; QString m_chatBoxEditId;
QString m_chatBoxAttachmentPath; QString m_chatBoxAttachmentPath;
QVector<Mention> m_mentions; QVector<Mention> m_mentions;
QVector<Mention> m_editMentions;
QString m_savedText; QString m_savedText;
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
QCache<QString, PollHandler> m_polls; QCache<QString, PollHandler> m_polls;
@@ -363,6 +378,7 @@ Q_SIGNALS:
void pushNotificationStateChanged(PushNotificationState::State state); void pushNotificationStateChanged(PushNotificationState::State state);
void showMessage(MessageType messageType, const QString &message); void showMessage(MessageType messageType, const QString &message);
void chatBoxTextChanged(); void chatBoxTextChanged();
void editTextChanged();
void chatBoxReplyIdChanged(); void chatBoxReplyIdChanged();
void chatBoxEditIdChanged(); void chatBoxEditIdChanged();
void chatBoxAttachmentPathChanged(); void chatBoxAttachmentPathChanged();

View File

@@ -14,9 +14,7 @@ QQC2.Control {
property alias textField: textField property alias textField: textField
property bool isReplying: currentRoom.chatBoxReplyId.length > 0 property bool isReplying: currentRoom.chatBoxReplyId.length > 0
property bool isEditing: currentRoom.chatBoxEditId.length > 0 property NeoChatUser replyUser: currentRoom.chatBoxReplyUser
property bool replyPaneVisible: isReplying || isEditing
property NeoChatUser replyUser: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser
property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0 property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0
signal messageSent() signal messageSent()
@@ -122,7 +120,7 @@ QQC2.Control {
leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing)
rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width
placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported) readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported)
@@ -198,8 +196,8 @@ QQC2.Control {
anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing) anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
active: visible active: visible
visible: root.replyPaneVisible || root.attachmentPaneVisible visible: root.isReplying || root.attachmentPaneVisible
sourceComponent: root.replyPaneVisible ? replyPane : attachmentPane sourceComponent: root.isReplying ? replyPane : attachmentPane
} }
Component { Component {
id: replyPane id: replyPane
@@ -207,8 +205,7 @@ QQC2.Control {
userName: root.replyUser ? root.replyUser.displayName : "" userName: root.replyUser ? root.replyUser.displayName : ""
userColor: root.replyUser ? root.replyUser.color : "" userColor: root.replyUser ? root.replyUser.color : ""
userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : "" userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : ""
isReply: root.isReplying text: currentRoom.chatBoxReplyMessage
text: isEditing ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage
} }
} }
Component { Component {
@@ -263,14 +260,13 @@ QQC2.Control {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0) anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
visible: root.replyPaneVisible visible: root.isReplying
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action { action: Kirigami.Action {
text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit") text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close" icon.name: "dialog-close"
onTriggered: { onTriggered: {
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
currentRoom.chatBoxAttachmentPath = ""; currentRoom.chatBoxAttachmentPath = "";
root.forceActiveFocus() root.forceActiveFocus()
} }
@@ -356,15 +352,6 @@ QQC2.Control {
} }
} }
Connections {
target: currentRoom
function onChatBoxEditIdChanged() {
if (currentRoom.chatBoxEditMessage.length > 0) {
textField.text = currentRoom.chatBoxEditMessage
}
}
}
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
document: textField.textDocument document: textField.textDocument
@@ -398,12 +385,11 @@ QQC2.Control {
} }
function postMessage() { function postMessage() {
actionsHandler.handleMessage(); actionsHandler.handleNewMessage();
repeatTimer.stop() repeatTimer.stop()
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
textField.clear(); textField.clear();
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
currentRoom.chatBoxEditId = "";
messageSent() messageSent()
} }
} }

View File

@@ -22,7 +22,7 @@ QQC2.Popup {
connection: Controller.activeConnection connection: Controller.activeConnection
} }
required property var chatDocumentHandler property var chatDocumentHandler
Component.onCompleted: { Component.onCompleted: {
chatDocumentHandler.completionModel.roomListModel = roomListModel; chatDocumentHandler.completionModel.roomListModel = roomListModel;
} }

View File

@@ -15,7 +15,6 @@ GridLayout {
property string userName property string userName
property color userColor: Kirigami.Theme.highlightColor property color userColor: Kirigami.Theme.highlightColor
property var userAvatar: "" property var userAvatar: ""
property bool isReply
property var text property var text
rows: 3 rows: 3
@@ -30,7 +29,7 @@ GridLayout {
Layout.columnSpan: 3 Layout.columnSpan: 3
topPadding: Kirigami.Units.smallSpacing topPadding: Kirigami.Units.smallSpacing
text: isReply ? i18n("Replying to:") : i18n("Editing message:") text: i18n("Replying to:")
} }
Rectangle { Rectangle {
id: verticalBorder id: verticalBorder

View File

@@ -21,8 +21,14 @@ TimelineContainer {
RichLabel { RichLabel {
id: label id: label
Layout.fillWidth: true Layout.fillWidth: true
visible: currentRoom.chatBoxEditId !== model.eventId
isEmote: messageDelegate.isEmote isEmote: messageDelegate.isEmote
} }
MessageEditComponent {
Layout.fillWidth: true
messageId: model.eventId
visible: currentRoom.chatBoxEditId === model.eventId
}
Loader { Loader {
id: linkPreviewLoader id: linkPreviewLoader
Layout.fillWidth: true Layout.fillWidth: true

View File

@@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.TextArea {
id: root
property string messageId
Layout.fillWidth: true
Layout.minimumHeight: editButtons.height + topPadding + bottomPadding
Layout.preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit
rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2
color: Kirigami.Theme.textColor
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
onVisibleChanged: {
if (visible) {
forceActiveFocus();
root.cursorPosition = root.length;
}
}
onTextChanged: {
currentRoom.editText = text
}
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
root.insert(cursorPosition, "\n")
} else {
root.postEdit();
}
}
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
root.insert(cursorPosition, "\n")
} else {
root.postEdit();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: {
if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
}
}
/**
* This is anchored like this so that control expands properly as the edited
* text grows in length.
*/
RowLayout {
id: editButtons
anchors.verticalCenter: root.verticalCenter
anchors.right: root.right
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: 0
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Confirm edit")
icon.name: "checkmark"
onTriggered: {
root.postEdit();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel edit")
icon.name: "dialog-close"
onTriggered: {
currentRoom.chatBoxEditId = "";
}
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
}
Connections {
target: currentRoom
function onChatBoxEditIdChanged() {
if (currentRoom.chatBoxEditId == messageId && currentRoom.chatBoxEditMessage.length > 0) {
root.text = currentRoom.chatBoxEditMessage
}
}
}
CompletionMenu {
id: completionMenu
height: implicitHeight
y: -height - 5
z: 10
chatDocumentHandler: documentHandler
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
ChatDocumentHandler {
id: documentHandler
isEdit: true
document: root.textDocument
cursorPosition: root.cursorPosition
selectionStart: root.selectionStart
selectionEnd: root.selectionEnd
room: currentRoom // We don't care about saving for edits so this is OK.
}
TextMetrics {
id: editTextMetrics
text: root.text
}
function postEdit() {
actionsHandler.handleEdit();
root.clear();
currentRoom.chatBoxEditId = "";
}
}

View File

@@ -540,7 +540,6 @@ Kirigami.ScrollablePage {
onClicked: { onClicked: {
currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxEditId = hoverActions.event.eventId;
currentRoom.chatBoxReplyId = ""; currentRoom.chatBoxReplyId = "";
chatBox.chatBar.forceActiveFocus();
} }
} }
QQC2.Button { QQC2.Button {

View File

@@ -47,6 +47,7 @@
<file alias="PollDelegate.qml">qml/Component/Timeline/PollDelegate.qml</file> <file alias="PollDelegate.qml">qml/Component/Timeline/PollDelegate.qml</file>
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file> <file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>
<file alias="StateComponent.qml">qml/Component/Timeline/StateComponent.qml</file> <file alias="StateComponent.qml">qml/Component/Timeline/StateComponent.qml</file>
<file alias="MessageEditComponent.qml">qml/Component/Timeline/MessageEditComponent.qml</file>
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file> <file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
<file alias="Login.qml">qml/Component/Login/Login.qml</file> <file alias="Login.qml">qml/Component/Login/Login.qml</file>
<file alias="Password.qml">qml/Component/Login/Password.qml</file> <file alias="Password.qml">qml/Component/Login/Password.qml</file>

View File

@@ -125,6 +125,9 @@ void RoomManager::openRoomForActiveConnection()
void RoomManager::enterRoom(NeoChatRoom *room) void RoomManager::enterRoom(NeoChatRoom *room)
{ {
if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) {
m_currentRoom->setChatBoxEditId("");
}
if (m_currentRoom && m_chatDocumentHandler) { if (m_currentRoom && m_chatDocumentHandler) {
// We're doing these things here because it is critical that they are switched at the same time // We're doing these things here because it is critical that they are switched at the same time
m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText()); m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());