1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-04 06:45:45 +02:00

call: add call status indicator when adding new participants into a conference

Gitlab: #410

Change-Id: Iff3b06b123363478794fd7e419db3d2d0ae10bb7
This commit is contained in:
Ming Rui Zhang 2021-05-17 11:06:11 -04:00
parent 7059a8cdab
commit 77aae84786
22 changed files with 614 additions and 158 deletions

View file

@ -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>

After

Width:  |  Height:  |  Size: 697 B

View file

@ -141,5 +141,7 @@
<file>src/mainview/components/CallActionBar.qml</file> <file>src/mainview/components/CallActionBar.qml</file>
<file>src/commoncomponents/HalfPill.qml</file> <file>src/commoncomponents/HalfPill.qml</file>
<file>src/commoncomponents/MaterialToolTip.qml</file> <file>src/commoncomponents/MaterialToolTip.qml</file>
<file>src/mainview/components/ParticipantCallInStatusDelegate.qml</file>
<file>src/mainview/components/ParticipantCallInStatusView.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -5,6 +5,7 @@
<file>images/icons/baseline-refresh-24px.svg</file> <file>images/icons/baseline-refresh-24px.svg</file>
<file>images/jami_rolling_spinner.gif</file> <file>images/jami_rolling_spinner.gif</file>
<file>images/icons/baseline-close-24px.svg</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-done-24px.svg</file>
<file>images/icons/baseline-error_outline-24px.svg</file> <file>images/icons/baseline-error_outline-24px.svg</file>
<file>projectcredits.html</file> <file>projectcredits.html</file>

View file

@ -643,29 +643,9 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo)
} }
void void
CallAdapter::hangupCall(const QString& uri) CallAdapter::hangUpCall(const QString& callId)
{ {
const auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri, accountId_); lrcInstance_->getCurrentCallModel()->hangUp(callId);
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);
}
}
} }
void void

View file

@ -56,7 +56,7 @@ public:
Q_INVOKABLE void sipInputPanelPlayDTMF(const QString& key); Q_INVOKABLE void sipInputPanelPlayDTMF(const QString& key);
// For Call Overlay // 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 maximizeParticipant(const QString& uri);
Q_INVOKABLE void minimizeParticipant(const QString& uri); Q_INVOKABLE void minimizeParticipant(const QString& uri);
Q_INVOKABLE void hangUpThisCall(); Q_INVOKABLE void hangUpThisCall();

View file

@ -112,6 +112,127 @@ IndexRangeFilterProxyModel::setRange(int min, int max)
invalidateFilter(); 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) CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent)
: QObject(parent) : QObject(parent)
, lrcInstance_(instance) , lrcInstance_(instance)
@ -120,6 +241,7 @@ CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent)
, overflowModel_(new IndexRangeFilterProxyModel(secondaryModel_)) , overflowModel_(new IndexRangeFilterProxyModel(secondaryModel_))
, overflowVisibleModel_(new IndexRangeFilterProxyModel(secondaryModel_)) , overflowVisibleModel_(new IndexRangeFilterProxyModel(secondaryModel_))
, overflowHiddenModel_(new IndexRangeFilterProxyModel(secondaryModel_)) , overflowHiddenModel_(new IndexRangeFilterProxyModel(secondaryModel_))
, pendingConferenceesModel_(new PendingConferenceesListModel(instance, this))
{ {
connect(this, connect(this,
&CallOverlayModel::overflowIndexChanged, &CallOverlayModel::overflowIndexChanged,
@ -177,6 +299,12 @@ CallOverlayModel::overflowHiddenModel()
return QVariant::fromValue(overflowHiddenModel_); return QVariant::fromValue(overflowHiddenModel_);
} }
QVariant
CallOverlayModel::pendingConferenceesModel()
{
return QVariant::fromValue(pendingConferenceesModel_);
}
void void
CallOverlayModel::clearControls() CallOverlayModel::clearControls()
{ {

View file

@ -27,6 +27,12 @@
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QQuickItem> #include <QQuickItem>
#define PC_ROLES \
X(PrimaryName) \
X(PendingConferenceeCallId) \
X(CallStatus) \
X(ContactUri)
namespace CallControl { namespace CallControl {
Q_NAMESPACE Q_NAMESPACE
enum Role { ItemAction = Qt::UserRole + 1, BadgeCount }; enum Role { ItemAction = Qt::UserRole + 1, BadgeCount };
@ -39,6 +45,17 @@ struct Item
}; };
} // namespace CallControl } // 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 class CallControlListModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
@ -73,6 +90,28 @@ private:
int max_ {-1}; 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 class CallOverlayModel : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -91,6 +130,7 @@ public:
Q_INVOKABLE QVariant overflowModel(); Q_INVOKABLE QVariant overflowModel();
Q_INVOKABLE QVariant overflowVisibleModel(); Q_INVOKABLE QVariant overflowVisibleModel();
Q_INVOKABLE QVariant overflowHiddenModel(); Q_INVOKABLE QVariant overflowHiddenModel();
Q_INVOKABLE QVariant pendingConferenceesModel();
Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item); Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item);
Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item); Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item);
@ -110,6 +150,7 @@ private:
IndexRangeFilterProxyModel* overflowModel_; IndexRangeFilterProxyModel* overflowModel_;
IndexRangeFilterProxyModel* overflowVisibleModel_; IndexRangeFilterProxyModel* overflowVisibleModel_;
IndexRangeFilterProxyModel* overflowHiddenModel_; IndexRangeFilterProxyModel* overflowHiddenModel_;
PendingConferenceesListModel* pendingConferenceesModel_;
QList<QQuickItem*> watchedItems_; QList<QQuickItem*> watchedItems_;
}; };

View file

@ -23,10 +23,10 @@ import net.jami.Adapters 1.0
import net.jami.Constants 1.0 import net.jami.Constants 1.0
import net.jami.Models 1.0 import net.jami.Models 1.0
Item { SpinningAnimation {
id: root id: root
enum Mode { enum AvatarMode {
FromAccount = 0, FromAccount = 0,
FromFile, FromFile,
FromContactUri, FromContactUri,
@ -40,22 +40,22 @@ Item {
property alias sourceSize: rootImage.sourceSize property alias sourceSize: rootImage.sourceSize
property int transitionDuration: 150 property int transitionDuration: 150
property bool saveToConfig: false property bool saveToConfig: false
property int mode: AvatarImage.Mode.FromAccount property int avatarMode: AvatarImage.AvatarMode.FromAccount
property string imageProviderIdPrefix: { property string imageProviderIdPrefix: {
switch(mode) { switch (avatarMode) {
case AvatarImage.Mode.FromAccount: case AvatarImage.AvatarMode.FromAccount:
return "account_" return "account_"
case AvatarImage.Mode.FromFile: case AvatarImage.AvatarMode.FromFile:
return "file_" return "file_"
case AvatarImage.Mode.FromContactUri: case AvatarImage.AvatarMode.FromContactUri:
return "contact_" return "contact_"
case AvatarImage.Mode.FromConvUid: case AvatarImage.AvatarMode.FromConvUid:
return "conversation_" return "conversation_"
case AvatarImage.Mode.FromTemporaryName: case AvatarImage.AvatarMode.FromTemporaryName:
return "fallback_" return "fallback_"
case AvatarImage.Mode.FromBase64: case AvatarImage.AvatarMode.FromBase64:
return "base64_" return "base64_"
case AvatarImage.Mode.Default: case AvatarImage.AvatarMode.Default:
return "default_" return "default_"
default: default:
return "" return ""
@ -63,24 +63,23 @@ Item {
} }
// Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx // Full request url example: forceUpdateUrl_xxxxxxx_account_xxxxxxxx
property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl + "_" + property string imageProviderUrl: "image://avatarImage/" + forceUpdateUrl
imageProviderIdPrefix + "_" + imageProviderIdPrefix
property string imageId: "" property string imageId: ""
property string forceUpdateUrl: Date.now() property string forceUpdateUrl: Date.now()
property alias presenceStatus: presenceIndicator.status property alias presenceStatus: presenceIndicator.status
property bool showPresenceIndicator: true property bool showPresenceIndicator: true
property int unreadMessagesCount: 0 property int unreadMessagesCount: 0
property bool enableAnimation: true property bool enableFadeAnimation: true
property bool showSpinningAnimation: false
signal imageIsReady signal imageIsReady
function saveAvatarToConfig() { function saveAvatarToConfig() {
switch(mode) { switch (avatarMode) {
case AvatarImage.Mode.FromFile: case AvatarImage.AvatarMode.FromFile:
AccountAdapter.setCurrAccAvatar(true, imageId) AccountAdapter.setCurrAccAvatar(true, imageId)
break break
case AvatarImage.Mode.FromBase64: case AvatarImage.AvatarMode.FromBase64:
AccountAdapter.setCurrAccAvatar(false, imageId) AccountAdapter.setCurrAccAvatar(false, imageId)
break break
default: default:
@ -102,60 +101,38 @@ Item {
} }
function reloadImageSource() { function reloadImageSource() {
var tempEnableAnimation = enableAnimation var tempEnableAnimation = enableFadeAnimation
var tempImageSource = rootImage.source var tempImageSource = rootImage.source
enableAnimation = false enableFadeAnimation = false
rootImage.source = "" rootImage.source = ""
enableAnimation = tempEnableAnimation enableFadeAnimation = tempEnableAnimation
rootImage.source = tempImageSource rootImage.source = tempImageSource
} }
function rootImageOverlayReadyCallback() { function rootImageOverlayReadyCallback() {
if (rootImageOverlay.status === Image.Ready && if (rootImageOverlay.status === Image.Ready
(rootImageOverlay.state === "avatarImgFadeIn")) { && (rootImageOverlay.state === "avatarImgFadeIn")) {
rootImageOverlay.statusChanged.disconnect(rootImageOverlayReadyCallback) rootImageOverlay.statusChanged.disconnect(
rootImageOverlayReadyCallback)
rootImageOverlay.state = '' rootImageOverlay.state = ''
} }
} }
Image { Item {
id: rootImage id: imageGroup
anchors.fill: root anchors.centerIn: root
smooth: true width: root.width - spinningAnimationWidth
antialiasing: true height: root.height - spinningAnimationWidth
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 = ""
}
Image { Image {
id: rootImageOverlay id: rootImage
anchors.fill: rootImage anchors.fill: imageGroup
smooth: true smooth: true
antialiasing: true antialiasing: true
@ -166,65 +143,87 @@ Item {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
states: State { onStatusChanged: {
name: "avatarImgFadeIn" if (status === Image.Ready) {
PropertyChanges { if (enableFadeAnimation) {
target: rootImageOverlay rootImageOverlay.state = "avatarImgFadeIn"
opacity: 0 } else {
rootImageOverlay.source = rootImage.source
root.imageIsReady()
}
} }
} }
transitions: Transition { Component.onCompleted: {
NumberAnimation { if (imageId)
properties: "opacity" return source = imageProviderUrl + imageId
easing.type: Easing.InOutQuad return source = ""
duration: enableAnimation ? 400 : 0 }
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: { transitions: Transition {
if ((rootImageOverlay.state === "avatarImgFadeIn") && (!running)) { NumberAnimation {
if (rootImageOverlay.source === rootImage.source) { properties: "opacity"
rootImageOverlay.state = '' easing.type: Easing.InOutQuad
return 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 { PresenceIndicator {
id: presenceIndicator id: presenceIndicator
anchors.right: root.right anchors.right: imageGroup.right
anchors.rightMargin: -1 anchors.rightMargin: -1
anchors.bottom: root.bottom anchors.bottom: imageGroup.bottom
anchors.bottomMargin: -1 anchors.bottomMargin: -1
size: root.width * 0.26 size: imageGroup.width * 0.26
visible: showPresenceIndicator visible: showPresenceIndicator
} }
SpinningAnimation { Connections {
id: spinningAnimation target: ScreenInfo
anchors.horizontalCenter: root.horizontalCenter function onDevicePixelRatioChanged() {
anchors.verticalCenter: root.verticalCenter reloadImageSource()
}
visible: showSpinningAnimation
width: Math.ceil(root.width * 1.05)
height: Math.ceil(root.height * 1.05)
z: -1
}
Connections {
target: ScreenInfo
function onDevicePixelRatioChanged(){
reloadImageSource()
} }
} }
} }

View file

@ -49,7 +49,7 @@ ColumnLayout {
photoState = PhotoboothView.PhotoState.Default photoState = PhotoboothView.PhotoState.Default
avatarSet = false avatarSet = false
if (useDefaultAvatar) if (useDefaultAvatar)
setAvatarImage(AvatarImage.Mode.Default, "") setAvatarImage(AvatarImage.AvatarMode.Default, "")
} }
function startBooth() { function startBooth() {
@ -65,16 +65,16 @@ ColumnLayout {
} catch(erro){console.log("Exception: " + erro.message)} } catch(erro){console.log("Exception: " + erro.message)}
} }
function setAvatarImage(mode = AvatarImage.Mode.FromAccount, function setAvatarImage(mode = AvatarImage.AvatarMode.FromAccount,
imageId = LRCInstance.currentAccountId){ imageId = LRCInstance.currentAccountId){
if (mode !== AvatarImage.Mode.FromBase64) if (mode !== AvatarImage.AvatarMode.FromBase64)
avatarImg.enableAnimation = true avatarImg.enableFadeAnimation = true
else 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) avatarImg.updateImage(imageId)
return return
} }
@ -116,7 +116,7 @@ ColumnLayout {
return return
} }
setAvatarImage(AvatarImage.Mode.FromFile, setAvatarImage(AvatarImage.AvatarMode.FromFile,
UtilsAdapter.getAbsPath(fileName)) UtilsAdapter.getAbsPath(fileName))
} }
} }
@ -161,7 +161,7 @@ ColumnLayout {
} }
onImageIsReady: { onImageIsReady: {
if (mode === AvatarImage.Mode.FromBase64) if (avatarMode === AvatarImage.AvatarMode.FromBase64)
photoState = PhotoboothView.PhotoState.Taken photoState = PhotoboothView.PhotoState.Taken
if (photoState === PhotoboothView.PhotoState.Taken) { if (photoState === PhotoboothView.PhotoState.Taken) {
@ -268,7 +268,7 @@ ColumnLayout {
startBooth() startBooth()
return return
} else { } else {
setAvatarImage(AvatarImage.Mode.FromBase64, setAvatarImage(AvatarImage.AvatarMode.FromBase64,
previewWidget.takePhoto(boothWidth)) previewWidget.takePhoto(boothWidth))
avatarSet = true avatarSet = true

View file

@ -1,6 +1,7 @@
/* /*
* Copyright (C) 2021 by Savoir-faire Linux * Copyright (C) 2021 by Savoir-faire Linux
* Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -25,8 +26,23 @@ import QtGraphicalEffects 1.12
Item { Item {
id: root 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 { ConicalGradient {
id: conicalGradientOne
anchors.fill: parent anchors.fill: parent
visible: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED
angle: 0.0 angle: 0.0
gradient: Gradient { gradient: Gradient {
GradientStop { position: 0.5; color: "transparent" } GradientStop { position: 0.5; color: "transparent" }
@ -35,17 +51,74 @@ Item {
RotationAnimation on angle { RotationAnimation on angle {
loops: Animation.Infinite loops: Animation.Infinite
duration: 1000 duration: spinningAnimationDuration
from: 0 from: 0
to: 360 to: 360
} }
}
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
maskSource: Rectangle { invert: true
width: root.height maskSource: Item {
height: root.height width: conicalGradientOne.width
radius: root.height / 2 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: spinningAnimationMode !== SpinningAnimation.SpinningAnimationMode.DISABLED
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: outerCutRadius
}
}
}

View file

@ -129,6 +129,9 @@ Item {
// Plugin Preferences View // Plugin Preferences View
property color comboBoxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor property color comboBoxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor
// ParticipantCallInStatusView
property color participantCallInStatusTextColor: whiteColor
// Chatview // Chatview
property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3) property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3)
property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e" property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e"
@ -191,6 +194,16 @@ Item {
property real smartListAvatarSize: 52 property real smartListAvatarSize: 52
property real avatarSizeInCall: 130 property real avatarSizeInCall: 130
property real callButtonPreferredSize: 50 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 maximumWidthSettingsView: 600
property real settingsHeaderpreferredHeight: 64 property real settingsHeaderpreferredHeight: 64

View file

@ -39,7 +39,7 @@ ItemDelegate {
width: 40 width: 40
height: 40 height: 40
mode: AvatarImage.Mode.FromContactUri avatarMode: AvatarImage.AvatarMode.FromContactUri
imageId: URI imageId: URI
} }

View file

@ -34,7 +34,7 @@ Rectangle {
property bool isIncoming: false property bool isIncoming: false
property bool isAudioOnly: false property bool isAudioOnly: false
property var accountConvPair: ["",""] property var accountConvPair: ["", ""]
property int callStatus: 0 property int callStatus: 0
property string bestName: "" property string bestName: ""
@ -76,12 +76,12 @@ Rectangle {
id: contactImg id: contactImg
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: JamiTheme.avatarSizeInCall Layout.preferredWidth: JamiTheme.avatarSizeInCall + spinningAnimationWidth
Layout.preferredHeight: JamiTheme.avatarSizeInCall Layout.preferredHeight: JamiTheme.avatarSizeInCall + spinningAnimationWidth
mode: AvatarImage.Mode.FromConvUid avatarMode: AvatarImage.AvatarMode.FromConvUid
showPresenceIndicator: false showPresenceIndicator: false
showSpinningAnimation: true spinningAnimationMode: SpinningAnimation.SpinningAnimationMode.NORMAL
} }
Text { Text {

View file

@ -44,7 +44,8 @@ Item {
property bool frozen: callActionBar.overflowOpen || property bool frozen: callActionBar.overflowOpen ||
callActionBar.hovered || callActionBar.hovered ||
callActionBar.subMenuOpen callActionBar.subMenuOpen ||
participantCallInStatusView.visible
opacity: 0 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 { CallActionBar {
id: __callActionBar id: __callActionBar

View file

@ -348,7 +348,7 @@ Rectangle {
Layout.preferredWidth: JamiTheme.avatarSizeInCall Layout.preferredWidth: JamiTheme.avatarSizeInCall
Layout.preferredHeight: JamiTheme.avatarSizeInCall Layout.preferredHeight: JamiTheme.avatarSizeInCall
mode: AvatarImage.Mode.FromConvUid avatarMode: AvatarImage.AvatarMode.FromConvUid
showPresenceIndicator: false showPresenceIndicator: false
} }

View file

@ -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)
}
}
}

View file

@ -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 {}
}

View file

@ -58,16 +58,16 @@ Item {
contactImage.visible = false contactImage.visible = false
else { else {
if (avatar) { if (avatar) {
contactImage.mode = AvatarImage.Mode.FromBase64 contactImage.avatarMode = AvatarImage.AvatarMode.FromBase64
contactImage.updateImage(avatar) contactImage.updateImage(avatar)
} else if (local) { } else if (local) {
contactImage.mode = AvatarImage.Mode.FromAccount contactImage.avatarMode = AvatarImage.AvatarMode.FromAccount
contactImage.updateImage(LRCInstance.currentAccountId) contactImage.updateImage(LRCInstance.currentAccountId)
} else if (isContact) { } else if (isContact) {
contactImage.mode = AvatarImage.Mode.FromContactUri contactImage.avatarMode = AvatarImage.AvatarMode.FromContactUri
contactImage.updateImage(uri) contactImage.updateImage(uri)
} else { } else {
contactImage.mode = AvatarImage.Mode.FromTemporaryName contactImage.avatarMode = AvatarImage.AvatarMode.FromTemporaryName
contactImage.updateImage(uri) contactImage.updateImage(uri)
} }
contactImage.visible = true contactImage.visible = true
@ -195,7 +195,7 @@ Item {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
imageId: "" imageId: ""
visible: false visible: false
mode: AvatarImage.Mode.Default avatarMode: AvatarImage.AvatarMode.Default
showPresenceIndicator: false showPresenceIndicator: false
layer.enabled: true layer.enabled: true

View file

@ -68,7 +68,7 @@ ItemDelegate {
Layout.preferredWidth: JamiTheme.smartListAvatarSize Layout.preferredWidth: JamiTheme.smartListAvatarSize
Layout.preferredHeight: JamiTheme.smartListAvatarSize Layout.preferredHeight: JamiTheme.smartListAvatarSize
mode: AvatarImage.Mode.FromContactUri avatarMode: AvatarImage.AvatarMode.FromContactUri
showPresenceIndicator: Presence === undefined ? false : Presence showPresenceIndicator: Presence === undefined ? false : Presence
transitionDuration: 0 transitionDuration: 0
} }

View file

@ -66,7 +66,7 @@ BaseDialog {
sourceSize.width: preferredImgSize sourceSize.width: preferredImgSize
sourceSize.height: preferredImgSize sourceSize.height: preferredImgSize
mode: AvatarImage.Mode.FromConvUid avatarMode: AvatarImage.AvatarMode.FromConvUid
showPresenceIndicator: false showPresenceIndicator: false
} }

View file

@ -65,7 +65,7 @@ ItemDelegate {
anchors.fill: parent anchors.fill: parent
mode: AvatarImage.Mode.FromContactUri avatarMode: AvatarImage.AvatarMode.FromContactUri
showPresenceIndicator: false showPresenceIndicator: false
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop

View file

@ -54,7 +54,7 @@ Rectangle {
color: JamiTheme.backgroundColor color: JamiTheme.backgroundColor
onCreatedAccountIdChanged: { onCreatedAccountIdChanged: {
setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount, setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount,
createdAccountId) createdAccountId)
} }
@ -126,14 +126,14 @@ Rectangle {
onTextEdited: { onTextEdited: {
if (!(setAvatarWidget.avatarSet)) { if (!(setAvatarWidget.avatarSet)) {
if (text.length === 0) { if (text.length === 0) {
setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromAccount, setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromAccount,
createdAccountId) createdAccountId)
return return
} }
if (text.length == 1 && text.charAt(0) !== lastInitialCharacter) { if (text.length == 1 && text.charAt(0) !== lastInitialCharacter) {
lastInitialCharacter = text.charAt(0) lastInitialCharacter = text.charAt(0)
setAvatarWidget.setAvatarImage(AvatarImage.Mode.FromTemporaryName, setAvatarWidget.setAvatarImage(AvatarImage.AvatarMode.FromTemporaryName,
text) text)
} }
} }