1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-15 21:15:24 +02:00

misc: fix ups for conversationmodel/messagelistmodel

Removes some remaining excess complexity in the way interactions are managed by the client. Removes raw iterator access and provides thread-safe alternatives.

Change-Id: I482bf599de869245f96c4aab418127f30508ef41
This commit is contained in:
Andreas Traczyk 2023-12-20 17:16:07 -05:00 committed by Sébastien Blin
parent 009a3902cb
commit 46a955aa3d
14 changed files with 870 additions and 1059 deletions

View file

@ -108,35 +108,38 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
case Role::UnreadMessagesCount:
return QVariant(item.unreadMessages);
case Role::LastInteractionTimeStamp: {
if (!item.interactions->empty()) {
auto ts = static_cast<qint32>(item.interactions->rbegin()->second.timestamp);
return QVariant(ts);
}
break;
qint32 ts = 0;
item.interactions->withLast([&ts](const QString&, const interaction::Info& interaction) {
ts = interaction.timestamp;
});
return QVariant(ts);
}
case Role::LastInteraction: {
if (!item.interactions->empty()) {
auto interaction = item.interactions->rbegin()->second;
QString lastInteractionBody;
item.interactions->withLast([&](const QString&, const interaction::Info& interaction) {
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
if (interaction.type == interaction::Type::DATA_TRANSFER) {
return QVariant(interaction.commit.value("displayName"));
if (interaction.type == interaction::Type::UPDATE_PROFILE) {
lastInteractionBody = interaction::getProfileUpdatedString();
} else if (interaction.type == interaction::Type::DATA_TRANSFER) {
lastInteractionBody = interaction.commit.value("displayName");
} else if (interaction.type == lrc::api::interaction::Type::CALL) {
return QVariant(interaction::getCallInteractionString(interaction.authorUri
== accInfo.profileInfo.uri,
interaction));
const auto isOutgoing = interaction.authorUri == accInfo.profileInfo.uri;
lastInteractionBody = interaction::getCallInteractionString(isOutgoing, interaction);
} else if (interaction.type == lrc::api::interaction::Type::CONTACT) {
auto bestName = interaction.authorUri == accInfo.profileInfo.uri
? accInfo.accountModel->bestNameForAccount(accInfo.id)
: accInfo.contactModel->bestNameForContact(
interaction.authorUri);
return QVariant(
interaction::getContactInteractionString(bestName,
interaction::to_action(
interaction.commit["action"])));
lastInteractionBody
= interaction::getContactInteractionString(bestName,
interaction::to_action(
interaction.commit["action"]));
} else {
lastInteractionBody = interaction.body.isEmpty() ? tr("(deleted message)")
: interaction.body;
}
return QVariant(interaction.body);
}
break;
});
return QVariant(lastInteractionBody);
}
case Role::IsSwarm:
return QVariant(item.isSwarm());

View file

@ -37,7 +37,6 @@ JamiListView {
function loadMoreMsgsIfNeeded() {
if (atYBeginning && !CurrentConversation.allMessagesLoaded) {
print("load more messages", atYBeginning, CurrentConversation.allMessagesLoaded)
MessagesAdapter.loadMoreMessages()
}
}
@ -175,7 +174,8 @@ JamiListView {
Connections {
target: CurrentConversation
function onScrollTo(id) {
var idx = MessagesAdapter.getMessageIndexFromId(id)
// Get the filtered index from the interaction ID.
var idx = MessagesAdapter.messageListModel.getDisplayIndex(id)
positionViewAtIndex(idx, ListView.Visible)
}
}

View file

@ -73,8 +73,6 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
filteredMsgListModel_->setSourceModel(conversation.interactions.get());
set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
});
connect(messageParser_, &MessageParser::messageParsed, this, &MessagesAdapter::onMessageParsed);
@ -83,9 +81,10 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated);
timestampTimer_->start(timestampUpdateIntervalMs_);
connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, this, [this]() {
connectConversationModel();
});
connect(lrcInstance_,
&LRCInstance::currentAccountIdChanged,
this,
&MessagesAdapter::connectConversationModel);
connectConversationModel();
}
@ -142,6 +141,9 @@ MessagesAdapter::connectConversationModel()
this,
&MessagesAdapter::onMessagesFoundProcessed,
Qt::UniqueConnection);
mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
}
void
@ -379,18 +381,7 @@ QVariant
MessagesAdapter::dataForInteraction(const QString& interactionId, int role) const
{
if (auto* model = getMsgListSourceModel()) {
auto idx = model->indexOfMessage(interactionId);
if (idx != -1)
return model->data(idx, role);
}
return {};
}
int
MessagesAdapter::getIndexOfMessage(const QString& interactionId) const
{
if (auto* model = getMsgListSourceModel()) {
return model->indexOfMessage(interactionId);
return model->data(interactionId, role);
}
return {};
}
@ -584,7 +575,7 @@ MessagesAdapter::onMessagesFoundProcessed(const QString& accountId,
bool isSearchInProgress = messageInformation.size();
if (isSearchInProgress) {
for (auto it = messageInformation.begin(); it != messageInformation.end(); it++) {
mediaInteractions_->insert(qMakePair(it.key(), it.value()));
mediaInteractions_->append(it.key(), it.value());
}
} else {
set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
@ -745,23 +736,6 @@ MessagesAdapter::startSearch(const QString& text, bool isMedia)
}
}
int
MessagesAdapter::getMessageIndexFromId(const QString& id)
{
const QString& convId = lrcInstance_->get_selectedConvUid();
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
auto allInteractions = conversation.interactions.get();
int index = 0;
for (auto it = allInteractions->rbegin(); it != allInteractions->rend(); it++) {
if (interaction::isDisplayedInChatview(it->second.type)) {
if (it->first == id)
return index;
index++;
}
}
return -1;
}
MessageListModel*
MessagesAdapter::getMsgListSourceModel() const
{

View file

@ -45,12 +45,19 @@ public:
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
auto type = static_cast<interaction::Type>(
sourceModel()->data(index, MessageList::Role::Type).toInt());
return interaction::isDisplayedInChatview(type);
return interaction::isTypeDisplayable(type);
};
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
{
return left.row() > right.row();
};
Q_INVOKABLE int getDisplayIndex(const QString& id)
{
auto sourceRow = ((MessageListModel*) sourceModel())->indexOfMessage(id);
auto index = mapFromSource(sourceModel()->index(sourceRow, 0));
return index.row();
};
};
class MessagesAdapter final : public QmlAdapterBase
@ -129,13 +136,11 @@ protected:
const QColor& linkColor = QColor(0x06, 0x45, 0xad),
const QColor& backgroundColor = QColor(0x0, 0x0, 0x0));
Q_INVOKABLE void onPaste();
Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const;
Q_INVOKABLE QString getStatusString(int status);
Q_INVOKABLE QVariantMap getTransferStats(const QString& messageId, int);
Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId,
int role = Qt::DisplayRole) const;
Q_INVOKABLE void startSearch(const QString& text, bool isMedia);
Q_INVOKABLE int getMessageIndexFromId(const QString& id);
// Run corrsponding js functions, c++ to qml.
void setMessagesImageContent(const QString& path, bool isBased64 = false);

View file

@ -436,7 +436,7 @@ Utils::contactPhoto(LRCInstance* instance,
auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
photo = Utils::fallbackAvatar("jami:" + contactInfo.profileInfo.uri, avatarName);
}
} catch (const std::exception& e) {
} catch (const std::exception&) {
photo = fallbackAvatar("jami:" + contactUri, QString(), size);
}
return Utils::scaleAndFrame(photo, size);

View file

@ -48,7 +48,6 @@
#endif
#include "api/account.h"
#include "api/contact.h"
#include "api/contactmodel.h"
#include "api/conversationmodel.h"

View file

@ -309,6 +309,7 @@ set(LIBCLIENT_HEADERS_API
api/contactmodel.h
api/conversationmodel.h
api/datatransfermodel.h
api/messagelistmodel.h
api/datatransfer.h
api/interaction.h
api/lrc.h

View file

@ -20,12 +20,11 @@
#include "interaction.h"
#include "messagelistmodel.h"
#include "account.h"
#include "member.h"
#include "typedefs.h"
#include <map>
#include <memory>
#include <vector>
namespace lrc {

View file

@ -47,12 +47,11 @@ enum class Type {
COUNT__
};
Q_ENUM_NS(Type)
static inline bool
isDisplayedInChatview(const Type& type)
isTypeDisplayable(const Type& type)
{
return type != interaction::Type::MERGE && type != interaction::Type::EDITED
&& type != interaction::Type::REACTION && type != interaction::Type::VOTE
&& type != interaction::Type::UPDATE_PROFILE && type != interaction::Type::INVALID;
return type != interaction::Type::VOTE && type != interaction::Type::UPDATE_PROFILE;
}
static inline const QString
@ -380,7 +379,7 @@ struct Info
QString react_to;
QVector<Body> previousBodies;
Info() {}
Info() = default;
Info(QString authorUri,
QString body,
@ -399,6 +398,11 @@ struct Info
this->isRead = isRead;
}
Info(const Info& other) = default;
Info(Info&& other) = default;
Info& operator=(const Info& other) = delete;
Info& operator=(Info&& other) = default;
void init(const MapStringString& message, const QString& accountURI)
{
type = to_type(message["type"]);
@ -479,6 +483,13 @@ getCallInteractionString(bool isSelf, const Info& info)
return getCallInteractionStringNonSwarm(isSelf, info.duration);
}
static inline QString
getProfileUpdatedString()
{
// Perhaps one day this will be more detailed.
return QObject::tr("(profile updated)");
}
} // namespace interaction
} // namespace api
} // namespace lrc

View file

@ -0,0 +1,157 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
*
* This program is free 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "api/interaction.h"
#include <QAbstractListModel>
#include <mutex>
namespace lrc {
namespace api {
namespace account {
struct Info;
}
#define MSG_ROLES \
X(Id) \
X(Author) \
X(Body) \
X(ParentId) \
X(Timestamp) \
X(Duration) \
X(Type) \
X(Status) \
X(IsRead) \
X(ContactAction) \
X(ActionUri) \
X(ConfId) \
X(DeviceId) \
X(LinkPreviewInfo) \
X(ParsedBody) \
X(PreviousBodies) \
X(Reactions) \
X(ReplyTo) \
X(ReplyToBody) \
X(ReplyToAuthor) \
X(TotalSize) \
X(TransferName) \
X(FileExtension) \
X(Readers) \
X(IsEmojiOnly) \
X(Index)
namespace MessageList {
Q_NAMESPACE
enum Role {
DummyRole = Qt::UserRole + 1,
#define X(role) role,
MSG_ROLES
#undef X
};
Q_ENUM_NS(Role)
} // namespace MessageList
class MessageListModel : public QAbstractListModel
{
Q_OBJECT
public:
// A pair of message id and interaction info.
using item_t = QPair<QString, interaction::Info>;
using container_t = QList<item_t>;
using iterator = container_t::iterator;
explicit MessageListModel(const account::Info* account, QObject* parent = nullptr);
~MessageListModel() = default;
// QAbstractListModel interface
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
// QAbstractListModel helpers
Q_INVOKABLE QVariant data(const QString& id, int role = Qt::DisplayRole) const;
// Basic container + mutation/update methods.
bool empty() const;
int indexOfMessage(const QString& messageId) const;
void clear();
void reloadHistory();
bool insert(const QString& id, const interaction::Info& interaction, int index = -1);
bool append(const QString& id, const interaction::Info& interaction);
bool update(const QString& id, const interaction::Info& interaction);
bool updateStatus(const QString& id, interaction::Status newStatus, const QString& newBody = {});
QPair<bool, bool> addOrUpdate(const QString& id, const interaction::Info& interaction);
// Thread-safe access to interactions.
// Note: be careful when using these functions to modify interactions, as
// the dataChanged() signal is not emitted. Use add/update/remove instead
// if per-message UI updates are required. Also, DO NOT use these to
// mutate the interactions_ container.
using InteractionCb = std::function<void(const QString&, interaction::Info&)>;
void forEach(const InteractionCb&);
// Operations on a single interaction. Returns true if the interaction is found.
// Note: if idHint is an empty string, the last interaction is used.
bool with(const QString& idHint, const InteractionCb&);
// A convenience function to access the last interaction.
bool withLast(const InteractionCb&);
// Used when sorting conversations by timestamp, where locking multiple
// interactions simultaneously is required.
std::recursive_mutex& getMutex();
// Methods to manage message metadata.
void addHyperlinkInfo(const QString& messageId, const QVariantMap& info);
void addReaction(const QString& messageId, const MapStringString& reaction);
void rmReaction(const QString& messageId, const QString& reactionId);
void setParsedMessage(const QString& messageId, const QString& parsed);
void setRead(const QString& peer, const QString& messageId);
QString getRead(const QString& peer);
QString lastSelfMessageId(const QString& id) const;
QPair<QString, time_t> getDisplayedInfoForPeer(const QString& peerId);
private:
using Role = MessageList::Role;
container_t interactions_;
mutable std::recursive_mutex mutex_;
const account::Info* account_;
// Note: because read status are updated even if interaction is not loaded we need to
// keep track of these status outside the interaction::Info to allow quick access.
// lastDisplayedMessageUid_ is used to keep track of the last message displayed for each
// peer. This is used to update the far end read status of the interaction. messageToReaders_
// is used to keep track of the readers of each message. This is a different view of
// lastDisplayedMessageUid_, and is used to update the read status of the interaction.
QMap<QString, QString> lastDisplayedMessageUid_; // {"peerId": "messageId"}
QMap<QString, QStringList> messageToReaders_; // {"messageId": ["peer1", "peer2"]}
QMap<QString, QSet<QString>> replyTo_;
iterator find(const QString& msgId);
int move(iterator it, const QString& newParentId);
QVariant data(int idx, int role = Qt::DisplayRole) const;
QVariant dataForItem(const item_t& item, int indexRow, int role = Qt::DisplayRole) const;
void updateReplies(const item_t& message);
};
} // namespace api
} // namespace lrc
Q_DECLARE_METATYPE(lrc::api::MessageListModel*)

View file

@ -496,7 +496,7 @@ getHistory(Database& db, api::conversation::Info& conversation, const QString& l
type,
status,
(payloads[i + 6] == "1" ? true : false)});
conversation.interactions->emplace(payloads[i], std::move(msg));
conversation.interactions->append(payloads[i], std::move(msg));
if (status != api::interaction::Status::DISPLAYED || !payloads[i + 1].isEmpty()) {
continue;
}

View file

@ -51,7 +51,6 @@
#include <algorithm>
#include <mutex>
#include <regex>
#include <fstream>
#include <sstream>
namespace lrc {
@ -172,11 +171,11 @@ public:
* @param peerId, peer id
* @param status, new status for this interaction
*/
void slotUpdateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerId,
const QString& messageId,
int status);
void updateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerId,
const QString& messageId,
int status);
/**
* place a call
@ -236,7 +235,6 @@ public:
FilterType typeFilter;
FilterType customTypeFilter;
std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex}
MapStringString transfIdToDbIntId;
uint32_t mediaResearchRequestId;
uint32_t msgResearchRequestId;
@ -493,8 +491,8 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q
// filter out calls from conference
for (const auto& c : conferences) {
for (const auto& subcall : owner.callModel->getConferenceSubcalls(c)) {
auto position = std::find(calls.begin(), calls.end(), subcall);
if (position != calls.end()) {
const auto position = std::find(calls.cbegin(), calls.cend(), subcall);
if (position != calls.cend()) {
calls.erase(position);
}
}
@ -567,12 +565,12 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q
} catch (...) {
}
}
for (auto it : tempConferences.toStdMap()) {
for (const auto& it : tempConferences.toStdMap()) {
if (filter.isEmpty()) {
callsVector.push_back(it.second);
continue;
}
for (AccountConversation accConv : it.second) {
for (const AccountConversation& accConv : it.second) {
try {
auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get();
@ -889,8 +887,8 @@ ConversationModel::joinCall(const QString& uid,
isAudioOnly);
// Update interaction status
pimpl_->invalidateModel();
emit selectConversation(uid);
emit conversationUpdated(uid);
selectConversation(uid);
Q_EMIT conversationUpdated(uid);
} catch (...) {
}
}
@ -913,8 +911,8 @@ ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
// Update interaction status
invalidateModel();
emit linked.selectConversation(conversation.uid);
emit linked.conversationUpdated(conversation.uid);
linked.selectConversation(conversation.uid);
Q_EMIT linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
return;
}
@ -1158,11 +1156,13 @@ ConversationModel::notificationsCount() const
void
ConversationModel::reloadHistory() const
{
std::for_each(pimpl_->conversations.begin(), pimpl_->conversations.end(), [&](const auto& c) {
c.interactions->reloadHistory();
Q_EMIT conversationUpdated(c.uid);
Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
});
std::for_each(pimpl_->conversations.begin(),
pimpl_->conversations.end(),
[&](const conversation::Info& c) {
c.interactions->reloadHistory();
Q_EMIT conversationUpdated(c.uid);
Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
});
}
QString
@ -1345,19 +1345,8 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS
storage::addDaemonMsgId(pimpl_->db, msgId, toQString(daemonMsgId));
}
bool ret = false;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[newConv.uid]);
ret = newConv.interactions->insert(std::pair<QString, interaction::Info>(msgId, msg))
.second;
}
if (!ret) {
qDebug()
<< "ConversationModel::sendMessage failed to send message because an existing "
"key was already present in the database key ="
<< msgId;
if (!newConv.interactions->append(msgId, msg)) {
qWarning() << Q_FUNC_INFO << "Append failed: duplicate ID";
return;
}
@ -1503,10 +1492,7 @@ ConversationModel::clearHistory(const QString& uid)
// Remove all TEXT interactions from database
storage::clearHistory(pimpl_->db, uid);
// Update conversation
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
conversation.interactions->clear();
}
conversation.interactions->clear();
storage::getHistory(pimpl_->db,
conversation,
pimpl_->linked.owner.profileInfo.uri); // will contain "Conversation started"
@ -1541,7 +1527,6 @@ ConversationModel::clearAllHistory()
// WARNING: clear all history is not implemented for swarm
continue;
}
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
conversation.interactions->clear();
}
storage::getHistory(pimpl_->db, conversation, pimpl_->linked.owner.profileInfo.uri);
@ -1558,37 +1543,30 @@ ConversationModel::clearUnreadInteractions(const QString& convId)
return;
}
auto& conversation = conversationOpt->get();
bool emitUpdated = false;
QString lastDisplayed;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
auto& interactions = conversation.interactions;
if (conversation.isSwarm()) {
emitUpdated = true;
if (!interactions->empty())
lastDisplayed = interactions->rbegin()->first;
} else {
std::for_each(interactions->begin(),
interactions->end(),
[&](decltype(*interactions->begin())& it) {
if (!it.second.isRead) {
emitUpdated = true;
it.second.isRead = true;
if (owner.profileInfo.type != profile::Type::SIP)
lastDisplayed = storage::getDaemonIdByInteractionId(pimpl_->db,
it.first);
storage::setInteractionRead(pimpl_->db, it.first);
}
});
}
bool updated = false;
QString lastDisplayedId;
if (conversation.isSwarm()) {
updated = true;
conversation.interactions->withLast(
[&](const QString& id, interaction::Info&) { lastDisplayedId = id; });
} else {
conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
if (interaction.isRead)
return;
updated = true;
interaction.isRead = true;
if (owner.profileInfo.type != profile::Type::SIP)
lastDisplayedId = storage::getDaemonIdByInteractionId(pimpl_->db, id);
storage::setInteractionRead(pimpl_->db, id);
});
}
if (!lastDisplayed.isEmpty()) {
if (!lastDisplayedId.isEmpty()) {
auto to = conversation.isSwarm()
? "swarm:" + convId
: "jami:" + pimpl_->peersForConversation(conversation).front();
ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayed, 3);
ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayedId, 3);
}
if (emitUpdated) {
if (updated) {
conversation.unreadMessages = 0;
pimpl_->invalidateModel();
Q_EMIT conversationUpdated(convId);
@ -1607,8 +1585,9 @@ ConversationModel::loadConversationMessages(const QString& conversationId, const
if (conversation.allMessagesLoaded) {
return -1;
}
auto lastMsgId = conversation.interactions->empty() ? ""
: conversation.interactions->front().first;
QString lastMsgId;
conversation.interactions->withLast(
[&lastMsgId](const QString& id, interaction::Info&) { lastMsgId = id; });
return ConfigurationManager::instance().loadConversation(owner.id,
conversationId,
lastMsgId,
@ -1719,7 +1698,7 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
connect(&callbacksHandler,
&CallbacksHandler::accountMessageStatusChanged,
this,
&ConversationModelPimpl::slotUpdateInteractionStatus);
&ConversationModelPimpl::updateInteractionStatus);
// Call related
connect(&*linked.owner.contactModel,
@ -1886,7 +1865,7 @@ ConversationModelPimpl::~ConversationModelPimpl()
disconnect(&callbacksHandler,
&CallbacksHandler::accountMessageStatusChanged,
this,
&ConversationModelPimpl::slotUpdateInteractionStatus);
&ConversationModelPimpl::updateInteractionStatus);
// Call related
disconnect(&*linked.owner.contactModel,
@ -2061,23 +2040,22 @@ ConversationModelPimpl::initConversations()
auto convIdx = indexOf(conv[0]);
// Check if file transfer interactions were left in an incorrect state
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
for (auto& interaction : *(conversations[convIdx].interactions)) {
if (interaction.second.status == interaction::Status::TRANSFER_CREATED
|| interaction.second.status == interaction::Status::TRANSFER_AWAITING_HOST
|| interaction.second.status == interaction::Status::TRANSFER_AWAITING_PEER
|| interaction.second.status == interaction::Status::TRANSFER_ONGOING
|| interaction.second.status == interaction::Status::TRANSFER_ACCEPTED) {
// Resolve any file transfer interactions were left in an incorrect state
auto& interactions = conversations[convIdx].interactions;
interactions->forEach([&](const QString& id, interaction::Info& interaction) {
if (interaction.status == interaction::Status::TRANSFER_CREATED
|| interaction.status == interaction::Status::TRANSFER_AWAITING_HOST
|| interaction.status == interaction::Status::TRANSFER_AWAITING_PEER
|| interaction.status == interaction::Status::TRANSFER_ONGOING
|| interaction.status == interaction::Status::TRANSFER_ACCEPTED) {
// If a datatransfer was left in a non-terminal status in DB, we switch this status
// to ERROR
// TODO : Improve for DBus clients as daemon and transfer may still be ongoing
storage::updateInteractionStatus(db,
interaction.first,
interaction::Status::TRANSFER_ERROR);
interaction.second.status = interaction::Status::TRANSFER_ERROR;
storage::updateInteractionStatus(db, id, interaction::Status::TRANSFER_ERROR);
interaction.status = interaction::Status::TRANSFER_ERROR;
}
}
});
}
invalidateModel();
@ -2226,15 +2204,13 @@ ConversationModelPimpl::sort(const conversation::Info& convA, const conversation
if (convA.uid == convB.uid)
return false;
auto& mtxA = interactionsLocks[convA.uid];
auto& mtxB = interactionsLocks[convB.uid];
std::lock(mtxA, mtxB);
std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock);
std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock);
auto& historyA = convA.interactions;
auto& historyB = convB.interactions;
std::lock(historyA->getMutex(), historyB->getMutex());
std::lock_guard<std::recursive_mutex> lockConvA(historyA->getMutex(), std::adopt_lock);
std::lock_guard<std::recursive_mutex> lockConvB(historyB->getMutex(), std::adopt_lock);
// A or B is a new conversation (without CONTACT interaction)
if (convA.uid.isEmpty() || convB.uid.isEmpty())
return convA.uid.isEmpty();
@ -2256,14 +2232,14 @@ ConversationModelPimpl::sort(const conversation::Info& convA, const conversation
if (historyB->empty())
return true;
// Sort by last Interaction
try {
auto lastMessageA = historyA->rbegin()->second;
auto lastMessageB = historyB->rbegin()->second;
return lastMessageA.timestamp > lastMessageB.timestamp;
} catch (const std::exception& e) {
qDebug() << "ConversationModel::sortConversations(), can't get lastMessage";
return false;
}
time_t timestampA, timestampB;
historyA->withLast([&](const QString&, const interaction::Info& interaction) {
timestampA = interaction.timestamp;
});
historyB->withLast([&](const QString&, const interaction::Info& interaction) {
timestampB = interaction.timestamp;
});
return timestampA > timestampB;
}
void
@ -2319,18 +2295,10 @@ ConversationModelPimpl::slotSwarmLoaded(uint32_t requestId,
downloadFile = (bytesProgress == 0);
}
{
// If message is loaded, insert message at beginning
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
auto itExists = conversation.interactions->find(msgId);
// If found, nothing to do.
if (itExists != conversation.interactions->end())
continue;
auto result = conversation.interactions->insert(std::make_pair(msgId, msg), true);
if (!result.second) {
continue;
}
// If message is loaded, insert message at beginning
if (!conversation.interactions->insert(msgId, msg, 0)) {
qDebug() << Q_FUNC_INFO << "Insert failed: duplicate ID";
continue;
}
if (downloadFile) {
@ -2380,13 +2348,13 @@ ConversationModelPimpl::slotMessagesFound(uint32_t requestId,
bytesProgress);
intInfo.body = path;
}
messageDetailedInformation[msg["id"]] = intInfo;
messageDetailedInformation[msg["id"]] = std::move(intInfo);
}
} else if (requestId == msgResearchRequestId) {
Q_FOREACH (const MapStringString& msg, messageIds) {
auto intInfo = interaction::Info(msg, "");
if (intInfo.type == interaction::Type::TEXT) {
messageDetailedInformation[msg["id"]] = intInfo;
messageDetailedInformation[msg["id"]] = std::move(intInfo);
}
}
}
@ -2437,23 +2405,11 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
}
{
// If message is received, insert message after its parent.
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
auto itExists = conversation.interactions->find(msgId);
// If found, nothing to do.
if (itExists != conversation.interactions->end())
return;
int index = conversation.interactions->indexOfMessage(msg.parentId);
if (index >= 0) {
auto result = conversation.interactions->insert(index + 1, qMakePair(msgId, msg));
if (!result.second) {
return;
}
} else {
return;
}
if (!conversation.interactions->append(msgId, msg)) {
qDebug() << Q_FUNC_INFO << "Append failed: duplicate ID" << msgId;
return;
}
auto updateUnread = msg.authorUri != linked.owner.profileInfo.uri;
if (updateUnread)
conversation.unreadMessages++;
@ -2491,26 +2447,11 @@ ConversationModelPimpl::slotMessageUpdated(const QString& accountId,
QString msgId = message.id;
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
auto itExists = conversation.interactions->find(msgId);
// If not found, nothing to do.
if (itExists == conversation.interactions->end())
return;
// Now there is two cases:
// ParentId changed, in this case, remove previous message and re-insert at new place
// Else, just update body
conversation.interactions->erase(itExists);
int index = conversation.interactions->indexOfMessage(msg.parentId);
if (index >= 0) {
auto result = conversation.interactions->insert(index + 1, qMakePair(msgId, msg));
if (!result.second) {
return;
}
} else {
return;
}
if (!conversation.interactions->update(msgId, msg)) {
qDebug() << "message not found or could not be reparented";
return;
}
// The conversation is updated, so we need to notify the view.
invalidateModel();
Q_EMIT linked.modelChanged();
Q_EMIT linked.dataChanged(indexOf(conversationId));
@ -2870,11 +2811,7 @@ ConversationModelPimpl::addConversationRequest(const MapStringString& convReques
{"linearizedParent", ""},
};
auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
conversation.interactions->insert(std::make_pair(convId, msg), true);
}
conversation.interactions->insert(convId, msg);
// add the author to the contact model's contact list as a PENDING
// if they aren't already a contact
@ -2935,8 +2872,7 @@ ConversationModelPimpl::slotPendingContactAccepted(const QString& uri)
interaction::Status::SUCCESS);
auto convIdx = indexOf(convs[0]);
if (convIdx >= 0) {
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
conversations[convIdx].interactions->emplace(msgId, interaction);
conversations[convIdx].interactions->append(msgId, interaction);
}
filteredConversations.invalidate();
Q_EMIT linked.newInteraction(convs[0], msgId, interaction);
@ -2958,7 +2894,7 @@ ConversationModelPimpl::slotContactRemoved(const QString& uri)
}
// actually remove them from the list
for (auto id : convIdsToRemove) {
for (const auto& id : convIdsToRemove) {
eraseConversation(id);
Q_EMIT linked.conversationRemoved(id);
}
@ -3092,11 +3028,7 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId)
{"linearizedParent", ""},
};
auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
conversation.interactions->insert(std::make_pair(convId, msg), true);
}
conversation.interactions->append(convId, msg);
conversation.needsSyncing = true;
Q_EMIT linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
@ -3127,34 +3059,31 @@ ConversationModelPimpl::addConversationWith(const QString& convId,
conversation.callId = "";
}
storage::getHistory(db, conversation, linked.owner.profileInfo.uri);
std::vector<std::function<void(void)>> updateSlots;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
for (auto& interaction : (*(conversation.interactions))) {
if (interaction.second.status != interaction::Status::SENDING) {
continue;
}
// Get the message status from daemon, else unknown
auto id = storage::getDaemonIdByInteractionId(db, interaction.first);
int status = 0;
if (id.isEmpty()) {
continue;
}
try {
auto msgId = std::stoull(id.toStdString());
status = ConfigurationManager::instance().getMessageStatus(msgId);
updateSlots.emplace_back([this, convId, contactUri, id, status]() -> void {
auto accId = linked.owner.id;
slotUpdateInteractionStatus(accId, convId, contactUri, id, status);
});
} catch (const std::exception& e) {
qDebug() << "message id was invalid";
}
QList<std::function<void(void)>> toUpdate;
conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
if (interaction.status != interaction::Status::SENDING) {
return;
}
}
for (const auto& s : updateSlots) {
s();
}
// Get the message status from daemon, else unknown
auto daemonId = storage::getDaemonIdByInteractionId(db, id);
int status = 0;
if (daemonId.isEmpty()) {
return;
}
try {
auto msgId = std::stoull(daemonId.toStdString());
status = ConfigurationManager::instance().getMessageStatus(msgId);
toUpdate.emplace_back([this, convId, contactUri, daemonId, status]() {
auto accId = linked.owner.id;
updateInteractionStatus(accId, convId, contactUri, daemonId, status);
});
} catch (const std::exception& e) {
qWarning() << Q_FUNC_INFO << "Failed: message id was invalid";
}
});
Q_FOREACH (const auto& func, toUpdate)
func();
conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
@ -3296,7 +3225,7 @@ ConversationModelPimpl::slotCallStatusChanged(const QString& callId, int code)
if (i != conversations.end()) {
// Update interaction status
invalidateModel();
Q_EMIT linked.selectConversation(i->uid);
linked.selectConversation(i->uid);
Q_EMIT linked.conversationUpdated(i->uid);
Q_EMIT linked.dataChanged(indexOf(i->uid));
}
@ -3378,7 +3307,6 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
// do not save call interaction for swarm conversation
if (conv_it->isSwarm())
return;
auto uid = conv_it->uid;
auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type)
: linked.owner.profileInfo.uri;
auto msg = interaction::Info {uriString,
@ -3393,20 +3321,12 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
// now set the formatted call message string in memory only
msg.body = interaction::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri,
msg);
bool newInteraction = false;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
auto interactionIt = conv_it->interactions->find(msgId);
newInteraction = interactionIt == conv_it->interactions->end();
if (newInteraction) {
conv_it->interactions->emplace(msgId, msg);
} else {
interactionIt->second = msg;
conv_it->interactions->emitDataChanged(interactionIt);
}
auto [added, success] = conv_it->interactions->addOrUpdate(msgId, msg);
if (!success) {
qWarning() << Q_FUNC_INFO << QString("Failed: to %1 msg").arg(added ? "add" : "update");
return;
}
if (newInteraction)
if (added)
Q_EMIT linked.newInteraction(conv_it->uid, msgId, msg);
invalidateModel();
@ -3423,11 +3343,12 @@ ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
if (accountId != linked.owner.id)
return;
for (const auto& payload : payloads.keys()) {
for (auto it = payloads.constBegin(); it != payloads.constEnd(); ++it) {
const auto& payload = it.key();
if (payload.contains(TEXT_PLAIN)) {
addIncomingMessage(peerId, payloads.value(payload), 0, msgId);
addIncomingMessage(peerId, it.value(), 0, msgId);
} else {
qWarning() << payload;
qDebug() << payload;
}
}
}
@ -3504,10 +3425,8 @@ ConversationModelPimpl::addIncomingMessage(const QString& peerId,
addConversationWith(convIds[0], peerId, isRequest);
Q_EMIT linked.newConversation(convIds[0]);
} else {
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
conversations[conversationIdx].interactions->emplace(msgId, msg);
}
// Maybe check if this is failing?
conversations[conversationIdx].interactions->append(msgId, msg);
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
}
@ -3532,25 +3451,25 @@ ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const Q
MapStringString confDetails = CallManager::instance()
.getConferenceDetails(linked.owner.id, confId);
if (confDetails["STATE"] == "ACTIVE_ATTACHED")
Q_EMIT linked.selectConversation(conversation.uid);
linked.selectConversation(conversation.uid);
return;
}
}
}
void
ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerId,
const QString& messageId,
int status)
ConversationModelPimpl::updateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerUri,
const QString& messageId,
int status)
{
if (accountId != linked.owner.id) {
return;
}
// it may be not swarm conversation check in db
// non-swarm conversation
if (conversationId.isEmpty() || conversationId == linked.owner.profileInfo.uri) {
auto convIds = storage::getConversationsWithPeer(db, peerId);
auto convIds = storage::getConversationsWithPeer(db, peerUri);
if (convIds.empty()) {
return;
}
@ -3591,79 +3510,83 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
idString = QString::number(id);
}
// Update database
auto interactionId = storage::getInteractionIdByDaemonId(db, idString);
if (interactionId.isEmpty()) {
auto msgId = storage::getInteractionIdByDaemonId(db, idString);
if (msgId.isEmpty()) {
return;
}
auto msgId = interactionId;
storage::updateInteractionStatus(db, msgId, newStatus);
// Update conversations
bool emitUpdated = false;
bool updated = false;
bool updateDisplayedUid = false;
QString oldDisplayedUid = 0;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
auto& interactions = conversation.interactions;
auto it = interactions->find(msgId);
auto messageId = conversation.interactions->getRead(peerId);
if (it != interactions->end()) {
it->second.status = newStatus;
interactions->emitDataChanged(it, {MessageList::Role::Status});
bool interactionDisplayed = newStatus == interaction::Status::DISPLAYED
&& isOutgoing(it->second);
if (messageId != "") {
auto lastDisplayedIt = interactions->find(messageId);
bool interactionIsLast = lastDisplayedIt == interactions->end()
|| lastDisplayedIt->second.timestamp
< it->second.timestamp;
updateDisplayedUid = interactionDisplayed && interactionIsLast;
if (updateDisplayedUid) {
oldDisplayedUid = messageId;
if (peerId != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerId, it->first);
// Try to update the status.
if (interactions->updateStatus(msgId, newStatus)) {
updated = true;
interactions->with(msgId, [&](const QString& id, interaction::Info& interaction) {
// Determine if the interaction is outgoing and has been displayed.
bool interactionIsDisplayed = newStatus == interaction::Status::DISPLAYED
&& interaction::isOutgoing(interaction);
// Get the last displayed interaction ID and timestamp for this peer.
auto [lastIdForPeer, lastTimestampForPeer]
= interactions->getDisplayedInfoForPeer(peerUri);
if (lastIdForPeer.isEmpty()) {
oldDisplayedUid = "";
if (peerUri != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerUri, msgId);
updateDisplayedUid = true;
} else {
bool interactionIsLast = lastTimestampForPeer < interaction.timestamp;
updateDisplayedUid = interactionIsDisplayed && interactionIsLast;
if (updateDisplayedUid) {
oldDisplayedUid = messageId;
if (peerUri != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerUri, msgId);
}
}
} else {
oldDisplayedUid = "";
if (peerId != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerId, it->first);
updateDisplayedUid = true;
}
emitUpdated = true;
});
}
}
if (updateDisplayedUid) {
Q_EMIT linked.displayedInteractionChanged(conversation.uid,
peerId,
peerUri,
oldDisplayedUid,
msgId);
}
if (emitUpdated) {
if (updated) {
invalidateModel();
}
return;
}
// swarm conversation
try {
auto& conversation = getConversationForUid(conversationId).get();
if (conversation.mode != conversation::Mode::NON_SWARM) {
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
if (conversation.isSwarm()) {
using namespace libjami::Account;
auto msgState = static_cast<MessageStates>(status);
auto& interactions = conversation.interactions;
auto it = interactions->find(messageId);
if (it != interactions->end() && it->second.type == interaction::Type::TEXT) {
if (static_cast<libjami::Account::MessageStates>(status)
== libjami::Account::MessageStates::SENDING) {
it->second.status = interaction::Status::SENDING;
} else if (static_cast<libjami::Account::MessageStates>(status)
== libjami::Account::MessageStates::SENT) {
it->second.status = interaction::Status::SUCCESS;
}
interactions->emitDataChanged(it, {MessageList::Role::Status});
}
interactions->with(messageId,
[&](const QString& id, const interaction::Info& interaction) {
if (interaction.type == interaction::Type::TEXT) {
interaction::Status newState;
if (msgState == MessageStates::SENDING) {
newState = interaction::Status::SENDING;
} else if (msgState == MessageStates::SENT) {
newState = interaction::Status::SUCCESS;
} else {
return;
}
interactions->updateStatus(id, newState);
}
});
if (static_cast<libjami::Account::MessageStates>(status)
== libjami::Account::MessageStates::DISPLAYED) {
auto previous = conversation.interactions->getRead(peerId);
if (peerId != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerId, messageId);
if (msgState == MessageStates::DISPLAYED) {
auto previous = conversation.interactions->getRead(peerUri);
if (peerUri != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerUri, messageId);
else {
// Here, this means that the daemon synced the displayed message
// so, compute the number of unread messages.
@ -3672,11 +3595,11 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
conversationId,
messageId,
"",
peerId);
peerUri);
Q_EMIT linked.dataChanged(indexOf(conversationId));
}
Q_EMIT linked.displayedInteractionChanged(conversationId,
peerId,
peerUri,
previous,
messageId);
}
@ -3843,13 +3766,8 @@ ConversationModel::cancelTransfer(const QString& convUid, const QString& fileId)
auto conversationIdx = pimpl_->indexOf(convUid);
bool emitUpdated = false;
if (conversationIdx != -1) {
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convUid]);
auto& interactions = pimpl_->conversations[conversationIdx].interactions;
auto it = interactions->find(fileId);
if (it != interactions->end()) {
it->second.status = interaction::Status::TRANSFER_CANCELED;
interactions->emitDataChanged(it, {MessageList::Role::Status});
if (interactions->updateStatus(fileId, interaction::Status::TRANSFER_CANCELED)) {
// update information in the db
storage::updateInteractionStatus(pimpl_->db,
fileId,
@ -3901,14 +3819,7 @@ ConversationModel::removeFile(const QString& conversationId,
return;
QFile::remove(path);
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convOpt->get().uid]);
auto& interactions = convOpt->get().interactions;
auto it = interactions->find(interactionId);
if (it != interactions->end()) {
it->second.status = interaction::Status::TRANSFER_AWAITING_HOST;
interactions->emitDataChanged(it, {MessageList::Role::Status});
}
convOpt->get().interactions->updateStatus(interactionId, interaction::Status::TRANSFER_CANCELED);
}
int
@ -3991,10 +3902,7 @@ ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatra
addConversationWith(convId, info.peerUri, isRequest);
Q_EMIT linked.newConversation(convId);
} else {
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
conversations[conversationIdx].interactions->emplace(interactionId, interaction);
}
conversations[conversationIdx].interactions->append(interactionId, interaction);
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
}
Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id,
@ -4087,15 +3995,18 @@ ConversationModelPimpl::acceptTransfer(const QString& convUid, const QString& in
if (conversation.isLegacy()) // Ignore legacy
return;
auto interaction = conversation.interactions->find(interactionId);
if (interaction != conversation.interactions->end()) {
auto fileId = interaction->second.commit["fileId"];
if (fileId.isEmpty()) {
qWarning() << "Cannot download file without fileId";
return;
}
linked.owner.dataTransferModel->download(linked.owner.id, convUid, interactionId, fileId);
} else {
auto& interactions = conversation.interactions;
if (!interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
auto fileId = interaction.commit["fileId"];
if (fileId.isEmpty()) {
qWarning() << "Cannot download file without fileId";
return;
}
linked.owner.dataTransferModel->download(linked.owner.id,
convUid,
interactionId,
fileId);
})) {
qWarning() << "Cannot download file without valid interaction";
}
}
@ -4150,7 +4061,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatra
}
auto conversationIdx = indexOf(conversationId);
auto* timer = new QTimer();
connect(timer, &QTimer::timeout, [=] {
connect(timer, &QTimer::timeout, this, [=] {
updateTransferProgress(timer, conversationIdx, interactionId);
});
timer->start(1000);
@ -4170,20 +4081,15 @@ ConversationModelPimpl::slotTransferStatusFinished(const QString& fileId, datatr
if (conversationIdx != -1) {
bool emitUpdated = false;
auto newStatus = interaction::Status::TRANSFER_FINISHED;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions->find(interactionId);
if (it != interactions->end()) {
// We need to check if current status is ONGOING as CANCELED must not be
// transformed into FINISHED
if (it->second.status == interaction::Status::TRANSFER_ONGOING) {
emitUpdated = true;
it->second.status = newStatus;
interactions->emitDataChanged(it, {MessageList::Role::Status});
}
auto& interactions = conversations[conversationIdx].interactions;
interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
// We need to check if current status is ONGOING as CANCELED must not be
// transformed into FINISHED
if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
emitUpdated = true;
interactions->updateStatus(id, newStatus);
}
}
});
if (emitUpdated) {
invalidateModel();
if (conversations[conversationIdx].mode != conversation::Mode::NON_SWARM) {
@ -4256,23 +4162,10 @@ ConversationModelPimpl::updateTransferStatus(const QString& fileId,
if (conversation.isLegacy()) {
storage::updateInteractionStatus(db, interactionId, newStatus);
}
bool emitUpdated = false;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions->find(interactionId);
if (it != interactions->end()) {
emitUpdated = true;
VectorInt roles;
it->second.status = newStatus;
roles += MessageList::Role::Status;
if (conversation.isSwarm()) {
it->second.body = info.path;
roles += MessageList::Role::Body;
}
interactions->emitDataChanged(it, roles);
}
}
auto& interactions = conversations[conversationIdx].interactions;
bool emitUpdated = interactions->updateStatus(interactionId,
newStatus,
conversation.isSwarm() ? info.path : QString());
if (emitUpdated) {
invalidateModel();
}
@ -4288,15 +4181,13 @@ ConversationModelPimpl::updateTransferProgress(QTimer* timer,
try {
bool emitUpdated = false;
{
auto convId = conversations[conversationIdx].uid;
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
const auto& interactions = conversations[conversationIdx].interactions;
const auto& it = interactions->find(interactionId);
if (it != interactions->cend()
and it->second.status == interaction::Status::TRANSFER_ONGOING) {
interactions->emitDataChanged(it, {MessageList::Role::Status});
emitUpdated = true;
}
interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
emitUpdated = true;
interactions->updateStatus(id, interaction::Status::TRANSFER_ONGOING);
}
});
}
if (emitUpdated)
return;

View file

@ -1,9 +1,6 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
*
* This program is free 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
@ -19,334 +16,16 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "messagelistmodel.h"
#include "api/messagelistmodel.h"
#include "authority/storagehelper.h"
#include "api/accountmodel.h"
#include "api/contactmodel.h"
#include "api/conversationmodel.h"
#include "api/interaction.h"
#include "qtwrapper/conversions_wrap.hpp"
#include <QAbstractListModel>
#include <QFileInfo>
namespace lrc {
using namespace api;
using constIterator = MessageListModel::constIterator;
using iterator = MessageListModel::iterator;
using reverseIterator = MessageListModel::reverseIterator;
MessageListModel::MessageListModel(const account::Info* account, QObject* parent)
: QAbstractListModel(parent)
, account_(account)
{}
QPair<iterator, bool>
MessageListModel::emplace(const QString& msgId, interaction::Info message, bool beginning)
{
iterator it;
for (it = interactions_.begin(); it != interactions_.end(); ++it) {
if (it->first == msgId) {
return qMakePair(it, false);
}
}
auto iter = beginning ? interactions_.begin() : interactions_.end();
auto iterator = insertMessage(iter, qMakePair(msgId, message));
return qMakePair(iterator, true);
}
iterator
MessageListModel::find(const QString& msgId)
{
iterator it;
for (it = interactions_.begin(); it != interactions_.end(); ++it) {
if (it->first == msgId) {
return it;
}
}
return interactions_.end();
}
iterator
MessageListModel::findActiveCall(const MapStringString& commit)
{
iterator it;
for (it = interactions_.begin(); it != interactions_.end(); ++it) {
const auto& itCommit = it->second.commit;
if (itCommit["confId"] == commit["confId"] && itCommit["uri"] == commit["uri"]
&& itCommit["device"] == commit["device"]) {
return it;
}
}
return interactions_.end();
}
iterator
MessageListModel::erase(const iterator& it)
{
auto index = std::distance(begin(), it);
Q_EMIT beginRemoveRows(QModelIndex(), index, index);
auto erased = interactions_.erase(it);
Q_EMIT endRemoveRows();
return erased;
}
constIterator
MessageListModel::find(const QString& msgId) const
{
constIterator it;
for (it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
if (it->first == msgId) {
return it;
}
}
return interactions_.cend();
}
QPair<iterator, bool>
MessageListModel::insert(std::pair<QString, interaction::Info> message, bool beginning)
{
return emplace(message.first, message.second, beginning);
}
int
MessageListModel::erase(const QString& msgId)
{
iterator it;
int index = 0;
for (it = interactions_.begin(); it != interactions_.end(); ++it) {
if (it->first == msgId) {
removeMessage(index, it);
return 1;
}
index++;
}
return 0;
}
interaction::Info&
MessageListModel::operator[](const QString& messageId)
{
for (auto it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
if (it->first == messageId) {
return const_cast<interaction::Info&>(it->second);
}
}
// element not find, add it to the end
interaction::Info newMessage = {};
insertMessage(interactions_.end(), qMakePair(messageId, newMessage));
if (interactions_.last().first == messageId) {
return const_cast<interaction::Info&>(interactions_.last().second);
}
throw std::out_of_range("Cannot find message");
}
iterator
MessageListModel::end()
{
return interactions_.end();
}
constIterator
MessageListModel::end() const
{
return interactions_.end();
}
reverseIterator
MessageListModel::rend()
{
return interactions_.rend();
}
constIterator
MessageListModel::cend() const
{
return interactions_.cend();
}
iterator
MessageListModel::begin()
{
return interactions_.begin();
}
constIterator
MessageListModel::begin() const
{
return interactions_.begin();
}
reverseIterator
MessageListModel::rbegin()
{
return interactions_.rbegin();
}
int
MessageListModel::size() const
{
return interactions_.size();
}
void
MessageListModel::clear()
{
Q_EMIT beginResetModel();
interactions_.clear();
replyTo_.clear();
Q_EMIT endResetModel();
}
void
MessageListModel::reloadHistory()
{
Q_EMIT beginResetModel();
for (auto& interaction : interactions_) {
interaction.second.linkPreviewInfo.clear();
}
Q_EMIT endResetModel();
}
bool
MessageListModel::empty() const
{
return interactions_.empty();
}
interaction::Info
MessageListModel::at(const QString& msgId) const
{
for (auto it = interactions_.cbegin(); it != interactions_.cend(); ++it) {
if (it->first == msgId) {
return it->second;
}
}
return {};
}
QPair<QString, interaction::Info>
MessageListModel::front() const
{
return interactions_.front();
}
QPair<QString, interaction::Info>
MessageListModel::last() const
{
return interactions_.last();
}
QPair<QString, interaction::Info>
MessageListModel::atIndex(int index) const
{
return interactions_.at(index);
}
QPair<iterator, bool>
MessageListModel::insert(int index, QPair<QString, interaction::Info> message)
{
iterator itr;
for (itr = interactions_.begin(); itr != interactions_.end(); ++itr) {
if (itr->first == message.first) {
return qMakePair(itr, false);
}
}
if (index >= size()) {
auto iterator = insertMessage(interactions_.end(), message);
return qMakePair(iterator, true);
}
insertMessage(index, message);
return qMakePair(interactions_.end(), true);
}
int
MessageListModel::indexOfMessage(const QString& msgId, bool reverse) const
{
auto getIndex = [reverse, &msgId](const auto& start, const auto& end) -> int {
auto it = std::find_if(start, end, [&msgId](const auto& it) { return it.first == msgId; });
if (it == end) {
return -1;
}
return reverse ? std::distance(it, end) - 1 : std::distance(start, it);
};
return reverse ? getIndex(interactions_.rbegin(), interactions_.rend())
: getIndex(interactions_.begin(), interactions_.end());
}
void
MessageListModel::updateReplies(item_t& message)
{
auto replyId = message.second.commit["reply-to"];
auto commitId = message.second.commit["id"];
if (!replyId.isEmpty()) {
replyTo_[replyId].insert(commitId);
}
for (const auto& msgId : replyTo_[commitId]) {
int index = getIndexOfMessage(msgId);
if (index == -1)
continue;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToAuthor, Role::ReplyToBody});
}
}
void
MessageListModel::insertMessage(int index, item_t& message)
{
Q_EMIT beginInsertRows(QModelIndex(), index, index);
interactions_.insert(index, message);
Q_EMIT endInsertRows();
updateReplies(message);
}
iterator
MessageListModel::insertMessage(iterator it, item_t& message)
{
auto index = std::distance(begin(), it);
Q_EMIT beginInsertRows(QModelIndex(), index, index);
auto insertion = interactions_.insert(it, message);
Q_EMIT endInsertRows();
updateReplies(message);
return insertion;
}
void
MessageListModel::removeMessage(int index, iterator it)
{
Q_EMIT beginRemoveRows(QModelIndex(), index, index);
interactions_.erase(it);
Q_EMIT endRemoveRows();
}
bool
MessageListModel::contains(const QString& msgId)
{
return find(msgId) != interactions_.end();
}
int
MessageListModel::rowCount(const QModelIndex&) const
{
return interactions_.size();
}
QHash<int, QByteArray>
MessageListModel::roleNames() const
{
using namespace MessageList;
QHash<int, QByteArray> roles;
#define X(role) roles[role] = #role;
MSG_ROLES
#undef X
return roles;
}
bool
MessageListModel::isOnlyEmoji(const QString& text) const
static bool
isOnlyEmoji(const QString& text)
{
if (text.isEmpty())
return false;
@ -370,14 +49,408 @@ MessageListModel::isOnlyEmoji(const QString& text) const
return true;
}
QVariant
MessageListModel::dataForItem(item_t item, int, int role) const
namespace lrc {
using namespace api;
MessageListModel::MessageListModel(const account::Info* account, QObject* parent)
: QAbstractListModel(parent)
, account_(account)
{}
int
MessageListModel::rowCount(const QModelIndex&) const
{
QString replyId = item.second.commit["reply-to"];
int repliedMsg = -1;
if (!replyId.isEmpty() && (role == Role::ReplyToAuthor || role == Role::ReplyToBody)) {
repliedMsg = getIndexOfMessage(replyId);
std::lock_guard<std::recursive_mutex> lk(mutex_);
return interactions_.size();
}
QVariant
MessageListModel::data(const QModelIndex& index, int role) const
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return {};
}
return dataForItem(interactions_.at(index.row()), index.row(), role);
}
QHash<int, QByteArray>
MessageListModel::roleNames() const
{
using namespace MessageList;
QHash<int, QByteArray> roles;
#define X(role) roles[role] = #role;
MSG_ROLES
#undef X
return roles;
}
QVariant
MessageListModel::data(const QString& id, int role) const
{
return data(indexOfMessage(id), role);
}
bool
MessageListModel::empty() const
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
return interactions_.empty();
}
int
MessageListModel::indexOfMessage(const QString& messageId) const
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
auto it = std::find_if(interactions_.rbegin(),
interactions_.rend(),
[&messageId](const auto& it) { return it.first == messageId; });
if (it == interactions_.rend()) {
return -1;
}
return std::distance(it, interactions_.rend()) - 1;
}
void
MessageListModel::clear()
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
beginResetModel();
interactions_.clear();
replyTo_.clear();
endResetModel();
}
void
MessageListModel::reloadHistory()
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
beginResetModel();
for (auto& interaction : interactions_) {
interaction.second.linkPreviewInfo.clear();
}
endResetModel();
}
bool
MessageListModel::insert(const QString& id, const interaction::Info& interaction, int index)
{
const std::lock_guard<std::recursive_mutex> lk(mutex_);
// If the index parameter is -1, then insert at the parent of the message.
if (index == -1) {
index = indexOfMessage(interaction.parentId);
}
// The index should be valid and don't add duplicate messages.
if (index < 0 || index > interactions_.size() || find(id) != interactions_.end()) {
return false;
}
beginInsertRows(QModelIndex(), index, index);
interactions_.emplace(interactions_.cbegin() + index, id, interaction);
endInsertRows();
return true;
}
bool
MessageListModel::append(const QString& id, const interaction::Info& interaction)
{
const std::lock_guard<std::recursive_mutex> lk(mutex_);
// Don't add duplicate messages.
if (find(id) != interactions_.end()) {
return false;
}
beginInsertRows(QModelIndex(), interactions_.size(), interactions_.size());
interactions_.emplace_back(id, interaction);
endInsertRows();
return true;
}
bool
MessageListModel::update(const QString& id, const interaction::Info& interaction)
{
// There are two cases: a) Parent ID changed, b) body changed (edit/delete).
const std::lock_guard<std::recursive_mutex> lk(mutex_);
auto it = find(id);
if (find(id) == interactions_.end()) {
return false;
}
interaction::Info& current = it->second;
if (current.parentId != interaction.parentId) {
// Parent ID changed, in this case, move the interaction to the new parent.
it->second.parentId = interaction.parentId;
auto newIndex = move(it, interaction.parentId);
if (newIndex >= 0) {
// The iterator is invalid now. But we can update all the roles.
auto modelIndex = QAbstractListModel::index(newIndex);
Q_EMIT dataChanged(modelIndex, modelIndex, roleNames().keys());
return true;
}
}
// Just update bodies notify the view.
current.body = interaction.body;
current.previousBodies = interaction.previousBodies;
current.parsedBody = interaction.parsedBody;
auto modelIndex = QAbstractListModel::index(indexOfMessage(id), 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Body, Role::PreviousBodies, Role::ParsedBody});
return true;
}
bool
MessageListModel::updateStatus(const QString& id,
interaction::Status newStatus,
const QString& newBody)
{
const std::lock_guard<std::recursive_mutex> lk(mutex_);
auto it = find(id);
if (it == interactions_.end()) {
return false;
}
VectorInt roles;
it->second.status = newStatus;
roles.push_back(Role::Status);
if (!newBody.isEmpty()) {
it->second.body = newBody;
roles.push_back(Role::Body);
}
auto modelIndex = QAbstractListModel::index(indexOfMessage(id), 0);
Q_EMIT dataChanged(modelIndex, modelIndex, roles);
return true;
}
QPair<bool, bool>
MessageListModel::addOrUpdate(const QString& id, const interaction::Info& interaction)
{
if (find(id) == interactions_.end()) {
// The ID doesn't exist, appending cannot fail here.
return {true, append(id, interaction)};
} else {
// Update can only fail if the new parent ID is invalid.
return {false, update(id, interaction)};
}
}
void
MessageListModel::forEach(const InteractionCb& callback)
{
const std::lock_guard<std::recursive_mutex> lk(mutex_);
for (auto& interaction : interactions_) {
callback(interaction.first, interaction.second);
}
}
bool
MessageListModel::with(const QString& idHint, const InteractionCb& callback)
{
const std::lock_guard<std::recursive_mutex> lk(mutex_);
if (interactions_.empty()) {
return false;
}
// If the ID is empty, then return the last interaction.
auto it = idHint.isEmpty() ? std::prev(interactions_.end()) : find(idHint);
if (it == interactions_.end()) {
return false;
}
callback(it->first, it->second);
return true;
}
bool
MessageListModel::withLast(const InteractionCb& callback)
{
return with(QString(), callback);
}
std::recursive_mutex&
MessageListModel::getMutex()
{
return mutex_;
}
void
MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& info)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
int index = indexOfMessage(messageId);
if (index == -1) {
return;
}
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
interactions_[index].second.linkPreviewInfo = info;
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::LinkPreviewInfo});
}
void
MessageListModel::addReaction(const QString& messageId, const MapStringString& reaction)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
int index = indexOfMessage(messageId);
if (index == -1)
return;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
auto emoji = api::interaction::Emoji {reaction["id"], reaction["body"]};
auto& pList = interactions_[index].second.reactions[reaction["author"]];
QList<QVariant> newList = pList.toList();
newList.emplace_back(QVariant::fromValue(emoji));
pList = QVariantList::fromVector(newList);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
}
void
MessageListModel::rmReaction(const QString& messageId, const QString& reactionId)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
int index = indexOfMessage(messageId);
if (index == -1)
return;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
auto& reactions = interactions_[index].second.reactions;
for (auto reactionIt = reactions.begin(); reactionIt != reactions.end(); ++reactionIt) {
// Use a temporary QList to store updated emojis
QList<QVariant> updatedEmojis;
bool found = false;
for (const auto& item : reactionIt.value().toList()) {
auto emoji = item.value<api::interaction::Emoji>();
if (emoji.commitId != reactionId || found)
updatedEmojis.append(item);
else {
found = true;
break;
}
}
if (found) {
// Update the reactions with the modified list
reactionIt.value() = QVariant::fromValue(updatedEmojis);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
return;
}
}
}
void
MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
int index = indexOfMessage(messageId);
if (index == -1) {
return;
}
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
interactions_[index].second.parsedBody = parsed;
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody});
}
void
MessageListModel::setRead(const QString& peer, const QString& messageId)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
auto i = lastDisplayedMessageUid_.find(peer);
if (i != lastDisplayedMessageUid_.end()) {
auto old = i.value();
messageToReaders_[old].removeAll(peer);
auto msgIdx = indexOfMessage(old);
// Remove from latest read
if (msgIdx != -1) {
QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
}
}
// update map
lastDisplayedMessageUid_[peer] = messageId;
messageToReaders_[messageId].append(peer);
// update interaction
auto msgIdx = indexOfMessage(messageId);
// Remove from latest read
if (msgIdx != -1) {
QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
}
}
QString
MessageListModel::getRead(const QString& peer)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
auto i = lastDisplayedMessageUid_.find(peer);
if (i != lastDisplayedMessageUid_.end())
return i.value();
return "";
}
QString
MessageListModel::lastSelfMessageId(const QString& id) const
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) {
auto lastType = it->second.type;
if (lastType == interaction::Type::TEXT and !it->second.body.isEmpty()
and (it->second.authorUri.isEmpty() || it->second.authorUri == id)) {
return it->first;
}
}
return {};
}
QPair<QString, time_t>
MessageListModel::getDisplayedInfoForPeer(const QString& peerId)
{
std::lock_guard<std::recursive_mutex> lk(mutex_);
auto it = lastDisplayedMessageUid_.find(peerId);
if (it == lastDisplayedMessageUid_.end())
return {};
const auto interaction = find(it.value());
if (interaction == interactions_.end())
return {};
return {it.value(), interaction->second.timestamp};
}
MessageListModel::iterator
MessageListModel::find(const QString& msgId)
{
// Note: assumes that the caller has locked the mutex.
return std::find_if(interactions_.begin(), interactions_.end(), [&msgId](const auto& it) {
return it.first == msgId;
});
}
int
MessageListModel::move(iterator it, const QString& newParentId)
{
// Note: assumes the new parent exists and that the caller has locked the mutex.
auto oldIndex = indexOfMessage(it->first);
auto newIndex = indexOfMessage(newParentId) + 1;
if (newIndex >= 0 && oldIndex != newIndex) {
qDebug() << "Moving message" << it->first << "from" << oldIndex << "to" << newIndex;
beginMoveRows(QModelIndex(), oldIndex, oldIndex, QModelIndex(), newIndex);
interactions_.move(oldIndex, newIndex);
endMoveRows();
return newIndex;
}
return -1;
}
QVariant
MessageListModel::data(int idx, int role) const
{
QModelIndex index = QAbstractListModel::index(idx, 0);
return data(index, role);
}
QVariant
MessageListModel::dataForItem(const item_t& item, int, int role) const
{
// Used only for reply roles.
const auto getReplyIndex = [this, &item, &role]() -> int {
QString replyId = item.second.commit["reply-to"];
int repliedMsgIndex = -1;
if (!replyId.isEmpty() && (role == Role::ReplyToAuthor || role == Role::ReplyToBody)) {
repliedMsgIndex = indexOfMessage(replyId);
}
return repliedMsgIndex;
};
switch (role) {
case Role::Id:
return QVariant(item.first);
@ -440,16 +513,19 @@ MessageListModel::dataForItem(item_t item, int, int role) const
return variantList;
}
case Role::ReplyTo:
return QVariant(replyId);
case Role::ReplyToAuthor:
return repliedMsg == -1 ? QVariant("") : QVariant(data(repliedMsg, Role::Author));
return QVariant(item.second.commit["reply-to"]);
case Role::ReplyToAuthor: {
const auto replyIndex = getReplyIndex();
return replyIndex == -1 ? QVariant("") : data(replyIndex, Role::Author);
}
case Role::ReplyToBody: {
if (repliedMsg == -1)
const auto replyIndex = getReplyIndex();
if (replyIndex == -1)
return QVariant("");
auto parsed = data(repliedMsg, Role::ParsedBody).toString();
auto parsed = data(replyIndex, Role::ParsedBody).toString();
if (!parsed.isEmpty())
return QVariant(parsed);
return QVariant(data(repliedMsg, Role::Body).toString());
return QVariant(data(replyIndex, Role::Body).toString());
}
case Role::TotalSize:
return QVariant(item.second.commit["totalSize"].toInt());
@ -460,173 +536,35 @@ MessageListModel::dataForItem(item_t item, int, int role) const
case Role::Readers:
return QVariant(messageToReaders_[item.first]);
case Role::IsEmojiOnly:
return QVariant(replyId.isEmpty() && item.second.previousBodies.isEmpty()
&& isOnlyEmoji(item.second.body));
return QVariant(item.second.commit["reply-to"].isEmpty()
&& item.second.previousBodies.isEmpty() && isOnlyEmoji(item.second.body));
case Role::Reactions:
return QVariant(item.second.reactions);
case Role::Index:
// For DEBUG only
return QVariant(indexOfMessage(item.first));
default:
return {};
}
}
QVariant
MessageListModel::data(int idx, int role) const
{
QModelIndex index = QAbstractListModel::index(idx, 0);
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return {};
}
return dataForItem(interactions_.at(index.row()), index.row(), role);
}
QVariant
MessageListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return {};
}
return dataForItem(interactions_.at(index.row()), index.row(), role);
}
int
MessageListModel::getIndexOfMessage(const QString& messageId) const
{
for (int i = 0; i < interactions_.size(); i++) {
if (atIndex(i).first == messageId) {
return i;
}
}
return -1;
}
void
MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap& info)
MessageListModel::updateReplies(const item_t& message)
{
int index = getIndexOfMessage(messageId);
if (index == -1) {
return;
auto replyId = message.second.commit["reply-to"];
auto commitId = message.second.commit["id"];
if (!replyId.isEmpty()) {
replyTo_[replyId].insert(commitId);
}
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
interactions_[index].second.linkPreviewInfo = info;
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::LinkPreviewInfo});
}
void
MessageListModel::addReaction(const QString& messageId, const MapStringString& reaction)
{
int index = getIndexOfMessage(messageId);
if (index == -1)
return;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
auto emoji = api::interaction::Emoji {reaction["id"], reaction["body"]};
auto& pList = interactions_[index].second.reactions[reaction["author"]];
QList<QVariant> newList = pList.toList();
newList.emplace_back(QVariant::fromValue(emoji));
pList = QVariantList::fromVector(newList);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
}
void
MessageListModel::rmReaction(const QString& messageId, const QString& reactionId)
{
int index = getIndexOfMessage(messageId);
if (index == -1)
return;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
auto& reactions = interactions_[index].second.reactions;
for (const auto& key : reactions.keys()) {
QList<QVariant> emojis = reactions[key].toList();
for (auto it = emojis.begin(); it != emojis.end(); ++it) {
auto emoji = it->value<api::interaction::Emoji>();
if (emoji.commitId == reactionId) {
emojis.erase(it);
reactions[key] = emojis;
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Reactions});
return;
}
}
// Use a const reference to avoid detaching
const auto& replies = replyTo_[commitId];
for (const auto& msgId : replies) {
int index = indexOfMessage(msgId);
if (index == -1)
continue;
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ReplyToAuthor, Role::ReplyToBody});
}
}
void
MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed)
{
int index = getIndexOfMessage(messageId);
if (index == -1) {
return;
}
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
interactions_[index].second.parsedBody = parsed;
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody});
}
void
MessageListModel::setRead(const QString& peer, const QString& messageId)
{
auto i = lastDisplayedMessageUid_.find(peer);
if (i != lastDisplayedMessageUid_.end()) {
auto old = i.value();
messageToReaders_[old].removeAll(peer);
auto msgIdx = getIndexOfMessage(old);
// Remove from latest read
if (msgIdx != -1) {
QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
}
}
// update map
lastDisplayedMessageUid_[peer] = messageId;
messageToReaders_[messageId].append(peer);
// update interaction
auto msgIdx = getIndexOfMessage(messageId);
// Remove from latest read
if (msgIdx != -1) {
QModelIndex modelIndex = QAbstractListModel::index(msgIdx, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Readers});
}
}
QString
MessageListModel::getRead(const QString& peer)
{
auto i = lastDisplayedMessageUid_.find(peer);
if (i != lastDisplayedMessageUid_.end())
return i.value();
return "";
}
void
MessageListModel::emitDataChanged(iterator it, VectorInt roles)
{
auto index = std::distance(begin(), it);
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, roles);
}
void
MessageListModel::emitDataChanged(const QString& msgId, VectorInt roles)
{
int index = getIndexOfMessage(msgId);
if (index == -1) {
return;
}
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
Q_EMIT dataChanged(modelIndex, modelIndex, roles);
}
QString
MessageListModel::lastSelfMessageId(const QString& id) const
{
for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) {
auto lastType = it->second.type;
if (lastType == interaction::Type::TEXT and !it->second.body.isEmpty()
and (it->second.authorUri.isEmpty() || it->second.authorUri == id)) {
return it->first;
}
}
return {};
}
} // namespace lrc

View file

@ -1,167 +0,0 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* This program is free 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "api/interaction.h"
#include "api/account.h"
#include <QAbstractListModel>
namespace lrc {
namespace api {
namespace interaction {
struct Info;
}
#define MSG_ROLES \
X(Id) \
X(Author) \
X(Body) \
X(ParentId) \
X(Timestamp) \
X(Duration) \
X(Type) \
X(Status) \
X(IsRead) \
X(ContactAction) \
X(ActionUri) \
X(ConfId) \
X(DeviceId) \
X(LinkPreviewInfo) \
X(ParsedBody) \
X(PreviousBodies) \
X(Reactions) \
X(ReplyTo) \
X(ReplyToBody) \
X(ReplyToAuthor) \
X(TotalSize) \
X(TransferName) \
X(FileExtension) \
X(Readers) \
X(IsEmojiOnly)
namespace MessageList {
Q_NAMESPACE
enum Role {
DummyRole = Qt::UserRole + 1,
#define X(role) role,
MSG_ROLES
#undef X
};
Q_ENUM_NS(Role)
} // namespace MessageList
class MessageListModel : public QAbstractListModel
{
Q_OBJECT
public:
using item_t = const QPair<QString, interaction::Info>;
typedef QList<QPair<QString, interaction::Info>>::ConstIterator constIterator;
typedef QList<QPair<QString, interaction::Info>>::Iterator iterator;
typedef QList<QPair<QString, interaction::Info>>::reverse_iterator reverseIterator;
explicit MessageListModel(const account::Info* account, QObject* parent = nullptr);
~MessageListModel() = default;
// map functions
QPair<iterator, bool> emplace(const QString& msgId,
interaction::Info message,
bool beginning = false);
iterator find(const QString& msgId);
iterator findActiveCall(const MapStringString& commit);
iterator erase(const iterator& it);
constIterator find(const QString& msgId) const;
QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message,
bool beginning = false);
Q_INVOKABLE int erase(const QString& msgId);
interaction::Info& operator[](const QString& messageId);
iterator end();
constIterator end() const;
reverseIterator rend();
constIterator cend() const;
iterator begin();
constIterator begin() const;
reverseIterator rbegin();
Q_INVOKABLE int size() const;
void clear();
void reloadHistory();
bool empty() const;
interaction::Info at(const QString& intId) const;
QPair<QString, interaction::Info> front() const;
QPair<QString, interaction::Info> last() const;
QPair<QString, interaction::Info> atIndex(int index) const;
QPair<iterator, bool> insert(int index, QPair<QString, interaction::Info> message);
int indexOfMessage(const QString& msgId, bool reverse = true) const;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
Q_INVOKABLE virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
Q_INVOKABLE virtual QVariant data(int idx, int role = Qt::DisplayRole) const;
QHash<int, QByteArray> roleNames() const override;
QVariant dataForItem(item_t item, int indexRow, int role = Qt::DisplayRole) const;
bool contains(const QString& msgId);
int getIndexOfMessage(const QString& messageId) const;
void addHyperlinkInfo(const QString& messageId, const QVariantMap& info);
void addReaction(const QString& messageId, const MapStringString& reaction);
void rmReaction(const QString& messageId, const QString& reactionId);
void setParsedMessage(const QString& messageId, const QString& parsed);
void setRead(const QString& peer, const QString& messageId);
QString getRead(const QString& peer);
// use these if the underlying data model is changed from conversationmodel
// Note: this is not ideal, and this class should be refactored into a proper
// view model and absorb the interaction management logic to avoid exposing
// these emission wrappers
void emitDataChanged(iterator it, VectorInt roles = {});
void emitDataChanged(const QString& msgId, VectorInt roles = {});
bool isOnlyEmoji(const QString& text) const;
QVariantMap convertReactMessagetoQVariant(const QSet<QString>&);
QString lastSelfMessageId(const QString& id) const;
protected:
using Role = MessageList::Role;
private:
QList<QPair<QString, interaction::Info>> interactions_;
// Note: because read status are updated even if interaction is not loaded
// we need to keep track of these status outside the interaction::Info
// lastDisplayedMessageUid_ stores: {"peerId":"messageId"}
// messageToReaders_ caches: "messageId":["peer1", "peer2"]
// to allow quick access.
QMap<QString, QString> lastDisplayedMessageUid_;
QMap<QString, QStringList> messageToReaders_;
QMap<QString, QSet<QString>> replyTo_;
const account::Info* account_;
void updateReplies(item_t& message);
void insertMessage(int index, item_t& message);
iterator insertMessage(iterator it, item_t& message);
void removeMessage(int index, iterator it);
};
} // namespace api
} // namespace lrc
Q_DECLARE_METATYPE(lrc::api::MessageListModel*)