From 916834fb9c8129a35f8c88bc35ceba32560ce4ca Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Tue, 26 Jan 2021 20:56:22 -0800 Subject: [PATCH] feat(chatlog): Add SystemMessages to SessionChatLog * Rendering of system messages consolidated into single place * API added to ichatlog to insert a system message at current location * System messages are now used as type + args which will fit nicely with the work in #6221 --- src/model/chathistory.cpp | 7 ++ src/model/chathistory.h | 2 + src/model/chatlogitem.cpp | 22 ++++++ src/model/chatlogitem.h | 5 ++ src/model/ichatlog.h | 10 ++- src/model/sessionchatlog.cpp | 16 ++++ src/model/sessionchatlog.h | 2 + src/model/systemmessage.h | 114 ++++++++++++++++++++++++++++ src/widget/form/chatform.cpp | 22 +++--- src/widget/form/genericchatform.cpp | 86 +++++++++++++++++++-- src/widget/form/genericchatform.h | 4 +- src/widget/form/groupchatform.cpp | 10 +-- src/widget/widget.cpp | 7 +- 13 files changed, 278 insertions(+), 29 deletions(-) create mode 100644 src/model/systemmessage.h diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp index 497ccda8f..c0494fdf1 100644 --- a/src/model/chathistory.cpp +++ b/src/model/chathistory.cpp @@ -212,6 +212,13 @@ std::vector ChatHistory::getDateIdxs(const QDate& } } +void ChatHistory::addSystemMessage(const SystemMessage& message) +{ + // FIXME: #6221 Insert into history + sessionChatLog.addSystemMessage(message); +} + + void ChatHistory::onFileUpdated(const ToxPk& sender, const ToxFile& file) { if (canUseHistory()) { diff --git a/src/model/chathistory.h b/src/model/chathistory.h index 366aff110..fe6d0c9d6 100644 --- a/src/model/chathistory.h +++ b/src/model/chathistory.h @@ -42,12 +42,14 @@ public: ChatLogIdx getFirstIdx() const override; ChatLogIdx getNextIdx() const override; std::vector getDateIdxs(const QDate& startDate, size_t maxDates) const override; + void addSystemMessage(const SystemMessage& message) override; public slots: void onFileUpdated(const ToxPk& sender, const ToxFile& file); void onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file, bool paused); void onFileTransferBrokenUnbroken(const ToxPk& sender, const ToxFile& file, bool broken); + private slots: void onMessageReceived(const ToxPk& sender, const Message& message); void onMessageSent(DispatchedMessageId id, const Message& message); diff --git a/src/model/chatlogitem.cpp b/src/model/chatlogitem.cpp index 07d67154b..5f10aa8d3 100644 --- a/src/model/chatlogitem.cpp +++ b/src/model/chatlogitem.cpp @@ -50,6 +50,11 @@ ChatLogItem::ChatLogItem(ToxPk sender_, const QString& displayName, ChatLogMessa ChatLogItemDeleter::doDelete)) {} +ChatLogItem::ChatLogItem(SystemMessage systemMessage) + : contentType(ContentType::systemMessage) + , content(new SystemMessage(std::move(systemMessage)), ChatLogItemDeleter::doDelete) +{} + ChatLogItem::ChatLogItem(ToxPk sender_, const QString& displayName_, ContentType contentType_, ContentPtr content_) : sender(std::move(sender_)) , displayName(displayName_) @@ -91,6 +96,19 @@ const ChatLogMessage& ChatLogItem::getContentAsMessage() const return *static_cast(content.get()); } +SystemMessage& ChatLogItem::getContentAsSystemMessage() +{ + assert(contentType == ContentType::systemMessage); + return *static_cast(content.get()); +} + +const SystemMessage& ChatLogItem::getContentAsSystemMessage() const +{ + assert(contentType == ContentType::systemMessage); + return *static_cast(content.get()); +} + + QDateTime ChatLogItem::getTimestamp() const { switch (contentType) { @@ -102,6 +120,10 @@ QDateTime ChatLogItem::getTimestamp() const const auto& file = getContentAsFile(); return file.timestamp; } + case ChatLogItem::ContentType::systemMessage: { + const auto& systemMessage = getContentAsSystemMessage(); + return systemMessage.timestamp; + } } assert(false); diff --git a/src/model/chatlogitem.h b/src/model/chatlogitem.h index c0e75f58e..5d2617e09 100644 --- a/src/model/chatlogitem.h +++ b/src/model/chatlogitem.h @@ -22,6 +22,7 @@ #include "src/core/toxfile.h" #include "src/core/toxpk.h" #include "src/model/message.h" +#include "src/model/systemmessage.h" #include "src/persistence/history.h" #include @@ -48,16 +49,20 @@ public: { message, fileTransfer, + systemMessage, }; ChatLogItem(ToxPk sender, const QString& displayName, ChatLogFile file); ChatLogItem(ToxPk sender, const QString& displayName, ChatLogMessage message); + ChatLogItem(SystemMessage message); const ToxPk& getSender() const; ContentType getContentType() const; ChatLogFile& getContentAsFile(); const ChatLogFile& getContentAsFile() const; ChatLogMessage& getContentAsMessage(); const ChatLogMessage& getContentAsMessage() const; + SystemMessage& getContentAsSystemMessage(); + const SystemMessage& getContentAsSystemMessage() const; QDateTime getTimestamp() const; void setDisplayName(QString name); const QString& getDisplayName() const; diff --git a/src/model/ichatlog.h b/src/model/ichatlog.h index 4729a5c2f..44a4c0937 100644 --- a/src/model/ichatlog.h +++ b/src/model/ichatlog.h @@ -28,9 +28,9 @@ #include "src/model/chatlogitem.h" #include "src/model/friend.h" #include "src/model/group.h" -#include "src/persistence/history.h" -#include "util/strongtype.h" +#include "src/model/systemmessage.h" #include "src/widget/searchtypes.h" +#include "util/strongtype.h" #include @@ -137,6 +137,12 @@ public: virtual std::vector getDateIdxs(const QDate& startDate, size_t maxDates) const = 0; + /** + * @brief Inserts a system message at the end of the chat + * @param[in] message systemMessage to insert + */ + virtual void addSystemMessage(const SystemMessage& message) = 0; + signals: void itemUpdated(ChatLogIdx idx); }; diff --git a/src/model/sessionchatlog.cpp b/src/model/sessionchatlog.cpp index 5de8fa217..19e378455 100644 --- a/src/model/sessionchatlog.cpp +++ b/src/model/sessionchatlog.cpp @@ -319,6 +319,15 @@ std::vector SessionChatLog::getDateIdxs(const QDat return ret; } +void SessionChatLog::addSystemMessage(const SystemMessage& message) +{ + auto messageIdx = nextIdx++; + + items.emplace(messageIdx, ChatLogItem(message)); + + emit this->itemUpdated(messageIdx); +} + void SessionChatLog::insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, QString senderName, const ChatLogMessage& message) { @@ -358,6 +367,13 @@ void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, QStrin items.emplace(idx, std::move(item)); } +void SessionChatLog::insertSystemMessageAtIdx(ChatLogIdx idx, SystemMessage message) +{ + auto item = ChatLogItem(std::move(message)); + + items.emplace(idx, std::move(item)); +} + /** * @brief Inserts message data into the chatlog buffer * @note Owner of SessionChatLog is in charge of attaching this to the appropriate IMessageDispatcher diff --git a/src/model/sessionchatlog.h b/src/model/sessionchatlog.h index 99c8fa52a..bb34219dd 100644 --- a/src/model/sessionchatlog.h +++ b/src/model/sessionchatlog.h @@ -44,6 +44,7 @@ public: ChatLogIdx getFirstIdx() const override; ChatLogIdx getNextIdx() const override; std::vector getDateIdxs(const QDate& startDate, size_t maxDates) const override; + void addSystemMessage(const SystemMessage& message) override; void insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, QString senderName, const ChatLogMessage& message); @@ -52,6 +53,7 @@ public: void insertBrokenMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, QString senderName, const ChatLogMessage& message); void insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, QString senderName, const ChatLogFile& file); + void insertSystemMessageAtIdx(ChatLogIdx idx, SystemMessage message); public slots: void onMessageReceived(const ToxPk& sender, const Message& message); diff --git a/src/model/systemmessage.h b/src/model/systemmessage.h new file mode 100644 index 000000000..3779b5e21 --- /dev/null +++ b/src/model/systemmessage.h @@ -0,0 +1,114 @@ +/* + Copyright © 2015-2021 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include +#include +#include + +#include + +enum class SystemMessageType +{ + // DO NOT CHANGE ORDER + // These values are saved directly to the DB and read back, changing the + // order will break persistence! + fileSendFailed = 0, + userJoinedGroup, + userLeftGroup, + peerNameChanged, + peerStateChange, + titleChanged, + cleared, + unexpectedCallEnd, + outgoingCall, + incomingCall, + callEnd, + messageSendFailed, +}; + +struct SystemMessage +{ + using Args = std::array; + SystemMessageType messageType; + QDateTime timestamp; + Args args; + + QString toString() const + { + QString translated; + size_t numArgs = 0; + + switch (messageType) { + case SystemMessageType::fileSendFailed: + translated = QObject::tr("Failed to send file \"%1\""); + numArgs = 1; + break; + case SystemMessageType::userJoinedGroup: + translated = QObject::tr("%1 has joined the group"); + numArgs = 1; + break; + case SystemMessageType::userLeftGroup: + translated = QObject::tr("%1 has left the group"); + numArgs = 1; + break; + case SystemMessageType::peerNameChanged: + translated = QObject::tr("%1 is now known as %2"); + numArgs = 2; + break; + case SystemMessageType::titleChanged: + translated = QObject::tr("%1 has set the title to %2"); + numArgs = 2; + break; + case SystemMessageType::cleared: + translated = QObject::tr("Cleared"); + break; + case SystemMessageType::unexpectedCallEnd: + translated = QObject::tr("Call with %1 ended unexpectedly. %2"); + numArgs = 2; + break; + case SystemMessageType::callEnd: + translated = QObject::tr("Call with %1 ended. %2"); + numArgs = 2; + break; + case SystemMessageType::peerStateChange: + translated = QObject::tr("%1 is now %2", "e.g. \"Dubslow is now online\""); + numArgs = 2; + break; + case SystemMessageType::outgoingCall: + translated = QObject::tr("Calling %1"); + numArgs = 1; + break; + case SystemMessageType::incomingCall: + translated = QObject::tr("%1 calling"); + numArgs = 1; + break; + case SystemMessageType::messageSendFailed: + translated = QObject::tr("Message failed to send"); + break; + } + + for (size_t i = 0; i < numArgs; ++i) { + translated = translated.arg(args[i]); + } + + return translated; + } +}; diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 11785e6e7..d0b4f1942 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -289,9 +289,11 @@ void ChatForm::onAvInvite(uint32_t friendId, bool video) } QString displayedName = f->getDisplayedName(); - insertChatMessage(ChatMessage::createChatInfoMessage(tr("%1 calling").arg(displayedName), - ChatMessage::INFO, - QDateTime::currentDateTime())); + + SystemMessage systemMessage; + systemMessage.messageType = SystemMessageType::incomingCall; + systemMessage.args = {displayedName}; + chatLog.addSystemMessage(systemMessage); auto testedFlag = video ? Settings::AutoAcceptCall::Video : Settings::AutoAcceptCall::Audio; // AutoAcceptCall is set for this friend @@ -349,8 +351,8 @@ void ChatForm::onAvEnd(uint32_t friendId, bool error) void ChatForm::showOutgoingCall(bool video) { headWidget->showOutgoingCall(video); - addSystemInfoMessage(tr("Calling %1").arg(f->getDisplayedName()), ChatMessage::INFO, - QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::outgoingCall, + {f->getDisplayedName()}); emit outgoingNotification(); emit updateFriendActivity(*f); } @@ -444,10 +446,8 @@ void ChatForm::onFriendStatusChanged(const ToxPk& friendPk, Status::Status statu if (Settings::getInstance().getStatusChangeNotificationEnabled()) { QString fStatus = Status::getTitle(status); - addSystemInfoMessage(tr("%1 is now %2", "e.g. \"Dubslow is now online\"") - .arg(f->getDisplayedName()) - .arg(fStatus), - ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::peerStateChange, + {f->getDisplayedName(), fStatus}); } } @@ -652,10 +652,10 @@ void ChatForm::stopCounter(bool error) } QString dhms = secondsToDHMS(timeElapsed.elapsed() / 1000); QString name = f->getDisplayedName(); - QString mess = error ? tr("Call with %1 ended unexpectedly. %2") : tr("Call with %1 ended. %2"); + auto messageType = error ? SystemMessageType::unexpectedCallEnd : SystemMessageType::callEnd; // TODO: add notification once notifications are implemented - addSystemInfoMessage(mess.arg(name, dhms), ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), messageType, {name, dhms}); callDurationTimer->stop(); callDuration->setText(""); callDuration->hide(); diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index 64ede8c18..c66a80c22 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -209,6 +209,32 @@ ChatLogIdx firstItemAfterDate(QDate date, const IChatLog& chatLog) return chatLog.getNextIdx(); } } + +/** + * @return Chat message message type (info/warning) for the given system message + * @param[in] systemMessage + */ +ChatMessage::SystemMessageType getChatMessageType(const SystemMessage& systemMessage) +{ + switch (systemMessage.messageType) { + case SystemMessageType::fileSendFailed: + case SystemMessageType::messageSendFailed: + case SystemMessageType::unexpectedCallEnd: + return ChatMessage::ERROR; + case SystemMessageType::userJoinedGroup: + case SystemMessageType::userLeftGroup: + case SystemMessageType::peerNameChanged: + case SystemMessageType::peerStateChange: + case SystemMessageType::titleChanged: + case SystemMessageType::cleared: + case SystemMessageType::outgoingCall: + case SystemMessageType::incomingCall: + case SystemMessageType::callEnd: + return ChatMessage::INFO; + } + + return ChatMessage::INFO; +} } // namespace GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, IChatLog& chatLog, @@ -407,7 +433,42 @@ QDateTime GenericChatForm::getLatestTime() const if (chatLog.getFirstIdx() == chatLog.getNextIdx()) return QDateTime(); - return chatLog.at(chatLog.getNextIdx() - 1).getTimestamp(); + const auto shouldUseTimestamp = [this] (ChatLogIdx idx) { + if (chatLog.at(idx).getContentType() != ChatLogItem::ContentType::systemMessage) { + return true; + } + + const auto& message = chatLog.at(idx).getContentAsSystemMessage(); + switch (message.messageType) { + case SystemMessageType::incomingCall: + case SystemMessageType::outgoingCall: + case SystemMessageType::callEnd: + case SystemMessageType::unexpectedCallEnd: + return true; + case SystemMessageType::cleared: + case SystemMessageType::titleChanged: + case SystemMessageType::peerStateChange: + case SystemMessageType::peerNameChanged: + case SystemMessageType::userLeftGroup: + case SystemMessageType::userJoinedGroup: + case SystemMessageType::fileSendFailed: + case SystemMessageType::messageSendFailed: + return false; + } + + qWarning("Unexpected system message type %d", static_cast(message.messageType)); + return false; + }; + + ChatLogIdx idx = chatLog.getNextIdx(); + while (idx > chatLog.getFirstIdx()) { + idx = idx - 1; + if (shouldUseTimestamp(idx)) { + return chatLog.at(idx).getTimestamp(); + } + } + + return QDateTime(); } void GenericChatForm::reloadTheme() @@ -591,10 +652,14 @@ void GenericChatForm::setColorizedNames(bool enable) colorizeNames = enable; } -void GenericChatForm::addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type, - const QDateTime& datetime) +void GenericChatForm::addSystemInfoMessage(const QDateTime& datetime, SystemMessageType messageType, + SystemMessage::Args messageArgs) { - insertChatMessage(ChatMessage::createChatInfoMessage(message, type, datetime)); + SystemMessage systemMessage; + systemMessage.messageType = static_cast(messageType); + systemMessage.timestamp = datetime; + systemMessage.args = std::move(messageArgs); + chatLog.addSystemMessage(systemMessage); } void GenericChatForm::addSystemDateMessage(const QDate& date) @@ -759,7 +824,7 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform) chatWidget->clear(); if (inform) - addSystemInfoMessage(tr("Cleared"), ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::cleared, {}); messages.clear(); } @@ -1070,6 +1135,17 @@ void GenericChatForm::renderItem(const ChatLogItem& item, bool hideName, bool co renderFile(item.getDisplayName(), file.file, isSelf, item.getTimestamp(), chatMessage); break; } + case ChatLogItem::ContentType::systemMessage: { + const auto& systemMessage = item.getContentAsSystemMessage(); + + auto chatMessageType = getChatMessageType(systemMessage); + chatMessage = ChatMessage::createChatInfoMessage(systemMessage.toString(), chatMessageType, + QDateTime::currentDateTime()); + // Ignore caller's decision to hide the name. We show the icon in the + // slot of the sender's name so we always want it visible + hideName = false; + break; + } } if (hideName) { diff --git a/src/widget/form/genericchatform.h b/src/widget/form/genericchatform.h index 3919ac670..359b69644 100644 --- a/src/widget/form/genericchatform.h +++ b/src/widget/form/genericchatform.h @@ -76,8 +76,8 @@ public: void setName(const QString& newName); virtual void show(ContentLayout* contentLayout); - void addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type, - const QDateTime& datetime); + void addSystemInfoMessage(const QDateTime& datetime, SystemMessageType messageType, + SystemMessage::Args messageArgs); static QString resolveToxPk(const ToxPk& pk); QDateTime getLatestTime() const; diff --git a/src/widget/form/groupchatform.cpp b/src/widget/form/groupchatform.cpp index 28c6a329e..525c8b0ad 100644 --- a/src/widget/form/groupchatform.cpp +++ b/src/widget/form/groupchatform.cpp @@ -148,9 +148,8 @@ void GroupChatForm::onTitleChanged(const QString& author, const QString& title) return; } - const QString message = tr("%1 has set the title to %2").arg(author, title); const QDateTime curTime = QDateTime::currentDateTime(); - addSystemInfoMessage(message, ChatMessage::INFO, curTime); + addSystemInfoMessage(curTime, SystemMessageType::titleChanged, {author, title}); } void GroupChatForm::onScreenshotClicked() @@ -232,19 +231,20 @@ void GroupChatForm::updateUserNames() void GroupChatForm::onUserJoined(const ToxPk& user, const QString& name) { - addSystemInfoMessage(tr("%1 has joined the group").arg(name), ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::userJoinedGroup, {name}); updateUserNames(); } void GroupChatForm::onUserLeft(const ToxPk& user, const QString& name) { - addSystemInfoMessage(tr("%1 has left the group").arg(name), ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::userLeftGroup, {name}); updateUserNames(); } void GroupChatForm::onPeerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName) { - addSystemInfoMessage(tr("%1 is now known as %2").arg(oldName, newName), ChatMessage::INFO, QDateTime::currentDateTime()); + addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::peerNameChanged, + {oldName, newName}); updateUserNames(); } diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index c35853ed3..02fad6760 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -1130,8 +1130,8 @@ void Widget::dispatchFileSendFailed(uint32_t friendId, const QString& fileName) return; } - chatForm.value()->addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fileName), - ChatMessage::ERROR, QDateTime::currentDateTime()); + chatForm.value()->addSystemInfoMessage(QDateTime::currentDateTime(), + SystemMessageType::fileSendFailed, {fileName}); } void Widget::onRejectCall(uint32_t friendId) @@ -2342,10 +2342,9 @@ void Widget::onGroupSendFailed(uint32_t groupnumber) const auto& groupId = GroupList::id2Key(groupnumber); assert(GroupList::findGroup(groupId)); - const auto message = tr("Message failed to send"); const auto curTime = QDateTime::currentDateTime(); auto form = groupChatForms[groupId].data(); - form->addSystemInfoMessage(message, ChatMessage::INFO, curTime); + form->addSystemInfoMessage(curTime, SystemMessageType::messageSendFailed, {}); } void Widget::onFriendTypingChanged(uint32_t friendnumber, bool isTyping)