Commit 74503503 authored by Albert  Babí Oller's avatar Albert Babí Oller

settings: add support for default and local moderators

- add configurable list of default moderators
- add option for enabling local moderators
- indicate when participant is locally muted on moderation overlay
- small typo fixes

Change-Id: I1669c903be3c3a3f2344f1d95d8b618e62b9d412
parent 4a7c8591
......@@ -138,6 +138,7 @@ unix {
# Input
HEADERS += \
src/avatarimageprovider.h \
src/moderatorlistmodel.h \
src/networkmanager.h \
src/smartlistmodel.h \
src/updatemanager.h \
......@@ -187,6 +188,7 @@ HEADERS += \
SOURCES += \
src/bannedlistmodel.cpp \
src/accountlistmodel.cpp \
src/moderatorlistmodel.cpp \
src/networkmanager.cpp \
src/runguard.cpp \
src/updatemanager.cpp \
......
......@@ -47,7 +47,7 @@
<file>src/settingsview/components/PluginItemDelegate.qml</file>
<file>src/mainview/components/MediaHandlerItemDelegate.qml</file>
<file>src/commoncomponents/PreferenceItemDelegate.qml</file>
<file>src/settingsview/components/BannedItemDelegate.qml</file>
<file>src/settingsview/components/ContactItemDelegate.qml</file>
<file>src/settingsview/components/MediaCodecDelegate.qml</file>
<file>src/settingsview/components/NameRegistrationDialog.qml</file>
<file>src/settingsview/components/LinkDeviceDialog.qml</file>
......@@ -92,7 +92,6 @@
<file>src/mainview/js/incomingcallpagecreation.js</file>
<file>src/mainview/components/ContactSearchBar.qml</file>
<file>src/mainview/components/VideoCallPage.qml</file>
<file>src/mainview/components/CallAdvancedOptions.qml</file>
<file>src/mainview/components/ParticipantOverlay.qml</file>
<file>src/mainview/components/ProjectCreditsScrollView.qml</file>
<file>src/mainview/components/AccountComboBoxPopup.qml</file>
......
......@@ -356,18 +356,9 @@ AccountAdapter::connectAccount(const QString& accountId)
QObject::disconnect(accountStatusChangedConnection_);
QObject::disconnect(contactAddedConnection_);
QObject::disconnect(accountProfileChangedConnection_);
QObject::disconnect(addedToConferenceConnection_);
QObject::disconnect(contactUnbannedConnection_);
accountProfileChangedConnection_
= QObject::connect(&LRCInstance::accountModel(),
&lrc::api::NewAccountModel::profileUpdated,
[this](const QString& accountId) {
if (LRCInstance::getCurrAccId() == accountId)
emit accountStatusChanged(accountId);
});
accountStatusChangedConnection_
= QObject::connect(accInfo.accountModel,
&lrc::api::NewAccountModel::accountStatusChanged,
......
......@@ -108,6 +108,7 @@ signals:
* Trigger other components to reconnect account related signals.
*/
void accountStatusChanged(QString accountId = {});
void updateConversationForAddedContact();
/*
* send report failure to QML to make it show the right UI state .
......@@ -144,7 +145,6 @@ private:
QMetaObject::Connection accountStatusChangedConnection_;
QMetaObject::Connection contactAddedConnection_;
QMetaObject::Connection addedToConferenceConnection_;
QMetaObject::Connection accountProfileChangedConnection_;
QMetaObject::Connection contactUnbannedConnection_;
QMetaObject::Connection registeredNameSavedConnection_;
};
......
......@@ -30,7 +30,6 @@
class AudioOutputDeviceModel : public QAbstractListModel
{
Q_OBJECT
public:
public:
enum Role { Device_ID = Qt::UserRole + 1, ID_UTF8 };
Q_ENUM(Role)
......
......@@ -24,8 +24,6 @@
class BannedListModel : public QAbstractListModel
{
Q_OBJECT
BannedListModel(const BannedListModel& cpy);
public:
enum Role { ContactName = Qt::UserRole + 1, ContactID };
Q_ENUM(Role)
......
......@@ -413,6 +413,14 @@ Item {
property string maximizeParticipant: qsTr("Maximize")
property string minimizeParticipant: qsTr("Minimize")
property string hangupParticipant: qsTr("Hangup")
property string localMuted: qsTr("local muted")
// Settings moderation
property string conferenceModeration: qsTr("Conference moderation")
property string defaultModerators: qsTr("Default moderators")
property string enableLocalModerators: qsTr("Enable local moderators")
property string addDefaultModerator: qsTr("Add default moderator")
property string removeDefaultModerator: qsTr("Remove default moderator")
// Daemon reconnection
property string reconnectDaemon: qsTr("Trying to reconnect to the Jami daemon (dring)…")
......
......@@ -35,15 +35,31 @@ ContactAdapter::getContactSelectableModel(int type)
* Called from qml every time contact picker refreshes.
*/
listModeltype_ = static_cast<SmartListModel::Type>(type);
smartListModel_.reset(new SmartListModel(this, listModeltype_));
if (listModeltype_ == SmartListModel::Type::CONVERSATION) {
defaultModerators_ =
LRCInstance::accountModel().getDefaultModerators(LRCInstance::getCurrAccId());
smartListModel_.reset(new SmartListModel(this, listModeltype_));
smartListModel_->fillConversationsList();
} else {
smartListModel_.reset(new SmartListModel(this, listModeltype_));
}
selectableProxyModel_->setSourceModel(smartListModel_.get());
/*
* Adjust filter.
*/
switch (listModeltype_) {
case SmartListModel::Type::CONVERSATION:
selectableProxyModel_->setPredicate([this]
(const QModelIndex& index, const QRegExp&) {
return !defaultModerators_.contains(index.data(SmartListModel::URI).toString());
});
break;
case SmartListModel::Type::CONFERENCE:
selectableProxyModel_->setPredicate([this](const QModelIndex& index, const QRegExp&) {
selectableProxyModel_->setPredicate([](const QModelIndex& index, const QRegExp&) {
return index.data(SmartListModel::Presence).toBool();
});
break;
......@@ -77,9 +93,16 @@ ContactAdapter::setSearchFilter(const QString& filter)
{
if (listModeltype_ == SmartListModel::Type::CONFERENCE) {
smartListModel_->setConferenceableFilter(filter);
} else if (listModeltype_ == SmartListModel::Type::CONVERSATION) {
selectableProxyModel_->setPredicate([this, filter](
const QModelIndex& index,
const QRegExp&) {
return (!defaultModerators_.contains(index.data(SmartListModel::URI).toString())
&& index.data(SmartListModel::DisplayName).toString().contains(filter));
});
}
selectableProxyModel_->setFilterRegExp(
QRegExp(filter, Qt::CaseInsensitive, QRegExp::FixedString));
QRegExp(filter, Qt::CaseInsensitive, QRegExp::FixedString));
}
void
......@@ -157,6 +180,17 @@ ContactAdapter::contactSelected(int index)
callModel->hangUp(destCallId);
}
} break;
case SmartListModel::Type::CONVERSATION: {
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
if (contactUri.isEmpty()) {
return;
}
LRCInstance::accountModel().setDefaultModerator(
LRCInstance::getCurrAccId(), contactUri, true);
emit defaultModeratorsUpdated();
} break;
default:
break;
}
......
......@@ -95,4 +95,10 @@ private:
*/
std::unique_ptr<SmartListModel> smartListModel_;
std::unique_ptr<SelectableProxyModel> selectableProxyModel_;
QStringList defaultModerators_;
signals:
void defaultModeratorsUpdated();
};
......@@ -19,7 +19,7 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import net.jami.Models 1.0 //Albert
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.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 QtQuick.Layouts 1.14
import QtQuick.Controls.Universal 2.14
import net.jami.Models 1.0
import net.jami.Constants 1.0
import "../../commoncomponents"
Popup {
id: contactPickerPopup
property int type: ContactPicker.ContactPickerType.JAMICONFERENCE
// Important to keep it one, since enum in c++ starts at one for conferences.
enum ContactPickerType {
JAMICONFERENCE = 1,
SIPTRANSFER
}
contentWidth: 250
contentHeight: contactPickerPopupRectColumnLayout.height + 50
padding: 0
modal: true
contentItem: Rectangle {
id: contactPickerPopupRect
width: 250
PushButton {
id: closeButton
anchors.top: contactPickerPopupRect.top
anchors.topMargin: 5
anchors.right: contactPickerPopupRect.right
anchors.rightMargin: 5
source: "qrc:/images/icons/ic_close_black_24dp.png"
onClicked: {
contactPickerPopup.close()
}
}
ColumnLayout {
id: contactPickerPopupRectColumnLayout
anchors.top: contactPickerPopupRect.top
anchors.topMargin: 15
Text {
id: contactPickerTitle
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: contactPickerPopupRect.width
Layout.preferredHeight: 30
font.pointSize: JamiTheme.textFontSize
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: type === ContactPicker.ContactPickerType.JAMICONFERENCE ? qsTr("Add to conference") : qsTr("Transfer this call")
}
ContactSearchBar {
id: contactPickerContactSearchBar
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.preferredWidth: contactPickerPopupRect.width - 10
Layout.preferredHeight: 35
onContactSearchBarTextChanged: {
ContactAdapter.setSearchFilter(text)
}
Component.onCompleted: {
contactPickerContactSearchBar.setPlaceholderString(
qsTr("Search contacts"))
}
}
ListView {
id: contactPickerListView
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: contactPickerPopupRect.width
Layout.preferredHeight: 200
model: ContactAdapter.getContactSelectableModel(type)
clip: true
delegate: ContactPickerItemDelegate {
id: contactPickerItemDelegate
}
ScrollIndicator.vertical: ScrollIndicator {}
}
}
radius: 10
color: "white"
}
onAboutToShow: {
// Reset the model on each show.
contactPickerListView.model = ContactAdapter.getContactSelectableModel(
type)
}
background: Rectangle {
color: "transparent"
}
}
......@@ -440,8 +440,6 @@ Rectangle {
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.JAMICONFERENCE,
callOverlayRect)
ContactPickerCreation.calculateCurrentGeo(
callOverlayRect.width / 2, callOverlayRect.height / 2)
ContactPickerCreation.openContactPicker()
}
......@@ -551,20 +549,6 @@ Rectangle {
color: "transparent"
onWidthChanged: {
ContactPickerCreation.calculateCurrentGeo(callOverlayRect.width / 2,
callOverlayRect.height / 2)
MediaHandlerPickerCreation.calculateCurrentGeo(callOverlayRect.width / 2,
callOverlayRect.height / 2)
}
onHeightChanged: {
ContactPickerCreation.calculateCurrentGeo(callOverlayRect.width / 2,
callOverlayRect.height / 2)
MediaHandlerPickerCreation.calculateCurrentGeo(callOverlayRect.width / 2,
callOverlayRect.height / 2)
}
CallViewContextMenu {
id: callViewContextMenu
......@@ -573,16 +557,12 @@ Rectangle {
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.SIPTRANSFER,
callOverlayRect)
ContactPickerCreation.calculateCurrentGeo(
callOverlayRect.width / 2, callOverlayRect.height / 2)
ContactPickerCreation.openContactPicker()
}
onPluginItemClicked: {
// Create media handler picker - PLUGINS
MediaHandlerPickerCreation.createMediaHandlerPickerObjects(callOverlayRect)
MediaHandlerPickerCreation.calculateCurrentGeo(
callOverlayRect.width / 2, callOverlayRect.height / 2)
MediaHandlerPickerCreation.openMediaHandlerPicker()
}
}
......
......@@ -33,7 +33,8 @@ Popup {
// Important to keep it one, since enum in c++ starts at one for conferences.
enum ContactPickerType {
JAMICONFERENCE = 1,
CONVERSATION = 0,
JAMICONFERENCE,
SIPTRANSFER
}
......@@ -84,7 +85,16 @@ Popup {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: type === ContactPicker.ContactPickerType.JAMICONFERENCE ? qsTr("Add to conference") : qsTr("Transfer this call")
text: {
switch(type) {
case ContactPicker.ContactPickerType.JAMICONFERENCE:
return qsTr("Add to conference")
case ContactPicker.ContactPickerType.SIPTRANSFER:
return qsTr("Transfer this call")
default:
return qsTr("Add default moderator")
}
}
}
ContactSearchBar {
......@@ -127,9 +137,8 @@ Popup {
}
onAboutToShow: {
// Reset the model on each show.
contactPickerListView.model = ContactAdapter.getContactSelectableModel(
type)
contactPickerListView.model =
ContactAdapter.getContactSelectableModel(type)
}
background: Rectangle {
......
......@@ -70,12 +70,12 @@ Rectangle {
overlayMenu.showUnsetModerator = isHost && !isLocal && participantIsModerator
var muteState = CallAdapter.getMuteState(overlayMenu.uri)
var isLocalMuted = muteState === CallAdapter.LOCAL_MUTED
overlayMenu.isLocalMuted = muteState === CallAdapter.LOCAL_MUTED
|| muteState === CallAdapter.BOTH_MUTED
var isModeratorMuted = muteState === CallAdapter.MODERATOR_MUTED
|| muteState === CallAdapter.BOTH_MUTED
participantIsMuted = isLocalMuted || isModeratorMuted
participantIsMuted = overlayMenu.isLocalMuted || isModeratorMuted
overlayMenu.showModeratorMute = isModerator && !isModeratorMuted
overlayMenu.showModeratorUnmute = isModerator && isModeratorMuted
......
......@@ -41,6 +41,7 @@ Rectangle {
property string uri: ""
property string bestName: ""
property bool isLocalMuted: false
property bool showSetModerator: false
property bool showUnsetModerator: false
property bool showModeratorMute: false
......@@ -150,7 +151,10 @@ Rectangle {
: JamiTheme.whiteColor
onClicked: CallAdapter.muteParticipant(uri, showModeratorMute)
onHoveredChanged: toggleParticipantToolTip.visible = hovered
onHoveredChanged: {
toggleParticipantToolTip.visible = hovered
localMutedText.visible = hovered && isLocalMuted
}
Text {
id: toggleParticipantToolTip
......@@ -167,6 +171,22 @@ Rectangle {
color: JamiTheme.whiteColor
font.pointSize: JamiTheme.tinyFontSize
}
Text {
id: localMutedText
visible: false
width: parent.width
text: "(" + JamiStrings.localMuted + ")"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignTop
anchors.top: parent.bottom
anchors.topMargin: 16
color: JamiTheme.whiteColor
font.pointSize: JamiTheme.tinyFontSize
}
}
PushButton {
......
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
......@@ -27,7 +26,6 @@ var contactPickerObject
function createContactPickerObjects(type, parent) {
if (contactPickerObject) {
/*
* If already created, reset parameters, since object cannot be destroyed.
*/
......@@ -49,23 +47,15 @@ function finishCreation(type, parent) {
"type": type
})
if (contactPickerObject === null) {
/*
* Error Handling.
*/
console.log("Error creating object for contact picker")
}
}
/*
* Put contact picker in the middle of container.
*/
function calculateCurrentGeo(containerX, containerY) {
if (contactPickerObject) {
contactPickerObject.x = containerX - contactPickerObject.width / 2
contactPickerObject.y = containerY - contactPickerObject.height / 2
} else {
contactPickerObject.x = Qt.binding(function(){
return parent.width/2 - contactPickerObject.width / 2})
contactPickerObject.y = Qt.binding(function(){
return parent.height/2 - contactPickerObject.height / 2})
}
}
......
......@@ -47,17 +47,11 @@ function finishCreation(parent) {
* Error Handling.
*/
console.log("Error creating object for mediahandler picker")
}
}
/*
* Put mediahandler picker in the middle of container.
*/
function calculateCurrentGeo(containerX, containerY) {
if (mediahandlerPickerObject) {
mediahandlerPickerObject.x = containerX - mediahandlerPickerObject.width / 2
mediahandlerPickerObject.y = containerY - mediahandlerPickerObject.height / 2
} else {
mediahandlerPickerObject.x = Qt.binding(function(){
return parent.width/2 - mediahandlerPickerObject.width / 2})
mediahandlerPickerObject.y = Qt.binding(function(){
return parent.height/2 - mediahandlerPickerObject.height / 2})
}
}
......
/*
* Copyright (C) 2019-2020 by Savoir-faire Linux
* Author: Albert Babí Oller <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 <http://www.gnu.org/licenses/>.
*/
#include "moderatorlistmodel.h"
#include "lrcinstance.h"
ModeratorListModel::ModeratorListModel(QObject* parent)
: QAbstractListModel(parent)
{}
ModeratorListModel::~ModeratorListModel() {}
int
ModeratorListModel::rowCount(const QModelIndex& parent) const
{
if (!parent.isValid()) {
return LRCInstance::accountModel().getDefaultModerators(
LRCInstance::getCurrentAccountInfo().id).size();
}
return 0;
}
int
ModeratorListModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
/*
* Only need one column.
*/
return 1;
}
QVariant
ModeratorListModel::data(const QModelIndex& index, int role) const
{
QStringList list = LRCInstance::accountModel().getDefaultModerators(
LRCInstance::getCurrAccId());
if (!index.isValid() || list.size() <= index.row()) {
return QVariant();
}
auto contactInfo = LRCInstance::getCurrentAccountInfo().contactModel->getContact(