1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-14 04:25:22 +02:00

migration: use image provider to show avatar image

1. Use avatarimageprovider
2. Remove redundant base64 code

Change-Id: I2a2517890e95b4a9f9a363fbea2251d6d5dd1c8f
This commit is contained in:
Ming Rui Zhang 2020-10-19 14:51:31 -04:00
parent b4b56aec4b
commit 173cf2be50
50 changed files with 587 additions and 562 deletions

View file

@ -42,7 +42,6 @@ set(COMMON_SOURCES
src/main.cpp
src/smartlistmodel.cpp
src/utils.cpp
src/pixbufmanipulator.cpp
src/rendermanager.cpp
src/connectivitymonitor.cpp
src/mainapplication.cpp
@ -85,7 +84,6 @@ set(COMMON_HEADERS
src/globalsystemtray.h
src/appsettingsmanager.h
src/webchathelpers.h
src/pixbufmanipulator.h
src/rendermanager.h
src/connectivitymonitor.h
src/jamiavatartheme.h

View file

@ -111,6 +111,7 @@ unix {
# Input
HEADERS += \
src/avatarimageprovider.h \
src/networkmanager.h \
src/smartlistmodel.h \
src/updatemanager.h \
@ -123,7 +124,6 @@ HEADERS += \
src/globalsystemtray.h \
src/appsettingsmanager.h \
src/webchathelpers.h \
src/pixbufmanipulator.h \
src/rendermanager.h \
src/connectivitymonitor.h \
src/jamiavatartheme.h \
@ -168,7 +168,6 @@ SOURCES += \
src/main.cpp \
src/smartlistmodel.cpp \
src/utils.cpp \
src/pixbufmanipulator.cpp \
src/rendermanager.cpp \
src/connectivitymonitor.cpp \
src/mainapplication.cpp \

View file

@ -97,7 +97,6 @@
<file>src/mainview/components/ProjectCreditsScrollView.qml</file>
<file>src/mainview/components/AccountComboBoxPopup.qml</file>
<file>src/mainview/components/ConversationSmartListViewItemDelegate.qml</file>
<file>src/mainview/components/ConversationSmartListUserImage.qml</file>
<file>src/mainview/components/SidePanelTabBar.qml</file>
<file>src/mainview/components/WelcomePageQrDialog.qml</file>
<file>src/commoncomponents/GeneralMenuItem.qml</file>
@ -137,5 +136,6 @@
<file>src/commoncomponents/SimpleMessageDialog.qml</file>
<file>src/commoncomponents/ResponsiveImage.qml</file>
<file>src/commoncomponents/PresenceIndicator.qml</file>
<file>src/commoncomponents/AvatarImage.qml</file>
</qresource>
</RCC>

View file

@ -373,13 +373,15 @@ AccountAdapter::connectAccount(const QString& accountId)
&lrc::api::NewAccountModel::profileUpdated,
[this](const QString& accountId) {
if (LRCInstance::getCurrAccId() == accountId)
emit accountStatusChanged();
emit accountStatusChanged(accountId);
});
accountStatusChangedConnection_
= QObject::connect(accInfo.accountModel,
&lrc::api::NewAccountModel::accountStatusChanged,
[this] { emit accountStatusChanged(); });
[this](const QString& accountId) {
emit accountStatusChanged(accountId);
});
contactAddedConnection_
= QObject::connect(accInfo.contactModel.get(),

View file

@ -110,7 +110,7 @@ signals:
/*
* Trigger other components to reconnect account related signals.
*/
void accountStatusChanged();
void accountStatusChanged(QString accountId = {});
void updateConversationForAddedContact();
/*
* send report failure to QML to make it show the right UI state .

View file

@ -21,10 +21,7 @@
#include <QDateTime>
#include "globalinstances.h"
#include "lrcinstance.h"
#include "pixbufmanipulator.h"
#include "utils.h"
AccountListModel::AccountListModel(QObject* parent)
@ -68,6 +65,8 @@ AccountListModel::data(const QModelIndex& index, int role) const
auto& accountInfo = LRCInstance::accountModel().getAccountInfo(accountList.at(index.row()));
// Since we are using image provider right now, image url representation should be unique to
// be able to use the image cache, account avatar will only be updated once PictureUid changed
switch (role) {
case Role::Alias:
return QVariant(Utils::bestNameForAccount(accountInfo));
@ -77,11 +76,10 @@ AccountListModel::data(const QModelIndex& index, int role) const
return QVariant(static_cast<int>(accountInfo.profileInfo.type));
case Role::Status:
return QVariant(static_cast<int>(accountInfo.status));
case Role::Picture:
return QString::fromLatin1(
Utils::QImageToByteArray(Utils::accountPhoto(accountInfo)).toBase64().data());
case Role::ID:
return QVariant(accountInfo.id);
case Role::PictureUid:
return avatarUidMap_[accountInfo.id];
}
return QVariant();
}
@ -92,10 +90,10 @@ AccountListModel::roleNames() const
QHash<int, QByteArray> roles;
roles[Alias] = "Alias";
roles[Username] = "Username";
roles[Picture] = "Picture";
roles[Type] = "Type";
roles[Status] = "Status";
roles[ID] = "ID";
roles[PictureUid] = "PictureUid";
return roles;
}
@ -134,5 +132,28 @@ void
AccountListModel::reset()
{
beginResetModel();
fillAvatarUidMap(LRCInstance::accountModel().getAccountList());
endResetModel();
}
void
AccountListModel::updateAvatarUid(const QString& accountId)
{
avatarUidMap_[accountId] = Utils::generateUid();
}
void
AccountListModel::fillAvatarUidMap(const QStringList& accountList)
{
if (accountList.size() == 0) {
avatarUidMap_.clear();
return;
}
if (avatarUidMap_.isEmpty() || accountList.size() != avatarUidMap_.size()) {
for (int i = 0; i < accountList.size(); ++i) {
if (!avatarUidMap_.contains(accountList.at(i)))
avatarUidMap_.insert(accountList.at(i), Utils::generateUid());
}
}
}

View file

@ -30,7 +30,7 @@ class AccountListModel : public QAbstractListModel
Q_OBJECT
public:
enum Role { Alias = Qt::UserRole + 1, Username, Picture, Type, Status, ID };
enum Role { Alias = Qt::UserRole + 1, Username, Type, Status, ID, PictureUid };
Q_ENUM(Role)
explicit AccountListModel(QObject* parent = 0);
@ -55,4 +55,17 @@ public:
* This function is to reset the model when there's new account added.
*/
Q_INVOKABLE void reset();
/*
* This function is to update avatar uuid when there's an avatar changed.
*/
Q_INVOKABLE void updateAvatarUid(const QString& accountId);
private:
/*
* Give a uuid for each account avatar and it will serve PictureUid role
*/
void fillAvatarUidMap(const QStringList& accountList);
QMap<QString, QString> avatarUidMap_;
};

View file

@ -92,9 +92,6 @@ AccountsToMigrateListModel::data(const QModelIndex& index, int role) const
return QVariant(avatarInfo.confProperties.username);
case Role::Alias:
return QVariant(LRCInstance::accountModel().getAccountInfo(accountId).profileInfo.alias);
case Role::Picture:
return QString::fromLatin1(
Utils::QImageToByteArray(Utils::accountPhoto(avatarInfo)).toBase64().data());
}
return QVariant();
}
@ -108,7 +105,6 @@ AccountsToMigrateListModel::roleNames() const
roles[ManagerUri] = "ManagerUri";
roles[Username] = "Username";
roles[Alias] = "Alias";
roles[Picture] = "Picture";
return roles;
}

View file

@ -31,14 +31,7 @@ class AccountsToMigrateListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Role {
Account_ID = Qt::UserRole + 1,
ManagerUsername,
ManagerUri,
Username,
Alias,
Picture
};
enum Role { Account_ID = Qt::UserRole + 1, ManagerUsername, ManagerUri, Username, Alias };
Q_ENUM(Role)
explicit AccountsToMigrateListModel(QObject* parent = 0);

71
src/avatarimageprovider.h Normal file
View file

@ -0,0 +1,71 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "utils.h"
#include <QImage>
#include <QQuickImageProvider>
class AvatarImageProvider : public QObject, public QQuickImageProvider
{
public:
AvatarImageProvider()
: QQuickImageProvider(QQuickImageProvider::Image,
QQmlImageProviderBase::ForceAsynchronousImageLoading)
{}
/*
* Request function
* id could be
* 1. account_ + account id
* 2. file_ + file path
* 3. contact_+ contact uri
* 4. conversation_+ conversation uid
*/
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override
{
Q_UNUSED(size)
auto idInfo = id.split("_");
// Id type -> account_
auto idType = idInfo[1];
// Id content -> every after account_
auto idContent = id.mid(id.indexOf(idType) + idType.length() + 1);
if (idContent.isEmpty())
return QImage();
if (idType == "account") {
return Utils::accountPhoto(LRCInstance::accountModel().getAccountInfo(idContent),
requestedSize);
} else if (idType == "conversation") {
auto* convModel = LRCInstance::getCurrentAccountInfo().conversationModel.get();
const auto& conv = convModel->getConversationForUID(idContent);
return Utils::contactPhoto(conv.participants[0], requestedSize);
} else if (idType == "contact") {
return Utils::contactPhoto(idContent, requestedSize);
} else {
auto image = Utils::cropImage(QImage(idContent));
return image.scaled(requestedSize,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
}
}
};

View file

@ -61,12 +61,6 @@ BannedListModel::data(const QModelIndex& index, int role) const
return QVariant(contactInfo.registeredName);
case Role::ContactID:
return QVariant(contactInfo.profileInfo.uri);
case Role::ContactPicture:
QImage avatarImage = Utils::fallbackAvatar(contactInfo.profileInfo.uri,
contactInfo.registeredName,
QSize(48, 48));
return QString::fromLatin1(Utils::QImageToByteArray(avatarImage).toBase64().data());
}
return QVariant();
}
@ -77,7 +71,6 @@ BannedListModel::roleNames() const
QHash<int, QByteArray> roles;
roles[ContactName] = "ContactName";
roles[ContactID] = "ContactID";
roles[ContactPicture] = "ContactPicture";
return roles;
}

View file

@ -27,7 +27,7 @@ class BannedListModel : public QAbstractListModel
BannedListModel(const BannedListModel& cpy);
public:
enum Role { ContactName = Qt::UserRole + 1, ContactID, ContactPicture };
enum Role { ContactName = Qt::UserRole + 1, ContactID };
Q_ENUM(Role)
explicit BannedListModel(QObject* parent = nullptr);

View file

@ -42,7 +42,6 @@ Window {
property bool nonOperationClosing: true
property bool successState : true
property string imgBase64: ""
signal accountMigrationFinished
@ -88,8 +87,7 @@ Window {
accountID = accountsToMigrateListModel.data(accountsToMigrateListModel.index(
0, 0), AccountsToMigrateListModel.Account_ID)
imgBase64 = accountsToMigrateListModel.data(accountsToMigrateListModel.index(
0, 0), AccountsToMigrateListModel.Picture)
avatarImg.updateImage(accountID)
connectionMigrationEnded.enabled = false
migrationPushButton.enabled = false
@ -284,17 +282,13 @@ Window {
anchors.fill: parent
color: "transparent"
Image {
AvatarImage {
id: avatarImg
anchors.fill: parent
source: {
if (imgBase64.length === 0) {
return ""
} else {
return "data:image/png;base64," + imgBase64
}
}
showPresenceIndicator: false
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {

View file

@ -0,0 +1,183 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Window 2.14
import net.jami.Models 1.0
Item {
id: root
// FromUrl here is for grabToImage image url
enum Mode {
FromAccount = 0,
FromFile,
FromContactUri,
FromConvUid,
FromUrl,
Default
}
property alias fillMode: rootImage.fillMode
property alias sourceSize: rootImage.sourceSize
property int mode: AvatarImage.Mode.FromAccount
property string imageProviderIdPrefix: {
switch(mode) {
case AvatarImage.Mode.FromAccount:
return "account_"
case AvatarImage.Mode.FromFile:
return "file_"
case AvatarImage.Mode.FromContactUri:
return "contact_"
case AvatarImage.Mode.FromConvUid:
return "conversation_"
default:
return ""
}
}
// Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx
property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + "_" +
imageProviderIdPrefix
property string imageId: ""
property string defaultImgUrl: "qrc:/images/default_avatar_overlay.svg"
property string forceUpdateUrl: Date.now()
property alias presenceStatus: presenceIndicator.status
property bool showPresenceIndicator: true
property int unreadMessagesCount: 0
signal imageIsReady
function updateImage(updatedId, oneTimeForceUpdateUrl) {
imageId = updatedId
if (oneTimeForceUpdateUrl === undefined)
forceUpdateUrl = Date.now()
else
forceUpdateUrl = oneTimeForceUpdateUrl
if (mode === AvatarImage.Mode.FromUrl)
rootImage.source = imageId
else if (imageId)
rootImage.source = imageProviderUrl + imageId
}
onModeChanged: {
if (mode === AvatarImage.Mode.Default)
rootImage.source = defaultImgUrl
}
Image {
id: rootImage
anchors.fill: root
smooth: false
antialiasing: true
sourceSize.width: Math.max(24, width)
sourceSize.height: Math.max(24, height)
fillMode: Image.PreserveAspectFit
onStatusChanged: {
if (status === Image.Ready) {
rootImageOverlay.state = ""
rootImageOverlay.state = "rootImageLoading"
}
}
Component.onCompleted: {
if (imageId)
return source = imageProviderUrl + imageId
return source = ""
}
Image {
id: rootImageOverlay
anchors.fill: rootImage
smooth: false
antialiasing: true
sourceSize.width: Math.max(24, width)
sourceSize.height: Math.max(24, height)
fillMode: Image.PreserveAspectFit
onOpacityChanged: {
if (opacity === 0)
source = rootImage.source
}
onStatusChanged: {
if (status === Image.Ready && opacity === 0) {
opacity = 1
root.imageIsReady()
}
}
states: State {
name: "rootImageLoading"
PropertyChanges { target: rootImageOverlay; opacity: 0}
}
transitions: Transition {
NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad; duration: 400}
}
}
}
PresenceIndicator {
id: presenceIndicator
anchors.right: root.right
anchors.bottom: root.bottom
size: root.width * 0.3
visible: showPresenceIndicator
}
Rectangle {
id: unreadMessageCountRect
anchors.right: root.right
anchors.top: root.top
width: root.width * 0.3
height: root.width * 0.3
visible: unreadMessagesCount > 0
Text {
id: unreadMessageCounttext
anchors.centerIn: unreadMessageCountRect
text: unreadMessagesCount > 9 ? "…" : unreadMessagesCount
color: "white"
font.pointSize: JamiTheme.textFontSize - 2
}
radius: 30
color: JamiTheme.notificationRed
}
}

View file

@ -10,9 +10,10 @@ import net.jami.Adapters 1.0
ColumnLayout {
property bool takePhotoState: false
property bool hasAvatar: false
property bool isDefaultIcon: false
property string imgBase64: ""
// saveToConfig is to specify whether the image should be saved to account config
property bool saveToConfig: false
property string fileName: ""
property var boothImg: ""
property int boothWidth: 224
@ -20,9 +21,6 @@ ColumnLayout {
buttonsRowLayout.height +
JamiTheme.preferredMarginSize / 2
signal imageAcquired
signal imageCleared
function startBooth(force = false){
hasAvatar = false
AccountAdapter.startPreviewing(force)
@ -39,12 +37,15 @@ ColumnLayout {
takePhotoState = false
}
function setAvatarPixmap(avatarPixmapBase64, defaultValue = false){
imgBase64 = avatarPixmapBase64
stopBooth()
if(defaultValue){
isDefaultIcon = defaultValue
}
function setAvatarImage(mode = AvatarImage.Mode.FromAccount,
imageId = AccountAdapter.currentAccountId){
if (mode === AvatarImage.Mode.Default)
boothImg = ""
avatarImg.mode = mode
if (imageId)
avatarImg.updateImage(imageId)
}
onVisibleChanged: {
@ -68,14 +69,13 @@ ColumnLayout {
onAccepted: {
fileName = file
if (fileName.length === 0) {
imageCleared()
SettingsAdapter.clearCurrentAvatar()
setAvatarImage()
return
}
imgBase64 = UtilsAdapter.getCroppedImageBase64FromFile(
UtilsAdapter.getAbsPath(fileName),
boothWidth)
imageAcquired()
stopBooth()
setAvatarImage(AvatarImage.Mode.FromFile,
UtilsAdapter.getAbsPath(fileName))
}
}
@ -96,29 +96,40 @@ ColumnLayout {
color: "grey"
radius: height / 2
Image {
AvatarImage {
id: avatarImg
anchors.fill: parent
source: {
if(imgBase64.length === 0){
return "qrc:/images/default_avatar_overlay.svg"
} else {
return "data:image/png;base64," + imgBase64
}
}
imageId: AccountAdapter.currentAccountId
showPresenceIndicator: false
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: avatarImg.width
height: avatarImg.height
radius: {
var size = ((avatarImg.width <= avatarImg.height)? avatarImg.width:avatarImg.height)
return size /2
var size = ((avatarImg.width <= avatarImg.height) ?
avatarImg.width:avatarImg.height)
return size / 2
}
}
}
onImageIsReady: {
// Once image is loaded (updated), save to boothImg
avatarImg.grabToImage(function(result) {
if (mode !== AvatarImage.Mode.Default)
boothImg = result.image
if (saveToConfig)
SettingsAdapter.setCurrAccAvatar(result.image)
})
}
}
}
}
@ -126,9 +137,7 @@ ColumnLayout {
PhotoboothPreviewRender {
id:previewWidget
onHideBooth:{
stopBooth()
}
onHideBooth: stopBooth()
visible: takePhotoState
focus: visible
@ -143,8 +152,9 @@ ColumnLayout {
width: previewWidget.width
height: previewWidget.height
radius: {
var size = ((previewWidget.width <= previewWidget.height)? previewWidget.width:previewWidget.height)
return size /2
var size = ((previewWidget.width <= previewWidget.height) ?
previewWidget.width:previewWidget.height)
return size / 2
}
}
}
@ -191,7 +201,6 @@ ColumnLayout {
radius: height / 6
source: {
if(takePhotoState) {
toolTipText = qsTr("Take photo")
return cameraAltIconUrl
@ -205,9 +214,9 @@ ColumnLayout {
return addPhotoIconUrl
}
}
onClicked: {
if(!takePhotoState){
imageCleared()
startBooth()
return
} else {
@ -215,11 +224,13 @@ ColumnLayout {
flashOverlay.visible = true
flashAnimation.restart()
// run concurrent function call to take photo
imgBase64 = previewWidget.takeCroppedPhotoToBase64(boothWidth)
hasAvatar = true
imageAcquired()
stopBooth()
previewWidget.grabToImage(function(result) {
setAvatarImage(AvatarImage.Mode.FromUrl, result.url)
hasAvatar = true
stopBooth()
})
}
}
}

View file

@ -209,6 +209,14 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
emit modelSorted(QVariant::fromValue(conversation.uid));
});
contactProfileUpdatedConnection_
= QObject::connect(LRCInstance::getCurrentAccountInfo().contactModel.get(),
&lrc::api::ContactModel::profileUpdated,
[this](const QString& contactUri) {
conversationSmartListModel_->updateContactAvatarUid(contactUri);
emit updateListViewRequested();
});
modelUpdatedConnection_ = QObject::connect(currentConversationModel,
&lrc::api::ConversationModel::conversationUpdated,
[this](const QString& convUid) {
@ -262,7 +270,7 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
&lrc::api::ConversationModel::searchStatusChanged,
[this](const QString& status) { emit showSearchStatus(status); });
// This connection is ideal when separated search results list.
// This connection is ideal when separated search results list.
// This signal is guaranteed to fire just after filterChanged during a search if results are
// changed, and once before filterChanged when calling setFilter.
// NOTE: Currently, when searching, the entire conversation list will be copied 2-3 times each
@ -295,6 +303,7 @@ ConversationsAdapter::disconnectConversationModel()
QObject::disconnect(interactionRemovedConnection_);
QObject::disconnect(searchStatusChangedConnection_);
QObject::disconnect(searchResultUpdatedConnection_);
QObject::disconnect(contactProfileUpdatedConnection_);
}
void

View file

@ -82,6 +82,7 @@ private:
QMetaObject::Connection newConversationConnection_;
QMetaObject::Connection conversationRemovedConnection_;
QMetaObject::Connection conversationClearedConnection;
QMetaObject::Connection contactProfileUpdatedConnection_;
QMetaObject::Connection selectedCallChanged_;
QMetaObject::Connection smartlistSelectionConnection_;
QMetaObject::Connection interactionRemovedConnection_;

View file

@ -336,15 +336,6 @@ public:
return -1;
}
static const QPixmap getCurrAccPixmap()
{
return instance()
.accountListModel_
.data(instance().accountListModel_.index(getCurrentAccountIndex()),
AccountListModel::Role::Picture)
.value<QPixmap>();
}
static void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID)
{
QByteArray ba;

View file

@ -26,10 +26,8 @@
#include "globalsystemtray.h"
#include "qmlregister.h"
#include "qrimageprovider.h"
#include "pixbufmanipulator.h"
#include "tintedbuttonimageprovider.h"
#include "globalinstances.h"
#include "avatarimageprovider.h"
#include <QAction>
#include <QCommandLineParser>
@ -148,7 +146,6 @@ MainApplication::init()
gnutls_global_init();
#endif
GlobalInstances::setPixmapManipulator(std::make_unique<PixbufManipulator>());
initLrc(results[opts::UPDATEURL].toString(), connectivityMonitor_);
#ifdef Q_OS_WIN
@ -322,6 +319,7 @@ MainApplication::initQmlEngine()
engine_->addImageProvider(QLatin1String("qrImage"), new QrImageProvider());
engine_->addImageProvider(QLatin1String("tintedPixmap"), new TintedButtonImageProvider());
engine_->addImageProvider(QLatin1String("avatarImage"), new AvatarImageProvider());
engine_->load(QUrl(QStringLiteral("qrc:/src/MainApplicationWindow.qml")));
}

View file

@ -308,8 +308,8 @@ Window {
mainViewWindowSidePanel.forceReselectConversationSmartListCurrentIndex()
}
function onAccountStatusChanged() {
accountComboBox.resetAccountListModel()
function onAccountStatusChanged(accountId) {
accountComboBox.resetAccountListModel(accountId)
}
}

View file

@ -31,7 +31,8 @@ ComboBox {
signal settingBtnClicked
// Reset accountListModel.
function resetAccountListModel() {
function resetAccountListModel(accountId) {
accountListModel.updateAvatarUid(accountId)
accountListModel.reset()
}
@ -39,9 +40,11 @@ ComboBox {
target: accountListModel
function onModelReset() {
userImageRoot.source = "data:image/png;base64," + accountListModel.data(
accountListModel.index(0, 0), AccountListModel.Picture)
currentAccountPresenceIndicator.status =
userImageRoot.updateImage(
AccountAdapter.currentAccountId,
accountListModel.data(
accountListModel.index(0, 0), AccountListModel.PictureUid))
userImageRoot.presenceStatus =
accountListModel.data(accountListModel.index(0, 0), AccountListModel.Status)
textMetricsUserAliasRoot.text = accountListModel.data(accountListModel.index(0,0),
AccountListModel.Alias)
@ -50,34 +53,20 @@ ComboBox {
}
}
Image {
AvatarImage {
id: userImageRoot
anchors.left: root.left
anchors.leftMargin: 16
anchors.verticalCenter: root.verticalCenter
width: 30
height: 30
width: 40
height: 40
fillMode: Image.PreserveAspectFit
imageId: AccountAdapter.currentAccountId
// Base 64 format
source: "data:image/png;base64," + accountListModel.data(
accountListModel.index(0, 0), AccountListModel.Picture)
mipmap: true
PresenceIndicator {
id: currentAccountPresenceIndicator
anchors.right: userImageRoot.right
anchors.rightMargin: -2
anchors.bottom: userImageRoot.bottom
anchors.bottomMargin: -2
status: accountListModel.data(accountListModel.index(0, 0),
AccountListModel.Status)
}
presenceStatus: accountListModel.data(accountListModel.index(0, 0),
AccountListModel.Status)
}
Text {
@ -251,8 +240,6 @@ ComboBox {
}
}
indicator: null
// Overwrite the combo box pop up to add footer (for add accounts).

View file

@ -45,42 +45,29 @@ Popup {
contentItem: ListView {
id: comboBoxPopupListView
// In list view, index is an interger.
clip: true
model: accountListModel
implicitHeight: contentHeight
delegate: ItemDelegate {
Image {
AvatarImage {
id: userImage
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: 30
height: 30
width: 40
height: 40
fillMode: Image.PreserveAspectFit
mipmap: true
presenceStatus: Status
// Role::Picture
source: {
var data = accountListModel.data(accountListModel.index(index, 0),
AccountListModel.Picture)
if (data === undefined) {
return ""
}
return "data:image/png;base64," + data
}
PresenceIndicator {
anchors.right: userImage.right
anchors.rightMargin: -2
anchors.bottom: userImage.bottom
anchors.bottomMargin: -2
status: Status
Component.onCompleted: {
return updateImage(
accountListModel.data(
accountListModel.index(index, 0), AccountListModel.ID),
accountListModel.data(
accountListModel.index(index, 0), AccountListModel.PictureUid))
}
}

View file

@ -28,7 +28,6 @@ import "../../commoncomponents"
Rectangle {
id: audioCallPageRect
property string contactImgSource: ""
property string bestName: "Best Name"
property string bestId: "Best Id"
@ -37,8 +36,7 @@ Rectangle {
signal showFullScreenReqested
function updateUI(accountId, convUid) {
contactImgSource = "data:image/png;base64," + UtilsAdapter.getContactImageString(
accountId, convUid)
contactImage.updateImage(convUid)
bestName = UtilsAdapter.getBestName(accountId, convUid)
var id = UtilsAdapter.getBestId(accountId, convUid)
@ -162,7 +160,7 @@ Rectangle {
ColumnLayout {
id: audioCallPageRectColumnLayout
Image {
AvatarImage {
id: contactImage
Layout.alignment: Qt.AlignCenter
@ -170,9 +168,8 @@ Rectangle {
Layout.preferredWidth: 100
Layout.preferredHeight: 100
fillMode: Image.PreserveAspectFit
source: contactImgSource
asynchronous: true
mode: AvatarImage.Mode.FromConvUid
showPresenceIndicator: false
}
Text {

View file

@ -125,8 +125,6 @@ Popup {
}
onAboutToShow: {
// Reset the model on each show.
contactPickerListView.model = ContactAdapter.getContactSelectableModel(
type)

View file

@ -26,7 +26,7 @@ import "../../commoncomponents"
ItemDelegate {
id: contactPickerItemDelegate
Image {
AvatarImage {
id: contactPickerContactImage
anchors.left: parent.left
@ -36,9 +36,8 @@ ItemDelegate {
width: 40
height: 40
fillMode: Image.PreserveAspectFit
source: "data:image/png;base64," + Picture
mipmap: true
mode: AvatarImage.Mode.FromContactUri
imageId: URI
}
Rectangle {

View file

@ -1,68 +0,0 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0
import "../../commoncomponents"
Image {
id: userImage
width: 40
height: 40
fillMode: Image.PreserveAspectFit
source: "data:image/png;base64," + Picture
mipmap: true
PresenceIndicator {
anchors.right: userImage.right
anchors.bottom: userImage.bottom
visible: Presence === undefined ? false : Presence
}
Rectangle {
id: unreadMessageCountRect
anchors.right: userImage.right
anchors.rightMargin: -2
anchors.top: userImage.top
anchors.topMargin: -2
width: 14
height: 14
visible: UnreadMessagesCount > 0
Text {
id: unreadMessageCounttext
anchors.centerIn: unreadMessageCountRect
text: UnreadMessagesCount > 9 ? "···" : UnreadMessagesCount
color: "white"
font.pointSize: JamiTheme.textFontSize
}
radius: 30
color: JamiTheme.notificationRed
}
}

View file

@ -89,6 +89,8 @@ ListView {
delegate: ConversationSmartListViewItemDelegate {
id: smartListItemDelegate
onUpdateContactAvatarUidRequested: root.model.updateContactAvatarUid(uid)
}
ScrollIndicator.vertical: ScrollIndicator {}

View file

@ -30,6 +30,8 @@ ItemDelegate {
property int lastInteractionPreferredWidth: 80
signal updateContactAvatarUidRequested(string uid)
function convUid() {
return UID
}
@ -76,14 +78,29 @@ ItemDelegate {
}
}
ConversationSmartListUserImage {
AvatarImage {
id: conversationSmartListUserImage
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
}
width: 40
height: 40
mode: AvatarImage.Mode.FromContactUri
showPresenceIndicator: Presence === undefined ? false : Presence
unreadMessagesCount: UnreadMessagesCount
Component.onCompleted: {
var contactUid = URI
if (ContactType === Profile.Type.TEMPORARY)
updateContactAvatarUidRequested(contactUid)
updateImage(contactUid, PictureUid)
}
}
RowLayout {
id: rowUsernameAndLastInteractionDate
@ -202,7 +219,7 @@ ItemDelegate {
userProfile.aliasText = DisplayName
userProfile.registeredNameText = DisplayID
userProfile.idText = URI
userProfile.contactPicBase64 = Picture
userProfile.contactImageUid = UID
smartListContextMenu.openMenu()
} else if (mouse.button === Qt.LeftButton) {
conversationSmartListView.currentIndex = -1

View file

@ -39,6 +39,7 @@ Rectangle {
participantName.text = name
}
// TODO: try to use AvatarImage as well
function setAvatar(avatar) {
if (avatar === "") {
opacity = 0

View file

@ -30,13 +30,11 @@ Rectangle {
id: userInfoCallRect
property int buttonPreferredSize: 48
property string contactImgSource: ""
property string bestName: "Best Name"
property string bestId: "Best Id"
function updateUI(accountId, convUid) {
contactImgSource = "data:image/png;base64," + UtilsAdapter.getContactImageString(
accountId, convUid)
contactImg.updateImage(convUid)
bestName = UtilsAdapter.getBestName(accountId, convUid)
var id = UtilsAdapter.getBestId(accountId, convUid)
bestId = (bestName !== id) ? id : ""
@ -74,7 +72,7 @@ Rectangle {
onClicked: mainViewWindow.showWelcomeView()
}
Image {
AvatarImage {
id: contactImg
Layout.alignment: Qt.AlignCenter
@ -83,9 +81,8 @@ Rectangle {
Layout.preferredWidth: 100
Layout.preferredHeight: 100
fillMode: Image.PreserveAspectFit
source: contactImgSource
asynchronous: true
mode: AvatarImage.Mode.FromConvUid
showPresenceIndicator: false
}
Rectangle {

View file

@ -28,7 +28,7 @@ BaseDialog {
id: root
property string responsibleConvUid: ""
property string contactPicBase64: ""
property string contactImageUid: ""
property string aliasText: ""
property string registeredNameText: ""
property string idText: ""
@ -53,17 +53,17 @@ BaseDialog {
rowSpacing: 16
columnSpacing: 24
Image {
AvatarImage {
id: contactImage
Layout.alignment: Qt.AlignRight
Layout.preferredWidth: 130
Layout.preferredWidth: preferredImgSize
sourceSize.width: preferredImgSize
sourceSize.height: preferredImgSize
fillMode: Image.PreserveAspectFit
mipmap: true
mode: AvatarImage.Mode.FromConvUid
showPresenceIndicator: false
}
// Visible when user alias is not empty or equals to id.
@ -196,8 +196,5 @@ BaseDialog {
contactQrImage.source = "image://qrImage/contact_" + responsibleConvUid
}
onContactPicBase64Changed: {
if (contactPicBase64 !== "")
contactImage.source = "data:image/png;base64," + contactPicBase64
}
onContactImageUidChanged: contactImage.updateImage(contactImageUid)
}

View file

@ -450,14 +450,14 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
auto& contact = accInfo->contactModel->getContact(contactUri);
auto bestName = Utils::bestNameForConversation(convInfo, *convModel);
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
|| contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
|| contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
bestName,
contactUri);
if (!contact.profileInfo.avatar.isEmpty()) {
setSenderImage(contactUri, contact.profileInfo.avatar);
} else {
auto avatar = Utils::conversationPhoto(convInfo.uid, *accInfo, true);
auto avatar = Utils::contactPhoto(convInfo.participants[0]);
QByteArray ba;
QBuffer bu(&ba);
avatar.save(&bu, "PNG");

View file

@ -1,127 +0,0 @@
/*
* Copyright (C) 2015-2020 by Savoir-faire Linux
* 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>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "pixbufmanipulator.h"
#include <QBuffer>
#include <QByteArray>
#include <QIODevice>
#include <QImage>
#include <QMetaType>
#include <QPainter>
#include <QSize>
#include "globalinstances.h"
#include <api/account.h>
#include <api/contact.h>
#include <api/contactmodel.h>
#include <api/conversation.h>
#include "utils.h"
#undef interface
QVariant
PixbufManipulator::personPhoto(const QByteArray& data, const QString& type)
{
QImage avatar;
const bool ret = avatar.loadFromData(QByteArray::fromBase64(data), type.toLatin1());
if (!ret) {
qDebug() << "vCard image loading failed";
return QVariant();
}
return QPixmap::fromImage(Utils::getCirclePhoto(avatar, avatar.size().width()));
}
QVariant
PixbufManipulator::numberCategoryIcon(const QVariant& p,
const QSize& size,
bool displayPresence,
bool isPresent)
{
Q_UNUSED(p)
Q_UNUSED(size)
Q_UNUSED(displayPresence)
Q_UNUSED(isPresent)
return QVariant();
}
QByteArray
PixbufManipulator::toByteArray(const QVariant& pxm)
{
auto image = pxm.value<QImage>();
QByteArray ba = Utils::QImageToByteArray(image);
return ba;
}
QVariant
PixbufManipulator::userActionIcon(const UserActionElement& state) const
{
Q_UNUSED(state)
return QVariant();
}
QVariant
PixbufManipulator::decorationRole(const QModelIndex& index)
{
Q_UNUSED(index)
return QVariant();
}
QVariant
PixbufManipulator::decorationRole(const lrc::api::conversation::Info& conversationInfo,
const lrc::api::account::Info& accountInfo)
{
QImage photo;
auto contacts = conversationInfo.participants;
if (contacts.empty()) {
return QVariant::fromValue(photo);
}
try {
/*
* Get first contact photo.
*/
auto contactUri = contacts.front();
auto contactInfo = accountInfo.contactModel->getContact(contactUri);
auto contactPhoto = contactInfo.profileInfo.avatar;
auto bestName = Utils::bestNameForContact(contactInfo);
auto bestId = Utils::bestIdForContact(contactInfo);
if (accountInfo.profileInfo.type == lrc::api::profile::Type::SIP
&& contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY) {
photo = Utils::fallbackAvatar(QString(), QString());
} else if (contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY
&& contactInfo.profileInfo.uri.isEmpty()) {
photo = Utils::fallbackAvatar(QString(), QString());
} else if (!contactPhoto.isEmpty()) {
QByteArray byteArray = contactPhoto.toLocal8Bit();
photo = personPhoto(byteArray, nullptr).value<QImage>();
if (photo.isNull()) {
auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
}
} else {
auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
}
} catch (...) {
}
return QVariant::fromValue(Utils::scaleAndFrame(photo));
}

View file

@ -1,49 +0,0 @@
/*
* Copyright (C) 2015-2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QImage>
#include <interfaces/pixmapmanipulatori.h>
#include <memory>
Q_DECLARE_METATYPE(QImage);
class Person;
QByteArray QImageToByteArray(QImage image);
class PixbufManipulator : public Interfaces::PixmapManipulatorI
{
public:
QVariant personPhoto(const QByteArray& data, const QString& type = "PNG") override;
/*
* TODO: the following methods return an empty QVariant/QByteArray.
*/
QVariant numberCategoryIcon(const QVariant& p,
const QSize& size,
bool displayPresence = false,
bool isPresent = false) override;
QByteArray toByteArray(const QVariant& pxm) override;
QVariant userActionIcon(const UserActionElement& state) const override;
QVariant decorationRole(const QModelIndex& index) override;
QVariant decorationRole(const lrc::api::conversation::Info& conversation,
const lrc::api::account::Info& accountInfo) override;
};

View file

@ -112,24 +112,6 @@ PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent)
PhotoboothPreviewRender::~PhotoboothPreviewRender() {}
QImage
PhotoboothPreviewRender::takePhoto()
{
if (auto previewImage = LRCInstance::renderer()->getPreviewFrame()) {
return previewImage->copy();
}
return QImage();
}
QString
PhotoboothPreviewRender::takeCroppedPhotoToBase64(int size)
{
auto image = Utils::cropImage(takePhoto());
auto avatar = image.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
return QString::fromLatin1(Utils::QImageToByteArray(avatar).toBase64().data());
}
void
PhotoboothPreviewRender::paint(QPainter* painter)
{

View file

@ -63,9 +63,6 @@ public:
explicit PhotoboothPreviewRender(QQuickItem* parent = 0);
virtual ~PhotoboothPreviewRender();
QImage takePhoto();
Q_INVOKABLE QString takeCroppedPhotoToBase64(int size);
signals:
void hideBooth();

View file

@ -263,15 +263,6 @@ SettingsAdapter::getAccountBestName()
return Utils::bestNameForAccount(LRCInstance::getCurrentAccountInfo());
}
QString
SettingsAdapter::getAvatarImage_Base64(int avatarSize)
{
auto& accountInfo = LRCInstance::getCurrentAccountInfo();
auto avatar = Utils::accountPhoto(accountInfo, {avatarSize, avatarSize});
return QString::fromLatin1(Utils::QImageToByteArray(avatar).toBase64().data());
}
bool
SettingsAdapter::getIsDefaultAvatar()
{
@ -280,18 +271,10 @@ SettingsAdapter::getIsDefaultAvatar()
return accountInfo.profileInfo.avatar.isEmpty();
}
bool
SettingsAdapter::setCurrAccAvatar(QString avatarImgBase64)
void
SettingsAdapter::setCurrAccAvatar(QVariant avatarImg)
{
QImage avatarImg;
const bool ret = avatarImg.loadFromData(QByteArray::fromBase64(avatarImgBase64.toLatin1()));
if (!ret) {
qDebug() << "Current avatar loading from base64 fail";
return false;
} else {
LRCInstance::setCurrAccAvatar(QPixmap::fromImage(avatarImg));
}
return true;
LRCInstance::setCurrAccAvatar(QPixmap::fromImage(avatarImg.value<QImage>()));
}
void

View file

@ -94,9 +94,8 @@ public:
Q_INVOKABLE QString getAccountBestName();
// getters and setters of avatar image
Q_INVOKABLE QString getAvatarImage_Base64(int avatarSize);
Q_INVOKABLE bool getIsDefaultAvatar();
Q_INVOKABLE bool setCurrAccAvatar(QString avatarImgBase64);
Q_INVOKABLE void setCurrAccAvatar(QVariant avatarImg);
Q_INVOKABLE void clearCurrentAvatar();
/*

View file

@ -52,7 +52,7 @@ ColumnLayout {
}
function setAvatar() {
currentAccountAvatar.setAvatarPixmap(SettingsAdapter.getAvatarImage_Base64(currentAccountAvatar.boothWidth), SettingsAdapter.getIsDefaultAvatar())
currentAccountAvatar.setAvatarImage()
}
function stopBooth() {
@ -79,14 +79,8 @@ ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
saveToConfig: true
boothWidth: 180
onImageAcquired: SettingsAdapter.setCurrAccAvatar(imgBase64)
onImageCleared: {
SettingsAdapter.clearCurrentAvatar()
setAvatar()
}
}
MaterialLineEdit {

View file

@ -137,7 +137,6 @@ ColumnLayout {
contactName : ContactName
contactID: ContactID
contactPicture_base64: ContactPicture
onClicked: bannedListWidget.currentIndex = index

View file

@ -31,12 +31,13 @@ ItemDelegate {
property string contactName : ""
property string contactID: ""
property string contactPicture_base64:""
signal btnReAddContactClicked
highlighted: ListView.isCurrentItem
onContactIDChanged: avatarImg.updateImage(contactID)
RowLayout {
anchors.fill: parent
@ -52,11 +53,14 @@ ItemDelegate {
background: Rectangle {
anchors.fill: parent
color: "transparent"
Image {
AvatarImage {
id: avatarImg
anchors.fill: parent
source: contactPicture_base64 === "" ? "" : "data:image/png;base64," + contactPicture_base64
mode: AvatarImage.Mode.FromContactUri
showPresenceIndicator: false
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {

View file

@ -21,12 +21,8 @@
#include "smartlistmodel.h"
#include "lrcinstance.h"
#include "pixbufmanipulator.h"
#include "utils.h"
#include "api/contactmodel.h"
#include "globalinstances.h"
#include <QDateTime>
SmartListModel::SmartListModel(QObject* parent,
@ -148,7 +144,6 @@ SmartListModel::roleNames() const
QHash<int, QByteArray> roles;
roles[DisplayName] = "DisplayName";
roles[DisplayID] = "DisplayID";
roles[Picture] = "Picture";
roles[Presence] = "Presence";
roles[URI] = "URI";
roles[UnreadMessagesCount] = "UnreadMessagesCount";
@ -163,6 +158,7 @@ SmartListModel::roleNames() const
roles[SectionName] = "SectionName";
roles[AccountId] = "AccountId";
roles[Draft] = "Draft";
roles[PictureUid] = "PictureUid";
return roles;
}
@ -183,6 +179,8 @@ void
SmartListModel::fillConversationsList()
{
beginResetModel();
fillContactAvatarUidMap(LRCInstance::getCurrentAccountInfo().contactModel->getAllContacts());
auto* convModel = LRCInstance::getCurrentConversationModel();
conversations_.clear();
@ -208,6 +206,39 @@ SmartListModel::updateConversation(const QString& convUid)
}
}
void
SmartListModel::updateContactAvatarUid(const QString& contactUri)
{
contactAvatarUidMap_[contactUri] = Utils::generateUid();
}
void
SmartListModel::fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts)
{
if (contacts.size() == 0) {
contactAvatarUidMap_.clear();
return;
}
if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
bool useContacts = contacts.size() > contactAvatarUidMap_.size();
auto contactsKeyList = contacts.keys();
auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
for (int i = 0;
i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
++i) {
// Insert or update
if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
// Remove
if (i < contactAvatarUidMapKeyList.size()
&& !contacts.contains(contactAvatarUidMapKeyList.at(i)))
contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
}
}
}
void
SmartListModel::toggleSection(const QString& section)
{
@ -241,12 +272,10 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
return QVariant();
}
auto& contactModel = accountInfo.contactModel;
// Since we are using image provider right now, image url representation should be unique to
// be able to use the image cache, account avatar will only be updated once PictureUid changed
switch (role) {
case Role::Picture: {
auto contactImage
= GlobalInstances::pixmapManipulator().decorationRole(item, accountInfo).value<QImage>();
return QString::fromLatin1(Utils::QImageToByteArray(contactImage).toBase64().data());
}
case Role::DisplayName: {
if (!item.participants.isEmpty()) {
auto& contact = contactModel->getContact(item.participants[0]);
@ -268,10 +297,15 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
}
return QVariant(false);
}
case Role::PictureUid: {
if (!item.participants.isEmpty()) {
return QVariant(contactAvatarUidMap_[item.participants[0]]);
}
return QVariant("");
}
case Role::URI: {
if (!item.participants.isEmpty()) {
auto& contact = contactModel->getContact(item.participants[0]);
return QVariant(contact.profileInfo.uri);
return QVariant(item.participants[0]);
}
return QVariant("");
}
@ -331,13 +365,13 @@ SmartListModel::getConversationItemData(const conversation::Info& item,
if (!convInfo.uid.isEmpty()) {
auto* callModel = LRCInstance::getCurrentCallModel();
const auto call = callModel->getCall(convInfo.callId);
return QVariant(callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == lrc::api::call::Status::IN_PROGRESS
|| call.status == lrc::api::call::Status::PAUSED
|| call.status == lrc::api::call::Status::INCOMING_RINGING))
|| (call.isOutgoing
&& call.status != lrc::api::call::Status::ENDED)));
return QVariant(
callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == lrc::api::call::Status::IN_PROGRESS
|| call.status == lrc::api::call::Status::PAUSED
|| call.status == lrc::api::call::Status::INCOMING_RINGING))
|| (call.isOutgoing && call.status != lrc::api::call::Status::ENDED)));
}
return QVariant(false);
}

View file

@ -24,6 +24,7 @@
#include "api/contact.h"
#include "api/conversation.h"
#include "api/conversationmodel.h"
#include "api/contactmodel.h"
#include <QAbstractItemModel>
@ -42,7 +43,6 @@ public:
enum Role {
DisplayName = Qt::UserRole + 1,
DisplayID,
Picture,
Presence,
URI,
UnreadMessagesCount,
@ -58,6 +58,7 @@ public:
CallState,
SectionName,
AccountId,
PictureUid,
Draft
};
Q_ENUM(Role)
@ -85,15 +86,28 @@ public:
Q_INVOKABLE void fillConversationsList();
Q_INVOKABLE void updateConversation(const QString& conv);
/*
* This function is to update contact avatar uuid for current account when there's an contact
* avatar changed.
*/
Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
private:
QVariant getConversationItemData(const ConversationInfo& item,
const AccountInfo& accountInfo,
int role) const;
/*
* Give a uuid for each contact avatar for current account and it will serve PictureUid role
*/
void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
/*
* List sectioning.
*/
Type listModelType_;
QMap<QString, bool> sectionState_;
QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
QMap<QString, QString> contactAvatarUidMap_;
ConversationModel::ConversationQueue conversations_;
};

View file

@ -25,9 +25,7 @@
#include "globalsystemtray.h"
#include "jamiavatartheme.h"
#include "lrcinstance.h"
#include "pixbufmanipulator.h"
#include <globalinstances.h>
#include <qrencode.h>
#include <QApplication>
@ -43,6 +41,7 @@
#include <QSvgRenderer>
#include <QTranslator>
#include <QtConcurrent/QtConcurrent>
#include <QUuid>
#ifdef Q_OS_WIN
#include <lmcons.h>
@ -245,14 +244,52 @@ Utils::GetISODate()
#endif
}
QString
Utils::getContactImageString(const QString& accountId, const QString& uid)
QImage
Utils::contactPhoto(const QString& contactUri, const QSize& size)
{
return QString::fromLatin1(
Utils::QImageToByteArray(
Utils::conversationPhoto(uid, LRCInstance::getAccountInfo(accountId)))
.toBase64()
.data());
QImage photo;
try {
/*
* Get first contact photo.
*/
auto& accountInfo = LRCInstance::accountModel().getAccountInfo(LRCInstance::getCurrAccId());
auto contactInfo = accountInfo.contactModel->getContact(contactUri);
auto contactPhoto = contactInfo.profileInfo.avatar;
auto bestName = Utils::bestNameForContact(contactInfo);
auto bestId = Utils::bestIdForContact(contactInfo);
if (accountInfo.profileInfo.type == lrc::api::profile::Type::SIP
&& contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY) {
photo = Utils::fallbackAvatar(QString(), QString());
} else if (contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY
&& contactInfo.profileInfo.uri.isEmpty()) {
photo = Utils::fallbackAvatar(QString(), QString());
} else if (!contactPhoto.isEmpty()) {
QByteArray byteArray = contactPhoto.toLocal8Bit();
photo = contactPhotoFromBase64(byteArray, nullptr);
if (photo.isNull()) {
auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
}
} else {
auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
photo = Utils::fallbackAvatar("ring:" + contactInfo.profileInfo.uri, avatarName);
}
} catch (...) {
}
return Utils::scaleAndFrame(photo, size);
}
QImage
Utils::contactPhotoFromBase64(const QByteArray& data, const QString& type)
{
QImage avatar;
const bool ret = avatar.loadFromData(QByteArray::fromBase64(data), type.toLatin1());
if (!ret) {
qDebug() << "Utils: vCard image loading failed";
return QImage();
}
return Utils::getCirclePhoto(avatar, avatar.size().width());
}
QImage
@ -549,21 +586,6 @@ Utils::getReplyMessageBox(QWidget* widget, const QString& title, const QString&
return false;
}
QImage
Utils::conversationPhoto(const QString& convUid,
const lrc::api::account::Info& accountInfo,
bool filtered)
{
auto* convModel = LRCInstance::getCurrentConversationModel();
const auto convInfo = convModel->getConversationForUID(convUid);
if (!convInfo.uid.isEmpty()) {
return GlobalInstances::pixmapManipulator()
.decorationRole(convInfo, accountInfo)
.value<QImage>();
}
return QImage();
}
QColor
Utils::getAvatarColor(const QString& canonicalUri)
{
@ -587,10 +609,12 @@ Utils::getAvatarColor(const QString& canonicalUri)
QImage
Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr, const QSize& size)
{
auto sizeToUse = size.height() >= defaultAvatarSize.height() ? size : defaultAvatarSize;
/*
* We start with a transparent avatar.
*/
QImage avatar(size, QImage::Format_ARGB32);
QImage avatar(sizeToUse, QImage::Format_ARGB32);
avatar.fill(Qt::transparent);
/*
@ -651,7 +675,7 @@ Utils::fallbackAvatar(const QString& canonicalUriStr, const QString& letterStr,
painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
}
return avatar;
return avatar.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
QImage
@ -802,7 +826,7 @@ Utils::accountPhoto(const lrc::api::account::Info& accountInfo, const QSize& siz
QImage photo;
if (!accountInfo.profileInfo.avatar.isEmpty()) {
QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
photo = contactPhotoFromBase64(ba, nullptr);
} else {
auto bestId = bestIdForAccount(accountInfo);
auto bestName = bestNameForAccount(accountInfo);
@ -843,3 +867,9 @@ Utils::isImage(const QString& fileExt)
return true;
return false;
}
QString
Utils::generateUid()
{
return QUuid::createUuid().toString();
}

View file

@ -100,11 +100,9 @@ bool getReplyMessageBox(QWidget* widget, const QString& title, const QString& te
* Image manipulation
*/
static const QSize defaultAvatarSize {128, 128};
QString getContactImageString(const QString& accountId, const QString& uid);
QImage contactPhotoFromBase64(const QByteArray& data, const QString& type);
QImage contactPhoto(const QString& contactUri, const QSize& size = defaultAvatarSize);
QImage getCirclePhoto(const QImage original, int sizePhoto);
QImage conversationPhoto(const QString& convUid,
const lrc::api::account::Info& accountInfo,
bool filtered = false);
QColor getAvatarColor(const QString& canonicalUri);
QImage fallbackAvatar(const QString& canonicalUriStr,
const QString& letterStr = QString(),
@ -123,6 +121,7 @@ QImage cropImage(const QImage& img);
QPixmap pixmapFromSvg(const QString& svg_resource, const QSize& size);
QImage setupQRCode(QString ringID, int margin);
bool isImage(const QString& fileExt);
QString generateUid();
/*
* Misc

View file

@ -105,12 +105,6 @@ UtilsAdapter::checkStartupLink()
return Utils::CheckStartupLink(L"Jami");
}
const QString
UtilsAdapter::getContactImageString(const QString& accountId, const QString& uid)
{
return Utils::getContactImageString(accountId, uid);
}
const QString
UtilsAdapter::getBestName(const QString& accountId, const QString& uid)
{
@ -356,17 +350,6 @@ UtilsAdapter::getAbsPath(QString path)
#endif
}
QString
UtilsAdapter::getCroppedImageBase64FromFile(QString fileName, int size)
{
auto image = Utils::cropImage(QImage(fileName));
auto croppedImage = image.scaled(size,
size,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
return QString::fromLatin1(Utils::QImageToByteArray(croppedImage).toBase64().data());
}
bool
UtilsAdapter::checkShowPluginsButton()
{

View file

@ -44,7 +44,6 @@ public:
Q_INVOKABLE bool createStartupLink();
Q_INVOKABLE QString GetRingtonePath();
Q_INVOKABLE bool checkStartupLink();
Q_INVOKABLE const QString getContactImageString(const QString& accountId, const QString& uid);
Q_INVOKABLE void removeConversation(const QString& accountId,
const QString& uid,
bool banContact = false);
@ -77,7 +76,6 @@ public:
Q_INVOKABLE QString toFileInfoName(QString inputFileName);
Q_INVOKABLE QString toFileAbsolutepath(QString inputFileName);
Q_INVOKABLE QString getAbsPath(QString path);
Q_INVOKABLE QString getCroppedImageBase64FromFile(QString fileName, int size);
Q_INVOKABLE bool checkShowPluginsButton();
Q_INVOKABLE QString fileName(const QString& path);
Q_INVOKABLE QString getExt(const QString& path);

View file

@ -385,14 +385,13 @@ Rectangle {
}
onSaveProfile: {
SettingsAdapter.setCurrAccAvatar(profilePage.boothImgBase64)
if (profilePage.profileImg)
SettingsAdapter.setCurrAccAvatar(profilePage.profileImg)
AccountAdapter.setCurrAccDisplayName(profilePage.displayName)
leave()
}
onLeavePage: {
leave()
}
onLeavePage: leave()
}
}
}

View file

@ -32,8 +32,6 @@ Rectangle {
property alias text_sipPasswordEditAlias: sipPasswordEdit.text
property int preferredHeight: createSIPAccountPageColumnLayout.implicitHeight
property var boothImgBase64: null
signal createAccount
signal leavePage

View file

@ -26,11 +26,13 @@ import "../../commoncomponents"
Rectangle {
id: root
property alias profileImg: setAvatarWidget.boothImg
property int preferredHeight: profilePageColumnLayout.implicitHeight
function initializeOnShowUp() {
setAvatarWidget.hasAvatar = false
setAvatarWidget.setAvatarImage(AvatarImage.Mode.Default, "")
clearAllTextFields()
boothImgBase64 = ""
saveProfileBtn.spinnerTriggered = true
}
@ -48,7 +50,6 @@ Rectangle {
signal saveProfile
property var showBottom: false
property alias boothImgBase64: setAvatarWidget.imgBase64
property alias displayName: aliasEdit.text
property bool isRdv: false