diff --git a/images/icons/moderator.svg b/images/icons/moderator.svg
new file mode 100644
index 00000000..60bc71b7
--- /dev/null
+++ b/images/icons/moderator.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/qml.qrc b/qml.qrc
index 7f2d1227..b06ef1cb 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -102,7 +102,6 @@
src/commoncomponents/GeneralMenuItem.qml
src/mainview/components/ConversationSmartListContextMenu.qml
src/mainview/components/CallViewContextMenu.qml
- src/mainview/components/ParticipantContextMenu.qml
src/commoncomponents/GeneralMenuSeparator.qml
src/mainview/components/UserProfile.qml
src/mainview/js/videodevicecontextmenuitemcreation.js
@@ -137,5 +136,6 @@
src/commoncomponents/ResponsiveImage.qml
src/commoncomponents/PresenceIndicator.qml
src/commoncomponents/AvatarImage.qml
+ src/mainview/components/ParticipantOverlayMenu.qml
diff --git a/resources.qrc b/resources.qrc
index efc77e06..6d7dbdab 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -135,5 +135,6 @@
images/icons/settings_backup_restore-24px.svg
images/logo-jami-standard-coul.svg
images/logo-jami-standard-coul-white.svg
+ images/icons/moderator.svg
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index 2797a82d..d3e5f363 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -399,6 +399,7 @@ CallAdapter::connectCallModel(const QString& accountId)
const auto convInfo = LRCInstance::getConversationFromCallId(callId);
if (!convInfo.uid.isEmpty()) {
emit callStatusChanged(static_cast(call.status), accountId, convInfo.uid);
+ updateCallOverlay(convInfo);
}
switch (call.status) {
@@ -531,45 +532,13 @@ CallAdapter::hangupCall(const QString& uri)
}
}
}
-
callModel->hangUp(convInfo.callId);
}
}
}
void
-CallAdapter::maximizeParticipant(const QString& uri, bool isActive)
-{
- auto* callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
- auto* convModel = LRCInstance::getCurrentConversationModel();
- const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid());
- auto confId = conversation.confId;
- if (confId.isEmpty())
- confId = conversation.callId;
- try {
- const auto call = callModel->getCall(confId);
- switch (call.layout) {
- case lrc::api::call::Layout::GRID:
- callModel->setActiveParticipant(confId, uri);
- callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
- break;
- case lrc::api::call::Layout::ONE_WITH_SMALL:
- callModel->setActiveParticipant(confId, uri);
- callModel->setConferenceLayout(confId,
- isActive ? lrc::api::call::Layout::ONE
- : lrc::api::call::Layout::ONE_WITH_SMALL);
- break;
- case lrc::api::call::Layout::ONE:
- callModel->setActiveParticipant(confId, uri);
- callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
- break;
- };
- } catch (...) {
- }
-}
-
-void
-CallAdapter::minimizeParticipant()
+CallAdapter::maximizeParticipant(const QString& uri)
{
auto* callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
auto* convModel = LRCInstance::getCurrentConversationModel();
@@ -579,16 +548,52 @@ CallAdapter::minimizeParticipant()
confId = conversation.callId;
try {
auto call = callModel->getCall(confId);
- switch (call.layout) {
- case lrc::api::call::Layout::GRID:
- break;
- case lrc::api::call::Layout::ONE_WITH_SMALL:
- callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
- break;
- case lrc::api::call::Layout::ONE:
- callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
- break;
- };
+ if (call.participantsInfos.size() > 0) {
+ for (const auto& participant : call.participantsInfos) {
+ if (participant["uri"] == uri) {
+ if (participant["active"] == "false") {
+ callModel->setActiveParticipant(confId, uri);
+ callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+ } else if (participant["y"].toInt() != 0) {
+ callModel->setActiveParticipant(confId, uri);
+ callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE);
+ } else {
+ callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+ }
+ return;
+ }
+ }
+ }
+ } catch (...) {
+ }
+}
+
+void
+CallAdapter::minimizeParticipant(const QString& uri)
+{
+ auto* callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
+ auto* convModel = LRCInstance::getCurrentConversationModel();
+ const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid());
+ auto confId = conversation.confId;
+
+ if (confId.isEmpty())
+ confId = conversation.callId;
+ try {
+ auto call = callModel->getCall(confId);
+ if (call.participantsInfos.size() > 0) {
+ for (const auto& participant : call.participantsInfos) {
+ if (participant["uri"] == uri) {
+ if (participant["active"] == "true") {
+ if (participant["y"].toInt() == 0) {
+ callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+ } else {
+ callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+ }
+ }
+ return;
+ }
+ }
+ }
} catch (...) {
}
}
@@ -626,7 +631,10 @@ CallAdapter::isCurrentHost() const
if (!convInfo.uid.isEmpty()) {
auto* callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
try {
- auto call = callModel->getCall(convInfo.callId);
+ auto confId = convInfo.confId;
+ if (confId.isEmpty())
+ confId = convInfo.callId;
+ auto call = callModel->getCall(confId);
if (call.participantsInfos.size() == 0) {
return true;
} else {
@@ -647,13 +655,12 @@ CallAdapter::participantIsHost(const QString& uri) const
auto& accInfo = LRCInstance::getAccountInfo(accountId_);
auto* callModel = accInfo.callModel.get();
try {
- auto call = callModel->getCall(convInfo.callId);
- if (call.participantsInfos.size() == 0) {
- return (uri.isEmpty() || uri == accInfo.profileInfo.uri);
+ if (isCurrentHost()) {
+ return uri == accInfo.profileInfo.uri;
} else {
- return !convInfo.confId.isEmpty()
- && callModel->hasCall(convInfo.confId)
- && (uri.isEmpty() || uri == accInfo.profileInfo.uri);
+ auto call = callModel->getCall(convInfo.callId);
+ auto peer = call.peerUri.remove("ring:");
+ return (uri == peer);
}
} catch (...) {
}
@@ -778,22 +785,6 @@ CallAdapter::isCurrentMuted() const
return true;
}
-int
-CallAdapter::getCurrentLayoutType() const
-{
- auto* convModel = LRCInstance::getCurrentConversationModel();
- const auto convInfo = convModel->getConversationForUID(convUid_);
- if (!convInfo.uid.isEmpty()) {
- auto* callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
- try {
- auto call = callModel->getCall(convInfo.confId);
- return static_cast(call.layout);
- } catch (...) {
- }
- }
- return -1;
-}
-
void
CallAdapter::holdThisCallToggle()
{
diff --git a/src/calladapter.h b/src/calladapter.h
index af6a3a25..f33e6679 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -53,15 +53,14 @@ public:
* For Call Overlay
*/
Q_INVOKABLE void hangupCall(const QString& uri);
- Q_INVOKABLE void maximizeParticipant(const QString& uri, bool isActive);
- Q_INVOKABLE void minimizeParticipant();
+ Q_INVOKABLE void maximizeParticipant(const QString& uri);
+ Q_INVOKABLE void minimizeParticipant(const QString& uri);
Q_INVOKABLE void hangUpThisCall();
- Q_INVOKABLE void setModerator(const QString& uri, const bool state);
Q_INVOKABLE bool isCurrentHost() const;
- Q_INVOKABLE bool participantIsHost(const QString& uri = {}) const;
+ Q_INVOKABLE bool participantIsHost(const QString& uri) const;
+ Q_INVOKABLE void setModerator(const QString& uri, const bool state);
Q_INVOKABLE bool isModerator(const QString& uri = {}) const;
Q_INVOKABLE bool isCurrentModerator() const;
- Q_INVOKABLE int getCurrentLayoutType() const;
Q_INVOKABLE void holdThisCallToggle();
Q_INVOKABLE void muteThisCallToggle();
Q_INVOKABLE void recordThisCallToggle();
diff --git a/src/commoncomponents/ResponsiveImage.qml b/src/commoncomponents/ResponsiveImage.qml
index 08d1eb72..6fe7da23 100644
--- a/src/commoncomponents/ResponsiveImage.qml
+++ b/src/commoncomponents/ResponsiveImage.qml
@@ -62,8 +62,8 @@ Image {
function setSourceSize() {
if (isSvg) {
- sourceSize.width = Math.max(24, width)
- sourceSize.height = Math.max(24, height)
+ sourceSize.width = width
+ sourceSize.height = height
} else
sourceSize = undefined
}
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index 97c17cb0..517fa28b 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -397,4 +397,14 @@ Item {
// Generic dialog options
property string optionOk: qsTr("Ok")
property string optionCancel: qsTr("Cancel")
+
+ // Conference moderation
+ property string setModerator: qsTr("Set moderator")
+ property string unsetModerator: qsTr("Unset moderator")
+ property string muteParticipant: qsTr("Mute")
+ property string unmuteParticipant: qsTr("Unmute")
+ property string maximizeParticipant: qsTr("Maximize")
+ property string minimizeParticipant: qsTr("Minimize")
+ property string hangupParticipant: qsTr("Hangup")
}
+
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index 70ce90aa..ae2061e9 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -30,6 +30,8 @@ Item {
// General
property color blackColor: "#000000"
property color whiteColor: "#ffffff"
+ property color darkGreyColor: "#272727"
+ property color darkGreyColorOpacity: "#4D272727" // 77%
property color transparentColor: "transparent"
property color primaryForegroundColor: darkTheme? whiteColor : blackColor
property color primaryBackgroundColor: darkTheme? bgDarkMode_ : whiteColor
@@ -91,6 +93,10 @@ Item {
property color sipInputButtonHoverColor: "#4477aa"
property color sipInputButtonPressColor: "#5588bb"
+ property string buttonConference: "#110000"
+ property string buttonConferenceHovered: "#66cfff"
+ property string buttonConferencePressed: "#66cfff"
+
// Wizard / account manager
property color accountCreationOtherStepColor: "grey"
property color accountCreationCurrentStepColor: "#28b1ed"
@@ -151,6 +157,7 @@ Item {
property int textFontSize: 9
property int settingsFontSize: 9
property int buttonFontSize: 9
+ property int participantFontSize: 10
property int menuFontSize: 12
property int headerFontSize: 13
property int titleFontSize: 16
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index d2c8bae0..621d0f28 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -47,7 +47,8 @@ Rectangle {
recordingRect.visible = isRecording
}
- function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, isRecording, isSIP, isConferenceCall) {
+ function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
+ isRecording, isSIP, isConferenceCall) {
callViewContextMenu.isSIP = isSIP
callViewContextMenu.isPaused = isPaused
callViewContextMenu.isAudioOnly = isAudioOnly
@@ -75,46 +76,116 @@ Rectangle {
MediaHandlerPickerCreation.closeMediaHandlerPicker()
}
+ // returns true if participant is not fully maximized
+ function showMaximize(pX, pY, pW, pH) {
+ // Hack: -1 offset added to avoid problems with odd sizes
+ return (pX - distantRenderer.getXOffset() !== 0
+ || pY - distantRenderer.getYOffset() !== 0
+ || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1)
+ || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1))
+ }
+
+ // returns true if participant takes renderer's width
+ function showMinimize(pX, pW) {
+ return (pX - distantRenderer.getXOffset() === 0
+ && pW >= distantRenderer.width - distantRenderer.getXOffset() * 2 - 1)
+ }
+
+
function handleParticipantsInfo(infos) {
+ // TODO: in the future the conference layout should be entirely managed by the client
videoCallOverlay.updateMenu()
- var isModerator = CallAdapter.isCurrentModerator()
- var isHost = CallAdapter.isCurrentHost()
+ var showMax = false
+ var showMin = false
+
+ var deletedUris = []
+ var currentUris = []
for (var p in participantOverlays) {
- if (participantOverlays[p])
- participantOverlays[p].destroy()
+ if (participantOverlays[p]) {
+ var participant = infos.find(e => e.uri === participantOverlays[p].uri);
+ if (participant) {
+ // Update participant's information
+ var newX = distantRenderer.getXOffset()
+ + participant.x * distantRenderer.getScaledWidth()
+ var newY = distantRenderer.getYOffset()
+ + participant.y * distantRenderer.getScaledHeight()
+ var newWidth = participant.w * distantRenderer.getScaledWidth()
+ var newHeight = participant.h * distantRenderer.getScaledHeight()
+ var newVisible = participant.w !== 0 && participant.h !== 0
+
+ if (participantOverlays[p].x !== newX)
+ participantOverlays[p].x = newX
+ if (participantOverlays[p].y !== newY)
+ participantOverlays[p].y = newY
+ if (participantOverlays[p].width !== newWidth)
+ participantOverlays[p].width = newWidth
+ if (participantOverlays[p].height !== newHeight)
+ participantOverlays[p].height = newHeight
+ if (participantOverlays[p].visible !== newVisible)
+ participantOverlays[p].visible = newVisible
+
+ showMax = showMaximize(participantOverlays[p].x,
+ participantOverlays[p].y,
+ participantOverlays[p].width,
+ participantOverlays[p].height)
+ showMin = showMinimize(participantOverlays[p].x,
+ participantOverlays[p].width)
+
+ participantOverlays[p].setMenu(participant.uri, participant.bestName,
+ participant.isLocal, showMax, showMin)
+ if (participant.videoMuted)
+ participantOverlays[p].setAvatar(participant.avatar)
+ else
+ participantOverlays[p].setAvatar("")
+ currentUris.push(participantOverlays[p].uri)
+ } else {
+ // Participant is no longer in conference
+ deletedUris.push(participantOverlays[p].uri)
+ participantOverlays[p].destroy()
+ }
+ }
}
- participantOverlays = []
- if (infos.length == 0) {
+ participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri))
+
+ if (infos.length === 0) { // Return to normal call
previewRenderer.visible = true
+ for (var part in participantOverlays) {
+ if (participantOverlays[part]) {
+ participantOverlays[part].destroy()
+ }
+ }
+ participantOverlays = []
} else {
previewRenderer.visible = false
for (var infoVariant in infos) {
- var hover = participantComponent.createObject(callOverlayRectMouseArea, {
- x: distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth(),
- y: distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight(),
- width: infos[infoVariant].w * distantRenderer.getScaledWidth(),
- height: infos[infoVariant].h * distantRenderer.getScaledHeight(),
- visible: infos[infoVariant].w != 0 && infos[infoVariant].h != 0
- })
- if (!hover) {
- console.log("Error when creating the hover")
- return
- }
+ // Only create overlay for new participants
+ if (!currentUris.includes(infos[infoVariant].uri)) {
+ var hover = participantComponent.createObject(callOverlayRectMouseArea, {
+ x: distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth(),
+ y: distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight(),
+ width: infos[infoVariant].w * distantRenderer.getScaledWidth(),
+ height: infos[infoVariant].h * distantRenderer.getScaledHeight(),
+ visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0
+ })
+ if (!hover) {
+ console.log("Error when creating the hover")
+ return
+ }
- hover.setParticipantName(infos[infoVariant].bestName)
- hover.active = infos[infoVariant].active;
- hover.isLocal = infos[infoVariant].isLocal;
- hover.setMenuVisible(isModerator)
- hover.setEndCallVisible(isHost)
- hover.uri = infos[infoVariant].uri
- if (infos[infoVariant].videoMuted)
- hover.setAvatar(infos[infoVariant].avatar)
- else
- hover.setAvatar("")
- hover.injectedContextMenu = participantContextMenu
- participantOverlays.push(hover)
+ showMax = showMaximize(hover.x, hover.y, hover.width, hover.height)
+ showMin = showMinimize(hover.x, hover.width)
+
+ hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName,
+ infos[infoVariant].isLocal, showMax, showMin)
+ if (infos[infoVariant].videoMuted)
+ hover.setAvatar(infos[infoVariant].avatar)
+ else
+ hover.setAvatar("")
+ participantOverlays.push(hover)
+ }
}
}
+
}
// x, y position does not need to be translated
@@ -477,8 +548,4 @@ Rectangle {
MediaHandlerPickerCreation.openMediaHandlerPicker()
}
}
-
- ParticipantContextMenu {
- id: participantContextMenu
- }
}
diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml
index f8bfc0e8..84295ba0 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -33,22 +33,23 @@ Rectangle {
// ButtonCounts here is to make sure that flow layout margin is calculated correctly,
// since no other methods can make buttons at the layout center.
property int buttonPreferredSize: 48
- property var isHost: true
+ property var isModerator: true
property var isSip: false
signal chatButtonClicked
signal addToConferenceButtonClicked
function updateMenu() {
- root.isHost = CallAdapter.isCurrentHost()
- addToConferenceButton.visible = !root.isSip && root.isHost
+ root.isModerator = CallAdapter.isCurrentModerator()
+ addToConferenceButton.visible = !root.isSip && root.isModerator
}
- function setButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, isRecording, isSIP, isConferenceCall) {
- root.isHost = CallAdapter.isCurrentModerator()
+ function setButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
+ isRecording, isSIP, isConferenceCall) {
+ root.isModerator = CallAdapter.isCurrentModerator()
root.isSip = isSIP
noVideoButton.visible = !isAudioOnly
- addToConferenceButton.visible = !isSIP && isHost
+ addToConferenceButton.visible = !root.isSIP && root.isModerator
noMicButton.checked = isAudioMuted
noVideoButton.checked = isVideoMuted
@@ -150,7 +151,7 @@ Rectangle {
Layout.preferredWidth: buttonPreferredSize
Layout.preferredHeight: buttonPreferredSize
- visible: !isHost
+ visible: !isModerator
pressedColor: JamiTheme.invertedPressedButtonColor
hoveredColor: JamiTheme.invertedHoveredButtonColor
diff --git a/src/mainview/components/ParticipantContextMenu.qml b/src/mainview/components/ParticipantContextMenu.qml
deleted file mode 100644
index 5c9f4985..00000000
--- a/src/mainview/components/ParticipantContextMenu.qml
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Sébastien Blin
- * Author: Mingrui Zhang
- *
- * 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 .
- */
-
-import QtQuick 2.14
-import QtQuick.Controls 2.14
-import QtGraphicalEffects 1.14
-import net.jami.Models 1.0
-import net.jami.Constants 1.0
-
-import "../../commoncomponents"
-import "../../commoncomponents/js/contextmenugenerator.js" as ContextMenuGenerator
-
-Item {
- id: root
-
- property var uri: ""
- property var maximized: true
- property var active: true
- property var showHangup: false
- property var showMaximize: false
- property var showMinimize: false
- property var showSetModerator: false
- property var showUnsetModerator: false
- property var showMute: false
- property var showUnmute: false
-
- function openMenu(){
- ContextMenuGenerator.initMenu()
- if (showHangup)
- ContextMenuGenerator.addMenuItem(JamiStrings.hangup,
- "qrc:/images/icons/ic_call_end_white_24px.svg",
- function (){
- CallAdapter.hangupCall(uri)
- })
-
- if (showMaximize)
- ContextMenuGenerator.addMenuItem(qsTr("Maximize participant"),
- "qrc:/images/icons/open_in_full-24px.svg",
- function (){
- CallAdapter.maximizeParticipant(uri, active)
- })
- if (showMinimize)
- ContextMenuGenerator.addMenuItem(qsTr("Minimize participant"),
- "qrc:/images/icons/close_fullscreen-24px.svg",
- function (){
- CallAdapter.minimizeParticipant()
- })
-
- if (showSetModerator)
- ContextMenuGenerator.addMenuItem(qsTr("Set moderator"),
- "qrc:/images/icons/person_add-24px.svg",
- function (){
- CallAdapter.setModerator(uri, true)
- })
-
- if (showUnsetModerator)
- ContextMenuGenerator.addMenuItem(qsTr("Unset moderator"),
- "qrc:/images/icons/round-close-24px.svg",
- function (){
- CallAdapter.setModerator(uri, false)
- })
-
- if (showMute)
- ContextMenuGenerator.addMenuItem(qsTr("Mute participant"),
- "qrc:/images/icons/mic_off-24px.svg",
- function (){
- CallAdapter.muteParticipant(uri, true)
- })
-
- if (showUnmute)
- ContextMenuGenerator.addMenuItem(qsTr("Unmute participant"),
- "qrc:/images/icons/mic-24px.svg",
- function (){
- CallAdapter.muteParticipant(uri, false)
- })
-
-
- root.height = ContextMenuGenerator.getMenu().height
- root.width = ContextMenuGenerator.getMenu().width
- ContextMenuGenerator.getMenu().open()
- }
-
- Component.onCompleted: {
- ContextMenuGenerator.createBaseContextMenuObjects(root)
- }
-}
-
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
index 38e19e75..a28f630d 100644
--- a/src/mainview/components/ParticipantOverlay.qml
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Sébastien Blin
+ * Author: Albert Babí
*
* 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
@@ -19,6 +20,7 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
+import QtQuick.Shapes 1.14
import QtQuick.Controls.Universal 2.14
import QtGraphicalEffects 1.14
import net.jami.Models 1.0
@@ -29,190 +31,215 @@ import "../../commoncomponents"
Rectangle {
id: root
- property int buttonPreferredSize: 12
- property var uri: ""
- property var active: true
- property var isLocal: true
- property var showEndCall: true
- property var injectedContextMenu: null
+ // svg path for the background participant shape (width is offset dependant)
+ property int offset: indicatorsRowLayout.width
+ property int shapeHeight: 16
+ property string pathShape: "M 0.0,%8
+ C 0.0,%8 %1,%8 %1,%8 %2,%8 %3,%9 %4,10.0 %5,5.0 %5,0.0 %6,0.0 %7,0.0 %4,0.0
+ 0.0,0.0 0.0,0.0 0.0,%8 0.0,%8 Z".arg(offset).arg(4.0+offset).arg(7+offset)
+ .arg(9+offset).arg(11+offset).arg(15+offset).arg(18+offset).arg(shapeHeight)
+ .arg(shapeHeight-2)
- function setParticipantName(name) {
- participantName.text = name
- }
+ // TODO: properties should be
+ property string uri: overlayMenu.uri
+ property bool participantIsModerator: false
+ property bool participantIsMuted: false
// TODO: try to use AvatarImage as well
function setAvatar(avatar) {
if (avatar === "") {
- opacity = 0
contactImage.source = ""
} else {
- opacity = 1
contactImage.source = "data:image/png;base64," + avatar
}
}
- function setMenuVisible(isVisible) {
- optionsButton.visible = isVisible
+ function setMenu(newUri, bestName, isLocal, showMax, showMin) {
+
+ overlayMenu.uri = newUri
+ overlayMenu.bestName = bestName
+
+ var isHost = CallAdapter.isCurrentHost()
+ var isModerator = CallAdapter.isCurrentModerator()
+ var participantIsHost = CallAdapter.participantIsHost(overlayMenu.uri)
+ participantIsModerator = CallAdapter.isModerator(overlayMenu.uri)
+ overlayMenu.showSetModerator = isHost && !isLocal && !participantIsModerator
+ overlayMenu.showUnsetModerator = isHost && !isLocal && participantIsModerator
+
+ participantIsMuted = CallAdapter.isMuted(overlayMenu.uri)
+ overlayMenu.showMute = isModerator && !participantIsMuted
+ overlayMenu.showUnmute = isModerator && participantIsMuted && isLocal
+ overlayMenu.showMaximize = isModerator && showMax
+ overlayMenu.showMinimize = isModerator && showMin
+ overlayMenu.showHangup = isModerator && !isLocal && !participantIsHost
}
- function setEndCallVisible(isVisible) {
- showEndCall = isVisible
- }
-
- border.width: 1
- opacity: 0
color: "transparent"
z: 1
- MouseArea {
- id: mouseAreaHover
- anchors.fill: parent
- hoverEnabled: true
- propagateComposedEvents: true
- acceptedButtons: Qt.LeftButton
+ // Participant header with moderator / mute indicators
+ Rectangle {
+ id: participantIndicators
+ width: indicatorsRowLayout.width
+ height: shapeHeight
+ visible: participantIsModerator || participantIsMuted
+ color: "transparent"
- Image {
- id: contactImage
-
- anchors.centerIn: parent
-
- height: Math.min(parent.width / 2, parent.height / 2)
- width: Math.min(parent.width / 2, parent.height / 2)
-
- fillMode: Image.PreserveAspectFit
- source: ""
- asynchronous: true
-
- layer.enabled: true
- layer.effect: OpacityMask {
- maskSource: Rectangle{
- width: contactImage.width
- height: contactImage.height
- radius: {
- var size = ((contactImage.width <= contactImage.height)? contactImage.width:contactImage.height)
- return size /2
- }
- }
+ Shape {
+ id: myShape
+ ShapePath {
+ id: backgroundShape
+ strokeColor: "transparent"
+ fillColor: JamiTheme.darkGreyColorOpacity
+ capStyle: ShapePath.RoundCap
+ PathSvg { path: pathShape }
}
}
RowLayout {
- id: bottomLabel
+ id: indicatorsRowLayout
+ height: parent.height
+ anchors.verticalCenter: parent.verticalCenter
- height: 24
- width: parent.width
- anchors.bottom: parent.bottom
+ ResponsiveImage {
+ id: isModeratorIndicator
- Rectangle {
- color: "black"
- opacity: 0.8
- height: parent.height
- width: parent.width
- Layout.fillWidth: true
- Layout.preferredHeight: parent.height
+ visible: participantIsModerator
- Text {
- id: participantName
- anchors.fill: parent
- leftPadding: 8.0
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: 6
+ containerHeight: 12
+ containerWidth: 12
- TextMetrics {
- id: participantMetrics
- elide: Text.ElideRight
- elideWidth: bottomLabel.width - 8
- }
-
- text: participantMetrics.elidedText
-
- color: "white"
- font.pointSize: JamiTheme.textFontSize
- horizontalAlignment: Text.AlignLeft
- verticalAlignment: Text.AlignVCenter
- }
-
- Button {
- id: optionsButton
-
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
-
- background: Rectangle {
- color: "transparent"
- }
-
-
- icon.color: "white"
- icon.height: buttonPreferredSize
- icon.width: buttonPreferredSize
- icon.source: "qrc:/images/icons/more_vert-24px.svg"
-
- onClicked: {
- if (!injectedContextMenu) {
- console.log("Participant's overlay don't have any injected context menu")
- return
- }
- var mousePos = mapToItem(videoCallPageRect, parent.x, parent.y)
- var layout = CallAdapter.getCurrentLayoutType()
- var showMaximized = layout !== 2
- var showMinimized = !(layout === 0 || (layout === 1 && !active))
- var isModerator = CallAdapter.isModerator(uri)
- var isHost = CallAdapter.isCurrentHost()
- var participantIsHost = CallAdapter.participantIsHost(uri)
- var isMuted = CallAdapter.isMuted(uri)
- injectedContextMenu.showHangup = !root.isLocal && showEndCall
- injectedContextMenu.showMaximize = showMaximized
- injectedContextMenu.showMinimize = showMinimized
- injectedContextMenu.uri = uri
- injectedContextMenu.active = active
- injectedContextMenu.x = mousePos.x
- injectedContextMenu.y = mousePos.y - injectedContextMenu.height
- injectedContextMenu.showSetModerator = (isHost && !participantIsHost && !isModerator)
- injectedContextMenu.showUnsetModerator = (isHost && !participantIsHost && isModerator)
- injectedContextMenu.showMute = !isMuted
- injectedContextMenu.showUnmute = isMuted && root.isLocal
- injectedContextMenu.openMenu()
- }
+ source: "qrc:/images/icons/moderator.svg"
+ layer {
+ enabled: true
+ effect: ColorOverlay { color: JamiTheme.whiteColor }
+ mipmap: false
+ smooth: true
}
}
- }
- onClicked: {
- CallAdapter.maximizeParticipant(uri, active)
- }
+ ResponsiveImage {
+ id: isMutedIndicator
- onEntered: {
- if (contactImage.status === Image.Null)
- root.state = "entered"
- }
+ visible: participantIsMuted
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: 6
+ containerHeight: 12
+ containerWidth: 12
- onExited: {
- if (contactImage.status === Image.Null)
- root.state = "exited"
+ source: "qrc:/images/icons/mic_off-24px.svg"
+ layer {
+ enabled: true
+ effect: ColorOverlay { color: JamiTheme.whiteColor }
+ mipmap: false
+ smooth: true
+ }
+ }
}
}
- states: [
- State {
- name: "entered"
- PropertyChanges {
- target: root
- opacity: 1
+ // Participant background, mousearea, hover and buttons for moderation
+ Rectangle {
+ id: participantRect
+
+ anchors.fill: parent
+ opacity: 0
+ color: JamiTheme.darkGreyColorOpacity
+ z: 1
+
+ MouseArea {
+ id: mouseAreaHover
+
+ anchors.fill: parent
+ hoverEnabled: true
+ propagateComposedEvents: false
+ acceptedButtons: Qt.LeftButton
+
+ Image {
+ id: contactImage
+
+ anchors.centerIn: parent
+ height: Math.min(parent.width / 2, parent.height / 2)
+ width: Math.min(parent.width / 2, parent.height / 2)
+
+ fillMode: Image.PreserveAspectFit
+ source: ""
+ asynchronous: true
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle{
+ width: contactImage.width
+ height: contactImage.height
+ radius: {
+ var size = ((contactImage.width <= contactImage.height)?
+ contactImage.width : contactImage.height)
+ return size / 2
+ }
+ }
+ }
+ layer.mipmap: false
+ layer.smooth: true
}
- },
- State {
- name: "exited"
- PropertyChanges {
- target: root
- opacity: 0
+
+ ParticipantOverlayMenu {
+ id: overlayMenu
+ visible: participantRect.opacity !== 0
+ anchors.centerIn: parent
+ hasMinimumSize: root.width > minimumWidth && root.height > minimumHeight
+
+ onMouseAreaExited: {
+ if (contactImage.status === Image.Null) {
+ root.z = 1
+ participantRect.state = "exited"
+ }
+ }
+ }
+
+ onClicked: {
+ CallAdapter.maximizeParticipant(uri)
+ }
+
+ onEntered: {
+ if (contactImage.status === Image.Null) {
+ root.z = 2
+ participantRect.state = "entered"
+ }
+ }
+
+ onExited: {
+ if (contactImage.status === Image.Null) {
+ root.z = 1
+ participantRect.state = "exited"
+ }
}
}
- ]
- transitions: Transition {
- PropertyAnimation {
- target: root
- property: "opacity"
- duration: 500
+ states: [
+ State {
+ name: "entered"
+ PropertyChanges {
+ target: participantRect
+ opacity: 1
+ }
+ },
+ State {
+ name: "exited"
+ PropertyChanges {
+ target: participantRect
+ opacity: 0
+ }
+ }
+ ]
+
+ transitions: Transition {
+ PropertyAnimation {
+ target: participantRect
+ property: "opacity"
+ duration: 500
+ }
}
}
}
diff --git a/src/mainview/components/ParticipantOverlayMenu.qml b/src/mainview/components/ParticipantOverlayMenu.qml
new file mode 100644
index 00000000..3383abb3
--- /dev/null
+++ b/src/mainview/components/ParticipantOverlayMenu.qml
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Albert Babí
+ *
+ * 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 .
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtGraphicalEffects 1.14
+import QtQuick.Layouts 1.14
+import net.jami.Models 1.0
+import net.jami.Constants 1.0
+
+import "../../commoncomponents"
+
+// Overlay menu for conference moderation
+Rectangle {
+ id: root
+
+ property bool hasMinimumSize: true
+ property int buttonPreferredSize: 30
+ property int minimumWidth: Math.max(114, visibleButtons * 37 + 21 * 2)
+ property int minimumHeight: 114
+ property int visibleButtons: toggleModerator.visible
+ + toggleMute.visible
+ + maximizeParticipant.visible
+ + minimizeParticipant.visible
+ + hangupParticipant.visible
+
+ property string uri: ""
+ property string bestName: ""
+ property bool showSetModerator: false
+ property bool showUnsetModerator: false
+ property bool showMute: false
+ property bool showUnmute: false
+ property bool showMaximize: false
+ property bool showMinimize: false
+ property bool showHangup: false
+
+ signal mouseAreaExited
+
+ // values taken from sketch
+ width: hasMinimumSize? parent.width : minimumWidth
+ height: hasMinimumSize? parent.height: minimumHeight
+
+ color: hasMinimumSize? "transparent" : JamiTheme.darkGreyColorOpacity
+ radius: 10
+
+ MouseArea {
+ id: mouseAreaHover
+
+ anchors.fill: parent
+ hoverEnabled: true
+ propagateComposedEvents: true
+ acceptedButtons: Qt.LeftButton
+
+ onExited: mouseAreaExited()
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 8
+
+ Text {
+ id: participantName
+
+ TextMetrics {
+ id: participantMetrics
+ text: bestName
+ elide: Text.ElideRight
+ elideWidth: root.width - JamiTheme.preferredMarginSize * 2
+ }
+
+ text: participantMetrics.elidedText
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.participantFontSize
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ RowLayout {
+ id: rowLayoutButtons
+
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+ Layout.fillWidth: true
+ spacing: 7
+
+ PushButton {
+ id: toggleModerator
+
+ visible: (showSetModerator || showUnsetModerator)
+ Layout.preferredWidth: buttonPreferredSize
+ Layout.preferredHeight: buttonPreferredSize
+ preferredSize: 16
+ normalColor: JamiTheme.buttonConference
+ hoveredColor: JamiTheme.buttonConferenceHovered
+ pressedColor: JamiTheme.buttonConferencePressed
+
+ source: "qrc:/images/icons/moderator.svg"
+ imageColor: hovered? JamiTheme.darkGreyColor
+ : JamiTheme.whiteColor
+
+ onClicked: CallAdapter.setModerator(uri, showSetModerator)
+ onHoveredChanged: toggleModeratorToolTip.visible = hovered
+
+ Text {
+ id: toggleModeratorToolTip
+
+ visible: false
+ width: parent.width
+ text: showSetModerator? JamiStrings.setModerator
+ : JamiStrings.unsetModerator
+ horizontalAlignment: Text.AlignHCenter
+ anchors.top: parent.bottom
+ anchors.topMargin: 6
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.tinyFontSize
+ }
+ }
+
+ PushButton {
+ id: toggleMute
+
+ visible: showMute || showUnmute
+ Layout.preferredWidth: buttonPreferredSize
+ Layout.preferredHeight: buttonPreferredSize
+ preferredSize: 16
+
+ normalColor: JamiTheme.buttonConference
+ hoveredColor: JamiTheme.buttonConferenceHovered
+ pressedColor: JamiTheme.buttonConferencePressed
+
+ source: showMute? "qrc:/images/icons/mic-24px.svg"
+ : "qrc:/images/icons/mic_off-24px.svg"
+ imageColor: hovered? JamiTheme.darkGreyColor
+ : JamiTheme.whiteColor
+
+ onClicked: CallAdapter.muteParticipant(uri, showMute)
+ onHoveredChanged: toggleParticipantToolTip.visible = hovered
+
+ Text {
+ id: toggleParticipantToolTip
+
+ visible: false
+ width: parent.width
+ text: showMute? JamiStrings.muteParticipant
+ : JamiStrings.unmuteParticipant
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignTop
+
+ anchors.top: parent.bottom
+ anchors.topMargin: 6
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.tinyFontSize
+ }
+ }
+
+ PushButton {
+ id: maximizeParticipant
+
+ visible: showMaximize
+ Layout.preferredWidth: buttonPreferredSize
+ Layout.preferredHeight: buttonPreferredSize
+ preferredSize: 16
+
+ normalColor: JamiTheme.buttonConference
+ hoveredColor: JamiTheme.buttonConferenceHovered
+ pressedColor: JamiTheme.buttonConferencePressed
+
+ source: "qrc:/images/icons/open_in_full-24px.svg"
+ imageColor: hovered? JamiTheme.darkGreyColor
+ : JamiTheme.whiteColor
+
+ onClicked: CallAdapter.maximizeParticipant(uri)
+ onHoveredChanged: maximizeParticipantToolTip.visible = hovered
+
+ Text {
+ id: maximizeParticipantToolTip
+
+ visible: false
+ width: parent.width
+ text: JamiStrings.maximizeParticipant
+ horizontalAlignment: Text.AlignHCenter
+ anchors.top: parent.bottom
+ anchors.topMargin: 6
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.tinyFontSize
+ }
+ }
+
+ PushButton {
+ id: minimizeParticipant
+
+ visible: showMinimize
+ Layout.preferredWidth: buttonPreferredSize
+ Layout.preferredHeight: buttonPreferredSize
+ preferredSize: 16
+
+ normalColor: JamiTheme.buttonConference
+ hoveredColor: JamiTheme.buttonConferenceHovered
+ pressedColor: JamiTheme.buttonConferencePressed
+
+ source: "qrc:/images/icons/close_fullscreen-24px.svg"
+ imageColor: hovered? JamiTheme.darkGreyColor
+ : JamiTheme.whiteColor
+ onClicked: CallAdapter.minimizeParticipant(uri)
+ onHoveredChanged: minimizeParticipantToolTip.visible = hovered
+
+ Text {
+ id: minimizeParticipantToolTip
+
+ visible: false
+ width: parent.width
+ text: JamiStrings.minimizeParticipant
+ horizontalAlignment: Text.AlignHCenter
+ anchors.top: parent.bottom
+ anchors.topMargin: 6
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.tinyFontSize
+ }
+ }
+
+ PushButton {
+ id: hangupParticipant
+
+ visible: showHangup
+ Layout.preferredWidth: buttonPreferredSize
+ Layout.preferredHeight: buttonPreferredSize
+ preferredSize: 16
+
+ normalColor: JamiTheme.buttonConference
+ hoveredColor: JamiTheme.buttonConferenceHovered
+ pressedColor: JamiTheme.buttonConferencePressed
+
+ source: "qrc:/images/icons/ic_block_24px.svg"
+ imageColor: hovered? JamiTheme.darkGreyColor
+ : JamiTheme.whiteColor
+ onClicked: CallAdapter.hangupCall(uri)
+ onHoveredChanged: hangupParticipantToolTip.visible = hovered
+
+ Text {
+ id: hangupParticipantToolTip
+
+ visible: false
+ width: parent.width
+ text: JamiStrings.hangupParticipant
+ horizontalAlignment: Text.AlignHCenter
+ anchors.top: parent.bottom
+ anchors.topMargin: 6
+ color: JamiTheme.whiteColor
+ font.pointSize: JamiTheme.tinyFontSize
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/mainview/components/VideoCallPage.qml b/src/mainview/components/VideoCallPage.qml
index 4befacc2..45ad4d5b 100644
--- a/src/mainview/components/VideoCallPage.qml
+++ b/src/mainview/components/VideoCallPage.qml
@@ -191,6 +191,7 @@ Rectangle {
isRecording, isSIP,
isConferenceCall)
videoCallPageRect.bestName = bestName
+ videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
}
function onShowOnHoldLabel(isPaused) {