1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-24 09:25:33 +02:00

conference: change UI for moderator

- set UI according to sketch prototype
- add moderation overlay menu instead of context menu
- only update participants overlay when necessary
- avoid minimum size for ResponsiveImage

Gitlab: #207, #208

Change-Id: I65c9932319e55840518cbb0ce3cfa1a46e2275f0
This commit is contained in:
ababi 2020-11-20 12:14:24 +01:00 committed by Albert Babí Oller
parent 18abbce09d
commit 0fa4fe6fe4
14 changed files with 646 additions and 365 deletions

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Moderateur</title>
<g id="Icones_Outline" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Moderateur" fill-rule="nonzero" stroke="#000000" stroke-width="1.3">
<path d="M12.0856077,6.17414866 L15.2188905,10.399333 L21.4126996,6.23175137 L21.4126996,18.3907977 L2.73757526,18.3907977 L2.73757526,6.23175137 L8.92952352,10.3980809 L12.0856077,6.17414866 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 643 B

View file

@ -102,7 +102,6 @@
<file>src/commoncomponents/GeneralMenuItem.qml</file>
<file>src/mainview/components/ConversationSmartListContextMenu.qml</file>
<file>src/mainview/components/CallViewContextMenu.qml</file>
<file>src/mainview/components/ParticipantContextMenu.qml</file>
<file>src/commoncomponents/GeneralMenuSeparator.qml</file>
<file>src/mainview/components/UserProfile.qml</file>
<file>src/mainview/js/videodevicecontextmenuitemcreation.js</file>
@ -137,5 +136,6 @@
<file>src/commoncomponents/ResponsiveImage.qml</file>
<file>src/commoncomponents/PresenceIndicator.qml</file>
<file>src/commoncomponents/AvatarImage.qml</file>
<file>src/mainview/components/ParticipantOverlayMenu.qml</file>
</qresource>
</RCC>

View file

@ -135,5 +135,6 @@
<file>images/icons/settings_backup_restore-24px.svg</file>
<file>images/logo-jami-standard-coul.svg</file>
<file>images/logo-jami-standard-coul-white.svg</file>
<file>images/icons/moderator.svg</file>
</qresource>
</RCC>

View file

@ -399,6 +399,7 @@ CallAdapter::connectCallModel(const QString& accountId)
const auto convInfo = LRCInstance::getConversationFromCallId(callId);
if (!convInfo.uid.isEmpty()) {
emit callStatusChanged(static_cast<int>(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<int>(call.layout);
} catch (...) {
}
}
return -1;
}
void
CallAdapter::holdThisCallToggle()
{

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,103 +0,0 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Sébastien Blin <sebastien.blin@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
* 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 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)
}
}

View file

@ -1,6 +1,7 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
* Author: Albert Babí <albert.babi@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
@ -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
}
}
}
}

View file

@ -0,0 +1,271 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Albert Babí <albert.babi@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 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
}
}
}
}
}
}

View file

@ -191,6 +191,7 @@ Rectangle {
isRecording, isSIP,
isConferenceCall)
videoCallPageRect.bestName = bestName
videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos())
}
function onShowOnHoldLabel(isPaused) {