diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 883fd8cce..524b6d670 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -56,6 +56,7 @@ set(SOURCES
src/app/DefaultTranslator.cpp
src/app/Logger.cpp
src/app/Paths.cpp
+ src/app/ThumbnailProvider.cpp
src/components/camera/Camera.cpp
src/components/chat/ChatModel.cpp
src/components/chat/ChatProxyModel.cpp
@@ -81,6 +82,7 @@ set(HEADERS
src/app/DefaultTranslator.hpp
src/app/Logger.hpp
src/app/Paths.hpp
+ src/app/ThumbnailProvider.hpp
src/components/camera/Camera.hpp
src/components/chat/ChatModel.hpp
src/components/chat/ChatProxyModel.hpp
diff --git a/tests/assets/images/attachment_disabled.svg b/tests/assets/images/attachment_disabled.svg
new file mode 100644
index 000000000..2f35b1c31
--- /dev/null
+++ b/tests/assets/images/attachment_disabled.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/assets/languages/en.ts b/tests/assets/languages/en.ts
index 45e73f5de..6755d2916 100644
--- a/tests/assets/languages/en.ts
+++ b/tests/assets/languages/en.ts
@@ -41,6 +41,11 @@
newMessagePlaceholder
Enter your message
+
+ noFileTransferUrl
+ Unable to send file.
+Server url not configured.
+
ConfirmDialog
diff --git a/tests/assets/languages/fr.ts b/tests/assets/languages/fr.ts
index be7564282..71f21dcf9 100644
--- a/tests/assets/languages/fr.ts
+++ b/tests/assets/languages/fr.ts
@@ -33,6 +33,11 @@
newMessagePlaceholder
Entrer votre message.
+
+ noFileTransferUrl
+ Impossible d'envoyer un fichier.
+Url du serveur non configurée.
+
ConfirmDialog
diff --git a/tests/resources.qrc b/tests/resources.qrc
index 5b7e9f2ce..896da1a33 100644
--- a/tests/resources.qrc
+++ b/tests/resources.qrc
@@ -4,6 +4,7 @@
assets/images/add_hovered.svg
assets/images/add_normal.svg
assets/images/add_pressed.svg
+ assets/images/attachment_disabled.svg
assets/images/attachment_hovered.svg
assets/images/attachment_normal.svg
assets/images/attachment_pressed.svg
@@ -204,6 +205,7 @@
ui/modules/Linphone/Call/PausedCallControls.qml
ui/modules/Linphone/Chat/Chat.qml
ui/modules/Linphone/Chat/Event.qml
+ ui/modules/Linphone/Chat/FileMessage.qml
ui/modules/Linphone/Chat/IncomingMessage.qml
ui/modules/Linphone/Chat/Message.qml
ui/modules/Linphone/Chat/OutgoingMessage.qml
diff --git a/tests/src/app/App.cpp b/tests/src/app/App.cpp
index 78a90f89f..10e01f9ca 100644
--- a/tests/src/app/App.cpp
+++ b/tests/src/app/App.cpp
@@ -43,8 +43,9 @@ App::App (int &argc, char **argv) : QApplication(argc, argv) {
.arg(current_locale.name());
}
- // Provide avatars loader.
+ // Provide avatars/thumbnails providers.
m_engine.addImageProvider(AvatarProvider::PROVIDER_ID, &m_avatar_provider);
+ m_engine.addImageProvider(ThumbnailProvider::PROVIDER_ID, &m_thumbnail_provider);
setWindowIcon(QIcon(WINDOW_ICON_PATH));
diff --git a/tests/src/app/App.hpp b/tests/src/app/App.hpp
index 82648c53f..28a6fb6a2 100644
--- a/tests/src/app/App.hpp
+++ b/tests/src/app/App.hpp
@@ -9,6 +9,7 @@
#include "../components/notifier/Notifier.hpp"
#include "AvatarProvider.hpp"
#include "DefaultTranslator.hpp"
+#include "ThumbnailProvider.hpp"
// =============================================================================
@@ -57,6 +58,7 @@ private:
QSystemTrayIcon *m_system_tray_icon = nullptr;
AvatarProvider m_avatar_provider;
+ ThumbnailProvider m_thumbnail_provider;
DefaultTranslator m_default_translator;
QTranslator m_english_translator;
diff --git a/tests/src/app/AvatarProvider.cpp b/tests/src/app/AvatarProvider.cpp
index f0d05b604..04f50521b 100644
--- a/tests/src/app/AvatarProvider.cpp
+++ b/tests/src/app/AvatarProvider.cpp
@@ -7,18 +7,13 @@
const QString AvatarProvider::PROVIDER_ID = "avatar";
-AvatarProvider::AvatarProvider () :
- QQuickImageProvider(
+AvatarProvider::AvatarProvider () : QQuickImageProvider(
QQmlImageProviderBase::Image,
QQmlImageProviderBase::ForceAsynchronousImageLoading
) {
m_avatars_path = Utils::linphoneStringToQString(Paths::getAvatarsDirpath());
}
-QImage AvatarProvider::requestImage (
- const QString &id,
- QSize *,
- const QSize &
-) {
+QImage AvatarProvider::requestImage (const QString &id, QSize *, const QSize &) {
return QImage(m_avatars_path + id);
}
diff --git a/tests/src/app/AvatarProvider.hpp b/tests/src/app/AvatarProvider.hpp
index b57707ef8..0d21ab163 100644
--- a/tests/src/app/AvatarProvider.hpp
+++ b/tests/src/app/AvatarProvider.hpp
@@ -10,11 +10,7 @@ public:
AvatarProvider ();
~AvatarProvider () = default;
- QImage requestImage (
- const QString &id,
- QSize *size,
- const QSize &requested_size
- ) override;
+ QImage requestImage (const QString &id, QSize *size, const QSize &requested_size) override;
static const QString PROVIDER_ID;
diff --git a/tests/src/app/Paths.cpp b/tests/src/app/Paths.cpp
index 51ece121e..e50033a8e 100644
--- a/tests/src/app/Paths.cpp
+++ b/tests/src/app/Paths.cpp
@@ -11,7 +11,7 @@
#ifdef _WIN32
#define MAIN_PATH \
- QStandardPaths::writableLocation(QStandardPaths::DataLocation)
+ (QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/")
#define PATH_CONFIG "linphonerc"
#define LINPHONE_FOLDER "linphone/"
@@ -19,15 +19,16 @@
#else
#define MAIN_PATH \
- QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
+ (QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/")
#define PATH_CONFIG ".linphonerc"
#define LINPHONE_FOLDER ".linphone/"
#endif // ifdef _WIN32
-#define PATH_AVATARS LINPHONE_FOLDER "avatars/"
-#define PATH_LOGS LINPHONE_FOLDER "logs/"
+#define PATH_AVATARS (LINPHONE_FOLDER "avatars/")
+#define PATH_LOGS (LINPHONE_FOLDER "logs/")
+#define PATH_THUMBNAILS (LINPHONE_FOLDER "thumbnails/")
#define PATH_CALL_HISTORY_LIST ".linphone-call-history.db"
#define PATH_FRIENDS_LIST ".linphone-friends.db"
@@ -65,25 +66,29 @@ inline string getFilePath (const QString &filename) {
// -----------------------------------------------------------------------------
string Paths::getAvatarsDirpath () {
- return getDirectoryPath(MAIN_PATH + "/" PATH_AVATARS);
+ return getDirectoryPath(MAIN_PATH + PATH_AVATARS);
}
string Paths::getCallHistoryFilepath () {
- return getFilePath(MAIN_PATH + "/" + PATH_CALL_HISTORY_LIST);
+ return getFilePath(MAIN_PATH + PATH_CALL_HISTORY_LIST);
}
string Paths::getConfigFilepath () {
- return getFilePath(MAIN_PATH + "/" + PATH_CONFIG);
+ return getFilePath(MAIN_PATH + PATH_CONFIG);
}
string Paths::getFriendsListFilepath () {
- return getFilePath(MAIN_PATH + "/" + PATH_FRIENDS_LIST);
+ return getFilePath(MAIN_PATH + PATH_FRIENDS_LIST);
}
string Paths::getLogsDirpath () {
- return getDirectoryPath(MAIN_PATH + "/" PATH_LOGS);
+ return getDirectoryPath(MAIN_PATH + PATH_LOGS);
}
string Paths::getMessageHistoryFilepath () {
- return getFilePath(MAIN_PATH + "/" + PATH_MESSAGE_HISTORY_LIST);
+ return getFilePath(MAIN_PATH + PATH_MESSAGE_HISTORY_LIST);
+}
+
+string Paths::getThumbnailsDirPath () {
+ return getDirectoryPath(MAIN_PATH + PATH_THUMBNAILS);
}
diff --git a/tests/src/app/Paths.hpp b/tests/src/app/Paths.hpp
index 85faab4a3..555341d72 100644
--- a/tests/src/app/Paths.hpp
+++ b/tests/src/app/Paths.hpp
@@ -12,6 +12,7 @@ namespace Paths {
std::string getFriendsListFilepath ();
std::string getLogsDirpath ();
std::string getMessageHistoryFilepath ();
+ std::string getThumbnailsDirPath ();
}
#endif // PATHS_H_
diff --git a/tests/src/app/ThumbnailProvider.cpp b/tests/src/app/ThumbnailProvider.cpp
new file mode 100644
index 000000000..df6c5555d
--- /dev/null
+++ b/tests/src/app/ThumbnailProvider.cpp
@@ -0,0 +1,19 @@
+#include "Paths.hpp"
+#include "../utils.hpp"
+
+#include "ThumbnailProvider.hpp"
+
+// =============================================================================
+
+const QString ThumbnailProvider::PROVIDER_ID = "thumbnail";
+
+ThumbnailProvider::ThumbnailProvider () : QQuickImageProvider(
+ QQmlImageProviderBase::Image,
+ QQmlImageProviderBase::ForceAsynchronousImageLoading
+ ) {
+ m_thumbnails_path = Utils::linphoneStringToQString(Paths::getThumbnailsDirPath());
+}
+
+QImage ThumbnailProvider::requestImage (const QString &id, QSize *, const QSize &) {
+ return QImage(m_thumbnails_path + id);
+}
diff --git a/tests/src/app/ThumbnailProvider.hpp b/tests/src/app/ThumbnailProvider.hpp
new file mode 100644
index 000000000..a2b52eccc
--- /dev/null
+++ b/tests/src/app/ThumbnailProvider.hpp
@@ -0,0 +1,21 @@
+#ifndef THUMBNAIL_PROVIDER_H_
+#define THUMBNAIL_PROVIDER_H_
+
+#include
+
+// =============================================================================
+
+class ThumbnailProvider : public QQuickImageProvider {
+public:
+ ThumbnailProvider ();
+ ~ThumbnailProvider () = default;
+
+ QImage requestImage (const QString &id, QSize *size, const QSize &requested_size) override;
+
+ static const QString PROVIDER_ID;
+
+private:
+ QString m_thumbnails_path;
+};
+
+#endif // THUMBNAIL_PROVIDER_H_
diff --git a/tests/src/components/chat/ChatModel.cpp b/tests/src/components/chat/ChatModel.cpp
index 7fb3cc2e1..bf8ce2e4e 100644
--- a/tests/src/components/chat/ChatModel.cpp
+++ b/tests/src/components/chat/ChatModel.cpp
@@ -1,18 +1,48 @@
#include
#include
-#include
+#include
+#include
#include
+#include
+#include
+#include "../../app/Paths.hpp"
+#include "../../app/ThumbnailProvider.hpp"
#include "../../utils.hpp"
#include "../core/CoreManager.hpp"
#include "ChatModel.hpp"
+#define THUMBNAIL_IMAGE_FILE_HEIGHT 100
+#define THUMBNAIL_IMAGE_FILE_WIDTH 100
+
using namespace std;
// =============================================================================
+inline void fillThumbnailProperty (QVariantMap &dest, const shared_ptr &message) {
+ string data = message->getAppdata();
+ if (!data.empty())
+ dest["thumbnail"] = QStringLiteral("image://%1/%2")
+ .arg(ThumbnailProvider::PROVIDER_ID).arg(::Utils::linphoneStringToQString(data));
+}
+
+inline void removeFileMessageThumbnail (const shared_ptr &message) {
+ if (message && message->getFileTransferInformation()) {
+ message->cancelFileTransfer();
+
+ string file_id = message->getAppdata();
+ if (!file_id.empty()) {
+ QString thumbnail_path = ::Utils::linphoneStringToQString(Paths::getThumbnailsDirPath() + file_id);
+ if (!QFile::remove(thumbnail_path))
+ qWarning() << QStringLiteral("Unable to remove `%1`.").arg(thumbnail_path);
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------
+
class ChatModel::MessageHandlers : public linphone::ChatMessageListener {
friend class ChatModel;
@@ -22,48 +52,93 @@ public:
~MessageHandlers () = default;
private:
+ QList::iterator findMessageEntry (const shared_ptr &message) {
+ return find_if(
+ m_chat_model->m_entries.begin(), m_chat_model->m_entries.end(), [&message](const ChatEntryData &pair) {
+ return pair.second == message;
+ }
+ );
+ }
+
+ void signalDataChanged (const QList::iterator &it) {
+ int row = static_cast(distance(m_chat_model->m_entries.begin(), it));
+ emit m_chat_model->dataChanged(m_chat_model->index(row, 0), m_chat_model->index(row, 0));
+ }
+
void onFileTransferRecv (
- const shared_ptr &message,
- const shared_ptr &content,
- const shared_ptr &buffer
+ const shared_ptr &,
+ const shared_ptr &,
+ const shared_ptr &
) override {
- qDebug() << "Not yet implemented";
+ qWarning() << "`onFileTransferRecv` called.";
}
shared_ptr onFileTransferSend (
- const shared_ptr &message,
- const shared_ptr &content,
- size_t offset,
- size_t size
+ const shared_ptr &,
+ const shared_ptr &,
+ size_t,
+ size_t
) override {
- qDebug() << "Not yet implemented";
+ qWarning() << "`onFileTransferSend` called.";
+ return nullptr;
}
void onFileTransferProgressIndication (
const shared_ptr &message,
- const shared_ptr &content,
+ const shared_ptr &,
size_t offset,
- size_t total
+ size_t
) override {
- qDebug() << "Not yet implemented";
+ if (!m_chat_model)
+ return;
+
+ auto it = findMessageEntry(message);
+ if (it == m_chat_model->m_entries.end())
+ return;
+
+ (*it).first["fileOffset"] = static_cast(offset);
+
+ signalDataChanged(it);
}
void onMsgStateChanged (const shared_ptr &message, linphone::ChatMessageState state) override {
if (!m_chat_model)
return;
- ChatModel &chat = *m_chat_model;
-
- auto it = find_if(chat.m_entries.begin(), chat.m_entries.end(), [&message](const ChatEntryData &pair) {
- return pair.second == message;
- });
- if (it == chat.m_entries.end())
+ auto it = findMessageEntry(message);
+ if (it == m_chat_model->m_entries.end())
return;
- (*it).first["status"] = state;
- int row = distance(chat.m_entries.begin(), it);
+ if (state == linphone::ChatMessageStateFileTransferError)
+ state = linphone::ChatMessageStateNotDelivered;
+ else if (state == linphone::ChatMessageStateFileTransferDone) {
+ QString thumbnail_path = ::Utils::linphoneStringToQString(message->getFileTransferFilepath());
- emit chat.dataChanged(chat.index(row, 0), chat.index(row, 0));
+ QImage image(thumbnail_path);
+ if (!image.isNull()) {
+ QImage thumbnail = image.scaled(
+ THUMBNAIL_IMAGE_FILE_WIDTH, THUMBNAIL_IMAGE_FILE_HEIGHT,
+ Qt::KeepAspectRatio, Qt::SmoothTransformation
+ );
+
+ QString uuid = QUuid::createUuid().toString();
+ QString file_id = QStringLiteral("%1.jpg").arg(uuid.mid(1, uuid.length() - 2));
+
+ if (!thumbnail.save(::Utils::linphoneStringToQString(Paths::getThumbnailsDirPath()) + file_id, "jpg", 100)) {
+ qWarning() << QStringLiteral("Unable to create thumbnail of: `%1`.").arg(thumbnail_path);
+ return;
+ }
+
+ message->setAppdata(::Utils::qStringToLinphoneString(file_id));
+ fillThumbnailProperty((*it).first, message);
+ }
+
+ state = linphone::ChatMessageStateDelivered;
+ }
+
+ (*it).first["status"] = state;
+
+ signalDataChanged(it);
}
ChatModel *m_chat_model;
@@ -245,15 +320,22 @@ void ChatModel::removeAllEntries () {
}
void ChatModel::sendMessage (const QString &message) {
+ if (!m_chat_room)
+ return;
+
shared_ptr _message = m_chat_room->createMessage(::Utils::qStringToLinphoneString(message));
_message->setListener(m_message_handlers);
- m_chat_room->sendMessage(_message);
+
insertMessageAtEnd(_message);
+ m_chat_room->sendMessage(_message);
emit messageSent(_message);
}
void ChatModel::resendMessage (int id) {
+ if (!m_chat_room)
+ return;
+
if (id < 0 || id > m_entries.count()) {
qWarning() << QStringLiteral("Entry %1 not exists.").arg(id);
return;
@@ -275,23 +357,48 @@ void ChatModel::resendMessage (int id) {
m_chat_room->sendMessage(message);
}
+void ChatModel::sendFileMessage (const QString &path) {
+ if (!m_chat_room)
+ return;
+
+ QFile file(path);
+ if (!file.exists())
+ return;
+
+ shared_ptr content = CoreManager::getInstance()->getCore()->createContent();
+ content->setType("application");
+ content->setSubtype("octet-stream");
+ content->setSize(file.size());
+ content->setName(::Utils::qStringToLinphoneString(QFileInfo(file).fileName()));
+
+ shared_ptr message = m_chat_room->createFileTransferMessage(content);
+ message->setFileTransferFilepath(::Utils::qStringToLinphoneString(path));
+ message->setListener(m_message_handlers);
+
+ insertMessageAtEnd(message);
+ m_chat_room->sendMessage(message);
+
+ emit messageSent(message);
+}
+
// -----------------------------------------------------------------------------
-void ChatModel::fillMessageEntry (
- QVariantMap &dest,
- const shared_ptr &message
-) {
+void ChatModel::fillMessageEntry (QVariantMap &dest, const shared_ptr &message) {
dest["type"] = EntryType::MessageEntry;
dest["timestamp"] = QDateTime::fromMSecsSinceEpoch(message->getTime() * 1000);
dest["content"] = ::Utils::linphoneStringToQString(message->getText());
- dest["isOutgoing"] = message->isOutgoing();
+ dest["isOutgoing"] = message->isOutgoing() || message->getState() == linphone::ChatMessageStateIdle;
dest["status"] = message->getState();
+
+ shared_ptr content = message->getFileTransferInformation();
+ if (content) {
+ dest["fileSize"] = static_cast(content->getSize());
+ dest["fileName"] = ::Utils::linphoneStringToQString(content->getName());
+ fillThumbnailProperty(dest, message);
+ }
}
-void ChatModel::fillCallStartEntry (
- QVariantMap &dest,
- const shared_ptr &call_log
-) {
+void ChatModel::fillCallStartEntry (QVariantMap &dest, const shared_ptr &call_log) {
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(call_log->getStartDate() * 1000);
dest["type"] = EntryType::CallEntry;
@@ -301,10 +408,7 @@ void ChatModel::fillCallStartEntry (
dest["isStart"] = true;
}
-void ChatModel::fillCallEndEntry (
- QVariantMap &dest,
- const shared_ptr &call_log
-) {
+void ChatModel::fillCallEndEntry (QVariantMap &dest, const shared_ptr &call_log) {
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch((call_log->getStartDate() + call_log->getDuration()) * 1000);
dest["type"] = EntryType::CallEntry;
@@ -320,10 +424,14 @@ void ChatModel::removeEntry (ChatEntryData &pair) {
int type = pair.first["type"].toInt();
switch (type) {
- case ChatModel::MessageEntry:
- m_chat_room->deleteMessage(static_pointer_cast(pair.second));
+ case ChatModel::MessageEntry: {
+ shared_ptr message = static_pointer_cast(pair.second);
+ removeFileMessageThumbnail(message);
+ m_chat_room->deleteMessage(message);
break;
- case ChatModel::CallEntry:
+ }
+
+ case ChatModel::CallEntry: {
if (pair.first["status"].toInt() == linphone::CallStatusSuccess) {
// WARNING: Unable to remove symmetric call here. (start/end)
// We are between `beginRemoveRows` and `endRemoveRows`.
@@ -343,6 +451,8 @@ void ChatModel::removeEntry (ChatEntryData &pair) {
CoreManager::getInstance()->getCore()->removeCallLog(static_pointer_cast(pair.second));
break;
+ }
+
default:
qWarning() << QStringLiteral("Unknown chat entry type: %1.").arg(type);
}
diff --git a/tests/src/components/chat/ChatModel.hpp b/tests/src/components/chat/ChatModel.hpp
index 48b5c5c05..1a18af568 100644
--- a/tests/src/components/chat/ChatModel.hpp
+++ b/tests/src/components/chat/ChatModel.hpp
@@ -18,8 +18,6 @@ class ChatModel : public QAbstractListModel {
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
public:
- typedef QPair > ChatEntryData;
-
enum Roles {
ChatEntry = Qt::DisplayRole,
SectionDate
@@ -70,6 +68,8 @@ public:
void resendMessage (int id);
+ void sendFileMessage (const QString &path);
+
signals:
void sipAddressChanged (const QString &sip_address);
void allEntriesRemoved ();
@@ -80,20 +80,11 @@ signals:
void messagesCountReset ();
private:
- void fillMessageEntry (
- QVariantMap &dest,
- const std::shared_ptr &message
- );
+ typedef QPair > ChatEntryData;
- void fillCallStartEntry (
- QVariantMap &dest,
- const std::shared_ptr &call_log
- );
-
- void fillCallEndEntry (
- QVariantMap &dest,
- const std::shared_ptr &call_log
- );
+ void fillMessageEntry (QVariantMap &dest, const std::shared_ptr &message);
+ void fillCallStartEntry (QVariantMap &dest, const std::shared_ptr &call_log);
+ void fillCallEndEntry (QVariantMap &dest, const std::shared_ptr &call_log);
void removeEntry (ChatEntryData &pair);
diff --git a/tests/src/components/chat/ChatProxyModel.cpp b/tests/src/components/chat/ChatProxyModel.cpp
index 6a901a21f..47e0cff3c 100644
--- a/tests/src/components/chat/ChatProxyModel.cpp
+++ b/tests/src/components/chat/ChatProxyModel.cpp
@@ -105,10 +105,18 @@ void ChatProxyModel::resendMessage (int id) {
);
}
+void ChatProxyModel::sendFileMessage (const QString &path) {
+ static_cast(m_chat_model_filter->sourceModel())->sendFileMessage(path);
+}
+
+// -----------------------------------------------------------------------------
+
bool ChatProxyModel::filterAcceptsRow (int source_row, const QModelIndex &) const {
return m_chat_model_filter->rowCount() - source_row <= m_n_max_displayed_entries;
}
+// -----------------------------------------------------------------------------
+
QString ChatProxyModel::getSipAddress () const {
return static_cast(m_chat_model_filter->sourceModel())->getSipAddress();
}
diff --git a/tests/src/components/chat/ChatProxyModel.hpp b/tests/src/components/chat/ChatProxyModel.hpp
index 01ec056ed..88535566e 100644
--- a/tests/src/components/chat/ChatProxyModel.hpp
+++ b/tests/src/components/chat/ChatProxyModel.hpp
@@ -12,12 +12,7 @@ class ChatProxyModel : public QSortFilterProxyModel {
Q_OBJECT;
- Q_PROPERTY(
- QString sipAddress
- READ getSipAddress
- WRITE setSipAddress
- NOTIFY sipAddressChanged
- );
+ Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
public:
ChatProxyModel (QObject *parent = Q_NULLPTR);
@@ -31,6 +26,8 @@ public:
Q_INVOKABLE void sendMessage (const QString &message);
Q_INVOKABLE void resendMessage (int id);
+ Q_INVOKABLE void sendFileMessage (const QString &path);
+
signals:
void sipAddressChanged (const QString &sip_address);
void moreEntriesLoaded (int n);
diff --git a/tests/src/components/settings/SettingsModel.cpp b/tests/src/components/settings/SettingsModel.cpp
index 29d715a48..9e1fae890 100644
--- a/tests/src/components/settings/SettingsModel.cpp
+++ b/tests/src/components/settings/SettingsModel.cpp
@@ -1,3 +1,4 @@
+#include "../../utils.hpp"
#include "../core/CoreManager.hpp"
#include "SettingsModel.hpp"
@@ -18,7 +19,19 @@ bool SettingsModel::getAutoAnswerStatus () const {
return !!m_config->getInt(UI_SECTION, "auto_answer", 0);
}
-bool SettingsModel::setAutoAnswerStatus (bool status) {
+void SettingsModel::setAutoAnswerStatus (bool status) {
m_config->setInt(UI_SECTION, "auto_answer", status);
emit autoAnswerStatusChanged(status);
}
+
+QString SettingsModel::getFileTransferUrl () const {
+ return ::Utils::linphoneStringToQString(
+ CoreManager::getInstance()->getCore()->getFileTransferServer()
+ );
+}
+
+void SettingsModel::setFileTransferUrl (const QString &url) {
+ CoreManager::getInstance()->getCore()->setFileTransferServer(
+ ::Utils::qStringToLinphoneString(url)
+ );
+}
diff --git a/tests/src/components/settings/SettingsModel.hpp b/tests/src/components/settings/SettingsModel.hpp
index cffa208d9..783c42c62 100644
--- a/tests/src/components/settings/SettingsModel.hpp
+++ b/tests/src/components/settings/SettingsModel.hpp
@@ -4,24 +4,27 @@
#include
#include
-#include "AccountSettingsModel.hpp"
-
// =============================================================================
class SettingsModel : public QObject {
Q_OBJECT;
Q_PROPERTY(bool autoAnswerStatus READ getAutoAnswerStatus WRITE setAutoAnswerStatus NOTIFY autoAnswerStatusChanged);
+ Q_PROPERTY(QString fileTransferUrl READ getFileTransferUrl WRITE setFileTransferUrl NOTIFY fileTransferUrlChanged);
public:
SettingsModel (QObject *parent = Q_NULLPTR);
signals:
void autoAnswerStatusChanged (bool status);
+ void fileTransferUrlChanged (const QString &url);
private:
bool getAutoAnswerStatus () const;
- bool setAutoAnswerStatus (bool status);
+ void setAutoAnswerStatus (bool status);
+
+ QString getFileTransferUrl () const;
+ void setFileTransferUrl (const QString &url);
std::shared_ptr m_config;
diff --git a/tests/ui/modules/Common/Colors.qml b/tests/ui/modules/Common/Colors.qml
index 2abd11381..e0810f722 100644
--- a/tests/ui/modules/Common/Colors.qml
+++ b/tests/ui/modules/Common/Colors.qml
@@ -21,6 +21,7 @@ QtObject {
property color k: '#FFFFFF'
property color k50: '#32FFFFFF'
property color l: '#000000'
+ property color l50: '#32000000'
property color m: '#D1D1D1'
property color n: '#C0C0C0'
property color o: '#232323'
@@ -34,4 +35,5 @@ QtObject {
property color w: '#A1A1A1'
property color x: '#96A5B1'
property color y: '#D0D8DE'
+ property color z: '#17A81A'
}
diff --git a/tests/ui/modules/Common/DroppableTextArea.qml b/tests/ui/modules/Common/DroppableTextArea.qml
index 5505a280d..27069b3f9 100644
--- a/tests/ui/modules/Common/DroppableTextArea.qml
+++ b/tests/ui/modules/Common/DroppableTextArea.qml
@@ -7,9 +7,14 @@ import Common.Styles 1.0
// =============================================================================
Item {
+ id: droppableTextArea
+
property alias placeholderText: textArea.placeholderText
property alias text: textArea.text
+ property bool dropEnabled: true
+ property string dropDisabledReason
+
// ---------------------------------------------------------------------------
signal dropped (var files)
@@ -73,6 +78,7 @@ Item {
DroppableTextAreaStyle.fileChooserButton.margins
verticalCenter: parent.verticalCenter
}
+ enabled: droppableTextArea.dropEnabled
icon: 'attachment'
iconSize: DroppableTextAreaStyle.fileChooserButton.size
@@ -88,7 +94,9 @@ Item {
}
TooltipArea {
- text: qsTr('attachmentTooltip')
+ text: droppableTextArea.dropEnabled
+ ? qsTr('attachmentTooltip')
+ : droppableTextArea.dropDisabledReason
}
}
@@ -111,6 +119,7 @@ Item {
DropArea {
anchors.fill: parent
keys: [ 'text/uri-list' ]
+ visible: droppableTextArea.dropEnabled
onDropped: {
state = ''
diff --git a/tests/ui/modules/Common/Form/ExclusiveButtons.spec.qml b/tests/ui/modules/Common/Form/ExclusiveButtons.spec.qml
index eb5dd4fa6..be92a1c4f 100644
--- a/tests/ui/modules/Common/Form/ExclusiveButtons.spec.qml
+++ b/tests/ui/modules/Common/Form/ExclusiveButtons.spec.qml
@@ -25,13 +25,7 @@ Item {
ExclusiveButtons {
id: exclusiveButtons
- texts: [
- qsTr('A'),
- qsTr('B'),
- qsTr('C'),
- qsTr('D'),
- qsTr('E')
- ]
+ texts: ['A', 'B', 'C', 'D', 'E']
}
SignalSpy {
diff --git a/tests/ui/modules/Linphone/Chat/Chat.qml b/tests/ui/modules/Linphone/Chat/Chat.qml
index 3ce86dfcc..565366ae9 100644
--- a/tests/ui/modules/Linphone/Chat/Chat.qml
+++ b/tests/ui/modules/Linphone/Chat/Chat.qml
@@ -191,6 +191,11 @@ ColumnLayout {
OutgoingMessage {}
}
+ Component {
+ id: fileMessage
+ FileMessage {}
+ }
+
// -----------------------------------------------------------------------
MouseArea {
@@ -223,9 +228,17 @@ ColumnLayout {
// Display content.
Loader {
Layout.fillWidth: true
- sourceComponent: $chatEntry.type === ChatModel.MessageEntry
- ? ($chatEntry.isOutgoing ? outgoingMessage : incomingMessage)
- : event
+ sourceComponent: {
+ if ($chatEntry.fileName) {
+ return fileMessage
+ }
+
+ if ($chatEntry.type === ChatModel.CallEntry) {
+ return event
+ }
+
+ return $chatEntry.isOutgoing ? outgoingMessage : incomingMessage
+ }
}
}
}
@@ -245,8 +258,15 @@ ColumnLayout {
DroppableTextArea {
anchors.fill: parent
+ dropEnabled: SettingsModel.fileTransferUrl.length > 0
+ dropDisabledReason: qsTr('noFileTransferUrl')
placeholderText: qsTr('newMessagePlaceholder')
+ onDropped: {
+ _bindToEnd = true
+ files.forEach(proxyModel.sendFileMessage)
+ }
+
onValidText: {
this.text = ''
_bindToEnd = true
diff --git a/tests/ui/modules/Linphone/Chat/FileMessage.qml b/tests/ui/modules/Linphone/Chat/FileMessage.qml
new file mode 100644
index 000000000..7829c57ab
--- /dev/null
+++ b/tests/ui/modules/Linphone/Chat/FileMessage.qml
@@ -0,0 +1,225 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.3
+
+import Common 1.0
+import Linphone 1.0
+import LinphoneUtils 1.0
+import Linphone.Styles 1.0
+import Utils 1.0
+
+// =============================================================================
+
+Row {
+ // ---------------------------------------------------------------------------
+ // Avatar if it's an incoming message.
+ // ---------------------------------------------------------------------------
+
+ Item {
+ height: ChatStyle.entry.lineHeight
+ width: ChatStyle.entry.metaWidth
+
+ Component {
+ id: avatar
+
+ Avatar {
+ height: ChatStyle.entry.message.incoming.avatarSize
+ width: ChatStyle.entry.message.incoming.avatarSize
+
+ image: _contactObserver.contact ? _contactObserver.contact.avatar : ''
+ username: LinphoneUtils.getContactUsername(_contactObserver.contact || proxyModel.sipAddress)
+ }
+ }
+
+ Loader {
+ anchors.centerIn: parent
+ sourceComponent: !$chatEntry.isOutgoing ? avatar : undefined
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // File message.
+ // ---------------------------------------------------------------------------
+
+ Row {
+ spacing: ChatStyle.entry.message.extraContent.leftMargin
+
+ Rectangle {
+ id: rectangle
+
+ color: $chatEntry.isOutgoing
+ ? ChatStyle.entry.message.outgoing.backgroundColor
+ : ChatStyle.entry.message.incoming.backgroundColor
+
+ height: ChatStyle.entry.message.file.height
+ width: ChatStyle.entry.message.file.width
+
+ radius: ChatStyle.entry.message.radius
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: ChatStyle.entry.message.file.margins
+ }
+
+ spacing: ChatStyle.entry.message.file.spacing
+
+ // ---------------------------------------------------------------------
+ // Thumbnail or extension.
+ // ---------------------------------------------------------------------
+
+ Component {
+ id: thumbnail
+
+ Image {
+ source: $chatEntry.thumbnail
+ }
+ }
+
+ Component {
+ id: extension
+
+ Rectangle {
+ color: Colors.l50
+
+ Text {
+ anchors.fill: parent
+
+ color: Colors.k
+ font.bold: true
+ elide: Text.ElideRight
+ text: Utils.getExtension($chatEntry.fileName).toUpperCase()
+
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ }
+
+ Loader {
+ Layout.preferredHeight: ChatStyle.entry.message.file.thumbnail.height
+ Layout.preferredWidth: ChatStyle.entry.message.file.thumbnail.width
+
+ sourceComponent: $chatEntry.thumbnail ? thumbnail : extension
+ }
+
+ // ---------------------------------------------------------------------
+ // Upload or file status.
+ // ---------------------------------------------------------------------
+
+ Column {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ spacing: ChatStyle.entry.message.file.status.spacing
+
+ Text {
+ id: fileName
+
+ color: $chatEntry.isOutgoing
+ ? ChatStyle.entry.message.outgoing.text.color
+ : ChatStyle.entry.message.incoming.text.color
+ elide: Text.ElideRight
+
+ font {
+ bold: true
+ pointSize: $chatEntry.isOutgoing
+ ? ChatStyle.entry.message.outgoing.text.fontSize
+ : ChatStyle.entry.message.incoming.text.fontSize
+ }
+
+ text: $chatEntry.fileName
+ width: parent.width
+ }
+
+ ProgressBar {
+ id: progressBar
+
+ height: ChatStyle.entry.message.file.status.bar.height
+ width: parent.width
+
+ to: $chatEntry.fileSize
+ value: $chatEntry.fileOffset || 0
+ visible: $chatEntry.status === ChatModel.MessageStatusInProgress
+
+ background: Rectangle {
+ color: Colors.f
+ radius: ChatStyle.entry.message.file.status.bar.radius
+ }
+
+ contentItem: Item {
+ Rectangle {
+ color: Colors.z
+ height: parent.height
+ width: progressBar.visualPosition * parent.width
+ }
+ }
+ }
+
+ Text {
+ color: fileName.color
+ elide: Text.ElideRight
+ font.pointSize: fileName.font.pointSize
+ text: {
+ var fileSize = Utils.formatSize($chatEntry.fileSize)
+ return progressBar.visible
+ ? Utils.formatSize($chatEntry.fileOffset) + '/' + fileSize
+ : fileSize
+ }
+ }
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Resend/Remove file message.
+ // -------------------------------------------------------------------------
+
+ Row {
+ spacing: ChatStyle.entry.message.extraContent.spacing
+
+ Component {
+ id: icon
+
+ Icon {
+ readonly property bool isNotDelivered:
+ $chatEntry.status === ChatModel.MessageStatusNotDelivered
+
+ icon: isNotDelivered ? 'chat_error' : 'chat_send'
+ iconSize: ChatStyle.entry.message.outgoing.sendIconSize
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: isNotDelivered && proxyModel.resendMessage(index)
+ }
+ }
+ }
+
+ Component {
+ id: indicator
+ BusyIndicator {
+ width: ChatStyle.entry.message.outgoing.sendIconSize
+ }
+ }
+
+ Loader {
+ height: ChatStyle.entry.lineHeight
+ sourceComponent: $chatEntry.isOutgoing
+ ? (
+ $chatEntry.status === ChatModel.MessageStatusInProgress
+ ? indicator
+ : icon
+ ) : undefined
+ }
+
+ ActionButton {
+ height: ChatStyle.entry.lineHeight
+ icon: 'delete'
+ iconSize: ChatStyle.entry.deleteIconSize
+ visible: isHoverEntry()
+
+ onClicked: removeEntry()
+ }
+ }
+ }
+}
diff --git a/tests/ui/modules/Linphone/Chat/OutgoingMessage.qml b/tests/ui/modules/Linphone/Chat/OutgoingMessage.qml
index ffafa9b09..52b48d7c5 100644
--- a/tests/ui/modules/Linphone/Chat/OutgoingMessage.qml
+++ b/tests/ui/modules/Linphone/Chat/OutgoingMessage.qml
@@ -1,5 +1,5 @@
import QtQuick 2.7
-import QtQuick.Controls 1.4
+import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import Common 1.0
diff --git a/tests/ui/modules/Linphone/Styles/ChatStyle.qml b/tests/ui/modules/Linphone/Styles/ChatStyle.qml
index 2213f602d..45664b64c 100644
--- a/tests/ui/modules/Linphone/Styles/ChatStyle.qml
+++ b/tests/ui/modules/Linphone/Styles/ChatStyle.qml
@@ -56,6 +56,27 @@ QtObject {
property int rightMargin: 5
}
+ property QtObject file: QtObject {
+ property int height: 64
+ property int margins: 8
+ property int spacing: 8
+ property int width: 250
+
+ property QtObject status: QtObject {
+ property int spacing: 4
+
+ property QtObject bar: QtObject {
+ property int height: 6
+ property int radius: 3
+ }
+ }
+
+ property QtObject thumbnail: QtObject {
+ property int height: 50
+ property int width: 50
+ }
+ }
+
property QtObject images: QtObject {
property int height: 48
// `width` can be used.
diff --git a/tests/ui/scripts/Utils/utils.js b/tests/ui/scripts/Utils/utils.js
index c43b3773d..38f69fdc2 100644
--- a/tests/ui/scripts/Utils/utils.js
+++ b/tests/ui/scripts/Utils/utils.js
@@ -314,6 +314,25 @@ function find (obj, cb, context) {
// -----------------------------------------------------------------------------
+function formatSize (size) {
+ var units = ['KB', 'MB', 'GB', 'TB']
+ var unit = 'B'
+
+ if (size == null) {
+ size = 0
+ }
+
+ var length = units.length
+ for (var i = 0; size >= 1024 && i < length; i++) {
+ unit = units[i]
+ size /= 1024
+ }
+
+ return parseFloat(size.toFixed(2)) + unit
+}
+
+// -----------------------------------------------------------------------------
+
// Generate a random number in the [min, max[ interval.
// Uniform distrib.
function genRandomNumber (min, max) {