Commit 530c3129 authored by Ronan Abhamon's avatar Ronan Abhamon

feat(ui/modules/Linphone/Chat/Chat): supports file upload

parent 581eb69c
...@@ -56,6 +56,7 @@ set(SOURCES ...@@ -56,6 +56,7 @@ set(SOURCES
src/app/DefaultTranslator.cpp src/app/DefaultTranslator.cpp
src/app/Logger.cpp src/app/Logger.cpp
src/app/Paths.cpp src/app/Paths.cpp
src/app/ThumbnailProvider.cpp
src/components/camera/Camera.cpp src/components/camera/Camera.cpp
src/components/chat/ChatModel.cpp src/components/chat/ChatModel.cpp
src/components/chat/ChatProxyModel.cpp src/components/chat/ChatProxyModel.cpp
...@@ -81,6 +82,7 @@ set(HEADERS ...@@ -81,6 +82,7 @@ set(HEADERS
src/app/DefaultTranslator.hpp src/app/DefaultTranslator.hpp
src/app/Logger.hpp src/app/Logger.hpp
src/app/Paths.hpp src/app/Paths.hpp
src/app/ThumbnailProvider.hpp
src/components/camera/Camera.hpp src/components/camera/Camera.hpp
src/components/chat/ChatModel.hpp src/components/chat/ChatModel.hpp
src/components/chat/ChatProxyModel.hpp src/components/chat/ChatProxyModel.hpp
......
<?xml version="1.0" encoding="UTF-8"?>
<svg width="10px" height="20px" viewBox="0 0 10 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
<title>attachment_clic</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="attachment_clic" fill="#D0D8DE">
<path d="M8.76252348,14.9551548 C8.76095805,17.0249882 7.07341891,18.7247345 4.99686913,18.7489385 C2.91953663,18.7692387 1.23747652,17.1061888 1.23982467,15.0371361 L1.23982467,3.7807088 C1.23982467,2.39873777 2.36693801,1.26505306 3.75234815,1.25099912 C5.13619286,1.23538363 6.25469631,2.342522 6.25469631,3.7252738 L6.25469631,14.9824819 C6.25469631,15.6734674 5.6895742,16.2379674 4.99765185,16.2465559 C4.30572949,16.2543637 3.74843456,15.7007945 3.74686913,15.0082474 L3.74686913,5.00418259 L2.49295554,5.01667498 L2.49295554,15.0215206 C2.49295554,16.4034916 3.61380714,17.5114108 5,17.4973568 C6.38306199,17.4817414 7.50704446,16.350399 7.50860989,14.9699895 L7.50860989,3.71200064 C7.51095805,1.64216719 5.82889793,-0.0208826913 3.75078272,0.000198222717 C1.67501565,0.0236214605 0.00313087038,1.72102542 0,3.79163964 L0,15.6742482 C0.304477145,18.137592 2.43973075,20.025505 4.99452098,19.9997394 C7.55244208,19.9708508 9.69082655,18.0392144 10,15.567282 L10,2.4440227 L8.76252348,2.4440227 L8.76252348,14.9551548 Z"></path>
</g>
</g>
</svg>
...@@ -41,6 +41,11 @@ ...@@ -41,6 +41,11 @@
<source>newMessagePlaceholder</source> <source>newMessagePlaceholder</source>
<translation>Enter your message</translation> <translation>Enter your message</translation>
</message> </message>
<message>
<source>noFileTransferUrl</source>
<translation>Unable to send file.
Server url not configured.</translation>
</message>
</context> </context>
<context> <context>
<name>ConfirmDialog</name> <name>ConfirmDialog</name>
......
...@@ -33,6 +33,11 @@ ...@@ -33,6 +33,11 @@
<source>newMessagePlaceholder</source> <source>newMessagePlaceholder</source>
<translation>Entrer votre message.</translation> <translation>Entrer votre message.</translation>
</message> </message>
<message>
<source>noFileTransferUrl</source>
<translation>Impossible d&apos;envoyer un fichier.
Url du serveur non configurée.</translation>
</message>
</context> </context>
<context> <context>
<name>ConfirmDialog</name> <name>ConfirmDialog</name>
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<file>assets/images/add_hovered.svg</file> <file>assets/images/add_hovered.svg</file>
<file>assets/images/add_normal.svg</file> <file>assets/images/add_normal.svg</file>
<file>assets/images/add_pressed.svg</file> <file>assets/images/add_pressed.svg</file>
<file>assets/images/attachment_disabled.svg</file>
<file>assets/images/attachment_hovered.svg</file> <file>assets/images/attachment_hovered.svg</file>
<file>assets/images/attachment_normal.svg</file> <file>assets/images/attachment_normal.svg</file>
<file>assets/images/attachment_pressed.svg</file> <file>assets/images/attachment_pressed.svg</file>
...@@ -204,6 +205,7 @@ ...@@ -204,6 +205,7 @@
<file>ui/modules/Linphone/Call/PausedCallControls.qml</file> <file>ui/modules/Linphone/Call/PausedCallControls.qml</file>
<file>ui/modules/Linphone/Chat/Chat.qml</file> <file>ui/modules/Linphone/Chat/Chat.qml</file>
<file>ui/modules/Linphone/Chat/Event.qml</file> <file>ui/modules/Linphone/Chat/Event.qml</file>
<file>ui/modules/Linphone/Chat/FileMessage.qml</file>
<file>ui/modules/Linphone/Chat/IncomingMessage.qml</file> <file>ui/modules/Linphone/Chat/IncomingMessage.qml</file>
<file>ui/modules/Linphone/Chat/Message.qml</file> <file>ui/modules/Linphone/Chat/Message.qml</file>
<file>ui/modules/Linphone/Chat/OutgoingMessage.qml</file> <file>ui/modules/Linphone/Chat/OutgoingMessage.qml</file>
......
...@@ -43,8 +43,9 @@ App::App (int &argc, char **argv) : QApplication(argc, argv) { ...@@ -43,8 +43,9 @@ App::App (int &argc, char **argv) : QApplication(argc, argv) {
.arg(current_locale.name()); .arg(current_locale.name());
} }
// Provide avatars loader. // Provide avatars/thumbnails providers.
m_engine.addImageProvider(AvatarProvider::PROVIDER_ID, &m_avatar_provider); m_engine.addImageProvider(AvatarProvider::PROVIDER_ID, &m_avatar_provider);
m_engine.addImageProvider(ThumbnailProvider::PROVIDER_ID, &m_thumbnail_provider);
setWindowIcon(QIcon(WINDOW_ICON_PATH)); setWindowIcon(QIcon(WINDOW_ICON_PATH));
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "../components/notifier/Notifier.hpp" #include "../components/notifier/Notifier.hpp"
#include "AvatarProvider.hpp" #include "AvatarProvider.hpp"
#include "DefaultTranslator.hpp" #include "DefaultTranslator.hpp"
#include "ThumbnailProvider.hpp"
// ============================================================================= // =============================================================================
...@@ -57,6 +58,7 @@ private: ...@@ -57,6 +58,7 @@ private:
QSystemTrayIcon *m_system_tray_icon = nullptr; QSystemTrayIcon *m_system_tray_icon = nullptr;
AvatarProvider m_avatar_provider; AvatarProvider m_avatar_provider;
ThumbnailProvider m_thumbnail_provider;
DefaultTranslator m_default_translator; DefaultTranslator m_default_translator;
QTranslator m_english_translator; QTranslator m_english_translator;
......
...@@ -7,18 +7,13 @@ ...@@ -7,18 +7,13 @@
const QString AvatarProvider::PROVIDER_ID = "avatar"; const QString AvatarProvider::PROVIDER_ID = "avatar";
AvatarProvider::AvatarProvider () : AvatarProvider::AvatarProvider () : QQuickImageProvider(
QQuickImageProvider(
QQmlImageProviderBase::Image, QQmlImageProviderBase::Image,
QQmlImageProviderBase::ForceAsynchronousImageLoading QQmlImageProviderBase::ForceAsynchronousImageLoading
) { ) {
m_avatars_path = Utils::linphoneStringToQString(Paths::getAvatarsDirpath()); m_avatars_path = Utils::linphoneStringToQString(Paths::getAvatarsDirpath());
} }
QImage AvatarProvider::requestImage ( QImage AvatarProvider::requestImage (const QString &id, QSize *, const QSize &) {
const QString &id,
QSize *,
const QSize &
) {
return QImage(m_avatars_path + id); return QImage(m_avatars_path + id);
} }
...@@ -10,11 +10,7 @@ public: ...@@ -10,11 +10,7 @@ public:
AvatarProvider (); AvatarProvider ();
~AvatarProvider () = default; ~AvatarProvider () = default;
QImage requestImage ( QImage requestImage (const QString &id, QSize *size, const QSize &requested_size) override;
const QString &id,
QSize *size,
const QSize &requested_size
) override;
static const QString PROVIDER_ID; static const QString PROVIDER_ID;
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
#ifdef _WIN32 #ifdef _WIN32
#define MAIN_PATH \ #define MAIN_PATH \
QStandardPaths::writableLocation(QStandardPaths::DataLocation) (QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/")
#define PATH_CONFIG "linphonerc" #define PATH_CONFIG "linphonerc"
#define LINPHONE_FOLDER "linphone/" #define LINPHONE_FOLDER "linphone/"
...@@ -19,15 +19,16 @@ ...@@ -19,15 +19,16 @@
#else #else
#define MAIN_PATH \ #define MAIN_PATH \
QStandardPaths::writableLocation(QStandardPaths::HomeLocation) (QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/")
#define PATH_CONFIG ".linphonerc" #define PATH_CONFIG ".linphonerc"
#define LINPHONE_FOLDER ".linphone/" #define LINPHONE_FOLDER ".linphone/"
#endif // ifdef _WIN32 #endif // ifdef _WIN32
#define PATH_AVATARS LINPHONE_FOLDER "avatars/" #define PATH_AVATARS (LINPHONE_FOLDER "avatars/")
#define PATH_LOGS LINPHONE_FOLDER "logs/" #define PATH_LOGS (LINPHONE_FOLDER "logs/")
#define PATH_THUMBNAILS (LINPHONE_FOLDER "thumbnails/")
#define PATH_CALL_HISTORY_LIST ".linphone-call-history.db" #define PATH_CALL_HISTORY_LIST ".linphone-call-history.db"
#define PATH_FRIENDS_LIST ".linphone-friends.db" #define PATH_FRIENDS_LIST ".linphone-friends.db"
...@@ -65,25 +66,29 @@ inline string getFilePath (const QString &filename) { ...@@ -65,25 +66,29 @@ inline string getFilePath (const QString &filename) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
string Paths::getAvatarsDirpath () { string Paths::getAvatarsDirpath () {
return getDirectoryPath(MAIN_PATH + "/" PATH_AVATARS); return getDirectoryPath(MAIN_PATH + PATH_AVATARS);
} }
string Paths::getCallHistoryFilepath () { string Paths::getCallHistoryFilepath () {
return getFilePath(MAIN_PATH + "/" + PATH_CALL_HISTORY_LIST); return getFilePath(MAIN_PATH + PATH_CALL_HISTORY_LIST);
} }
string Paths::getConfigFilepath () { string Paths::getConfigFilepath () {
return getFilePath(MAIN_PATH + "/" + PATH_CONFIG); return getFilePath(MAIN_PATH + PATH_CONFIG);
} }
string Paths::getFriendsListFilepath () { string Paths::getFriendsListFilepath () {
return getFilePath(MAIN_PATH + "/" + PATH_FRIENDS_LIST); return getFilePath(MAIN_PATH + PATH_FRIENDS_LIST);
} }
string Paths::getLogsDirpath () { string Paths::getLogsDirpath () {
return getDirectoryPath(MAIN_PATH + "/" PATH_LOGS); return getDirectoryPath(MAIN_PATH + PATH_LOGS);
} }
string Paths::getMessageHistoryFilepath () { 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);
} }
...@@ -12,6 +12,7 @@ namespace Paths { ...@@ -12,6 +12,7 @@ namespace Paths {
std::string getFriendsListFilepath (); std::string getFriendsListFilepath ();
std::string getLogsDirpath (); std::string getLogsDirpath ();
std::string getMessageHistoryFilepath (); std::string getMessageHistoryFilepath ();
std::string getThumbnailsDirPath ();
} }
#endif // PATHS_H_ #endif // PATHS_H_
#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);
}
#ifndef THUMBNAIL_PROVIDER_H_
#define THUMBNAIL_PROVIDER_H_
#include <QQuickImageProvider>
// =============================================================================
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_
#include <algorithm> #include <algorithm>
#include <QDateTime> #include <QDateTime>
#include <QTimer> #include <QFileInfo>
#include <QImage>
#include <QtDebug> #include <QtDebug>
#include <QTimer>
#include <QUuid>
#include "../../app/Paths.hpp"
#include "../../app/ThumbnailProvider.hpp"
#include "../../utils.hpp" #include "../../utils.hpp"
#include "../core/CoreManager.hpp" #include "../core/CoreManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#define THUMBNAIL_IMAGE_FILE_HEIGHT 100
#define THUMBNAIL_IMAGE_FILE_WIDTH 100
using namespace std; using namespace std;
// ============================================================================= // =============================================================================
inline void fillThumbnailProperty (QVariantMap &dest, const shared_ptr<linphone::ChatMessage> &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<linphone::ChatMessage> &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 { class ChatModel::MessageHandlers : public linphone::ChatMessageListener {
friend class ChatModel; friend class ChatModel;
...@@ -22,48 +52,93 @@ public: ...@@ -22,48 +52,93 @@ public:
~MessageHandlers () = default; ~MessageHandlers () = default;
private: private:
QList<ChatEntryData>::iterator findMessageEntry (const shared_ptr<linphone::ChatMessage> &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<ChatEntryData>::iterator &it) {
int row = static_cast<int>(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 ( void onFileTransferRecv (
const shared_ptr<linphone::ChatMessage> &message, const shared_ptr<linphone::ChatMessage> &,
const shared_ptr<linphone::Content> &content, const shared_ptr<linphone::Content> &,
const shared_ptr<linphone::Buffer> &buffer const shared_ptr<linphone::Buffer> &
) override { ) override {
qDebug() << "Not yet implemented"; qWarning() << "`onFileTransferRecv` called.";
} }
shared_ptr<linphone::Buffer> onFileTransferSend ( shared_ptr<linphone::Buffer> onFileTransferSend (
const shared_ptr<linphone::ChatMessage> &message, const shared_ptr<linphone::ChatMessage> &,
const shared_ptr<linphone::Content> &content, const shared_ptr<linphone::Content> &,
size_t offset, size_t,
size_t size size_t
) override { ) override {
qDebug() << "Not yet implemented"; qWarning() << "`onFileTransferSend` called.";
return nullptr;
} }
void onFileTransferProgressIndication ( void onFileTransferProgressIndication (
const shared_ptr<linphone::ChatMessage> &message, const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content, const shared_ptr<linphone::Content> &,
size_t offset, size_t offset,
size_t total size_t
) override { ) 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<quint64>(offset);
signalDataChanged(it);
} }
void onMsgStateChanged (const shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessageState state) override { void onMsgStateChanged (const shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessageState state) override {
if (!m_chat_model) if (!m_chat_model)
return; return;
ChatModel &chat = *m_chat_model; auto it = findMessageEntry(message);
if (it == m_chat_model->m_entries.end())
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())
return; return;
if (state == linphone::ChatMessageStateFileTransferError)
state = linphone::ChatMessageStateNotDelivered;
else if (state == linphone::ChatMessageStateFileTransferDone) {
QString thumbnail_path = ::Utils::linphoneStringToQString(message->getFileTransferFilepath());
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; (*it).first["status"] = state;
int row = distance(chat.m_entries.begin(), it);
emit chat.dataChanged(chat.index(row, 0), chat.index(row, 0)); signalDataChanged(it);
} }
ChatModel *m_chat_model; ChatModel *m_chat_model;
...@@ -245,15 +320,22 @@ void ChatModel::removeAllEntries () { ...@@ -245,15 +320,22 @@ void ChatModel::removeAllEntries () {
} }
void ChatModel::sendMessage (const QString &message) { void ChatModel::sendMessage (const QString &message) {
if (!m_chat_room)
return;
shared_ptr<linphone::ChatMessage> _message = m_chat_room->createMessage(::Utils::qStringToLinphoneString(message)); shared_ptr<linphone::ChatMessage> _message = m_chat_room->createMessage(::Utils::qStringToLinphoneString(message));
_message->setListener(m_message_handlers); _message->setListener(m_message_handlers);
m_chat_room->sendMessage(_message);
insertMessageAtEnd(_message); insertMessageAtEnd(_message);
m_chat_room->sendMessage(_message);
emit messageSent(_message); emit messageSent(_message);
} }
void ChatModel::resendMessage (int id) { void ChatModel::resendMessage (int id) {
if (!m_chat_room)
return;
if (id < 0 || id > m_entries.count()) { if (id < 0 || id > m_entries.count()) {
qWarning() << QStringLiteral("Entry %1 not exists.").arg(id); qWarning() << QStringLiteral("Entry %1 not exists.").arg(id);
return; return;
...@@ -275,23 +357,48 @@ void ChatModel::resendMessage (int id) { ...@@ -275,23 +357,48 @@ void ChatModel::resendMessage (int id) {
m_chat_room->sendMessage(message); 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<linphone::Content> 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<linphone::ChatMessage> 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 ( void ChatModel::fillMessageEntry (QVariantMap &dest, const shared_ptr<linphone::ChatMessage> &message) {
QVariantMap &dest,
const shared_ptr<linphone::ChatMessage> &message
) {
dest["type"] = EntryType::MessageEntry; dest["type"] = EntryType::MessageEntry;
dest["timestamp"] = QDateTime::fromMSecsSinceEpoch(message->getTime() * 1000); dest["timestamp"] = QDateTime::fromMSecsSinceEpoch(message->getTime() * 1000);
dest["content"] = ::Utils::linphoneStringToQString(message->getText()); dest["content"] = ::Utils::linphoneStringToQString(message->getText());
dest["isOutgoing"] = message->isOutgoing(); dest["isOutgoing"] = message->isOutgoing() || message->getState() == linphone::ChatMessageStateIdle;
dest["status"] = message->getState(); dest["status"] = message->getState();
shared_ptr<linphone::Content> content = message->getFileTransferInformation();
if (content) {
dest["fileSize"] = static_cast<quint64>(content->getSize());
dest["fileName"] = ::Utils::linphoneStringToQString(content->getName());
fillThumbnailProperty(dest, message);
}
} }
void ChatModel::fillCallStartEntry ( void ChatModel::fillCallStartEntry (QVariantMap &dest, const shared_ptr<linphone::CallLog> &call_log) {
QVariantMap &dest,
const shared_ptr<linphone::CallLog> &call_log
) {
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(call_log->getStartDate() * 1000); QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(call_log->getStartDate() * 1000);
dest["type"] = EntryType::CallEntry; dest["type"] = EntryType::CallEntry;
...@@ -301,10 +408,7 @@ void ChatModel::fillCallStartEntry ( ...@@ -301,10 +408,7 @@ void ChatModel::fillCallStartEntry (
dest["isStart"] = true; dest["isStart"] = true;
} }
void ChatModel::fillCallEndEntry ( void ChatModel::fillCallEndEntry (QVariantMap &dest, const shared_ptr<linphone::CallLog> &call_log) {
QVariantMap &dest,
const shared_ptr<linphone::CallLog> &call_log
) {
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch((call_log->getStartDate() + call_log->getDuration()) * 1000); QDateTime timestamp = QDateTime::fromMSecsSinceEpoch((call_log->getStartDate() + call_log->getDuration()) * 1000);
dest["type"] = EntryType::CallEntry; dest["type"] = EntryType::CallEntry;
...@@ -320,10 +424,14 @@ void ChatModel::removeEntry (ChatEntryData &pair) { ...@@ -320,10 +424,14 @@ void ChatModel::removeEntry (ChatEntryData &pair) {
int type = pair.first["type"].toInt(); int type = pair.first["type"].toInt();
switch (type) { switch (type) {
case ChatModel::MessageEntry: case ChatModel::MessageEntry: {
m_chat_room->deleteMessage(static_pointer_cast<linphone::ChatMessage>(pair.second)); shared_ptr<linphone::ChatMessage> message = static_pointer_cast<linphone::ChatMessage>(pair.second);
removeFileMessageThumbnail(message);
m_chat_room->deleteMessage(message);
break; break;
case ChatModel::CallEntry: }
case ChatModel::CallEntry: {
if (pair.first["status"].toInt() == linphone::CallStatusSuccess) { if (pair.first["status"].toInt() == linphone::CallStatusSuccess) {
// WARNING: Unable to remove symmetric call here. (start/end) // WARNING: Unable to remove symmetric call here. (start/end)
// We are between `beginRemoveRows` and `endRemoveRows`. // We are between `beginRemoveRows` and `endRemoveRows`.
...@@ -343,6 +451,8 @@ void ChatModel::removeEntry (ChatEntryData &pair) { ...@@ -343,6 +451,8 @@ void ChatModel::removeEntry (ChatEntryData &pair) {
CoreManager::getInstance()->getCore()->removeCallLog(static_pointer_cast<linphone::CallLog>(pair.second)); CoreManager::getInstance()->getCore()->removeCallLog(static_pointer_cast<linphone::CallLog>(pair.second));
break; break;
}
default: default:
qWarning() << QStringLiteral("Unknown chat entry type: %1.").arg(type); qWarning() << QStringLiteral("Unknown chat entry type: %1.").arg(type);
} }
......
...@@ -18,8 +18,6 @@ class ChatModel : public QAbstractListModel { ...@@ -18,8 +18,6 @@ class ChatModel : public QAbstractListModel {
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged); Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
public: public:
typedef QPair<QVariantMap, std::shared_ptr<void> > ChatEntryData;
enum Roles { enum Roles {
ChatEntry = Qt::DisplayRole, ChatEntry = Qt::DisplayRole,
SectionDate SectionDate
...@@ -70,6 +68,8 @@ public: ...@@ -70,6 +68,8 @@ public:
void resendMessage (int id); void resendMessage (int id);
void sendFileMessage (const QString &path);
signals: signals:
void sipAddressChanged (const QString &sip_address); void sipAddressChanged (const QString &sip_address);
void allEntriesRemoved (); void allEntriesRemoved ();
...@@ -80,20 +80,11 @@ signals: ...@@ -80,20 +80,11 @@ signals:
void messagesCountReset (); void messagesCountReset ();
private: private:
void fillMessageEntry ( typedef QPair<QVariantMap, std::shared_ptr<void> > ChatEntryData;
QVariantMap &dest,
const std::shared_ptr<linphone::ChatMessage> &message void fillMessageEntry (QVariantMap &dest, const std::shared_ptr<linphone::ChatMessage> &message);
); void fillCallStartEntry (QVariantMap &dest, const std::shared_ptr<linphone::CallLog> &call_log);
void fillCallEndEntry (QVariantMap &dest, const std::shared_ptr<linphone::CallLog> &call_log);
void fillCallStartEntry (
QVariantMap &dest,
const std::shared_ptr<linphone::CallLog> &call_log
);
void fillCallEndEntry (
QVariantMap &dest,
const std::shared_ptr<linphone::CallLog> &call_log
);
void removeEntry (ChatEntryData &pair); void removeEntry (ChatEntryData &pair);
......
...@@ -105,10 +105,18 @@ void ChatProxyModel::resendMessage (int id) { ...@@ -105,10 +105,18 @@ void ChatProxyModel::resendMessage (int id) {
); );
} }
void ChatProxyModel::sendFileMessage (const QString &path) {
static_cast<ChatModel *>(m_chat_model_filter->sourceModel())->sendFileMessage(path);
}
// -----------------------------------------------------------------------------
bool ChatProxyModel::filterAcceptsRow (int source_row, const QModelIndex &) const { bool ChatProxyModel::filterAcceptsRow (int source_row, const QModelIndex &) const {
return m_chat_model_filter->rowCount() - source_row <= m_n_max_displayed_entries; return m_chat_model_filter->rowCount() - source_row <= m_n_max_displayed_entries;
} }
// -----------------------------------------------------------------------------
QString ChatProxyModel::getSipAddress () const { QString ChatProxyModel::getSipAddress () const {
return static_cast<ChatModel *>(m_chat_model_filter->sourceModel())->getSipAddress(); return static_cast<ChatModel *>(m_chat_model_filter->sourceModel())->getSipAddress();
} }
......
...@@ -12,12 +12,7 @@ class ChatProxyModel : public QSortFilterProxyModel { ...@@ -12,12 +12,7 @@ class ChatProxyModel : public QSortFilterProxyModel {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY( Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
QString sipAddress
READ getSipAddress
WRITE setSipAddress
NOTIFY sipAddressChanged
);
public: public:
ChatProxyModel (QObject *parent = Q_NULLPTR); ChatProxyModel (QObject *parent = Q_NULLPTR);
...@@ -31,6 +26,8 @@ public: ...@@ -31,6 +26,8 @@ public:
Q_INVOKABLE void sendMessage (const QString &message); Q_INVOKABLE void sendMessage (const QString &message);
Q_INVOKABLE void resendMessage (int id); Q_INVOKABLE void resendMessage (int id);
Q_INVOKABLE void sendFileMessage (const QString &path);
signals: signals:
void sipAddressChanged (const QString &sip_address); void sipAddressChanged (const QString &sip_address);
void moreEntriesLoaded (int n); void moreEntriesLoaded (int n);
......
#include "../../utils.hpp"
#include "../core/CoreManager.hpp" #include "../core/CoreManager.hpp"
#include "SettingsModel.hpp" #include "SettingsModel.hpp"
...@@ -18,7 +19,19 @@ bool SettingsModel::getAutoAnswerStatus () const { ...@@ -18,7 +19,19 @@ bool SettingsModel::getAutoAnswerStatus () const {
return !!m_config->getInt(UI_SECTION, "auto_answer", 0); 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); m_config->setInt(UI_SECTION, "auto_answer", status);
emit autoAnswerStatusChanged(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)
);
}
...@@ -4,24 +4,27 @@ ...@@ -4,24 +4,27 @@
#include <linphone++/linphone.hh> #include <linphone++/linphone.hh>
#include <QObject> #include <QObject>
#include "AccountSettingsModel.hpp"
// ============================================================================= // =============================================================================
class SettingsModel : public QObject { class SettingsModel : public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(bool autoAnswerStatus READ getAutoAnswerStatus WRITE setAutoAnswerStatus NOTIFY autoAnswerStatusChanged); Q_PROPERTY(bool autoAnswerStatus READ getAutoAnswerStatus WRITE setAutoAnswerStatus NOTIFY autoAnswerStatusChanged);
Q_PROPERTY(QString fileTransferUrl READ getFileTransferUrl WRITE setFileTransferUrl NOTIFY fileTransferUrlChanged);
public: public:
SettingsModel (QObject *parent = Q_NULLPTR); SettingsModel (QObject *parent = Q_NULLPTR);
signals: signals:
void autoAnswerStatusChanged (bool status); void autoAnswerStatusChanged (bool status);
void fileTransferUrlChanged (const QString &url);
private: private:
bool getAutoAnswerStatus () const; bool getAutoAnswerStatus () const;
bool setAutoAnswerStatus (bool status); void setAutoAnswerStatus (bool status);
QString getFileTransferUrl () const;
void setFileTransferUrl (const QString &url);
std::shared_ptr<linphone::Config> m_config; std::shared_ptr<linphone::Config> m_config;
......
...@@ -21,6 +21,7 @@ QtObject { ...@@ -21,6 +21,7 @@ QtObject {
property color k: '#FFFFFF' property color k: '#FFFFFF'
property color k50: '#32FFFFFF' property color k50: '#32FFFFFF'
property color l: '#000000' property color l: '#000000'
property color l50: '#32000000'
property color m: '#D1D1D1' property color m: '#D1D1D1'
property color n: '#C0C0C0' property color n: '#C0C0C0'
property color o: '#232323' property color o: '#232323'
...@@ -34,4 +35,5 @@ QtObject { ...@@ -34,4 +35,5 @@ QtObject {
property color w: '#A1A1A1' property color w: '#A1A1A1'
property color x: '#96A5B1' property color x: '#96A5B1'
property color y: '#D0D8DE' property color y: '#D0D8DE'
property color z: '#17A81A'
} }
...@@ -7,9 +7,14 @@ import Common.Styles 1.0 ...@@ -7,9 +7,14 @@ import Common.Styles 1.0
// ============================================================================= // =============================================================================
Item { Item {
id: droppableTextArea
property alias placeholderText: textArea.placeholderText property alias placeholderText: textArea.placeholderText
property alias text: textArea.text property alias text: textArea.text
property bool dropEnabled: true
property string dropDisabledReason
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
signal dropped (var files) signal dropped (var files)
...@@ -73,6 +78,7 @@ Item { ...@@ -73,6 +78,7 @@ Item {
DroppableTextAreaStyle.fileChooserButton.margins DroppableTextAreaStyle.fileChooserButton.margins
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
enabled: droppableTextArea.dropEnabled
icon: 'attachment' icon: 'attachment'
iconSize: DroppableTextAreaStyle.fileChooserButton.size iconSize: DroppableTextAreaStyle.fileChooserButton.size
...@@ -88,7 +94,9 @@ Item { ...@@ -88,7 +94,9 @@ Item {
} }
TooltipArea { TooltipArea {
text: qsTr('attachmentTooltip') text: droppableTextArea.dropEnabled
? qsTr('attachmentTooltip')
: droppableTextArea.dropDisabledReason
} }
} }
...@@ -111,6 +119,7 @@ Item { ...@@ -111,6 +119,7 @@ Item {
DropArea { DropArea {
anchors.fill: parent anchors.fill: parent
keys: [ 'text/uri-list' ] keys: [ 'text/uri-list' ]
visible: droppableTextArea.dropEnabled
onDropped: { onDropped: {
state = '' state = ''
......
...@@ -25,13 +25,7 @@ Item { ...@@ -25,13 +25,7 @@ Item {
ExclusiveButtons { ExclusiveButtons {
id: exclusiveButtons id: exclusiveButtons
texts: [ texts: ['A', 'B', 'C', 'D', 'E']
qsTr('A'),
qsTr('B'),
qsTr('C'),
qsTr('D'),
qsTr('E')
]
} }
SignalSpy { SignalSpy {
......
...@@ -191,6 +191,11 @@ ColumnLayout { ...@@ -191,6 +191,11 @@ ColumnLayout {
OutgoingMessage {} OutgoingMessage {}
} }
Component {
id: fileMessage
FileMessage {}
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
MouseArea { MouseArea {
...@@ -223,9 +228,17 @@ ColumnLayout { ...@@ -223,9 +228,17 @@ ColumnLayout {
// Display content. // Display content.
Loader { Loader {
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: $chatEntry.type === ChatModel.MessageEntry sourceComponent: {
? ($chatEntry.isOutgoing ? outgoingMessage : incomingMessage) if ($chatEntry.fileName) {
: event return fileMessage
}
if ($chatEntry.type === ChatModel.CallEntry) {
return event
}
return $chatEntry.isOutgoing ? outgoingMessage : incomingMessage
}
} }
} }
} }
...@@ -245,8 +258,15 @@ ColumnLayout { ...@@ -245,8 +258,15 @@ ColumnLayout {
DroppableTextArea { DroppableTextArea {
anchors.fill: parent anchors.fill: parent
dropEnabled: SettingsModel.fileTransferUrl.length > 0
dropDisabledReason: qsTr('noFileTransferUrl')
placeholderText: qsTr('newMessagePlaceholder') placeholderText: qsTr('newMessagePlaceholder')
onDropped: {
_bindToEnd = true
files.forEach(proxyModel.sendFileMessage)
}
onValidText: { onValidText: {
this.text = '' this.text = ''
_bindToEnd = true _bindToEnd = true
......
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()
}
}
}
}
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Common 1.0 import Common 1.0
......
...@@ -56,6 +56,27 @@ QtObject { ...@@ -56,6 +56,27 @@ QtObject {
property int rightMargin: 5 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 QtObject images: QtObject {
property int height: 48 property int height: 48
// `width` can be used. // `width` can be used.
......
...@@ -314,6 +314,25 @@ function find (obj, cb, context) { ...@@ -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. // Generate a random number in the [min, max[ interval.
// Uniform distrib. // Uniform distrib.
function genRandomNumber (min, max) { function genRandomNumber (min, max) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment