From 21c7d65027a8c4ff8c9aa29d0f71bdb89c54fa80 Mon Sep 17 00:00:00 2001 From: blue Date: Mon, 13 Apr 2020 22:57:23 +0300 Subject: [PATCH] offline avatars in mucs --- CHANGELOG.md | 1 + core/account.cpp | 7 ++++- core/archive.cpp | 33 +++++++++++++++++++-- core/archive.h | 3 +- core/conference.cpp | 58 ++++++++++++++++++++++++++---------- core/conference.h | 15 ++++++++-- core/rosteritem.cpp | 35 +++++++++++++--------- core/rosteritem.h | 8 +++-- shared/global.cpp | 1 + shared/global.h | 4 +++ ui/models/room.cpp | 27 ++++++++++++++++- ui/models/room.h | 2 ++ ui/utils/messageline.cpp | 59 +++++++++++++++++++++++++++++++++---- ui/utils/messageline.h | 3 ++ ui/widgets/conversation.cpp | 10 +++++++ ui/widgets/room.cpp | 4 ++- 16 files changed, 225 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c5e75..e528bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - going offline related segfault fix - statuses now behave better: they wrap if they don't fit, you can select them, you can follow links from there - messages and statuses don't loose content if you use < ore > symbols +- now avatars of those who are not in the MUC right now but was also display next to the message ## Squawk 0.1.3 (Mar 31, 2020) diff --git a/core/account.cpp b/core/account.cpp index 2f8238a..4c12761 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -1063,6 +1063,7 @@ void Core::Account::onContactHistoryResponse(const std::list& l void Core::Account::onClientError(QXmppClient::Error err) { + qDebug() << "Error"; QString errorText; QString errorType; switch (err) { @@ -1140,6 +1141,9 @@ void Core::Account::onClientError(QXmppClient::Error err) case QXmppStanza::Error::UnexpectedRequest: errorText = "Unexpected request"; break; + case QXmppStanza::Error::PolicyViolation: + errorText = "Policy violation"; + break; } errorType = "Client stream error"; @@ -1367,7 +1371,8 @@ void Core::Account::addNewRoom(const QString& jid, const QString& nick, const QS {"autoJoin", conf->getAutoJoin()}, {"joined", conf->getJoined()}, {"nick", conf->getNick()}, - {"name", conf->getName()} + {"name", conf->getName()}, + {"avatars", conf->getAllAvatars()} }; Archive::AvatarInfo info; diff --git a/core/archive.cpp b/core/archive.cpp index bd159ca..50acf81 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -675,7 +675,7 @@ bool Core::Archive::dropAvatar(const std::string& resource) } } -bool Core::Archive::setAvatar(const QByteArray& data, bool generated, const QString& resource) +bool Core::Archive::setAvatar(const QByteArray& data, AvatarInfo& newInfo, bool generated, const QString& resource) { if (!opened) { throw Closed("setAvatar", jid.toStdString()); @@ -726,7 +726,9 @@ bool Core::Archive::setAvatar(const QByteArray& data, bool generated, const QStr MDB_val lmdbKey, lmdbData; QByteArray value; - AvatarInfo newInfo(ext, newHash, generated); + newInfo.type = ext; + newInfo.hash = newHash; + newInfo.autogenerated = generated; newInfo.serialize(&value); lmdbKey.mv_size = res.size(); lmdbKey.mv_data = (char*)res.c_str(); @@ -802,6 +804,33 @@ bool Core::Archive::readAvatarInfo(Core::Archive::AvatarInfo& target, const std: } } +void Core::Archive::readAllResourcesAvatars(std::map& data) const +{ + if (!opened) { + throw Closed("readAllResourcesAvatars", jid.toStdString()); + } + + int rc; + MDB_val lmdbKey, lmdbData; + MDB_txn *txn; + MDB_cursor* cursor; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + mdb_cursor_open(txn, avatars, &cursor); + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_FIRST); + + do { + std::string sId((char*)lmdbKey.mv_data, lmdbKey.mv_size); + QString res(sId.c_str()); + if (res != jid) { + data.emplace(res, AvatarInfo()); + data[res].deserialize((char*)lmdbData.mv_data, lmdbData.mv_size); + } + } while (mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_NEXT) == 0); + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); +} + Core::Archive::AvatarInfo Core::Archive::getAvatarInfo(const QString& resource) const { if (!opened) { diff --git a/core/archive.h b/core/archive.h index 6facf68..ef6ca23 100644 --- a/core/archive.h +++ b/core/archive.h @@ -56,9 +56,10 @@ public: std::list getBefore(int count, const QString& id); bool isFromTheBeginning(); void setFromTheBeginning(bool is); - bool setAvatar(const QByteArray& data, bool generated = false, const QString& resource = ""); + bool setAvatar(const QByteArray& data, AvatarInfo& info, bool generated = false, const QString& resource = ""); AvatarInfo getAvatarInfo(const QString& resource = "") const; bool readAvatarInfo(AvatarInfo& target, const QString& resource = "") const; + void readAllResourcesAvatars(std::map& data) const; public: const QString jid; diff --git a/core/conference.cpp b/core/conference.cpp index d745227..cda19fd 100644 --- a/core/conference.cpp +++ b/core/conference.cpp @@ -25,7 +25,8 @@ Core::Conference::Conference(const QString& p_jid, const QString& p_account, boo nick(p_nick), room(p_room), joined(false), - autoJoin(p_autoJoin) + autoJoin(p_autoJoin), + exParticipants() { muc = true; name = p_name; @@ -44,6 +45,8 @@ Core::Conference::Conference(const QString& p_jid, const QString& p_account, boo if (autoJoin) { room->join(); } + + archive->readAllResourcesAvatars(exParticipants); } Core::Conference::~Conference() @@ -140,8 +143,8 @@ void Core::Conference::onRoomParticipantAdded(const QString& p_name) resource = ""; } - Archive::AvatarInfo info; - bool hasAvatar = readAvatarInfo(info, resource); + std::map::const_iterator itr = exParticipants.find(resource); + bool hasAvatar = itr != exParticipants.end(); if (resource.size() > 0) { QDateTime lastInteraction = pres.lastUserInteraction(); @@ -158,12 +161,12 @@ void Core::Conference::onRoomParticipantAdded(const QString& p_name) }; if (hasAvatar) { - if (info.autogenerated) { + if (itr->second.autogenerated) { cData.insert("avatarState", static_cast(Shared::Avatar::valid)); } else { cData.insert("avatarState", static_cast(Shared::Avatar::autocreated)); } - cData.insert("avatarPath", avatarPath(resource) + "." + info.type); + cData.insert("avatarPath", avatarPath(resource) + "." + itr->second.type); } else { cData.insert("avatarState", static_cast(Shared::Avatar::empty)); cData.insert("avatarPath", ""); @@ -179,14 +182,14 @@ void Core::Conference::onRoomParticipantAdded(const QString& p_name) case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here break; case QXmppPresence::VCardUpdateNoPhoto: { //there is no photo, need to drop if any - if (!hasAvatar || !info.autogenerated) { + if (!hasAvatar || !itr->second.autogenerated) { setAutoGeneratedAvatar(resource); } } break; case QXmppPresence::VCardUpdateValidPhoto:{ //there is a photo, need to load if (hasAvatar) { - if (info.autogenerated || info.hash != pres.photoHash()) { + if (itr->second.autogenerated || itr->second.hash != pres.photoHash()) { emit requestVCard(p_name); } } else { @@ -285,30 +288,46 @@ void Core::Conference::handlePresence(const QXmppPresence& pres) bool Core::Conference::setAutoGeneratedAvatar(const QString& resource) { - bool result = RosterItem::setAutoGeneratedAvatar(resource); + Archive::AvatarInfo newInfo; + bool result = RosterItem::setAutoGeneratedAvatar(newInfo, resource); if (result && resource.size() != 0) { + std::map::iterator itr = exParticipants.find(resource); + if (itr == exParticipants.end()) { + exParticipants.insert(std::make_pair(resource, newInfo)); + } else { + itr->second = newInfo; + } emit changeParticipant(resource, { {"avatarState", static_cast(Shared::Avatar::autocreated)}, - {"avatarPath", avatarPath(resource) + ".png"} + {"avatarPath", avatarPath(resource) + "." + newInfo.type} }); } return result; } -bool Core::Conference::setAvatar(const QByteArray& data, const QString& resource) +bool Core::Conference::setAvatar(const QByteArray& data, Archive::AvatarInfo& info, const QString& resource) { - bool result = RosterItem::setAvatar(data, resource); + bool result = RosterItem::setAvatar(data, info, resource); if (result && resource.size() != 0) { if (data.size() > 0) { - QMimeDatabase db; - QMimeType type = db.mimeTypeForData(data); - QString ext = type.preferredSuffix(); + std::map::iterator itr = exParticipants.find(resource); + if (itr == exParticipants.end()) { + exParticipants.insert(std::make_pair(resource, info)); + } else { + itr->second = info; + } + emit changeParticipant(resource, { {"avatarState", static_cast(Shared::Avatar::autocreated)}, - {"avatarPath", avatarPath(resource) + "." + ext} + {"avatarPath", avatarPath(resource) + "." + info.type} }); } else { + std::map::iterator itr = exParticipants.find(resource); + if (itr != exParticipants.end()) { + exParticipants.erase(itr); + } + emit changeParticipant(resource, { {"avatarState", static_cast(Shared::Avatar::empty)}, {"avatarPath", ""} @@ -333,3 +352,12 @@ Shared::VCard Core::Conference::handleResponseVCard(const QXmppVCardIq& card, co return result; } + +QMap Core::Conference::getAllAvatars() const +{ + QMap result; + for (const std::pair& pair : exParticipants) { + result.insert(pair.first, avatarPath(pair.first) + "." + pair.second.type); + } + return result; +} diff --git a/core/conference.h b/core/conference.h index c00c472..4e0e463 100644 --- a/core/conference.h +++ b/core/conference.h @@ -19,9 +19,15 @@ #ifndef CORE_CONFERENCE_H #define CORE_CONFERENCE_H -#include "rosteritem.h" +#include + #include +#include + +#include "rosteritem.h" +#include "shared/global.h" + namespace Core { @@ -46,8 +52,8 @@ public: void setAutoJoin(bool p_autoJoin); void handlePresence(const QXmppPresence & pres) override; bool setAutoGeneratedAvatar(const QString& resource = "") override; - bool setAvatar(const QByteArray &data, const QString &resource = "") override; Shared::VCard handleResponseVCard(const QXmppVCardIq & card, const QString &resource) override; + QMap getAllAvatars() const; signals: void nickChanged(const QString& nick); @@ -58,11 +64,16 @@ signals: void changeParticipant(const QString& name, const QMap& data); void removeParticipant(const QString& name); +protected: + bool setAvatar(const QByteArray &data, Archive::AvatarInfo& info, const QString &resource = "") override; + private: QString nick; QXmppMucRoom* room; bool joined; bool autoJoin; + std::map exParticipants; + static const std::set supportedList; private slots: void onRoomJoined(); diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index 59b84f8..c25b339 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -410,28 +410,37 @@ bool Core::RosterItem::isMuc() const QString Core::RosterItem::avatarPath(const QString& resource) const { - QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - path += "/" + account + "/" + jid + "/" + (resource.size() == 0 ? jid : resource); + QString path = folderPath() + "/" + (resource.size() == 0 ? jid : resource); return path; } -bool Core::RosterItem::setAvatar(const QByteArray& data, const QString& resource) +QString Core::RosterItem::folderPath() const { - bool result = archive->setAvatar(data, false, resource); + QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + path += "/" + account + "/" + jid; + return path; +} + +bool Core::RosterItem::setAvatar(const QByteArray& data, Archive::AvatarInfo& info, const QString& resource) +{ + bool result = archive->setAvatar(data, info, false, resource); if (resource.size() == 0 && result) { if (data.size() == 0) { emit avatarChanged(Shared::Avatar::empty, ""); } else { - QMimeDatabase db; - QMimeType type = db.mimeTypeForData(data); - QString ext = type.preferredSuffix(); - emit avatarChanged(Shared::Avatar::valid, avatarPath(resource) + "." + ext); + emit avatarChanged(Shared::Avatar::valid, avatarPath(resource) + "." + info.type); } } return result; } bool Core::RosterItem::setAutoGeneratedAvatar(const QString& resource) +{ + Archive::AvatarInfo info; + return setAutoGeneratedAvatar(info, resource); +} + +bool Core::RosterItem::setAutoGeneratedAvatar(Archive::AvatarInfo& info, const QString& resource) { QImage image(96, 96, QImage::Format_ARGB32_Premultiplied); QPainter painter(&image); @@ -453,7 +462,7 @@ bool Core::RosterItem::setAutoGeneratedAvatar(const QString& resource) stream.open(QBuffer::WriteOnly); image.save(&stream, "PNG"); stream.close(); - bool result = archive->setAvatar(arr, true, resource); + bool result = archive->setAvatar(arr, info, true, resource); if (resource.size() == 0 && result) { emit avatarChanged(Shared::Avatar::autocreated, avatarPath(resource) + ".png"); } @@ -468,6 +477,7 @@ bool Core::RosterItem::readAvatarInfo(Archive::AvatarInfo& target, const QString Shared::VCard Core::RosterItem::handleResponseVCard(const QXmppVCardIq& card, const QString& resource) { Archive::AvatarInfo info; + Archive::AvatarInfo newInfo; bool hasAvatar = readAvatarInfo(info, resource); QByteArray ava = card.photo(); @@ -477,13 +487,10 @@ Shared::VCard Core::RosterItem::handleResponseVCard(const QXmppVCardIq& card, co QString path = ""; if (ava.size() > 0) { - bool changed = setAvatar(ava, resource); + bool changed = setAvatar(ava, newInfo, resource); if (changed) { type = Shared::Avatar::valid; - QMimeDatabase db; - QMimeType type = db.mimeTypeForData(ava); - QString ext = type.preferredSuffix(); - path = avatarPath(resource) + "." + ext; + path = avatarPath(resource) + "." + newInfo.type; } else if (hasAvatar) { if (info.autogenerated) { type = Shared::Avatar::autocreated; diff --git a/core/rosteritem.h b/core/rosteritem.h index 387ebfc..47470b1 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -69,8 +69,8 @@ public: void requestHistory(int count, const QString& before); void requestFromEmpty(int count, const QString& before); QString avatarPath(const QString& resource = "") const; + QString folderPath() const; bool readAvatarInfo(Archive::AvatarInfo& target, const QString& resource = "") const; - virtual bool setAvatar(const QByteArray& data, const QString& resource = ""); virtual bool setAutoGeneratedAvatar(const QString& resource = ""); virtual Shared::VCard handleResponseVCard(const QXmppVCardIq& card, const QString& resource); virtual void handlePresence(const QXmppPresence& pres) = 0; @@ -89,6 +89,10 @@ public: const QString jid; const QString account; +protected: + virtual bool setAvatar(const QByteArray& data, Archive::AvatarInfo& info, const QString& resource = ""); + virtual bool setAutoGeneratedAvatar(Archive::AvatarInfo& info, const QString& resource = ""); + protected: QString name; ArchiveState archiveState; @@ -103,7 +107,7 @@ protected: std::list> requestCache; std::map toCorrect; bool muc; - + private: void nextRequest(); void performRequest(int count, const QString& before); diff --git a/shared/global.cpp b/shared/global.cpp index c8e5cf2..a6b7b60 100644 --- a/shared/global.cpp +++ b/shared/global.cpp @@ -21,6 +21,7 @@ #include "enums.h" Shared::Global* Shared::Global::instance = 0; +const std::set Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; Shared::Global::Global(): availability({ diff --git a/shared/global.h b/shared/global.h index 481ac01..54e1584 100644 --- a/shared/global.h +++ b/shared/global.h @@ -24,6 +24,8 @@ #include "exception.h" #include +#include +#include #include #include @@ -60,6 +62,8 @@ namespace Shared { static bool supported(const QString& pluginName); static void setSupported(const QString& pluginName, bool support); + static const std::set supportedImagesExts; + template static T fromInt(int src); diff --git a/ui/models/room.cpp b/ui/models/room.cpp index be92d41..cc19d2c 100644 --- a/ui/models/room.cpp +++ b/ui/models/room.cpp @@ -32,7 +32,8 @@ Models::Room::Room(const QString& p_jid, const QMap& data, Mo avatarState(Shared::Avatar::empty), avatarPath(""), messages(), - participants() + participants(), + exParticipantAvatars() { QMap::const_iterator itr = data.find("autoJoin"); if (itr != data.end()) { @@ -62,6 +63,15 @@ Models::Room::Room(const QString& p_jid, const QMap& data, Mo if (itr != data.end()) { setAvatarPath(itr.value().toString()); } + + + itr = data.find("avatars"); + if (itr != data.end()) { + QMap avs = itr.value().toMap(); + for (QMap::const_iterator itr = avs.begin(), end = avs.end(); itr != end; ++itr) { + exParticipantAvatars.insert(std::make_pair(itr.key(), itr.value().toString())); + } + } } Models::Room::~Room() @@ -284,6 +294,11 @@ void Models::Room::addParticipant(const QString& p_name, const QMap::const_iterator eitr = exParticipantAvatars.find(name); + if (eitr != exParticipantAvatars.end()) { + exParticipantAvatars.erase(eitr); + } + Participant* part = new Participant(data); part->setName(p_name); participants.insert(std::make_pair(p_name, part)); @@ -311,6 +326,11 @@ void Models::Room::removeParticipant(const QString& p_name) Participant* p = itr->second; participants.erase(itr); removeChild(p->row()); + + if (p->getAvatarState() != Shared::Avatar::empty) { + exParticipantAvatars.insert(std::make_pair(p_name, p->getAvatarPath())); + } + p->deleteLater(); emit participantLeft(p_name); } @@ -408,3 +428,8 @@ QString Models::Room::getParticipantIconPath(const QString& name) const return itr->second->getAvatarPath(); } + +std::map Models::Room::getExParticipantAvatars() const +{ + return exParticipantAvatars; +} diff --git a/ui/models/room.h b/ui/models/room.h index 382b6e9..9ea70bf 100644 --- a/ui/models/room.h +++ b/ui/models/room.h @@ -75,6 +75,7 @@ public: QString getAvatarPath() const; std::map getParticipants() const; QString getParticipantIconPath(const QString& name) const; + std::map getExParticipantAvatars() const; signals: void participantJoined(const Participant& participant); @@ -99,6 +100,7 @@ private: QString avatarPath; Messages messages; std::map participants; + std::map exParticipantAvatars; }; diff --git a/ui/utils/messageline.cpp b/ui/utils/messageline.cpp index ecd10a1..0ef5f07 100644 --- a/ui/utils/messageline.cpp +++ b/ui/utils/messageline.cpp @@ -29,6 +29,7 @@ MessageLine::MessageLine(bool p_room, QWidget* parent): palMessages(), uploadPaths(), palAvatars(), + exPalAvatars(), layout(new QVBoxLayout(this)), myName(), myAvatarPath(), @@ -80,6 +81,11 @@ MessageLine::Position MessageLine::message(const Shared::Message& msg, bool forc std::map::iterator aItr = palAvatars.find(sender); if (aItr != palAvatars.end()) { aPath = aItr->second; + } else { + aItr = exPalAvatars.find(sender); + if (aItr != exPalAvatars.end()) { + aPath = aItr->second; + } } outgoing = false; } @@ -248,6 +254,11 @@ void MessageLine::setPalAvatar(const QString& jid, const QString& path) std::map::iterator itr = palAvatars.find(jid); if (itr == palAvatars.end()) { palAvatars.insert(std::make_pair(jid, path)); + + std::map::const_iterator eitr = exPalAvatars.find(jid); + if (eitr != exPalAvatars.end()) { + exPalAvatars.erase(eitr); + } } else { itr->second = path; } @@ -265,16 +276,36 @@ void MessageLine::dropPalAvatar(const QString& jid) std::map::iterator itr = palAvatars.find(jid); if (itr != palAvatars.end()) { palAvatars.erase(itr); - - std::map::iterator pItr = palMessages.find(jid); - if (pItr != palMessages.end()) { - for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) { - itr->second->setAvatarPath(""); - } + } + + std::map::const_iterator eitr = exPalAvatars.find(jid); + if (eitr != exPalAvatars.end()) { + exPalAvatars.erase(eitr); + } + + std::map::iterator pItr = palMessages.find(jid); + if (pItr != palMessages.end()) { + for (Index::const_iterator itr = pItr->second.begin(), end = pItr->second.end(); itr != end; ++itr) { + itr->second->setAvatarPath(""); } } } +void MessageLine::movePalAvatarToEx(const QString& name) +{ + std::map::iterator itr = palAvatars.find(name); + if (itr != palAvatars.end()) { + std::map::iterator eitr = exPalAvatars.find(name); + if (eitr != exPalAvatars.end()) { + eitr->second = itr->second; + } else { + exPalAvatars.insert(std::make_pair(name, itr->second)); + } + + palAvatars.erase(itr); + } +} + void MessageLine::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); @@ -459,3 +490,19 @@ void MessageLine::setMyAvatarPath(const QString& p_path) } } } + +void MessageLine::setExPalAvatars(const std::map& data) +{ + exPalAvatars = data; + + for (const std::pair& pair : palMessages) { + if (palAvatars.find(pair.first) == palAvatars.end()) { + std::map::const_iterator eitr = exPalAvatars.find(pair.first); + if (eitr != exPalAvatars.end()) { + for (const std::pair& mp : pair.second) { + mp.second->setAvatarPath(eitr->second); + } + } + } + } +} diff --git a/ui/utils/messageline.h b/ui/utils/messageline.h index 104dc72..a0a7b6c 100644 --- a/ui/utils/messageline.h +++ b/ui/utils/messageline.h @@ -59,6 +59,8 @@ public: void setPalAvatar(const QString& jid, const QString& path); void dropPalAvatar(const QString& jid); void changeMessage(const QString& id, const QMap& data); + void setExPalAvatars(const std::map& data); + void movePalAvatarToEx(const QString& name); signals: void resize(int amount); @@ -90,6 +92,7 @@ private: std::map palMessages; std::map uploadPaths; std::map palAvatars; + std::map exPalAvatars; QVBoxLayout* layout; QString myName; diff --git a/ui/widgets/conversation.cpp b/ui/widgets/conversation.cpp index 50f7dcf..e677bc8 100644 --- a/ui/widgets/conversation.cpp +++ b/ui/widgets/conversation.cpp @@ -92,6 +92,16 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c line->setMyAvatarPath(acc->getAvatarPath()); line->setMyName(acc->getName()); + QFont nf = m_ui->nameLabel->font(); + nf.setBold(true); + nf.setPointSize(nf.pointSize() + 2); + m_ui->nameLabel->setFont(nf); + + QFont sf = statusLabel->font(); + sf.setItalic(true); + sf.setPointSize(sf.pointSize() - 2); + statusLabel->setFont(sf); + applyVisualEffects(); } diff --git a/ui/widgets/room.cpp b/ui/widgets/room.cpp index 6d5340f..66ae5f7 100644 --- a/ui/widgets/room.cpp +++ b/ui/widgets/room.cpp @@ -38,6 +38,8 @@ Room::Room(Models::Account* acc, Models::Room* p_room, QWidget* parent): line->setPalAvatar(pair.first, aPath); } } + + line->setExPalAvatars(room->getExParticipantAvatars()); } Room::~Room() @@ -104,5 +106,5 @@ void Room::onParticipantJoined(const Models::Participant& participant) void Room::onParticipantLeft(const QString& name) { - line->dropPalAvatar(name); + line->movePalAvatarToEx(name); }