forked from blue/squawk
receiving account owner vCard, displaying avatars in roster
This commit is contained in:
parent
64e33b6139
commit
dc1ec1c9d4
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0)
|
|||||||
project(squawk)
|
project(squawk)
|
||||||
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
set(CMAKE_CXX_STANDARD 11)
|
set(CMAKE_CXX_STANDARD 14)
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
202
core/account.cpp
202
core/account.cpp
@ -41,7 +41,10 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
|
|||||||
maxReconnectTimes(0),
|
maxReconnectTimes(0),
|
||||||
reconnectTimes(0),
|
reconnectTimes(0),
|
||||||
queuedContacts(),
|
queuedContacts(),
|
||||||
outOfRosterContacts()
|
outOfRosterContacts(),
|
||||||
|
avatarHash(),
|
||||||
|
avatarType(),
|
||||||
|
ownVCardRequestInProgress(false)
|
||||||
{
|
{
|
||||||
config.setUser(p_login);
|
config.setUser(p_login);
|
||||||
config.setDomain(p_server);
|
config.setDomain(p_server);
|
||||||
@ -81,6 +84,52 @@ Account::Account(const QString& p_login, const QString& p_server, const QString&
|
|||||||
|
|
||||||
QXmppVCardManager& vm = client.vCardManager();
|
QXmppVCardManager& vm = client.vCardManager();
|
||||||
QObject::connect(&vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived);
|
QObject::connect(&vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived);
|
||||||
|
//QObject::connect(&vm, &QXmppVCardManager::clientVCardReceived, this, &Account::onOwnVCardReceived); //for some reason it doesn't work, launching from common handler
|
||||||
|
|
||||||
|
QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
||||||
|
path += "/" + name;
|
||||||
|
QDir dir(path);
|
||||||
|
|
||||||
|
if (!dir.exists()) {
|
||||||
|
bool res = dir.mkpath(path);
|
||||||
|
if (!res) {
|
||||||
|
qDebug() << "Couldn't create a cache directory for account" << name;
|
||||||
|
throw 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile* avatar = new QFile(path + "/avatar.png");
|
||||||
|
QString type = "png";
|
||||||
|
if (!avatar->exists()) {
|
||||||
|
delete avatar;
|
||||||
|
avatar = new QFile(path + "/avatar.jpg");
|
||||||
|
QString type = "jpg";
|
||||||
|
if (!avatar->exists()) {
|
||||||
|
delete avatar;
|
||||||
|
avatar = new QFile(path + "/avatar.jpeg");
|
||||||
|
QString type = "jpeg";
|
||||||
|
if (!avatar->exists()) {
|
||||||
|
delete avatar;
|
||||||
|
avatar = new QFile(path + "/avatar.gif");
|
||||||
|
QString type = "gif";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatar->exists()) {
|
||||||
|
if (avatar->open(QFile::ReadOnly)) {
|
||||||
|
QCryptographicHash sha1(QCryptographicHash::Sha1);
|
||||||
|
sha1.addData(avatar);
|
||||||
|
avatarHash = sha1.result();
|
||||||
|
avatarType = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (avatarType.size() != 0) {
|
||||||
|
presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
|
||||||
|
presence.setPhotoHash(avatarHash.toUtf8());
|
||||||
|
} else {
|
||||||
|
presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Account::~Account()
|
Account::~Account()
|
||||||
@ -191,6 +240,9 @@ QString Core::Account::getServer() const
|
|||||||
|
|
||||||
void Core::Account::onRosterReceived()
|
void Core::Account::onRosterReceived()
|
||||||
{
|
{
|
||||||
|
client.vCardManager().requestClientVCard(); //TODO need to make sure server actually supports vCards
|
||||||
|
ownVCardRequestInProgress = true;
|
||||||
|
|
||||||
QXmppRosterManager& rm = client.rosterManager();
|
QXmppRosterManager& rm = client.rosterManager();
|
||||||
QStringList bj = rm.getRosterBareJids();
|
QStringList bj = rm.getRosterBareJids();
|
||||||
for (int i = 0; i < bj.size(); ++i) {
|
for (int i = 0; i < bj.size(); ++i) {
|
||||||
@ -283,14 +335,15 @@ void Core::Account::addedAccount(const QString& jid)
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (contact->hasAvatar()) {
|
if (contact->hasAvatar()) {
|
||||||
if (contact->isAvatarAutoGenerated()) {
|
if (!contact->isAvatarAutoGenerated()) {
|
||||||
cData.insert("avatarType", static_cast<uint>(Shared::Avatar::valid));
|
cData.insert("avatarState", static_cast<uint>(Shared::Avatar::valid));
|
||||||
} else {
|
} else {
|
||||||
cData.insert("avatarType", static_cast<uint>(Shared::Avatar::autocreated));
|
cData.insert("avatarState", static_cast<uint>(Shared::Avatar::autocreated));
|
||||||
}
|
}
|
||||||
cData.insert("avatarPath", contact->avatarPath());
|
cData.insert("avatarPath", contact->avatarPath());
|
||||||
} else {
|
} else {
|
||||||
cData.insert("avatarType", static_cast<uint>(Shared::Avatar::empty));
|
cData.insert("avatarState", static_cast<uint>(Shared::Avatar::empty));
|
||||||
|
cData.insert("avatarPath", "");
|
||||||
client.vCardManager().requestVCard(jid);
|
client.vCardManager().requestVCard(jid);
|
||||||
pendingVCardRequests.insert(jid);
|
pendingVCardRequests.insert(jid);
|
||||||
}
|
}
|
||||||
@ -337,9 +390,9 @@ void Core::Account::handleNewConference(Core::Conference* contact)
|
|||||||
QObject::connect(contact, &Conference::removeParticipant, this, &Account::onMucRemoveParticipant);
|
QObject::connect(contact, &Conference::removeParticipant, this, &Account::onMucRemoveParticipant);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Core::Account::onPresenceReceived(const QXmppPresence& presence)
|
void Core::Account::onPresenceReceived(const QXmppPresence& p_presence)
|
||||||
{
|
{
|
||||||
QString id = presence.from();
|
QString id = p_presence.from();
|
||||||
QStringList comps = id.split("/");
|
QStringList comps = id.split("/");
|
||||||
QString jid = comps.front();
|
QString jid = comps.front();
|
||||||
QString resource = comps.back();
|
QString resource = comps.back();
|
||||||
@ -348,16 +401,35 @@ void Core::Account::onPresenceReceived(const QXmppPresence& presence)
|
|||||||
|
|
||||||
if (jid == myJid) {
|
if (jid == myJid) {
|
||||||
if (resource == getResource()) {
|
if (resource == getResource()) {
|
||||||
emit availabilityChanged(presence.availableStatusType());
|
emit availabilityChanged(p_presence.availableStatusType());
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "Received a presence for another resource of my " << name << " account, skipping";
|
if (!ownVCardRequestInProgress) {
|
||||||
|
switch (p_presence.vCardUpdateType()) {
|
||||||
|
case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo
|
||||||
|
break;
|
||||||
|
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 (avatarType.size() > 0) {
|
||||||
|
client.vCardManager().requestClientVCard();
|
||||||
|
ownVCardRequestInProgress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load
|
||||||
|
if (avatarHash != p_presence.photoHash()) {
|
||||||
|
client.vCardManager().requestClientVCard();
|
||||||
|
ownVCardRequestInProgress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) {
|
if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) {
|
||||||
std::map<QString, Contact*>::const_iterator itr = contacts.find(jid);
|
std::map<QString, Contact*>::const_iterator itr = contacts.find(jid);
|
||||||
if (itr != contacts.end()) {
|
if (itr != contacts.end()) {
|
||||||
Contact* cnt = itr->second;
|
Contact* cnt = itr->second;
|
||||||
switch (presence.vCardUpdateType()) {
|
switch (p_presence.vCardUpdateType()) {
|
||||||
case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo
|
case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo
|
||||||
break;
|
break;
|
||||||
case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here
|
case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here
|
||||||
@ -373,7 +445,7 @@ void Core::Account::onPresenceReceived(const QXmppPresence& presence)
|
|||||||
client.vCardManager().requestVCard(jid);
|
client.vCardManager().requestVCard(jid);
|
||||||
pendingVCardRequests.insert(jid);
|
pendingVCardRequests.insert(jid);
|
||||||
} else {
|
} else {
|
||||||
if (cnt->avatarHash() != presence.photoHash()) {
|
if (cnt->avatarHash() != p_presence.photoHash()) {
|
||||||
client.vCardManager().requestVCard(jid);
|
client.vCardManager().requestVCard(jid);
|
||||||
pendingVCardRequests.insert(jid);
|
pendingVCardRequests.insert(jid);
|
||||||
}
|
}
|
||||||
@ -388,19 +460,19 @@ void Core::Account::onPresenceReceived(const QXmppPresence& presence)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (presence.type()) {
|
switch (p_presence.type()) {
|
||||||
case QXmppPresence::Error:
|
case QXmppPresence::Error:
|
||||||
qDebug() << "An error reported by presence from" << id << presence.error().text();
|
qDebug() << "An error reported by presence from" << id << p_presence.error().text();
|
||||||
break;
|
break;
|
||||||
case QXmppPresence::Available:{
|
case QXmppPresence::Available:{
|
||||||
QDateTime lastInteraction = presence.lastUserInteraction();
|
QDateTime lastInteraction = p_presence.lastUserInteraction();
|
||||||
if (!lastInteraction.isValid()) {
|
if (!lastInteraction.isValid()) {
|
||||||
lastInteraction = QDateTime::currentDateTime();
|
lastInteraction = QDateTime::currentDateTime();
|
||||||
}
|
}
|
||||||
emit addPresence(jid, resource, {
|
emit addPresence(jid, resource, {
|
||||||
{"lastActivity", lastInteraction},
|
{"lastActivity", lastInteraction},
|
||||||
{"availability", presence.availableStatusType()}, //TODO check and handle invisible
|
{"availability", p_presence.availableStatusType()}, //TODO check and handle invisible
|
||||||
{"status", presence.statusText()}
|
{"status", p_presence.statusText()}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1267,7 +1339,11 @@ void Core::Account::onVCardReceived(const QXmppVCardIq& card)
|
|||||||
if (contItr == contacts.end()) {
|
if (contItr == contacts.end()) {
|
||||||
std::map<QString, Conference*>::const_iterator confItr = conferences.find(jid);
|
std::map<QString, Conference*>::const_iterator confItr = conferences.find(jid);
|
||||||
if (confItr == conferences.end()) {
|
if (confItr == conferences.end()) {
|
||||||
qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping";
|
if (jid == getLogin() + "@" + getServer()) {
|
||||||
|
onOwnVCardReceived(card);
|
||||||
|
} else {
|
||||||
|
qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
item = confItr->second;
|
item = confItr->second;
|
||||||
@ -1284,15 +1360,99 @@ void Core::Account::onVCardReceived(const QXmppVCardIq& card)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Core::Account::onOwnVCardReceived(const QXmppVCardIq& card)
|
||||||
|
{
|
||||||
|
QByteArray ava = card.photo();
|
||||||
|
bool changed = false;
|
||||||
|
QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/";
|
||||||
|
if (ava.size() > 0) {
|
||||||
|
QCryptographicHash sha1(QCryptographicHash::Sha1);
|
||||||
|
sha1.addData(ava);
|
||||||
|
QString newHash(sha1.result());
|
||||||
|
QMimeDatabase db;
|
||||||
|
QMimeType newType = db.mimeTypeForData(ava);
|
||||||
|
if (avatarType.size() > 0) {
|
||||||
|
if (avatarHash != newHash) {
|
||||||
|
QString oldPath = path + "avatar." + avatarType;
|
||||||
|
QFile oldAvatar(oldPath);
|
||||||
|
bool oldToRemove = false;
|
||||||
|
if (oldAvatar.exists()) {
|
||||||
|
if (oldAvatar.rename(oldPath + ".bak")) {
|
||||||
|
oldToRemove = true;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Received new avatar for account" << name << "but can't get rid of the old one, doing nothing";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
|
||||||
|
if (newAvatar.open(QFile::WriteOnly)) {
|
||||||
|
newAvatar.write(ava);
|
||||||
|
newAvatar.close();
|
||||||
|
avatarHash = newHash;
|
||||||
|
avatarType = newType.preferredSuffix();
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Received new avatar for account" << name << "but can't save it";
|
||||||
|
if (oldToRemove) {
|
||||||
|
qDebug() << "rolling back to the old avatar";
|
||||||
|
if (!oldAvatar.rename(oldPath)) {
|
||||||
|
qDebug() << "Couldn't roll back to the old avatar in account" << name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QFile newAvatar(path + "avatar." + newType.preferredSuffix());
|
||||||
|
if (newAvatar.open(QFile::WriteOnly)) {
|
||||||
|
newAvatar.write(ava);
|
||||||
|
newAvatar.close();
|
||||||
|
avatarHash = newHash;
|
||||||
|
avatarType = newType.preferredSuffix();
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Received new avatar for account" << name << "but can't save it";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (avatarType.size() > 0) {
|
||||||
|
QFile oldAvatar(path + "avatar." + avatarType);
|
||||||
|
if (!oldAvatar.remove()) {
|
||||||
|
qDebug() << "Received vCard for account" << name << "without avatar, but can't get rid of the file, doing nothing";
|
||||||
|
} else {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
QMap<QString, QVariant> change;
|
||||||
|
if (avatarType.size() > 0) {
|
||||||
|
presence.setPhotoHash(avatarHash.toUtf8());
|
||||||
|
presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto);
|
||||||
|
change.insert("avatarPath", path + "avatar." + avatarType);
|
||||||
|
} else {
|
||||||
|
presence.setPhotoHash("");
|
||||||
|
presence.setVCardUpdateType(QXmppPresence::VCardUpdateNoPhoto);
|
||||||
|
change.insert("avatarPath", "");
|
||||||
|
}
|
||||||
|
client.setClientPresence(presence);
|
||||||
|
}
|
||||||
|
|
||||||
|
ownVCardRequestInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Core::Account::getAvatarPath() const
|
||||||
|
{
|
||||||
|
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/" + "avatar." + avatarType;
|
||||||
|
}
|
||||||
|
|
||||||
void Core::Account::onContactAvatarChanged(Shared::Avatar type, const QString& path)
|
void Core::Account::onContactAvatarChanged(Shared::Avatar type, const QString& path)
|
||||||
{
|
{
|
||||||
RosterItem* item = static_cast<RosterItem*>(sender());
|
RosterItem* item = static_cast<RosterItem*>(sender());
|
||||||
QMap<QString, QVariant> cData({
|
QMap<QString, QVariant> cData({
|
||||||
{"avatarType", static_cast<uint>(type)}
|
{"avatarState", static_cast<uint>(type)},
|
||||||
|
{"avatarPath", path}
|
||||||
});
|
});
|
||||||
if (type != Shared::Avatar::empty) {
|
|
||||||
cData.insert("avatarPath", path);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit changeContact(item->jid, cData);
|
emit changeContact(item->jid, cData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,13 @@
|
|||||||
#ifndef CORE_ACCOUNT_H
|
#ifndef CORE_ACCOUNT_H
|
||||||
#define CORE_ACCOUNT_H
|
#define CORE_ACCOUNT_H
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
#include <QObject>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
@ -56,6 +62,7 @@ public:
|
|||||||
QString getServer() const;
|
QString getServer() const;
|
||||||
QString getPassword() const;
|
QString getPassword() const;
|
||||||
QString getResource() const;
|
QString getResource() const;
|
||||||
|
QString getAvatarPath() const;
|
||||||
Shared::Availability getAvailability() const;
|
Shared::Availability getAvailability() const;
|
||||||
|
|
||||||
void setName(const QString& p_name);
|
void setName(const QString& p_name);
|
||||||
@ -82,6 +89,7 @@ public:
|
|||||||
void addRoomRequest(const QString& jid, const QString& nick, const QString& password, bool autoJoin);
|
void addRoomRequest(const QString& jid, const QString& nick, const QString& password, bool autoJoin);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
void changed(const QMap<QString, QVariant>& data);
|
||||||
void connectionStateChanged(int);
|
void connectionStateChanged(int);
|
||||||
void availabilityChanged(int);
|
void availabilityChanged(int);
|
||||||
void addGroup(const QString& name);
|
void addGroup(const QString& name);
|
||||||
@ -123,6 +131,10 @@ private:
|
|||||||
std::set<QString> outOfRosterContacts;
|
std::set<QString> outOfRosterContacts;
|
||||||
std::set<QString> pendingVCardRequests;
|
std::set<QString> pendingVCardRequests;
|
||||||
|
|
||||||
|
QString avatarHash;
|
||||||
|
QString avatarType;
|
||||||
|
bool ownVCardRequestInProgress;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onClientConnected();
|
void onClientConnected();
|
||||||
void onClientDisconnected();
|
void onClientDisconnected();
|
||||||
@ -165,6 +177,7 @@ private slots:
|
|||||||
void onMamLog(QXmppLogger::MessageType type, const QString &msg);
|
void onMamLog(QXmppLogger::MessageType type, const QString &msg);
|
||||||
|
|
||||||
void onVCardReceived(const QXmppVCardIq& card);
|
void onVCardReceived(const QXmppVCardIq& card);
|
||||||
|
void onOwnVCardReceived(const QXmppVCardIq& card);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addedAccount(const QString &bareJid);
|
void addedAccount(const QString &bareJid);
|
||||||
|
@ -28,9 +28,9 @@ Core::Squawk::Squawk(QObject* parent):
|
|||||||
amap(),
|
amap(),
|
||||||
network()
|
network()
|
||||||
{
|
{
|
||||||
connect(&network, SIGNAL(fileLocalPathResponse(const QString&, const QString&)), this, SIGNAL(fileLocalPathResponse(const QString&, const QString&)));
|
connect(&network, &NetworkAccess::fileLocalPathResponse, this, &Squawk::fileLocalPathResponse);
|
||||||
connect(&network, SIGNAL(downloadFileProgress(const QString&, qreal)), this, SIGNAL(downloadFileProgress(const QString&, qreal)));
|
connect(&network, &NetworkAccess::downloadFileProgress, this, &Squawk::downloadFileProgress);
|
||||||
connect(&network, SIGNAL(downloadFileError(const QString&, const QString&)), this, SIGNAL(downloadFileError(const QString&, const QString&)));
|
connect(&network, &NetworkAccess::downloadFileError, this, &Squawk::downloadFileError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::Squawk::~Squawk()
|
Core::Squawk::~Squawk()
|
||||||
@ -110,36 +110,28 @@ void Core::Squawk::addAccount(const QString& login, const QString& server, const
|
|||||||
accounts.push_back(acc);
|
accounts.push_back(acc);
|
||||||
amap.insert(std::make_pair(name, acc));
|
amap.insert(std::make_pair(name, acc));
|
||||||
|
|
||||||
connect(acc, SIGNAL(connectionStateChanged(int)), this, SLOT(onAccountConnectionStateChanged(int)));
|
connect(acc, &Account::connectionStateChanged, this, &Squawk::onAccountConnectionStateChanged);
|
||||||
connect(acc, SIGNAL(error(const QString&)), this, SLOT(onAccountError(const QString&)));
|
connect(acc, &Account::changed, this, &Squawk::onAccountChanged);
|
||||||
connect(acc, SIGNAL(availabilityChanged(int)), this, SLOT(onAccountAvailabilityChanged(int)));
|
connect(acc, &Account::error, this, &Squawk::onAccountError);
|
||||||
connect(acc, SIGNAL(addContact(const QString&, const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::availabilityChanged, this, &Squawk::onAccountAvailabilityChanged);
|
||||||
this, SLOT(onAccountAddContact(const QString&, const QString&, const QMap<QString, QVariant>&)));
|
connect(acc, &Account::addContact, this, &Squawk::onAccountAddContact);
|
||||||
connect(acc, SIGNAL(addGroup(const QString&)), this, SLOT(onAccountAddGroup(const QString&)));
|
connect(acc, &Account::addGroup, this, &Squawk::onAccountAddGroup);
|
||||||
connect(acc, SIGNAL(removeGroup(const QString&)), this, SLOT(onAccountRemoveGroup(const QString&)));
|
connect(acc, &Account::removeGroup, this, &Squawk::onAccountRemoveGroup);
|
||||||
connect(acc, SIGNAL(removeContact(const QString&)), this, SLOT(onAccountRemoveContact(const QString&)));
|
connect(acc, qOverload<const QString&, const QString&>(&Account::removeContact), this, qOverload<const QString&, const QString&>(&Squawk::onAccountRemoveContact));
|
||||||
connect(acc, SIGNAL(removeContact(const QString&, const QString&)), this, SLOT(onAccountRemoveContact(const QString&, const QString&)));
|
connect(acc, qOverload<const QString&>(&Account::removeContact), this, qOverload<const QString&>(&Squawk::onAccountRemoveContact));
|
||||||
connect(acc, SIGNAL(changeContact(const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::changeContact, this, &Squawk::onAccountChangeContact);
|
||||||
this, SLOT(onAccountChangeContact(const QString&, const QMap<QString, QVariant>&)));
|
connect(acc, &Account::addPresence, this, &Squawk::onAccountAddPresence);
|
||||||
connect(acc, SIGNAL(addPresence(const QString&, const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::removePresence, this, &Squawk::onAccountRemovePresence);
|
||||||
this, SLOT(onAccountAddPresence(const QString&, const QString&, const QMap<QString, QVariant>&)));
|
connect(acc, &Account::message, this, &Squawk::onAccountMessage);
|
||||||
connect(acc, SIGNAL(removePresence(const QString&, const QString&)), this, SLOT(onAccountRemovePresence(const QString&, const QString&)));
|
connect(acc, &Account::responseArchive, this, &Squawk::onAccountResponseArchive);
|
||||||
connect(acc, SIGNAL(message(const Shared::Message&)), this, SLOT(onAccountMessage(const Shared::Message&)));
|
|
||||||
connect(acc, SIGNAL(responseArchive(const QString&, const std::list<Shared::Message>&)),
|
|
||||||
this, SLOT(onAccountResponseArchive(const QString&, const std::list<Shared::Message>&)));
|
|
||||||
|
|
||||||
connect(acc, SIGNAL(addRoom(const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::addRoom, this, &Squawk::onAccountAddRoom);
|
||||||
this, SLOT(onAccountAddRoom(const QString&, const QMap<QString, QVariant>&)));
|
connect(acc, &Account::changeRoom, this, &Squawk::onAccountChangeRoom);
|
||||||
connect(acc, SIGNAL(changeRoom(const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::removeRoom, this, &Squawk::onAccountRemoveRoom);
|
||||||
this, SLOT(onAccountChangeRoom(const QString&, const QMap<QString, QVariant>&)));
|
|
||||||
connect(acc, SIGNAL(removeRoom(const QString&)), this, SLOT(onAccountRemoveRoom(const QString&)));
|
|
||||||
|
|
||||||
connect(acc, SIGNAL(addRoomParticipant(const QString&, const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::addRoomParticipant, this, &Squawk::onAccountAddRoomPresence);
|
||||||
this, SLOT(onAccountAddRoomPresence(const QString&, const QString&, const QMap<QString, QVariant>&)));
|
connect(acc, &Account::changeRoomParticipant, this, &Squawk::onAccountChangeRoomPresence);
|
||||||
connect(acc, SIGNAL(changeRoomParticipant(const QString&, const QString&, const QMap<QString, QVariant>&)),
|
connect(acc, &Account::removeRoomParticipant, this, &Squawk::onAccountRemoveRoomPresence);
|
||||||
this, SLOT(onAccountChangeRoomPresence(const QString&, const QString&, const QMap<QString, QVariant>&)));
|
|
||||||
connect(acc, SIGNAL(removeRoomParticipant(const QString&, const QString&)),
|
|
||||||
this, SLOT(onAccountRemoveRoomPresence(const QString&, const QString&)));
|
|
||||||
|
|
||||||
|
|
||||||
QMap<QString, QVariant> map = {
|
QMap<QString, QVariant> map = {
|
||||||
@ -150,8 +142,10 @@ void Core::Squawk::addAccount(const QString& login, const QString& server, const
|
|||||||
{"resource", resource},
|
{"resource", resource},
|
||||||
{"state", Shared::disconnected},
|
{"state", Shared::disconnected},
|
||||||
{"offline", Shared::offline},
|
{"offline", Shared::offline},
|
||||||
{"error", ""}
|
{"error", ""},
|
||||||
|
{"avatarPath", acc->getAvatarPath()}
|
||||||
};
|
};
|
||||||
|
|
||||||
emit newAccount(map);
|
emit newAccount(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +257,12 @@ void Core::Squawk::onAccountAvailabilityChanged(int state)
|
|||||||
emit changeAccount(acc->getName(), {{"availability", state}});
|
emit changeAccount(acc->getName(), {{"availability", state}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Core::Squawk::onAccountChanged(const QMap<QString, QVariant>& data)
|
||||||
|
{
|
||||||
|
Account* acc = static_cast<Account*>(sender());
|
||||||
|
emit changeAccount(acc->getName(), data);
|
||||||
|
}
|
||||||
|
|
||||||
void Core::Squawk::onAccountMessage(const Shared::Message& data)
|
void Core::Squawk::onAccountMessage(const Shared::Message& data)
|
||||||
{
|
{
|
||||||
Account* acc = static_cast<Account*>(sender());
|
Account* acc = static_cast<Account*>(sender());
|
||||||
|
@ -23,7 +23,8 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <deque>
|
#include <QtGlobal>
|
||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
|
||||||
#include "account.h"
|
#include "account.h"
|
||||||
@ -106,6 +107,7 @@ private:
|
|||||||
private slots:
|
private slots:
|
||||||
void onAccountConnectionStateChanged(int state);
|
void onAccountConnectionStateChanged(int state);
|
||||||
void onAccountAvailabilityChanged(int state);
|
void onAccountAvailabilityChanged(int state);
|
||||||
|
void onAccountChanged(const QMap<QString, QVariant>& data);
|
||||||
void onAccountAddGroup(const QString& name);
|
void onAccountAddGroup(const QString& name);
|
||||||
void onAccountError(const QString& text);
|
void onAccountError(const QString& text);
|
||||||
void onAccountRemoveGroup(const QString& name);
|
void onAccountRemoveGroup(const QString& name);
|
||||||
|
@ -26,6 +26,7 @@ Models::Account::Account(const QMap<QString, QVariant>& data, Models::Item* pare
|
|||||||
server(data.value("server").toString()),
|
server(data.value("server").toString()),
|
||||||
resource(data.value("resource").toString()),
|
resource(data.value("resource").toString()),
|
||||||
error(data.value("error").toString()),
|
error(data.value("error").toString()),
|
||||||
|
avatarPath(data.value("avatarPath").toString()),
|
||||||
state(Shared::disconnected),
|
state(Shared::disconnected),
|
||||||
availability(Shared::offline)
|
availability(Shared::offline)
|
||||||
{
|
{
|
||||||
@ -162,6 +163,8 @@ QVariant Models::Account::data(int column) const
|
|||||||
return QCoreApplication::translate("Global", Shared::availabilityNames[availability].toLatin1());
|
return QCoreApplication::translate("Global", Shared::availabilityNames[availability].toLatin1());
|
||||||
case 7:
|
case 7:
|
||||||
return resource;
|
return resource;
|
||||||
|
case 8:
|
||||||
|
return avatarPath;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -169,7 +172,7 @@ QVariant Models::Account::data(int column) const
|
|||||||
|
|
||||||
int Models::Account::columnCount() const
|
int Models::Account::columnCount() const
|
||||||
{
|
{
|
||||||
return 8;
|
return 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Models::Account::update(const QString& field, const QVariant& value)
|
void Models::Account::update(const QString& field, const QVariant& value)
|
||||||
@ -190,6 +193,8 @@ void Models::Account::update(const QString& field, const QVariant& value)
|
|||||||
setResource(value.toString());
|
setResource(value.toString());
|
||||||
} else if (field == "error") {
|
} else if (field == "error") {
|
||||||
setError(value.toString());
|
setError(value.toString());
|
||||||
|
} else if (field == "avatarPath") {
|
||||||
|
setAvatarPath(value.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,3 +229,15 @@ void Models::Account::toOfflineState()
|
|||||||
setAvailability(Shared::offline);
|
setAvailability(Shared::offline);
|
||||||
Item::toOfflineState();
|
Item::toOfflineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Models::Account::getAvatarPath()
|
||||||
|
{
|
||||||
|
return avatarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Account::setAvatarPath(const QString& path)
|
||||||
|
{
|
||||||
|
avatarPath = path;
|
||||||
|
changed(8); //it's uncoditional because the path doesn't change when one avatar of the same type replaces another, sha1 sums checks are on the backend
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,9 @@ namespace Models {
|
|||||||
void setError(const QString& p_resource);
|
void setError(const QString& p_resource);
|
||||||
QString getError() const;
|
QString getError() const;
|
||||||
|
|
||||||
|
void setAvatarPath(const QString& path);
|
||||||
|
QString getAvatarPath();
|
||||||
|
|
||||||
void setAvailability(Shared::Availability p_avail);
|
void setAvailability(Shared::Availability p_avail);
|
||||||
void setAvailability(unsigned int p_avail);
|
void setAvailability(unsigned int p_avail);
|
||||||
Shared::Availability getAvailability() const;
|
Shared::Availability getAvailability() const;
|
||||||
@ -67,6 +70,7 @@ namespace Models {
|
|||||||
QString server;
|
QString server;
|
||||||
QString resource;
|
QString resource;
|
||||||
QString error;
|
QString error;
|
||||||
|
QString avatarPath;
|
||||||
Shared::ConnectionState state;
|
Shared::ConnectionState state;
|
||||||
Shared::Availability availability;
|
Shared::Availability availability;
|
||||||
|
|
||||||
|
@ -25,14 +25,26 @@ Models::Contact::Contact(const QString& p_jid ,const QMap<QString, QVariant> &da
|
|||||||
jid(p_jid),
|
jid(p_jid),
|
||||||
availability(Shared::offline),
|
availability(Shared::offline),
|
||||||
state(Shared::none),
|
state(Shared::none),
|
||||||
|
avatarState(Shared::Avatar::empty),
|
||||||
presences(),
|
presences(),
|
||||||
messages(),
|
messages(),
|
||||||
childMessages(0)
|
childMessages(0),
|
||||||
|
status(),
|
||||||
|
avatarPath()
|
||||||
{
|
{
|
||||||
QMap<QString, QVariant>::const_iterator itr = data.find("state");
|
QMap<QString, QVariant>::const_iterator itr = data.find("state");
|
||||||
if (itr != data.end()) {
|
if (itr != data.end()) {
|
||||||
setState(itr.value().toUInt());
|
setState(itr.value().toUInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itr = data.find("avatarState");
|
||||||
|
if (itr != data.end()) {
|
||||||
|
setAvatarState(itr.value().toUInt());
|
||||||
|
}
|
||||||
|
itr = data.find("avatarPath");
|
||||||
|
if (itr != data.end()) {
|
||||||
|
setAvatarPath(itr.value().toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Models::Contact::~Contact()
|
Models::Contact::~Contact()
|
||||||
@ -100,7 +112,7 @@ void Models::Contact::setStatus(const QString& p_state)
|
|||||||
|
|
||||||
int Models::Contact::columnCount() const
|
int Models::Contact::columnCount() const
|
||||||
{
|
{
|
||||||
return 6;
|
return 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant Models::Contact::data(int column) const
|
QVariant Models::Contact::data(int column) const
|
||||||
@ -118,6 +130,10 @@ QVariant Models::Contact::data(int column) const
|
|||||||
return getMessagesCount();
|
return getMessagesCount();
|
||||||
case 5:
|
case 5:
|
||||||
return getStatus();
|
return getStatus();
|
||||||
|
case 6:
|
||||||
|
return static_cast<quint8>(getAvatarState());
|
||||||
|
case 7:
|
||||||
|
return getAvatarPath();
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -142,6 +158,10 @@ void Models::Contact::update(const QString& field, const QVariant& value)
|
|||||||
setAvailability(value.toUInt());
|
setAvailability(value.toUInt());
|
||||||
} else if (field == "state") {
|
} else if (field == "state") {
|
||||||
setState(value.toUInt());
|
setState(value.toUInt());
|
||||||
|
} else if (field == "avatarState") {
|
||||||
|
setAvatarState(value.toUInt());
|
||||||
|
} else if (field == "avatarPath") {
|
||||||
|
setAvatarPath(value.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,3 +368,39 @@ Models::Contact::Contact(const Models::Contact& other):
|
|||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Models::Contact::getAvatarPath() const
|
||||||
|
{
|
||||||
|
return avatarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shared::Avatar Models::Contact::getAvatarState() const
|
||||||
|
{
|
||||||
|
return avatarState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Contact::setAvatarPath(const QString& path)
|
||||||
|
{
|
||||||
|
if (path != avatarPath) {
|
||||||
|
avatarPath = path;
|
||||||
|
changed(7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Contact::setAvatarState(Shared::Avatar p_state)
|
||||||
|
{
|
||||||
|
if (avatarState != p_state) {
|
||||||
|
avatarState = p_state;
|
||||||
|
changed(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Contact::setAvatarState(unsigned int p_state)
|
||||||
|
{
|
||||||
|
if (p_state <= static_cast<quint8>(Shared::Avatar::valid)) {
|
||||||
|
Shared::Avatar state = static_cast<Shared::Avatar>(p_state);
|
||||||
|
setAvatarState(state);
|
||||||
|
} else {
|
||||||
|
qDebug() << "An attempt to set invalid avatar state" << p_state << "to the contact" << jid << ", skipping";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -40,6 +40,8 @@ public:
|
|||||||
QString getJid() const;
|
QString getJid() const;
|
||||||
Shared::Availability getAvailability() const;
|
Shared::Availability getAvailability() const;
|
||||||
Shared::SubscriptionState getState() const;
|
Shared::SubscriptionState getState() const;
|
||||||
|
Shared::Avatar getAvatarState() const;
|
||||||
|
QString getAvatarPath() const;
|
||||||
QIcon getStatusIcon(bool big = false) const;
|
QIcon getStatusIcon(bool big = false) const;
|
||||||
|
|
||||||
int columnCount() const override;
|
int columnCount() const override;
|
||||||
@ -75,6 +77,9 @@ protected:
|
|||||||
void setAvailability(unsigned int p_state);
|
void setAvailability(unsigned int p_state);
|
||||||
void setState(Shared::SubscriptionState p_state);
|
void setState(Shared::SubscriptionState p_state);
|
||||||
void setState(unsigned int p_state);
|
void setState(unsigned int p_state);
|
||||||
|
void setAvatarState(Shared::Avatar p_state);
|
||||||
|
void setAvatarState(unsigned int p_state);
|
||||||
|
void setAvatarPath(const QString& path);
|
||||||
void setJid(const QString p_jid);
|
void setJid(const QString p_jid);
|
||||||
void setStatus(const QString& p_state);
|
void setStatus(const QString& p_state);
|
||||||
|
|
||||||
@ -82,10 +87,12 @@ private:
|
|||||||
QString jid;
|
QString jid;
|
||||||
Shared::Availability availability;
|
Shared::Availability availability;
|
||||||
Shared::SubscriptionState state;
|
Shared::SubscriptionState state;
|
||||||
|
Shared::Avatar avatarState;
|
||||||
QMap<QString, Presence*> presences;
|
QMap<QString, Presence*> presences;
|
||||||
Messages messages;
|
Messages messages;
|
||||||
unsigned int childMessages;
|
unsigned int childMessages;
|
||||||
QString status;
|
QString status;
|
||||||
|
QString avatarPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ const Models::Item * Models::Item::parentItemConst() const
|
|||||||
|
|
||||||
int Models::Item::columnCount() const
|
int Models::Item::columnCount() const
|
||||||
{
|
{
|
||||||
return 1;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Models::Item::getName() const
|
QString Models::Item::getName() const
|
||||||
|
@ -68,6 +68,9 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
|
|||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
{
|
{
|
||||||
|
if (index.column() != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
switch (item->type) {
|
switch (item->type) {
|
||||||
case Item::group: {
|
case Item::group: {
|
||||||
Group* gr = static_cast<Group*>(item);
|
Group* gr = static_cast<Group*>(item);
|
||||||
@ -91,26 +94,50 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const
|
|||||||
case Qt::DecorationRole:
|
case Qt::DecorationRole:
|
||||||
switch (item->type) {
|
switch (item->type) {
|
||||||
case Item::account: {
|
case Item::account: {
|
||||||
|
quint8 col = index.column();
|
||||||
Account* acc = static_cast<Account*>(item);
|
Account* acc = static_cast<Account*>(item);
|
||||||
result = acc->getStatusIcon(false);
|
if (col == 0) {
|
||||||
|
result = acc->getStatusIcon(false);
|
||||||
|
} else if (col == 1) {
|
||||||
|
QString path = acc->getAvatarPath();
|
||||||
|
if (path.size() > 0) {
|
||||||
|
result = QIcon(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Item::contact: {
|
case Item::contact: {
|
||||||
Contact* contact = static_cast<Contact*>(item);
|
Contact* contact = static_cast<Contact*>(item);
|
||||||
result = contact->getStatusIcon(false);
|
quint8 col = index.column();
|
||||||
|
if (col == 0) {
|
||||||
|
result = contact->getStatusIcon(false);
|
||||||
|
} else if (col == 1) {
|
||||||
|
if (contact->getAvatarState() != Shared::Avatar::empty) {
|
||||||
|
result = QIcon(contact->getAvatarPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Item::presence: {
|
case Item::presence: {
|
||||||
|
if (index.column() != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
Presence* presence = static_cast<Presence*>(item);
|
Presence* presence = static_cast<Presence*>(item);
|
||||||
result = presence->getStatusIcon(false);
|
result = presence->getStatusIcon(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Item::room: {
|
case Item::room: {
|
||||||
|
if (index.column() != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
Room* room = static_cast<Room*>(item);
|
Room* room = static_cast<Room*>(item);
|
||||||
result = room->getStatusIcon(false);
|
result = room->getStatusIcon(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Item::participant: {
|
case Item::participant: {
|
||||||
|
if (index.column() != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
Participant* p = static_cast<Participant*>(item);
|
Participant* p = static_cast<Participant*>(item);
|
||||||
result = p->getStatusIcon(false);
|
result = p->getStatusIcon(false);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,10 @@ Squawk::Squawk(QWidget *parent) :
|
|||||||
m_ui->setupUi(this);
|
m_ui->setupUi(this);
|
||||||
m_ui->roster->setModel(&rosterModel);
|
m_ui->roster->setModel(&rosterModel);
|
||||||
m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu);
|
m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
m_ui->roster->setColumnWidth(1, 20);
|
||||||
|
m_ui->roster->setIconSize(QSize(20, 20));
|
||||||
|
m_ui->roster->header()->setStretchLastSection(false);
|
||||||
|
m_ui->roster->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
|
||||||
for (unsigned int i = Shared::availabilityLowest; i < Shared::availabilityHighest + 1; ++i) {
|
for (unsigned int i = Shared::availabilityLowest; i < Shared::availabilityHighest + 1; ++i) {
|
||||||
Shared::Availability av = static_cast<Shared::Availability>(i);
|
Shared::Availability av = static_cast<Shared::Availability>(i);
|
||||||
|
@ -51,6 +51,9 @@
|
|||||||
<property name="frameShadow">
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Sunken</enum>
|
<enum>QFrame::Sunken</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="uniformRowHeights">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="expandsOnDoubleClick">
|
<property name="expandsOnDoubleClick">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -122,7 +125,8 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset theme="resource-group-new"/>
|
<iconset theme="resource-group-new">
|
||||||
|
<normaloff>.</normaloff>.</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add conference</string>
|
<string>Add conference</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user