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

This commit is contained in:
Ronan Abhamon 2017-01-16 11:57:57 +01:00
parent 581eb69cd0
commit 530c3129f4
27 changed files with 577 additions and 99 deletions

View file

@ -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

View file

@ -0,0 +1,12 @@
<?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>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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));

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -12,6 +12,7 @@ namespace Paths {
std::string getFriendsListFilepath ();
std::string getLogsDirpath ();
std::string getMessageHistoryFilepath ();
std::string getThumbnailsDirPath ();
}
#endif // PATHS_H_

View file

@ -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);
}

View file

@ -0,0 +1,21 @@
#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_

View file

@ -1,18 +1,48 @@
#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 = 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<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);
}

View file

@ -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
);
typedef QPair<QVariantMap, std::shared_ptr<void> > ChatEntryData;
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 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);

View file

@ -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();
}

View file

@ -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);

View file

@ -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)
);
}

View file

@ -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;

View file

@ -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'
}

View file

@ -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 = ''

View file

@ -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 {

View file

@ -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

View file

@ -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()
}
}
}
}

View file

@ -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

View file

@ -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.

View file

@ -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) {