-
Change-Id: Ia94e47c26ded5f3d7271c1d81253eca6d67001dd
Change-Id: Ia94e47c26ded5f3d7271c1d81253eca6d67001dd
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
messagesadapter.cpp 26.08 KiB
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
* Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@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, see <http://www.gnu.org/licenses/>.
*/
#include "messagesadapter.h"
#include "appsettingsmanager.h"
#include "qtutils.h"
#include <api/datatransfermodel.h>
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QList>
#include <QMimeData>
#include <QMimeDatabase>
#include <QUrl>
#include <QtMath>
#include <QRegExp>
MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
PreviewEngine* previewEngine,
LRCInstance* instance,
QObject* parent)
: QmlAdapterBase(instance, parent)
, settingsManager_(settingsManager)
, previewEngine_(previewEngine)
, filteredMsgListModel_(new FilteredMsgListModel(this))
, mediaInteractions_(std::make_unique<MessageListModel>())
, timestampTimer_(new QTimer(this))
{
setObjectName(typeid(*this).name());
set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, this, [this]() {
set_replyToId("");
set_editId("");
const QString& convId = lrcInstance_->get_selectedConvUid();
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
// Reset the source model for the proxy model.
filteredMsgListModel_->setSourceModel(conversation.interactions.get());
set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
});
connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady);
connect(previewEngine_, &PreviewEngine::linkified, this, &MessagesAdapter::onMessageLinkified);
connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated);
timestampTimer_->start(timestampUpdateIntervalMs_);
connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, this, [this]() {
connectConversationModel();
});
connectConversationModel();
}
void
MessagesAdapter::loadMoreMessages()
{
auto accountId = lrcInstance_->get_currentAccountId();
auto convId = lrcInstance_->get_selectedConvUid();
try {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
if (convInfo.isSwarm()) {
auto* convModel = lrcInstance_->getCurrentConversationModel();
convModel->loadConversationMessages(convId, loadChunkSize_);
}
} catch (const std::exception& e) {
qWarning() << e.what();
}
}
void
MessagesAdapter::loadConversationUntil(const QString& to)
{
try {
if (auto* model = getMsgListSourceModel()) {
auto idx = model->indexOfMessage(to);
if (idx == -1) {
auto accountId = lrcInstance_->get_currentAccountId();
auto convId = lrcInstance_->get_selectedConvUid();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
if (convInfo.isSwarm()) {
auto* convModel = lrcInstance_->getCurrentConversationModel();
convModel->loadConversationUntil(convId, to);
}
}
}
} catch (const std::exception& e) {
qWarning() << e.what();
}
}
void
MessagesAdapter::connectConversationModel()
{
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
if (currentConversationModel == nullptr) {
return;
}
QObject::connect(currentConversationModel,
&ConversationModel::newInteraction,
this,
&MessagesAdapter::onNewInteraction,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::conversationMessagesLoaded,
this,
&MessagesAdapter::onConversationMessagesLoaded,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::composingStatusChanged,
this,
&MessagesAdapter::onComposingStatusChanged,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::messagesFoundProcessed,
this,
&MessagesAdapter::onMessagesFoundProcessed,
Qt::UniqueConnection);
}
void
MessagesAdapter::sendConversationRequest()
{
lrcInstance_->makeConversationPermanent();
}
void
MessagesAdapter::sendMessage(const QString& message)
{
try {
const auto convUid = lrcInstance_->get_selectedConvUid();
lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_);
set_replyToId("");
} catch (...) {
qDebug() << "Exception during sendMessage:" << message;
}
}
void
MessagesAdapter::editMessage(const QString& convId, const QString& newBody, const QString& messageId)
{
try {
auto editId = !messageId.isEmpty() ? messageId : editId_;
if (editId.isEmpty()) {
return;
}
set_editId("");
lrcInstance_->getCurrentConversationModel()->editMessage(convId, newBody, editId);
} catch (...) {
qDebug() << "Exception during message edition:" << messageId;
}
}
void
MessagesAdapter::removeEmojiReaction(const QString& convId,
const QString& emoji,
const QString& messageId)
{
try {
const auto authorUri = lrcInstance_->getCurrentAccountInfo().profileInfo.uri;
// check if this emoji has already been added by this author
auto emojiId = lrcInstance_->getConversationFromConvUid(convId)
.interactions->findEmojiReaction(emoji, authorUri, messageId);
editMessage(convId, "", emojiId);
} catch (...) {
qDebug() << "Exception during removeEmojiReaction():" << messageId;
}
}
void
MessagesAdapter::addEmojiReaction(const QString& convId,
const QString& emoji,
const QString& messageId)
{
try {
lrcInstance_->getCurrentConversationModel()->reactMessage(convId, emoji, messageId);
} catch (...) {
qDebug() << "Exception during addEmojiReaction():" << messageId;
}
}
void
MessagesAdapter::sendFile(const QString& message)
{
QFileInfo fi(message);
QString fileName = fi.fileName();
try {
auto convUid = lrcInstance_->get_selectedConvUid();
lrcInstance_->getCurrentConversationModel()->sendFile(convUid, message, fileName);
} catch (...) {
qDebug() << "Exception during sendFile";
}
}
void
MessagesAdapter::retryInteraction(const QString& interactionId)
{
lrcInstance_->getCurrentConversationModel()
->retryInteraction(lrcInstance_->get_selectedConvUid(), interactionId);
}
void
MessagesAdapter::joinCall(const QString& uri,
const QString& deviceId,
const QString& confId,
bool isAudioOnly)
{
lrcInstance_->getCurrentConversationModel()->joinCall(lrcInstance_->get_selectedConvUid(),
uri,
deviceId,
confId,
isAudioOnly);
}
void
MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName)
{
auto downloadDir = lrcInstance_->accountModel().downloadDirectory;
if (auto accInfo = &lrcInstance_->getCurrentAccountInfo())
accInfo->dataTransferModel->copyTo(lrcInstance_->get_currentAccountId(),
lrcInstance_->get_selectedConvUid(),
interactionId,
downloadDir,
displayName);
}
void
MessagesAdapter::deleteInteraction(const QString& interactionId)
{
lrcInstance_->getCurrentConversationModel()
->clearInteractionFromConversation(lrcInstance_->get_selectedConvUid(), interactionId);
}
void
MessagesAdapter::openUrl(const QString& url)
{
if (!QDesktopServices::openUrl(url)) {
qDebug() << "Couldn't open url: " << url;
}
}
void
MessagesAdapter::openDirectory(const QString& path)
{
QString p = path;
QFileInfo f(p);
if (f.exists()) {
if (!f.isDir())
p = f.dir().absolutePath();
QString url;
if (!p.startsWith("file:/"))
url = "file:///" + p;
else
url = p;
openUrl(url);
}
}
void
MessagesAdapter::acceptFile(const QString& interactionId)
{
auto convUid = lrcInstance_->get_selectedConvUid();
lrcInstance_->getCurrentConversationModel()->acceptTransfer(convUid, interactionId);
}
void
MessagesAdapter::cancelFile(const QString& interactionId)
{
const auto convUid = lrcInstance_->get_selectedConvUid();
lrcInstance_->getCurrentConversationModel()->cancelTransfer(convUid, interactionId);
}
void
MessagesAdapter::onPaste()
{
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
if (mimeData->hasImage()) {
// Save temp data into a temp file.
QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());
auto img_name_hash
= QCryptographicHash::hash(QString::number(pixmap.cacheKey()).toLocal8Bit(),
QCryptographicHash::Sha1);
QString fileName = "img_" + QString(img_name_hash.toHex()) + ".png";
QString path = QDir::temp().filePath(fileName);
if (!pixmap.save(path, "PNG")) {
qDebug().noquote() << "Errors during QPixmap save"
<< "\n";
return;
}
Q_EMIT newFilePasted(path);
} else if (mimeData->hasUrls()) {
QList<QUrl> urlList = mimeData->urls();
// Extract the local paths of the files.
for (int i = 0; i < urlList.size(); ++i) {
// Trim file:// or file:/// from url.
QString filePath = urlList.at(i).toString().remove(
QRegularExpression("^file:\\/{2,3}"));
Q_EMIT newFilePasted(filePath);
}
} else {
// Treat as text content, make chatview.js handle in order to
// avoid string escape problems
Q_EMIT newTextPasted();
}
}
QString
MessagesAdapter::getStatusString(int status)
{
switch (static_cast<interaction::Status>(status)) {
case interaction::Status::SENDING:
return QObject::tr("Sending");
case interaction::Status::FAILURE:
return QObject::tr("Failure");
case interaction::Status::SUCCESS:
return QObject::tr("Sent");
case interaction::Status::TRANSFER_CREATED:
return QObject::tr("Connecting");
case interaction::Status::TRANSFER_ACCEPTED:
return QObject::tr("Accept");
case interaction::Status::TRANSFER_CANCELED:
return QObject::tr("Canceled");
case interaction::Status::TRANSFER_ERROR:
case interaction::Status::TRANSFER_UNJOINABLE_PEER:
return QObject::tr("Unable to make contact");
case interaction::Status::TRANSFER_ONGOING:
return QObject::tr("Ongoing");
case interaction::Status::TRANSFER_AWAITING_PEER:
return QObject::tr("Waiting for contact");
case interaction::Status::TRANSFER_AWAITING_HOST:
return QObject::tr("Incoming transfer");
case interaction::Status::TRANSFER_TIMEOUT_EXPIRED:
return QObject::tr("Timed out waiting for contact");
case interaction::Status::TRANSFER_FINISHED:
return QObject::tr("Finished");
default:
return {};
}
}
QVariantMap
MessagesAdapter::getTransferStats(const QString& msgId, int status)
{
Q_UNUSED(status)
auto convModel = lrcInstance_->getCurrentConversationModel();
lrc::api::datatransfer::Info info = {};
convModel->getTransferInfo(lrcInstance_->get_selectedConvUid(), msgId, info);
return {{"totalSize", qint64(info.totalSize)}, {"progress", qint64(info.progress)}};
}
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 {};
}
void
MessagesAdapter::userIsComposing(bool isComposing)
{
if (!settingsManager_->getValue(Settings::Key::EnableTypingIndicator).toBool()
|| lrcInstance_->get_selectedConvUid().isEmpty()) {
return;
}
lrcInstance_->getCurrentConversationModel()->setIsComposing(lrcInstance_->get_selectedConvUid(),
isComposing);
}
void
MessagesAdapter::onNewInteraction(const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction)
{
Q_UNUSED(interactionId);
try {
if (convUid.isEmpty() || convUid != lrcInstance_->get_selectedConvUid()) {
return;
}
auto accountId = lrcInstance_->get_currentAccountId();
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
auto& convModel = accountInfo.conversationModel;
convModel->clearUnreadInteractions(convUid);
Q_EMIT newInteraction(interactionId, static_cast<int>(interaction.type));
} catch (...) {
}
}
void
MessagesAdapter::acceptInvitation(const QString& convId)
{
auto conversationId = convId.isEmpty() ? lrcInstance_->get_selectedConvUid() : convId;
auto* convModel = lrcInstance_->getCurrentConversationModel();
convModel->acceptConversationRequest(conversationId);
}
void
MessagesAdapter::refuseInvitation(const QString& convUid)
{
const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, false);
}
void
MessagesAdapter::blockConversation(const QString& convUid)
{
const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, true);
}
void
MessagesAdapter::unbanContact(int index)
{
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
auto bannedContactList = accountInfo.contactModel->getBannedContacts();
auto it = bannedContactList.begin();
std::advance(it, index);
try {
auto contactInfo = accountInfo.contactModel->getContact(*it);
accountInfo.contactModel->addContact(contactInfo);
} catch (const std::out_of_range& e) {
qDebug() << e.what();
}
}
void
MessagesAdapter::unbanConversation(const QString& convUid)
{
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
try {
const auto contactUri = accInfo.conversationModel->peersForConversation(convUid).at(0);
auto contactInfo = accInfo.contactModel->getContact(contactUri);
accInfo.contactModel->addContact(contactInfo);
} catch (const std::out_of_range& e) {
qDebug() << e.what();
}
}
void
MessagesAdapter::clearConversationHistory(const QString& accountId, const QString& convUid)
{
lrcInstance_->getAccountInfo(accountId).conversationModel->clearHistory(convUid);
}
void
MessagesAdapter::removeConversation(const QString& convUid)
{
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
accInfo.conversationModel->removeConversation(convUid);
}
void
MessagesAdapter::removeConversationMember(const QString& convUid, const QString& memberUri)
{
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
accInfo.conversationModel->removeConversationMember(convUid, memberUri);
}
void
MessagesAdapter::removeContact(const QString& convUid, bool banContact)
{
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
// remove the uri from the default moderators list
// TODO: seems like this should be done in libringclient
QStringList list = lrcInstance_->accountModel().getDefaultModerators(accInfo.id);
const auto contactUri = accInfo.conversationModel->peersForConversation(convUid).at(0);
if (!contactUri.isEmpty() && list.contains(contactUri)) {
lrcInstance_->accountModel().setDefaultModerator(accInfo.id, contactUri, false);
}
// actually remove the contact
accInfo.contactModel->removeContact(contactUri, banContact);
}
void
MessagesAdapter::onPreviewInfoReady(QString messageId, QVariantMap info)
{
const QString& convId = lrcInstance_->get_selectedConvUid();
const QString& accId = lrcInstance_->get_currentAccountId();
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
conversation.interactions->addHyperlinkInfo(messageId, info);
}
void
MessagesAdapter::onConversationMessagesLoaded(uint32_t, const QString& convId)
{
if (convId != lrcInstance_->get_selectedConvUid())
return;
Q_EMIT moreMessagesLoaded();
}
void
MessagesAdapter::parseMessageUrls(const QString& messageId,
const QString& msg,
bool showPreview,
QColor color)
{
previewEngine_->parseMessage(messageId, msg, showPreview, color);
}
void
MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& linkified)
{
const QString& convId = lrcInstance_->get_selectedConvUid();
const QString& accId = lrcInstance_->get_currentAccountId();
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
conversation.interactions->linkifyMessage(messageId, linkified);
}
void
MessagesAdapter::onComposingStatusChanged(const QString& convId,
const QString& contactUri,
bool isComposing)
{
Q_UNUSED(contactUri)
if (lrcInstance_->get_selectedConvUid() == convId) {
const QString& accId = lrcInstance_->get_currentAccountId();
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
}
}
void
MessagesAdapter::onMessagesFoundProcessed(const QString& accountId,
const VectorMapStringString& messageIds,
const QVector<interaction::Info>& messageInformations)
{
if (lrcInstance_->get_currentAccountId() != accountId) {
return;
}
bool isSearchInProgress = messageIds.length();
if (isSearchInProgress) {
int index = -1;
Q_FOREACH (const MapStringString& msg, messageIds) {
index++;
try {
std::pair<QString, interaction::Info> message(msg["id"],
messageInformations.at(index));
mediaInteractions_->insert(message);
} catch (...) {
qWarning() << "error in onMessagesFoundProcessed, message insertion on index: "
<< index;
}
}
} else {
set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
}
}
QList<QString>
MessagesAdapter::conversationTypersUrlToName(const QSet<QString>& typersSet)
{
QList<QString> nameList;
for (const auto& id : typersSet) {
auto name = lrcInstance_->getCurrentContactModel()->bestNameForContact(id);
nameList.append(name);
}
return nameList;
}
QVariantMap
MessagesAdapter::isLocalImage(const QString& mimename)
{
if (mimename.startsWith("image/")) {
QString fileFormat = mimename;
fileFormat.replace("image/", "");
QImageReader reader;
QList<QByteArray> supportedFormats = reader.supportedImageFormats();
auto iterator = std::find_if(supportedFormats.begin(),
supportedFormats.end(),
[fileFormat](QByteArray format) {
return format == fileFormat;
});
if (iterator != supportedFormats.end() && *iterator == "gif") {
return {{"isAnimatedImage", true}};
}
return {{"isImage", iterator != supportedFormats.end()}};
}
return {{"isImage", false}};
}
QVariantMap
MessagesAdapter::getMediaInfo(const QString& msg)
{
auto filePath = QFileInfo(msg).absoluteFilePath();
static const QString html
= "<body style='margin:0;padding:0;'>"
"<%1 style='width:100%;height:%2;outline:none;background-color:#f1f3f4;"
"object-fit:cover;' "
"controls controlsList='nodownload' src='file://%3' type='%4'/></body>";
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(filePath);
QVariantMap fileInfo = isLocalImage(mime.name());
if (fileInfo["isImage"].toBool() || fileInfo["isAnimatedImage"].toBool()) {
return fileInfo;
}
static const QRegExp vPattern("(video/)(avi|mov|webm|webp|rmvb)$", Qt::CaseInsensitive);
vPattern.indexIn(mime.name());
QString type = vPattern.capturedTexts().size() == 3 ? vPattern.capturedTexts()[1] : "";
if (!type.isEmpty()) {
return {
{"isVideo", true},
{"html", html.arg("video", "100%", filePath, mime.name())},
};
} else {
static const QRegExp aPattern("(audio/)(ogg|flac|wav|mpeg|mp3)$", Qt::CaseInsensitive);
aPattern.indexIn(mime.name());
type = aPattern.capturedTexts().size() == 3 ? aPattern.capturedTexts()[1] : "";
if (!type.isEmpty()) {
return {
{"isVideo", false},
{"html", html.arg("audio", "54px", filePath, mime.name())},
};
}
}
return {};
}
bool
MessagesAdapter::isRemoteImage(const QString& msg)
{
// TODO: test if all these open in the AnimatedImage component
QRegularExpression pattern("[^\\s]+(.*?)\\.(jpg|jpeg|png|gif|apng|webp|avif|flif)$",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch match = pattern.match(msg);
return match.hasMatch();
}
QString
MessagesAdapter::getFormattedTime(const quint64 timestamp)
{
const auto currentTime = QDateTime::currentDateTime();
const auto seconds = currentTime.toSecsSinceEpoch() - timestamp;
auto interval = qFloor(seconds / 60);
if (interval > 1) {
auto curLang = settingsManager_->getLanguage();
auto curLocal = QLocale(curLang);
auto curTime = QDateTime::fromSecsSinceEpoch(timestamp).time();
QString timeLocale;
timeLocale = curLocal.toString(curTime, curLocal.ShortFormat);
return timeLocale;
}
return QObject::tr("just now");
}
QString
MessagesAdapter::getBestFormattedDate(const quint64 timestamp)
{
auto currentDate = QDate::currentDate();
auto timestampDate = QDateTime::fromSecsSinceEpoch(timestamp).date();
if (timestampDate == currentDate)
return getFormattedTime(timestamp);
return getFormattedDay(timestamp);
}
QString
MessagesAdapter::getFormattedDay(const quint64 timestamp)
{
auto currentDate = QDate::currentDate();
auto timestampDate = QDateTime::fromSecsSinceEpoch(timestamp).date();
if (timestampDate == currentDate)
return QObject::tr("Today");
if (timestampDate.daysTo(currentDate) == 1)
return QObject::tr("Yesterday");
auto curLang = settingsManager_->getLanguage();
auto curLocal = QLocale(curLang);
auto curDate = QDateTime::fromSecsSinceEpoch(timestamp).date();
QString dateLocale;
dateLocale = curLocal.toString(curDate, curLocal.ShortFormat);
return dateLocale;
}
void
MessagesAdapter::getConvMedias()
{
auto accountId = lrcInstance_->get_currentAccountId();
auto convId = lrcInstance_->get_selectedConvUid();
mediaInteractions_.reset(new MessageListModel(this));
try {
lrcInstance_->getCurrentConversationModel()->getConvMediasInfos(accountId, convId);
} catch (...) {
qDebug() << "Exception during getConvMedia:";
}
}
int
MessagesAdapter::getMessageIndexFromId(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
{
// We are certain that filteredMsgListModel_'s source model is a MessageListModel,
// However it may be a nullptr if not yet set.
return static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel());
}