linphone-desktop/linphone-app/ui/views/App/Main/Conversation.qml
Julien Wadel 9e1c605ab7 Use file viewer only on VFS mode.
Fix animated image fitting size.
Remove picture zoom on mouse over.
2023-04-03 15:35:16 +02:00

699 lines
24 KiB
QML

import QtQuick 2.7
import QtQuick.Layouts 1.3
import Clipboard 1.0
import Common 1.0
import Linphone 1.0
import Utils 1.0
import UtilsCpp 1.0
import LinphoneEnums 1.0
import App.Styles 1.0
import Common.Styles 1.0
import Units 1.0
import ColorsList 1.0
import 'Conversation.js' as Logic
// =============================================================================
ColumnLayout {
id: conversation
// 1) chatRoomModel : chat + calls + conference
// 2) no chatRoomModel : calls
property string defaultPeerAddress
property string defaultLocalAddress
property string defaultFullPeerAddress
property string defaultFullLocalAddress
property ChatRoomModel chatRoomModel
property string localAddress : chatRoomModel?chatRoomModel.getLocalAddress() : defaultLocalAddress
property string fullPeerAddress : getFullPeerAddress()
property string fullLocalAddress : chatRoomModel?chatRoomModel.getFullLocalAddress() : defaultFullLocalAddress
property int securityLevel : chatRoomModel ? chatRoomModel.securityLevel : 1
property SipAddressObserver _sipAddressObserver: SipAddressesModel.getSipAddressObserver((fullPeerAddress?fullPeerAddress:peerAddress), (fullLocalAddress?fullLocalAddress:localAddress))
property bool haveMoreThanOneParticipants: chatRoomModel ? chatRoomModel.participants.count > 2 : false
property bool haveLessThanMinParticipantsForCall: chatRoomModel ? chatRoomModel.participants.count <= 5 : false
function getFullPeerAddress() {
if(chatRoomModel) {
if(chatRoomModel.groupEnabled || chatRoomModel.isSecure()) {
return chatRoomModel.getParticipantAddress()
}else {
return chatRoomModel.sipAddress;
}
}else {
return defaultFullPeerAddress;
}
}
// ---------------------------------------------------------------------------
spacing: 0
clip:false
Component.onDestruction: _sipAddressObserver=null// Need to set it to null because of not calling destructor if not.
// ---------------------------------------------------------------------------
// Contact bar.
// ---------------------------------------------------------------------------
Rectangle {
id:mainBar
Layout.fillWidth: true
Layout.preferredHeight: ConversationStyle.bar.height
color: ConversationStyle.bar.backgroundColor.color
clip:false
RowLayout {
id:contactBar
anchors {
fill: parent
leftMargin: ConversationStyle.bar.leftMargin
rightMargin: ConversationStyle.bar.rightMargin
}
spacing: ConversationStyle.bar.spacing
Avatar {
id: avatar
Layout.preferredHeight: ConversationStyle.bar.avatarSize
Layout.preferredWidth: ConversationStyle.bar.avatarSize
image: Logic.getAvatar()
presenceLevel: chatRoomModel && chatRoomModel.presenceStatus
presenceTimestamp: chatRoomModel && chatRoomModel.presenceTimestamp
//username: Logic.getUsername()
username: chatRoomModel?chatRoomModel.username:( conversation._sipAddressObserver ? UtilsCpp.getDisplayName(conversation._sipAddressObserver.peerAddress) : '')
isOneToOne: chatRoomModel==undefined || chatRoomModel.isOneToOne==undefined || chatRoomModel.isOneToOne
}
Item{
Layout.fillHeight: true
Layout.fillWidth: true
RowLayout{
anchors.fill: parent
spacing:0
ColumnLayout{
property int maximumContentWidth: contactBar.width
-(avatar.visible?avatar.width:0)
-actionBar.width - (secureIcon.visible?secureIcon.width :0)
-3*ConversationStyle.bar.spacing
Layout.fillHeight: true
Layout.minimumWidth: 20
Layout.maximumWidth: maximumContentWidth
Layout.preferredWidth: contactDescription.contentWidth
spacing: 5
Row{
Layout.topMargin: 15
Layout.preferredHeight: implicitHeight
Layout.alignment: Qt.AlignBottom
visible: chatRoomModel && chatRoomModel.isMeAdmin && !usernameEdit.visible && !chatRoomModel.isOneToOne
Icon{
id:adminIcon
icon : ConversationStyle.bar.status.adminStatusIcon
overwriteColor: ConversationStyle.bar.status.adminStatusColor.color
iconSize: ConversationStyle.bar.status.adminStatusIconSize
}
Text{
anchors.verticalCenter: parent.verticalCenter
//: 'Admin' : Admin(istrator)
//~ Context One word title for describing the current admin status
text: qsTr('adminStatus')
color: ConversationStyle.bar.status.adminTextColor.color
font.pointSize: Units.dp * 8
}
}
ContactDescription {
id:contactDescription
Layout.minimumWidth: 20
Layout.maximumWidth: parent.maximumContentWidth
Layout.preferredWidth: contentWidth
Layout.preferredHeight: contentHeight
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
visible: !usernameEdit.visible
contactDescriptionStyle: ConversationStyle.bar.contactDescription
titleText: avatar.username
titleClickable: chatRoomModel && chatRoomModel.isMeAdmin && !chatRoomModel.isOneToOne
subtitleText: if(chatRoomModel) {
if(chatRoomModel.groupEnabled) {
return chatRoomModel.participants.displayNamesToString;
}else if(avatar.hasPresence) {
return avatar.presenceText
}else if(chatRoomModel.isSecure())
return chatRoomModel.participants.addressesToString;
else
return SipAddressesModel.cleanSipAddress(chatRoomModel.sipAddress)
}else
return ''
/*
participants: if(chatRoomModel) {
if(chatRoomModel.groupEnabled) {
return chatRoomModel.participants.displayNamesToString;
}else if(chatRoomModel.isSecure()) {
return chatRoomModel.participants.addressesToString;
}else
return ''
}else
return ''
sipAddress: {
if(chatRoomModel) {
if(chatRoomModel.groupEnabled) {
return '';
}else if(chatRoomModel.isSecure()) {
return '';
}else {
return chatRoomModel.sipAddress;
}
}else {
return conversation.fullPeerAddress || conversation.peerAddress || '';
}
}
*/
onTitleClicked: {
if(!conversation.chatRoomModel.isReadOnly) {
usernameEdit.visible = !usernameEdit.visible
usernameEdit.forceActiveFocus()
}
}
}
Item{
Layout.fillHeight: true
Layout.fillWidth: true
visible: chatRoomModel && chatRoomModel.isMeAdmin && !chatRoomModel.isOneToOne
}
}
Icon{
id: secureIcon
Layout.alignment: Qt.AlignVCenter
visible: securityLevel != 1
icon: securityLevel === 2?'secure_level_1': securityLevel===3? 'secure_level_2' : 'secure_level_unsafe'
iconSize:30
MouseArea{
anchors.fill:parent
visible: conversation.chatRoomModel && !conversation.chatRoomModel.isReadOnly && (SettingsModel.standardChatEnabled || SettingsModel.secureChatEnabled)
onClicked : {
window.detachVirtualWindow()
window.attachVirtualWindow(Qt.resolvedUrl('Dialogs/InfoEncryption.qml')
,{securityLevel:securityLevel}
, function (status) {
if(status){
window.detachVirtualWindow()
window.attachVirtualWindow(Qt.resolvedUrl('Dialogs/ParticipantsDevices.qml')
,{chatRoomModel:chatRoomModel
, window:window})
}
})
}
}
}
Item{//Spacer
Layout.fillWidth: true
}
}
ColumnLayout{
id: usernameEdit
anchors.fill: parent
visible: false
TextField{
Layout.fillWidth: true
text: avatar.username
onEditingFinished: {
chatRoomModel.subject = text
usernameEdit.visible = false
}
font.bold: true
onFocusChanged: if(!focus) usernameEdit.visible = false
}
}
}
Row {
id:actionBar
Layout.fillHeight: true
spacing: ConversationStyle.bar.actions.spacing
ActionBar {
anchors.verticalCenter: parent.verticalCenter
iconSize: ConversationStyle.bar.actions.call.iconSize
ActionButton {
isCustom: true
backgroundRadius: 1000
colorSet: ConversationStyle.bar.actions.videoCall
visible: SettingsModel.videoEnabled && SettingsModel.outgoingCallsEnabled && SettingsModel.showStartVideoCallButton && !conversation.haveMoreThanOneParticipants
onClicked: CallsListModel.launchVideoCall(chatRoomModel.participants.addressesToString)
}
ActionButton {
isCustom: true
backgroundRadius: 1000
colorSet: ConversationStyle.bar.actions.call
visible: SettingsModel.outgoingCallsEnabled && !conversation.haveMoreThanOneParticipants
onClicked: CallsListModel.launchAudioCall(chatRoomModel.participants.addressesToString)
}
ActionButton {
isCustom: true
backgroundRadius: 1000
colorSet: ConversationStyle.bar.actions.chat
visible: SettingsModel.standardChatEnabled && SettingsModel.getShowStartChatButton() && !conversation.haveMoreThanOneParticipants && conversation.securityLevel != 1
onClicked: CallsListModel.launchChat(chatRoomModel.participants.addressesToString, 0)
}
ActionButton {
isCustom: true
backgroundRadius: 1000
colorSet: ConversationStyle.bar.actions.chat
visible: SettingsModel.secureChatEnabled && SettingsModel.getShowStartChatButton() && !conversation.haveMoreThanOneParticipants && conversation.securityLevel == 1
onClicked: CallsListModel.launchChat(chatRoomModel.participants.addressesToString, 1)
Icon{
icon:'secure_level_1'
iconSize: parent.height/2
anchors.top:parent.top
anchors.horizontalCenter: parent.right
}
}
ActionButton {
id: groupCallButton
property ConferenceInfoModel conferenceInfoModel: ConferenceInfoModel{}
Connections{
target: groupCallButton.conferenceInfoModel
onConferenceCreated: {
groupCallButton.toggled = false
}
onConferenceCreationFailed:{
groupCallButton.toggled = false
}
}
isCustom: true
backgroundRadius: 1000
colorSet: ConversationStyle.bar.actions.groupChat
visible: conversation.chatRoomModel && !conversation.chatRoomModel.isReadOnly && conversation.haveMoreThanOneParticipants && SettingsModel.outgoingCallsEnabled && (SettingsModel.videoConferenceEnabled || conversation.haveLessThanMinParticipantsForCall)
//onClicked: CallsListModel. Logic.openConferenceManager({chatRoomModel:conversation.chatRoomModel, autoCall:true})
onClicked:{
if( SettingsModel.videoConferenceEnabled ){
groupCallButton.toggled = true
conferenceInfoModel.resetConferenceInfo();
conferenceInfoModel.isScheduled = false
conferenceInfoModel.subject = chatRoomModel.subject
conferenceInfoModel.setParticipants(conversation.chatRoomModel.participants)
conferenceInfoModel.inviteMode = 0;
conferenceInfoModel.createConference(false)// TODO activate it when secure video conference is implemented
}else{
Logic.openConferenceManager({chatRoomModel:conversation.chatRoomModel, autoCall:true})
}
}
//: "Call all chat room's participants" : tooltip on a button for calling all participant in the current chat room
tooltipText: qsTr("groupChatCallButton")
}
}
ActionBar {
id:actionsBar
anchors.verticalCenter: parent.verticalCenter
ActionButton {
id:dotButton
isCustom: true
backgroundRadius: 90
colorSet: ConversationStyle.bar.actions.openMenu
visible: true //conversationMenu.showGroupInfo || conversationMenu.showDevices || conversationMenu.showEphemerals
toggled: conversationMenu.opened
longPressedTimeout: 3000
onPressed: {// Bug : Not working : Menu is still closed before pressing on button (even with closePolicy)
if( conversationMenu.opened ) {
conversationMenu.close()
}else {
conversationMenu.open()
}
}
property string debugData: chatRoomModel ? 'Chat room ID:\n'+chatRoomModel.getFullPeerAddress()
+'\nLocal account:\n'+chatRoomModel.getFullLocalAddress()
: 'Chat room is null'
onLongPressed:{
if( SettingsModel.logsEnabled){
conversationMenu.close()
window.attachVirtualWindow(Utils.buildCommonDialogUri('ConfirmDialog'), {
descriptionText: debugData,
showButtonOnly: 1,
buttonTexts: ['', 'COPY'],
height:320,
}, function (status) {
Clipboard.text = debugData
})
}
}
}
}
Menu{
id:conversationMenu
x:mainBar.width-width
y:mainBar.height
menuStyle : MenuStyle.aux2
property bool showGroupInfo: chatRoomModel && !chatRoomModel.isOneToOne
property bool showDevices : conversation.securityLevel != 1
property bool showEphemerals: conversation.securityLevel != 1 // && chatRoomModel.isMeAdmin // Uncomment when session mode will be implemented
property bool showScheduleMeeting: showGroupInfo && SettingsModel.conferenceEnabled
MenuItem{
id:contactMenu
property bool editMode: conversation._sipAddressObserver && conversation._sipAddressObserver.contact
text: editMode ?
//: 'View contact' : Item menu to view the contact in address book
qsTr('conversationMenuViewContact')
//: 'Add contact' : Item menu to add the contact to address book
: qsTr('conversationMenuAddContact')
iconMenu: editMode ? MenuItemStyle.contact.view : MenuItemStyle.contact.add
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
visible: conversation.chatRoomModel && SettingsModel.contactsEnabled && conversation.chatRoomModel.isOneToOne
onTriggered: {
window.setView('ContactEdit', {
sipAddress: conversation.getFullPeerAddress()
})
}
TooltipArea {
text: Logic.getEditTooltipText()
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: groupInfoMenu.visible && contactMenu.visible
}
MenuItem{
id:groupInfoMenu
//: 'Group information' : Item menu to get information about the chat room
text: qsTr('conversationMenuGroupInformations')
iconMenu: MenuItemStyle.info.icon
//iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
visible: conversationMenu.showGroupInfo
onTriggered: {
window.detachVirtualWindow()
window.attachVirtualWindow(Qt.resolvedUrl('Dialogs/InfoChatRoom.qml')
,{chatRoomModel:chatRoomModel})
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: devicesMenuItem.visible && (contactMenu.visible || groupInfoMenu.visible)
}
MenuItem{
id: devicesMenuItem
//: "Conversation's devices" : Item menu to get all participant devices of the chat room
text: qsTr('conversationMenuDevices')
iconMenu: MenuItemStyle.devices.icon
visible: conversationMenu.showDevices && (SettingsModel.standardChatEnabled || SettingsModel.secureChatEnabled)
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
onTriggered: {
window.detachVirtualWindow()
window.attachVirtualWindow(Qt.resolvedUrl('Dialogs/ParticipantsDevices.qml')
,{chatRoomModel:chatRoomModel, window:window})
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: ephemeralMenuItem.visible && (contactMenu.visible || groupInfoMenu.visible || devicesMenuItem.visible)
}
MenuItem{
id: ephemeralMenuItem
//: 'Ephemeral messages' : Item menu to enable ephemeral mode
text: qsTr('conversationMenuEphemeral')
iconMenu: MenuItemStyle.ephemeral.icon
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
visible: conversationMenu.showEphemerals && (SettingsModel.standardChatEnabled || SettingsModel.secureChatEnabled)
onTriggered: {
window.detachVirtualWindow()
window.attachVirtualWindow(Qt.resolvedUrl('Dialogs/EphemeralChatRoom.qml')
,{chatRoomModel:chatRoomModel})
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: scheduleMeetingMenuItem.visible && (contactMenu.visible || groupInfoMenu.visible || devicesMenuItem.visible || ephemeralMenuItem.visible)
}
MenuItem{
id: scheduleMeetingMenuItem
property ConferenceInfoModel conferenceInfoModel: ConferenceInfoModel{}
//: 'Schedule a meeting' : Item menu to schedule a meeting with the chat participants.
text: qsTr('conversationMenuScheduleMeeting')
iconMenu: MenuItemStyle.scheduleMeeting.icon
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
visible: SettingsModel.videoConferenceEnabled && conversationMenu.showScheduleMeeting
onClicked: {
conferenceInfoModel.resetConferenceInfo()
conferenceInfoModel.isScheduled = true
conferenceInfoModel.subject = chatRoomModel.subject
conferenceInfoModel.setParticipants(conversation.chatRoomModel.participants)
window.detachVirtualWindow()
window.attachVirtualWindow(Utils.buildAppDialogUri('NewConference')
,{conferenceInfoModel: scheduleMeetingMenuItem.conferenceInfoModel}
, function (status) {
if( status){
setView('Conferences')
}
})
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: muteMenuItem.visible && (contactMenu.visible || groupInfoMenu.visible || devicesMenuItem.visible || ephemeralMenuItem.visible || scheduleMeetingMenuItem.visible)
}
MenuItem{
id: muteMenuItem
visible: SettingsModel.standardChatEnabled || SettingsModel.secureChatEnabled
text: chatRoomModel.notificationsEnabled
//: 'Disable notifications' : Item menu to disable chat's notifications
? qsTr('conversationMenuDeactivate')
//: 'Enable notifications' : Item menu to enable chat's notifications
: qsTr('conversationMenuActivate')
iconMenu: chatRoomModel.notificationsEnabled ? MenuItemStyle.notifications.off : MenuItemStyle.notifications.on
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2
onTriggered: {
chatRoomModel.notificationsEnabled = !chatRoomModel.notificationsEnabled
}
}
Rectangle{
height:visible ? 1 : 0
width:parent.width
color: ConversationStyle.menu.separatorColor.color
visible: deleteMenuItem.visible && (contactMenu.visible || groupInfoMenu.visible || devicesMenuItem.visible || ephemeralMenuItem.visible || scheduleMeetingMenuItem.visible || muteMenuItem.visible)
}
MenuItem{
id: deleteMenuItem
//: 'Delete history' : Item menu to delete the chat's history
text: qsTr('conversationMenuDelete')
iconMenu: MenuItemStyle.deleteEntry.icon
iconSizeMenu: 40
menuItemStyle : MenuItemStyle.aux2Error
visible: true
onTriggered: {
Logic.removeAllEntries()
}
TooltipArea {
text: qsTr('cleanHistory')
}
}
}
}
}
}
// ---------------------------------------------------------------------------
// Messages/Calls filters.
// ---------------------------------------------------------------------------
Borders {
id:filtersBar
Layout.fillWidth: true
Layout.preferredHeight: active ? ConversationStyle.filters.height : 0
borderColor: ConversationStyle.filters.border.colorModel.color
bottomWidth: ConversationStyle.filters.border.bottomWidth
color: ConversationStyle.filters.backgroundColor.color
topWidth: ConversationStyle.filters.border.topWidth
visible: chatRoomModel && (!chatRoomModel.haveEncryption && SettingsModel.standardChatEnabled || chatRoomModel.haveEncryption && SettingsModel.secureChatEnabled)
ExclusiveButtons {
id: filterButtons
anchors {
left: parent.left
leftMargin: ConversationStyle.filters.leftMargin
verticalCenter: parent.verticalCenter
}
texts: [
qsTr('displayCallsAndMessages'),
qsTr('displayCalls'),
qsTr('displayMessages')
]
onClicked: Logic.updateChatFilter(button)
}
BusyIndicator{
id: chatLoading
width: 20
height: 20
anchors.left: filterButtons.right
anchors.leftMargin: 50
anchors.verticalCenter: parent.verticalCenter
color: BusyIndicatorStyle.alternateColor.color
running: chatArea.tryingToLoadMoreEntries
}
// -------------------------------------------------------------------------
// Search.
// -------------------------------------------------------------------------
MouseArea{
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.rightMargin: 10
anchors.topMargin: 10
anchors.bottomMargin: 10
width: 30
Icon{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
icon: (searchView.visible? 'close_custom': 'search_custom')
iconSize: 30
overwriteColor: ConversationStyle.bar.searchIconColor.color
}
onClicked: {
searchView.visible = !searchView.visible
chatRoomProxyModel.filterText = searchView.text
}
}
Rectangle{
id:searchView
property alias text: searchBar.text
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left : chatLoading.right
anchors.rightMargin: 10
anchors.leftMargin: 50
anchors.topMargin: 10
anchors.bottomMargin: 10
visible: true
TextField {
id:searchBar
anchors {
fill: parent
margins: 0
}
width: parent.width-14
icon: text != '' ? 'close_custom' : 'search_custom'
overwriteColor: ConversationStyle.filters.iconColor.color
//: 'Search in messages' : this is a placeholder when searching something in the timeline list
placeholderText: qsTr('searchMessagesPlaceholder')
onTextChanged: searchDelay.restart()
font.pointSize: ConversationStyle.filters.pointSize
Timer{
id: searchDelay
interval: 500
running: false
onTriggered: if( searchView.visible){
chatRoomProxyModel.filterText = searchBar.text
}
}
}
}
}
// ---------------------------------------------------------------------------
// Chat.
// ---------------------------------------------------------------------------
Chat {
id:chatArea
Layout.fillHeight: true
Layout.fillWidth: true
proxyModel: ChatRoomProxyModel {
id: chatRoomProxyModel
function updateFilter(){
if ( chatRoomModel && ((!chatRoomModel.haveEncryption && !SettingsModel.standardChatEnabled)
|| (chatRoomModel.haveEncryption && !SettingsModel.secureChatEnabled)) ) {
setEntryTypeFilter(ChatRoomModel.CallEntry)
}
}
chatRoomModel: conversation.chatRoomModel
fullPeerAddress: conversation.fullPeerAddress
fullLocalAddress: conversation.fullLocalAddress
localAddress: conversation.localAddress// Reload is done on localAddress. Use this order
onChatRoomModelChanged: updateFilter()
Component.onCompleted: {
updateFilter()
}
}
onAddContactClicked: window.setView('ContactEdit', {
sipAddress: contactAddress
})
onViewContactClicked: window.setView('ContactEdit', {
sipAddress: contactAddress
})
}
Connections {
target: AccountSettingsModel
onSipAddressChanged: {
if (conversation.localAddress !== AccountSettingsModel.sipAddress) {
window.setView('Home')
}
}
}
}