mirror of
https://gitlab.linphone.org/BC/public/linphone-desktop.git
synced 2026-01-24 07:08:07 +00:00
feat(ui/modules/Linphone/Chat/Chat): supports file upload
This commit is contained in:
parent
581eb69cd0
commit
530c3129f4
27 changed files with 577 additions and 99 deletions
|
|
@ -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
|
||||
|
|
|
|||
12
tests/assets/images/attachment_disabled.svg
Normal file
12
tests/assets/images/attachment_disabled.svg
Normal 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 |
|
|
@ -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'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_
|
||||
|
|
|
|||
19
tests/src/app/ThumbnailProvider.cpp
Normal file
19
tests/src/app/ThumbnailProvider.cpp
Normal 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);
|
||||
}
|
||||
21
tests/src/app/ThumbnailProvider.hpp
Normal file
21
tests/src/app/ThumbnailProvider.hpp
Normal 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_
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
225
tests/ui/modules/Linphone/Chat/FileMessage.qml
Normal file
225
tests/ui/modules/Linphone/Chat/FileMessage.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue