linphone-desktop/Linphone/view/Page/Layout/Main/MainLayout.qml
Gaelle Braud 93418cb7c9 hide empty chat rooms flag default value to true #LINQT-1893
fix crash

call history/chats/chat messages lists loading ui
2025-08-20 11:00:12 +02:00

702 lines
37 KiB
QML

import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic as Control
import QtQuick.Effects
import Linphone
import UtilsCpp
import SettingsCpp
import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils
import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle
Item {
id: mainItem
property var callObj
property var contextualMenuOpenedComponent: undefined
signal addAccountRequest
signal openNewCallRequest
signal callCreated
signal openCallHistory
signal openNumPadRequest
signal displayContactRequested(string contactAddress)
signal displayChatRequested(string contactAddress)
signal openChatRequested(ChatGui chat)
signal createContactRequested(string name, string address)
signal scheduleMeetingRequested(string subject, list<string> addresses)
signal accountRemoved
function goToNewCall() {
tabbar.currentIndex = 0
mainItem.openNewCallRequest()
}
function goToCallHistory() {
tabbar.currentIndex = 0
mainItem.openCallHistory()
}
function displayContactPage(contactAddress) {
tabbar.currentIndex = 1
mainItem.displayContactRequested(contactAddress)
}
function displayChatPage(contactAddress) {
tabbar.currentIndex = 2
mainItem.displayChatRequested(contactAddress)
}
function openChat(chat) {
tabbar.currentIndex = 2
mainItem.openChatRequested(chat)
}
function createContact(name, address) {
tabbar.currentIndex = 1
mainItem.createContactRequested(name, address)
}
function scheduleMeeting(subject, addresses) {
tabbar.currentIndex = 3
mainItem.scheduleMeetingRequested(subject, addresses)
}
function openContextualMenuComponent(component) {
if (mainItem.contextualMenuOpenedComponent
&& mainItem.contextualMenuOpenedComponent != component) {
mainStackView.pop()
mainItem.contextualMenuOpenedComponent = undefined
}
if (!mainItem.contextualMenuOpenedComponent) {
mainStackView.push(component)
mainItem.contextualMenuOpenedComponent = component
}
settingsMenuButton.popup.close()
}
function closeContextualMenuComponent() {
mainStackView.pop()
mainItem.contextualMenuOpenedComponent = undefined
}
function openAccountSettings(account) {
var page = accountSettingsPageComponent.createObject(parent, {
"account": account
})
openContextualMenuComponent(page)
}
AccountProxy {
id: accountProxy
sourceModel: AppCpp.accounts
onDefaultAccountChanged: if (tabbar.currentIndex === 0 && defaultAccount)
defaultAccount.core?.lResetMissedCalls()
}
CallProxy {
id: callsModel
sourceModel: AppCpp.calls
}
Item {
anchors.fill: parent
Popup {
id: currentCallNotif
background: Item {}
closePolicy: Control.Popup.NoAutoClose
visible: currentCall
&& currentCall.core.state != LinphoneEnums.CallState.Idle
&& currentCall.core.state != LinphoneEnums.CallState.IncomingReceived
&& currentCall.core.state != LinphoneEnums.CallState.PushIncomingReceived
x: mainItem.width / 2 - width / 2
y: contentItem.height / 2
property var currentCall: callsModel.currentCall ? callsModel.currentCall : null
property string remoteName: currentCall ? currentCall.core.remoteName : ""
contentItem: MediumButton {
style: ButtonStyle.toast
text: currentCallNotif.currentCall ? currentCallNotif.currentCall.core.conference ? ("Réunion en cours : ") + currentCallNotif.currentCall.core.conference.core.subject : (("Appel en cours : ") + currentCallNotif.remoteName) : "appel en cours"
onClicked: {
var callsWindow = UtilsCpp.getCallsWindow(
currentCallNotif.currentCall)
UtilsCpp.smartShowWindow(callsWindow)
}
}
}
RowLayout {
anchors.fill: parent
spacing: 0
anchors.topMargin: Math.round(25 * DefaultStyle.dp)
VerticalTabBar {
id: tabbar
Layout.fillHeight: true
Layout.preferredWidth: Math.round(82 * DefaultStyle.dp)
defaultAccount: accountProxy.defaultAccount
Binding on currentIndex {
when: mainItem.contextualMenuOpenedComponent != undefined
value: -1
}
model: [{
"icon": AppIcons.phone,
"selectedIcon": AppIcons.phoneSelected,
//: "Appels"
"label": qsTr("bottom_navigation_calls_label")
}, {
"icon": AppIcons.adressBook,
"selectedIcon": AppIcons.adressBookSelected,
//: "Contacts"
"label": qsTr("bottom_navigation_contacts_label")
}, {
"icon": AppIcons.chatTeardropText,
"selectedIcon": AppIcons.chatTeardropTextSelected,
//: "Conversations"
"label": qsTr("bottom_navigation_conversations_label"),
"visible": !SettingsCpp.disableChatFeature
}, {
"icon": AppIcons.videoconference,
"selectedIcon": AppIcons.videoconferenceSelected,
//: "Réunions"
"label": qsTr("bottom_navigation_meetings_label"),
"visible": !SettingsCpp.disableMeetingsFeature
}]
onCurrentIndexChanged: {
if (currentIndex == -1)
return
if (currentIndex === 0 && accountProxy.defaultAccount)
accountProxy.defaultAccount.core?.lResetMissedCalls()
if (mainItem.contextualMenuOpenedComponent) {
closeContextualMenuComponent()
}
}
Keys.onPressed: event => {
if (event.key == Qt.Key_Right) {
mainStackView.currentItem.forceActiveFocus()
}
}
Component.onCompleted: {
if (SettingsCpp.shortcutCount > 0) {
var shortcuts = SettingsCpp.shortcuts
shortcuts.forEach(shortcut => {
model.push({
"icon": shortcut.icon,
"selectedIcon": shortcut.icon,
"label": shortcut.name,
"colored": true,
"link": shortcut.link
})
})
}
initButtons()
currentIndex = SettingsCpp.getLastActiveTabIndex()
if (currentIndex === -1) currentIndex = 0
}
}
ColumnLayout {
spacing: 0
RowLayout {
id: topRow
Layout.preferredHeight: Math.round(50 * DefaultStyle.dp)
Layout.leftMargin: Math.round(45 * DefaultStyle.dp)
Layout.rightMargin: Math.round(41 * DefaultStyle.dp)
spacing: Math.round(25 * DefaultStyle.dp)
SearchBar {
id: magicSearchBar
Layout.fillWidth: true
//: "Rechercher un contact, appeler %1"
placeholderText: qsTr("searchbar_placeholder_text").arg(SettingsCpp.disableChatFeature
? "…"
//: "ou envoyer un message …"
: qsTr("searchbar_placeholder_text_chat_feature_enabled"))
focusedBorderColor: DefaultStyle.main1_500_main
numericPadButton.visible: text.length === 0
numericPadButton.checkable: false
handleNumericPadPopupButtonsPressed: false
onOpenNumericPadRequested: mainItem.goToNewCall()
Connections {
target: mainItem
function onCallCreated() {
magicSearchBar.focus = false
magicSearchBar.clearText()
}
}
onTextChanged: {
if (text.length != 0)
listPopup.open()
else
listPopup.close()
}
KeyNavigation.down: contactList //contactLoader.item?.count > 0 || !contactLoader.item?.footerItem? contactLoader.item : contactLoader.item?.footerItem
KeyNavigation.up: contactList //contactLoader.item?.footerItem ? contactLoader.item?.footerItem : contactLoader.item
Popup {
id: listPopup
width: magicSearchBar.width
property real maxHeight: Math.round(400 * DefaultStyle.dp)
property bool displayScrollbar: contactList.height > maxHeight
height: Math.min(
contactList.contentHeight,
maxHeight) + topPadding + bottomPadding
y: magicSearchBar.height
// closePolicy: Popup.CloseOnEscape
topPadding: Math.round(20 * DefaultStyle.dp)
bottomPadding: Math.round((contactList.haveContacts ? 20 : 10) * DefaultStyle.dp)
rightPadding: Math.round(8 * DefaultStyle.dp)
leftPadding: Math.round(20 * DefaultStyle.dp)
visible: magicSearchBar.text.length != 0
background: Item {
anchors.fill: parent
Rectangle {
id: popupBg
radius: Math.round(16 * DefaultStyle.dp)
color: DefaultStyle.grey_0
anchors.fill: parent
border.color: DefaultStyle.main1_500_main
border.width: contactList.activeFocus ? 2 : 0
}
MultiEffect {
source: popupBg
anchors.fill: popupBg
shadowEnabled: true
shadowBlur: 0.1
shadowColor: DefaultStyle.grey_1000
shadowOpacity: 0.1
}
}
contentItem: AllContactListView {
id: contactList
width: listPopup.width - listPopup.leftPadding
- listPopup.rightPadding
itemsRightMargin: Math.round(5 * DefaultStyle.dp) //(Actions have already 10 of margin)
showInitials: false
showContactMenu: false
showActions: true
showFavorites: false
selectionEnabled: false
searchOnEmpty: false
sectionsPixelSize: Typography.p2.pixelSize
sectionsWeight: Typography.p2.weight
sectionsSpacing: Math.round(5 * DefaultStyle.dp)
searchBarText: magicSearchBar.text
}
}
}
RowLayout {
spacing: Math.round(10 * DefaultStyle.dp)
PopupButton {
id: deactivateDndButton
Layout.preferredWidth: Math.round(32 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(32 * DefaultStyle.dp)
popup.padding: Math.round(14 * DefaultStyle.dp)
visible: SettingsCpp.dnd
contentItem: EffectImage {
imageSource: AppIcons.bellDnd
width: Math.round(32 * DefaultStyle.dp)
height: Math.round(32 * DefaultStyle.dp)
Layout.preferredWidth: Math.round(32 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(32 * DefaultStyle.dp)
fillMode: Image.PreserveAspectFit
colorizationColor: DefaultStyle.main1_500_main
}
popup.contentItem: ColumnLayout {
IconLabelButton {
Layout.fillWidth: true
focus: visible
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Désactiver ne pas déranger"
text: qsTr("contact_presence_status_disable_do_not_disturb")
icon.source: AppIcons.bellDnd
onClicked: {
deactivateDndButton.popup.close()
SettingsCpp.dnd = false
}
}
}
}
Voicemail {
id: voicemail
Layout.preferredWidth: Math.round(42 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(36 * DefaultStyle.dp)
Repeater {
model: accountProxy
delegate: Item {
Connections {
target: modelData.core
function onShowMwiChanged() {
voicemail.updateCumulatedMwi()
}
function onVoicemailAddressChanged() {
voicemail.updateCumulatedMwi()
}
}
}
}
function updateCumulatedMwi() {
var count = 0
var showMwi = false
var supportsVoiceMail = false
for (var i = 0; i < accountProxy.count; i++) {
var core = accountProxy.getAt(i).core
count += core.voicemailCount
showMwi |= core.showMwi
supportsVoiceMail |= core.voicemailAddress.length > 0
}
voicemail.showMwi = showMwi
voicemail.voicemailCount = count
voicemail.visible = showMwi || supportsVoiceMail
}
Component.onCompleted: {
updateCumulatedMwi()
}
onClicked: {
if (accountProxy.count > 1) {
avatarButton.popup.open()
} else {
if (accountProxy.defaultAccount.core.voicemailAddress.length
> 0)
UtilsCpp.createCall(
accountProxy.defaultAccount.core.voicemailAddress)
else
UtilsCpp.showInformationPopup(qsTr("information_popup_error_title"),
//: "L'URI de messagerie vocale n'est pas définie."
qsTr("no_voicemail_uri_error_message"), false)
}
}
}
PopupButton {
id: avatarButton
Layout.preferredWidth: Math.round(54 * DefaultStyle.dp)
Layout.preferredHeight: width
popup.topPadding: Math.round(23 * DefaultStyle.dp)
popup.bottomPadding: Math.round(23 * DefaultStyle.dp)
popup.leftPadding: Math.round(24 * DefaultStyle.dp)
popup.rightPadding: Math.round(24 * DefaultStyle.dp)
contentItem: Avatar {
id: avatar
height: avatarButton.height
width: avatarButton.width
account: accountProxy.defaultAccount
}
popup.contentItem: ColumnLayout {
AccountListView {
id: accounts
onAddAccountRequest: mainItem.addAccountRequest()
onEditAccount: function (account) {
avatarButton.popup.close()
openAccountSettings(account)
}
}
}
}
PopupButton {
id: settingsMenuButton
Layout.preferredWidth: Math.round(24 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(24 * DefaultStyle.dp)
popup.width: Math.round(271 * DefaultStyle.dp)
popup.padding: Math.round(14 * DefaultStyle.dp)
popup.contentItem: FocusScope {
id: popupFocus
implicitHeight: settingsButtons.implicitHeight
implicitWidth: settingsButtons.implicitWidth
Keys.onPressed: event => {
if (event.key == Qt.Key_Left
|| event.key == Qt.Key_Escape) {
settingsMenuButton.popup.close()
event.accepted = true
}
}
ColumnLayout {
id: settingsButtons
spacing: Math.round(16 * DefaultStyle.dp)
anchors.fill: parent
IconLabelButton {
id: accountButton
Layout.fillWidth: true
visible: !SettingsCpp.hideAccountSettings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: Mon compte
text: qsTr("drawer_menu_manage_account")
icon.source: AppIcons.manageProfile
onClicked: openAccountSettings(
accountProxy.defaultAccount ? accountProxy.defaultAccount : accountProxy.firstAccount())
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
0) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
0) : null
}
IconLabelButton {
id: dndButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
text: SettingsCpp.dnd ? qsTr("contact_presence_status_disable_do_not_disturb")
//: "Activer ne pas déranger"
: qsTr("contact_presence_status_enable_do_not_disturb")
icon.source: AppIcons.bellDnd
onClicked: {
settingsMenuButton.popup.close()
SettingsCpp.dnd = !SettingsCpp.dnd
}
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
1) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
1) : null
}
IconLabelButton {
id: settingsButton
Layout.fillWidth: true
visible: !SettingsCpp.hideSettings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
text: qsTr("settings_title")
icon.source: AppIcons.settings
onClicked: openContextualMenuComponent(
settingsPageComponent)
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
2) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
2) : null
}
IconLabelButton {
id: recordsButton
Layout.fillWidth: true
visible: !SettingsCpp.disableCallRecordings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Enregistrements"
text: qsTr("recordings_title")
icon.source: AppIcons.micro
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
3) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
3) : null
}
IconLabelButton {
id: helpButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Aide"
text: qsTr("help_title")
icon.source: AppIcons.question
onClicked: openContextualMenuComponent(
helpPageComponent)
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
4) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
4) : null
}
IconLabelButton {
id: quitButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Quitter l'application"
text: qsTr("help_quit_title")
icon.source: AppIcons.power
onClicked: {
settingsMenuButton.popup.close()
//: "Quitter %1 ?"
UtilsCpp.getMainWindow().showConfirmationLambdaPopup("", qsTr("quit_app_question").arg(applicationName),"",
function (confirmed) {
if (confirmed) {
console.info("Exiting App from Top Menu")
Qt.quit()
}
})
}
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
5) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
5) : null
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(Math.round(1 * DefaultStyle.dp), 1)
visible: addAccountButton.visible
color: DefaultStyle.main2_400
}
IconLabelButton {
id: addAccountButton
Layout.fillWidth: true
visible: SettingsCpp.maxAccount == 0
|| SettingsCpp.maxAccount > accountProxy.count
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Ajouter un compte"
text: qsTr("drawer_menu_add_account")
icon.source: AppIcons.plusCircle
onClicked: mainItem.addAccountRequest()
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
7) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
7) : null
}
}
}
}
}
}
Component {
id: mainStackLayoutComponent
StackLayout {
id: mainStackLayout
objectName: "mainStackLayout"
property int _currentIndex: tabbar.currentIndex
currentIndex: -1
onActiveFocusChanged: if (activeFocus
&& currentIndex >= 0)
children[currentIndex].forceActiveFocus()
on_CurrentIndexChanged: {
if (count > 0) {
if (_currentIndex >= count
&& tabbar.model[_currentIndex].link) {
Qt.openUrlExternally(
tabbar.model[_currentIndex].link)
} else if (_currentIndex >= 0) {
currentIndex = _currentIndex
SettingsCpp.setLastActiveTabIndex(
currentIndex)
}
}
}
CallPage {
id: callPage
Connections {
target: mainItem
function onOpenNewCallRequest() {
callPage.goToNewCall()
}
function onCallCreated() {
callPage.goToCallHistory()
}
function onOpenCallHistory() {
callPage.goToCallHistory()
}
function onOpenNumPadRequest() {
callPage.openNumPadRequest()
}
}
onCreateContactRequested: (name, address) => {
mainItem.createContact(
name, address)
}
Component.onCompleted: {
magicSearchBar.numericPadPopup = callPage.numericPadPopup
}
onGoToCallForwardSettings: {
var page = settingsPageComponent.createObject(parent, {
defaultIndex: 1
});
openContextualMenuComponent(page)
}
}
ContactPage {
id: contactPage
Connections {
target: mainItem
function onCreateContactRequested(name, address) {
contactPage.createContact(name, address)
}
function onDisplayContactRequested(contactAddress) {
contactPage.initialFriendToDisplay = contactAddress
}
}
}
ChatPage {
id: chatPage
Connections {
target: mainItem
function onDisplayChatRequested(contactAddress) {
console.log("display chat requested, open with address", contactAddress)
chatPage.remoteAddress = ""
chatPage.remoteAddress = contactAddress
}
function onOpenChatRequested(chat) {
console.log("open chat requested, open", chat.core.title)
chatPage.selectedChatGui = chat
}
}
}
MeetingPage {
id: meetingPage
Connections {
target: mainItem
function onScheduleMeetingRequested(subject, addresses) {
meetingPage.createPreFilledMeeting(subject, addresses)
}
}
}
}
}
Component {
id: accountSettingsPageComponent
AccountSettingsPage {
onGoBack: closeContextualMenuComponent()
onAccountRemoved: {
closeContextualMenuComponent()
mainItem.accountRemoved()
}
}
}
Component {
id: settingsPageComponent
SettingsPage {
onGoBack: closeContextualMenuComponent()
}
}
Component {
id: helpPageComponent
HelpPage {
onGoBack: closeContextualMenuComponent()
}
}
Control.StackView {
id: mainStackView
property Transition noTransition: Transition {
PropertyAnimation {
property: "opacity"
from: 1
to: 1
duration: 0
}
}
pushEnter: noTransition
pushExit: noTransition
popEnter: noTransition
popExit: noTransition
Layout.topMargin: Math.round(24 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.fillHeight: true
initialItem: mainStackLayoutComponent
}
}
}
}
}