1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-04-21 21:52:03 +02:00

chatview: replace web chat view with qml listview

Introduces a primitive QML ListView based chat view lacking
features present in the previous web chat view, that will be added
in subsequent commits(styling, preview/media/link/file-transfer
message type support, etc.).

Gitlab: #467
Change-Id: Iedc40f6172a6cdacc48cda6f4187053fbf226713
This commit is contained in:
Trevor Tabah 2021-07-06 10:20:46 -04:00 committed by Ming Rui Zhang
parent e0b28eed7b
commit 2e67dc1bd8
32 changed files with 930 additions and 935 deletions

View file

@ -45,7 +45,6 @@ set(COMMON_SOURCES
${SRC_DIR}/networkmanager.cpp
${SRC_DIR}/runguard.cpp
${SRC_DIR}/updatemanager.cpp
${SRC_DIR}/webchathelpers.cpp
${SRC_DIR}/main.cpp
${SRC_DIR}/smartlistmodel.cpp
${SRC_DIR}/utils.cpp
@ -87,7 +86,8 @@ set(COMMON_SOURCES
${SRC_DIR}/avatarregistry.cpp
${SRC_DIR}/currentconversation.cpp
${SRC_DIR}/currentaccount.cpp
${SRC_DIR}/videodevices.cpp)
${SRC_DIR}/videodevices.cpp
${SRC_DIR}/previewengine.cpp)
set(COMMON_HEADERS
${SRC_DIR}/avatarimageprovider.h
@ -99,7 +99,6 @@ set(COMMON_HEADERS
${SRC_DIR}/version.h
${SRC_DIR}/accountlistmodel.h
${SRC_DIR}/runguard.h
${SRC_DIR}/webchathelpers.h
${SRC_DIR}/rendermanager.h
${SRC_DIR}/connectivitymonitor.h
${SRC_DIR}/jamiavatartheme.h
@ -144,7 +143,8 @@ set(COMMON_HEADERS
${SRC_DIR}/avatarregistry.h
${SRC_DIR}/currentconversation.h
${SRC_DIR}/currentaccount.h
${SRC_DIR}/videodevices.h)
${SRC_DIR}/videodevices.h
${SRC_DIR}/previewengine.h)
set(QML_LIBS
Qt5::Quick
@ -155,7 +155,8 @@ set(QML_LIBS
Qt5::Concurrent
Qt5::QuickControls2
Qt5::WebEngine
Qt5::Core)
Qt5::Core
Qt5::WebEngineWidgets)
set(QML_LIBS_LIST
Core
@ -166,7 +167,8 @@ set(QML_LIBS_LIST
Svg
Sql
QuickControls2
WebEngine)
WebEngine
WebEngineWidgets)
set(WINDOWS_SYS_LIBS Shell32.lib
Ole32.lib

View file

@ -94,7 +94,7 @@
<file>src/mainview/components/AboutPopUp.qml</file>
<file>src/mainview/components/SidePanel.qml</file>
<file>src/mainview/components/WelcomePage.qml</file>
<file>src/mainview/components/MessageWebView.qml</file>
<file>src/mainview/components/ChatView.qml</file>
<file>src/mainview/components/MessageWebViewHeader.qml</file>
<file>src/mainview/components/AccountComboBox.qml</file>
<file>src/mainview/components/CallStackView.qml</file>
@ -143,7 +143,7 @@
<file>src/commoncomponents/contextmenu/GeneralMenuSeparator.qml</file>
<file>src/mainview/components/ParticipantOverlayButton.qml</file>
<file>src/mainview/components/ParticipantControlLayout.qml</file>
<file>src/mainview/components/MessageWebViewFooter.qml</file>
<file>src/mainview/components/ChatViewFooter.qml</file>
<file>src/commoncomponents/emojipicker/EmojiPicker.qml</file>
<file>src/commoncomponents/emojipicker/emojiPickerLoader.js</file>
<file>src/commoncomponents/emojipicker/emojiPickerLoader.html</file>
@ -161,5 +161,7 @@
<file>src/commoncomponents/BackButton.qml</file>
<file>src/commoncomponents/JamiSwitch.qml</file>
<file>src/mainview/components/ReadOnlyFooter.qml</file>
<file>src/commoncomponents/MessageDelegate.qml</file>
<file>src/mainview/components/MessageListView.qml</file>
</qresource>
</RCC>

View file

@ -0,0 +1,54 @@
_ = new QWebChannel(qt.webChannelTransport, function (channel) {
window.jsbridge = channel.objects.jsbridge
})
function log(msg) {
window.jsbridge.log(msg)
}
function getPreviewInfo(messageId, url) {
var title = null
var description = null
var image = null
if (!url.includes("http://") && !url.includes("https://")) {
url = "http://".concat(url)
}
fetch(url, {
mode: 'no-cors',
headers: {'Set-Cookie': 'SameSite=None; Secure'}
}).then(function (response) {
return response.text()
}).then(function (html) {
// create DOM from html string
var parser = new DOMParser()
var doc = parser.parseFromString(html, "text/html")
if (!url.includes("twitter.com")){
title = getTitle(doc)
image = getImage(doc, url)
description = getDescription(doc)
var domain = (new URL(url))
domain = (domain.hostname).replace("www.", "")
} else {
title = "Twitter. It's what's happening."
}
window.jsbridge.infoReady(messageId, {
'title': title,
'image': image,
'description': description,
'url': url,
'domain': domain,
})
}).catch(function (err) {
log("Error occured while fetching document: " + err)
})
}
function parseMessage(messageId, message) {
var links = linkify.find(message)
if (links.length === 0) {
return
}
getPreviewInfo(messageId, links[0].href)
window.jsbridge.linkifyReady(messageId, linkifyStr(message))
}

View file

@ -0,0 +1,191 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtWebEngine 1.10
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
Control {
id: root
readonly property bool isGenerated: Type === Interaction.Type.CALL ||
Type === Interaction.Type.CONTACT
readonly property string author: Author
readonly property var timestamp: Timestamp
readonly property bool isOutgoing: model.Author === ""
readonly property var formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
readonly property bool isImage: MessagesAdapter.isImage(Body)
readonly property bool isAnimatedImage: MessagesAdapter.isAnimatedImage(Body)
readonly property var linkPreviewInfo: LinkPreviewInfo
readonly property var body: Body
readonly property real msgMargin: 64
width: parent ? parent.width : 0
height: loader.height
Loader {
id: loader
property alias isOutgoing: root.isOutgoing
property alias isGenerated: root.isGenerated
readonly property var author: Author
readonly property var body: Body
sourceComponent: isGenerated ?
generatedMsgComp :
userMsgComp
}
Component {
id: generatedMsgComp
Column {
width: root.width
spacing: 2
TextArea {
width: parent.width
text: body
horizontalAlignment: Qt.AlignHCenter
readOnly: true
font.pointSize: 11
color: JamiTheme.chatviewTextColor
}
Item {
id: infoCell
width: parent.width
height: childrenRect.height
Component.onCompleted: children = timestampLabel
}
bottomPadding: 12
}
}
Component {
id: userMsgComp
GridLayout {
id: gridLayout
width: root.width
columns: 2
rows: 2
columnSpacing: 2
rowSpacing: 2
Column {
id: msgCell
Layout.column: isOutgoing ? 0 : 1
Layout.row: 0
Layout.fillWidth: true
Layout.maximumWidth: 640
Layout.preferredHeight: childrenRect.height
Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: isOutgoing ? msgMargin : 0
Layout.rightMargin: isOutgoing ? 0 : msgMargin
Control {
id: msgBlock
width: parent.width
contentItem: Column {
id: msgContent
property real txtWidth: ta.contentWidth + 3 * ta.padding
TextArea {
id: ta
width: parent.width
text: body
padding: 10
font.pointSize: 11
font.hintingPreference: Font.PreferNoHinting
renderType: Text.NativeRendering
textFormat: TextEdit.RichText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
transform: Translate { x: bg.x }
rightPadding: isOutgoing ? padding * 1.5 : 0
color: isOutgoing ?
JamiTheme.messageOutTxtColor :
JamiTheme.messageInTxtColor
}
}
background: Rectangle {
id: bg
anchors.right: isOutgoing ? msgContent.right : undefined
width: msgContent.txtWidth
radius: 18
color: isOutgoing ?
JamiTheme.messageOutBgColor :
JamiTheme.messageInBgColor
}
}
}
Item {
id: infoCell
Layout.column: isOutgoing ? 0 : 1
Layout.row: 1
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
Component.onCompleted: children = timestampLabel
}
Item {
id: avatarCell
Layout.column: isOutgoing ? 1 : 0
Layout.row: 0
Layout.preferredWidth: isOutgoing ? 16 : avatar.width
Layout.preferredHeight: msgCell.height
Layout.leftMargin: isOutgoing ? 0 : 6
Layout.rightMargin: Layout.leftMargin
Avatar {
id: avatar
visible: !isOutgoing
anchors.bottom: parent.bottom
width: 32
height: 32
imageId: author
showPresenceIndicator: false
mode: Avatar.Mode.Contact
}
}
}
}
Label {
id: timestampLabel
text: formattedTime
color: JamiTheme.timestampColor
anchors.right: isGenerated || !isOutgoing ? undefined : parent.right
anchors.rightMargin: 6
anchors.left: isGenerated || isOutgoing ? undefined : parent.left
anchors.leftMargin: 6
anchors.horizontalCenter: isGenerated ? parent.horizontalCenter : undefined
}
opacity: 0
Behavior on opacity { NumberAnimation { duration: 40 } }
Component.onCompleted: {
opacity = 1
if (!Linkified && !isImage && !isAnimatedImage) {
MessagesAdapter.parseMessageUrls(Id, Body)
}
}
}

View file

@ -147,12 +147,12 @@ Item {
// Chatview
property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3)
property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e"
property color chatviewTextColor: textColor
property color chatviewTextColor: darkTheme ? "#f0f0f0" : "#353637"
property color timestampColor: darkTheme ? "#bbb" : "#333"
property color messageOutBgColor: darkTheme ? "#28b1ed" : "#cfd8dc"
property color messageOutTxtColor: textColor
property color messageOutTxtColor: chatviewTextColor
property color messageInBgColor: darkTheme? "#616161" : "#cfebf5"
property color messageInTxtColor: textColor
property color messageInTxtColor: chatviewTextColor
property color fileOutTimestampColor: darkTheme ? "#eee" : "#555"
property color fileInTimestampColor: darkTheme ? "#999" : "#555"
property color chatviewBgColor: darkTheme ? bgDarkMode_ : whiteColor
@ -271,17 +271,17 @@ Item {
property real modalPopupDropShadowSamples: 16
// MessageWebView
property real messageWebViewHairLineSize: 1
property real chatViewHairLineSize: 1
property real messageWebViewHeaderPreferredHeight: 64
property real messageWebViewFooterContentMaximumWidth: 1000
property real messageWebViewFooterPreferredHeight: 50
property real messageWebViewFooterMaximumHeight: 280
property real messageWebViewFooterRowSpacing: 1
property real messageWebViewFooterButtonSize: 36
property real messageWebViewFooterButtonIconSize: 48
property real messageWebViewFooterButtonRadius: 5
property real messageWebViewFooterFileContainerPreferredHeight: 150
property real messageWebViewFooterTextAreaMaximumHeight: 130
property real chatViewMaximumWidth: 900
property real chatViewFooterPreferredHeight: 50
property real chatViewFooterMaximumHeight: 280
property real chatViewFooterRowSpacing: 1
property real chatViewFooterButtonSize: 36
property real chatViewFooterButtonIconSize: 48
property real chatViewFooterButtonRadius: 5
property real chatViewFooterFileContainerPreferredHeight: 150
property real chatViewFooterTextAreaMaximumHeight: 130
// MessageWebView File Transfer Container
property real filesToSendContainerSpacing: 5

View file

@ -108,22 +108,22 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
case Role::UnreadMessagesCount:
return QVariant(item.unreadMessages);
case Role::LastInteractionTimeStamp: {
if (!item.interactions.empty()) {
auto ts = static_cast<qint32>(item.interactions.at(item.lastMessageUid).timestamp);
if (!item.interactions->empty()) {
auto ts = static_cast<qint32>(item.interactions->at(item.lastMessageUid).timestamp);
return QVariant(ts);
}
break;
}
case Role::LastInteractionDate: {
if (!item.interactions.empty()) {
if (!item.interactions->empty()) {
return QVariant(
Utils::formatTimeString(item.interactions.at(item.lastMessageUid).timestamp));
Utils::formatTimeString(item.interactions->at(item.lastMessageUid).timestamp));
}
break;
}
case Role::LastInteraction: {
if (!item.interactions.empty()) {
return QVariant(item.interactions.at(item.lastMessageUid).body);
if (!item.interactions->empty()) {
return QVariant(item.interactions->at(item.lastMessageUid).body);
}
break;
}

View file

@ -60,6 +60,7 @@ CurrentConversation::updateData()
set_needsSyncing(convInfo.needsSyncing);
set_isSip(accInfo.profileInfo.type == profile::Type::SIP);
set_callId(convInfo.getCallId());
set_allMessagesLoaded(convInfo.allMessagesLoaded);
if (accInfo.callModel->hasCall(callId_)) {
auto call = accInfo.callModel->getCall(callId_);
set_callState(call.status);

View file

@ -44,6 +44,7 @@ class CurrentConversation final : public QObject
QML_PROPERTY(bool, inCall)
QML_PROPERTY(bool, isTemporary)
QML_PROPERTY(bool, isContact)
QML_PROPERTY(bool, allMessagesLoaded)
public:
explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2015-2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@ -25,6 +25,7 @@
#include "appsettingsmanager.h"
#include "connectivitymonitor.h"
#include "systemtray.h"
#include "previewengine.h"
#include <QAction>
#include <QCommandLineParser>
@ -149,18 +150,12 @@ MainApplication::MainApplication(int& argc, char** argv)
, connectivityMonitor_(new ConnectivityMonitor(this))
, settingsManager_(new AppSettingsManager(this))
, systemTray_(new SystemTray(settingsManager_.get(), this))
, previewEngine_(new PreviewEngine(this))
{
QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); });
}
MainApplication::~MainApplication()
{
engine_.reset();
systemTray_.reset();
settingsManager_.reset();
lrcInstance_.reset();
connectivityMonitor_.reset();
}
MainApplication::~MainApplication() {}
bool
MainApplication::init()
@ -414,6 +409,7 @@ MainApplication::initQmlLayer()
systemTray_.get(),
lrcInstance_.get(),
settingsManager_.get(),
previewEngine_.get(),
&screenInfo_,
this);

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@ -35,6 +35,7 @@ class ConnectivityMonitor;
class AppSettingsManager;
class SystemTray;
class CallAdapter;
class PreviewEngine;
// Provides information about the screen the app is displayed on
class ScreenInfo : public QObject
@ -97,8 +98,7 @@ private:
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
QScopedPointer<AppSettingsManager> settingsManager_;
QScopedPointer<SystemTray> systemTray_;
QScopedPointer<PreviewEngine> previewEngine_;
ScreenInfo screenInfo_;
CallAdapter* callAdapter_;
};

View file

@ -76,8 +76,8 @@ Rectangle {
callStackView.needToCloseInCallConversationAndPotentialWindow()
LRCInstance.deselectConversation()
if (isPageInStack("callStackViewObject", sidePanelViewStack) ||
isPageInStack("communicationPageMessageWebView", sidePanelViewStack) ||
isPageInStack("communicationPageMessageWebView", mainViewStack) ||
isPageInStack("chatView", sidePanelViewStack) ||
isPageInStack("chatView", mainViewStack) ||
isPageInStack("callStackViewObject", mainViewStack)) {
sidePanelViewStack.pop(StackView.Immediate)
mainViewStack.pop(welcomePage, StackView.Immediate)
@ -98,10 +98,10 @@ Rectangle {
function pushCommunicationMessageWebView() {
if (sidePanelOnly) {
sidePanelViewStack.pop(StackView.Immediate)
sidePanelViewStack.push(communicationPageMessageWebView, StackView.Immediate)
sidePanelViewStack.push(chatView, StackView.Immediate)
} else {
mainViewStack.pop(welcomePage, StackView.Immediate)
mainViewStack.push(communicationPageMessageWebView, StackView.Immediate)
mainViewStack.push(chatView, StackView.Immediate)
}
}
@ -164,24 +164,17 @@ Rectangle {
}
function setMainView(convId) {
if (!(communicationPageMessageWebView.jsLoaded)) {
communicationPageMessageWebView.jsLoadedChanged.connect(
function(convId) {
return function() { setMainView(convId) }
}(convId))
return
}
var item = ConversationsAdapter.getConvInfoMap(convId)
if (item.convId === undefined)
return
communicationPageMessageWebView.headerUserAliasLabelText = item.title
communicationPageMessageWebView.headerUserUserNameLabelText = item.bestId
chatView.headerUserAliasLabelText = item.title
chatView.headerUserUserNameLabelText = item.bestId
if (item.callStackViewShouldShow) {
if (inSettingsView) {
toggleSettingsView()
}
MessagesAdapter.setupChatView(item)
callStackView.setLinkedWebview(communicationPageMessageWebView)
callStackView.setLinkedWebview(chatView)
callStackView.responsibleAccountId = LRCInstance.currentAccountId
callStackView.responsibleConvUid = convId
callStackView.isAudioOnly = item.isAudioOnly
@ -201,13 +194,13 @@ Rectangle {
callStackView.needToCloseInCallConversationAndPotentialWindow()
MessagesAdapter.setupChatView(item)
pushCommunicationMessageWebView()
communicationPageMessageWebView.focusMessageWebView()
chatView.focusChatView()
currentConvUID = convId
} else if (isPageInStack("callStackViewObject", sidePanelViewStack)
|| isPageInStack("callStackViewObject", mainViewStack)) {
callStackView.needToCloseInCallConversationAndPotentialWindow()
pushCommunicationMessageWebView()
communicationPageMessageWebView.focusMessageWebView()
chatView.focusChatView()
}
}
}
@ -396,21 +389,12 @@ Rectangle {
onSettingsBackArrowClicked: sidePanelViewStack.pop(StackView.Immediate)
}
MessageWebView {
id: communicationPageMessageWebView
objectName: "communicationPageMessageWebView"
signal toSendMessageContentSaved(string arg)
signal toMessagesCleared
signal toMessagesLoaded
ChatView {
id: chatView
objectName: "chatView"
visible: false
Component.onCompleted: {
// Set qml MessageWebView object pointer to c++.
MessagesAdapter.setQmlObject(this)
}
Component.onCompleted: MessagesAdapter.setQmlObject(this)
}
onWidthChanged: {

View file

@ -0,0 +1,147 @@
/*
* Copyright (C) 2020-2021 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
Rectangle {
id: root
property string headerUserAliasLabelText: ""
property string headerUserUserNameLabelText: ""
property bool allMessagesLoaded
signal needToHideConversationInCall
signal messagesCleared
signal messagesLoaded
function focusChatView() {
chatViewFooter.textInput.forceActiveFocus()
}
color: JamiTheme.chatviewBgColor
ColumnLayout {
anchors.fill: root
spacing: 0
MessageWebViewHeader {
id: messageWebViewHeader
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.messageWebViewHeaderPreferredHeight
Layout.maximumHeight: JamiTheme.messageWebViewHeaderPreferredHeight
userAliasLabelText: headerUserAliasLabelText
userUserNameLabelText: headerUserUserNameLabelText
DropArea {
anchors.fill: parent
onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
}
onBackClicked: {
mainView.showWelcomeView()
}
onNeedToHideConversationInCall: {
root.needToHideConversationInCall()
}
onPluginSelector: {
// Create plugin handler picker - PLUGINS
PluginHandlerPickerCreation.createPluginHandlerPickerObjects(
root, false)
PluginHandlerPickerCreation.calculateCurrentGeo(root.width / 2,
root.height / 2)
PluginHandlerPickerCreation.openPluginHandlerPicker()
}
}
StackLayout {
id: chatViewStack
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
Layout.fillHeight: true
Layout.topMargin: JamiTheme.chatViewHairLineSize
Layout.bottomMargin: JamiTheme.chatViewHairLineSize
currentIndex: CurrentConversation.isRequest ||
CurrentConversation.needsSyncing
Loader {
active: CurrentConversation.id !== ""
sourceComponent: MessageListView {
DropArea {
anchors.fill: parent
onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
}
}
}
InvitationView {
id: invitationView
Layout.fillWidth: true
Layout.fillHeight: true
}
}
ReadOnlyFooter {
visible: CurrentConversation.readOnly
Layout.fillWidth: true
}
ChatViewFooter {
id: chatViewFooter
visible: {
if (CurrentConversation.needsSyncing || CurrentConversation.readOnly)
return false
else if (CurrentConversation.isSwarm && CurrentConversation.isRequest)
return false
return true
}
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.maximumHeight: JamiTheme.chatViewFooterMaximumHeight
DropArea {
anchors.fill: parent
onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
}
}
}
}

View file

@ -136,7 +136,7 @@ Rectangle {
emojiPicker.y = Qt.binding(function() {
var buttonY = JamiQmlUtils.audioRecordMessageButtonInMainViewPoint.y
return buttonY - emojiPicker.height - messageBar.marginSize
- JamiTheme.messageWebViewHairLineSize
- JamiTheme.chatViewHairLineSize
})
emojiPicker.openEmojiPicker()
@ -201,9 +201,9 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: footerColumnLayout.width
Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
Layout.preferredHeight: filesToSendCount ?
JamiTheme.messageWebViewFooterFileContainerPreferredHeight : 0
JamiTheme.chatViewFooterFileContainerPreferredHeight : 0
}
}
}

View file

@ -48,9 +48,9 @@ ColumnLayout {
id: messageBarHairLine
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.preferredHeight: JamiTheme.messageWebViewHairLineSize
Layout.preferredHeight: JamiTheme.chatViewHairLineSize
Layout.fillWidth: true
Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
color: JamiTheme.tabbarBorderColor
}
@ -60,20 +60,20 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter
Layout.fillWidth: true
Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
spacing: JamiTheme.messageWebViewFooterRowSpacing
spacing: JamiTheme.chatViewFooterRowSpacing
PushButton {
id: sendFileButton
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: marginSize
Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.messageWebViewFooterButtonRadius
preferredSize: JamiTheme.messageWebViewFooterButtonIconSize - 6
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
toolTipText: JamiStrings.sendFile
@ -89,11 +89,11 @@ ColumnLayout {
id: audioRecordMessageButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.messageWebViewFooterButtonRadius
preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize
toolTipText: JamiStrings.leaveAudioMessage
@ -111,11 +111,11 @@ ColumnLayout {
id: videoRecordMessageButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.messageWebViewFooterButtonRadius
preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize
toolTipText: JamiStrings.leaveVideoMessage
@ -144,10 +144,10 @@ ColumnLayout {
Layout.fillWidth: true
Layout.margins: marginSize / 2
Layout.preferredHeight: {
return JamiTheme.messageWebViewFooterPreferredHeight
> contentHeight ? JamiTheme.messageWebViewFooterPreferredHeight : contentHeight
return JamiTheme.chatViewFooterPreferredHeight
> contentHeight ? JamiTheme.chatViewFooterPreferredHeight : contentHeight
}
Layout.maximumHeight: JamiTheme.messageWebViewFooterTextAreaMaximumHeight
Layout.maximumHeight: JamiTheme.chatViewFooterTextAreaMaximumHeight
- marginSize / 2
onSendMessagesRequired: root.sendMessageButtonClicked()
@ -158,11 +158,11 @@ ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: sendMessageButton.visible ? 0 : marginSize
Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.messageWebViewFooterButtonRadius
preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize
toolTipText: JamiStrings.addEmoji
@ -183,11 +183,11 @@ ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: visible ? marginSize : 0
Layout.preferredWidth: scale * JamiTheme.messageWebViewFooterButtonSize
Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
Layout.preferredWidth: scale * JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.messageWebViewFooterButtonRadius
preferredSize: JamiTheme.messageWebViewFooterButtonIconSize - 6
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
toolTipText: JamiStrings.send

View file

@ -136,7 +136,7 @@ Flickable {
// Shift + Enter -> Next Line
Keys.onPressed: function (keyEvent) {
if (keyEvent.matches(StandardKey.Paste)) {
MessagesAdapter.pasteKeyDetected()
MessagesAdapter.onPaste()
keyEvent.accepted = true
} else if (keyEvent.key === Qt.Key_Enter ||
keyEvent.key === Qt.Key_Return) {

View file

@ -0,0 +1,106 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
ListView {
id: root
// fade-in mechanism
Component.onCompleted: fadeAnimation.start()
Rectangle {
id: overlay
anchors.fill: parent
color: JamiTheme.chatviewBgColor
visible: opacity !== 0
SequentialAnimation {
id: fadeAnimation
NumberAnimation {
target: overlay; property: "opacity"
to: 1; duration: 0
}
NumberAnimation {
target: overlay; property: "opacity"
to: 0; duration: 240
}
}
}
Connections {
target: CurrentConversation
function onIdChanged() { fadeAnimation.start() }
}
topMargin: 12
bottomMargin: 6
spacing: 2
anchors.centerIn: parent
height: parent.height
width: parent.width
displayMarginBeginning: 2048
displayMarginEnd: 2048
maximumFlickVelocity: 2048
verticalLayoutDirection: ListView.BottomToTop
clip: true
boundsBehavior: Flickable.StopAtBounds
currentIndex: -1
ScrollBar.vertical: ScrollBar {}
model: MessagesAdapter.messageListModel
delegate: MessageDelegate {}
function getDistanceToBottom() {
const scrollDiff = ScrollBar.vertical.position -
(1.0 - ScrollBar.vertical.size)
return Math.abs(scrollDiff) * contentHeight
}
onAtYBeginningChanged: loadMoreMsgsIfNeeded()
function loadMoreMsgsIfNeeded() {
if (atYBeginning && !CurrentConversation.allMessagesLoaded)
MessagesAdapter.loadMoreMessages()
}
Connections {
target: MessagesAdapter
function onNewInteraction() {
if (root.getDistanceToBottom() < 80 &&
!root.atYEnd) {
Qt.callLater(root.positionViewAtBeginning)
}
}
function onMoreMessagesLoaded() {
if (root.contentHeight < root.height) {
root.loadMoreMsgsIfNeeded()
}
}
}
}

View file

@ -1,287 +0,0 @@
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtWebEngine 1.10
import QtWebChannel 1.15
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
Rectangle {
id: root
property string headerUserAliasLabelText: ""
property string headerUserUserNameLabelText: ""
property bool jsLoaded: false
signal needToHideConversationInCall
signal messagesCleared
signal messagesLoaded
function setSendMessageContent(content) {
jsBridgeObject.setSendMessageContentRequest(content)
}
function focusMessageWebView() {
messageWebViewFooter.textInput.forceActiveFocus()
}
function webViewRunJavaScript(arg) {
messageWebView.runJavaScript(arg)
}
function updateChatviewTheme() {
var theme = 'setTheme("\
--svg-invert-percentage:' + JamiTheme.invertPercentageInDecimal + ';\
--jami-light-blue:' + JamiTheme.jamiLightBlue + ';\
--jami-dark-blue: ' + JamiTheme.jamiDarkBlue + ';\
--text-color: ' + JamiTheme.chatviewTextColor + ';\
--timestamp-color:' + JamiTheme.timestampColor + ';\
--message-out-bg:' + JamiTheme.messageOutBgColor + ';\
--message-out-txt:' + JamiTheme.messageOutTxtColor + ';\
--message-in-bg:' + JamiTheme.messageInBgColor + ';\
--message-in-txt:' + JamiTheme.messageInTxtColor + ';\
--file-in-timestamp-color:' + JamiTheme.fileOutTimestampColor + ';\
--file-out-timestamp-color:' + JamiTheme.fileInTimestampColor + ';\
--bg-color:' + JamiTheme.chatviewBgColor + ';\
--action-icon-color:' + JamiTheme.chatviewButtonColor + ';\
--action-icon-hover-color:' + JamiTheme.hoveredButtonColor + ';\
--action-icon-press-color:' + JamiTheme.pressedButtonColor + ';\
--placeholder-text-color:' + JamiTheme.placeholderTextColor + ';\
--invite-hover-color:' + JamiTheme.inviteHoverColor + ';\
--bg-text-input:' + JamiTheme.bgTextInput + ';\
--bg-invitation-rect:' + JamiTheme.bgInvitationRectColor + ';\
--preview-text-container-color:' + JamiTheme.previewTextContainerColor + ';\
--preview-title-color:' + JamiTheme.previewTitleColor + ';\
--preview-subtitle-color:' + JamiTheme.previewSubtitleColor + ';\
--preview-image-background-color:' + JamiTheme.previewImageBackgroundColor + ';\
--preview-card-container-color:' + JamiTheme.previewCardContainerColor + ';\
--preview-url-color:' + JamiTheme.previewUrlColor + ';")'
messageWebView.runJavaScript("init_picker(" + JamiTheme.darkTheme + ");")
messageWebView.runJavaScript(theme);
}
color: JamiTheme.primaryBackgroundColor
Connections {
target: JamiTheme
function onDarkThemeChanged() {
updateChatviewTheme()
}
}
QtObject {
id: jsBridgeObject
// ID, under which this object will be known at chatview.js side.
WebChannel.id: "jsbridge"
// signals to trigger functions in chatview.js
// mainly used to avoid input arg string escape
signal setSendMessageContentRequest(string content)
// Functions that are exposed, return code can be derived from js side
// by setting callback function.
function deleteInteraction(arg) {
MessagesAdapter.deleteInteraction(arg)
}
function retryInteraction(arg) {
MessagesAdapter.retryInteraction(arg)
}
function openFile(arg) {
MessagesAdapter.openFile(arg)
}
function acceptFile(arg) {
MessagesAdapter.acceptFile(arg)
}
function refuseFile(arg) {
MessagesAdapter.refuseFile(arg)
}
function emitMessagesCleared() {
root.messagesCleared()
}
function emitMessagesLoaded() {
root.messagesLoaded()
}
function copyToDownloads(interactionId, displayName) {
MessagesAdapter.copyToDownloads(interactionId, displayName)
}
function parseI18nData() {
return MessagesAdapter.chatviewTranslatedStrings
}
function loadMessages(n) {
return MessagesAdapter.loadMessages(n)
}
}
ColumnLayout {
anchors.fill: root
spacing: 0
MessageWebViewHeader {
id: messageWebViewHeader
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.messageWebViewHeaderPreferredHeight
Layout.maximumHeight: JamiTheme.messageWebViewHeaderPreferredHeight
userAliasLabelText: headerUserAliasLabelText
userUserNameLabelText: headerUserUserNameLabelText
DropArea {
anchors.fill: parent
onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
}
onBackClicked: {
mainView.showWelcomeView()
}
onNeedToHideConversationInCall: {
root.needToHideConversationInCall()
}
onPluginSelector: {
// Create plugin handler picker - PLUGINS
PluginHandlerPickerCreation.createPluginHandlerPickerObjects(
root, false)
PluginHandlerPickerCreation.calculateCurrentGeo(root.width / 2,
root.height / 2)
PluginHandlerPickerCreation.openPluginHandlerPicker()
}
}
StackLayout {
id: messageWebViewStack
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: JamiTheme.messageWebViewHairLineSize
Layout.bottomMargin: JamiTheme.messageWebViewHairLineSize
currentIndex: CurrentConversation.isRequest || CurrentConversation.needsSyncing
GeneralWebEngineView {
id: messageWebView
Layout.fillWidth: true
Layout.fillHeight: true
onCompletedLoadHtml: ":/chatview.html"
webChannel.registeredObjects: [jsBridgeObject]
DropArea {
anchors.fill: parent
onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
}
onLoadingChanged: {
if (loadRequest.status == WebEngineView.LoadSucceededStatus) {
messageWebView.runJavaScript(UtilsAdapter.getStyleSheet(
"chatcss",
UtilsAdapter.qStringFromFile(
":/chatview.css")))
messageWebView.runJavaScript(UtilsAdapter.getStyleSheet(
"chatwin",
UtilsAdapter.qStringFromFile(
":/chatview-qt.css")))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/linkify.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/linkify-html.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/linkify-string.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/qwebchannel.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/jed.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/emoji.js"))
messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
":/previewInfo.js"))
messageWebView.runJavaScript(
UtilsAdapter.qStringFromFile(":/chatview.js"),
function() {
messageWebView.runJavaScript("init_i18n();")
MessagesAdapter.setDisplayLinks()
updateChatviewTheme()
messageWebView.runJavaScript("displayNavbar(false);")
messageWebView.runJavaScript("hideMessageBar(true);")
jsLoaded = true
})
}
}
}
InvitationView {
id: invitationView
Layout.fillWidth: true
Layout.fillHeight: true
}
}
ReadOnlyFooter {
visible: CurrentConversation.readOnly
Layout.fillWidth: true
}
MessageWebViewFooter {
id: messageWebViewFooter
visible: {
if (CurrentConversation.needsSyncing || CurrentConversation.readOnly)
return false
else if (CurrentConversation.isSwarm && CurrentConversation.isRequest)
return false
return true
}
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.maximumHeight: JamiTheme.messageWebViewFooterMaximumHeight
DropArea {
anchors.fill: parent
onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
}
}
}
}

View file

@ -223,7 +223,7 @@ Rectangle {
lBorderwidth: 0
rBorderwidth: 0
tBorderwidth: 0
bBorderwidth: JamiTheme.messageWebViewHairLineSize
bBorderwidth: JamiTheme.chatViewHairLineSize
borderColor: JamiTheme.tabbarBorderColor
}
}

View file

@ -34,7 +34,7 @@ Control {
Rectangle {
anchors.top: parent.top
height: JamiTheme.messageWebViewHairLineSize
height: JamiTheme.chatViewHairLineSize
width: parent.width
color: JamiTheme.tabbarBorderColor
}

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
@ -26,7 +26,6 @@
#include "appsettingsmanager.h"
#include "qtutils.h"
#include "utils.h"
#include "webchathelpers.h"
#include <api/datatransfermodel.h>
@ -39,13 +38,30 @@
#include <QUrl>
#include <QMimeData>
#include <QBuffer>
#include <QtMath>
MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
PreviewEngine* previewEngine,
LRCInstance* instance,
QObject* parent)
: QmlAdapterBase(instance, parent)
, settingsManager_(settingsManager)
{}
, previewEngine_(previewEngine)
, filteredMsgListModel_(new FilteredMsgListModel(this))
{
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
const QString& convId = lrcInstance_->get_selectedConvUid();
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
filteredMsgListModel_->setSourceModel(conversation.interactions.get());
set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
});
connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady);
connect(previewEngine_,
&PreviewEngine::linkifyReady,
this,
&MessagesAdapter::onMessageLinkified);
}
void
MessagesAdapter::safeInit()
@ -59,71 +75,26 @@ MessagesAdapter::safeInit()
void
MessagesAdapter::setupChatView(const QVariantMap& convInfo)
{
Utils::oneShotConnect(qmlObj_, SIGNAL(messagesCleared()), this, SLOT(slotMessagesCleared()));
setMessagesVisibility(false);
clearChatView();
setIsSwarm(convInfo["isSwarm"].toBool());
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto convId = convInfo["convId"].toString();
if (convInfo["isSwarm"].toBool()) {
convModel->loadConversationMessages(convId, loadChunkSize_);
}
// TODO: current conv observe
Q_EMIT newMessageBarPlaceholderText(convInfo["title"].toString());
}
void
MessagesAdapter::onNewInteraction(const QString& convUid,
const QString& interactionId,
const lrc::api::interaction::Info& interaction)
MessagesAdapter::loadMoreMessages()
{
auto accountId = lrcInstance_->get_currentAccountId();
newInteraction(accountId, convUid, interactionId, interaction);
}
void
MessagesAdapter::onInteractionStatusUpdated(const QString& convUid,
const QString& interactionId,
const lrc::api::interaction::Info& interaction)
{
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
updateInteraction(*currentConversationModel, interactionId, interaction);
}
void
MessagesAdapter::onInteractionRemoved(const QString& convUid, const QString& interactionId)
{
Q_UNUSED(convUid);
removeInteraction(interactionId);
}
void
MessagesAdapter::onNewMessagesAvailable(const QString& accountId, const QString& conversationId)
{
auto* convModel = lrcInstance_->accountModel().getAccountInfo(accountId).conversationModel.get();
auto optConv = convModel->getConversationForUid(conversationId);
if (!optConv)
return;
updateHistory(*convModel, optConv->get().interactions, optConv->get().allMessagesLoaded);
Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));
}
void
MessagesAdapter::updateConversation(const QString& conversationId)
{
if (conversationId != lrcInstance_->get_selectedConvUid())
return;
auto convId = lrcInstance_->get_selectedConvUid();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
if (convInfo.isSwarm()) {
auto* convModel = lrcInstance_->getCurrentConversationModel();
if (auto optConv = convModel->getConversationForUid(conversationId))
setConversationProfileData(optConv->get());
convModel->loadConversationMessages(convId, loadChunkSize_);
}
void
MessagesAdapter::onComposingStatusChanged(const QString& convId,
const QString& contactUri,
bool isComposing)
{
if (convId != lrcInstance_->get_selectedConvUid())
return;
if (!settingsManager_->getValue(Settings::Key::EnableTypingIndicator).toBool()) {
return;
}
contactIsComposing(contactUri, isComposing);
}
void
@ -138,33 +109,9 @@ MessagesAdapter::connectConversationModel()
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::interactionStatusUpdated,
&ConversationModel::conversationMessagesLoaded,
this,
&MessagesAdapter::onInteractionStatusUpdated,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::interactionRemoved,
this,
&MessagesAdapter::onInteractionRemoved,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::newMessagesAvailable,
this,
&MessagesAdapter::onNewMessagesAvailable,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::conversationReady,
this,
&MessagesAdapter::updateConversation,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::composingStatusChanged,
this,
&MessagesAdapter::onComposingStatusChanged,
&MessagesAdapter::onConversationMessagesLoaded,
Qt::UniqueConnection);
}
@ -174,29 +121,6 @@ MessagesAdapter::sendConversationRequest()
lrcInstance_->makeConversationPermanent();
}
void
MessagesAdapter::slotMessagesCleared()
{
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto optConv = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
if (!optConv)
return;
if (optConv->get().isSwarm() && !optConv->get().allMessagesLoaded) {
convModel->loadConversationMessages(optConv->get().uid, 20);
} else {
updateHistory(*convModel, optConv->get().interactions, optConv->get().allMessagesLoaded);
Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));
}
setConversationProfileData(optConv->get());
}
void
MessagesAdapter::slotMessagesLoaded()
{
setMessagesVisibility(true);
}
void
MessagesAdapter::sendMessage(const QString& message)
{
@ -279,7 +203,7 @@ MessagesAdapter::refuseFile(const QString& interactionId)
}
void
MessagesAdapter::pasteKeyDetected()
MessagesAdapter::onPaste()
{
const QMimeData* mimeData = QApplication::clipboard()->mimeData();
@ -328,20 +252,7 @@ MessagesAdapter::userIsComposing(bool isComposing)
}
void
MessagesAdapter::setConversationProfileData(const conversation::Info& convInfo)
{
// make the all the participant avatars available within the web view
for (const auto& participant : convInfo.participants) {
QByteArray ba;
QBuffer bu(&ba);
Utils::conversationAvatar(lrcInstance_, convInfo.uid).save(&bu, "PNG");
setSenderImage(participant, QString::fromLocal8Bit(ba.toBase64()));
}
}
void
MessagesAdapter::newInteraction(const QString& accountId,
const QString& convUid,
MessagesAdapter::onNewInteraction(const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction)
{
@ -350,164 +261,15 @@ MessagesAdapter::newInteraction(const QString& accountId,
if (convUid.isEmpty() || convUid != lrcInstance_->get_selectedConvUid()) {
return;
}
auto accountId = lrcInstance_->get_currentAccountId();
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
auto& convModel = accountInfo.conversationModel;
convModel->clearUnreadInteractions(convUid);
printNewInteraction(*convModel, interactionId, interaction);
Q_EMIT newInteraction(static_cast<int>(interaction.type));
} catch (...) {
}
}
/*
* JS invoke.
*/
void
MessagesAdapter::setMessagesVisibility(bool visible)
{
QString s = QString::fromLatin1(visible ? "showMessagesDiv();" : "hideMessagesDiv();");
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::setIsSwarm(bool isSwarm)
{
QString s = QString::fromLatin1("set_is_swarm(%1)").arg(isSwarm);
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::clearChatView()
{
QString s = QString::fromLatin1("clearMessages();");
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::setDisplayLinks()
{
QString s
= QString::fromLatin1("setDisplayLinks(%1);")
.arg(settingsManager_->getValue(Settings::Key::DisplayHyperlinkPreviews).toBool());
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::updateHistory(lrc::api::ConversationModel& conversationModel,
MessagesList interactions,
bool allLoaded)
{
auto conversationId = lrcInstance_->get_selectedConvUid();
auto interactionsStr
= interactionsToJsonArrayObject(conversationModel, conversationId, interactions).toUtf8();
QString s;
QTextStream out(&s);
out << "updateHistory(" << interactionsStr << ", " << (allLoaded? "true" : "false") << ");";
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
conversationModel.clearUnreadInteractions(conversationId);
}
void
MessagesAdapter::setSenderImage(const QString& sender, const QString& senderImage)
{
QJsonObject setSenderImageObject = QJsonObject();
setSenderImageObject.insert("sender_contact_method", QJsonValue(sender));
setSenderImageObject.insert("sender_image", QJsonValue(senderImage));
auto setSenderImageObjectString = QString(
QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact));
QString s = QString::fromLatin1("setSenderImage(%1);")
.arg(setSenderImageObjectString.toUtf8().constData());
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::printNewInteraction(lrc::api::ConversationModel& conversationModel,
const QString& msgId,
const lrc::api::interaction::Info& interaction)
{
auto interactionObject = interactionToJsonInteractionObject(conversationModel,
lrcInstance_->get_selectedConvUid(),
msgId,
interaction)
.toUtf8();
if (interactionObject.isEmpty()) {
return;
}
QString s = QString::fromLatin1("addMessage(%1);").arg(interactionObject.constData());
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::updateInteraction(lrc::api::ConversationModel& conversationModel,
const QString& msgId,
const lrc::api::interaction::Info& interaction)
{
auto interactionObject = interactionToJsonInteractionObject(conversationModel,
lrcInstance_->get_selectedConvUid(),
msgId,
interaction)
.toUtf8();
if (interactionObject.isEmpty()) {
return;
}
QString s = QString::fromLatin1("updateMessage(%1);").arg(interactionObject.constData());
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::setMessagesImageContent(const QString& path, bool isBased64)
{
if (isBased64) {
QString param = QString("addImage_base64('%1')").arg(path);
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
} else {
QString param = QString("addImage_path('file://%1')").arg(path);
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
}
}
void
MessagesAdapter::setMessagesFileContent(const QString& path)
{
qint64 fileSize = QFileInfo(path).size();
QString fileName = QFileInfo(path).fileName();
QString param = QString("addFile_path('%1','%2','%3')")
.arg(path, fileName, Utils::humanFileSize(fileSize));
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
}
void
MessagesAdapter::removeInteraction(const QString& interactionId)
{
QString s = QString::fromLatin1("removeInteraction(%1);").arg(interactionId);
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::setSendMessageContent(const QString& content)
{
QMetaObject::invokeMethod(qmlObj_, "setSendMessageContent", Q_ARG(QVariant, content));
}
void
MessagesAdapter::contactIsComposing(const QString& contactUri, bool isComposing)
{
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto convInfo = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
if (!convInfo)
return;
auto& conv = convInfo->get();
bool showIsComposing = conv.participants.first() == contactUri;
if (showIsComposing) {
QString s
= QString::fromLatin1("showTypingIndicator(`%1`, %2);").arg(contactUri).arg(isComposing);
QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
}
void
MessagesAdapter::acceptInvitation(const QString& convId)
{
@ -577,12 +339,75 @@ MessagesAdapter::removeContact(const QString& convUid, bool banContact)
}
void
MessagesAdapter::loadMessages(int n)
MessagesAdapter::onPreviewInfoReady(QString messageId, QVariantMap info)
{
auto* convModel = lrcInstance_->getCurrentConversationModel();
auto convOpt = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
if (!convOpt)
return;
if (convOpt->get().isSwarm() && !convOpt->get().allMessagesLoaded)
convModel->loadConversationMessages(convOpt->get().uid, n);
const QString& convId = lrcInstance_->get_selectedConvUid();
const QString& accId = lrcInstance_->get_currentAccountId();
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
conversation.interactions->addHyperlinkInfo(messageId, info);
}
void
MessagesAdapter::onConversationMessagesLoaded(uint32_t, const QString& convId)
{
if (convId != lrcInstance_->get_selectedConvUid())
return;
Q_EMIT moreMessagesLoaded();
}
void
MessagesAdapter::parseMessageUrls(const QString& messageId, const QString& msg)
{
previewEngine_->parseMessage(messageId, msg);
}
void
MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& linkified)
{
const QString& convId = lrcInstance_->get_selectedConvUid();
const QString& accId = lrcInstance_->get_currentAccountId();
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
conversation.interactions->linkifyMessage(messageId, linkified);
}
bool
MessagesAdapter::isImage(const QString& message)
{
QRegularExpression pattern("[^\\s]+(.*?)\\.(jpg|jpeg|png)$",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch match = pattern.match(message);
return match.hasMatch();
}
bool
MessagesAdapter::isAnimatedImage(const QString& msg)
{
QRegularExpression pattern("[^\\s]+(.*?)\\.(gif|apng|webp|avif|flif)$",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch match = pattern.match(msg);
return match.hasMatch();
}
QString
MessagesAdapter::getFormattedTime(const quint64 timestamp)
{
const auto now = QDateTime::currentDateTime();
const auto seconds = now.toSecsSinceEpoch() - timestamp;
auto interval = qFloor(seconds / (3600 * 24));
if (interval > 5)
return QLocale::system().toString(QDateTime::fromSecsSinceEpoch(timestamp),
QLocale::ShortFormat);
if (interval > 1)
return QObject::tr("%1 days ago").arg(interval);
if (interval == 1)
return QObject::tr("one day ago");
interval = qFloor(seconds / 3600);
if (interval > 1)
return QObject::tr("%1 hours ago").arg(interval);
if (interval == 1)
return QObject::tr("one hour ago");
interval = qFloor(seconds / 60);
if (interval > 1)
return QObject::tr("%1 minutes ago").arg(interval);
return QObject::tr("just now");
}

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
@ -20,28 +20,66 @@
#include "lrcinstance.h"
#include "qmladapterbase.h"
#include "previewengine.h"
#include "api/chatview.h"
#include <QObject>
#include <QString>
#include <QSortFilterProxyModel>
class FilteredMsgListModel final : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit FilteredMsgListModel(QObject* parent = nullptr)
: QSortFilterProxyModel(parent)
{
sort(0, Qt::AscendingOrder);
}
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
{
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
auto type = sourceModel()->data(index, MessageList::Role::Type).toInt();
return static_cast<interaction::Type>(type) != interaction::Type::MERGE;
};
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
{
return left.row() > right.row();
};
};
class AppSettingsManager;
class MessagesAdapter final : public QmlAdapterBase
{
Q_OBJECT
Q_PROPERTY(QVariantMap chatviewTranslatedStrings MEMBER chatviewTranslatedStrings_ CONSTANT)
QML_RO_PROPERTY(QVariant, messageListModel)
public:
explicit MessagesAdapter(AppSettingsManager* settingsManager,
PreviewEngine* previewEngine,
LRCInstance* instance,
QObject* parent = nullptr);
~MessagesAdapter() = default;
Q_SIGNALS:
void newInteraction(int type);
void newMessageBarPlaceholderText(QString placeholderText);
void newFilePasted(QString filePath);
void newTextPasted();
void previewInformationToQML(QString messageId, QStringList previewInformation);
void moreMessagesLoaded();
protected:
void safeInit() override;
Q_INVOKABLE void setupChatView(const QVariantMap& convInfo);
Q_INVOKABLE void loadMoreMessages();
Q_INVOKABLE void connectConversationModel();
Q_INVOKABLE void sendConversationRequest();
Q_INVOKABLE void removeConversation(const QString& convUid);
@ -51,70 +89,39 @@ protected:
Q_INVOKABLE void refuseInvitation(const QString& convUid = "");
Q_INVOKABLE void blockConversation(const QString& convUid = "");
Q_INVOKABLE void unbanContact(int index);
// JS Q_INVOKABLE.
Q_INVOKABLE void setDisplayLinks();
Q_INVOKABLE void sendMessage(const QString& message);
Q_INVOKABLE void sendFile(const QString& message);
Q_INVOKABLE void retryInteraction(const QString& interactionId);
Q_INVOKABLE void deleteInteraction(const QString& interactionId);
Q_INVOKABLE void openUrl(const QString& url);
Q_INVOKABLE void openFile(const QString& arg);
Q_INVOKABLE void acceptFile(const QString& arg);
Q_INVOKABLE void refuseFile(const QString& arg);
Q_INVOKABLE void pasteKeyDetected();
Q_INVOKABLE void userIsComposing(bool isComposing);
Q_INVOKABLE void loadMessages(int n);
Q_INVOKABLE void openUrl(const QString& url);
Q_INVOKABLE void openFile(const QString& arg);
Q_INVOKABLE void retryInteraction(const QString& interactionId);
Q_INVOKABLE void deleteInteraction(const QString& interactionId);
Q_INVOKABLE void copyToDownloads(const QString& interactionId, const QString& displayName);
Q_INVOKABLE void userIsComposing(bool isComposing);
Q_INVOKABLE bool isImage(const QString& msg);
Q_INVOKABLE bool isAnimatedImage(const QString& msg);
Q_INVOKABLE QString getFormattedTime(const quint64 timestamp);
Q_INVOKABLE void parseMessageUrls(const QString& messageId, const QString& msg);
Q_INVOKABLE void onPaste();
// Run corrsponding js functions, c++ to qml.
void setMessagesVisibility(bool visible);
void setIsSwarm(bool isSwarm);
void clearChatView();
void updateHistory(ConversationModel& conversationModel,
MessagesList interactions,
bool allLoaded);
void setSenderImage(const QString& sender, const QString& senderImage);
void printNewInteraction(lrc::api::ConversationModel& conversationModel,
const QString& msgId,
const lrc::api::interaction::Info& interaction);
void updateInteraction(lrc::api::ConversationModel& conversationModel,
const QString& msgId,
const lrc::api::interaction::Info& interaction);
void setMessagesImageContent(const QString& path, bool isBased64 = false);
void setMessagesFileContent(const QString& path);
void removeInteraction(const QString& interactionId);
void setSendMessageContent(const QString& content);
void contactIsComposing(const QString& contactUri, bool isComposing);
Q_SIGNALS:
void newInteraction(int type);
void newMessageBarPlaceholderText(QString placeholderText);
void newFilePasted(QString filePath);
void newTextPasted();
private Q_SLOTS:
void slotMessagesCleared();
void slotMessagesLoaded();
void onNewInteraction(const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction);
void onInteractionStatusUpdated(const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction);
void onInteractionRemoved(const QString& convUid, const QString& interactionId);
void onNewMessagesAvailable(const QString& accountId, const QString& conversationId);
void updateConversation(const QString& conversationId);
void onComposingStatusChanged(const QString& uid, const QString& contactUri, bool isComposing);
void onPreviewInfoReady(QString messageIndex, QVariantMap urlInMessage);
void onConversationMessagesLoaded(uint32_t requestId, const QString& convId);
void onMessageLinkified(const QString& messageId, const QString& linkified);
private:
void setConversationProfileData(const lrc::api::conversation::Info& convInfo);
void newInteraction(const QString& accountId,
const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction);
const QVariantMap chatviewTranslatedStrings_ {lrc::api::chatview::getTranslatedStrings()};
AppSettingsManager* settingsManager_;
PreviewEngine* previewEngine_;
FilteredMsgListModel* filteredMsgListModel_;
static constexpr const int loadChunkSize_ {20};
};

94
src/previewengine.cpp Normal file
View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "previewengine.h"
#include <QtWebEngine>
#include <QWebEngineScript>
#include <QWebEngineProfile>
#include <QWebEngineSettings>
PreviewEngine::PreviewEngine(QObject* parent)
: QWebEngineView(qobject_cast<QWidget*>(parent))
, pimpl_(new PreviewEnginePrivate(this))
{
QWebEngineProfile* profile = QWebEngineProfile::defaultProfile();
QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
dataDir.cdUp();
auto cachePath = dataDir.absolutePath() + "/jami";
profile->setCachePath(cachePath);
profile->setPersistentStoragePath(cachePath);
profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
profile->setHttpCacheType(QWebEngineProfile::NoCache);
setPage(new QWebEnginePage(profile, this));
settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false);
settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false);
settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true);
settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false);
settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu);
channel_ = new QWebChannel(this);
channel_->registerObject(QStringLiteral("jsbridge"), pimpl_);
page()->setWebChannel(channel_);
page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify.js"), QWebEngineScript::MainWorld);
page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-string.js"),
QWebEngineScript::MainWorld);
page()->runJavaScript(Utils::QByteArrayFromFile(":/qwebchannel.js"),
QWebEngineScript::MainWorld);
page()->runJavaScript(Utils::QByteArrayFromFile(":/previewInfo.js"),
QWebEngineScript::MainWorld);
page()->runJavaScript(Utils::QByteArrayFromFile(":/misc/previewInterop.js"),
QWebEngineScript::MainWorld);
}
void
PreviewEngine::parseMessage(const QString& messageId, const QString& msg)
{
page()->runJavaScript(QString("parseMessage(`%1`, `%2`)").arg(messageId, msg));
}
void
PreviewEnginePrivate::log(const QString& str)
{
qDebug() << str;
}
void
PreviewEnginePrivate::infoReady(const QString& messageId, const QVariantMap& info)
{
Q_EMIT parent_->infoReady(messageId, info);
}
void
PreviewEnginePrivate::linkifyReady(const QString& messageId, const QString& linkified)
{
Q_EMIT parent_->linkifyReady(messageId, linkified);
}

64
src/previewengine.h Normal file
View file

@ -0,0 +1,64 @@
/*
* Copyright (C) 2021 by Savoir-faire Linux
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "utils.h"
#include <QtWebChannel>
#include <QtWebEngine>
#include <QtWebEngineCore>
#include <QtWebEngine>
#include <QWebEngineView>
class PreviewEngine;
class PreviewEnginePrivate : public QObject
{
Q_OBJECT
public:
explicit PreviewEnginePrivate(PreviewEngine* parent)
: parent_(parent)
{}
Q_INVOKABLE void infoReady(const QString& messageId, const QVariantMap& info);
Q_INVOKABLE void linkifyReady(const QString& messageId, const QString& linkified);
Q_INVOKABLE void log(const QString& str);
private:
PreviewEngine* parent_;
};
class PreviewEngine : public QWebEngineView
{
Q_OBJECT
public:
explicit PreviewEngine(QObject* parent = nullptr);
~PreviewEngine() = default;
void parseMessage(const QString& messageId, const QString& msg);
Q_SIGNALS:
void infoReady(const QString& messageId, const QVariantMap& info);
void linkifyReady(const QString& messageId, const QString& linkified);
private:
QWebChannel* channel_;
PreviewEnginePrivate* pimpl_;
};

View file

@ -24,6 +24,7 @@
#include "contactadapter.h"
#include "pluginadapter.h"
#include "messagesadapter.h"
#include "previewengine.h"
#include "utilsadapter.h"
#include "conversationsadapter.h"
#include "currentconversation.h"
@ -99,21 +100,22 @@ void
registerTypes(QQmlEngine* engine,
SystemTray* systemTray,
LRCInstance* lrcInstance,
AppSettingsManager* appSettingsManager,
AppSettingsManager* settingsManager,
PreviewEngine* previewEngine,
ScreenInfo* screenInfo,
QObject* parent)
{
// setup the adapters (their lifetimes are that of MainApplication)
auto callAdapter = new CallAdapter(systemTray, lrcInstance, parent);
auto messagesAdapter = new MessagesAdapter(appSettingsManager, lrcInstance, parent);
auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, parent);
auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent);
auto avAdapter = new AvAdapter(lrcInstance, parent);
auto contactAdapter = new ContactAdapter(lrcInstance, parent);
auto accountAdapter = new AccountAdapter(appSettingsManager, lrcInstance, parent);
auto utilsAdapter = new UtilsAdapter(appSettingsManager, systemTray, lrcInstance, parent);
auto accountAdapter = new AccountAdapter(settingsManager, lrcInstance, parent);
auto utilsAdapter = new UtilsAdapter(settingsManager, systemTray, lrcInstance, parent);
auto pluginAdapter = new PluginAdapter(lrcInstance, parent);
auto currentConversation = new CurrentConversation(lrcInstance, parent);
auto currentAccount = new CurrentAccount(lrcInstance, appSettingsManager, parent);
auto currentAccount = new CurrentAccount(lrcInstance, settingsManager, parent);
auto videoDevices = new VideoDevices(lrcInstance, parent);
// qml adapter registration
@ -155,12 +157,14 @@ registerTypes(QQmlEngine* engine,
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
QML_REGISTERTYPE(NS_MODELS, SmartListModel);
QML_REGISTERTYPE(NS_MODELS, MessageListModel);
// Roles & type enums for models
QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
QML_REGISTERNAMESPACE(NS_MODELS, ConversationList::staticMetaObject, "ConversationList");
QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList");
QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend");
QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList");
// QQuickItems
QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
@ -176,10 +180,10 @@ registerTypes(QQmlEngine* engine,
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, screenInfo, "ScreenInfo")
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, appSettingsManager, "AppSettingsManager")
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
auto avatarRegistry = new AvatarRegistry(lrcInstance, parent);
auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, accountAdapter, appSettingsManager, parent);
auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, accountAdapter, settingsManager, parent);
QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, avatarRegistry, "AvatarRegistry");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, wizardViewStepModel, "WizardViewStepModel")

View file

@ -33,6 +33,7 @@
class SystemTray;
class LRCInstance;
class AppSettingsManager;
class PreviewEngine;
class ScreenInfo;
// Hack for QtCreator autocomplete (part 1)
@ -63,6 +64,7 @@ void registerTypes(QQmlEngine* engine,
SystemTray* systemTray,
LRCInstance* lrcInstance,
AppSettingsManager* appSettingsManager,
PreviewEngine* previewEngine,
ScreenInfo* screenInfo,
QObject* parent);
}

View file

@ -789,7 +789,7 @@ Utils::QByteArrayFromFile(const QString& filename)
if (file.open(QIODevice::ReadOnly)) {
return file.readAll();
} else {
qDebug() << "can't open file";
qDebug() << "QByteArrayFromFile: can't open file";
return QByteArray();
}
}

View file

@ -1,159 +0,0 @@
/*
* Copyright (C) 2017-2020 by Savoir-faire Linux
* Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
* Author: Sbastien Blin <sebastien.blin@savoirfairelinux.com>
* Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "webchathelpers.h"
QJsonObject
buildInteractionJson(lrc::api::ConversationModel& conversationModel,
const QString& convId,
const QString msgId,
const lrc::api::interaction::Info& inter)
{
QRegExp reg(".(jpeg|jpg|gif|png)$");
auto interaction = inter;
if (interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
if (interaction.body.isEmpty())
return {};
else if (interaction.body.toLower().contains(reg))
interaction.body = "file://" + interaction.body;
}
if (interaction.type == lrc::api::interaction::Type::MERGE)
return {};
auto sender = interaction.authorUri;
auto timestamp = QString::number(interaction.timestamp);
auto direction = lrc::api::interaction::isOutgoing(interaction) ? QString("out")
: QString("in");
QJsonObject interactionObject = QJsonObject();
interactionObject.insert("text", QJsonValue(interaction.body));
interactionObject.insert("id", QJsonValue(msgId));
interactionObject.insert("sender", QJsonValue(sender));
interactionObject.insert("sender_contact_method", QJsonValue(sender));
interactionObject.insert("timestamp", QJsonValue(timestamp));
interactionObject.insert("direction", QJsonValue(direction));
interactionObject.insert("duration", QJsonValue(static_cast<int>(interaction.duration)));
switch (interaction.type) {
case lrc::api::interaction::Type::TEXT:
interactionObject.insert("type", QJsonValue("text"));
break;
case lrc::api::interaction::Type::CALL:
interactionObject.insert("type", QJsonValue("call"));
break;
case lrc::api::interaction::Type::CONTACT:
interactionObject.insert("type", QJsonValue("contact"));
break;
case lrc::api::interaction::Type::DATA_TRANSFER: {
interactionObject.insert("type", QJsonValue("data_transfer"));
lrc::api::datatransfer::Info info = {};
conversationModel.getTransferInfo(convId, msgId, info);
if (info.status != lrc::api::datatransfer::Status::INVALID) {
interactionObject.insert("totalSize", QJsonValue(qint64(info.totalSize)));
interactionObject.insert("progress", QJsonValue(qint64(info.progress)));
}
interactionObject.insert("displayName", QJsonValue(inter.commit["displayName"]));
break;
}
case lrc::api::interaction::Type::INVALID:
default:
return {};
}
if (interaction.isRead) {
interactionObject.insert("delivery_status", QJsonValue("read"));
}
switch (interaction.status) {
case lrc::api::interaction::Status::SUCCESS:
interactionObject.insert("delivery_status", QJsonValue("sent"));
break;
case lrc::api::interaction::Status::FAILURE:
case lrc::api::interaction::Status::TRANSFER_ERROR:
interactionObject.insert("delivery_status", QJsonValue("failure"));
break;
case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
interactionObject.insert("delivery_status", QJsonValue("unjoinable peer"));
break;
case lrc::api::interaction::Status::SENDING:
interactionObject.insert("delivery_status", QJsonValue("sending"));
break;
case lrc::api::interaction::Status::TRANSFER_CREATED:
interactionObject.insert("delivery_status", QJsonValue("connecting"));
break;
case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
interactionObject.insert("delivery_status", QJsonValue("accepted"));
break;
case lrc::api::interaction::Status::TRANSFER_CANCELED:
interactionObject.insert("delivery_status", QJsonValue("canceled"));
break;
case lrc::api::interaction::Status::TRANSFER_ONGOING:
interactionObject.insert("delivery_status", QJsonValue("ongoing"));
break;
case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
interactionObject.insert("delivery_status", QJsonValue("awaiting peer"));
break;
case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST:
interactionObject.insert("delivery_status", QJsonValue("awaiting host"));
break;
case lrc::api::interaction::Status::TRANSFER_TIMEOUT_EXPIRED:
interactionObject.insert("delivery_status", QJsonValue("awaiting peer timeout"));
break;
case lrc::api::interaction::Status::TRANSFER_FINISHED:
interactionObject.insert("delivery_status", QJsonValue("finished"));
break;
case lrc::api::interaction::Status::INVALID:
case lrc::api::interaction::Status::UNKNOWN:
default:
interactionObject.insert("delivery_status", QJsonValue("unknown"));
break;
}
return interactionObject;
}
QString
interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
const QString& convId,
const QString& msgId,
const lrc::api::interaction::Info& interaction)
{
auto interactionObject = buildInteractionJson(conversationModel, convId, msgId, interaction);
return QString(QJsonDocument(interactionObject).toJson(QJsonDocument::Compact));
}
QString
interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
const QString& convId,
MessagesList interactions)
{
QJsonArray array;
for (const auto& interaction : interactions) {
auto interactionObject = buildInteractionJson(conversationModel,
convId,
interaction.first,
interaction.second);
if (!interactionObject.isEmpty()) {
array.append(interactionObject);
}
}
return QString(QJsonDocument(array).toJson(QJsonDocument::Compact));
}

View file

@ -1,42 +0,0 @@
/*
* Copyright (C) 2017-2020 by Savoir-faire Linux
* Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
* Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "lrcinstance.h"
#include "api/conversationmodel.h"
QJsonObject buildInteractionJson(lrc::api::ConversationModel& conversationModel,
const QString& convId,
const QString& msgId,
lrc::api::interaction::Info& interaction);
QString interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
const QString& convId,
const QString& msgId,
const lrc::api::interaction::Info& interaction);
QString interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
const QString& convId,
MessagesList interactions);

View file

@ -22,6 +22,7 @@
#include "appsettingsmanager.h"
#include "connectivitymonitor.h"
#include "systemtray.h"
#include "previewengine.h"
#include <atomic>
@ -75,6 +76,7 @@ public:
systemTray_.get(),
lrcInstance_.get(),
settingsManager_.get(),
previewEngine_.get(),
&screenInfo_,
this);
}
@ -116,6 +118,7 @@ private:
QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
QScopedPointer<AppSettingsManager> settingsManager_;
QScopedPointer<SystemTray> systemTray_;
QScopedPointer<PreviewEngine> previewEngine_;
ScreenInfo screenInfo_;
bool muteDring_ {false};

View file

@ -2,7 +2,7 @@
<qresource prefix="/">
<file>src/tst_LocalAccount.qml</file>
<file>src/tst_PresenceIndicator.qml</file>
<file>src/tst_MessageWebViewFooter.qml</file>
<file>src/tst_ChatViewFooter.qml</file>
<file>src/resources/gif_test.gif</file>
<file>src/resources/gz_test.gz</file>
<file>src/resources/png_test.png</file>

View file

@ -35,13 +35,13 @@ ColumnLayout {
width: 300
height: uut.implicitHeight
MessageWebViewFooter {
ChatViewFooter {
id: uut
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.maximumHeight: JamiTheme.messageWebViewFooterMaximumHeight
Layout.maximumHeight: JamiTheme.chatViewMaximumWidth
TestCase {
name: "MessageWebViewFooter Send Message Button Visibility Test"

View file

@ -40,9 +40,9 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: root.width
Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
Layout.preferredHeight: filesToSendCount ?
JamiTheme.messageWebViewFooterFileContainerPreferredHeight : 0
JamiTheme.chatViewFooterFileContainerPreferredHeight : 0
TestCase {
name: "FilesToSendContainer add/remove file test"