From 77aae84786f64956bb95d4b880832b0a30267c47 Mon Sep 17 00:00:00 2001 From: Ming Rui Zhang <mingrui.zhang@savoirfairelinux.com> Date: Mon, 17 May 2021 11:06:11 -0400 Subject: [PATCH] call: add call status indicator when adding new participants into a conference Gitlab: #410 Change-Id: Iff3b06b123363478794fd7e419db3d2d0ae10bb7 --- images/icons/cross_black_24dp.svg | 8 + qml.qrc | 2 + resources.qrc | 1 + src/calladapter.cpp | 24 +-- src/calladapter.h | 2 +- src/calloverlaymodel.cpp | 128 ++++++++++++ src/calloverlaymodel.h | 41 ++++ src/commoncomponents/AvatarImage.qml | 193 +++++++++--------- src/commoncomponents/PhotoboothView.qml | 20 +- src/commoncomponents/SpinningAnimation.qml | 83 +++++++- src/constant/JamiTheme.qml | 13 ++ .../components/ContactPickerItemDelegate.qml | 2 +- src/mainview/components/InitialCallPage.qml | 10 +- src/mainview/components/MainOverlay.qml | 12 +- src/mainview/components/OngoingCallPage.qml | 2 +- .../ParticipantCallInStatusDelegate.qml | 141 +++++++++++++ .../ParticipantCallInStatusView.qml | 60 ++++++ .../components/ParticipantOverlay.qml | 10 +- .../components/SmartListItemDelegate.qml | 2 +- src/mainview/components/UserProfile.qml | 2 +- .../components/ContactItemDelegate.qml | 2 +- src/wizardview/components/ProfilePage.qml | 6 +- 22 files changed, 610 insertions(+), 154 deletions(-) create mode 100644 images/icons/cross_black_24dp.svg create mode 100644 src/mainview/components/ParticipantCallInStatusDelegate.qml create mode 100644 src/mainview/components/ParticipantCallInStatusView.qml diff --git a/images/icons/cross_black_24dp.svg b/images/icons/cross_black_24dp.svg new file mode 100644 index 000000000..3e5b5f55b --- /dev/null +++ b/images/icons/cross_black_24dp.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> +<path d="M13.3,12.4l2.1-2.1c0.3-0.3,0.3-0.8,0-1.1c-0.3-0.3-0.8-0.3-1.1,0l-2.1,2.1l-2.1-2.1C9.8,8.9,9.3,8.9,9,9.2 + C8.9,9.3,8.8,9.5,8.8,9.7c0,0.2,0.1,0.4,0.2,0.6l2.1,2.1L9,14.5c-0.2,0.1-0.2,0.3-0.2,0.5c0,0.2,0.1,0.4,0.2,0.6 + c0.3,0.3,0.8,0.3,1.1,0l2.1-2.1l2.1,2.1c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1L13.3,12.4z"/> +</svg> diff --git a/qml.qrc b/qml.qrc index 80b9a37d1..b9c2e3bbe 100644 --- a/qml.qrc +++ b/qml.qrc @@ -141,5 +141,7 @@ <file>src/mainview/components/CallActionBar.qml</file> <file>src/commoncomponents/HalfPill.qml</file> <file>src/commoncomponents/MaterialToolTip.qml</file> + <file>src/mainview/components/ParticipantCallInStatusDelegate.qml</file> + <file>src/mainview/components/ParticipantCallInStatusView.qml</file> </qresource> </RCC> diff --git a/resources.qrc b/resources.qrc index bbfe9d506..e5e4217a5 100644 --- a/resources.qrc +++ b/resources.qrc @@ -5,6 +5,7 @@ <file>images/icons/baseline-refresh-24px.svg</file> <file>images/jami_rolling_spinner.gif</file> <file>images/icons/baseline-close-24px.svg</file> + <file>images/icons/cross_black_24dp.svg</file> <file>images/icons/baseline-done-24px.svg</file> <file>images/icons/baseline-error_outline-24px.svg</file> <file>projectcredits.html</file> diff --git a/src/calladapter.cpp b/src/calladapter.cpp index 57f70c1bb..cd5f72b65 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -643,29 +643,9 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo) } void -CallAdapter::hangupCall(const QString& uri) +CallAdapter::hangUpCall(const QString& callId) { - const auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri, accountId_); - if (!convInfo.uid.isEmpty()) { - auto callModel = lrcInstance_->getAccountInfo(accountId_).callModel.get(); - if (callModel->hasCall(convInfo.callId)) { - /* - * Store the last remaining participant of the conference, - * so we can switch the smartlist index after termination. - */ - if (!convInfo.confId.isEmpty()) { - auto callList = lrcInstance_->getConferenceSubcalls(convInfo.confId); - if (callList.size() == 2) { - for (const auto& cId : callList) { - if (cId != convInfo.callId) { - lrcInstance_->pushlastConference(convInfo.confId, cId); - } - } - } - } - callModel->hangUp(convInfo.callId); - } - } + lrcInstance_->getCurrentCallModel()->hangUp(callId); } void diff --git a/src/calladapter.h b/src/calladapter.h index 06d9b84aa..1e56d3df8 100644 --- a/src/calladapter.h +++ b/src/calladapter.h @@ -56,7 +56,7 @@ public: Q_INVOKABLE void sipInputPanelPlayDTMF(const QString& key); // For Call Overlay - Q_INVOKABLE void hangupCall(const QString& uri); + Q_INVOKABLE void hangUpCall(const QString& callId); Q_INVOKABLE void maximizeParticipant(const QString& uri); Q_INVOKABLE void minimizeParticipant(const QString& uri); Q_INVOKABLE void hangUpThisCall(); diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp index beb49d019..a450ac685 100644 --- a/src/calloverlaymodel.cpp +++ b/src/calloverlaymodel.cpp @@ -112,6 +112,127 @@ IndexRangeFilterProxyModel::setRange(int min, int max) invalidateFilter(); } +PendingConferenceesListModel::PendingConferenceesListModel(LRCInstance* instance, QObject* parent) + : QAbstractListModel(parent) + , lrcInstance_(instance) +{ + connectSignals(); + connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, [this]() { connectSignals(); }); +} + +int +PendingConferenceesListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return lrcInstance_->getCurrentCallModel()->getPendingConferencees().size(); +} + +QVariant +PendingConferenceesListModel::data(const QModelIndex& index, int role) const +{ + using namespace PendingConferences; + + // WARNING: not swarm ready + QString pendingConferenceeCallId; + QString pendingConferenceeContactUri; + ContactModel* contactModel {nullptr}; + lrc::api::call::Status callStatus; + try { + auto callModel = lrcInstance_->getCurrentCallModel(); + auto currentPendingConferenceeInfo = callModel->getPendingConferencees().at(index.row()); + pendingConferenceeCallId = currentPendingConferenceeInfo.callId; + const auto call = callModel->getCall(pendingConferenceeCallId); + + callStatus = call.status; + pendingConferenceeContactUri = currentPendingConferenceeInfo.uri; + contactModel = lrcInstance_->getCurrentContactModel(); + } catch (...) { + return QVariant(false); + } + + // 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::PrimaryName: + return QVariant(contactModel->bestNameForContact(pendingConferenceeContactUri)); + case Role::CallStatus: + return QVariant(lrc::api::call::to_string(callStatus)); + case Role::ContactUri: + return QVariant(pendingConferenceeContactUri); + case Role::PendingConferenceeCallId: + return QVariant(pendingConferenceeCallId); + } + return QVariant(); +} + +QHash<int, QByteArray> +PendingConferenceesListModel::roleNames() const +{ + using namespace PendingConferences; + QHash<int, QByteArray> roles; +#define X(role) roles[role] = #role; + PC_ROLES +#undef X + return roles; +} + +void +PendingConferenceesListModel::connectSignals() +{ + beginResetModel(); + + disconnect(callsStatusChanged_); + disconnect(beginInsertPendingConferencesRows_); + disconnect(endInsertPendingConferencesRows_); + disconnect(beginRemovePendingConferencesRows_); + disconnect(endRemovePendingConferencesRows_); + + callsStatusChanged_ = connect(lrcInstance_->getCurrentCallModel(), + &NewCallModel::callStatusChanged, + this, + [this]() { + dataChanged(index(0, 0), + index(rowCount() - 1), + {PendingConferences::Role::CallStatus}); + }); + + beginInsertPendingConferencesRows_ = connect( + lrcInstance_->getCurrentCallModel(), + &NewCallModel::beginInsertPendingConferenceesRows, + this, + [this](int position, int rows) { + beginInsertRows(QModelIndex(), position, position + (rows - 1)); + }, + Qt::DirectConnection); + + endInsertPendingConferencesRows_ = connect( + lrcInstance_->getCurrentCallModel(), + &NewCallModel::endInsertPendingConferenceesRows, + this, + [this]() { endInsertRows(); }, + Qt::DirectConnection); + + beginRemovePendingConferencesRows_ = connect( + lrcInstance_->getCurrentCallModel(), + &NewCallModel::beginRemovePendingConferenceesRows, + this, + [this](int position, int rows) { + beginRemoveRows(QModelIndex(), position, position + (rows - 1)); + }, + Qt::DirectConnection); + + endRemovePendingConferencesRows_ = connect( + lrcInstance_->getCurrentCallModel(), + &NewCallModel::endRemovePendingConferenceesRows, + this, + [this]() { endRemoveRows(); }, + + Qt::DirectConnection); + + endResetModel(); +} + CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent) : QObject(parent) , lrcInstance_(instance) @@ -120,6 +241,7 @@ CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent) , overflowModel_(new IndexRangeFilterProxyModel(secondaryModel_)) , overflowVisibleModel_(new IndexRangeFilterProxyModel(secondaryModel_)) , overflowHiddenModel_(new IndexRangeFilterProxyModel(secondaryModel_)) + , pendingConferenceesModel_(new PendingConferenceesListModel(instance, this)) { connect(this, &CallOverlayModel::overflowIndexChanged, @@ -177,6 +299,12 @@ CallOverlayModel::overflowHiddenModel() return QVariant::fromValue(overflowHiddenModel_); } +QVariant +CallOverlayModel::pendingConferenceesModel() +{ + return QVariant::fromValue(pendingConferenceesModel_); +} + void CallOverlayModel::clearControls() { diff --git a/src/calloverlaymodel.h b/src/calloverlaymodel.h index 1776184a9..5aecc90e2 100644 --- a/src/calloverlaymodel.h +++ b/src/calloverlaymodel.h @@ -27,6 +27,12 @@ #include <QSortFilterProxyModel> #include <QQuickItem> +#define PC_ROLES \ + X(PrimaryName) \ + X(PendingConferenceeCallId) \ + X(CallStatus) \ + X(ContactUri) + namespace CallControl { Q_NAMESPACE enum Role { ItemAction = Qt::UserRole + 1, BadgeCount }; @@ -39,6 +45,17 @@ struct Item }; } // namespace CallControl +namespace PendingConferences { +Q_NAMESPACE +enum Role { + DummyRole = Qt::UserRole + 1, +#define X(role) role, + PC_ROLES +#undef X +}; +Q_ENUM_NS(Role) +} // namespace PendingConferences + class CallControlListModel : public QAbstractListModel { Q_OBJECT @@ -73,6 +90,28 @@ private: int max_ {-1}; }; +class PendingConferenceesListModel : public QAbstractListModel +{ + Q_OBJECT +public: + PendingConferenceesListModel(LRCInstance* instance, QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + void connectSignals(); + +private: + LRCInstance* lrcInstance_ {nullptr}; + + QMetaObject::Connection callsStatusChanged_; + QMetaObject::Connection beginInsertPendingConferencesRows_; + QMetaObject::Connection endInsertPendingConferencesRows_; + QMetaObject::Connection beginRemovePendingConferencesRows_; + QMetaObject::Connection endRemovePendingConferencesRows_; +}; + class CallOverlayModel : public QObject { Q_OBJECT @@ -91,6 +130,7 @@ public: Q_INVOKABLE QVariant overflowModel(); Q_INVOKABLE QVariant overflowVisibleModel(); Q_INVOKABLE QVariant overflowHiddenModel(); + Q_INVOKABLE QVariant pendingConferenceesModel(); Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item); Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item); @@ -110,6 +150,7 @@ private: IndexRangeFilterProxyModel* overflowModel_; IndexRangeFilterProxyModel* overflowVisibleModel_; IndexRangeFilterProxyModel* overflowHiddenModel_; + PendingConferenceesListModel* pendingConferenceesModel_; QList<QQuickItem*> watchedItems_; }; diff --git a/src/commoncomponents/AvatarImage.qml b/src/commoncomponents/AvatarImage.qml index 4a40cd5fd..b10a8bb63 100644 --- a/src/commoncomponents/AvatarImage.qml +++ b/src/commoncomponents/AvatarImage.qml @@ -23,10 +23,10 @@ import net.jami.Adapters 1.0 import net.jami.Constants 1.0 import net.jami.Models 1.0 -Item { +SpinningAnimation { id: root - enum Mode { + enum AvatarMode { FromAccount = 0, FromFile, FromContactUri, @@ -40,22 +40,22 @@ Item { property alias sourceSize: rootImage.sourceSize property int transitionDuration: 150 property bool saveToConfig: false - property int mode: AvatarImage.Mode.FromAccount + property int avatarMode: AvatarImage.AvatarMode.FromAccount property string imageProviderIdPrefix: { - switch(mode) { - case AvatarImage.Mode.FromAccount: + switch (avatarMode) { + case AvatarImage.AvatarMode.FromAccount: return "account_" - case AvatarImage.Mode.FromFile: + case AvatarImage.AvatarMode.FromFile: return "file_" - case AvatarImage.Mode.FromContactUri: + case AvatarImage.AvatarMode.FromContactUri: return "contact_" - case AvatarImage.Mode.FromConvUid: + case AvatarImage.AvatarMode.FromConvUid: return "conversation_" - case AvatarImage.Mode.FromTemporaryName: + case AvatarImage.AvatarMode.FromTemporaryName: return "fallback_" - case AvatarImage.Mode.FromBase64: + case AvatarImage.AvatarMode.FromBase64: return "base64_" - case AvatarImage.Mode.Default: + case AvatarImage.AvatarMode.Default: return "default_" default: return "" @@ -63,24 +63,23 @@ Item { } // Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx - property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + "_" + - imageProviderIdPrefix + property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + + "_" + imageProviderIdPrefix property string imageId: "" property string forceUpdateUrl: Date.now() property alias presenceStatus: presenceIndicator.status property bool showPresenceIndicator: true property int unreadMessagesCount: 0 - property bool enableAnimation: true - property bool showSpinningAnimation: false + property bool enableFadeAnimation: true signal imageIsReady function saveAvatarToConfig() { - switch(mode) { - case AvatarImage.Mode.FromFile: + switch (avatarMode) { + case AvatarImage.AvatarMode.FromFile: AccountAdapter.setCurrAccAvatar(true, imageId) break - case AvatarImage.Mode.FromBase64: + case AvatarImage.AvatarMode.FromBase64: AccountAdapter.setCurrAccAvatar(false, imageId) break default: @@ -102,60 +101,38 @@ Item { } function reloadImageSource() { - var tempEnableAnimation = enableAnimation + var tempEnableAnimation = enableFadeAnimation var tempImageSource = rootImage.source - enableAnimation = false + enableFadeAnimation = false rootImage.source = "" - enableAnimation = tempEnableAnimation + enableFadeAnimation = tempEnableAnimation rootImage.source = tempImageSource } function rootImageOverlayReadyCallback() { - if (rootImageOverlay.status === Image.Ready && - (rootImageOverlay.state === "avatarImgFadeIn")) { - rootImageOverlay.statusChanged.disconnect(rootImageOverlayReadyCallback) + if (rootImageOverlay.status === Image.Ready + && (rootImageOverlay.state === "avatarImgFadeIn")) { + rootImageOverlay.statusChanged.disconnect( + rootImageOverlayReadyCallback) rootImageOverlay.state = '' } } - Image { - id: rootImage + Item { + id: imageGroup - anchors.fill: root + anchors.centerIn: root - smooth: true - antialiasing: true - asynchronous: true - - sourceSize.width: Math.max(24, width) - sourceSize.height: Math.max(24, height) - - fillMode: Image.PreserveAspectFit - - onStatusChanged: { - if (status === Image.Ready) { - if (enableAnimation) { - rootImageOverlay.state = "avatarImgFadeIn" - } else { - rootImageOverlay.source = rootImage.source - root.imageIsReady() - } - } - } - - Component.onCompleted: { - if (imageId) - return source = imageProviderUrl + imageId - return source = "" - } + width: root.width - spinningAnimationWidth + height: root.height - spinningAnimationWidth Image { - id: rootImageOverlay + id: rootImage - anchors.fill: rootImage + anchors.fill: imageGroup smooth: true antialiasing: true @@ -166,65 +143,87 @@ Item { fillMode: Image.PreserveAspectFit - states: State { - name: "avatarImgFadeIn" - PropertyChanges { - target: rootImageOverlay - opacity: 0 + onStatusChanged: { + if (status === Image.Ready) { + if (enableFadeAnimation) { + rootImageOverlay.state = "avatarImgFadeIn" + } else { + rootImageOverlay.source = rootImage.source + root.imageIsReady() + } } } - transitions: Transition { - NumberAnimation { - properties: "opacity" - easing.type: Easing.InOutQuad - duration: enableAnimation ? 400 : 0 + Component.onCompleted: { + if (imageId) + return source = imageProviderUrl + imageId + return source = "" + } + + Image { + id: rootImageOverlay + + anchors.fill: rootImage + + smooth: true + antialiasing: true + asynchronous: true + + sourceSize.width: Math.max(24, width) + sourceSize.height: Math.max(24, height) + + fillMode: Image.PreserveAspectFit + + states: State { + name: "avatarImgFadeIn" + PropertyChanges { + target: rootImageOverlay + opacity: 0 + } } - onRunningChanged: { - if ((rootImageOverlay.state === "avatarImgFadeIn") && (!running)) { - if (rootImageOverlay.source === rootImage.source) { - rootImageOverlay.state = '' - return + transitions: Transition { + NumberAnimation { + properties: "opacity" + easing.type: Easing.InOutQuad + duration: enableFadeAnimation ? 400 : 0 + } + + onRunningChanged: { + if ((rootImageOverlay.state === "avatarImgFadeIn") + && (!running)) { + if (rootImageOverlay.source === rootImage.source) { + rootImageOverlay.state = '' + return + } + rootImageOverlay.statusChanged.connect( + rootImageOverlayReadyCallback) + rootImageOverlay.source = rootImage.source } - rootImageOverlay.statusChanged.connect(rootImageOverlayReadyCallback) - rootImageOverlay.source = rootImage.source } } } } - } - PresenceIndicator { - id: presenceIndicator + PresenceIndicator { + id: presenceIndicator - anchors.right: root.right - anchors.rightMargin: -1 - anchors.bottom: root.bottom - anchors.bottomMargin: -1 + anchors.right: imageGroup.right + anchors.rightMargin: -1 + anchors.bottom: imageGroup.bottom + anchors.bottomMargin: -1 - size: root.width * 0.26 + size: imageGroup.width * 0.26 - visible: showPresenceIndicator - } - - SpinningAnimation { - id: spinningAnimation - - anchors.horizontalCenter: root.horizontalCenter - anchors.verticalCenter: root.verticalCenter - - visible: showSpinningAnimation - width: Math.ceil(root.width * 1.05) - height: Math.ceil(root.height * 1.05) - z: -1 - } + visible: showPresenceIndicator + } - Connections { - target: ScreenInfo + Connections { + target: ScreenInfo - function onDevicePixelRatioChanged(){ - reloadImageSource() + function onDevicePixelRatioChanged() { + reloadImageSource() + } } } } diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml index 5f0e35c59..19434d548 100644 --- a/src/commoncomponents/PhotoboothView.qml +++ b/src/commoncomponents/PhotoboothView.qml @@ -49,7 +49,7 @@ ColumnLayout { photoState = PhotoboothView.PhotoState.Default avatarSet = false if (useDefaultAvatar) - setAvatarImage(AvatarImage.Mode.Default, "") + setAvatarImage(AvatarImage.AvatarMode.Default, "") } function startBooth() { @@ -65,16 +65,16 @@ ColumnLayout { } catch(erro){console.log("Exception: " + erro.message)} } - function setAvatarImage(mode = AvatarImage.Mode.FromAccount, + function setAvatarImage(mode = AvatarImage.AvatarMode.FromAccount, imageId = LRCInstance.currentAccountId){ - if (mode !== AvatarImage.Mode.FromBase64) - avatarImg.enableAnimation = true + if (mode !== AvatarImage.AvatarMode.FromBase64) + avatarImg.enableFadeAnimation = true else - avatarImg.enableAnimation = false + avatarImg.enableFadeAnimation = false - avatarImg.mode = mode + avatarImg.avatarMode = mode - if (mode === AvatarImage.Mode.Default) { + if (mode === AvatarImage.AvatarMode.Default) { avatarImg.updateImage(imageId) return } @@ -116,7 +116,7 @@ ColumnLayout { return } - setAvatarImage(AvatarImage.Mode.FromFile, + setAvatarImage(AvatarImage.AvatarMode.FromFile, UtilsAdapter.getAbsPath(fileName)) } } @@ -161,7 +161,7 @@ ColumnLayout { } onImageIsReady: { - if (mode === AvatarImage.Mode.FromBase64) + if (avatarMode === AvatarImage.AvatarMode.FromBase64) photoState = PhotoboothView.PhotoState.Taken if (photoState === PhotoboothView.PhotoState.Taken) { @@ -268,7 +268,7 @@ ColumnLayout { startBooth() return } else { - setAvatarImage(AvatarImage.Mode.FromBase64, + setAvatarImage(AvatarImage.AvatarMode.FromBase64, previewWidget.takePhoto(boothWidth)) avatarSet = true diff --git a/src/commoncomponents/SpinningAnimation.qml b/src/commoncomponents/SpinningAnimation.qml index 6147893af..f8061b23f 100644 --- a/src/commoncomponents/SpinningAnimation.qml +++ b/src/commoncomponents/SpinningAnimation.qml @@ -1,6 +1,7 @@ /* * Copyright (C) 2021 by Savoir-faire Linux * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,8 +26,23 @@ import QtGraphicalEffects 1.12 Item { id: root + enum SpinningAnimationMode { + DISABLED = 0, + NORMAL, + SYMMETRY + } + + property int spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.DISABLED + property int spinningAnimationWidth: 5 + property real outerCutRadius: root.height / 2 + property int spinningAnimationDuration: 1000 + ConicalGradient { + id: conicalGradientOne + anchors.fill: parent + + visible: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED angle: 0.0 gradient: Gradient { GradientStop { position: 0.5; color: "transparent" } @@ -35,17 +51,74 @@ Item { RotationAnimation on angle { loops: Animation.Infinite - duration: 1000 + duration: spinningAnimationDuration from: 0 to: 360 } + + layer.enabled: true + layer.effect: OpacityMask { + invert: true + maskSource: Item { + width: conicalGradientOne.width + height: conicalGradientOne.height + + Rectangle { + anchors.fill: parent + anchors.margins: spinningAnimationWidth + radius: outerCutRadius + } + } + } + } + + ConicalGradient { + id: conicalGradientTwo + + anchors.fill: parent + + visible: spinningAnimationMode === SpinningAnimation.SpinningAnimationMode.SYMMETRY + angle: 180.0 + gradient: Gradient { + GradientStop { + position: 0.75 + color: "transparent" + } + GradientStop { + position: 1.0 + color: "white" + } + } + + RotationAnimation on angle { + loops: Animation.Infinite + duration: spinningAnimationDuration + from: 180.0 + to: 540.0 + } + + layer.enabled: true + layer.effect: OpacityMask { + invert: true + maskSource: Item { + width: conicalGradientTwo.width + height: conicalGradientTwo.height + + Rectangle { + anchors.fill: parent + anchors.margins: spinningAnimationWidth + radius: outerCutRadius + } + } + } } - layer.enabled: true + + layer.enabled: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED layer.effect: OpacityMask { maskSource: Rectangle { - width: root.height + width: root.width height: root.height - radius: root.height / 2 + radius: outerCutRadius } } -} \ No newline at end of file +} diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml index 3cb860b18..a3357ba03 100644 --- a/src/constant/JamiTheme.qml +++ b/src/constant/JamiTheme.qml @@ -129,6 +129,9 @@ Item { // Plugin Preferences View property color comboBoxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor + // ParticipantCallInStatusView + property color participantCallInStatusTextColor: whiteColor + // Chatview property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3) property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e" @@ -191,6 +194,16 @@ Item { property real smartListAvatarSize: 52 property real avatarSizeInCall: 130 property real callButtonPreferredSize: 50 + property int participantCallInStatusViewWidth: 225 + property int participantCallInStatusViewHeight: 300 + property int participantCallInStatusDelegateHeight: 105 + property int participantCallInStatusDelegateRadius: 5 + property real participantCallInStatusOpacity: 0.77 + property int participantCallInAvatarSize: 75 + property int participantCallInNameFontSize: 11 + property int participantCallInStatusFontSize: 9 + property int participantCallInStatusTextWidthLimit: 100 + property int participantCallInStatusTextWidth: 68 property real maximumWidthSettingsView: 600 property real settingsHeaderpreferredHeight: 64 diff --git a/src/mainview/components/ContactPickerItemDelegate.qml b/src/mainview/components/ContactPickerItemDelegate.qml index d71bd2bcf..6c9a7d32a 100644 --- a/src/mainview/components/ContactPickerItemDelegate.qml +++ b/src/mainview/components/ContactPickerItemDelegate.qml @@ -39,7 +39,7 @@ ItemDelegate { width: 40 height: 40 - mode: AvatarImage.Mode.FromContactUri + avatarMode: AvatarImage.AvatarMode.FromContactUri imageId: URI } diff --git a/src/mainview/components/InitialCallPage.qml b/src/mainview/components/InitialCallPage.qml index 9c22c21e5..1c1484baf 100644 --- a/src/mainview/components/InitialCallPage.qml +++ b/src/mainview/components/InitialCallPage.qml @@ -34,7 +34,7 @@ Rectangle { property bool isIncoming: false property bool isAudioOnly: false - property var accountConvPair: ["",""] + property var accountConvPair: ["", ""] property int callStatus: 0 property string bestName: "" @@ -76,12 +76,12 @@ Rectangle { id: contactImg Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: JamiTheme.avatarSizeInCall - Layout.preferredHeight: JamiTheme.avatarSizeInCall + Layout.preferredWidth: JamiTheme.avatarSizeInCall + spinningAnimationWidth + Layout.preferredHeight: JamiTheme.avatarSizeInCall + spinningAnimationWidth - mode: AvatarImage.Mode.FromConvUid + avatarMode: AvatarImage.AvatarMode.FromConvUid showPresenceIndicator: false - showSpinningAnimation: true + spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.NORMAL } Text { diff --git a/src/mainview/components/MainOverlay.qml b/src/mainview/components/MainOverlay.qml index fd9632a81..d47de0998 100644 --- a/src/mainview/components/MainOverlay.qml +++ b/src/mainview/components/MainOverlay.qml @@ -44,7 +44,8 @@ Item { property bool frozen: callActionBar.overflowOpen || callActionBar.hovered || - callActionBar.subMenuOpen + callActionBar.subMenuOpen || + participantCallInStatusView.visible opacity: 0 @@ -184,6 +185,15 @@ Item { } } + ParticipantCallInStatusView { + id: participantCallInStatusView + + anchors.right: root.right + anchors.rightMargin: 10 + anchors.bottom: __callActionBar.top + anchors.bottomMargin: 20 + } + CallActionBar { id: __callActionBar diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml index c4cec1c0b..a68f2f42d 100644 --- a/src/mainview/components/OngoingCallPage.qml +++ b/src/mainview/components/OngoingCallPage.qml @@ -348,7 +348,7 @@ Rectangle { Layout.preferredWidth: JamiTheme.avatarSizeInCall Layout.preferredHeight: JamiTheme.avatarSizeInCall - mode: AvatarImage.Mode.FromConvUid + avatarMode: AvatarImage.AvatarMode.FromConvUid showPresenceIndicator: false } diff --git a/src/mainview/components/ParticipantCallInStatusDelegate.qml b/src/mainview/components/ParticipantCallInStatusDelegate.qml new file mode 100644 index 000000000..d73c3991b --- /dev/null +++ b/src/mainview/components/ParticipantCallInStatusDelegate.qml @@ -0,0 +1,141 @@ +/* + * 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.Adapters 1.0 +import net.jami.Models 1.0 +import net.jami.Constants 1.0 + +import "../../commoncomponents" + +SpinningAnimation { + id: root + + width: contentRect.width + spinningAnimationWidth + height: JamiTheme.participantCallInStatusDelegateHeight + + spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.SYMMETRY + outerCutRadius: JamiTheme.participantCallInStatusDelegateRadius + spinningAnimationDuration: 5000 + + Rectangle { + id: contentRect + + anchors.centerIn: root + + width: JamiTheme.participantCallInStatusViewWidth + callStatus.Layout.preferredWidth + - JamiTheme.participantCallInStatusTextWidth - spinningAnimationWidth + height: JamiTheme.participantCallInStatusDelegateHeight - spinningAnimationWidth + + color: JamiTheme.darkGreyColor + opacity: JamiTheme.participantCallInStatusOpacity + radius: JamiTheme.participantCallInStatusDelegateRadius + + AvatarImage { + id: avatar + + anchors.left: contentRect.left + anchors.leftMargin: 10 + anchors.verticalCenter: contentRect.verticalCenter + + width: JamiTheme.participantCallInAvatarSize + height: JamiTheme.participantCallInAvatarSize + + showPresenceIndicator: false + avatarMode: AvatarImage.AvatarMode.FromContactUri + imageId: ContactUri + } + + ColumnLayout { + id: infoColumnLayout + + anchors.left: avatar.right + anchors.leftMargin: 5 + anchors.verticalCenter: contentRect.verticalCenter + + implicitHeight: 50 + implicitWidth: JamiTheme.participantCallInStatusTextWidth + + spacing: 5 + + Text { + id: name + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.preferredWidth: JamiTheme.participantCallInStatusTextWidth + + font.weight: Font.ExtraBold + font.pointSize: JamiTheme.participantCallInNameFontSize + color: JamiTheme.participantCallInStatusTextColor + text: PrimaryName + elide: Text.ElideRight + } + + Text { + id: callStatus + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + font.weight: Font.Normal + font.pointSize: JamiTheme.participantCallInStatusFontSize + color: JamiTheme.participantCallInStatusTextColor + text: CallStatus + "…" + elide: Text.ElideRight + + onWidthChanged: { + if (width > JamiTheme.participantCallInStatusTextWidth + && width < JamiTheme.participantCallInStatusTextWidthLimit) + callStatus.Layout.preferredWidth = width + else if (width >= JamiTheme.participantCallInStatusTextWidthLimit) + callStatus.Layout.preferredWidth + = JamiTheme.participantCallInStatusTextWidthLimit + else + callStatus.Layout.preferredWidth + = JamiTheme.participantCallInStatusTextWidth + } + } + } + + PushButton { + id: callCancelButton + + anchors.right: contentRect.right + anchors.rightMargin: 10 + anchors.verticalCenter: contentRect.verticalCenter + + width: 40 + height: 40 + // To control the size of the svg + preferredSize: 50 + + pressedColor: JamiTheme.refuseRed + hoveredColor: JamiTheme.refuseRed + normalColor: JamiTheme.refuseRedTransparent + + source: "qrc:/images/icons/cross_black_24dp.svg" + imageColor: JamiTheme.whiteColor + + toolTipText: JamiStrings.optionCancel + + onClicked: CallAdapter.hangUpCall(PendingConferenceeCallId) + } + } +} diff --git a/src/mainview/components/ParticipantCallInStatusView.qml b/src/mainview/components/ParticipantCallInStatusView.qml new file mode 100644 index 000000000..81790813d --- /dev/null +++ b/src/mainview/components/ParticipantCallInStatusView.qml @@ -0,0 +1,60 @@ +/* + * 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 net.jami.Adapters 1.0 +import net.jami.Constants 1.0 + +ListView { + id: root + + width: currentItem ? currentItem.width + currentItem.spinningAnimationWidth + : JamiTheme.participantCallInStatusViewWidth + height: JamiTheme.participantCallInStatusDelegateHeight + + model: CallOverlayModel.pendingConferenceesModel() + delegate: ParticipantCallInStatusDelegate {} + + visible: currentItem ? true : false + + Connections { + target: model + + function onRowsInserted() { + var preferredHeight = JamiTheme.participantCallInStatusDelegateHeight * model.rowCount() + root.height = JamiTheme.participantCallInStatusViewHeight + < preferredHeight ? JamiTheme.participantCallInStatusViewHeight + : preferredHeight + } + + function onRowsRemoved() { + var preferredHeight = JamiTheme.participantCallInStatusDelegateHeight * model.rowCount() + root.height = JamiTheme.participantCallInStatusViewHeight + < preferredHeight ? JamiTheme.participantCallInStatusViewHeight + : preferredHeight + } + } + + clip: true + maximumFlickVelocity: 1024 + ScrollIndicator.vertical: ScrollIndicator {} +} diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml index 7d166ae17..6120491fe 100644 --- a/src/mainview/components/ParticipantOverlay.qml +++ b/src/mainview/components/ParticipantOverlay.qml @@ -58,16 +58,16 @@ Item { contactImage.visible = false else { if (avatar) { - contactImage.mode = AvatarImage.Mode.FromBase64 + contactImage.avatarMode = AvatarImage.AvatarMode.FromBase64 contactImage.updateImage(avatar) } else if (local) { - contactImage.mode = AvatarImage.Mode.FromAccount + contactImage.avatarMode = AvatarImage.AvatarMode.FromAccount contactImage.updateImage(LRCInstance.currentAccountId) } else if (isContact) { - contactImage.mode = AvatarImage.Mode.FromContactUri + contactImage.avatarMode = AvatarImage.AvatarMode.FromContactUri contactImage.updateImage(uri) } else { - contactImage.mode = AvatarImage.Mode.FromTemporaryName + contactImage.avatarMode = AvatarImage.AvatarMode.FromTemporaryName contactImage.updateImage(uri) } contactImage.visible = true @@ -195,7 +195,7 @@ Item { fillMode: Image.PreserveAspectFit imageId: "" visible: false - mode: AvatarImage.Mode.Default + avatarMode: AvatarImage.AvatarMode.Default showPresenceIndicator: false layer.enabled: true diff --git a/src/mainview/components/SmartListItemDelegate.qml b/src/mainview/components/SmartListItemDelegate.qml index 02f449d29..263d9981d 100644 --- a/src/mainview/components/SmartListItemDelegate.qml +++ b/src/mainview/components/SmartListItemDelegate.qml @@ -68,7 +68,7 @@ ItemDelegate { Layout.preferredWidth: JamiTheme.smartListAvatarSize Layout.preferredHeight: JamiTheme.smartListAvatarSize - mode: AvatarImage.Mode.FromContactUri + avatarMode: AvatarImage.AvatarMode.FromContactUri showPresenceIndicator: Presence === undefined ? false : Presence transitionDuration: 0 } diff --git a/src/mainview/components/UserProfile.qml b/src/mainview/components/UserProfile.qml index ac0690dcc..0e0f6b874 100644 --- a/src/mainview/components/UserProfile.qml +++ b/src/mainview/components/UserProfile.qml @@ -66,7 +66,7 @@ BaseDialog { sourceSize.width: preferredImgSize sourceSize.height: preferredImgSize - mode: AvatarImage.Mode.FromConvUid + avatarMode: AvatarImage.AvatarMode.FromConvUid showPresenceIndicator: false } diff --git a/src/settingsview/components/ContactItemDelegate.qml b/src/settingsview/components/ContactItemDelegate.qml index 92a8ce1c7..36faaae89 100644 --- a/src/settingsview/components/ContactItemDelegate.qml +++ b/src/settingsview/components/ContactItemDelegate.qml @@ -65,7 +65,7 @@ ItemDelegate { anchors.fill: parent - mode: AvatarImage.Mode.FromContactUri + avatarMode: AvatarImage.AvatarMode.FromContactUri showPresenceIndicator: false fillMode: Image.PreserveAspectCrop diff --git a/src/wizardview/components/ProfilePage.qml b/src/wizardview/components/ProfilePage.qml index 79b6c1641..d308c0756 100644 --- a/src/wizardview/components/ProfilePage.qml +++ b/src/wizardview/components/ProfilePage.qml @@ -54,7 +54,7 @@ Rectangle { color: JamiTheme.backgroundColor onCreatedAccountIdChanged: { - setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount, + setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount, createdAccountId) } @@ -126,14 +126,14 @@ Rectangle { onTextEdited: { if (!(setAvatarWidget.avatarSet)) { if (text.length === 0) { - setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount, + setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount, createdAccountId) return } if (text.length == 1 && text.charAt(0) !== lastInitialCharacter) { lastInitialCharacter = text.charAt(0) - setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromTemporaryName, + setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromTemporaryName, text) } } -- GitLab