send message with files

This commit is contained in:
Gaelle Braud 2025-06-06 14:26:13 +02:00
parent 9d5935fb53
commit f69c5c3703
20 changed files with 1588 additions and 1064 deletions

View file

@ -163,7 +163,7 @@ add_option(OPTION_LIST ENABLE_FFMPEG "Build mediastreamer2 with ffmpeg video sup
add_option(OPTION_LIST ENABLE_LDAP "Enable LDAP support." YES) add_option(OPTION_LIST ENABLE_LDAP "Enable LDAP support." YES)
add_option(OPTION_LIST ENABLE_NON_FREE_CODECS "Enable the use of non free codecs" YES) add_option(OPTION_LIST ENABLE_NON_FREE_CODECS "Enable the use of non free codecs" YES)
add_option(OPTION_LIST ENABLE_NON_FREE_FEATURES "Enable the use of non free codecs" ${ENABLE_NON_FREE_CODECS}) add_option(OPTION_LIST ENABLE_NON_FREE_FEATURES "Enable the use of non free codecs" ${ENABLE_NON_FREE_CODECS})
add_option(OPTION_LIST ENABLE_QT_KEYCHAIN "Build QtKeychain to manage VFS from System key stores." ON) add_option(OPTION_LIST ENABLE_QT_KEYCHAIN "Build QtKeychain to manage VFS from System key stores." OFF)
add_option(OPTION_LIST ENABLE_QRCODE "Enable QRCode support" OFF)#Experimental add_option(OPTION_LIST ENABLE_QRCODE "Enable QRCode support" OFF)#Experimental
add_option(OPTION_LIST ENABLE_RELATIVE_PREFIX "Set Internal packages relative to the binary" ON) add_option(OPTION_LIST ENABLE_RELATIVE_PREFIX "Set Internal packages relative to the binary" ON)
add_option(OPTION_LIST ENABLE_SANITIZER "Enable sanitizer." OFF) add_option(OPTION_LIST ENABLE_SANITIZER "Enable sanitizer." OFF)

View file

@ -20,6 +20,7 @@
#include "ChatCore.hpp" #include "ChatCore.hpp"
#include "core/App.hpp" #include "core/App.hpp"
#include "core/chat/message/content/ChatMessageContentGui.hpp"
#include "core/friend/FriendCore.hpp" #include "core/friend/FriendCore.hpp"
#include "core/setting/SettingsCore.hpp" #include "core/setting/SettingsCore.hpp"
#include "model/tool/ToolModel.hpp" #include "model/tool/ToolModel.hpp"
@ -208,6 +209,21 @@ void ChatCore::setSelf(QSharedPointer<ChatCore> me) {
linMessage->send(); linMessage->send();
}); });
}); });
mChatModelConnection->makeConnectToCore(&ChatCore::lSendMessage, [this](QString message, QVariantList files) {
if (Utils::isEmptyMessage(message) && files.size() == 0) return;
QList<std::shared_ptr<ChatMessageContentModel>> filesContent;
for (auto &file : files) {
auto contentGui = qvariant_cast<ChatMessageContentGui *>(file);
if (contentGui) {
auto contentCore = contentGui->mCore;
filesContent.append(contentCore->getContentModel());
}
}
mChatModelConnection->invokeToModel([this, message, filesContent]() {
auto linMessage = mChatModel->createMessage(message, filesContent);
linMessage->send();
});
});
mChatModelConnection->makeConnectToModel( mChatModelConnection->makeConnectToModel(
&ChatModel::chatMessageSending, [this](const std::shared_ptr<linphone::ChatRoom> &chatRoom, &ChatModel::chatMessageSending, [this](const std::shared_ptr<linphone::ChatRoom> &chatRoom,
const std::shared_ptr<const linphone::EventLog> &eventLog) { const std::shared_ptr<const linphone::EventLog> &eventLog) {

View file

@ -147,6 +147,7 @@ signals:
void lUpdateUnreadCount(); void lUpdateUnreadCount();
void lUpdateLastUpdatedTime(); void lUpdateLastUpdatedTime();
void lSendTextMessage(QString message); void lSendTextMessage(QString message);
void lSendMessage(QString message, QVariantList files);
void lCompose(); void lCompose();
void lLeave(); void lLeave();
void lSetMuted(bool muted); void lSetMuted(bool muted);

View file

@ -81,6 +81,117 @@ int ChatMessageContentList::findFirstUnreadIndex() {
return it == chatList.end() ? -1 : std::distance(chatList.begin(), it); return it == chatList.end() ? -1 : std::distance(chatList.begin(), it);
} }
void ChatMessageContentList::addFiles(const QStringList &paths) {
mustBeInMainThread(log().arg(Q_FUNC_INFO));
QStringList finalList;
QList<QFileInfo> fileList;
int nbNotFound = 0;
QString lastNotFound;
int nbExcess = 0;
int count = rowCount();
for (auto &path : paths) {
QFileInfo file(path.toUtf8());
// #ifdef _WIN32
// // A bug from FileDialog suppose that the file is local and overwrite the uri by removing "\\".
// if (!file.exists()) {
// path.prepend("\\\\");
// file.setFileName(path);
// }
// #endif
if (!file.exists()) {
++nbNotFound;
lastNotFound = path;
continue;
}
if (count + finalList.count() >= 12) {
++nbExcess;
continue;
}
finalList.append(path);
fileList.append(file);
}
if (nbNotFound > 0) {
//: Error adding file
Utils::showInformationPopup(tr("popup_error_title"),
//: File was not found: %1
(nbNotFound == 1 ? tr("popup_error_path_does_not_exist_message").arg(lastNotFound)
//: %n files were not found
: tr("popup_error_nb_files_not_found_message").arg(nbNotFound)),
false);
}
if (nbExcess > 0) {
//: Error
Utils::showInformationPopup(tr("popup_error_title"),
//: You can send 12 files maximum at a time. %n files were ignored
tr("popup_error_max_files_count_message", "", nbExcess), false);
}
mModelConnection->invokeToModel([this, finalList, fileList] {
int nbTooBig = 0;
int nbMimeError = 0;
QString lastMimeError;
QList<QSharedPointer<ChatMessageContentCore>> contentList;
for (auto &file : fileList) {
qint64 fileSize = file.size();
if (fileSize > Constants::FileSizeLimit) {
++nbTooBig;
qWarning() << QString("Unable to send file. (Size limit=%1)").arg(Constants::FileSizeLimit);
continue;
}
auto name = file.fileName().toStdString();
auto path = file.filePath();
std::shared_ptr<linphone::Content> content = CoreModel::getInstance()->getCore()->createContent();
QStringList mimeType = QMimeDatabase().mimeTypeForFile(path).name().split('/');
if (mimeType.length() != 2) {
++nbMimeError;
lastMimeError = path;
qWarning() << QString("Unable to get supported mime type for: `%1`.").arg(path);
continue;
}
content->setType(Utils::appStringToCoreString(mimeType[0]));
content->setSubtype(Utils::appStringToCoreString(mimeType[1]));
content->setSize(size_t(fileSize));
content->setName(name);
content->setFilePath(Utils::appStringToCoreString(path));
contentList.append(ChatMessageContentCore::create(content, nullptr));
}
if (nbTooBig > 0) {
//: Error adding file
Utils::showInformationPopup(
tr("popup_error_title"),
//: %n files were ignored cause they exceed the maximum size. (Size limit=%2)
tr("popup_error_file_too_big_message").arg(nbTooBig).arg(Constants::FileSizeLimit), false);
}
if (nbMimeError > 0) {
//: Error adding file
Utils::showInformationPopup(tr("popup_error_title"),
//: Unable to get supported mime type for: `%1`.
(nbMimeError == 1
? tr("popup_error_unsupported_file_message").arg(lastMimeError)
//: Unable to get supported mime type for %1 files.
: tr("popup_error_unsupported_files_message").arg(nbMimeError)),
false);
}
mModelConnection->invokeToCore([this, contentList] {
for (auto &contentCore : contentList) {
connect(contentCore.get(), &ChatMessageContentCore::isFileChanged, this, [this, contentCore] {
int i = -1;
get(contentCore.get(), &i);
emit dataChanged(index(i), index(i));
});
add(contentCore);
contentCore->lCreateThumbnail(
true); // Was not created because linphone::Content is not considered as a file (yet)
}
});
});
}
void ChatMessageContentList::setSelf(QSharedPointer<ChatMessageContentList> me) { void ChatMessageContentList::setSelf(QSharedPointer<ChatMessageContentList> me) {
mModelConnection = SafeConnection<ChatMessageContentList, CoreModel>::create(me, CoreModel::getInstance()); mModelConnection = SafeConnection<ChatMessageContentList, CoreModel>::create(me, CoreModel::getInstance());
@ -101,57 +212,8 @@ void ChatMessageContentList::setSelf(QSharedPointer<ChatMessageContentList> me)
} }
resetData<ChatMessageContentCore>(contents); resetData<ChatMessageContentCore>(contents);
}); });
mModelConnection->makeConnectToCore(&ChatMessageContentList::lAddFile, [this](const QString &path) { mModelConnection->makeConnectToCore(&ChatMessageContentList::lAddFiles,
QFile file(path); [this](const QStringList &paths) { addFiles(paths); });
// #ifdef _WIN32
// // A bug from FileDialog suppose that the file is local and overwrite the uri by removing "\\".
// if (!file.exists()) {
// path.prepend("\\\\");
// file.setFileName(path);
// }
// #endif
if (!file.exists()) return;
if (rowCount() >= 12) {
//: Error
Utils::showInformationPopup(tr("popup_error_title"),
//: You can add 12 files maximum
tr("popup_error_max_files_count_message"), false);
return;
}
qint64 fileSize = file.size();
if (fileSize > Constants::FileSizeLimit) {
qWarning() << QStringLiteral("Unable to send file. (Size limit=%1)").arg(Constants::FileSizeLimit);
return;
}
auto name = QFileInfo(file).fileName().toStdString();
mModelConnection->invokeToModel([this, path, fileSize, name] {
std::shared_ptr<linphone::Content> content = CoreModel::getInstance()->getCore()->createContent();
{
QStringList mimeType = QMimeDatabase().mimeTypeForFile(path).name().split('/');
if (mimeType.length() != 2) {
qWarning() << QStringLiteral("Unable to get supported mime type for: `%1`.").arg(path);
return;
}
content->setType(Utils::appStringToCoreString(mimeType[0]));
content->setSubtype(Utils::appStringToCoreString(mimeType[1]));
}
content->setSize(size_t(fileSize));
content->setName(name);
content->setFilePath(Utils::appStringToCoreString(path));
auto contentCore = ChatMessageContentCore::create(content, nullptr);
mModelConnection->invokeToCore([this, contentCore] {
connect(contentCore.get(), &ChatMessageContentCore::isFileChanged, this, [this, contentCore] {
int i = -1;
get(contentCore.get(), &i);
emit dataChanged(index(i), index(i));
});
add(contentCore);
contentCore->lCreateThumbnail(
true); // Was not created because linphone::Content is not considered as a file (yet)
});
});
});
emit lUpdate(); emit lUpdate();
} }

View file

@ -44,11 +44,13 @@ public:
int findFirstUnreadIndex(); int findFirstUnreadIndex();
void addFiles(const QStringList &paths);
void setSelf(QSharedPointer<ChatMessageContentList> me); void setSelf(QSharedPointer<ChatMessageContentList> me);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals: signals:
void lAddFile(QString path); void lAddFiles(QStringList paths);
void isFileChanged(); void isFileChanged();
void lUpdate(); void lUpdate();
void chatMessageChanged(); void chatMessageChanged();

View file

@ -68,9 +68,9 @@ void ChatMessageContentProxy::setChatMessageGui(ChatMessageGui *chat) {
// return nullptr; // return nullptr;
// } // }
void ChatMessageContentProxy::addFile(const QString &path) { void ChatMessageContentProxy::addFiles(const QStringList &paths) {
auto model = getListModel<ChatMessageContentList>(); auto model = getListModel<ChatMessageContentList>();
if (model) emit model->lAddFile(path.toUtf8()); if (model) emit model->lAddFiles(paths);
} }
void ChatMessageContentProxy::removeContent(ChatMessageContentGui *contentGui) { void ChatMessageContentProxy::removeContent(ChatMessageContentGui *contentGui) {

View file

@ -47,7 +47,7 @@ public:
void setSourceModel(QAbstractItemModel *sourceModel) override; void setSourceModel(QAbstractItemModel *sourceModel) override;
Q_INVOKABLE void addFile(const QString &path); Q_INVOKABLE void addFiles(const QStringList &paths);
Q_INVOKABLE void removeContent(ChatMessageContentGui *contentGui); Q_INVOKABLE void removeContent(ChatMessageContentGui *contentGui);
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();

View file

@ -71,6 +71,15 @@ QVariant LimitProxy::getAt(const int &atIndex) const {
return sourceModel()->data(mapToSource(modelIndex), 0); return sourceModel()->data(mapToSource(modelIndex), 0);
} }
QVariantList LimitProxy::getAll() const {
QVariantList ret;
for (int i = 0; i < getCount(); ++i) {
auto modelIndex = index(i, 0);
if (modelIndex.isValid()) ret.append(sourceModel()->data(mapToSource(modelIndex), 0));
}
return ret;
}
int LimitProxy::getCount() const { int LimitProxy::getCount() const {
return rowCount(); return rowCount();
} }

View file

@ -47,6 +47,7 @@ public:
Q_INVOKABLE void displayMore(); Q_INVOKABLE void displayMore();
Q_INVOKABLE QVariant getAt(const int &index) const; Q_INVOKABLE QVariant getAt(const int &index) const;
Q_INVOKABLE QVariantList getAll() const;
virtual int getCount() const; virtual int getCount() const;
// Get the item following by what is shown from 2 lists // Get the item following by what is shown from 2 lists

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -132,6 +132,16 @@ std::shared_ptr<linphone::ChatMessage> ChatModel::createTextMessageFromText(QStr
return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text)); return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text));
} }
std::shared_ptr<linphone::ChatMessage>
ChatModel::createMessage(QString text, QList<std::shared_ptr<ChatMessageContentModel>> filesContent) {
auto message = mMonitor->createEmptyMessage();
for (auto &content : filesContent) {
message->addFileContent(content->getContent());
}
if (!text.isEmpty()) message->addUtf8TextContent(Utils::appStringToCoreString(text));
return message;
}
void ChatModel::compose() { void ChatModel::compose() {
mMonitor->compose(); mMonitor->compose();
} }

View file

@ -21,6 +21,7 @@
#ifndef CHAT_MODEL_H_ #ifndef CHAT_MODEL_H_
#define CHAT_MODEL_H_ #define CHAT_MODEL_H_
#include "model/chat/message/content/ChatMessageContentModel.hpp"
#include "model/listener/Listener.hpp" #include "model/listener/Listener.hpp"
#include "tool/AbstractObject.hpp" #include "tool/AbstractObject.hpp"
#include "tool/LinphoneEnums.hpp" #include "tool/LinphoneEnums.hpp"
@ -49,12 +50,13 @@ public:
void deleteChatRoom(); void deleteChatRoom();
void leave(); void leave();
std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text); std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text);
std::shared_ptr<linphone::ChatMessage> createMessage(QString text,
QList<std::shared_ptr<ChatMessageContentModel>> filesContent);
void compose(); void compose();
linphone::ChatRoom::State getState() const; linphone::ChatRoom::State getState() const;
void setMuted(bool muted); void setMuted(bool muted);
void enableEphemeral(bool enable); void enableEphemeral(bool enable);
signals: signals:
void historyDeleted(); void historyDeleted();
void messagesRead(); void messagesRead();

View file

@ -110,7 +110,11 @@ QImage ThumbnailAsyncImageResponse::createThumbnail(const QString &path, QImage
else if (rotation == 7 || rotation == 8) transform.rotate(-90); else if (rotation == 7 || rotation == 8) transform.rotate(-90);
thumbnail = thumbnail.transformed(transform); thumbnail = thumbnail.transformed(transform);
if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7) if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7)
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
thumbnail = thumbnail.mirrored(true, false);
#else
thumbnail = thumbnail.flipped(Qt::Horizontal); thumbnail = thumbnail.flipped(Qt::Horizontal);
#endif
} }
} }
return thumbnail; return thumbnail;

View file

@ -47,7 +47,6 @@ ListView {
mainItem.currentIndex = indexToSelect mainItem.currentIndex = indexToSelect
} }
onLayoutChanged: { onLayoutChanged: {
var chatToSelect = getAt(mainItem.currentIndex)
selectChat(mainItem.currentChatGui) selectChat(mainItem.currentChatGui)
} }
} }

View file

@ -39,24 +39,28 @@ Item {
property bool isOutgoing: false property bool isOutgoing: false
MouseArea { MouseArea {
anchors.fill: parent
hoverEnabled: true hoverEnabled: true
propagateComposedEvents: true propagateComposedEvents: true
// Changing of cursor seems not to work with the Loader function handleMouseMove (mouse) {
// Use override cursor for this case thumbnailProvider.state = Utils.pointIsInItem(this, thumbnailProvider, mouse)
? 'hovered'
: ''
}
onMouseXChanged: (mouse) => handleMouseMove.call(this, mouse)
onMouseYChanged: (mouse) => handleMouseMove.call(this, mouse)
onEntered: { onEntered: {
UtilsCpp.setGlobalCursor(Qt.PointingHandCursor)
thumbnailProvider.state = 'hovered' thumbnailProvider.state = 'hovered'
} }
onExited: { onExited: {
UtilsCpp.restoreGlobalCursor()
thumbnailProvider.state = '' thumbnailProvider.state = ''
} }
anchors.fill: parent
onClicked: (mouse) => { onClicked: (mouse) => {
mouse.accepted = false mouse.accepted = false
if(mainItem.isTransferring) { if(mainItem.isTransferring) {
mainItem.contentGui.core.lCancelDownloadFile() mainItem.contentGui.core.lCancelDownloadFile()
mouse.accepted = true mouse.accepted = true
thumbnailProvider.state = ''
} }
else if(!mainItem.contentGui.core.wasDownloaded) { else if(!mainItem.contentGui.core.wasDownloaded) {
mouse.accepted = true mouse.accepted = true
@ -64,6 +68,7 @@ Item {
mainItem.contentGui.core.lDownloadFile() mainItem.contentGui.core.lDownloadFile()
} else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) { } else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) {
mouse.accepted = true mouse.accepted = true
thumbnailProvider.state = ''
// if(SettingsModel.isVfsEncrypted){ // if(SettingsModel.isVfsEncrypted){
// window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), { // window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), {
// contentGui: mainItem.contentGui, // contentGui: mainItem.contentGui,
@ -290,5 +295,11 @@ Item {
states: State { states: State {
name: 'hovered' name: 'hovered'
} }
// Changing cursor in MouseArea seems not to work with the Loader
// Use override cursor for this case
onStateChanged: {
if (state === 'hovered') UtilsCpp.setGlobalCursor(Qt.PointingHandCursor)
else UtilsCpp.restoreGlobalCursor()
}
} }
} }

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls.Basic as Control import QtQuick.Controls.Basic as Control
import QtQuick.Dialogs
import QtQuick.Layouts import QtQuick.Layouts
import Linphone import Linphone
import UtilsCpp import UtilsCpp
@ -47,6 +48,12 @@ Control.Control {
} }
} }
FileDialog {
id: fileDialog
fileMode: FileDialog.OpenFiles
onAccepted: _emitFiles(fileDialog.selectedFiles)
}
// width: mainItem.implicitWidth // width: mainItem.implicitWidth
// height: mainItem.height // height: mainItem.height
leftPadding: Math.round(15 * DefaultStyle.dp) leftPadding: Math.round(15 * DefaultStyle.dp)
@ -80,7 +87,7 @@ Control.Control {
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
icon.source: AppIcons.paperclip icon.source: AppIcons.paperclip
onClicked: { onClicked: {
console.log("TODO : open explorer to attach file") fileDialog.open()
} }
} }
Control.Control { Control.Control {
@ -144,10 +151,9 @@ Control.Control {
onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle) onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle)
wrapMode: TextEdit.WordWrap wrapMode: TextEdit.WordWrap
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return) if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return))
&& (!(event.modifier & Qt.ShiftModifier))) { if(!(event.modifiers & Qt.ShiftModifier)) {
mainItem.sendText() mainItem.sendText()
sendingTextArea.clear()
event.accepted = true event.accepted = true
} }
} }
@ -168,7 +174,6 @@ Control.Control {
icon.source: AppIcons.paperPlaneRight icon.source: AppIcons.paperPlaneRight
onClicked: { onClicked: {
mainItem.sendText() mainItem.sendText()
sendingTextArea.clear()
} }
} }
} }

View file

@ -148,10 +148,6 @@ RowLayout {
leftPadding: Math.round(19 * DefaultStyle.dp) leftPadding: Math.round(19 * DefaultStyle.dp)
rightPadding: Math.round(19 * DefaultStyle.dp) rightPadding: Math.round(19 * DefaultStyle.dp)
function addFile(path) {
contents.addFile(path)
}
Button { Button {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
@ -212,6 +208,14 @@ RowLayout {
onClicked: contents.removeContent(modelData) onClicked: contents.removeContent(modelData)
} }
} }
Control.ScrollBar.horizontal: selectedFilesScrollbar
}
ScrollBar {
id: selectedFilesScrollbar
active: true
anchors.bottom: selectedFilesArea.bottom
anchors.left: selectedFilesArea.left
anchors.right: selectedFilesArea.right
} }
} }
ChatDroppableTextArea { ChatDroppableTextArea {
@ -228,9 +232,16 @@ RowLayout {
} }
mainItem.chat.core.sendingText = text mainItem.chat.core.sendingText = text
} }
onSendText: mainItem.chat.core.lSendTextMessage(text) onSendText: {
var filesContents = contents.getAll()
if (filesContents.length === 0)
mainItem.chat.core.lSendTextMessage(text)
else mainItem.chat.core.lSendMessage(text, filesContents)
messageSender.textArea.clear()
contents.clear()
}
onDropped: (files) => { onDropped: (files) => {
files.forEach(selectedFilesArea.addFile) contents.addFiles(files)
} }
} }
} }

View file

@ -1,8 +1,6 @@
add_subdirectory(linphone-sdk/) add_subdirectory(linphone-sdk/)
if(ENABLE_QT_KEYCHAIN) if(ENABLE_QT_KEYCHAIN)
function(add_linphone_keychain) find_package(Qt6 REQUIRED COMPONENTS Test)
add_subdirectory(qtkeychain/) add_subdirectory(qtkeychain/)
endfunction()
add_linphone_keychain()
endif() endif()