From b41e5867c6aeb5d54bce4233a83087766fbd5f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Sun, 19 Mar 2023 11:36:12 -0400 Subject: [PATCH] SIP: possibility to set custom avatar/display name This allow users to be able to easily identify their contacts by changing the avatar/display name of a contact (for SIP and 1:1) https://git.jami.net/savoirfairelinux/jami-project/-/issues/757 Change-Id: I483a9116b78b08d43962abff982e73089bfec1d7 --- .../mainview/components/ChatViewHeader.qml | 3 +- .../components/ConversationExtrasPanel.qml | 4 +- .../mainview/components/SwarmDetailsPanel.qml | 9 ++- src/libclient/api/contactmodel.h | 2 + src/libclient/authority/storagehelper.cpp | 77 +++++++++++++------ src/libclient/authority/storagehelper.h | 9 ++- src/libclient/contactmodel.cpp | 23 +++++- src/libclient/conversationmodel.cpp | 11 +++ 8 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index 1de6afe7c..a7cadb83a 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -246,7 +246,8 @@ Rectangle { PushButton { id: detailsButton - visible: interactionButtonsVisibility && swarmDetailsVisibility + visible: interactionButtonsVisibility + && (swarmDetailsVisibility || LRCInstance.currentAccountType === Profile.Type.SIP) // TODO if SIP not a request source: JamiResources.swarm_details_panel_svg toolTipText: JamiStrings.details diff --git a/src/app/mainview/components/ConversationExtrasPanel.qml b/src/app/mainview/components/ConversationExtrasPanel.qml index f5db3f782..f11580246 100644 --- a/src/app/mainview/components/ConversationExtrasPanel.qml +++ b/src/app/mainview/components/ConversationExtrasPanel.qml @@ -34,7 +34,7 @@ StackLayout { function restoreState() { // Only applies to Jami accounts, and we musn't be in a call. - if (detailsShouldOpen && !inCallView && !CurrentConversation.isSip) { + if (detailsShouldOpen && !inCallView) { switchToPanel(ChatView.SwarmDetailsPanel, false) } else { closePanel() @@ -68,7 +68,7 @@ StackLayout { function closePanel() { // We need to close the panel, but not save it when appropriate. currentIndex = -1 - if (!inCallView && !CurrentConversation.isSip) + if (!inCallView) detailsShouldOpen = false } diff --git a/src/app/mainview/components/SwarmDetailsPanel.qml b/src/app/mainview/components/SwarmDetailsPanel.qml index e0ccfd065..15b52f31f 100644 --- a/src/app/mainview/components/SwarmDetailsPanel.qml +++ b/src/app/mainview/components/SwarmDetailsPanel.qml @@ -36,10 +36,10 @@ Rectangle { property int tabBarItemsLength: tabBar.contentChildren.length color: CurrentConversation.color - property var isAdmin: !CurrentConversation.isCoreDialog && - UtilsAdapter.getParticipantRole(CurrentAccount.id, + property var isAdmin: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) === Member.Role.ADMIN + || CurrentConversation.isCoreDialog ColumnLayout { id: swarmProfileDetails @@ -158,7 +158,7 @@ Rectangle { wrapMode: Text.NoWrap text: formattedDescription.elidedText - readOnly: !root.isAdmin + readOnly: !root.isAdmin || CurrentConversation.isCoreDialog visible: root.isAdmin || text.length > 0 placeholderText: JamiStrings.addADescription placeholderTextColor: { @@ -364,6 +364,7 @@ Rectangle { SwarmDetailsItem { Layout.fillWidth: true Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 + visible: CurrentAccount.type !== Profile.Type.SIP // TODO for SIP save in VCard RowLayout { anchors.fill: parent @@ -410,6 +411,7 @@ Rectangle { id: settingsSwarmItem Layout.fillWidth: true Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 + visible: !CurrentConversation.isCoreDialog RowLayout { anchors.fill: parent @@ -519,6 +521,7 @@ Rectangle { RowLayout { Layout.leftMargin: JamiTheme.preferredMarginSize Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 + visible: CurrentAccount.type !== Profile.Type.SIP Text { Layout.fillWidth: true diff --git a/src/libclient/api/contactmodel.h b/src/libclient/api/contactmodel.h index 46234849e..5820d36fb 100644 --- a/src/libclient/api/contactmodel.h +++ b/src/libclient/api/contactmodel.h @@ -87,6 +87,8 @@ public: const contact::Info getContact(const QString& contactUri) const; ContactInfoMap getSearchResults() const; + void updateContact(const QString& uri, const MapStringString& infos); + /** * Retrieve when a contact is added */ diff --git a/src/libclient/authority/storagehelper.cpp b/src/libclient/authority/storagehelper.cpp index 918b24425..d41baf2f8 100644 --- a/src/libclient/authority/storagehelper.cpp +++ b/src/libclient/authority/storagehelper.cpp @@ -65,14 +65,14 @@ getPath() } static QString -profileVcardPath(const QString& accountId, const QString& uri) +profileVcardPath(const QString& accountId, const QString& uri, bool ov = false) { auto accountLocalPath = getPath() + accountId + QDir::separator(); if (uri.isEmpty()) return accountLocalPath + "profile.vcf"; auto fileName = QString(uri.toUtf8().toBase64()); - return accountLocalPath + "profiles" + QDir::separator() + fileName + ".vcf"; + return accountLocalPath + "profiles" + QDir::separator() + fileName + (ov ? "_o.vcf" : ".vcf"); } static QString @@ -295,10 +295,10 @@ profileToVcard(const api::profile::Info& profileInfo, bool compressImage) } void -setProfile(const QString& accountId, const api::profile::Info& profileInfo, const bool isPeer) +setProfile(const QString& accountId, const api::profile::Info& profileInfo, bool isPeer, bool ov) { auto vcard = vcard::profileToVcard(profileInfo); - auto path = profileVcardPath(accountId, isPeer ? profileInfo.uri : ""); + auto path = profileVcardPath(accountId, isPeer ? profileInfo.uri : "", ov); QLockFile lf(path + ".lock"); QFile file(path); QFileInfo fileInfo(path); @@ -347,7 +347,8 @@ getPeerParticipantsForConversation(Database& db, const QString& conversationId) void createOrUpdateProfile(const QString& accountId, const api::profile::Info& profileInfo, - const bool isPeer) + bool isPeer, + bool ov) { if (isPeer) { auto contact = storage::buildContactFromProfile(accountId, @@ -357,19 +358,20 @@ createOrUpdateProfile(const QString& accountId, contact.profileInfo.alias = profileInfo.alias; if (!profileInfo.avatar.isEmpty()) contact.profileInfo.avatar = profileInfo.avatar; - vcard::setProfile(accountId, contact.profileInfo, isPeer); + vcard::setProfile(accountId, contact.profileInfo, isPeer, ov); return; } - vcard::setProfile(accountId, profileInfo, isPeer); + vcard::setProfile(accountId, profileInfo, isPeer, ov); } void removeProfile(const QString& accountId, const QString& peerUri) { auto path = profileVcardPath(accountId, peerUri); - if (!QFile::remove(path)) { + if (!QFile::remove(path)) qWarning() << "Couldn't remove vcard for" << peerUri << "at" << path; - } + auto opath = profileVcardPath(accountId, peerUri, true); + QFile::remove(opath); } QString @@ -388,21 +390,38 @@ getAccountAvatar(const QString& accountId) return photo; } +static QPair<QString, QString> +getOverridenInfos(const QString& accountId, const QString& peerUri) +{ + QString b64filePathOverride = profileVcardPath(accountId, peerUri, true); + QFile fileOverride(b64filePathOverride); + + QHash<QByteArray, QByteArray> overridenVCard; + QString overridenAlias, overridenAvatar; + if (fileOverride.open(QIODevice::ReadOnly)) { + overridenVCard = lrc::vCard::utils::toHashMap(fileOverride.readAll()); + overridenAlias = overridenVCard[vCard::Property::FORMATTED_NAME]; + for (const auto& key : overridenVCard.keys()) + if (key.contains("PHOTO")) + overridenAvatar = overridenVCard[key]; + } + return {overridenAlias, overridenAvatar}; +} + api::contact::Info buildContactFromProfile(const QString& accountId, - const QString& peer_uri, + const QString& peerUri, const api::profile::Type& type) { lrc::api::profile::Info profileInfo; - profileInfo.uri = peer_uri; + profileInfo.uri = peerUri; profileInfo.type = type; auto accountLocalPath = getPath() + accountId + "/"; - QString b64filePath; - b64filePath = profileVcardPath(accountId, peer_uri); + QString b64filePath = profileVcardPath(accountId, peerUri); QFile file(b64filePath); if (!file.open(QIODevice::ReadOnly)) { // try non-base64 path - QString filePath = accountLocalPath + "profiles/" + peer_uri + ".vcf"; + QString filePath = accountLocalPath + "profiles/" + peerUri + ".vcf"; file.setFileName(filePath); if (!file.open(QIODevice::ReadOnly)) { return {profileInfo, "", true, false}; @@ -416,30 +435,40 @@ buildContactFromProfile(const QString& accountId, return {profileInfo, "", true, false}; } } + + auto [overridenAlias, overridenAvatar] = getOverridenInfos(accountId, peerUri); + const auto vCard = lrc::vCard::utils::toHashMap(file.readAll()); const auto alias = vCard[vCard::Property::FORMATTED_NAME]; if (lrc::api::Lrc::cacheAvatars.load()) { - for (const auto& key : vCard.keys()) { - if (key.contains("PHOTO")) - profileInfo.avatar = vCard[key]; + if (overridenAvatar.isEmpty()) { + for (const auto& key : vCard.keys()) { + if (key.contains("PHOTO")) + profileInfo.avatar = vCard[key]; + } + } else { + profileInfo.avatar = overridenAvatar; } } - profileInfo.alias = alias; + profileInfo.alias = overridenAlias.isEmpty() ? alias : overridenAlias; return {profileInfo, "", type == api::profile::Type::JAMI, false}; } QString -avatar(const QString& accountId, const QString& contactId) +avatar(const QString& accountId, const QString& peerUri) { - if (contactId.isEmpty()) + if (peerUri.isEmpty()) return getAccountAvatar(accountId); - auto accountLocalPath = getPath() + accountId + "/"; + + auto [_overridenAlias, overridenAvatar] = getOverridenInfos(accountId, peerUri); + if (!overridenAvatar.isEmpty()) + return overridenAvatar; + QString b64filePath; - b64filePath = profileVcardPath(accountId, contactId); + b64filePath = profileVcardPath(accountId, peerUri); QFile file(b64filePath); - if (!file.open(QIODevice::ReadOnly)) { + if (!file.open(QIODevice::ReadOnly)) return {}; - } const auto vCard = lrc::vCard::utils::toHashMap(file.readAll()); for (const auto& key : vCard.keys()) { if (key.contains("PHOTO")) diff --git a/src/libclient/authority/storagehelper.h b/src/libclient/authority/storagehelper.h index 1104a8cbb..eaf66aa06 100644 --- a/src/libclient/authority/storagehelper.h +++ b/src/libclient/authority/storagehelper.h @@ -89,10 +89,13 @@ QString profileToVcard(const api::profile::Info& profileInfo, bool compressImage * @param accountId * @param profileInfo * @param isPeer + * @param ov If from daemon override must be false, if the client want to override the vcard + * should be true */ void setProfile(const QString& accountId, const api::profile::Info& profileInfo, - const bool isPeer = false); + bool isPeer = false, + bool ov = false); } // namespace vcard @@ -121,10 +124,12 @@ VectorString getPeerParticipantsForConversation(Database& db, const QString& con * @param accountId * @param profileInfo the contact info containing peer profile information * @param isPeer indicates that a the profileInfo is that of a peer + * @param ov if the client is storing a new vcard */ void createOrUpdateProfile(const QString& accountId, const api::profile::Info& profileInfo, - const bool isPeer = false); + bool isPeer = false, + bool ov = false); /** * Remove a profile vCard diff --git a/src/libclient/contactmodel.cpp b/src/libclient/contactmodel.cpp index 84d6b2af1..d77270b86 100644 --- a/src/libclient/contactmodel.cpp +++ b/src/libclient/contactmodel.cpp @@ -376,6 +376,27 @@ ContactModel::getContact(const QString& contactUri) const throw std::out_of_range("Contact out of range"); } +void +ContactModel::updateContact(const QString& uri, const MapStringString& infos) +{ + std::unique_lock<std::mutex> lk(pimpl_->contactsMtx_); + auto ci = pimpl_->contacts.find(uri); + if (ci != pimpl_->contacts.end()) { + if (infos.contains("avatar")) { + ci->profileInfo.avatar = storage::vcard::compressedAvatar(infos["avatar"]); + } else if (!lrc::api::Lrc::cacheAvatars.load()) { + // Else it will be reseted + ci->profileInfo.avatar = storage::avatar(owner.id, uri); + } + if (infos.contains("title")) + ci->profileInfo.alias = infos["title"]; + storage::createOrUpdateProfile(owner.id, ci->profileInfo, true, true); + lk.unlock(); + Q_EMIT profileUpdated(uri); + Q_EMIT modelUpdated(uri); + } +} + const QList<QString>& ContactModel::getBannedContacts() const { @@ -546,7 +567,7 @@ ContactModel::avatar(const QString& uri) const return contact.profileInfo.avatar; } } - // Else search in storage + // Else search in storage (because not cached!) return storage::avatar(owner.id, uri); } diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 090f77bfd..c008ca5d7 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -1047,6 +1047,17 @@ void ConversationModel::updateConversationInfos(const QString& conversationId, const MapStringString infos) { + auto conversationOpt = getConversationForUid(conversationId); + if (!conversationOpt.has_value()) + return; + auto& conversation = conversationOpt->get(); + if (conversation.isCoreDialog()) { + // If 1:1, we override a profile (as the peer will send their new profiles) + auto peer = pimpl_->peersForConversation(conversation); + if (!peer.isEmpty()) + owner.contactModel->updateContact(peer.at(0), infos); + return; + } MapStringString newInfos = infos; // Compress avatar as it will be sent in the conversation's request over the DHT if (infos.contains("avatar")) -- GitLab