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 @@ + + + + attachment_clic + Created with Sketch. + + + + + + + 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) {