mirror of
https://gitlab.linphone.org/BC/public/linphone-desktop.git
synced 2026-04-19 07:48:30 +00:00
Add transfer icon in forward menu. Confirmation on forward messages. Fix forward button on search bar in chat room selection. Revert back friends capability checks on lime. Update SDK to 5.2.92
469 lines
18 KiB
QML
469 lines
18 KiB
QML
import QtQuick 2.7
|
|
import QtQuick.Controls 2.2
|
|
import QtQuick.Layouts 1.3
|
|
|
|
import Common 1.0
|
|
import Linphone 1.0
|
|
import Linphone.Styles 1.0
|
|
import Utils 1.0
|
|
import UtilsCpp 1.0
|
|
import LinphoneEnums 1.0
|
|
|
|
import Units 1.0
|
|
|
|
import 'Chat.js' as Logic
|
|
import 'qrc:/ui/scripts/Utils/utils.js' as Utils
|
|
|
|
// =============================================================================
|
|
|
|
Rectangle {
|
|
id: container
|
|
|
|
property alias proxyModel: chat.model // ChatRoomProxyModel
|
|
property alias tryingToLoadMoreEntries : chat.tryToLoadMoreEntries
|
|
|
|
property alias noticeBannerText : messageBlock.noticeBannerText // When set, show a banner with text and hide after some time
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
signal messageToSend (string text)
|
|
signal addContactClicked(string contactAddress)
|
|
signal viewContactClicked(string contactAddress)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
color: ChatStyle.colorModel.color
|
|
clip: true
|
|
Timer{// Let some time to have a better cell sizes
|
|
id: repositionerDelay
|
|
property int indexToMove
|
|
interval: 100
|
|
onTriggered: chat.positionViewAtIndex(indexToMove, ListView.Center)
|
|
}
|
|
function positionViewAtIndex(index){
|
|
chat.bindToEnd = false
|
|
chat.positionViewAtIndex(index, ListView.Center)
|
|
repositionerDelay.indexToMove = index
|
|
repositionerDelay.restart()
|
|
}
|
|
|
|
function goToMessage(message){
|
|
positionViewAtIndex(container.proxyModel.loadTillMessage(message))
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
ScrollableListView {
|
|
id: chat
|
|
// -----------------------------------------------------------------------
|
|
property bool displaying: false
|
|
property bool loadingEntries: (container.proxyModel.chatRoomModel && container.proxyModel.chatRoomModel.entriesLoading) || displaying
|
|
property bool tryToLoadMoreEntries: loadingEntries || remainingLoadersCount>0
|
|
property bool isMoving : false // replace moving read-only property to allow using movement signals.
|
|
|
|
// Load optimizations
|
|
property int remainingLoadersCount: 0
|
|
property int visibleItemsEstimation: chat.height / (2 * textMetrics.height) // Title + body
|
|
property int syncLoaderBatch: visibleItemsEstimation // batch of simultaneous loaders on synchronous mode
|
|
//------------------------------------
|
|
|
|
signal refreshContents()
|
|
|
|
onLoadingEntriesChanged: {
|
|
if( loadingEntries && !displaying)
|
|
displaying = true
|
|
}
|
|
onBindToEndChanged: if( bindToEnd){
|
|
markAsReadTimer.start()
|
|
}
|
|
Timer{
|
|
id: markAsReadTimer
|
|
interval: 5000
|
|
repeat: false
|
|
running: false
|
|
onTriggered: if(container.proxyModel.chatRoomModel) container.proxyModel.chatRoomModel.resetMessageCount()
|
|
}
|
|
Timer{
|
|
id: refreshContentsTimer
|
|
interval: 200
|
|
repeat: true
|
|
running: false
|
|
onTriggered: chat.refreshContents()
|
|
}
|
|
TextMetrics{
|
|
id: textMetrics
|
|
font: SettingsModel.textMessageFont
|
|
text: "X"
|
|
}
|
|
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
clip: false
|
|
highlightFollowsCurrentItem: false
|
|
// Use moving event => this is a user action.
|
|
onIsMovingChanged:{
|
|
if(!chat.isMoving && chat.atYBeginning && !chat.loadingEntries){// Moving has stopped. Check if we are at beginning
|
|
chat.displaying = true
|
|
console.log("Trying to load more entries")
|
|
Qt.callLater(container.proxyModel.loadMoreEntriesAsync)
|
|
}
|
|
}
|
|
// -----------------------------------------------------------------------
|
|
Component.onCompleted: {
|
|
Logic.initView()
|
|
refreshContentsTimer.start()
|
|
console.debug("Chat loading with "+chat.visibleItemsEstimation+" visible items. "+chat.count)
|
|
if(chat.visibleItemsEstimation >= chat.count)
|
|
Qt.callLater(container.proxyModel.loadMoreEntriesAsync)
|
|
}
|
|
onMovementStarted: {Logic.handleMovementStarted(); chat.isMoving = true}
|
|
onMovementEnded: {Logic.handleMovementEnded(); chat.isMoving = false}
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
Connections {
|
|
target: proxyModel
|
|
|
|
// When the view is changed (for example `Calls` -> `Messages`),
|
|
// the position is set at end and it can be possible to load
|
|
// more entries.
|
|
onEntryTypeFilterChanged: Logic.initView()
|
|
|
|
onMoreEntriesLoaded: {
|
|
Logic.handleMoreEntriesLoaded(n)// move view to n - 1 item
|
|
chat.displaying = false
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Message/Event renderer.
|
|
// -----------------------------------------------------------------------
|
|
|
|
delegate: Rectangle {
|
|
id: entry
|
|
property var chatEntry: $chatEntry
|
|
property bool isNotice : chatEntry && (chatEntry.type === ChatRoomModel.NoticeEntry)
|
|
property bool isCall : chatEntry && (chatEntry.type === ChatRoomModel.CallEntry)
|
|
property bool isMessage : chatEntry && (chatEntry.type === ChatRoomModel.MessageEntry)
|
|
property var previousItem : proxyModel.count > 0 && index >0 ? proxyModel.getAt(index-1) : null
|
|
property var nextItem : proxyModel.count > 0 ? proxyModel.getAt(index+1) : null // bind to count
|
|
property bool displayDate: chatEntry && !Utils.equalDate(new Date(chatEntry.timestamp), new Date())
|
|
property bool isTopGrouped: isGrouped(entry.previousItem, chatEntry) || false
|
|
property bool isBottomGrouped: isGrouped(chatEntry, entry.nextItem) || false
|
|
|
|
|
|
onIsBottomGroupedChanged: if(loader.item) loader.item.isBottomGrouped = isBottomGrouped
|
|
onIsTopGroupedChanged: if(loader.item) loader.item.isTopGrouped = isTopGrouped
|
|
|
|
function isGrouped(item1, item2){
|
|
return item1 && item2 //Have a previous entry
|
|
&& item1.type == ChatRoomModel.MessageEntry // Previous entry is a message
|
|
&& item2.type == ChatRoomModel.MessageEntry // Previous entry is a message
|
|
&& item2.fromSipAddress == item1.fromSipAddress // Same user
|
|
&& Math.abs((new Date(item2.timestamp)).getTime() - (new Date(item1.timestamp)).getTime())/1000 < 60
|
|
}
|
|
function isHoverEntry () {
|
|
return mouseArea.containsMouse
|
|
}
|
|
|
|
function removeEntry () {
|
|
proxyModel.removeRow(index)
|
|
}
|
|
color: ChatStyle.colorModel.color
|
|
implicitHeight: layout.height + (entry.isBottomGrouped? 1 : ChatStyle.entry.bottomMargin)
|
|
|
|
width: chat.contentWidth // Fill all space
|
|
clip: false
|
|
visible: loader.status == Loader.Ready
|
|
// ---------------------------------------------------------------------
|
|
MouseArea {
|
|
id: mouseArea
|
|
|
|
cursorShape: Qt.ArrowCursor
|
|
hoverEnabled: true
|
|
implicitHeight: layout.height
|
|
width: parent.width + parent.anchors.rightMargin
|
|
anchors.top: parent.top
|
|
//anchors.topMargin: (entry.isTopGrouped? 1 : ChatStyle.entry.bottomMargin)
|
|
clip: false
|
|
acceptedButtons: Qt.NoButton
|
|
onContainsMouseChanged: if(loader.item) loader.item.isHovering = containsMouse
|
|
ColumnLayout{
|
|
id: layout
|
|
spacing: 0
|
|
width: entry.width
|
|
RowLayout{
|
|
id: headerLayout
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignTop | (entry.chatEntry && entry.chatEntry.isOutgoing ? Qt.AlignRight : Qt.AlignLeft)
|
|
Layout.leftMargin: ChatStyle.entry.metaWidth// + ChatStyle.entry.message.extraContent.spacing
|
|
Layout.rightMargin: ChatStyle.entry.message.outgoing.areaSize
|
|
spacing:0
|
|
// Display time.
|
|
visible: !entry.isTopGrouped && !entry.isNotice
|
|
|
|
Text {
|
|
id:timeDisplay
|
|
Layout.alignment: Qt.AlignTop | (entry.chatEntry && entry.chatEntry.isOutgoing ? Qt.AlignRight : Qt.AlignLeft)
|
|
Layout.preferredHeight: implicitHeight// ChatStyle.entry.lineHeight
|
|
//Layout.preferredWidth: ChatStyle.entry.time.width
|
|
|
|
color: ChatStyle.entry.event.text.colorModel.color
|
|
font.pointSize: ChatStyle.entry.time.pointSize
|
|
property bool displayYear: entry.displayDate && (new Date(entry.chatEntry.timestamp)).getFullYear() != (new Date()).getFullYear()
|
|
text: entry.chatEntry
|
|
? (entry.displayDate ? UtilsCpp.toDateString(entry.chatEntry.timestamp, (displayYear ? 'yyyy/':'') + 'MM/dd') + ' ' : '')
|
|
+ UtilsCpp.toTimeString(entry.chatEntry.timestamp, 'hh:mm') + (authorName.visible ? ' - ' : '')
|
|
: ''
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
TooltipArea {
|
|
text: entry.chatEntry ? UtilsCpp.toDateTimeString(entry.chatEntry.timestamp) : ''
|
|
}
|
|
}
|
|
Text{
|
|
id:authorName
|
|
//Layout.leftMargin: timeDisplay.width + ChatStyle.entry.metaWidth + ChatStyle.entry.message.extraContent.spacing
|
|
property var displayName: entry.chatEntry ? entry.chatEntry.fromDisplayName ? entry.chatEntry.fromDisplayName : entry.chatEntry.name : ''
|
|
text : displayName != undefined ? displayName : ''
|
|
|
|
color: ChatStyle.entry.event.text.colorModel.color
|
|
font.pointSize: ChatStyle.entry.event.text.pointSize
|
|
visible: entry.chatEntry && !entry.chatEntry.isOutgoing && text != ''
|
|
}
|
|
}
|
|
// Display content.
|
|
Loader {
|
|
id: loader
|
|
height: (item !== null && typeof(item)!== 'undefined')? item.height: 0
|
|
Layout.fillWidth: true
|
|
source: Logic.getComponentFromEntry(entry.chatEntry)
|
|
z:1
|
|
|
|
asynchronous: index < chat.count - 1 - chat.visibleItemsEstimation
|
|
property int loaderIndex: 0
|
|
function updateSync(){
|
|
if( asynchronous && loaderIndex > 0 && chat.remainingLoadersCount - loaderIndex - chat.syncLoaderBatch <= 0 ) asynchronous = false// Sync load the end
|
|
}
|
|
function stopLoading(){
|
|
chatConnections.enabled = false // No more update is needed : ignore signals.
|
|
--chat.remainingLoadersCount // Loader is ready: remove one from remaining count.
|
|
}
|
|
onStatusChanged: if( status == Loader.Ready) {
|
|
loader.item.isTopGrouped = entry.isTopGrouped
|
|
loader.item.isBottomGrouped = entry.isBottomGrouped
|
|
stopLoading();
|
|
}else if( status == Loader.Error) {
|
|
stopLoading();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
loaderIndex = ++chat.remainingLoadersCount // on new Loader : one more remaining
|
|
}
|
|
Component.onDestruction: if( status != Loader.Ready && status != Loader.Error) {stopLoading();} // Remove remaining count if not loaded
|
|
Connections{
|
|
id: chatConnections
|
|
target: chat
|
|
onRefreshContents:loader.updateSync()
|
|
}
|
|
}
|
|
|
|
Connections{
|
|
target: loader.item
|
|
ignoreUnknownSignals: true
|
|
//: "Copied to clipboard" : when a user copy a text from the menu, this message show up.
|
|
onCopyAllDone: container.noticeBannerText = qsTr("allTextCopied")
|
|
//: "Selection copied to clipboard" : when a user copy a text from the menu, this message show up.
|
|
onCopySelectionDone: container.noticeBannerText = qsTr("selectedTextCopied")
|
|
onReplyClicked: {
|
|
proxyModel.chatRoomModel.reply = entry.chatEntry
|
|
}
|
|
onForwardClicked:{
|
|
window.attachVirtualWindow(Qt.resolvedUrl('../Dialog/SipAddressDialog.qml')
|
|
//: 'Choose where to forward the message' : Dialog title for choosing where to forward the current message.
|
|
, {title: qsTr('forwardDialogTitle'),
|
|
addressSelectedCallback: function (sipAddress) {
|
|
Logic.forwardMessage(undefined, entry.chatEntry, {subject:'', haveEncryption: proxyModel.chatRoomModel.haveEncryption, participants: [sipAddress], toSelect: false} )
|
|
},
|
|
chatRoomSelectedCallback: function (chatRoomModel){
|
|
if(chatRoomModel)
|
|
Logic.forwardMessage(chatRoomModel, entry.chatEntry)
|
|
}
|
|
})
|
|
}
|
|
|
|
onGoToMessage:{
|
|
container.goToMessage(message) // sometimes, there is no access to chat id (maybe because of cleaning component while loading new items). Use a global intermediate.
|
|
}
|
|
onConferenceIcsCopied: container.noticeBannerText = qsTr('conferencesCopiedICS')
|
|
onAddContactClicked: container.addContactClicked(contactAddress)
|
|
onViewContactClicked: container.viewContactClicked(contactAddress)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
footer: Item{
|
|
implicitHeight: composersItem.implicitHeight
|
|
width: parent.width
|
|
clip: false
|
|
Text {
|
|
id: composersItem
|
|
property var composers : container.proxyModel.chatRoomModel ? container.proxyModel.chatRoomModel.composers : undefined
|
|
property int count : composers && composers.length ? composers.length : 0
|
|
color: ChatStyle.composingText.colorModel.color
|
|
font.pointSize: ChatStyle.composingText.pointSize
|
|
height: visible ? undefined : 0
|
|
leftPadding: ChatStyle.composingText.leftPadding
|
|
visible: count > 0 && ( (!proxyModel.chatRoomModel.haveEncryption && SettingsModel.standardChatEnabled)
|
|
|| (proxyModel.chatRoomModel.haveEncryption && SettingsModel.secureChatEnabled) )
|
|
wrapMode: Text.Wrap
|
|
//: '%1 is typing...' indicate that someone is composing in chat
|
|
text:(count==0?'': qsTr('chatTyping','',count).arg(container.proxyModel.getDisplayNameComposers()))
|
|
}
|
|
}
|
|
|
|
ActionButton{
|
|
id: gotToBottomButton
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 10
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 35
|
|
visible: !chat.endIsDisplayed
|
|
onVisibleChanged: updateMarkAsRead()
|
|
Component.onCompleted: updateMarkAsRead()
|
|
function updateMarkAsRead(){
|
|
if(!visible)
|
|
container.proxyModel.markAsReadEnabled = true
|
|
}
|
|
|
|
Connections{
|
|
target: container.proxyModel
|
|
onMarkAsReadEnabledChanged: if( !container.proxyModel.markAsReadEnabled)
|
|
gotToBottomButton.updateMarkAsRead()
|
|
}
|
|
|
|
isCustom: true
|
|
backgroundRadius: width/2
|
|
colorSet: ChatStyle.gotToBottom
|
|
onClicked: {
|
|
chat.bindToEnd = true
|
|
}
|
|
MessageCounter{
|
|
anchors.left: parent.right
|
|
anchors.bottom: parent.top
|
|
anchors.bottomMargin: 0
|
|
anchors.leftMargin: -14
|
|
count: container.proxyModel.chatRoomModel ? container.proxyModel.chatRoomModel.unreadMessagesCount : 0
|
|
showOnlyNumber: true
|
|
iconSize: 15
|
|
pointSize: Units.dp * 7
|
|
}
|
|
}
|
|
|
|
}
|
|
Rectangle {
|
|
id: bottomChatBackground
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: textAreaBorders.height + chatMessagePreview.height+messageBlock.height + chatEmojis.height
|
|
color: ChatStyle.sendArea.backgroundBorder.colorModel.color
|
|
visible: proxyModel.chatRoomModel && !proxyModel.chatRoomModel.isReadOnly && (!proxyModel.chatRoomModel.haveEncryption && SettingsModel.standardChatEnabled || proxyModel.chatRoomModel.haveEncryption && SettingsModel.secureChatEnabled)
|
|
|
|
ColumnLayout{
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
MessageBanner{
|
|
id: messageBlock
|
|
onHeightChanged: height = Layout.preferredHeight
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: fitHeight
|
|
Layout.leftMargin: ChatStyle.entry.leftMargin
|
|
Layout.rightMargin: ChatStyle.entry.rightMargin
|
|
noticeBannerText: ''
|
|
}
|
|
ChatMessagePreview{
|
|
id: chatMessagePreview
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: ChatStyle.sendArea.backgroundBorder.width
|
|
maxHeight: container.height - textAreaBorders.height
|
|
replyChatRoomModel: proxyModel.chatRoomModel
|
|
replyRightMargin: textArea.textRightMargin
|
|
replyLeftMargin: textArea.textLeftMargin
|
|
|
|
}
|
|
ChatEmojis{
|
|
id: chatEmojis
|
|
onEmojiClicked: textArea.insertEmoji(emoji)
|
|
Layout.fillWidth: true
|
|
}
|
|
// -------------------------------------------------------------------------
|
|
// Send area.
|
|
// -------------------------------------------------------------------------
|
|
|
|
Borders {
|
|
id: textAreaBorders
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: textArea.height
|
|
Layout.leftMargin: ChatStyle.sendArea.backgroundBorder.width
|
|
borderColor: ChatStyle.sendArea.border.colorModel.color
|
|
topWidth: ChatStyle.sendArea.border.width
|
|
|
|
DroppableTextArea {
|
|
id: textArea
|
|
|
|
enabled:proxyModel && proxyModel.chatRoomModel ? !proxyModel.chatRoomModel.isReadOnly:false
|
|
isEphemeral : proxyModel && proxyModel.chatRoomModel ? proxyModel.chatRoomModel.ephemeralEnabled:false
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
|
|
height: visible ? ChatStyle.sendArea.height + ChatStyle.sendArea.border.width : 0
|
|
minimumHeight:ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
|
|
maximumHeight:container.height/2
|
|
|
|
dropEnabled: SettingsModel.fileTransferUrl.length > 0
|
|
dropDisabledReason: qsTr('noFileTransferUrl')
|
|
placeholderText: qsTr('newMessagePlaceholder')
|
|
recordAudioToggled: RecorderManager.haveVocalRecorder && RecorderManager.getVocalRecorder().state != LinphoneEnums.RecorderStateClosed
|
|
emojiVisible: chatEmojis.visible
|
|
onDropped: Logic.handleFilesDropped(files)
|
|
property bool componentReady: false
|
|
onTextChanged: {// This slot can be call before the item has been completed because of Rich text. So the cache must not take it account.
|
|
if(componentReady) {
|
|
proxyModel.cachedText=text
|
|
Logic.handleTextChanged(textArea.getText())
|
|
}
|
|
}
|
|
onValidText: {
|
|
textArea.text = ''
|
|
chat.bindToEnd = true
|
|
if(proxyModel.chatRoomModel) {
|
|
proxyModel.sendMessage(text)//Note : 'text' is coming from validText. It's not the text member.
|
|
}else{
|
|
proxyModel.chatRoomModel = CallsListModel.createChat(proxyModel.peerAddress)
|
|
proxyModel.sendMessage(text)
|
|
}
|
|
}
|
|
onAudioRecordRequest: RecorderManager.resetVocalRecorder()
|
|
onEmojiClicked: {
|
|
chatEmojis.visible = !chatEmojis.visible
|
|
}
|
|
Component.onCompleted: {text = proxyModel.cachedText; cursorPosition=text.length;componentReady=true}
|
|
Rectangle{
|
|
anchors.fill:parent
|
|
color:'white'
|
|
opacity: 0.5
|
|
visible:!textArea.enabled
|
|
}
|
|
}
|
|
}// Send Area
|
|
}// ColumnLayout
|
|
}// Bottom background
|
|
}
|
|
}
|
|
|