diff --git a/src/callview/qml/callview.qml b/src/callview/qml/callview.qml index 6cdcfec1..0ebc76c7 100644 --- a/src/callview/qml/callview.qml +++ b/src/callview/qml/callview.qml @@ -1,272 +1,273 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.7 import QtQuick.Layouts 1.0 import net.lvindustries.ringqtquick 1.0 as RingQtQuick import org.kde.ringkde.jamicallview 1.0 as JamiCallView import org.kde.ringkde.jamivideoview 1.0 as JamiVideoView import org.kde.ringkde.jamidialview 1.0 as JamiDialView Item { id: videoDock signal callWithVideo () signal callWithAudio () signal callWithScreen() // C++ bindings property alias rendererName : videoWidget.rendererName property bool displayPreview : false property string mode : "PREVIEW" property bool previewRunning : false property alias peerRunning : videoWidget.started property QtObject call : null property QtObject renderer : call ? call.renderer : null + property alias actionFilter : actionToolbar.filter property alias individual: placeholderMessage.individual property bool previewVisible: mode != "PREVIEW" && call && RingSession.previewManager.previewing Connections { target: renderer onDestroyed: { videoWidget.started = false } } // Let the animations finish before Timer { id: toolbarTimer running: false interval: 150 repeat: false onTriggered: { actionToolbar.visible = false videoSource.visible = false controlToolbar.visible = false } } function showToolbars() { actionToolbar.visible = true videoSource.visible = true // This toolbar is only useful when there is video if (videoWidget.started) controlToolbar.visible = true actionToolbar.opacity = 1 videoSource.opacity = 1 controlToolbar.opacity = 1 videoPreview.opacity = 0.8 actionToolbar.anchors.bottomMargin = 0 videoSource.anchors.rightMargin = 0 controlToolbar.anchors.topMargin = 0 } function hideToolbars() { actionToolbar.opacity = 0 videoSource.opacity = 0 controlToolbar.opacity = 0 videoPreview.opacity = 1 videoSource.anchors.rightMargin = -20 actionToolbar.anchors.bottomMargin = -20 controlToolbar.anchors.topMargin = -20 toolbarTimer.running = true } // The main video widget JamiVideoView.VideoWidget { id: videoWidget anchors.fill: parent z: -100 started: false visible: started && !hasFailed call: videoDock.call } // The preview JamiVideoView.VideoWidget { id: videoPreview z: -95 started: false visible: previewVisible anchors.right: parent.right anchors.bottom: parent.bottom width: 192 height: 108 } // This toolbar allows to rotate video, take screenshots, etc JamiVideoView.VideoControlToolbar { id: controlToolbar anchors.top: parent.top visible: false Behavior on opacity { NumberAnimation {duration: 100} } Behavior on anchors.topMargin { NumberAnimation {duration: 150} } } // The has the currently supported call actions such as hang up JamiDialView.ActionToolbar { id: actionToolbar anchors.bottom: parent.bottom visible: false Behavior on opacity { NumberAnimation {duration: 100} } Behavior on anchors.bottomMargin { NumberAnimation {duration: 150} } } // Make it obvious when the call is being recording (locally, if the other // side choose to record, there is no way to really know. JamiCallView.RecordingIcon { anchors.right: videoDock.right anchors.top: controlToolbar.bottom } // This allows to switch to different camera or enable screen sharing JamiVideoView.VideoSource { id: videoSource z: 101 visible: false anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter Behavior on opacity { NumberAnimation {duration: 100} } Behavior on anchors.rightMargin { NumberAnimation {duration: 150} } } // The background JamiCallView.CallBackground { id: placeholderMessage z: -99 anchors.fill: parent bottomMargin: actionToolbar.visible ? actionToolbar.height : 0 } // Hide both toolbars when the mouse isn't moving //TODO keep visible if the mouse if over the toolbars MouseArea { id: mainMouseArea Timer { id: activityTimer interval: 3000 running: true repeat: false onTriggered: { hideToolbars() } } function trackActivity() { if (call && mode != "PREVIEW") showToolbars() activityTimer.restart() } anchors.fill: parent hoverEnabled: true propagateComposedEvents: true onMouseXChanged: trackActivity() onMouseYChanged: trackActivity() } onModeChanged: { if (mode == "PREVIEW") { hideToolbars() videoWidget.rendererName = "preview" } else if (mode == "CONVERSATION") { videoPreview.started = RingSession.previewManager.previewing videoWidget.rendererName = "peer" } placeholderMessage.mode = mode } onCallChanged: { if (call) { actionToolbar.userActionModel = call.userActionModel placeholderMessage.call = call videoSource.call = call controlToolbar.call = call } videoWidget.hasFailed = false mainMouseArea.visible = call != null || mode == "PREVIEW" } Connections { target: RingSession.previewManager onPreviewingChanged: { if (mode == "PREVIEW") videoWidget.started = RingSession.previewManager.previewing videoPreview.started = RingSession.previewManager.previewing } } Connections { target: call } Connections { target: call onVideoStarted: { videoWidget.started = true } onVideoStopped: { videoWidget.started = false } onLiveMediaIssuesChanaged: { // This isn't using properties because the renderer live in their // own thread and QML doesn't support this yet videoWidget.hasFailed = call.hasIssue(RingQtQuick.Call.VIDEO_ACQUISITION_FAILED) } } Connections { target: videoWidget onStartedChanged: { placeholderMessage.visible = (!videoWidget.started) || (videoWidget.hasFailed) } onHasFailedChanged: { placeholderMessage.visible = (!videoWidget.started) || (videoWidget.hasFailed) } } Connections { target: call onStateChanged: { if (call == null || call.lifeCycleState == RingQtQuick.Call.FINISHED) { call = null hideToolbars() } } } } diff --git a/src/callview/qml/cmselector.qml b/src/callview/qml/cmselector.qml index e5a48f0e..c8fb2aa7 100644 --- a/src/callview/qml/cmselector.qml +++ b/src/callview/qml/cmselector.qml @@ -1,141 +1,140 @@ /*************************************************************************** * Copyright (C) 2018 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami /** * A flexible way to handle the corder case when the ContactMethod cannot be * selected automatically. */ Dialog { id: phoneNumbers - property var currentIndividual: null + property var individual: null property var callback: undefined parent: applicationWindow().contentItem x: applicationWindow().contentItem.width / 2 - width/2 y: applicationWindow().contentItem.height / 2 - height/2 width: applicationWindow().contentItem.width * 0.5 - height: applicationWindow().contentItem.height * 0.5 + height: content.implicitHeight + 100 standardButtons: Dialog.Cancel | Dialog.Apply modal: true + onAccepted: { + if (phoneNumbers.callback) + phoneNumbers.callback(numbers.currentItem.cm) + } + property string text: i18n("This contact has multiple phone numbers, please select one below.") clip: true - Text { - id: label - wrapMode: Text.WordWrap - text: phoneNumbers.text - color: Kirigami.Theme.textColor - } + contentItem: ColumnLayout { + id: content + + Text { + id: label + wrapMode: Text.WordWrap + text: phoneNumbers.text + color: Kirigami.Theme.textColor + Layout.fillWidth: true + } + + ListView { + id: numbers + Layout.margins: 3 + Layout.topMargin: Kirigami.Units.spacing + model: individual + interactive: false + currentIndex: individual ? individual.defaultIndex.row : -1 + Layout.fillWidth: true + spacing: Kirigami.Units.spacing + + Layout.preferredHeight: contentHeight + + highlight: Rectangle { + color: Kirigami.Theme.highlightColor + } + + delegate: MouseArea { + property var cm: object + id: delegate + height: columns.implicitHeight + 2*Kirigami.Units.largeSpacing + width: parent.width + implicitHeight: columns.implicitHeight + 2*Kirigami.Units.largeSpacing - ListView { - id: numbers - anchors.fill: parent - anchors.margins: 3 - anchors.topMargin: label.implicitHeight + 10 - model: currentIndividual - currentIndex: currentIndividual.defaultIndex.row - - delegate: Rectangle { - id: delegate - radius: 3 - color: "transparent" - border.color: "transparent" - border.width: 1 - height: readOnly.height - width: parent.width - implicitHeight: readOnly.height - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true onClicked: { numbers.currentIndex = index } onDoubleClicked: { if (phoneNumbers.callback) phoneNumbers.callback(object) phoneNumbers.close() } - } - states: [ - State { - name: "" - PropertyChanges { - target: delegate - border.color: "transparent" - color: "transparent" - } - }, - State { - name: "selected" - when: numbers.currentItem == delegate - PropertyChanges { - target: delegate - border.color: Kirigami.Theme.highlightedTextColor - color: Kirigami.Theme.highlightColor - } - }, - State { - name: "hover" - when: mouseArea.containsMouse - PropertyChanges { - target: delegate - border.color: Kirigami.Theme.highlightedTextColor - } - } - ] + GridLayout { + id: columns + rows: 2 + columns: 3 + width: parent.width - RowLayout { - id: readOnly - anchors.leftMargin: 10 - anchors.fill: parent - height: columns.implicitHeight + 30 // 30 == 3*spacing - implicitHeight: columns.implicitHeight + 30 - spacing: 10 + anchors.verticalCenter: parent.verticalCenter + + Kirigami.Icon { + id: icon + width: Kirigami.Units.iconSizes.smallMedium + height: width + source: "call-start" + Layout.leftMargin: height/3 + Layout.alignment: Qt.AlignVCenter + Layout.rowSpan: 2 + color:numbers.currentIndex == index ? + Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + } - ColumnLayout { - id: columns - Layout.fillWidth: true Text { text: display - color: Kirigami.Theme.textColor + color:numbers.currentIndex == index ? + Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + } + + Text { + text: account ? (i18n(" (Account: ") + account.alias+ ")") : "" + color: numbers.currentIndex == index ? + Kirigami.Theme.highlightedTextColor : Kirigami.Theme.disabledTextColor Layout.fillWidth: true } Text { + Layout.columnSpan: 2 text: lastUsed == undefined || lastUsed == "" ? i18n("Never used") : i18n("Used ")+totalCallCount+i18n(" time (Last used on: ") + formattedLastUsed + ")" - color: Kirigami.Theme.textColor + color: numbers.currentIndex == index ? + Kirigami.Theme.highlightedTextColor : "#2980b9" Layout.fillWidth: true } } } } } } diff --git a/src/dialview/qml/actiontoolbar.qml b/src/dialview/qml/actiontoolbar.qml index 8c6ec06b..2509fc6c 100644 --- a/src/dialview/qml/actiontoolbar.qml +++ b/src/dialview/qml/actiontoolbar.qml @@ -1,330 +1,298 @@ /*************************************************************************** * Copyright (C) 2015 by Emmanuel Lepage Vallee * * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.0 import QtQuick.Layouts 1.0 as Layouts import org.kde.kirigami 2.2 as Kirigami import net.lvindustries.ringqtquick 1.0 as RingQtQuick import net.lvindustries.ringqtquick.models 1.0 as RingQtModels import org.kde.playground.kquickitemviews 1.0 as KQuickItemViews Rectangle { id: toolbar color: "#55000000" height: actionGrid.contentHeight width: parent.width y:parent.height-toolbar.height -10 z: 100 property var userActionModel: null + /* + * This filter allows to handle the action differently depending on the + * platform or context. The UserActionModel doesn't care about these + * use case and only tell if the action is available depending on the + * current state. + */ + property var filter: RingQtQuick.UserActionFilter { + model: RingSession.callModel.userActionModel + } + Timer { id: hideLabel running: false repeat: false interval: 5000 onTriggered: { currentText.visible = false } } // Use a separate label. This allows to use only icons in the buttons, // this reducing the footprint and avoiding a second row. Rectangle { id: currentText clip: true color: "#333333" height: 20 width: 200 radius: 99 // circle visible: false anchors.horizontalCenter: parent.horizontalCenter y: - 30 Text { id: currentTextText anchors.fill: parent color: "white" font.bold : true horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter onContentWidthChanged: parent.width = contentWidth + 20 onContentHeightChanged: parent.height = contentHeight + 10 } Behavior on width { NumberAnimation {duration: 50} } } // Show the accept and hangup buttons in green and red function selectColor(action) { if (action == RingQtModels.UserActionModel.HANGUP) return "#550000"; else if(action == RingQtModels.UserActionModel.ACCEPT) return "#005500" // Default return "#CC222222" } function selectLabelColor(action) { if (action == RingQtModels.UserActionModel.HANGUP || action == RingQtModels.UserActionModel.ACCEPT) return "white" // Default return "white" } Component { id: actionDelegate Item { id: mainArea width: actionGrid.cellWidth height: actionGrid.cellHeight Rectangle { id: background color: mouseArea.containsMouse ? "#CC333333" : selectColor(action) radius: 99 // circle anchors.leftMargin: 5 anchors.rightMargin: 5 anchors.fill: parent border.width: mouseArea.containsMouse ? 3 : 0 border.color: "#dd5555" Layouts.RowLayout { anchors.margins: 15 anchors.fill: parent KQuickItemViews.DecorationAdapter { Layouts.Layout.alignment: Qt.AlignVCenter pixmap: decoration width: 30 height: 30 } Text { id: label text: display visible: false horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: selectLabelColor(action) font.bold: true Layouts.Layout.leftMargin: 10 Layouts.Layout.fillHeight: true Layouts.Layout.fillWidth: true Layouts.Layout.alignment: Qt.AlignVCenter } } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true z: 101 onClicked: { userActionModel.execute(action) } onContainsMouseChanged: { if (containsMouse) { currentText.visible = true currentTextText.text = display } hideLabel.restart() } } Behavior on color { ColorAnimation {duration: 300} } Behavior on border.width { NumberAnimation {duration: 200} } StateGroup { id: stateGroup states: [ State { name: "" when: actionGrid.count > 2 || actionGrid.count == 0 PropertyChanges { target: background radius: 99 anchors.margins: 0 } PropertyChanges { target: mainArea width: 70 } PropertyChanges { target: label visible: false } }, State { name: "single" when: actionGrid.count == 1 PropertyChanges { target: background radius: 5 anchors.margins: 2 } PropertyChanges { target: mainArea width: (toolbar.width/1) } PropertyChanges { target: label visible: true } }, State { name: "two" when: actionGrid.count == 2 PropertyChanges { target: background radius: 5 anchors.margins: 2 } PropertyChanges { target: mainArea width: (toolbar.width/2) } PropertyChanges { target: label visible: true } } ] } } } } GridView { id: actionGrid height: parent.height - - /* - * This filter allows to handle the action differently depending on the - * platform or context. The UserActionModel doesn't care about these - * use case and only tell if the action is available depending on the - * current state. - */ - model: RingQtQuick.UserActionFilter { - id: filterModel - - // Record crashes on Android - RingQtQuick.UserAction { - action: RingQtModels.UserActionModel.RECORD - enabled: !Kirigami.Settings.isMobile - } - - // Not implemented on Android - RingQtQuick.UserAction { - action: RingQtModels.UserActionModel.MUTE_VIDEO - enabled: !Kirigami.Settings.isMobile - } - - // Not implemented on Android - RingQtQuick.UserAction { - action: RingQtModels.UserActionModel.MUTE_AUDIO - enabled: !Kirigami.Settings.isMobile - } - - // As of Feb 2019, this is currently broken upstream - RingQtQuick.UserAction { - action: RingQtModels.UserActionModel.HOLD - enabled: false - } - - // Unsuported by this client - RingQtQuick.UserAction { - action: RingQtModels.UserActionModel.SERVER_TRANSFER - enabled: false - } - - model: RingSession.callModel.userActionModel - } - + model: toolbar.filter delegate: actionDelegate cellWidth: 70; cellHeight: 60 anchors.centerIn: parent width: Math.min(toolbar.width, count*cellWidth) implicitWidth: Math.min(toolbar.width, count*cellWidth) StateGroup { id: stateGroup states: [ State { name: "" when: actionGrid.count > 2 || actionGrid.count == 0 PropertyChanges { target: actionGrid cellWidth: 70 } }, State { name: "single2" when: actionGrid.count == 1 PropertyChanges { target: actionGrid cellWidth: (toolbar.width/1) } }, State { name: "two2" when: actionGrid.count == 2 PropertyChanges { target: actionGrid cellWidth: (toolbar.width/2) } } ] } } // Hide the label when the mouse is out MouseArea { z: -100 anchors.fill: parent hoverEnabled: true onContainsMouseChanged: { if (!containsMouse) currentText.visible = false } } onVisibleChanged: { if (!visible) currentText.visible = false } onUserActionModelChanged: { if (!userActionModel) { userActionModel = RingSession.callModel.userActionModel return } - filterModel.model = userActionModel ? + toolbar.filter.model = userActionModel ? userActionModel : RingSession.callModel.userActionModel } } diff --git a/src/jamichatview/qml/chatpage.qml b/src/jamichatview/qml/chatpage.qml index 2e150f61..4358b760 100644 --- a/src/jamichatview/qml/chatpage.qml +++ b/src/jamichatview/qml/chatpage.qml @@ -1,251 +1,252 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami import QtGraphicalEffects 1.0 import org.kde.ringkde.jamitimeline 1.0 as JamiTimeline import org.kde.ringkde.jamitimelinebase 1.0 as JamiTimelineBase import org.kde.ringkde.jamichatview 1.0 as JamiChatView import org.kde.ringkde.jamicontactview 1.0 as JamiContactView import net.lvindustries.ringqtquick 1.0 as RingQtQuick import org.kde.playground.kquickitemviews 1.0 as KQuickItemViews Rectangle { id: timelinePage signal disableContactRequests() property bool showScrollbar: true property bool _sendRequestOverride: true property var currentContactMethod: null property var currentIndividual: null property bool canSendTexts: currentIndividual ? currentIndividual.canSendTexts : false property bool sendRequest: _sendRequestOverride && ( sendRequestLoader.active && sendRequestLoader.item && sendRequestLoader.item.sendRequests ) onDisableContactRequests: { if (timelinePage.setContactMethod()) currentContactMethod.confirmationEnabled = false timelinePage._sendRequestOverride = send } Kirigami.Theme.colorSet: Kirigami.Theme.View function focusEdit() { chatBox.focusEdit() } function showNewContent() { chatView.moveTo(Qt.BottomEdge) } function jumpTo(idx) { chatView.jumpTo(idx) } function setContactMethod() { if (currentIndividual && !currentContactMethod) { currentContactMethod = currentIndividual.preferredContactMethod( RingQtQuick.Media.TEXT ) if (!currentContactMethod) console.log("Cannot find a valid ContactMethod for", currentIndividual) } return currentContactMethod } onCurrentIndividualChanged: { currentContactMethod = null setContactMethod() } color: Kirigami.Theme.backgroundColor // Scroll to the search, unread messages, bookmark, etc RingQtQuick.TimelineIterator { id: iterator currentIndividual: timelinePage.currentIndividual firstVisibleIndex: chatView.topLeft lastVisibleIndex: chatView.bottomLeft onContentAdded: { lastVisibleIndex = chatView.indexAt(Qt.BottomEdge) timelinePage.showNewContent() } onProposeIndex: { if (poposedIndex == newestIndex) timelinePage.showNewContent() else chatView.contentY = chatView.itemRect(newestIndex).y } } // Add a blurry background ShaderEffectSource { id: effectSource visible: chatView.displayExtraTime sourceItem: chatView anchors.right: timelinePage.right anchors.top: timelinePage.top width: scrollbar.fullWidth + 15 height: chatView.height sourceRect: Qt.rect( blurryOverlay.x, blurryOverlay.y, blurryOverlay.width, blurryOverlay.height ) } ColumnLayout { anchors.fill: parent clip: true spacing: 0 Loader { id: sendRequestLoader height: active && item ? item.implicitHeight : 0 Layout.fillWidth: true active: chatBox.requireContactRequest Layout.minimumHeight: active && item ? item.implicitHeight : 0 Layout.maximumHeight: active && item ? item.implicitHeight : 0 sourceComponent: JamiContactView.SendRequest { width: sendRequestLoader.width } } RowLayout { id: chatScrollView Layout.fillHeight: true Layout.fillWidth: true Layout.bottomMargin: 0 property bool lock: false Item { Layout.fillHeight: true Layout.fillWidth: true // Buttons to navigate to relevant content JamiChatView.Navigation { timelineIterator: iterator - anchors.rightMargin: scrollbar.hasContent ? + anchors.rightMargin: timelinePage.showScrollbar && scrollbar.hasContent ? blurryOverlay.width : 0 anchors.right: parent.right anchors.bottom: parent.bottom Behavior on anchors.rightMargin { NumberAnimation {duration: 100; easing.type: Easing.InQuad} } } JamiChatView.ChatView { id: chatView individual: timelinePage.currentIndividual width: Math.min(600, timelinePage.width - 50) height: parent.height anchors.horizontalCenter: parent.horizontalCenter forceTime: scrollbar.overlayVisible } // It needs to be here due to z-index conflicts between // chatScrollView and timelinePage Item { id: blurryOverlay z: 2 - opacity: chatView.displayExtraTime && scrollbar.hasContent ? 1 : 0 + opacity: chatView.displayExtraTime && + timelinePage.showScrollbar && scrollbar.hasContent ? 1 : 0 anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: - 15 height: chatScrollView.height clip: true width: chatView.displayExtraTime ? scrollbar.fullWidth + 15 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation {duration: 300; easing.type: Easing.InQuad} } Repeater { anchors.fill: parent model: 5 FastBlur { anchors.fill: parent source: effectSource radius: 30 } } Rectangle { anchors.fill: parent color: Kirigami.Theme.backgroundColor opacity: 0.75 } } } JamiTimelineBase.Scrollbar { id: scrollbar z: 1000 bottomUp: true Layout.fillHeight: true Layout.preferredWidth: 10 - display: chatView.moving || timelinePage.showScrollbar + display: chatView.moving && timelinePage.showScrollbar model: chatView.model view: chatView - forceOverlay: chatView.displayExtraTime + forceOverlay: timelinePage.showScrollbar && chatView.displayExtraTime } } Kirigami.Separator { Layout.fillWidth: true } JamiChatView.ChatBox { id: chatBox Layout.fillWidth: true visible: canSendTexts RingQtQuick.MessageBuilder {id: builder} requireContactRequest: currentContactMethod && currentContactMethod.confirmationStatus == RingQtQuick.ContactMethod.UNCONFIRMED && currentContactMethod.confirmationStatus != RingQtQuick.ContactMethod.DISABLED } } Connections { target: chatBox onSendMessage: { timelinePage.setContactMethod() if (currentContactMethod) { if (currentContactMethod.account && currentContactMethod.confirmationStatus == RingQtQuick.ContactMethod.UNCONFIRMED) currentContactMethod.sendContactRequest() builder.addPayload("text/plain", message) builder.sendWidth(currentContactMethod) } } } } diff --git a/src/jamichatview/qml/chatview.qml b/src/jamichatview/qml/chatview.qml index 0c518300..0b052f0e 100644 --- a/src/jamichatview/qml/chatview.qml +++ b/src/jamichatview/qml/chatview.qml @@ -1,182 +1,184 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami import org.kde.playground.kquickitemviews 1.0 as KQuickItemViews import net.lvindustries.ringqtquick 1.0 as RingQtQuick import net.lvindustries.ringqtquick.models 1.0 as RingQtModels import org.kde.ringkde.jamichatview 1.0 as JamiChatView import org.kde.ringkde.genericutils 1.0 as GenericUtils KQuickItemViews.HierarchyView { id: chatView clip: true property alias individual: filterModel.individual function jumpTo(idx) { var ridx = filterModel.mapFromSource(idx) var pos = chatView.itemRect(ridx) chatView.contentY = pos.y } property var treeHelper: GenericUtils.TreeHelper {} property bool forceTime: false property var bubbleBackground: blendColor() property var bubbleForeground: "" property var unreadBackground: "" property var unreadForeground: "" property alias slideshow: slideshow property bool displayExtraTimePrivate: moving || dragging || forceTime property bool displayExtraTime: false onDisplayExtraTimePrivateChanged: { - if (displayExtraTimePrivate) + if (displayExtraTimePrivate) { displayExtraTime = true + dateTimer.running = false + } else dateTimer.running = true } Timer { id: dateTimer interval: 1500 repeat: false onTriggered: displayExtraTime = false } model: RingQtQuick.TimelineFilter { id: filterModel showCalls: false showEmptyGroups: true showMessages: true initDelay: 33 } function blendColor() { chatView.bubbleBackground = Qt.tint( Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor //base1 ) chatView.unreadBackground = Qt.tint( Kirigami.Theme.backgroundColor, "#99BB0000" ) chatView.bubbleForeground = Kirigami.Theme.highlightedTextColor chatView.unreadForeground = Kirigami.Theme.highlightedTextColor return chatView.bubbleBackground } JamiChatView.Slideshow { id: slideshow } // Display something when the chat is empty Text { color: Kirigami.Theme.textColor text: i18n("There is nothing yet, enter a message below or place a call using the buttons\nfound in the header") anchors.centerIn: parent visible: chatView.empty horizontalAlignment: Text.AlignHCenter } Component { id: messageDelegate Loader { id: chatLoader // Create a delegate for each type Component { id: sectionDelegate JamiChatView.TextMessageGroup { width: chatView.width } } Component { id: snapshotGroupDelegate JamiChatView.Snapshots { width: chatView.width onViewImage: { chatView.slideshow.active = true chatView.slideshow.model = model chatView.slideshow.source = path } } } Component { id: callDelegate JamiChatView.CallGroup { width: chatView.width } } Component { id: categoryDelegate JamiChatView.CategoryHeader { width: chatView.width } } Component { id: textDelegate JamiChatView.TextBubble { background: isRead ? chatView.bubbleBackground : chatView.unreadBackground foreground: isRead ? chatView.bubbleForeground : chatView.unreadForeground width: chatView.width onClicked: { chatView.treeHelper.setData(rootIndex, true, "isRead") } } } // Some elements don't have delegates because they are handled // by their parent delegates function selectDelegate() { if (nodeType == RingQtModels.IndividualTimelineModel.TIME_CATEGORY) return categoryDelegate if (nodeType == RingQtModels.IndividualTimelineModel.TEXT_MESSAGE) return textDelegate if (nodeType == RingQtModels.IndividualTimelineModel.SNAPSHOT_GROUP) return snapshotGroupDelegate if (nodeType == RingQtModels.IndividualTimelineModel.SECTION_DELIMITER) return sectionDelegate if ( nodeType == RingQtModels.IndividualTimelineModel.CALL_GROUP || nodeType == RingQtModels.IndividualTimelineModel.RECORDINGS ) return callDelegate } sourceComponent: selectDelegate() } } delegate: messageDelegate } diff --git a/src/jamichatview/qml/groupheader.qml b/src/jamichatview/qml/groupheader.qml index 9e3ac042..19fdc424 100644 --- a/src/jamichatview/qml/groupheader.qml +++ b/src/jamichatview/qml/groupheader.qml @@ -1,65 +1,67 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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.7 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami RowLayout { property string type: "text" - opacity: chatView.displayExtraTime ? 1 : 0.5 + // This information has no value 99% of the time, so fade it away + opacity: chatView.displayExtraTime ? 1 : 0.1 + Behavior on opacity { - NumberAnimation {duration: 500} + NumberAnimation {duration: 500} } Item { Layout.preferredWidth: 5 } function getIcon() { if (type == "text") return "dialog-messages" else return "call-start" } Rectangle { height: 30 width: 30 radius: 99 border.width: 1 border.color: Kirigami.Theme.disabledTextColor color: "transparent" Kirigami.Icon { anchors.margins: 6 anchors.fill: parent color: Kirigami.Theme.disabledTextColor source: getIcon() } } Item { Layout.preferredWidth: 10 } Text { text: display color: Kirigami.Theme.textColor } } diff --git a/views/basic/basic.qrc b/views/basic/basic.qrc index 63c7b86e..088710ad 100644 --- a/views/basic/basic.qrc +++ b/views/basic/basic.qrc @@ -1,16 +1,17 @@ - qml/actioncollection.qml + qml/banjiactioncollection.qml + qml/pagemanager.qml qml/basic.qml qml/chatpage.qml qml/desktopheader.qml qml/individualdetails.qml qml/detail.qml qml/individualeditor.qml qml/listpage.qml qml/timelinelist.qml qml/globaldrawer.qml qml/sidebar.qml qml/callpage.qml diff --git a/views/basic/basicviewplugin.cpp b/views/basic/basicviewplugin.cpp index e9782dc1..5b85f1ca 100644 --- a/views/basic/basicviewplugin.cpp +++ b/views/basic/basicviewplugin.cpp @@ -1,45 +1,46 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * 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 . * **************************************************************************/ #include "basicviewplugin.h" #include #include #include void BasicView::registerTypes(const char *uri) { - qmlRegisterType(QStringLiteral("qrc:/basicview/qml/actioncollection.qml"), uri, 1, 0, "ActionCollection"); + qmlRegisterType(QStringLiteral("qrc:/basicview/qml/banjiactioncollection.qml"), uri, 1, 0, "BanjiActionCollection"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/chatpage.qml"), uri, 1, 0, "ChatPage"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/callpage.qml"), uri, 1, 0, "CallPage"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/desktopheader.qml"), uri, 1, 0, "DesktopHeader"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/individualdetails.qml"), uri, 1, 0, "IndividualDetails"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/detail.qml"), uri, 1, 0, "Detail"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/individualeditor.qml"), uri, 1, 0, "IndividualEditor"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/listpage.qml"), uri, 1, 0, "ListPage"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/timelinelist.qml"), uri, 1, 0, "TimelineList"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/globaldrawer.qml"), uri, 1, 0, "GlobalDrawer"); qmlRegisterType(QStringLiteral("qrc:/basicview/qml/sidebar.qml"), uri, 1, 0, "SideBar"); + qmlRegisterType(QStringLiteral("qrc:/basicview/qml/pagemanager.qml"), uri, 1, 0, "PageManager"); } void BasicView::initializeEngine(QQmlEngine* engine, const char* uri) { Q_UNUSED(engine) Q_UNUSED(uri) } diff --git a/views/basic/qml/actioncollection.qml b/views/basic/qml/banjiactioncollection.qml similarity index 71% rename from views/basic/qml/actioncollection.qml rename to views/basic/qml/banjiactioncollection.qml index 7e1cc9d4..1a226d4b 100644 --- a/views/basic/qml/actioncollection.qml +++ b/views/basic/qml/banjiactioncollection.qml @@ -1,146 +1,110 @@ /* * Copyright 2018 Fabian Riethmayer * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.0 import org.kde.kirigami 2.4 as Kirigami -import QtQuick.Controls.Material 2.3 +/** + * This module contains the actions specific to the Banji application. + * + * This is different from the MainActionCollection because those are Kirigami + * toolbar (main) actions. They are not the JamiKDEIntegration.ActionCollection + * either. Those are business logic and (C++) platform integration. + */ QtObject { readonly property Kirigami.Action mailAction: Kirigami.Action { iconName: "mail-message" text: i18n("Write mail") } - readonly property Kirigami.Action videoCallAction: Kirigami.Action { - iconName: "camera-web" - text: i18n("Video call") - enabled: availabilityTracker.canVideoCall - onTriggered: { - showCallPage() - callpage.visible = true - callpage.videoCall() - } - } - - readonly property Kirigami.Action shareScreenAction: Kirigami.Action { - iconName: ":/sharedassets/outline/screen.svg" - text: i18n("Cast screen") - enabled: availabilityTracker.canVideoCall - onTriggered: { - showCallPage() - callpage.visible = true - callpage.screencast() - } - } - - readonly property Kirigami.Action audioCallAction: Kirigami.Action { - iconName: "call-start" - text: i18n("Audio call") - enabled: availabilityTracker.canCall - onTriggered: { - showCallPage() - callpage.visible = true - callpage.audioCall() - } - } - - readonly property Kirigami.Action chatAction: Kirigami.Action { - iconName: "kmouth-phrase-new" - text: i18n("Chat") - onTriggered: { - hideCall() - showChat() - } - } - readonly property Kirigami.Action bookmarkAction: Kirigami.Action { iconName: "favorite" text: i18n("Select as favorite") } readonly property Kirigami.Action shareAction: Kirigami.Action { iconName: "document-share" text: i18n("Share") } readonly property Kirigami.Action editAction: Kirigami.Action { iconName: "document-edit" text: i18n("Edit") } readonly property Kirigami.Action photoAction: Kirigami.Action { iconName: "edit-image-face-add" text: i18n("Choose photo") } readonly property Kirigami.Action banAction: Kirigami.Action { iconName: "im-kick-user" text: i18n("Block number") } readonly property Kirigami.Action deleteAction: Kirigami.Action { iconName: "delete" text: i18n("Delete contact") } readonly property Kirigami.Action clearAction: Kirigami.Action { iconName: "edit-clear-history" text: i18n("Delete history") } readonly property Kirigami.Action newContactAction: Kirigami.Action { iconName: "contact-new" text: "Create contact" } readonly property Kirigami.Action quitAction: Kirigami.Action { iconName: "application-exit" text: i18n("Exit") onTriggered: Qt.quit() } readonly property Kirigami.Action settings: Kirigami.Action { text: i18n("Settings") iconName: "configure" Kirigami.Action { text: i18n("Export account") iconName: "document-export" } Kirigami.Action { text: i18n("Add a Jami account") iconName: "tools-wizard" onTriggered: events.requestsWizard() } Kirigami.Action { text: i18n("Configure accounts") iconName: "configure" onTriggered: events.requestsConfigureAccounts() } Kirigami.Action { text: i18n("Video settings") iconName: "camera-web" onTriggered: events.configureVideo() } } } diff --git a/views/basic/qml/basic.qml b/views/basic/qml/basic.qml index 05598654..32c5571b 100644 --- a/views/basic/qml/basic.qml +++ b/views/basic/qml/basic.qml @@ -1,300 +1,336 @@ /* * Copyright 2018 Fabian Riethmayer * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import org.kde.kirigami 2.6 as Kirigami import net.lvindustries.ringqtquick.media 1.0 as RingQtMedia import net.lvindustries.ringqtquick 1.0 as RingQtQuick import org.kde.ringkde.basicview 1.0 as BasicView import org.kde.ringkde.jamicontactview 1.0 as JamiContactView import org.kde.ringkde.jamiwizard 1.0 as JamiWizard import org.kde.ringkde.jamikdeintegration 1.0 as JamiKDEIntegration Kirigami.ApplicationWindow { /* * Default to 4:3 because it has a nice balance between landscape and * portrait aspect ratios. */ width: 1024; height: 768 pageStack.defaultColumnWidth: width < 320 ? width : 320 /* * The call page is only in the stack when it is needed. */ pageStack.initialPage: [list, chat] /* * Always use the toolbar mode because otherwise the chatbox and call * toolbars gets covered by the Kirigami navigation widgets. */ pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar pageStack.globalToolBar.preferredHeight: Kirigami.Units.gridUnit * 3 // Localization is currently broken with the Material theme //TODO check if the theme is Material before "fixing" i18n function i18n(t) {return t;} - function showCallPage() { - callpage.visible = true - - if (pageStack.currentItem == callpage) - return - - showChat() - - for (var i = 0; i < pageStack.depth; i++) { - if (pageStack.get(i) == callpage) { - pageStack.currentIndex = i - return - } - } - pageStack.push(callpage) - } - - function hideCall() { - for (var i = 0; i < pageStack.depth; i++) { - if (pageStack.get(i) == callpage) { - pageStack.pop(callpage) - pageStack.currentIndex = 0 - return - } - } + /** + * There is multiple ways the Kirigami page stack is manipulated in Banji: + * + * * Adding a new contact + * * hide call when inactive + * * show chat expanded + * * select in the list page + * * Clicking on a timeline contact + * * Show the chat on mobile, keep the current state in wide mode + * * Click chat action during a call + * * Keep the call page, but show the chat page on the left (not expanded) + * * Click chat action without an active call + * * Remove the call page + * * Click the call / screen share / video chat without an active call + * * Show and expand the call page (with animation) + * * Click on the call / screen share / video chat actions during a call + * * Expand the chat page + * * When a call ends + * * No animation + * * When a new incoming call arrives + * * Show the call page, not expanded + * * When accepting a new call with the chat page visible + * * Expand the call page + * * When scrolling on mobile with an active call + * * [list] [chat] [call] + * * When scrolling on mobile from the call page without an active call + * * Delete the call page, then [list] [chat] + * * Calls ends while the call page is not visible + * * Remove silently without an animation + * + */ + BasicView.PageManager { + id: pageManager } - function showChat() { - if (pageStack.currentItem == callpage) - return - - for (var i = 0; i < pageStack.depth; i++) { - if (pageStack.get(i) == chat) { - pageStack.currentIndex = Kirigami.Settings.isMobile ? 1 : 0 - return - } - } - - pageStack.push(chat) - pageStack.currentIndex = Kirigami.Settings.isMobile ? 1 : 0 + /* + * Contains the business logic to create and configure a RingQtQuick.Call. + * + * It is tied with the `workflow` and `availabilityTracker` objects defined + * in this file to make sure the calls are created correctly. It reduces + * the odds of getting runtime errors. + */ + RingQtQuick.CallBuilder { + id: mainCallBuilder + individual: workflow.currentIndividual + contactMethod: workflow.currentContactMethod } /* * Track the network status and other information sources to ensure * actions *can* work at any given time */ RingQtMedia.AvailabilityTracker { id: availabilityTracker individual: workflow.currentIndividual } /* * This implements the workflow of having a single "current" Individual * (abstract contact) at once. It hold some references to all the shared * pointers to ensure QtQuick don't accidentally let them be freed. */ RingQtQuick.SharedModelLocker { id: workflow - - onCallChanged: { - if (!call) - hideCall() - else - showCallPage() - } - - onIndividualChanged: { - list.currentIndex = RingSession.peersTimelineModel.individualIndex( - currentIndividual - ).row - } } /* * This is the QtQuick action collection, there is also: * * org.kde.ringkde.jamikdeintegration.actioncollection * * The difference is that this one has the actions that can be implemented * in QML easily while the other one has the ones where the check and * enabled state depends on the backend state and thus are better * implemented in C++. */ - BasicView.ActionCollection { + BasicView.BanjiActionCollection { id: actionCollection } /* * This is a fixed non-expanded column on the left side with a list. * * Currently, it always display the timeline, but maybe eventually the other * will be supported too: * * * Peers timeline * * Contacts * * History * * Bookmarks * * Active calls and conferences * */ BasicView.ListPage { id: list } /* * This page holds the chat and "per individual" timeline. * * It also has a sidebar with some actions and statistics to perform on * the individual (abstract contact). */ BasicView.ChatPage { id: chat } /* * This is the multimedia page. * * It is used for audio, video and screencast communication. It is only in * the PageRow stack when explicitly selected using a QmlAction or when * there is an incoming (or active) communication. */ BasicView.CallPage { id: callpage visible: false + + /* + * This filter allows to handle the action differently depending on the + * platform or context. The RingqtQuick.UserActionModel doesn't care + * about these use case and only tell if the action is available + * depending on the current state. It does so regardless of whether or + * not it makes sense. + */ + actionFilter: RingQtQuick.UserActionFilter { + id: filterModel + + // Record crashes on Android + RingQtQuick.UserAction { + action: RingQtModels.UserActionModel.RECORD + enabled: !Kirigami.Settings.isMobile + } + + // Not implemented on Android + RingQtQuick.UserAction { + action: RingQtModels.UserActionModel.MUTE_VIDEO + enabled: !Kirigami.Settings.isMobile + } + + // Not implemented on Android + RingQtQuick.UserAction { + action: RingQtModels.UserActionModel.MUTE_AUDIO + enabled: !Kirigami.Settings.isMobile + } + + // As of Feb 2019, this is currently broken upstream + RingQtQuick.UserAction { + action: RingQtModels.UserActionModel.HOLD + enabled: false + } + + // Unsupported by this client + RingQtQuick.UserAction { + action: RingQtModels.UserActionModel.SERVER_TRANSFER + enabled: false + } + + model: RingSession.callModel.userActionModel + } } // Each page provide their actions contextDrawer: Kirigami.ContextDrawer {} globalDrawer: BasicView.GlobalDrawer { /* * Without this the handle might cover the "back" button or allow to * open the wizard from within the wizard, which "works" but is * obviously bad. */ handleVisible: !wizardLoader.active } /* * This is a "wormhole" object where the signals sent by any instance, * including in C++, ends up here. * * This is used as an abstraction to integrate the C++ backend with the QML * frontend without any setContextProperty. */ JamiKDEIntegration.WindowEvent { id: events function showDialog(path) { var component = Qt.createComponent(path) if (component.status == Component.Ready) { var window = component.createObject(applicationWindow().contentItem) window.open() } else console.log("ERROR", component.status, component.errorString()) } // Dialogs and overlays onRequestsConfigureAccounts: showDialog("qrc:/account/qml/accountdialog.qml") onRequestsVideo: showDialog("qrc:/jamivideoview/qml/settingpopup.qml") onRequestsWizard: wizardLoader.activate() // Window events onRequestsHideWindow: hide() } /* * This object tracks when showing the wizard is required. Unlike Ring-KDE, * Banji cannot work without an account and all component blindly expect * one to exist. * * In Ring-KDE, having no account was requested (and implemented), but this * made everything harder to maintain with long QML expressions checking * everything, everywhere. It was a mistake and Banji will *never* support * having no account. */ JamiWizard.Policies { id: wizardPolicies } /* * Do common operations in a wizard instead of the settings. * * It is less error prone. */ JamiWizard.Wizard { id: wizardLoader anchors.fill: parent onActiveChanged: { if (list.displayWelcome && !active) list.search() } } /* * View the current individual information. * * This isn't intended as a full contact manager, only an overview of the * relevant information. It is partially redundant with the sidebar. * * Please note that the sidebar only exists in wide mode with enough room to * "afford" it. So this information needs to be available as an overlay too. */ BasicView.IndividualDetails { id: viewContact } /* * Edit the current individual details. * * Like the `viewContact` above, this is partially redundant with the * sidebar. As with `viewContact`, this is due to the fact that the sidebar * isn't always there. * * It is also important to note that this will create a "real" contact * (LibRingQt.Person) object. For as long as possible, the peer will exist * as an LibRingQt.Individual object. It is a lower level abstraction and * it is easier to synchronize because it doesn't have a custom vCard. */ BasicView.IndividualEditor { id: editContact } /* * Delay showing the wizard or welcome page until the next event loop * iteration. This avoids having to handle a whole bunch of corner cases. * * It is important to keep in mind that net.lvindustries.ringqtquick is * loaded by the QML files, not ahead of time. So during the first iteration, * it isn't fully ready yet. */ Timer { interval: 0 running: true repeat: false onTriggered: { if (wizardPolicies.displayWizard) wizardLoader.activate() else if (list.displayWelcome) list.search() } } } diff --git a/views/basic/qml/callpage.qml b/views/basic/qml/callpage.qml index a65fbe39..b1d280d8 100644 --- a/views/basic/qml/callpage.qml +++ b/views/basic/qml/callpage.qml @@ -1,160 +1,89 @@ /* * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.2 import QtQuick.Layouts 1.4 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.6 as Kirigami import org.kde.ringkde.basicview 1.0 as BasicView import org.kde.ringkde.jamichatview 1.0 as JamiChatView import org.kde.ringkde.jamicallview 1.0 as JamiCallView import net.lvindustries.ringqtquick 1.0 as RingQtQuick import net.lvindustries.ringqtquick.media 1.0 as RingQtMedia Kirigami.Page { - spacing: 0 - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 + property alias actionFilter: callview.actionFilter + + // The JamiCallView.CallView has toolbars that must touch the edge. + // Remove all margins + spacing: 0; leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 padding: 0 + globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar Kirigami.Theme.colorSet: Kirigami.Theme.View + // Place the contact name. There is only 1 action, so there's usually + // enough room for at least part of the name. titleDelegate: BasicView.DesktopHeader { id: dheader Layout.fillWidth: true } - /** - * Get an ongoing call if it exists or request a dialing call to be created. - */ - function getCall(cm) { - return workflow.call && workflow.call.lifeCycleState != RingQtQuick.Call.FINISHED ? - workflow.call : RingSession.callModel.dialingCall(cm) - } - - /** - * An individual can have multiple phone numbers or Ring/Jami accounts. - * - * Pick one. - */ - function getDefaultCm() { - if (workflow.currentContactMethod) - return workflow.currentContactMethod - - if (workflow.currentIndividual) - return workflow.currentIndividual.mainContactMethod - - return null - } - - function callCommon(media) { - if (!workflow.currentIndividual) - return - - var cm = getDefaultCm() - - if (!cm) - cm = workflow.currentIndividual.preferredContactMethod(media) - - if (!cm) { - console.log("Failed to find a proper contact method for", workflow.currentIndividual) - return - } - - if (cm.hasInitCall) { - workflow.showCall(cm.firstActiveCall) - return - } - - var call = getCall(cm) - - call.performAction(RingQtQuick.Call.ACCEPT) - } - - function audioCall() { - callCommon(RingQtQuick.Media.AUDIO) - } - - function videoCall() { - callCommon(RingQtQuick.Media.VIDEO) - } - - function screencast() { - callCommon(RingQtQuick.Media.VIDEO) - } - JamiCallView.CallView { id: callview anchors.fill: parent individual: workflow.currentIndividual mode: "CONVERSATION" call: workflow.call + /** + * There is different ways a call can be selected. + * + * This is all handled by the workflow logic, so this callview should + * blindly obey the workflow object when it tells to select a call. + */ Connections { target: workflow onCallChanged: { callview.call = workflow.call } } - onCallWithAudio: { - var cm = getDefaultCm() - - if (!cm) - return - - audioCall() - } - onCallWithVideo: { - var cm = getDefaultCm() - - if (!cm) - return - - videoCall() - } - onCallWithScreen: { - var cm = getDefaultCm() - - if (!cm) - return - - screencast() - } + onCallWithAudio : pageManager.audioCall () + onCallWithVideo : pageManager.videoCall () + onCallWithScreen: pageManager.screencast() } actions { - main : actionCollection.chatAction + main : pageManager.chatAction } /** * Not worth it on mobile, they are the same as in the call toolbar. */ contextualActions: Kirigami.Settings.isMobile ? [] : [ ActionCollection.holdAction , ActionCollection.recordAction , ActionCollection.muteCaptureAction , ActionCollection.mutePlaybackAction, ActionCollection.hangupAction , ActionCollection.transferAction , ActionCollection.acceptAction , ActionCollection.newCallAction ] } diff --git a/views/basic/qml/chatpage.qml b/views/basic/qml/chatpage.qml index b2bae755..3fdd581f 100644 --- a/views/basic/qml/chatpage.qml +++ b/views/basic/qml/chatpage.qml @@ -1,135 +1,136 @@ /* * Copyright 2018 Fabian Riethmayer * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.2 import QtQuick.Layouts 1.4 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.6 as Kirigami import org.kde.ringkde.basicview 1.0 as BasicView import org.kde.ringkde.jamichatview 1.0 as JamiChatView Kirigami.Page { property bool _fits: false // Force the toolbar style to prevent the action and drawer handles from // getting on top of the chatbox. Kirigami.Theme.colorSet: Kirigami.Theme.View globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar id: chatPage // Remove all padding and spacing because otherwise the separators will have holes spacing: 0 leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 padding: 0 /* * When there is plenty of room, move the header into the toolbar. */ titleDelegate: BasicView.DesktopHeader { id: dheader visible: fits Layout.fillHeight: true photoSize: parent.parent.height - 2*Kirigami.Units.largeSpacing Layout.fillWidth: true Component.onCompleted: _fits = fits onFitsChanged: _fits = fits } /* * When there isn't enough room in the toolbar, add another row */ header: Controls.ToolBar { visible: (!_fits) height: visible ? Kirigami.Units.gridUnit * 2.5 : 0 Layout.fillWidth: true Layout.preferredHeight: visible ? Kirigami.Units.gridUnit * 5 : 0 Layout.margins: 0 BasicView.DesktopHeader { anchors.fill: parent } } RowLayout { anchors.fill: parent spacing: 0 /* * This is the main chat widget with the chatbox, messages, emojis * and timeline scrollbar. */ JamiChatView.ChatPage { id: chatView + showScrollbar: pageStack.wideMode Layout.fillWidth: true Layout.fillHeight: true Layout.bottomMargin: 0 currentIndividual: workflow.currentIndividual } Kirigami.Separator { Layout.fillHeight: true } /* * Only add a sidebar when there is more room than the chat can make use * of. It was decided to restrict the chat with to prevent long bubble. */ Loader { id: sidebarLoader // 750 is the 600pt maximum width of the chat + width of the sidebar active: pageStack.wideMode && (!Kirigami.Settings.isMobile) && parent.width > 750 Layout.preferredWidth: active ? 250 : 0 Layout.fillHeight: true sourceComponent: BasicView.SideBar { anchors.fill: sidebarLoader onSelectIndex: { chatView.jumpTo(idx) } } } } actions { - left : actionCollection.videoCallAction - main : actionCollection.audioCallAction + left : pageManager.videoCallAction + main : pageManager.audioCallAction right: Kirigami.Settings.isMobile ? - undefined : actionCollection.shareScreenAction + undefined : pageManager.shareScreenAction } // Not worth it on mobile contextualActions: Kirigami.Settings.isMobile ? [] : [ actionCollection.bookmarkAction, actionCollection.shareAction, actionCollection.editAction, actionCollection.photoAction, actionCollection.banAction, actionCollection.deleteAction, actionCollection.learAction, ] } diff --git a/views/basic/qml/listpage.qml b/views/basic/qml/listpage.qml index d7e81751..8df1550a 100644 --- a/views/basic/qml/listpage.qml +++ b/views/basic/qml/listpage.qml @@ -1,146 +1,145 @@ /* * Copyright 2018 Fabian Riethmayer * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.6 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.4 as Kirigami import QtQuick.Layouts 1.2 as Layouts import org.kde.ringkde.basicview 1.0 as BasicView import org.kde.ringkde.jamitroubleshooting 1.0 as JamiTroubleShooting import org.kde.ringkde.jamisearch 1.0 as JamiSearch Kirigami.Page { id: peerListPage - property alias currentIndex: list.currentIndex; property bool displayWelcome: false globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar spacing: 0 leftPadding: 0; rightPadding: 0; topPadding: 0;bottomPadding: 0; padding: 0 signal search() Kirigami.Theme.colorSet: Kirigami.Theme.View header: Layouts.ColumnLayout { visible: globalTroubleshoot.sourceComponent != null height: visible && globalTroubleshoot.active ? implicitHeight : 0 width: peerListPage.width spacing: Kirigami.Units.largeSpacing /* * This module displays the most severe error (if any) with a bunch * of options to fix this. This is only for account or global errors. * * Individual errors have their own popup, so are media related errors. */ JamiTroubleShooting.GlobalTroubleshoot { id: globalTroubleshoot Layouts.Layout.fillWidth: true Layouts.Layout.margins: Kirigami.Units.largeSpacing } Item { visible: globalTroubleshoot.sourceComponent != null height: 10 } } /* * The searchbox. * * When clicking on it, it grows and hijack the whole (list) page. It is * done this way because the animation are pretty. Otherwise this is so, so * wrong. */ titleDelegate: Item { id: header implicitHeight: parent.parent.height - 2*Kirigami.Units.largeSpacing implicitWidth: 10 JamiSearch.SearchBox { id: headerSearchbox searchView: _searchView anchors.centerIn: parent anchors.margins: Kirigami.Units.largeSpacing width: parent.width - 2 * Kirigami.Units.largeSpacing height: headerSearchbox.focus ? parent.height : parent.height * 1.5 z: 9999 Connections { target: peerListPage onSearch: { headerSearchbox.forceFocus() } } } /* * Add a blurry background when the search overlay is visible. */ JamiSearch.Overlay { id: _searchView source: peerListPage searchBox: headerSearchbox width: peerListPage.width height: peerListPage.height + header.height x: -(peerListPage.width - header.width) y: -Kirigami.Units.largeSpacing function forceGeometry() { if (!active) return width = peerListPage.width height = peerListPage.height + header.height x = -(peerListPage.width - header.width) } onDisplayWelcomeChanged: { peerListPage.displayWelcome = displayWelcome } onContactMethodSelected: { workflow.currentContactMethod = cm var idx = RingSession.peersTimelineModel.individualIndex(cm.individual) list.currentIndex = idx.row } //HACK obey god dammit onHeightChanged: _searchView.forceGeometry() onActiveChanged: _searchView.forceGeometry() //HACK fix breakage caused by the other hack Connections { target: peerListPage onWidthChanged: _searchView.forceGeometry() onHeightChanged: _searchView.forceGeometry() } z: 9998 } } BasicView.TimelineList { id: list width: parent.width height: parent.height } } diff --git a/views/basic/qml/pagemanager.qml b/views/basic/qml/pagemanager.qml new file mode 100644 index 00000000..652788fa --- /dev/null +++ b/views/basic/qml/pagemanager.qml @@ -0,0 +1,181 @@ +/* + * Copyright 2019 Emmanuel Lepage + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import QtQuick 2.9 +import net.lvindustries.ringqtquick 1.0 as RingQtQuick +import net.lvindustries.ringqtquick.media 1.0 as RingQtMedia +import org.kde.kirigami 2.4 as Kirigami + +/** + * This object hold the Banji toolbar (main) actions. + * + * They help to navigate the page stack and build the multimedia objects. + */ +QtObject { + + /* + * Individual with multiple ring account, SIP URIs or Phone number require + * special care given it isn't always obvious which to call. + */ + function selectContactMethod(callback) { + if (workflow.currentIndividual.requireUserSelection) { + var component = Qt.createComponent("qrc:/callview/qml/cmselector.qml") + if (component.status == Component.Ready) { + var window = component.createObject(applicationWindow().contentItem) + window.individual = workflow.currentIndividual + window.callback = function(cm) { + workflow.currentContactMethod = cm + console.log("\n\nCALLBACK!", cm, workflow.currentContactMethod) + callback() + } + window.open() + } + else + console.log("ERROR", component.status, component.errorString()) + return + } + } + + function showCallPage() { + callpage.visible = true + + if (pageStack.currentItem == callpage) + return + + showChat() + + for (var i = 0; i < pageStack.depth; i++) { + if (pageStack.get(i) == callpage) { + pageStack.currentIndex = i + return + } + } + pageStack.push(callpage) + } + + function hideCall() { + for (var i = 0; i < pageStack.depth; i++) { + if (pageStack.get(i) == callpage) { + pageStack.pop(callpage) + pageStack.currentIndex = 0 + return + } + } + } + + function showChat() { + if (pageStack.currentItem == callpage) + return + + for (var i = 0; i < pageStack.depth; i++) { + if (pageStack.get(i) == chat) { + pageStack.currentIndex = Kirigami.Settings.isMobile ? 1 : 0 + return + } + } + + pageStack.push(chat) + pageStack.currentIndex = Kirigami.Settings.isMobile ? 1 : 0 + } + + function audioCall() { + if (mainCallBuilder.choiceRequired) + return selectContactMethod(audioCall) + + mainCallBuilder.audio = true + mainCallBuilder.video = false + mainCallBuilder.screenSharing = false + workflow.showCall(mainCallBuilder.commit()) + } + + function videoCall() { + if (mainCallBuilder.choiceRequired) + return selectContactMethod(videoCall) + + mainCallBuilder.audio = true + mainCallBuilder.video = true + mainCallBuilder.screenSharing = false + workflow.showCall(mainCallBuilder.commit()) + } + + function screencast() { + if (mainCallBuilder.choiceRequired) + return selectContactMethod(screencast) + + mainCallBuilder.audio = true + mainCallBuilder.video = false + mainCallBuilder.screenSharing = true + workflow.showCall(mainCallBuilder.commit()) + } + + /* + * Change the page automatically when an incoming call arrives. + */ + property var _conn: Connections { + target: workflow + + onCallChanged: { + if (!call) + hideCall() + else + showCallPage() + } + } + + readonly property Kirigami.Action chatAction: Kirigami.Action { + iconName: "kmouth-phrase-new" + text: i18n("Chat") + onTriggered: { + hideCall() + showChat() + } + } + + readonly property Kirigami.Action videoCallAction: Kirigami.Action { + iconName: "camera-web" + text: i18n("Video call") + enabled: availabilityTracker.canVideoCall + onTriggered: { + showCallPage() + callpage.visible = true + videoCall() + } + } + + readonly property Kirigami.Action shareScreenAction: Kirigami.Action { + iconName: ":/sharedassets/outline/screen.svg" + text: i18n("Cast screen") + enabled: availabilityTracker.canVideoCall + onTriggered: { + showCallPage() + callpage.visible = true + screencast() + } + } + + readonly property Kirigami.Action audioCallAction: Kirigami.Action { + iconName: "call-start" + text: i18n("Audio call") + enabled: availabilityTracker.canCall + onTriggered: { + showCallPage() + callpage.visible = true + audioCall() + } + } +} diff --git a/views/basic/qml/timelinelist.qml b/views/basic/qml/timelinelist.qml index d6966c63..daddf392 100644 --- a/views/basic/qml/timelinelist.qml +++ b/views/basic/qml/timelinelist.qml @@ -1,100 +1,119 @@ /* * Copyright 2018 Fabian Riethmayer * Copyright 2019 Emmanuel Lepage * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 3, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.2 import QtQuick.Controls 2.2 as Controls import QtQuick.Layouts 1.4 import org.kde.kirigami 2.4 as Kirigami import QtGraphicalEffects 1.0 as Effect import org.kde.ringkde.jamicontactview 1.0 as JamiContactView ListView { currentIndex: -1 id: list model: RingSession.peersTimelineModel + /* + * Instead of setting the currentIndex directly, listen to the workflow + * proposed current individual and select this. + * + * This is done because sometime, such as when a new call arrives, the + * selection is change automatically. Avoiding performing direct action + * limits the amount of business logic required to keep the list selection + * in sync with the application state. + */ + Connections { + target: workflow + + onIndividualChanged: { + list.currentIndex = RingSession.peersTimelineModel.individualIndex( + workflow.currentIndividual + ).row + } + } + delegate: Kirigami.SwipeListItem { id: listItem onClicked: { - hideCall() - showChat() + pageManager.hideCall() + pageManager.showChat() workflow.setIndividual(object) } activeBackgroundColor: Kirigami.Theme.highlightColor highlighted: index == currentIndex clip: true height: 4 * Kirigami.Units.fontMetrics.height contentItem: GridLayout { height: 4 * Kirigami.Units.fontMetrics.height rows: 4 columns: 2 JamiContactView.ContactPhoto { id: img Layout.margins: 3 height: 3 * Kirigami.Units.fontMetrics.height width: 3 * Kirigami.Units.fontMetrics.height Layout.alignment: Qt.AlignVCenter individual: object defaultColor: highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor drawEmptyOutline: false Layout.rowSpan: 4 } Item { Layout.fillHeight: true } Kirigami.Heading { level: 3 text: object.bestName color: highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor } Kirigami.Heading { level: 4 text: object.formattedLastUsedTime Layout.fillHeight: true Layout.fillWidth: true opacity: 0.5 color: highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.disabledTextColor } Item { Layout.fillHeight: true } } actions: [ actionCollection.callAction, actionCollection.mailAction, ] } }