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
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
......
<?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 @@
<source>newMessagePlaceholder</source>
<translation>Enter your message</translation>
</message>
<message>
<source>noFileTransferUrl</source>
<translation>Unable to send file.
Server url not configured.</translation>
</message>
</context>
<context>
<name>ConfirmDialog</name>
......
......@@ -33,6 +33,11 @@
<source>newMessagePlaceholder</source>
<translation>Entrer votre message.</translation>
</message>
<message>
<source>noFileTransferUrl</source>
<translation>Impossible d&apos;envoyer un fichier.
Url du serveur non configurée.</translation>
</message>
</context>
<context>
<name>ConfirmDialog</name>
......
......@@ -4,6 +4,7 @@
<file>assets/images/add_hovered.svg</file>
<file>assets/images/add_normal.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_normal.svg</file>
<file>assets/images/attachment_pressed.svg</file>
......@@ -204,6 +205,7 @@
<file>ui/modules/Linphone/Call/PausedCallControls.qml</file>
<file>ui/modules/Linphone/Chat/Chat.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/Message.qml</file>
<file>ui/modules/Linphone/Chat/OutgoingMessage.qml</file>
......
......@@ -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));
......
......@@ -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;
......
......@@ -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);
}
......@@ -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;
......
......@@ -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);
}
......@@ -12,6 +12,7 @@ namespace Paths {
std::string getFriendsListFilepath ();
std::string getLogsDirpath ();
std::string getMessageHistoryFilepath ();
std::string getThumbnailsDirPath ();
}
#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 <QDateTime>
#include <QTimer>
#include <QFileInfo>
#include <QImage>
#include <QtDebug>
#include <QTimer>
#include <QUuid>
#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<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 {
friend class ChatModel;
......@@ -22,48 +52,93 @@ public:
~MessageHandlers () = default;
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 (
const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content,
const shared_ptr<linphone::Buffer> &buffer
const shared_ptr<linphone::ChatMessage> &,
const shared_ptr<linphone::Content> &,
const shared_ptr<linphone::Buffer> &
) override {
qDebug() << "Not yet implemented";
qWarning() << "`onFileTransferRecv` called.";
}
shared_ptr<linphone::Buffer> onFileTransferSend (
const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content,
size_t offset,
size_t size
const shared_ptr<linphone::ChatMessage> &,
const shared_ptr<linphone::Content> &,
size_t,
size_t
) override {
qDebug() << "Not yet implemented";
qWarning() << "`onFileTransferSend` called.";
return nullptr;
}
void onFileTransferProgressIndication (
const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content,
const shared_ptr<linphone::Content> &,
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<quint64>(offset);
signalDataChanged(it);
}
void onMsgStateChanged (const shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessageState state) override {
if (!m_chat_model)
return;
ChatModel &chat = *m_chat_model;
auto it = findMessageEntry(message);
if (it == m_chat_model->m_entries.end())
return;
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())
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;
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;
......@@ -245,15 +320,22 @@ void ChatModel::removeAllEntries () {
}
void ChatModel::sendMessage (const QString &message) {
if (!m_chat_room)
return;
shared_ptr<linphone::ChatMessage> _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<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 (
QVariantMap &dest,
const shared_ptr<linphone::ChatMessage> &message
) {
void ChatModel::fillMessageEntry (QVariantMap &dest, const shared_ptr<linphone::ChatMessage> &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<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 (
QVariantMap &dest,
const shared_ptr<linphone::CallLog> &call_log
) {
void ChatModel::fillCallStartEntry (QVariantMap &dest, const shared_ptr<linphone::CallLog> &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<linphone::CallLog> &call_log
) {
void ChatModel::fillCallEndEntry (QVariantMap &dest, const shared_ptr<linphone::CallLog> &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<linphone::ChatMessage>(pair.second));
case ChatModel::MessageEntry: {
shared_ptr<linphone::ChatMessage> message = static_pointer_cast<linphone::ChatMessage>(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<linphone::CallLog>(pair.second));
break;
}
default:
qWarning() << QStringLiteral("Unknown chat entry type: %1.").arg(type);
}
......
......@@ -18,8 +18,6 @@ class ChatModel : public QAbstractListModel {
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
public:
typedef QPair<QVariantMap, std::shared_ptr<void> > 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<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
);
typedef QPair<QVariantMap, std::shared_ptr<void> > ChatEntryData;
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 removeEntry (ChatEntryData &pair);
......
......@@ -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 {
return m_chat_model_filter->rowCount() - source_row <= m_n_max_displayed_entries;
}
// -----------------------------------------------------------------------------
QString ChatProxyModel::getSipAddress () const {
return static_cast<ChatModel *>(m_chat_model_filter->sourceModel())->getSipAddress();
}
......
......@@ -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);
......
#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)
);
}
......@@ -4,24 +4,27 @@
#include <linphone++/linphone.hh>
#include <QObject>
#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<linphone::Config> m_config;
......
......@@ -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'
}
......@@ -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 = ''
......
......@@ -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 {
......
......@@ -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
......
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.Controls 1.4
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import Common 1.0
......
......@@ -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.
......
......@@ -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) {
......
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