Chat message content

This commit is contained in:
Gaelle Braud 2025-05-26 17:34:00 +02:00
parent af2350cd16
commit 9d5935fb53
72 changed files with 6660 additions and 1266 deletions

View file

@ -263,16 +263,16 @@ else()
include(cmake/TasksMacos.cmake)
endif()
if (ENABLE_QT_KEYCHAIN)
target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/external/qtkeychain)
target_link_libraries(${TARGET_NAME} PUBLIC ${QTKEYCHAIN_TARGET_NAME})
message(STATUS "link libraries: ${TARGET_NAME} ${QTKEYCHAIN_TARGET_NAME}")
endif()
# if (ENABLE_QT_KEYCHAIN)
# target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/external/qtkeychain)
# target_link_libraries(${TARGET_NAME} PUBLIC ${QTKEYCHAIN_TARGET_NAME})
# message(STATUS "link libraries: ${TARGET_NAME} ${QTKEYCHAIN_TARGET_NAME}")
# message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
# message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
# message(STATUS "Contents of qtkeychain:")
# file(GLOB QTKEYCHAIN_HEADERS "${PROJECT_SOURCE_DIR}/external/qtkeychain/qtkeychain/*.h")
# message(STATUS "Found headers: ${QTKEYCHAIN_HEADERS}")
# endif()
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "Contents of qtkeychain:")
file(GLOB QTKEYCHAIN_HEADERS "${PROJECT_SOURCE_DIR}/external/qtkeychain/qtkeychain/*.h")
message(STATUS "Found headers: ${QTKEYCHAIN_HEADERS}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/cmake/hook/pre-commit" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/.git/hooks/")

View file

@ -54,10 +54,12 @@
#include "core/call/CallProxy.hpp"
#include "core/camera/CameraGui.hpp"
#include "core/chat/ChatProxy.hpp"
#include "core/chat/message/EventLogGui.hpp"
#include "core/chat/message/ChatMessageGui.hpp"
#include "core/chat/message/EventLogGui.hpp"
#include "core/chat/message/EventLogList.hpp"
#include "core/chat/message/EventLogProxy.hpp"
#include "core/chat/message/content/ChatMessageContentGui.hpp"
#include "core/chat/message/content/ChatMessageContentProxy.hpp"
#include "core/conference/ConferenceGui.hpp"
#include "core/conference/ConferenceInfoGui.hpp"
#include "core/conference/ConferenceInfoProxy.hpp"
@ -96,9 +98,11 @@
#include "tool/providers/EmojiProvider.hpp"
#include "tool/providers/ImageProvider.hpp"
#include "tool/providers/ScreenProvider.hpp"
#include "tool/providers/ThumbnailProvider.hpp"
#include "tool/request/CallbackHelper.hpp"
#include "tool/request/RequestDialog.hpp"
#include "tool/thread/Thread.hpp"
#include "tool/ui/DashRectangle.hpp"
DEFINE_ABSTRACT_OBJECT(App)
@ -495,6 +499,7 @@ void App::initCore() {
mEngine->addImageProvider(ScreenProvider::ProviderId, new ScreenProvider());
mEngine->addImageProvider(WindowProvider::ProviderId, new WindowProvider());
mEngine->addImageProvider(WindowIconProvider::ProviderId, new WindowIconProvider());
mEngine->addImageProvider(ThumbnailProvider::ProviderId, new ThumbnailProvider());
// Enable notifications.
mNotifier = new Notifier(mEngine);
@ -551,7 +556,7 @@ void App::initCore() {
static bool firstOpen = true;
if (!firstOpen || !mParser->isSet("minimized")) {
lDebug() << log().arg("Openning window");
window->show();
if (window) window->show();
} else lInfo() << log().arg("Stay minimized");
firstOpen = false;
}
@ -639,6 +644,7 @@ void App::initCppInterfaces() {
"SettingsCpp", 1, 0, "SettingsCpp",
[this](QQmlEngine *engine, QJSEngine *) -> QObject * { return mSettings.get(); });
qmlRegisterType<DashRectangle>(Constants::MainQmlUri, 1, 0, "DashRectangle");
qmlRegisterType<PhoneNumberProxy>(Constants::MainQmlUri, 1, 0, "PhoneNumberProxy");
qmlRegisterType<VariantObject>(Constants::MainQmlUri, 1, 0, "VariantObject");
qmlRegisterType<VariantList>(Constants::MainQmlUri, 1, 0, "VariantList");
@ -666,6 +672,8 @@ void App::initCppInterfaces() {
qmlRegisterType<ChatMessageGui>(Constants::MainQmlUri, 1, 0, "ChatMessageGui");
qmlRegisterType<EventLogList>(Constants::MainQmlUri, 1, 0, "EventLogList");
qmlRegisterType<EventLogProxy>(Constants::MainQmlUri, 1, 0, "EventLogProxy");
qmlRegisterType<ChatMessageContentProxy>(Constants::MainQmlUri, 1, 0, "ChatMessageContentProxy");
qmlRegisterType<ChatMessageContentGui>(Constants::MainQmlUri, 1, 0, "ChatMessageContentGui");
qmlRegisterUncreatableType<ConferenceCore>(Constants::MainQmlUri, 1, 0, "ConferenceCore",
QLatin1String("Uncreatable"));
qmlRegisterType<ConferenceGui>(Constants::MainQmlUri, 1, 0, "ConferenceGui");
@ -1284,3 +1292,11 @@ QString App::getSdkVersion() {
return tr('unknown');
#endif
}
float App::getScreenRatio() const {
return mScreenRatio;
}
void App::setScreenRatio(float ratio) {
mScreenRatio = ratio;
}

View file

@ -154,6 +154,9 @@ public:
QString getGitBranchName();
QString getSdkVersion();
float getScreenRatio() const;
Q_INVOKABLE void setScreenRatio(float ratio);
#ifdef Q_OS_LINUX
Q_INVOKABLE void exportDesktopFile();
@ -200,6 +203,7 @@ private:
DefaultTranslatorCore *mDefaultTranslatorCore = nullptr;
QTimer mDateUpdateTimer;
QDate mCurrentDate;
float mScreenRatio = 1;
DECLARE_ABSTRACT_OBJECT
};

View file

@ -29,6 +29,10 @@ list(APPEND _LINPHONEAPP_SOURCES
core/chat/message/EventLogGui.cpp
core/chat/message/EventLogList.cpp
core/chat/message/EventLogProxy.cpp
core/chat/message/content/ChatMessageContentCore.cpp
core/chat/message/content/ChatMessageContentGui.cpp
core/chat/message/content/ChatMessageContentList.cpp
core/chat/message/content/ChatMessageContentProxy.cpp
core/emoji/EmojiModel.cpp
core/fps-counter/FPSCounter.cpp
core/friend/FriendCore.cpp

View file

@ -68,10 +68,11 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership);
mChatMessageModel = Utils::makeQObject_ptr<ChatMessageModel>(chatmessage);
mChatMessageModel->setSelf(mChatMessageModel);
mText = mChatMessageModel->getText();
mText = ToolModel::getMessageFromContent(chatmessage->getContents());
mUtf8Text = mChatMessageModel->getUtf8Text();
mHasTextContent = mChatMessageModel->getHasTextContent();
mTimestamp = QDateTime::fromSecsSinceEpoch(chatmessage->getTime());
mIsOutgoing = chatmessage->isOutgoing();
mIsRemoteMessage = !chatmessage->isOutgoing();
mPeerAddress = Utils::coreStringToAppString(chatmessage->getPeerAddress()->asStringUriOnly());
mPeerName = ToolModel::getDisplayName(chatmessage->getPeerAddress()->clone());
@ -87,10 +88,8 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mMessageState = LinphoneEnums::fromLinphone(chatmessage->getState());
mMessageId = Utils::coreStringToAppString(chatmessage->getMessageId());
for (auto content : chatmessage->getContents()) {
if (content->isIcalendar()) {
auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content);
mConferenceInfo = ConferenceInfoCore::create(conferenceInfo);
}
auto contentCore = ChatMessageContentCore::create(content, mChatMessageModel);
mChatMessageContentList.push_back(contentCore);
}
auto reac = chatmessage->getOwnReaction();
mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString();
@ -201,6 +200,61 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
auto msgState = LinphoneEnums::fromLinphone(state);
mChatMessageModelConnection->invokeToCore([this, msgState] { setMessageState(msgState); });
});
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::fileTransferProgressIndication,
[this](const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &content,
size_t offset, size_t total) {
mChatMessageModelConnection->invokeToCore([this, content, offset, total] {
auto it =
std::find_if(mChatMessageContentList.begin(), mChatMessageContentList.end(),
[content](QSharedPointer<ChatMessageContentCore> item) {
return item->getContentModel()->getContent()->getName() == content->getName();
});
if (it != mChatMessageContentList.end()) {
auto contentCore = mChatMessageContentList.at(std::distance(mChatMessageContentList.begin(), it));
assert(contentCore);
contentCore->setFileOffset(offset);
}
});
});
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::fileTransferTerminated, [this](const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content) {
mChatMessageModelConnection->invokeToCore([this, content] {
auto it =
std::find_if(mChatMessageContentList.begin(), mChatMessageContentList.end(),
[content](QSharedPointer<ChatMessageContentCore> item) {
return item->getContentModel()->getContent()->getName() == content->getName();
});
if (it != mChatMessageContentList.end()) {
auto contentCore = mChatMessageContentList.at(std::distance(mChatMessageContentList.begin(), it));
assert(contentCore);
contentCore->setWasDownloaded(true);
}
});
});
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::fileTransferRecv,
[this](const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &content,
const std::shared_ptr<const linphone::Buffer> &buffer) { qDebug() << "transfer received"; });
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::fileTransferSend,
[this](const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &content,
size_t offset, size_t size) { qDebug() << "transfer send"; });
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::fileTransferSendChunk,
[this](const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &content,
size_t offset, size_t size,
const std::shared_ptr<linphone::Buffer> &buffer) { qDebug() << "transfer send chunk"; });
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::participantImdnStateChanged,
[this](const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ParticipantImdnState> &state) {});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageTimerStarted,
[this](const std::shared_ptr<linphone::ChatMessage> &message) {});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageDeleted,
[this](const std::shared_ptr<linphone::ChatMessage> &message) {});
}
QDateTime ChatMessageCore::getTimestamp() const {
@ -286,6 +340,10 @@ QList<QVariant> ChatMessageCore::getReactionsSingleton() const {
return mReactionsSingletonMap;
}
QList<QSharedPointer<ChatMessageContentCore>> ChatMessageCore::getChatMessageContentList() const {
return mChatMessageContentList;
}
void ChatMessageCore::setReactions(const QList<Reaction> &reactions) {
mustBeInMainThread(log().arg(Q_FUNC_INFO));
mReactions = reactions;
@ -370,6 +428,6 @@ std::shared_ptr<ChatMessageModel> ChatMessageCore::getModel() const {
return mChatMessageModel;
}
ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const {
return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr;
}
// ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const {
// return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr;
// }

View file

@ -22,6 +22,8 @@
#define CHATMESSAGECORE_H_
#include "EventLogCore.hpp"
#include "core/chat/message/content/ChatMessageContentCore.hpp"
#include "core/chat/message/content/ChatMessageContentProxy.hpp"
#include "core/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp"
#include "model/chat/message/ChatMessageModel.hpp"
@ -67,7 +69,6 @@ class ChatMessageCore : public QObject, public AbstractObject {
Q_PROPERTY(bool isRemoteMessage READ isRemoteMessage CONSTANT)
Q_PROPERTY(bool isFromChatGroup READ isFromChatGroup CONSTANT)
Q_PROPERTY(bool isRead READ isRead WRITE setIsRead NOTIFY isReadChanged)
Q_PROPERTY(ConferenceInfoGui *conferenceInfo READ getConferenceInfoGui CONSTANT)
Q_PROPERTY(QString ownReaction READ getOwnReaction WRITE setOwnReaction NOTIFY messageReactionChanged)
Q_PROPERTY(QList<Reaction> reactions READ getReactions WRITE setReactions NOTIFY messageReactionChanged)
Q_PROPERTY(QList<QVariant> reactionsSingleton READ getReactionsSingleton NOTIFY singletonReactionMapChanged)
@ -106,6 +107,7 @@ public:
void setOwnReaction(const QString &reaction);
QList<Reaction> getReactions() const;
QList<QVariant> getReactionsSingleton() const;
QList<QSharedPointer<ChatMessageContentCore>> getChatMessageContentList() const;
void removeOneReactionFromSingletonMap(const QString &body);
void resetReactionsSingleton();
void setReactions(const QList<Reaction> &reactions);
@ -116,7 +118,7 @@ public:
void setMessageState(LinphoneEnums::ChatMessageState state);
std::shared_ptr<ChatMessageModel> getModel() const;
ConferenceInfoGui *getConferenceInfoGui() const;
// ConferenceInfoGui *getConferenceInfoGui() const;
signals:
void timestampChanged(QDateTime timestamp);
@ -136,7 +138,8 @@ signals:
void lRemoveReaction();
private:
DECLARE_ABSTRACT_OBJECT QString mText;
DECLARE_ABSTRACT_OBJECT
QString mText;
QString mUtf8Text;
bool mHasTextContent;
QString mPeerAddress;
@ -158,8 +161,10 @@ private:
bool mIsCalendarInvite = false;
bool mIsVoiceRecording = false;
bool mIsOutgoing = false;
LinphoneEnums::ChatMessageState mMessageState;
QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr;
QList<QSharedPointer<ChatMessageContentCore>> mChatMessageContentList;
// QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr;
std::shared_ptr<ChatMessageModel> mChatMessageModel;
QSharedPointer<SafeConnection<ChatMessageCore, ChatMessageModel>> mChatMessageModelConnection;

View file

@ -0,0 +1,259 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ChatMessageContentCore.hpp"
#include "core/App.hpp"
#include "core/chat/ChatCore.hpp"
#include "model/tool/ToolModel.hpp"
#include "tool/providers/ThumbnailProvider.hpp"
DEFINE_ABSTRACT_OBJECT(ChatMessageContentCore)
QSharedPointer<ChatMessageContentCore>
ChatMessageContentCore::create(const std::shared_ptr<linphone::Content> &content,
std::shared_ptr<ChatMessageModel> chatMessageModel) {
auto sharedPointer = QSharedPointer<ChatMessageContentCore>(new ChatMessageContentCore(content, chatMessageModel),
&QObject::deleteLater);
sharedPointer->setSelf(sharedPointer);
sharedPointer->moveToThread(App::getInstance()->thread());
return sharedPointer;
}
ChatMessageContentCore::ChatMessageContentCore(const std::shared_ptr<linphone::Content> &content,
std::shared_ptr<ChatMessageModel> chatMessageModel) {
if (content) {
mName = Utils::coreStringToAppString(content->getName());
if (mName.isEmpty()) { // Try to find the name from file Path
QString fileName = Utils::coreStringToAppString(content->getFilePath());
if (!fileName.isEmpty()) {
mName = QFileInfo(fileName).baseName();
}
}
mFilePath = Utils::coreStringToAppString(content->getFilePath());
mIsFile = content->isFile();
mIsFileEncrypted = content->isFileEncrypted();
mIsFileTransfer = content->isFileTransfer();
mIsCalendar = content->isIcalendar();
if (content->isIcalendar()) {
auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content);
mConferenceInfo = ConferenceInfoCore::create(conferenceInfo);
}
mIsMultipart = content->isMultipart();
mIsText = content->isText();
mIsVoiceRecording = content->isVoiceRecording();
mIsVideo = Utils::isVideo(mFilePath);
mFileSize = (quint64)content->getFileSize();
mFileDuration = content->getFileDuration();
mFileOffset = 0;
mUtf8Text = Utils::coreStringToAppString(content->getUtf8Text());
mWasDownloaded = !mFilePath.isEmpty() && QFileInfo(mFilePath).isFile();
mThumbnail = mFilePath.isEmpty()
? QString()
: QStringLiteral("image://%1/%2").arg(ThumbnailProvider::ProviderId).arg(mFilePath);
mChatMessageContentModel = Utils::makeQObject_ptr<ChatMessageContentModel>(content, chatMessageModel);
}
}
ChatMessageContentCore ::~ChatMessageContentCore() {
}
void ChatMessageContentCore::setSelf(QSharedPointer<ChatMessageContentCore> me) {
mChatMessageContentModelConnection =
SafeConnection<ChatMessageContentCore, ChatMessageContentModel>::create(me, mChatMessageContentModel);
auto updateThumbnailType = [this] {
if (Utils::isVideo(mFilePath)) mIsVideo = true;
emit isVideoChanged();
};
mChatMessageContentModelConnection->makeConnectToCore(
&ChatMessageContentCore::lCreateThumbnail, [this](const bool &force = false) {
mChatMessageContentModelConnection->invokeToModel(
[this, force] { mChatMessageContentModel->createThumbnail(); });
});
mChatMessageContentModelConnection->makeConnectToModel(
&ChatMessageContentModel::thumbnailChanged, [this, updateThumbnailType](QString thumbnail) {
mChatMessageContentModelConnection->invokeToCore([this, thumbnail] { setThumbnail(thumbnail); });
});
mChatMessageContentModelConnection->makeConnectToCore(&ChatMessageContentCore::lDownloadFile, [this]() {
mChatMessageContentModelConnection->invokeToModel([this] { mChatMessageContentModel->downloadFile(mName); });
});
mChatMessageContentModelConnection->makeConnectToModel(
&ChatMessageContentModel::wasDownloadedChanged,
[this](const std::shared_ptr<linphone::Content> &content, bool downloaded) {
mChatMessageContentModelConnection->invokeToCore([this, downloaded] { setWasDownloaded(downloaded); });
});
mChatMessageContentModelConnection->makeConnectToModel(
&ChatMessageContentModel::filePathChanged,
[this](const std::shared_ptr<linphone::Content> &content, QString filePath) {
auto isFile = content->isFile();
auto isFileTransfer = content->isFileTransfer();
auto isFileEncrypted = content->isFileEncrypted();
mChatMessageContentModelConnection->invokeToCore([this, filePath, isFile, isFileTransfer, isFileEncrypted] {
setIsFile(isFile || QFileInfo(filePath).isFile());
setIsFileTransfer(isFileTransfer);
setIsFileEncrypted(isFileEncrypted);
setFilePath(filePath);
});
});
mChatMessageContentModelConnection->makeConnectToCore(&ChatMessageContentCore::lCancelDownloadFile, [this]() {
mChatMessageContentModelConnection->invokeToModel([this] { mChatMessageContentModel->cancelDownloadFile(); });
});
mChatMessageContentModelConnection->makeConnectToCore(
&ChatMessageContentCore::lOpenFile, [this](bool showDirectory = false) {
if (!QFileInfo(mFilePath).exists()) {
//: Error
Utils::showInformationPopup(tr("popup_error_title"),
//: Could not open file : unknown path %1
tr("popup_open_file_error_does_not_exist_message").arg(mFilePath), false);
} else {
mChatMessageContentModelConnection->invokeToModel([this, showDirectory] {
mChatMessageContentModel->openFile(mName, mWasDownloaded, showDirectory);
});
}
});
mChatMessageContentModelConnection->makeConnectToModel(
&ChatMessageContentModel::messageStateChanged, [this](linphone::ChatMessage::State state) {
mChatMessageContentModelConnection->invokeToCore(
[this, msgState = LinphoneEnums::fromLinphone(state)] { emit msgStateChanged(msgState); });
});
}
bool ChatMessageContentCore::isFile() const {
return mIsFile;
}
void ChatMessageContentCore::setIsFile(bool isFile) {
if (mIsFile != isFile) {
mIsFile = isFile;
emit isFileChanged();
}
}
bool ChatMessageContentCore::isVideo() const {
return mIsVideo;
}
bool ChatMessageContentCore::isFileEncrypted() const {
return mIsFileEncrypted;
}
void ChatMessageContentCore::setIsFileEncrypted(bool isFileEncrypted) {
if (mIsFileEncrypted != isFileEncrypted) {
mIsFileEncrypted = isFileEncrypted;
emit isFileEncryptedChanged();
}
}
bool ChatMessageContentCore::isFileTransfer() const {
return mIsFileTransfer;
}
void ChatMessageContentCore::setIsFileTransfer(bool isFileTransfer) {
if (mIsFileTransfer != isFileTransfer) {
mIsFileTransfer = isFileTransfer;
emit isFileTransferChanged();
}
}
bool ChatMessageContentCore::isCalendar() const {
return mIsCalendar;
}
bool ChatMessageContentCore::isMultipart() const {
return mIsMultipart;
}
bool ChatMessageContentCore::isText() const {
return mIsText;
}
bool ChatMessageContentCore::isVoiceRecording() const {
return mIsVoiceRecording;
}
QString ChatMessageContentCore::getFilePath() const {
return mFilePath;
}
void ChatMessageContentCore::setFilePath(QString path) {
if (mFilePath != path) {
mFilePath = path;
emit filePathChanged();
}
}
QString ChatMessageContentCore::getUtf8Text() const {
return mUtf8Text;
}
QString ChatMessageContentCore::getName() const {
return mName;
}
quint64 ChatMessageContentCore::getFileSize() const {
return mFileSize;
}
quint64 ChatMessageContentCore::getFileOffset() const {
return mFileOffset;
}
void ChatMessageContentCore::setFileOffset(quint64 fileOffset) {
if (mFileOffset != fileOffset) {
mFileOffset = fileOffset;
emit fileOffsetChanged();
}
}
int ChatMessageContentCore::getFileDuration() const {
return mFileDuration;
}
ConferenceInfoGui *ChatMessageContentCore::getConferenceInfoGui() const {
return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr;
}
bool ChatMessageContentCore::wasDownloaded() const {
return mWasDownloaded;
}
QString ChatMessageContentCore::getThumbnail() const {
return mThumbnail;
}
void ChatMessageContentCore::setThumbnail(const QString &data) {
if (mThumbnail != data) {
mThumbnail = data;
emit thumbnailChanged();
}
}
void ChatMessageContentCore::setWasDownloaded(bool wasDownloaded) {
if (mWasDownloaded != wasDownloaded) {
mWasDownloaded = wasDownloaded;
emit wasDownloadedChanged(wasDownloaded);
}
}
const std::shared_ptr<ChatMessageContentModel> &ChatMessageContentCore::getContentModel() const {
return mChatMessageContentModel;
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_MESSAGE_CONTENT_CORE_H_
#define CHAT_MESSAGE_CONTENT_CORE_H_
#include "core/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp"
#include "model/chat/message/content/ChatMessageContentModel.hpp"
#include "tool/AbstractObject.hpp"
#include "tool/thread/SafeConnection.hpp"
#include <QObject>
#include <QSharedPointer>
#include <linphone++/linphone.hh>
class ChatMessageContentCore : public QObject, public AbstractObject {
Q_OBJECT
Q_PROPERTY(QString name READ getName CONSTANT)
Q_PROPERTY(quint64 fileOffset READ getFileOffset WRITE setFileOffset NOTIFY fileOffsetChanged)
Q_PROPERTY(QString thumbnail READ getThumbnail WRITE setThumbnail NOTIFY thumbnailChanged)
Q_PROPERTY(bool wasDownloaded READ wasDownloaded WRITE setWasDownloaded NOTIFY wasDownloadedChanged)
Q_PROPERTY(QString filePath READ getFilePath WRITE setFilePath NOTIFY filePathChanged)
Q_PROPERTY(QString utf8Text READ getUtf8Text CONSTANT)
Q_PROPERTY(bool isFile READ isFile WRITE setIsFile NOTIFY isFileChanged)
Q_PROPERTY(bool isFileEncrypted READ isFileEncrypted WRITE setIsFileEncrypted NOTIFY isFileEncryptedChanged)
Q_PROPERTY(bool isFileTransfer READ isFileTransfer WRITE setIsFileTransfer NOTIFY isFileTransferChanged)
Q_PROPERTY(bool isCalendar READ isCalendar CONSTANT)
Q_PROPERTY(ConferenceInfoGui *conferenceInfo READ getConferenceInfoGui CONSTANT)
Q_PROPERTY(bool isMultipart READ isMultipart CONSTANT)
Q_PROPERTY(bool isText READ isText CONSTANT)
Q_PROPERTY(bool isVideo READ isVideo NOTIFY isVideoChanged)
Q_PROPERTY(bool isVoiceRecording READ isVoiceRecording CONSTANT)
Q_PROPERTY(int fileDuration READ getFileDuration CONSTANT)
Q_PROPERTY(quint64 fileSize READ getFileSize CONSTANT)
public:
static QSharedPointer<ChatMessageContentCore> create(const std::shared_ptr<linphone::Content> &content,
std::shared_ptr<ChatMessageModel> chatMessageModel);
ChatMessageContentCore(const std::shared_ptr<linphone::Content> &content,
std::shared_ptr<ChatMessageModel> chatMessageModel);
~ChatMessageContentCore();
void setSelf(QSharedPointer<ChatMessageContentCore> me);
bool isFile() const;
void setIsFile(bool isFile);
bool isFileEncrypted() const;
void setIsFileEncrypted(bool isFileEncrypted);
bool isFileTransfer() const;
void setIsFileTransfer(bool isFileTransfer);
bool isVideo() const;
bool isCalendar() const;
bool isMultipart() const;
bool isText() const;
bool isVoiceRecording() const;
QString getUtf8Text() const;
QString getName() const;
quint64 getFileSize() const;
quint64 getFileOffset() const;
void setFileOffset(quint64 fileOffset);
QString getFilePath() const;
void setFilePath(QString path);
int getFileDuration() const;
ConferenceInfoGui *getConferenceInfoGui() const;
void setThumbnail(const QString &data);
QString getThumbnail() const;
bool wasDownloaded() const;
void setWasDownloaded(bool downloaded);
const std::shared_ptr<ChatMessageContentModel> &getContentModel() const;
signals:
void msgStateChanged(LinphoneEnums::ChatMessageState state);
void thumbnailChanged();
void fileOffsetChanged();
void filePathChanged();
void isFileChanged();
void isFileTransferChanged();
void isFileEncryptedChanged();
void wasDownloadedChanged(bool downloaded);
void isVideoChanged();
void lCreateThumbnail(const bool &force = false);
void lRemoveDownloadedFile();
void lDownloadFile();
void lCancelDownloadFile();
void lOpenFile(bool showDirectory = false);
bool lSaveAs(const QString &path);
private:
DECLARE_ABSTRACT_OBJECT
bool mIsFile;
bool mIsVideo;
bool mIsFileEncrypted;
bool mIsFileTransfer;
bool mIsCalendar;
bool mIsMultipart;
bool mIsText;
bool mIsVoiceRecording;
int mFileDuration;
QString mThumbnail;
QString mUtf8Text;
QString mFilePath;
QString mName;
quint64 mFileSize;
quint64 mFileOffset;
bool mWasDownloaded;
QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr;
std::shared_ptr<ChatMessageContentModel> mChatMessageContentModel;
QSharedPointer<SafeConnection<ChatMessageContentCore, ChatMessageContentModel>> mChatMessageContentModelConnection;
};
#endif // CHAT_MESSAGE_CONTENT_CORE_H_

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ChatMessageContentGui.hpp"
#include "core/App.hpp"
DEFINE_ABSTRACT_OBJECT(ChatMessageContentGui)
ChatMessageContentGui::ChatMessageContentGui(QSharedPointer<ChatMessageContentCore> core) {
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);
mCore = core;
if (isInLinphoneThread()) moveToThread(App::getInstance()->thread());
}
ChatMessageContentGui::~ChatMessageContentGui() {
mustBeInMainThread("~" + getClassName());
}
ChatMessageContentCore *ChatMessageContentGui::getCore() const {
return mCore.get();
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_MESSAGE_CONTENT_GUI_H_
#define CHAT_MESSAGE_CONTENT_GUI_H_
#include "core/chat/message/content/ChatMessageContentCore.hpp"
#include <QObject>
#include <QSharedPointer>
class ChatMessageContentGui : public QObject, public AbstractObject {
Q_OBJECT
Q_PROPERTY(ChatMessageContentCore *core READ getCore CONSTANT)
public:
ChatMessageContentGui(QSharedPointer<ChatMessageContentCore> core);
~ChatMessageContentGui();
ChatMessageContentCore *getCore() const;
QSharedPointer<ChatMessageContentCore> mCore;
DECLARE_ABSTRACT_OBJECT
};
#endif

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ChatMessageContentList.hpp"
#include "core/App.hpp"
#include "core/chat/ChatCore.hpp"
#include "core/chat/message/content/ChatMessageContentGui.hpp"
#include <QMimeDatabase>
#include <QSharedPointer>
#include <linphone++/linphone.hh>
// =============================================================================
DEFINE_ABSTRACT_OBJECT(ChatMessageContentList)
QSharedPointer<ChatMessageContentList> ChatMessageContentList::create() {
auto model = QSharedPointer<ChatMessageContentList>(new ChatMessageContentList(), &QObject::deleteLater);
model->moveToThread(App::getInstance()->thread());
model->setSelf(model);
return model;
}
ChatMessageContentList::ChatMessageContentList(QObject *parent) : ListProxy(parent) {
mustBeInMainThread(getClassName());
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership);
}
ChatMessageContentList::~ChatMessageContentList() {
mustBeInMainThread("~" + getClassName());
mModelConnection = nullptr;
}
ChatMessageGui *ChatMessageContentList::getChatMessage() const {
if (mChatMessageCore) return new ChatMessageGui(mChatMessageCore);
else return nullptr;
}
QSharedPointer<ChatMessageCore> ChatMessageContentList::getChatMessageCore() const {
return mChatMessageCore;
}
void ChatMessageContentList::setChatMessageCore(QSharedPointer<ChatMessageCore> core) {
if (mChatMessageCore != core) {
// if (mChatMessageCore) disconnect(mChatMessageCore.get(), &ChatCore::, this, nullptr);
mChatMessageCore = core;
// if (mChatMessageCore)
// connect(mChatMessageCore.get(), &ChatCore::messageListChanged, this, &ChatMessageContentList::lUpdate);
emit chatMessageChanged();
lUpdate();
}
}
void ChatMessageContentList::setChatMessageGui(ChatMessageGui *chat) {
auto chatCore = chat ? chat->mCore : nullptr;
setChatMessageCore(chatCore);
}
int ChatMessageContentList::findFirstUnreadIndex() {
auto chatList = getSharedList<ChatMessageCore>();
auto it = std::find_if(chatList.begin(), chatList.end(),
[](const QSharedPointer<ChatMessageCore> item) { return !item->isRead(); });
return it == chatList.end() ? -1 : std::distance(chatList.begin(), it);
}
void ChatMessageContentList::setSelf(QSharedPointer<ChatMessageContentList> me) {
mModelConnection = SafeConnection<ChatMessageContentList, CoreModel>::create(me, CoreModel::getInstance());
mModelConnection->makeConnectToCore(&ChatMessageContentList::lUpdate, [this]() {
for (auto &content : getSharedList<ChatMessageContentCore>()) {
if (content) disconnect(content.get());
}
if (!mChatMessageCore) return;
auto contents = mChatMessageCore->getChatMessageContentList();
for (auto &content : contents) {
connect(content.get(), &ChatMessageContentCore::wasDownloadedChanged, this,
[this, content](bool wasDownloaded) {
if (wasDownloaded) {
content->lCreateThumbnail();
}
});
connect(content.get(), &ChatMessageContentCore::thumbnailChanged, this, [this] { emit lUpdate(); });
}
resetData<ChatMessageContentCore>(contents);
});
mModelConnection->makeConnectToCore(&ChatMessageContentList::lAddFile, [this](const QString &path) {
QFile file(path);
// #ifdef _WIN32
// // A bug from FileDialog suppose that the file is local and overwrite the uri by removing "\\".
// if (!file.exists()) {
// path.prepend("\\\\");
// file.setFileName(path);
// }
// #endif
if (!file.exists()) return;
if (rowCount() >= 12) {
//: Error
Utils::showInformationPopup(tr("popup_error_title"),
//: You can add 12 files maximum
tr("popup_error_max_files_count_message"), false);
return;
}
qint64 fileSize = file.size();
if (fileSize > Constants::FileSizeLimit) {
qWarning() << QStringLiteral("Unable to send file. (Size limit=%1)").arg(Constants::FileSizeLimit);
return;
}
auto name = QFileInfo(file).fileName().toStdString();
mModelConnection->invokeToModel([this, path, fileSize, name] {
std::shared_ptr<linphone::Content> content = CoreModel::getInstance()->getCore()->createContent();
{
QStringList mimeType = QMimeDatabase().mimeTypeForFile(path).name().split('/');
if (mimeType.length() != 2) {
qWarning() << QStringLiteral("Unable to get supported mime type for: `%1`.").arg(path);
return;
}
content->setType(Utils::appStringToCoreString(mimeType[0]));
content->setSubtype(Utils::appStringToCoreString(mimeType[1]));
}
content->setSize(size_t(fileSize));
content->setName(name);
content->setFilePath(Utils::appStringToCoreString(path));
auto contentCore = ChatMessageContentCore::create(content, nullptr);
mModelConnection->invokeToCore([this, contentCore] {
connect(contentCore.get(), &ChatMessageContentCore::isFileChanged, this, [this, contentCore] {
int i = -1;
get(contentCore.get(), &i);
emit dataChanged(index(i), index(i));
});
add(contentCore);
contentCore->lCreateThumbnail(
true); // Was not created because linphone::Content is not considered as a file (yet)
});
});
});
emit lUpdate();
}
QVariant ChatMessageContentList::data(const QModelIndex &index, int role) const {
int row = index.row();
if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant();
if (role == Qt::DisplayRole)
return QVariant::fromValue(new ChatMessageContentGui(mList[row].objectCast<ChatMessageContentCore>()));
return QVariant();
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_MESSAGE_CONTENT_LIST_H_
#define CHAT_MESSAGE_CONTENT_LIST_H_
#include "core/proxy/ListProxy.hpp"
#include "tool/AbstractObject.hpp"
#include "tool/thread/SafeConnection.hpp"
#include <QLocale>
class ChatMessageGui;
class ChatMessageCore;
// =============================================================================
class ChatMessageContentList : public ListProxy, public AbstractObject {
Q_OBJECT
public:
static QSharedPointer<ChatMessageContentList> create();
ChatMessageContentList(QObject *parent = Q_NULLPTR);
~ChatMessageContentList();
QSharedPointer<ChatMessageCore> getChatMessageCore() const;
ChatMessageGui *getChatMessage() const;
void setChatMessageCore(QSharedPointer<ChatMessageCore> core);
void setChatMessageGui(ChatMessageGui *chat);
int findFirstUnreadIndex();
void setSelf(QSharedPointer<ChatMessageContentList> me);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void lAddFile(QString path);
void isFileChanged();
void lUpdate();
void chatMessageChanged();
private:
QSharedPointer<ChatMessageCore> mChatMessageCore;
QSharedPointer<SafeConnection<ChatMessageContentList, CoreModel>> mModelConnection;
DECLARE_ABSTRACT_OBJECT
};
#endif

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ChatMessageContentProxy.hpp"
#include "ChatMessageContentGui.hpp"
#include "core/App.hpp"
#include "core/chat/message/ChatMessageGui.hpp"
DEFINE_ABSTRACT_OBJECT(ChatMessageContentProxy)
ChatMessageContentProxy::ChatMessageContentProxy(QObject *parent) : LimitProxy(parent) {
mList = ChatMessageContentList::create();
setSourceModel(mList.get());
}
ChatMessageContentProxy::~ChatMessageContentProxy() {
}
void ChatMessageContentProxy::setSourceModel(QAbstractItemModel *model) {
auto oldChatMessageContentList = getListModel<ChatMessageContentList>();
if (oldChatMessageContentList) {
// disconnect(oldChatMessageContentList);
}
auto newChatMessageContentList = dynamic_cast<ChatMessageContentList *>(model);
if (newChatMessageContentList) {
// connect(newChatMessageContentList, &ChatMessageContentList::chatChanged, this,
// &ChatMessageContentProxy::chatChanged);
}
setSourceModels(new SortFilterList(model));
sort(0);
}
ChatMessageGui *ChatMessageContentProxy::getChatMessageGui() {
auto model = getListModel<ChatMessageContentList>();
if (!mChatMessageGui && model) mChatMessageGui = model->getChatMessage();
return mChatMessageGui;
}
void ChatMessageContentProxy::setChatMessageGui(ChatMessageGui *chat) {
getListModel<ChatMessageContentList>()->setChatMessageGui(chat);
}
// ChatMessageGui *ChatMessageContentProxy::getChatMessageAtIndex(int i) {
// auto model = getListModel<ChatMessageContentList>();
// auto sourceIndex = mapToSource(index(i, 0)).row();
// if (model) {
// auto chat = model->getAt<ChatMessageCore>(sourceIndex);
// if (chat) return new ChatMessageGui(chat);
// else return nullptr;
// }
// return nullptr;
// }
void ChatMessageContentProxy::addFile(const QString &path) {
auto model = getListModel<ChatMessageContentList>();
if (model) emit model->lAddFile(path.toUtf8());
}
void ChatMessageContentProxy::removeContent(ChatMessageContentGui *contentGui) {
auto model = getListModel<ChatMessageContentList>();
if (model && contentGui) model->remove(contentGui->mCore);
}
void ChatMessageContentProxy::clear() {
auto model = getListModel<ChatMessageContentList>();
if (model) model->clearData();
}
bool ChatMessageContentProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const {
auto contentCore = getItemAtSource<ChatMessageContentList, ChatMessageContentCore>(sourceRow);
if (contentCore) {
if (mFilterType == (int)FilterContentType::Unknown) return false;
else if (mFilterType == (int)FilterContentType::File) {
return contentCore->isFile() || contentCore->isFileTransfer();
} else if (mFilterType == (int)FilterContentType::Text) return contentCore->isText();
else if (mFilterType == (int)FilterContentType::Voice) return contentCore->isVoiceRecording();
else if (mFilterType == (int)FilterContentType::Conference) return contentCore->isCalendar();
else if (mFilterType == (int)FilterContentType::All) return true;
}
return false;
}
bool ChatMessageContentProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft,
const QModelIndex &sourceRight) const {
auto l = getItemAtSource<ChatMessageContentList, ChatMessageCore>(sourceLeft.row());
auto r = getItemAtSource<ChatMessageContentList, ChatMessageCore>(sourceRight.row());
if (l && r) return l->getTimestamp() <= r->getTimestamp();
else return true;
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_MESSAGE_CONENT_PROXY_H_
#define CHAT_MESSAGE_CONENT_PROXY_H_
#include "ChatMessageContentList.hpp"
#include "core/proxy/LimitProxy.hpp"
#include "tool/AbstractObject.hpp"
// =============================================================================
class ChatMessageGui;
class ChatMessageContentGui;
class ChatMessageContentProxy : public LimitProxy, public AbstractObject {
Q_OBJECT
Q_PROPERTY(ChatMessageGui *chatMessageGui READ getChatMessageGui WRITE setChatMessageGui NOTIFY chatChanged)
public:
enum class FilterContentType { Unknown = 0, File = 1, Text = 2, Voice = 3, Conference = 4, All = 5 };
Q_ENUM(FilterContentType)
DECLARE_SORTFILTER_CLASS(ChatMessageContentProxy *mHideListProxy = nullptr;)
ChatMessageContentProxy(QObject *parent = Q_NULLPTR);
~ChatMessageContentProxy();
ChatMessageGui *getChatMessageGui();
void setChatMessageGui(ChatMessageGui *chat);
void setSourceModel(QAbstractItemModel *sourceModel) override;
Q_INVOKABLE void addFile(const QString &path);
Q_INVOKABLE void removeContent(ChatMessageContentGui *contentGui);
Q_INVOKABLE void clear();
signals:
void chatChanged();
void filterChanged();
void messageInserted(int index, ChatMessageGui *message);
protected:
QSharedPointer<ChatMessageContentList> mList;
ChatMessageGui *mChatMessageGui = nullptr;
DECLARE_ABSTRACT_OBJECT
};
#endif

View file

@ -269,7 +269,8 @@ QString Paths::getFriendsListFilePath() {
}
QString Paths::getDownloadDirPath() {
return getWritableDirPath(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + QDir::separator());
return getWritableDirPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
Constants::PathFiles);
}
QString Paths::getLimeDatabasePath() {

View file

@ -123,7 +123,9 @@ public:
}
virtual void clearData() override {
beginResetModel();
mList.clear();
endResetModel();
}
virtual void resetData(QList<T> newData = QList<T>()) {

View file

@ -95,6 +95,7 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) {
// Chat
mEmojiFont = settingsModel->getEmojiFont();
mTextMessageFont = settingsModel->getTextMessageFont();
// Ui
INIT_CORE_MEMBER(DisableChatFeature, settingsModel)
@ -539,6 +540,26 @@ void SettingsCore::setVfsEnabled(bool enabled) {
}
}
bool SettingsCore::getVfsEncrypted() {
mAppSettings.beginGroup("keychain");
return mAppSettings.value("enabled", false).toBool();
}
void SettingsCore::setVfsEncrypted(bool encrypted, const bool deleteUserData) {
#ifdef ENABLE_QT_KEYCHAIN
if (getVfsEncrypted() != encrypted) {
if (encrypted) {
mVfsUtils.newEncryptionKeyAsync();
shared_ptr<linphone::Factory> factory = linphone::Factory::get();
factory->setDownloadDir(Utils::appStringToCoreString(getDownloadFolder()));
} else { // Remove key, stop core, delete data and initiate reboot
mVfsUtils.needToDeleteUserData(deleteUserData);
mVfsUtils.deleteKey(mVfsUtils.getApplicationVfsEncryptionKey());
}
}
#endif
}
void SettingsCore::setVideoEnabled(bool enabled) {
if (mVideoEnabled != enabled) {
mVideoEnabled = enabled;

View file

@ -112,6 +112,9 @@ public:
}
void setVfsEnabled(bool enabled);
bool getVfsEncrypted();
void setVfsEncrypted(bool encrypted, const bool deleteUserData);
// Call. --------------------------------------------------------------------
bool getVideoEnabled() {
@ -234,6 +237,7 @@ public:
DECLARE_CORE_GETSET_MEMBER(bool, disableCallForward, DisableCallForward)
DECLARE_CORE_GETSET_MEMBER(QString, callForwardToAddress, CallForwardToAddress)
DECLARE_CORE_GET_CONSTANT(QFont, emojiFont, EmojiFont)
DECLARE_CORE_GET_CONSTANT(QFont, textMessageFont, TextMessageFont)
signals:

View file

@ -43,6 +43,10 @@ void VariantList::setModel(QList<QVariant> list) {
emit modelChanged();
}
QList<QVariant> VariantList::getModel() const {
return mList;
}
void VariantList::replace(int index, QVariant newValue) {
mList.replace(index, newValue);
}

View file

@ -38,6 +38,7 @@ public:
~VariantList();
void setModel(QList<QVariant> list);
QList<QVariant> getModel() const;
void replace(int index, QVariant newValue);

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"></path></svg>

After

Width:  |  Height:  |  Size: 341 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.25 3.75H3.75C3.35218 3.75 2.97064 3.90804 2.68934 4.18934C2.40804 4.47064 2.25 4.85218 2.25 5.25V18.75C2.25 19.1478 2.40804 19.5294 2.68934 19.8107C2.97064 20.092 3.35218 20.25 3.75 20.25H20.25C20.6478 20.25 21.0294 20.092 21.3107 19.8107C21.592 19.5294 21.75 19.1478 21.75 18.75V5.25C21.75 4.85218 21.592 4.47064 21.3107 4.18934C21.0294 3.90804 20.6478 3.75 20.25 3.75ZM20.25 5.25V14.8828L17.8059 12.4397C17.6666 12.3004 17.5013 12.1898 17.3193 12.1144C17.1372 12.039 16.9422 12.0002 16.7452 12.0002C16.5481 12.0002 16.3531 12.039 16.1711 12.1144C15.989 12.1898 15.8237 12.3004 15.6844 12.4397L13.8094 14.3147L9.68438 10.1897C9.4031 9.9086 9.02172 9.7507 8.62406 9.7507C8.22641 9.7507 7.84503 9.9086 7.56375 10.1897L3.75 14.0034V5.25H20.25ZM3.75 16.125L8.625 11.25L16.125 18.75H3.75V16.125ZM20.25 18.75H18.2466L14.8716 15.375L16.7466 13.5L20.25 17.0044V18.75ZM13.5 9.375C13.5 9.1525 13.566 8.93499 13.6896 8.74998C13.8132 8.56498 13.9889 8.42078 14.1945 8.33564C14.4 8.25049 14.6262 8.22821 14.8445 8.27162C15.0627 8.31502 15.2632 8.42217 15.4205 8.5795C15.5778 8.73684 15.685 8.93729 15.7284 9.15552C15.7718 9.37375 15.7495 9.59995 15.6644 9.80552C15.5792 10.0111 15.435 10.1868 15.25 10.3104C15.065 10.434 14.8475 10.5 14.625 10.5C14.3266 10.5 14.0405 10.3815 13.8295 10.1705C13.6185 9.95952 13.5 9.67337 13.5 9.375Z" fill="#343330"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,152a8,8,0,0,1-8,8H192v16h16a8,8,0,0,1,0,16H192v16a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8h32A8,8,0,0,1,224,152ZM92,172a28,28,0,0,1-28,28H56v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172Zm-16,0a12,12,0,0,0-12-12H56v24h8A12,12,0,0,0,76,172Zm88,8a36,36,0,0,1-36,36H112a8,8,0,0,1-8-8V152a8,8,0,0,1,8-8h16A36,36,0,0,1,164,180Zm-16,0a20,20,0,0,0-20-20h-8v40h8A20,20,0,0,0,148,180ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z"></path></svg>

After

Width:  |  Height:  |  Size: 669 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-40-64a8,8,0,0,1-8,8H136v16a8,8,0,0,1-16,0V160H104a8,8,0,0,1,0-16h16V128a8,8,0,0,1,16,0v16h16A8,8,0,0,1,160,152Z"></path></svg>

After

Width:  |  Height:  |  Size: 433 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"></path></svg>

After

Width:  |  Height:  |  Size: 320 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.955C22.0006 12.2019 21.9373 12.4448 21.8162 12.66C21.6951 12.8753 21.5204 13.0555 21.3091 13.1832L8.21091 21.1959C7.99008 21.3311 7.73715 21.4049 7.47825 21.4097C7.21935 21.4145 6.96387 21.3501 6.73818 21.2232C6.51465 21.0982 6.32843 20.9159 6.19869 20.6951C6.06896 20.4743 6.00037 20.2229 6 19.9668V3.94318C6.00037 3.68708 6.06896 3.43569 6.19869 3.21488C6.32843 2.99407 6.51465 2.8118 6.73818 2.68682C6.96387 2.55986 7.21935 2.49545 7.47825 2.50025C7.73715 2.50504 7.99008 2.57887 8.21091 2.71409L21.3091 10.7268C21.5204 10.8545 21.6951 11.0347 21.8162 11.25C21.9373 11.4652 22.0006 11.7081 22 11.955Z" fill="#343330"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ list(APPEND _LINPHONEAPP_SOURCES
model/chat/ChatModel.cpp
model/chat/message/ChatMessageModel.cpp
model/chat/message/content/ChatMessageContentModel.cpp
model/conference/ConferenceInfoModel.cpp
model/conference/ConferenceModel.cpp
@ -42,6 +43,7 @@ list(APPEND _LINPHONEAPP_SOURCES
model/tool/ToolModel.cpp
model/tool/VfsUtils.cpp
model/videoSource/VideoSourceDescriptorModel.cpp

View file

@ -0,0 +1,159 @@
/*
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ChatMessageContentModel.hpp"
#include <QDesktopServices>
#include <QMessageBox>
#include <QPainter>
#include <QQmlApplicationEngine>
#include "core/App.hpp"
#include "tool/providers/ExternalImageProvider.hpp"
#include "tool/providers/ThumbnailProvider.hpp"
#include "model/chat/message/ChatMessageModel.hpp"
#include "model/conference/ConferenceInfoModel.hpp"
#include "model/core/CoreModel.hpp"
#include "model/setting/SettingsModel.hpp"
#include "tool/Utils.hpp"
// =============================================================================
DEFINE_ABSTRACT_OBJECT(ChatMessageContentModel)
ChatMessageContentModel::ChatMessageContentModel(std::shared_ptr<linphone::Content> content,
std::shared_ptr<ChatMessageModel> chatMessageModel) {
mChatMessageModel = chatMessageModel;
mContent = content;
assert(content);
if (content->isFile() || content->isFileEncrypted() || content->isFileTransfer()) {
createThumbnail();
}
if (mChatMessageModel)
connect(mChatMessageModel.get(), &ChatMessageModel::msgStateChanged, this,
[this] { emit messageStateChanged(mChatMessageModel->getState()); });
}
ChatMessageContentModel::~ChatMessageContentModel() {
mustBeInLinphoneThread("~" + getClassName());
}
// Create a thumbnail from the first content that have a file
void ChatMessageContentModel::createThumbnail() {
auto path = Utils::coreStringToAppString(mContent->getFilePath());
if (!path.isEmpty()) {
auto isVideo = Utils::isVideo(path);
emit wasDownloadedChanged(mContent, QFileInfo(path).isFile());
emit thumbnailChanged(QStringLiteral("image://%1/%2").arg(ThumbnailProvider::ProviderId).arg(path));
emit filePathChanged(mContent, path);
}
}
void ChatMessageContentModel::removeDownloadedFile(QString filePath) {
if (!filePath.isEmpty()) {
QFile(filePath).remove();
}
}
void ChatMessageContentModel::downloadFile(const QString &name) {
if (!mChatMessageModel) return;
switch (mChatMessageModel->getState()) {
case linphone::ChatMessage::State::Delivered:
case linphone::ChatMessage::State::DeliveredToUser:
case linphone::ChatMessage::State::Displayed:
case linphone::ChatMessage::State::FileTransferDone:
break;
case linphone::ChatMessage::State::FileTransferInProgress:
return;
default:
qWarning() << QStringLiteral("Wrong message state when requesting downloading, state=.")
<< LinphoneEnums::fromLinphone(mChatMessageModel->getState());
}
bool soFarSoGood;
const QString safeFilePath = Utils::getSafeFilePath(
QStringLiteral("%1%2").arg(App::getInstance()->getSettings()->getDownloadFolder()).arg(name), &soFarSoGood);
if (!soFarSoGood) {
qWarning() << QStringLiteral("Unable to create safe file path for: %1.").arg(name);
return;
}
mContent->setFilePath(Utils::appStringToCoreString(safeFilePath));
if (!mContent->isFileTransfer()) {
Utils::showInformationPopup(
//: Error
tr("popup_error_title"),
//: This file was already downloaded and is no more on the server. Your peer have to resend it if you want
//: to get it
tr("popup_download_error_message"), false);
} else {
if (!mChatMessageModel->getMonitor()->downloadContent(mContent))
qWarning() << QStringLiteral("Unable to download file of entry %1.").arg(name);
}
}
void ChatMessageContentModel::cancelDownloadFile() {
if (mChatMessageModel && mChatMessageModel->getMonitor()) {
mChatMessageModel->getMonitor()->cancelFileTransfer();
}
}
void ChatMessageContentModel::openFile(const QString &name, bool wasDownloaded, bool showDirectory) {
if (mChatMessageModel &&
((!wasDownloaded && !mChatMessageModel->getMonitor()->isOutgoing()) || mContent->getFilePath() == "")) {
downloadFile(name);
} else {
QFileInfo info(Utils::coreStringToAppString(mContent->getFilePath()));
showDirectory = showDirectory || !info.exists();
if (!QDesktopServices::openUrl(QUrl(
QStringLiteral("file:///%1").arg(showDirectory ? info.absolutePath() : info.absoluteFilePath()))) &&
!showDirectory) {
QDesktopServices::openUrl(QUrl(QStringLiteral("file:///%1").arg(info.absolutePath())));
}
}
}
bool ChatMessageContentModel::saveAs(const QString &path) {
QString cachePath = Utils::coreStringToAppString(mContent->exportPlainFile());
bool toDelete = true;
bool result = false;
if (cachePath.isEmpty()) {
cachePath = Utils::coreStringToAppString(mContent->getFilePath());
toDelete = false;
}
if (!cachePath.isEmpty()) {
QString decodedPath = QUrl::fromPercentEncoding(path.toUtf8());
QFile file(cachePath);
QFile newFile(decodedPath);
if (newFile.exists()) newFile.remove();
result = file.copy(decodedPath);
if (toDelete) file.remove();
if (result) QDesktopServices::openUrl(QUrl(QStringLiteral("file:///%1").arg(decodedPath)));
}
emit fileSavedChanged(result);
return result;
}
const std::shared_ptr<linphone::Content> &ChatMessageContentModel::getContent() const {
return mContent;
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_MESSAGE_CONTENT_MODEL_H_
#define CHAT_MESSAGE_CONTENT_MODEL_H_
#include "tool/AbstractObject.hpp"
#include <linphone++/linphone.hh>
// =============================================================================
#include <QDateTime>
#include <QObject>
#include <QSharedPointer>
#include <QString>
class ChatMessageModel;
class ConferenceInfoModel;
class ChatMessageContentModel : public QObject, public AbstractObject {
Q_OBJECT
public:
ChatMessageContentModel(std::shared_ptr<linphone::Content> content,
std::shared_ptr<ChatMessageModel> chatMessageModel);
~ChatMessageContentModel();
QString getThumbnail() const;
void setThumbnail(const QString &data);
void setWasDownloaded(bool wasDownloaded);
void createThumbnail();
void removeDownloadedFile(QString filePath);
void downloadFile(const QString &name);
void cancelDownloadFile();
void openFile(const QString &name, bool wasDownloaded, bool showDirectory = false);
bool saveAs(const QString &path);
const std::shared_ptr<linphone::Content> &getContent() const;
signals:
void thumbnailChanged(QString thumbnail);
void fileOffsetChanged();
void wasDownloadedChanged(const std::shared_ptr<linphone::Content> &content, bool downloaded);
void fileSavedChanged(bool success);
void filePathChanged(const std::shared_ptr<linphone::Content> &content, QString filePath);
void messageStateChanged(linphone::ChatMessage::State state);
private:
DECLARE_ABSTRACT_OBJECT
std::shared_ptr<linphone::Content> mContent;
std::shared_ptr<ChatMessageModel> mChatMessageModel;
QSharedPointer<ConferenceInfoModel> mConferenceInfoModel;
};
#endif

View file

@ -87,30 +87,30 @@ void MagicSearchModel::onSearchResultsReceived(const std::shared_ptr<linphone::M
auto f = result->getFriend();
auto friendsManager = FriendsManager::getInstance();
if (f) {
qDebug() << "friend exists, append to unknown map";
auto friendAddress = f->getAddress() ? f->getAddress()->clone() : nullptr;
if (friendAddress) {
friendAddress->clean();
qDebug() << "friend exists, append to unknown map";
friendsManager->appendUnknownFriend(friendAddress, f);
if (friendsManager->isInOtherAddresses(
Utils::coreStringToAppString(friendAddress->asStringUriOnly()))) {
friendsManager->removeOtherAddress(Utils::coreStringToAppString(friendAddress->asStringUriOnly()));
}
}
}
auto fList = f ? f->getFriendList() : nullptr;
auto fList = f->getFriendList();
// qDebug() << log().arg("") << (f ? f->getName().c_str() : "NoFriend") << ", "
// << (result->getAddress() ? result->getAddress()->asString().c_str() : "NoAddr") << " / "
// << (fList ? fList->getDisplayName().c_str() : "NoList") << result->getSourceFlags() << " /
//"
// << (f ? f.get() : nullptr);
bool isLdap = (result->getSourceFlags() & (int)linphone::MagicSearch::Source::LdapServers) != 0;
// Do not add it into ldap_friends if it already exists in app_friends.
if (isLdap && f &&
(!fList || fList->getDisplayName() != "app_friends")) { // Double check because of SDK merging that lead to
// use a ldap result as of app_friends/ldap_friends.
updateFriendListWithFriend(f, ldapFriends);
// qDebug() << log().arg("") << (f ? f->getName().c_str() : "NoFriend") << ", "
// << (result->getAddress() ? result->getAddress()->asString().c_str() : "NoAddr") << " / "
// << (fList ? fList->getDisplayName().c_str() : "NoList") << result->getSourceFlags() << " /
//"
// << (f ? f.get() : nullptr);
bool isLdap = (result->getSourceFlags() & (int)linphone::MagicSearch::Source::LdapServers) != 0;
// Do not add it into ldap_friends if it already exists in app_friends.
if (isLdap && (!fList || fList->getDisplayName() !=
"app_friends")) { // Double check because of SDK merging that lead to
// use a ldap result as of app_friends/ldap_friends.
updateFriendListWithFriend(f, ldapFriends);
}
}
}
}

View file

@ -19,9 +19,11 @@
*/
#include "SettingsModel.hpp"
#include "core/App.hpp"
#include "core/path/Paths.hpp"
#include "model/core/CoreModel.hpp"
#include "model/tool/ToolModel.hpp"
// #include "model/tool/VfsUtils.hpp"
#include "tool/Utils.hpp"
// =============================================================================
@ -486,6 +488,26 @@ void SettingsModel::setVfsEnabled(bool enabled) {
emit vfsEnabledChanged(enabled);
}
bool SettingsModel::getVfsEncrypted() const {
return false;
// mAppSettings.beginGroup("keychain");
// return mAppSettings.value("enabled", false).toBool();
}
// void SettingsModel::setVfsEncrypted(bool encrypted, const bool deleteUserData) {
// #ifdef ENABLE_QT_KEYCHAIN
// if (getVfsEncrypted() != encrypted) {
// if (encrypted) {
// mVfsUtils.newEncryptionKeyAsync();
// shared_ptr<linphone::Factory> factory = linphone::Factory::get();
// factory->setDownloadDir(Utils::appStringToCoreString(getDownloadFolder()));
// } else { // Remove key, stop core, delete data and initiate reboot
// mVfsUtils.needToDeleteUserData(deleteUserData);
// mVfsUtils.deleteKey(mVfsUtils.getApplicationVfsEncryptionKey());
// }
// }
// #endif
// }
// =============================================================================
// Logs.
// =============================================================================
@ -740,6 +762,17 @@ int SettingsModel::getEmojiFontSize() const {
return mConfig->getInt(UiSection, "emoji_font_size", Constants::DefaultEmojiFontPointSize);
}
QFont SettingsModel::getTextMessageFont() const {
QString family = Utils::coreStringToAppString(mConfig->getString(
UiSection, "text_message_font", Utils::appStringToCoreString(App::getInstance()->font().family())));
int pointSize = getTextMessageFontSize();
return QFont(family, pointSize);
}
int SettingsModel::getTextMessageFontSize() const {
return mConfig->getInt(UiSection, "text_message_font_size", Constants::DefaultFontPointSize);
}
// clang-format off
void SettingsModel::notifyConfigReady(){
DEFINE_NOTIFY_CONFIG_READY(disableChatFeature, DisableChatFeature)

View file

@ -49,6 +49,7 @@ public:
std::shared_ptr<linphone::Config> mConfig;
bool getVfsEnabled() const;
bool getVfsEncrypted() const;
void setVfsEnabled(const bool enabled);
bool getVideoEnabled() const;
@ -159,6 +160,8 @@ public:
QFont getEmojiFont() const;
int getEmojiFontSize() const;
QFont getTextMessageFont() const;
int getTextMessageFontSize() const;
// UI
DECLARE_GETSET(bool, disableChatFeature, DisableChatFeature)
@ -240,6 +243,10 @@ private:
MediastreamerUtils::SimpleCaptureGraph *mSimpleCaptureGraph = nullptr;
int mCaptureGraphListenerCount = 0;
#ifdef ENABLE_QT_KEYCHAIN
VfsUtils mVfsUtils;
#endif
void enableCallForward(QString destination);
void disableCallForward();

View file

@ -0,0 +1,186 @@
// /*
// * Copyright (c) 2010-2022 Belledonne Communications SARL.
// *
// * This file is part of linphone-desktop
// * (see https://www.linphone.org).
// *
// * This program is free software: you can redistribute it and/or modify
// * it under the terms of the GNU General Public License as published by
// * the Free Software Foundation, either version 3 of the License, or
// * (at your option) any later version.
// *
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// * GNU General Public License for more details.
// *
// * You should have received a copy of the GNU General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// */
// #include "VfsUtils.hpp"
// #include <bctoolbox/crypto.hh>
// #include <linphone++/factory.hh>
// #include <linphone/api/c-factory.h>
// #include <core/path/Paths.hpp>
// #include <model/setting/SettingsModel.hpp>
// #include <tool/Constants.hpp>
// #include <tool/Utils.hpp>
// #include <QCoreApplication>
// #include <QDebug>
// // =============================================================================
// VfsUtils::VfsUtils(QObject *parent)
// : QObject(parent), mReadCredentialJob(QLatin1String(APPLICATION_ID)),
// mWriteCredentialJob(QLatin1String(APPLICATION_ID)), mDeleteCredentialJob(QLatin1String(APPLICATION_ID)) {
// mReadCredentialJob.setAutoDelete(false);
// mWriteCredentialJob.setAutoDelete(false);
// mDeleteCredentialJob.setAutoDelete(false);
// }
// void VfsUtils::deleteKey(const QString &key) {
// mDeleteCredentialJob.setKey(key);
// QObject::connect(&mDeleteCredentialJob, &QKeychain::DeletePasswordJob::finished, [=]() {
// if (mDeleteCredentialJob.error()) {
// emit error(tr("Delete key failed: %1").arg(qPrintable(mDeleteCredentialJob.errorString())));
// return;
// }
// emit keyDeleted(key);
// });
// mDeleteCredentialJob.start();
// }
// void VfsUtils::readKey(const QString &key) {
// mReadCredentialJob.setKey(key);
// QObject::connect(&mReadCredentialJob, &QKeychain::ReadPasswordJob::finished, [=]() {
// if (mReadCredentialJob.error()) {
// emit error(tr("Read key failed: %1").arg(qPrintable(mReadCredentialJob.errorString())));
// return;
// }
// emit keyRead(key, mReadCredentialJob.textData());
// });
// mReadCredentialJob.start();
// }
// void VfsUtils::writeKey(const QString &key, const QString &value) {
// mWriteCredentialJob.setKey(key);
// QObject::connect(&mWriteCredentialJob, &QKeychain::WritePasswordJob::finished, [=]() {
// if (mWriteCredentialJob.error()) {
// emit error(tr("Write key failed: %1").arg(qPrintable(mWriteCredentialJob.errorString())));
// return;
// }
// if (key == getApplicationVfsEncryptionKey()) updateSDKWithKey(value);
// emit keyWritten(key);
// });
// mWriteCredentialJob.setTextData(value);
// mWriteCredentialJob.start();
// }
// bool VfsUtils::needToDeleteUserData() const {
// return mNeedToDeleteUserData;
// }
// void VfsUtils::needToDeleteUserData(const bool &need) {
// mNeedToDeleteUserData = need;
// }
// //-----------------------------------------------------------------------------------------------
// void VfsUtils::newEncryptionKeyAsync() {
// QString value;
// bctoolbox::RNG rng;
// auto key = rng.randomize(32);
// size_t keySize = key.size();
// uint8_t *shaKey = new uint8_t[keySize];
// bctbx_sha256(&key[0], key.size(), keySize, shaKey);
// for (int i = 0; i < keySize; ++i)
// value += QString::number(shaKey[i], 16);
// writeKey(getApplicationVfsEncryptionKey(), value);
// }
// bool VfsUtils::newEncryptionKey() {
// int argc = 1;
// const char *argv = "dummy";
// QCoreApplication vfsSetter(argc, (char **)&argv);
// VfsUtils vfs;
// QObject::connect(
// &vfs, &VfsUtils::keyWritten, &vfsSetter, [&vfsSetter, &vfs](const QString &key) { vfsSetter.quit(); },
// Qt::QueuedConnection);
// QObject::connect(
// &vfs, &VfsUtils::error, &vfsSetter,
// [&vfsSetter](const QString &errorText) {
// qCritical() << "[VFS] " << errorText;
// vfsSetter.exit(-1);
// },
// Qt::QueuedConnection);
// vfs.newEncryptionKeyAsync();
// return vfsSetter.exec() != -1;
// }
// bool VfsUtils::updateSDKWithKey(int argc, char *argv[]) {
// QCoreApplication core(argc, argv);
// AppController::initQtAppDetails(); // Set settings context.
// QSettings settings;
// return updateSDKWithKey(&settings);
// }
// bool VfsUtils::updateSDKWithKey() {
// QSettings settings;
// return updateSDKWithKey(&settings);
// }
// bool VfsUtils::updateSDKWithKey(QSettings *settings) { // Update SDK if key exists. Return true if encrypted.
// bool isEnabled = false;
// // Check in factory if it is mandatory.
// auto config = linphone::Factory::get()->createConfigWithFactory("", Paths::getFactoryConfigFilePath());
// if (config->getBool(SettingsModel::UiSection, "vfs_encryption_enabled", false)) {
// isEnabled = true;
// }
// settings->beginGroup("keychain");
// bool settingsValue = settings->value("enabled", false).toBool();
// if (isEnabled && !settingsValue) settings->setValue("enabled", isEnabled);
// else if (!isEnabled) isEnabled = settingsValue;
// if (isEnabled) {
// int argc = 1;
// const char *argv = "dummy";
// QCoreApplication vfsSetter(argc, (char **)&argv);
// VfsUtils vfs;
// QObject::connect(
// &vfs, &VfsUtils::keyRead, &vfsSetter,
// [&vfsSetter, &vfs](const QString &key, const QString &value) {
// VfsUtils::updateSDKWithKey(value);
// vfs.mVfsEncrypted = true;
// vfsSetter.quit();
// },
// Qt::QueuedConnection);
// QObject::connect(
// &vfs, &VfsUtils::error, &vfsSetter, [&vfsSetter](const QString &errorText) { vfsSetter.quit(); },
// Qt::QueuedConnection);
// vfs.readKey(vfs.getApplicationVfsEncryptionKey());
// vfsSetter.exec();
// if (!vfs.mVfsEncrypted) { // Doesn't have key.
// return VfsUtils::newEncryptionKey(); // Return false on error.
// }
// return vfs.mVfsEncrypted;
// } else return false;
// }
// void VfsUtils::updateSDKWithKey(const QString &key) {
// std::string value = Utils::appStringToCoreString(key);
// linphone::Factory::get()->setVfsEncryption(LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256,
// (const uint8_t *)value.c_str(), std::min(32, (int)value.length()));
// }
// QString VfsUtils::getApplicationVfsEncryptionKey() const {
// return QString(APPLICATION_ID) + "VfsEncryption";
// }

View file

@ -0,0 +1,78 @@
// /*
// * Copyright (c) 2010-2022 Belledonne Communications SARL.
// *
// * This file is part of linphone-desktop
// * (see https://www.linphone.org).
// *
// * This program is free software: you can redistribute it and/or modify
// * it under the terms of the GNU General Public License as published by
// * the Free Software Foundation, either version 3 of the License, or
// * (at your option) any later version.
// *
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// * GNU General Public License for more details.
// *
// * You should have received a copy of the GNU General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// */
// #ifndef VFS_UTILS_H_
// #define VFS_UTILS_H_
// #include "config.h"
// #include <QObject>
// // #ifdef QTKEYCHAIN_USE_BUILD_INTERFACE
// // #include <keychain.h>
// // #elif defined(QTKEYCHAIN_TARGET_NAME)
// // #define KEYCHAIN_HEADER <qtkeychain/keychain.h>
// // #include KEYCHAIN_HEADER
// // #else
// // #include <EQt6Keychain/keychain.h>
// // #endif
// #include <QSettings>
// // #include <qtkeychain/keychain.h>
// // =============================================================================
// class VfsUtils : public QObject {
// Q_OBJECT
// public:
// VfsUtils(QObject *parent = Q_NULLPTR);
// Q_INVOKABLE void deleteKey(const QString &key); // Delete a key and send error() or keyDeleted()
// Q_INVOKABLE void readKey(const QString &key); // Read a key, send error() or keyStored()
// Q_INVOKABLE void writeKey(const QString &key, const QString &value); // Write a key and send error() or keyWritten()
// void newEncryptionKeyAsync(); // Generate a key, store it and update SDK. Wait for keyWritten() or error().
// static bool newEncryptionKey(); // Generate a key, store it and update SDK.
// static bool updateSDKWithKey(int argc, char *argv[]); // Can be calle outside application.
// static bool updateSDKWithKey(QSettings *settings); // Update SDK if key exists. Return true if encrypted.
// static bool updateSDKWithKey(); // Need it to pass QSettings
// static void updateSDKWithKey(const QString &key); // SDK->setVfsEncryption(key)
// QString getApplicationVfsEncryptionKey() const; // Get the key in store keys for VFS encryyption
// bool needToDeleteUserData() const;
// void needToDeleteUserData(const bool &need);
// signals:
// void keyDeleted(const QString &key);
// void keyRead(const QString &key, const QString &value);
// void keyWritten(const QString &key);
// void error(const QString &errorText);
// private:
// // QKeychain::ReadPasswordJob mReadCredentialJob;
// // QKeychain::WritePasswordJob mWriteCredentialJob;
// // QKeychain::DeletePasswordJob mDeleteCredentialJob;
// bool mNeedToDeleteUserData = false;
// bool mVfsEncrypted = false;
// };
// #endif

View file

@ -3,6 +3,7 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/EnumsToString.cpp
tool/Utils.cpp
tool/UriTools.cpp
tool/QExifImageHeader.cpp
tool/LinphoneEnums.cpp
tool/thread/SafeSharedPointer.hpp
@ -10,8 +11,11 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/thread/Thread.cpp
tool/providers/AvatarProvider.cpp
tool/providers/EmojiProvider.cpp
tool/providers/ExternalImageProvider.cpp
tool/providers/ImageProvider.cpp
tool/providers/ScreenProvider.cpp
tool/providers/ThumbnailProvider.cpp
tool/providers/VideoFrameGrabber.cpp
tool/native/DesktopTools.hpp
@ -21,6 +25,8 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/file/FileDownloader.cpp
tool/file/FileExtractor.cpp
tool/ui/DashRectangle.cpp
)
if (APPLE)

View file

@ -133,6 +133,7 @@ public:
static constexpr char PathAssistantConfig[] = "/" EXECUTABLE_NAME "/assistant/";
static constexpr char PathAvatars[] = "/avatars/";
static constexpr char PathFiles[] = "/files/";
static constexpr char PathCaptures[] = "/" EXECUTABLE_NAME "/captures/";
static constexpr char PathCodecs[] = "/codecs/";
static constexpr char PathData[] = "/" EXECUTABLE_NAME;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,336 @@
/****************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the Qt scene graph research project.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
// This file was copied from Qt Extended 4.5
#ifndef QEXIFIMAGEHEADER_H_
#define QEXIFIMAGEHEADER_H_
#include <QPair>
#include <QVector>
#include <QSharedData>
#include <QVariant>
#include <QSysInfo>
#include <QIODevice>
typedef QPair<quint32, quint32> QExifURational;
typedef QPair<qint32, qint32> QExifSRational;
class QExifValuePrivate;
class QExifValue {
public:
enum Type {
Byte = 1,
Ascii = 2,
Short = 3,
Long = 4,
Rational = 5,
Undefined = 7,
SignedLong = 9,
SignedRational = 10
};
enum TextEncoding {
NoEncoding,
AsciiEncoding,
JisEncoding,
UnicodeEncoding,
UndefinedEncoding
};
QExifValue ();
QExifValue (quint8 value);
QExifValue (const QVector<quint8> &value);
QExifValue (const QString &value, TextEncoding encoding = NoEncoding);
QExifValue (quint16 value);
QExifValue (const QVector<quint16> &value);
QExifValue (quint32 value);
QExifValue (const QVector<quint32> &value);
QExifValue (const QExifURational &value);
QExifValue (const QVector<QExifURational> &value);
QExifValue (const QByteArray &value);
QExifValue (qint32 value);
QExifValue (const QVector<qint32> &value);
QExifValue (const QExifSRational &value);
QExifValue (const QVector<QExifSRational> &value);
QExifValue (const QDateTime &value);
QExifValue (const QExifValue &other);
QExifValue &operator= (const QExifValue &other);
~QExifValue ();
bool operator== (const QExifValue &other) const;
bool isNull () const;
int type () const;
int count () const;
TextEncoding encoding () const;
quint8 toByte () const;
QVector<quint8> toByteVector () const;
QString toString () const;
quint16 toShort () const;
QVector<quint16> toShortVector () const;
quint32 toLong () const;
QVector<quint32> toLongVector () const;
QExifURational toRational () const;
QVector<QExifURational> toRationalVector () const;
QByteArray toByteArray () const;
qint32 toSignedLong () const;
QVector<qint32> toSignedLongVector () const;
QExifSRational toSignedRational () const;
QVector<QExifSRational> toSignedRationalVector () const;
QDateTime toDateTime () const;
private:
QExplicitlySharedDataPointer<QExifValuePrivate> d;
};
struct ExifIfdHeader;
class QExifImageHeaderPrivate;
class QExifImageHeader {
Q_DISABLE_COPY(QExifImageHeader)
public:
enum ImageTag {
ImageWidth = 0x0100,
ImageLength = 0x0101,
BitsPerSample = 0x0102,
Compression = 0x0103,
PhotometricInterpretation = 0x0106,
Orientation = 0x0112,
SamplesPerPixel = 0x0115,
PlanarConfiguration = 0x011C,
YCbCrSubSampling = 0x0212,
XResolution = 0x011A,
YResolution = 0x011B,
ResolutionUnit = 0x0128,
StripOffsets = 0x0111,
RowsPerStrip = 0x0116,
StripByteCounts = 0x0117,
TransferFunction = 0x012D,
WhitePoint = 0x013E,
PrimaryChromaciticies = 0x013F,
YCbCrCoefficients = 0x0211,
ReferenceBlackWhite = 0x0214,
DateTime = 0x0132,
ImageDescription = 0x010E,
Make = 0x010F,
Model = 0x0110,
Software = 0x0131,
Artist = 0x013B,
Copyright = 0x8298
};
enum ExifExtendedTag {
ExifVersion = 0x9000,
FlashPixVersion = 0xA000,
ColorSpace = 0xA001,
ComponentsConfiguration = 0x9101,
CompressedBitsPerPixel = 0x9102,
PixelXDimension = 0xA002,
PixelYDimension = 0xA003,
MakerNote = 0x927C,
UserComment = 0x9286,
RelatedSoundFile = 0xA004,
DateTimeOriginal = 0x9003,
DateTimeDigitized = 0x9004,
SubSecTime = 0x9290,
SubSecTimeOriginal = 0x9291,
SubSecTimeDigitized = 0x9292,
ImageUniqueId = 0xA420,
ExposureTime = 0x829A,
FNumber = 0x829D,
ExposureProgram = 0x8822,
SpectralSensitivity = 0x8824,
ISOSpeedRatings = 0x8827,
Oecf = 0x8828,
ShutterSpeedValue = 0x9201,
ApertureValue = 0x9202,
BrightnessValue = 0x9203,
ExposureBiasValue = 0x9204,
MaxApertureValue = 0x9205,
SubjectDistance = 0x9206,
MeteringMode = 0x9207,
LightSource = 0x9208,
Flash = 0x9209,
FocalLength = 0x920A,
SubjectArea = 0x9214,
FlashEnergy = 0xA20B,
SpatialFrequencyResponse = 0xA20C,
FocalPlaneXResolution = 0xA20E,
FocalPlaneYResolution = 0xA20F,
FocalPlaneResolutionUnit = 0xA210,
SubjectLocation = 0xA214,
ExposureIndex = 0xA215,
SensingMethod = 0xA217,
FileSource = 0xA300,
SceneType = 0xA301,
CfaPattern = 0xA302,
CustomRendered = 0xA401,
ExposureMode = 0xA402,
WhiteBalance = 0xA403,
DigitalZoomRatio = 0xA404,
FocalLengthIn35mmFilm = 0xA405,
SceneCaptureType = 0xA406,
GainControl = 0xA407,
Contrast = 0xA408,
Saturation = 0xA409,
Sharpness = 0xA40A,
DeviceSettingDescription = 0xA40B,
SubjectDistanceRange = 0x40C
};
enum GpsTag {
GpsVersionId = 0x0000,
GpsLatitudeRef = 0x0001,
GpsLatitude = 0x0002,
GpsLongitudeRef = 0x0003,
GpsLongitude = 0x0004,
GpsAltitudeRef = 0x0005,
GpsAltitude = 0x0006,
GpsTimeStamp = 0x0007,
GpsSatellites = 0x0008,
GpsStatus = 0x0009,
GpsMeasureMode = 0x000A,
GpsDop = 0x000B,
GpsSpeedRef = 0x000C,
GpsSpeed = 0x000D,
GpsTrackRef = 0x000E,
GpsTrack = 0x000F,
GpsImageDirectionRef = 0x0010,
GpsImageDirection = 0x0011,
GpsMapDatum = 0x0012,
GpsDestLatitudeRef = 0x0013,
GpsDestLatitude = 0x0014,
GpsDestLongitudeRef = 0x0015,
GpsDestLongitude = 0x0016,
GpsDestBearingRef = 0x0017,
GpsDestBearing = 0x0018,
GpsDestDistanceRef = 0x0019,
GpsDestDistance = 0x001A,
GpsProcessingMethod = 0x001B,
GpsAreaInformation = 0x001C,
GpsDateStamp = 0x001D,
GpsDifferential = 0x001E
};
QExifImageHeader ();
explicit QExifImageHeader (const QString &fileName);
~QExifImageHeader ();
bool loadFromJpeg (const QString &fileName);
bool loadFromJpeg (QIODevice *device);
bool saveToJpeg (const QString &fileName) const;
bool saveToJpeg (QIODevice *device) const;
bool read (QIODevice *device);
qint64 write (QIODevice *device) const;
qint64 size () const;
QSysInfo::Endian byteOrder () const;
void clear ();
QList<ImageTag> imageTags () const;
QList<ExifExtendedTag> extendedTags () const;
QList<GpsTag> gpsTags () const;
bool contains (ImageTag tag) const;
bool contains (ExifExtendedTag tag) const;
bool contains (GpsTag tag) const;
void remove (ImageTag tag);
void remove (ExifExtendedTag tag);
void remove (GpsTag tag);
QExifValue value (ImageTag tag) const;
QExifValue value (ExifExtendedTag tag) const;
QExifValue value (GpsTag tag) const;
void setValue (ImageTag tag, const QExifValue &value);
void setValue (ExifExtendedTag tag, const QExifValue &value);
void setValue (GpsTag tag, const QExifValue &value);
QImage thumbnail () const;
void setThumbnail (const QImage &thumbnail);
private:
enum PrivateTag {
ExifIfdPointer = 0x8769,
GpsInfoIfdPointer = 0x8825,
InteroperabilityIfdPointer = 0xA005,
JpegInterchangeFormat = 0x0201,
JpegInterchangeFormatLength = 0x0202
};
QByteArray extractExif (QIODevice *device) const;
QList<ExifIfdHeader> readIfdHeaders (QDataStream &stream) const;
QExifValue readIfdValue (QDataStream &stream, int startPos, const ExifIfdHeader &header) const;
template<typename T>
QMap<T, QExifValue> readIfdValues (QDataStream &stream, int startPos, const QList<ExifIfdHeader> &headers) const;
template<typename T>
QMap<T, QExifValue> readIfdValues (QDataStream &stream, int startPos, const QExifValue &pointer) const;
quint32 writeExifHeader (QDataStream &stream, quint16 tag, const QExifValue &value, quint32 offset) const;
void writeExifValue (QDataStream &stream, const QExifValue &value) const;
template<typename T>
quint32 writeExifHeaders (QDataStream &stream, const QMap<T, QExifValue> &values, quint32 offset) const;
template<typename T>
void writeExifValues (QDataStream &target, const QMap<T, QExifValue> &values) const;
quint32 sizeOf (const QExifValue &value) const;
template<typename T>
quint32 calculateSize (const QMap<T, QExifValue> &values) const;
QExifImageHeaderPrivate *d;
};
#endif // ifndef QEXIFIMAGEHEADER_H_

View file

@ -43,6 +43,7 @@
#include <QDesktopServices>
#include <QHostAddress>
#include <QImageReader>
#include <QMimeDatabase>
#include <QProcess>
#include <QQmlComponent>
#include <QQmlProperty>
@ -59,6 +60,10 @@
DEFINE_ABSTRACT_OBJECT(Utils)
namespace {
constexpr int SafeFilePathLimit = 100;
}
// =============================================================================
char *Utils::rstrstr(const char *a, const char *b) {
@ -347,6 +352,12 @@ QString Utils::formatTime(const QDateTime &date) {
return date.time().toString("hh:mm");
}
QString Utils::formatDuration(int durationMs) {
QTime duration(0, 0);
duration = duration.addMSecs(durationMs);
return duration.hour() > 0 ? duration.toString("hh:mm:ss") : duration.toString("mm:ss");
}
QString Utils::formatDateElapsedTime(const QDateTime &date) {
// auto y = floor(seconds / 31104000);
// if (y > 0) return QString::number(y) + " years";
@ -1870,6 +1881,18 @@ QString Utils::encodeEmojiToQmlRichFormat(const QString &body) {
return fmtBody;
}
static bool codepointIsVisible(uint code) {
return code > 0x00020;
}
bool Utils::isOnlyEmojis(const QString &text) {
if (text.isEmpty()) return false;
QVector<uint> utf32_string = text.toUcs4();
for (auto &code : utf32_string)
if (codepointIsVisible(code) && !Utils::codepointIsEmoji(code)) return false;
return true;
}
QString Utils::getFilename(QUrl url) {
return url.fileName();
}
@ -1906,3 +1929,79 @@ QString Utils::toTimeString(QDateTime date, const QString &format) {
// Issue : date.toString() will not print the good time in timezones. Get it from date and add ourself the offset.
return getOffsettedUTC(date).toString(format);
}
QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) {
if (soFarSoGood) *soFarSoGood = true;
QFileInfo info(filePath);
if (!info.exists()) return filePath;
const QString prefix = QStringLiteral("%1/%2").arg(info.absolutePath()).arg(info.baseName());
const QString ext = info.completeSuffix();
for (int i = 1; i < SafeFilePathLimit; ++i) {
QString safePath = QStringLiteral("%1 (%3).%4").arg(prefix).arg(i).arg(ext);
if (!QFileInfo::exists(safePath)) return safePath;
}
if (soFarSoGood) *soFarSoGood = false;
return QString("");
}
bool Utils::isVideo(const QString &path) {
if (path.isEmpty()) return false;
return QMimeDatabase().mimeTypeForFile(path).name().contains("video/");
}
bool Utils::isPdf(const QString &path) {
if (path.isEmpty()) return false;
return QMimeDatabase().mimeTypeForFile(path).name().contains("application/pdf");
}
bool Utils::isText(const QString &path) {
if (path.isEmpty()) return false;
return QMimeDatabase().mimeTypeForFile(path).name().contains("text");
}
bool Utils::isImage(const QString &path) {
if (path.isEmpty()) return false;
QFileInfo info(path);
if (!info.exists() || SettingsModel::getInstance()->getVfsEncrypted()) {
return QMimeDatabase().mimeTypeForFile(path).name().contains("image/");
} else {
if (!QMimeDatabase().mimeTypeForFile(info).name().contains("image/")) return false;
QImageReader reader(path);
return reader.canRead() && reader.imageCount() == 1;
}
}
bool Utils::isAnimatedImage(const QString &path) {
if (path.isEmpty()) return false;
QFileInfo info(path);
if (!info.exists() || !QMimeDatabase().mimeTypeForFile(info).name().contains("image/")) return false;
QImageReader reader(path);
return reader.canRead() && reader.supportsAnimation() && reader.imageCount() > 1;
}
bool Utils::canHaveThumbnail(const QString &path) {
if (path.isEmpty()) return false;
return isImage(path) || isAnimatedImage(path) /*|| isPdf(path)*/ || isVideo(path);
}
QImage Utils::getImage(const QString &pUri) {
QImage image(pUri);
QImageReader reader(pUri);
reader.setAutoTransform(true);
if (image.isNull()) { // Try to determine format from headers instead of using suffix
reader.setDecideFormatFromContent(true);
}
return reader.read();
}
void Utils::setGlobalCursor(Qt::CursorShape cursor) {
App::getInstance()->setOverrideCursor(QCursor(cursor));
}
void Utils::restoreGlobalCursor() {
App::getInstance()->restoreOverrideCursor();
}

View file

@ -95,6 +95,7 @@ public:
QString format = ""); // Return the date formated
Q_INVOKABLE static QString formatDateElapsedTime(const QDateTime &date);
Q_INVOKABLE static QString formatTime(const QDateTime &date); // Return the time formated
Q_INVOKABLE static QString formatDuration(int durationMs); // Return the duration formated
Q_INVOKABLE static QStringList generateSecurityLettersArray(int arraySize, int correctIndex, QString correctCode);
Q_INVOKABLE static int getRandomIndex(int size);
Q_INVOKABLE static bool copyToClipboard(const QString &text);
@ -154,9 +155,20 @@ public:
Q_INVOKABLE static QString encodeTextToQmlRichFormat(const QString &text,
const QVariantMap &options = QVariantMap());
Q_INVOKABLE static QString encodeEmojiToQmlRichFormat(const QString &body);
Q_INVOKABLE static bool isOnlyEmojis(const QString &text);
Q_INVOKABLE static QString getFilename(QUrl url);
static bool codepointIsEmoji(uint code);
Q_INVOKABLE static bool isVideo(const QString &path);
static QString getSafeFilePath(const QString &filePath, bool *soFarSoGood);
Q_INVOKABLE static bool isAnimatedImage(const QString &path);
Q_INVOKABLE static bool canHaveThumbnail(const QString &path);
Q_INVOKABLE static bool isImage(const QString &path);
Q_INVOKABLE static bool isPdf(const QString &path);
Q_INVOKABLE static bool isText(const QString &path);
Q_INVOKABLE static QImage getImage(const QString &pUri);
Q_INVOKABLE static void setGlobalCursor(Qt::CursorShape cursor);
Q_INVOKABLE static void restoreGlobalCursor();
Q_INVOKABLE static QString toDateTimeString(QDateTime date, const QString &format = "yyyy/MM/dd hh:mm:ss");
static QDateTime getOffsettedUTC(const QDateTime &date);

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "core/path/Paths.hpp"
#include "tool/Utils.hpp"
#include "ExternalImageProvider.hpp"
#include <QImageReader>
// =============================================================================
const QString ExternalImageProvider::ProviderId = "external";
ExternalImageProvider::ExternalImageProvider()
: QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) {
}
QImage ExternalImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) {
QImage image(Utils::getImage(QUrl::fromPercentEncoding(id.toUtf8())));
double requestedFactor = 1.0;
double factor = image.width() / (double)image.height();
if (requestedSize.isValid()) requestedFactor = requestedSize.width() / (double)requestedSize.height();
if (factor < 0.2) { // too height
image = image.copy(0, 0, image.width(), image.width() / requestedFactor);
} else if (factor > 5) { // too large
image = image.copy(0, 0, image.height() * requestedFactor, image.height());
}
*size = image.size();
return image;
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EXTERNAL_IMAGE_PROVIDER_H_
#define EXTERNAL_IMAGE_PROVIDER_H_
#include <QQuickImageProvider>
// =============================================================================
class ExternalImageProvider : public QQuickImageProvider {
public:
ExternalImageProvider();
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
static const QString ProviderId;
};
#endif // EXTERNAL_IMAGE_PROVIDER_H_

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ThumbnailProvider.hpp"
#include "model/setting/SettingsModel.hpp"
#include "tool/QExifImageHeader.hpp"
#include "tool/Utils.hpp"
#include <QFileInfo>
#include <QImageReader>
#include <QPainter>
#include <QSvgRenderer>
DEFINE_ABSTRACT_OBJECT(ThumbnailAsyncImageResponse)
// =============================================================================
const QString ThumbnailProvider::ProviderId = "thumbnail";
ThumbnailAsyncImageResponse::ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize) {
mPath = id;
connect(&mListener, &VideoFrameGrabberListener::imageGrabbed, this, &ThumbnailAsyncImageResponse::imageGrabbed);
if (QFileInfo(mPath).isFile()) {
bool removeExportedFile = SettingsModel::getInstance()->getVfsEncrypted();
if (removeExportedFile) {
std::shared_ptr<linphone::Content> content =
linphone::Factory::get()->createContentFromFile(Utils::appStringToCoreString(mPath));
mPath = Utils::coreStringToAppString(content->exportPlainFile());
}
QImage originalImage(mPath);
if (originalImage.isNull()) { // Try to determine format from headers
QImageReader reader(mPath);
reader.setDecideFormatFromContent(true);
QByteArray format = reader.format();
if (!format.isEmpty()) {
originalImage = QImage(mPath, format);
} else if (Utils::isVideo(mPath)) {
VideoFrameGrabber *grabber = new VideoFrameGrabber(removeExportedFile);
removeExportedFile = false;
connect(grabber, &VideoFrameGrabber::grabFinished, &mListener,
&VideoFrameGrabberListener::imageGrabbed);
grabber->requestFrame(mPath);
}
}
if (removeExportedFile) QFile(mPath).remove();
if (!originalImage.isNull()) {
emit imageGrabbed(originalImage);
}
}
}
QImage ThumbnailAsyncImageResponse::createThumbnail(const QString &path, QImage originalImage) {
QImage thumbnail;
if (!originalImage.isNull()) {
int rotation = 0;
QExifImageHeader exifImageHeader;
if (exifImageHeader.loadFromJpeg(path))
rotation = int(exifImageHeader.value(QExifImageHeader::ImageTag::Orientation).toShort());
double factor = originalImage.width() / (double)originalImage.height();
Qt::AspectRatioMode aspectRatio = Qt::KeepAspectRatio;
if (factor < 0.2 || factor > 5) aspectRatio = Qt::KeepAspectRatioByExpanding;
QImageReader reader(path);
if (reader.format() == "svg") {
QSvgRenderer svgRenderer(path);
if (svgRenderer.isValid()) {
thumbnail = QImage(Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight,
originalImage.format());
thumbnail.fill(QColor(Qt::transparent));
QPainter painter(&thumbnail);
svgRenderer.setAspectRatioMode(aspectRatio);
svgRenderer.render(&painter);
}
}
if (thumbnail.isNull()) {
QImage image(originalImage.size(), originalImage.format());
// Fill with color to replace transparency with white color instead of black (default).
image.fill(QColor(Qt::white).rgb());
QPainter painter(&image);
painter.drawImage(0, 0, originalImage);
//--------------------
thumbnail = image.scaled(Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight,
aspectRatio, Qt::SmoothTransformation);
if (aspectRatio == Qt::KeepAspectRatioByExpanding) // Cut
thumbnail =
thumbnail.copy(0, 0, Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight);
}
if (rotation != 0) {
QTransform transform;
if (rotation == 3 || rotation == 4) transform.rotate(180);
else if (rotation == 5 || rotation == 6) transform.rotate(90);
else if (rotation == 7 || rotation == 8) transform.rotate(-90);
thumbnail = thumbnail.transformed(transform);
if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7)
thumbnail = thumbnail.flipped(Qt::Horizontal);
}
}
return thumbnail;
}
void ThumbnailAsyncImageResponse::imageGrabbed(QImage image) {
mImage = createThumbnail(mPath, image);
emit finished();
}
QQuickTextureFactory *ThumbnailAsyncImageResponse::textureFactory() const {
return QQuickTextureFactory::textureFactoryForImage(mImage);
}
QQuickImageResponse *ThumbnailProvider::requestImageResponse(const QString &id, const QSize &requestedSize) {
ThumbnailAsyncImageResponse *response = new ThumbnailAsyncImageResponse(id, requestedSize);
return response;
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef THUMBNAIL_PROVIDER_H_
#define THUMBNAIL_PROVIDER_H_
#include <QQuickAsyncImageProvider>
#include "VideoFrameGrabber.hpp"
#include "tool/AbstractObject.hpp"
// Thumbnails are created asynchronously with QQuickAsyncImageProvider and not QQuickImageProvider.
// This ensure to have async objects like QMediaPlayer and QAbstractVideoSurface while keeping them in the main thread
// (mandatory for VideoSurface). If not, there seems to have some deadlocks in Qt library when GUI objects are deleted
// while still playing media.
// =============================================================================
class ThumbnailAsyncImageResponse : public QQuickImageResponse, public AbstractObject {
public:
ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize);
QQuickTextureFactory *textureFactory() const override; // Convert QImage into texture. If Image is null, then
// sourceSize will be egal to 0. So there will be no errors.
void imageGrabbed(QImage image);
QImage createThumbnail(const QString &path, QImage originalImage);
QImage mImage;
QString mPath;
VideoFrameGrabberListener mListener;
private:
DECLARE_ABSTRACT_OBJECT
};
class ThumbnailProvider : public QQuickAsyncImageProvider {
public:
virtual QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
static const QString ProviderId;
};
#endif // THUMBNAIL_PROVIDER_H_

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2023 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "VideoFrameGrabber.hpp"
#include <QFile>
#include <QVideoFrame>
VideoFrameGrabberListener::VideoFrameGrabberListener() {
}
VideoFrameGrabber::VideoFrameGrabber(bool deleteFile, QObject *parent) : QVideoSink(parent) {
mDeleteFile = deleteFile;
mPlayer = new QMediaPlayer();
mVideoSink = new QVideoSink();
connect(
mPlayer, &QMediaPlayer::errorOccurred, this,
[this](QMediaPlayer::Error error, const QString &errorString) { end(); }, Qt::DirectConnection);
QObject::connect(
mPlayer, &QMediaPlayer::mediaStatusChanged, this,
[this](QMediaPlayer::MediaStatus status) mutable {
switch (status) {
case QMediaPlayer::LoadedMedia:
if (!mLoadedMedia) {
mLoadedMedia = true;
if (mPlayer->hasVideo()) {
mPlayer->setPosition(mPlayer->duration() / 2);
mPlayer->play();
} else {
end();
}
}
break;
case QMediaPlayer::InvalidMedia:
case QMediaPlayer::EndOfMedia:
case QMediaPlayer::NoMedia:
end();
break;
default: {
}
}
},
Qt::DirectConnection);
connect(mVideoSink, &QVideoSink::videoFrameChanged, this, [this](const QVideoFrame &frame) {
if (isFormatSupported(frame)) mResult = frame.toImage().copy();
});
mPlayer->setVideoSink(mVideoSink);
mPlayer->setVideoOutput(this);
}
VideoFrameGrabber::~VideoFrameGrabber() {
if (mDeleteFile) QFile(mPath).remove();
}
void VideoFrameGrabber::requestFrame(const QString &path) {
mLoadedMedia = false;
mPath = path;
// mPlayer->set(QUrl::fromLocalFile(mPath));
}
void VideoFrameGrabber::end() {
if (mPlayer->mediaStatus() != QMediaPlayer::NoMedia) {
// mPlayer->setMedia(QUrl());
} else if (!mResultSent) { // Avoid sending multiple times before destroying the object
mResultSent = true;
emit grabFinished(mResult);
deleteLater();
}
}
QList<QVideoFrameFormat::PixelFormat> VideoFrameGrabber::supportedPixelFormats() const {
return QList<QVideoFrameFormat::PixelFormat>()
<< QVideoFrameFormat::Format_YUV420P << QVideoFrameFormat::Format_YV12 << QVideoFrameFormat::Format_UYVY
<< QVideoFrameFormat::Format_YUYV << QVideoFrameFormat::Format_NV12 << QVideoFrameFormat::Format_NV21
<< QVideoFrameFormat::Format_IMC1 << QVideoFrameFormat::Format_IMC2 << QVideoFrameFormat::Format_IMC3
<< QVideoFrameFormat::Format_IMC4 << QVideoFrameFormat::Format_Y8 << QVideoFrameFormat::Format_Y16
<< QVideoFrameFormat::Format_Jpeg << QVideoFrameFormat::Format_ABGR8888 << QVideoFrameFormat::Format_ARGB8888
<< QVideoFrameFormat::Format_ARGB8888_Premultiplied << QVideoFrameFormat::Format_AYUV
<< QVideoFrameFormat::Format_AYUV_Premultiplied << QVideoFrameFormat::Format_BGRA8888
<< QVideoFrameFormat::Format_BGRA8888_Premultiplied << QVideoFrameFormat::Format_BGRA8888_Premultiplied
<< QVideoFrameFormat::Format_BGRX8888;
}
bool VideoFrameGrabber::isFormatSupported(const QVideoFrame &frame) const {
const QImage::Format imageFormat = QVideoFrameFormat::imageFormatFromPixelFormat(frame.pixelFormat());
const QSize size = frame.size();
return imageFormat != QImage::Format_Invalid && !size.isEmpty() &&
frame.handleType() == QVideoFrame::HandleType::NoHandle;
}
bool VideoFrameGrabber::start(const QVideoFrameFormat::PixelFormat &format) {
return true;
// return QVideoSink::start(format);
}
void VideoFrameGrabber::stop() {
// QVideoSink::stop();
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2023 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef VIDEO_FRAME_GRABBER_H
#define VIDEO_FRAME_GRABBER_H
#include <QMediaPlayer>
#include <QVideoFrameFormat>
#include <QVideoSink>
// Call VideoFrameGrabber::requestFrame() and wait for imageGrabbed() to get the image.
// You will need to link your listener with connect(grabber, &VideoFrameGrabber::grabFinished, listener,
// &VideoFrameGrabberListener::imageGrabbed);
class VideoFrameGrabberListener : public QObject {
Q_OBJECT
public:
VideoFrameGrabberListener();
signals:
void imageGrabbed(QImage image);
};
class VideoFrameGrabber : public QVideoSink {
Q_OBJECT
public:
VideoFrameGrabber(bool deleteFile = false, QObject *parent = 0);
~VideoFrameGrabber();
void requestFrame(const QString &path); // Function to call.
void end();
QList<QVideoFrameFormat::PixelFormat> supportedPixelFormats() const;
bool isFormatSupported(const QVideoFrame &frame) const;
bool start(const QVideoFrameFormat::PixelFormat &format);
void stop();
QMediaPlayer *mPlayer = nullptr;
QVideoSink *mVideoSink = nullptr;
bool mLoadedMedia = false;
bool mResultSent = false;
bool mDeleteFile = false;
QString mPath;
QImage mResult;
signals:
void frameAvailable(QImage frame);
void grabFinished(QImage frame);
};
#endif

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "DashRectangle.hpp"
#include "core/App.hpp"
DashRectangle::DashRectangle(QQuickItem *parent) : QQuickPaintedItem(parent) {
connect(this, &DashRectangle::radiusChanged, this, [this] { update(); });
connect(this, &DashRectangle::colorChanged, this, [this] { update(); });
}
void DashRectangle::paint(QPainter *painter) {
QPen pen(Qt::DotLine);
pen.setColor(mColor);
pen.setWidthF(4 * App::getInstance()->getScreenRatio());
painter->setPen(pen);
painter->drawRoundedRect(x(), y(), width(), height(), mRadius, mRadius);
}
float DashRectangle::getRadius() const {
return mRadius;
}
void DashRectangle::setRadius(float radius) {
if (mRadius != radius) {
mRadius = radius;
emit radiusChanged();
}
}
QColor DashRectangle::getColor() const {
return mColor;
}
void DashRectangle::setColor(QColor Color) {
if (mColor != Color) {
mColor = Color;
emit colorChanged();
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DASHRECTANGLE_H
#define DASHRECTANGLE_H
#include <QObject>
#include <QPainter>
#include <QQuickPaintedItem>
class DashRectangle : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(float radius READ getRadius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
public:
explicit DashRectangle(QQuickItem *parent = nullptr);
virtual void paint(QPainter *painter);
float getRadius() const;
void setRadius(float radius);
QColor getColor() const;
void setColor(QColor color);
signals:
void radiusChanged();
void colorChanged();
private:
float mRadius = 0;
QColor mColor;
};
#endif // DASHRECTANGLE_H

View file

@ -35,6 +35,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Control/Container/Call/CallLayout.qml
view/Control/Container/Call/CallGridLayout.qml
view/Control/Container/Call/Mosaic.qml
view/Control/Container/Chat/ChatFilesGridLayout.qml
view/Control/Container/Contact/ContactLayout.qml
view/Control/Container/Contact/PresenceNoteLayout.qml
view/Control/Container/Main/MainRightPanel.qml
@ -46,6 +47,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Control/Display/TemporaryText.qml
view/Control/Display/ProgressBar.qml
view/Control/Display/RoundedPane.qml
view/Control/Display/RoundProgressBar.qml
view/Control/Display/Sticker.qml
view/Control/Display/Text.qml
view/Control/Display/ToolTip.qml
@ -53,11 +55,15 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Control/Display/Call/CallHistoryListView.qml
view/Control/Display/Call/CallStatistics.qml
view/Control/Display/Chat/Emoji/EmojiPicker.qml
view/Control/Display/Chat/ChatMessageContent.qml
view/Control/Display/Chat/ChatAudioContent.qml
view/Control/Display/Chat/ChatTextContent.qml
view/Control/Display/Chat/ChatListView.qml
view/Control/Display/Chat/ChatMessage.qml
view/Control/Display/Chat/ChatMessageInvitationBubble.qml
view/Control/Display/Chat/ChatMessagesListView.qml
view/Control/Display/Chat/Event.qml
view/Control/Display/Chat/FileView.qml
view/Control/Display/Contact/Avatar.qml
view/Control/Display/Contact/Contact.qml
view/Control/Display/Contact/Presence.qml
@ -78,6 +84,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Control/Form/Settings/MultimediaSettings.qml
view/Control/Form/Settings/ScreencastSettings.qml
view/Control/Input/Chat/ChatDroppableTextArea.qml
view/Control/Input/Calendar.qml
view/Control/Input/DecoratedTextField.qml
view/Control/Input/DigitInput.qml
@ -167,6 +174,7 @@ list(APPEND _LINPHONEAPP_QML_SINGLETONS
view/Style/AppIcons.qml
view/Style/buttonStyle.js
view/Style/DefaultStyle.qml
view/Style/FileViewStyle.qml
view/Style/Typography.qml
)

View file

@ -8,13 +8,13 @@ Button {
id: mainItem
textSize: Typography.p1s.pixelSize
textWeight: Typography.p1s.weight
topPadding: Math.round(16 * DefaultStyle.dp)
bottomPadding: Math.round(16 * DefaultStyle.dp)
leftPadding: Math.round(16 * DefaultStyle.dp)
rightPadding: Math.round(16 * DefaultStyle.dp)
icon.width: Math.round(24 * DefaultStyle.dp)
icon.height: Math.round(24 * DefaultStyle.dp)
radius: Math.round(40 * DefaultStyle.dp)
width: Math.round(24 * DefaultStyle.dp)
height: Math.round(24 * DefaultStyle.dp)
padding: Math.round(16 * DefaultStyle.dp)
// bottomPadding: Math.round(16 * DefaultStyle.dp)
// leftPadding: Math.round(16 * DefaultStyle.dp)
// rightPadding: Math.round(16 * DefaultStyle.dp)
icon.width: width
icon.height: width
radius: width * 2
// width: Math.round(24 * DefaultStyle.dp)
height: width
}

View file

@ -24,10 +24,6 @@ Mosaic {
qmlName: "G"
Component.onCompleted: console.log("Loaded : " +allDevices + " = " +allDevices.count)
}
property AccountProxy accounts: AccountProxy {
id: accountProxy
sourceModel: AppCpp.accounts
}
model: grid.call && grid.call.core.isConference ? participantDevices: [0,1]
delegate: Item{
id: avatarCell

View file

@ -3,9 +3,8 @@ import QtQuick.Controls.Basic
import QtQuick.Layouts
import QtQml.Models
// =============================================================================
ColumnLayout{
ColumnLayout {
id: mainLayout
property alias delegateModel: grid.model
property alias cellHeight: grid.cellHeight

View file

@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Layouts
import QtQml.Models
import Linphone
import UtilsCpp
// =============================================================================
GridLayout {
id: mainItem
property ChatMessageGui chatMessageGui: null
property bool isHoveringFile: false
property int itemCount: delModel.count
property int itemWidth: Math.round(95 * DefaultStyle.dp)
// cellWidth:
// cellHeight: Math.round(105 * DefaultStyle.dp)
property real maxWidth: 3 * 105 * DefaultStyle.dp
columns: optimalColumns
property int optimalColumns: {
let maxCols = Math.floor(maxWidth / itemWidth);
let bestCols = 1;
let minRows = Number.MAX_VALUE;
let minEmptySlots = Number.MAX_VALUE;
for (let cols = maxCols; cols >= 1; cols--) {
let rows = Math.ceil(itemCount / cols);
let emptySlots = cols * rows - itemCount;
if (
rows < minRows ||
(rows === minRows && emptySlots < minEmptySlots)
) {
bestCols = cols;
minRows = rows;
minEmptySlots = emptySlots;
}
}
return bestCols;
}
Repeater {
id: delModel
model: ChatMessageContentProxy {
id: contentProxy
filterType: ChatMessageContentProxy.FilterContentType.File
chatMessageGui: mainItem.chatMessageGui
}
delegate: FileView {
id: avatarCell
contentGui: modelData
visible: modelData
height: mainItem.itemWidth
width: mainItem.itemWidth
onIsHoveringChanged: mainItem.isHoveringFile = isHovering
}
}
}

View file

@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Layouts
import Linphone
import UtilsCpp
// =============================================================================
Loader{
id: mainItem
property ChatMessageContentGui chatMessageContentGui
property int availableWidth : parent.width
property int width: active ? Math.max(availableWidth - ChatAudioMessageStyle.emptySpace, ChatAudioMessageStyle.minWidth) : 0
property int fitHeight: active ? 60 : 0
property font customFont : SettingsModel.textMessageFont
property bool isActive: active
property string filePath : tempFile.filePath
active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording()
onChatMessageContentGuiChanged: if(chatMessageContentGui){
tempFile.createFileFromContentModel(chatMessageContentGui, false);
}
TemporaryFile {
id: tempFile
}
sourceComponent: Item{
id: loadedItem
property bool isPlaying : vocalPlayer.item && vocalPlayer.item.playbackState === SoundPlayer.PlayingState
onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop()
width: availableWidth < 0 || availableWidth > mainItem.width ? mainItem.width : availableWidth
height: mainItem.fitHeight
clip: false
Loader {
id: vocalPlayer
active: false
function play(){
if(!vocalPlayer.active)
vocalPlayer.active = true
else {
if(loadedItem.isPlaying){// Pause the play
vocalPlayer.item.pause()
}else{// Play the audio
vocalPlayer.item.play()
}
}
}
sourceComponent: SoundPlayer {
source: mainItem.chatMessageContentGui && mainItem.filePath
onStopped:{
mediaProgressBar.value = 101
}
Component.onCompleted: {
play()// This will open the file and allow seeking
pause()
mediaProgressBar.value = 0
mediaProgressBar.refresh()
}
}
onStatusChanged: if (loader.status == Loader.Ready) play()
}
RowLayout{
id: lineLayout
anchors.fill: parent
spacing: 5
ActionButton{
id: playButton
Layout.preferredHeight: iconSize
Layout.preferredWidth: iconSize
Layout.rightMargin: 5
Layout.leftMargin: 15
Layout.alignment: Qt.AlignVCenter
isCustom: true
backgroundRadius: width
colorSet: (loadedItem.isPlaying ? ChatAudioMessageStyle.pauseAction
: ChatAudioMessageStyle.playAction)
onClicked:{
vocalPlayer.play()
}
}
Item{
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 10
Layout.topMargin: 10
Layout.bottomMargin: 10
MediaProgressBar{
id: mediaProgressBar
anchors.fill: parent
progressDuration: vocalPlayer.item ? vocalPlayer.item.duration : chatMessageContentGui.core.getFileDuration()
progressPosition: 0
value: 0
stopAtEnd: true
resetAtEnd: false
backgroundColor: ChatAudioMessageStyle.backgroundColor.color
colorSet: ChatAudioMessageStyle.progressionWave
function refresh(){
if( vocalPlayer.item){
progressPosition = vocalPlayer.item.getPosition()
value = 100 * ( progressPosition / vocalPlayer.item.duration)
}
}
onEndReached:{
if(vocalPlayer.item)
vocalPlayer.item.stop()
}
onRefreshPositionRequested: refresh()
onSeekRequested: if( vocalPlayer.item){
vocalPlayer.item.seek(ms)
progressPosition = vocalPlayer.item.getPosition()
value = 100 * (progressPosition / vocalPlayer.item.duration)
}
}
}
}
}
}

View file

@ -311,8 +311,8 @@ ListView {
unread: modelData.core.unreadMessagesCount
}
EffectImage {
visible: modelData != undefined && lastMessageText.visible && modelData?.core.lastMessage && modelData?.core.lastMessageState !== LinphoneEnums.ChatMessageState.StateIdle
&& !modelData?.core.lastMessage.core.isRemoteMessage
visible: modelData?.core.lastMessage && modelData?.core.lastMessageState !== LinphoneEnums.ChatMessageState.StateIdle
&& !modelData.core.lastMessage.core.isRemoteMessage
Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0
Layout.preferredHeight: 14 * DefaultStyle.dp
colorizationColor: DefaultStyle.main1_500_main

View file

@ -14,37 +14,23 @@ Control.Control {
property color backgroundColor
property bool isFirstMessage
property string imgUrl
property ChatMessageGui chatMessage
property string ownReaction: chatMessage? chatMessage.core.ownReaction : ""
property string fromAddress: chatMessage? chatMessage.core.fromAddress : ""
property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false
property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false
property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle
property string richFormatText: chatMessage.core.hasTextContent ? UtilsCpp.encodeTextToQmlRichFormat(chatMessage.core.utf8Text) : ""
hoverEnabled: true
property bool linkHovered: false
property real maxWidth: parent?.width || Math.round(300 * DefaultStyle.dp)
leftPadding: isRemoteMessage ? Math.round(5 * DefaultStyle.dp) : 0
signal messageDeletionRequested()
signal isFileHoveringChanged(bool isFileHovering)
background: Item {
anchors.fill: parent
Text {
id: fromNameText
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage
anchors.top: parent.top
maximumLineCount: 1
width: implicitWidth
x: chatBubble.x
text: mainItem.chatMessage.core.fromName
color: DefaultStyle.main2_500main
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
}
}
}
function handleDefaultMouseEvent(event) {
@ -66,179 +52,162 @@ Control.Control {
Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
_address: chatMessage ? chatMessage.core.fromAddress : ""
}
Item {
Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
Layout.preferredHeight: childrenRect.height
Layout.preferredWidth: childrenRect.width
Control.Control {
id: chatBubble
spacing: Math.round(2 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(6 * DefaultStyle.dp)
leftPadding: Math.round(12 * DefaultStyle.dp)
rightPadding: Math.round(12 * DefaultStyle.dp)
width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth)
MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble.
id: defaultMouseArea
visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse)
cursorShape: mainItem.linkHovered ? Qt.PointingHandCursor : Qt.ArrowCursor
}
background: Item {
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: mainItem.backgroundColor
radius: Math.round(16 * DefaultStyle.dp)
}
Rectangle {
visible: mainItem.isFirstMessage && mainItem.isRemoteMessage
anchors.top: parent.top
anchors.left: parent.left
width: Math.round(parent.width / 4)
height: Math.round(parent.height / 4)
color: mainItem.backgroundColor
}
Rectangle {
visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage
anchors.bottom: parent.bottom
anchors.right: parent.right
width: Math.round(parent.width / 4)
height: Math.round(parent.height / 4)
color: mainItem.backgroundColor
}
}
contentItem: ColumnLayout {
id: contentLayout
Image {
visible: mainItem.imgUrl != undefined
id: contentimage
}
Text { // Uses default mouse area for link hovering.
id: textElement
visible: mainItem.richFormatText !== ""
text: mainItem.richFormatText
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.fillHeight: true
horizontalAlignment: Text.AlignLeft
color: DefaultStyle.main2_700
textFormat: Text.RichText
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
}
onLinkActivated: {
if (link.startsWith('sip'))
UtilsCpp.createCall(link)
else
Qt.openUrlExternally(link)
}
onHoveredLinkChanged: {
mainItem.linkHovered = hoveredLink !== ""
}
}
// Meeting invitation bubble
/////////////////////////////
Loader {
id: invitationLoader
active: mainItem.chatMessage.core.conferenceInfo !== null
sourceComponent: invitationComponent
}
Component {
id: invitationComponent
ChatMessageInvitationBubble {
conferenceInfoGui: mainItem.chatMessage.core.conferenceInfo
Layout.fillWidth: true
Layout.fillHeight: true
onMouseEvent: mainItem.handleDefaultMouseEvent(event)
}
}
/////////////////////////////
RowLayout {
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
Text {
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false)
color: DefaultStyle.main2_500main
font {
pixelSize: Typography.p3.pixelSize
weight: Typography.p3.weight
}
}
EffectImage {
visible: !mainItem.isRemoteMessage
Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0
Layout.preferredHeight: 14 * DefaultStyle.dp
colorizationColor: DefaultStyle.main1_500_main
imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered
? AppIcons.envelope
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser
? AppIcons.check
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered
? AppIcons.warningCircle
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed
? AppIcons.checks
: ""
}
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
spacing: 0
Text {
id: fromNameText
Layout.alignment: Qt.AlignTop
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.leftMargin: avatar.width// mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
maximumLineCount: 1
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
width: implicitWidth
x: mapToItem(this, chatBubble.x, chatBubble.y).x
text: mainItem.chatMessage.core.fromName
color: DefaultStyle.main2_500main
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
}
}
Button {
id: reactionsButton
visible: reactionList.count > 0
anchors.top: chatBubble.bottom
Binding {
target: reactionsButton
when: !mainItem.isRemoteMessage
property: "anchors.left"
value: chatBubble.left
}
Binding {
target: reactionsButton
when: mainItem.isRemoteMessage
property: "anchors.right"
value: chatBubble.right
}
anchors.topMargin: Math.round(-6 * DefaultStyle.dp)
topPadding: Math.round(8 * DefaultStyle.dp)
bottomPadding: Math.round(8 * DefaultStyle.dp)
leftPadding: Math.round(8 * DefaultStyle.dp)
rightPadding: Math.round(8 * DefaultStyle.dp)
background: Rectangle {
color: DefaultStyle.grey_100
border.color: DefaultStyle.grey_0
border.width: Math.round(2 * DefaultStyle.dp)
radius: Math.round(20 * DefaultStyle.dp)
}
contentItem: RowLayout {
spacing: Math.round(6 * DefaultStyle.dp)
Repeater {
id: reactionList
model: mainItem.chatMessage ? mainItem.chatMessage.core.reactionsSingleton : []
delegate: RowLayout {
spacing: Math.round(3 * DefaultStyle.dp)
Item {
// Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
Layout.preferredHeight: childrenRect.height
Layout.preferredWidth: childrenRect.width
Control.Control {
id: chatBubble
spacing: Math.round(2 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(6 * DefaultStyle.dp)
leftPadding: Math.round(12 * DefaultStyle.dp)
rightPadding: Math.round(12 * DefaultStyle.dp)
width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth)
MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble.
id: defaultMouseArea
// visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse)
}
background: Item {
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: mainItem.backgroundColor
radius: Math.round(16 * DefaultStyle.dp)
}
Rectangle {
visible: mainItem.isFirstMessage && mainItem.isRemoteMessage
anchors.top: parent.top
anchors.left: parent.left
width: Math.round(parent.width / 4)
height: Math.round(parent.height / 4)
color: mainItem.backgroundColor
}
Rectangle {
visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage
anchors.bottom: parent.bottom
anchors.right: parent.right
width: Math.round(parent.width / 4)
height: Math.round(parent.height / 4)
color: mainItem.backgroundColor
}
}
contentItem: ColumnLayout {
spacing: Math.round(5 * DefaultStyle.dp)
ChatMessageContent {
id: chatBubbleContent
Layout.fillWidth: true
Layout.fillHeight: true
chatMessageGui: mainItem.chatMessage
onMouseEvent: (event) => {
mainItem.handleDefaultMouseEvent(event)
}
}
RowLayout {
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
Text {
text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData.body)
textFormat: Text.RichText
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM")
color: DefaultStyle.main2_500main
font {
pixelSize: Math.round(15 * DefaultStyle.dp)
weight: Math.round(400 * DefaultStyle.dp)
pixelSize: Typography.p3.pixelSize
weight: Typography.p3.weight
}
}
Text {
visible: modelData.count > 1
text: modelData.count
verticalAlignment: Text.AlignBottom
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
EffectImage {
visible: !mainItem.isRemoteMessage
Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0
Layout.preferredHeight: 14 * DefaultStyle.dp
colorizationColor: DefaultStyle.main1_500_main
imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered
? AppIcons.envelope
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser
? AppIcons.check
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered
? AppIcons.warningCircle
: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed
? AppIcons.checks
: ""
}
}
}
}
Button {
id: reactionsButton
visible: reactionList.count > 0
anchors.top: chatBubble.bottom
Binding {
target: reactionsButton
when: !mainItem.isRemoteMessage
property: "anchors.left"
value: chatBubble.left
}
Binding {
target: reactionsButton
when: mainItem.isRemoteMessage
property: "anchors.right"
value: chatBubble.right
}
anchors.topMargin: Math.round(-6 * DefaultStyle.dp)
topPadding: Math.round(8 * DefaultStyle.dp)
bottomPadding: Math.round(8 * DefaultStyle.dp)
leftPadding: Math.round(8 * DefaultStyle.dp)
rightPadding: Math.round(8 * DefaultStyle.dp)
background: Rectangle {
color: DefaultStyle.grey_100
border.color: DefaultStyle.grey_0
border.width: Math.round(2 * DefaultStyle.dp)
radius: Math.round(20 * DefaultStyle.dp)
}
contentItem: RowLayout {
spacing: Math.round(6 * DefaultStyle.dp)
Repeater {
id: reactionList
model: mainItem.chatMessage ? mainItem.chatMessage.core.reactionsSingleton : []
delegate: RowLayout {
spacing: Math.round(3 * DefaultStyle.dp)
Text {
text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData.body)
textFormat: Text.RichText
font {
pixelSize: Math.round(15 * DefaultStyle.dp)
weight: Math.round(400 * DefaultStyle.dp)
}
}
Text {
visible: modelData.count > 1
text: modelData.count
verticalAlignment: Text.AlignBottom
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
}
}
}
}
@ -262,14 +231,17 @@ Control.Control {
spacing: 0
IconLabelButton {
inverseLayout: true
//: "Copy"
text: qsTr("chat_message_copy")
text: chatBubbleContent.selectedText != ""
//: "Copy selection"
? qsTr("chat_message_copy_selection")
//: "Copy"
: qsTr("chat_message_copy")
icon.source: AppIcons.copy
// spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: 45 * DefaultStyle.dp
onClicked: {
var success = UtilsCpp.copyToClipboard(mainItem.chatMessage.core.text)
var success = UtilsCpp.copyToClipboard(chatBubbleContent.selectedText != "" ? chatBubbleContent.selectedText : mainItem.chatMessage.core.text)
//: Copied
if (success) UtilsCpp.showInformationPopup(qsTr("chat_message_copied_to_clipboard_title"),
//: "to clipboard"

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Controls.Basic as Control
import Linphone
// =============================================================================
// Simple content display without reply and forward. These modules need to be splitted because of cyclic dependencies.
// See ChatFullContent
ColumnLayout {
id: mainItem
property ChatMessageGui chatMessageGui: null
signal isFileHoveringChanged(bool isFileHovering)
signal lastSelectedTextChanged(string selectedText)
// signal conferenceIcsCopied()
signal mouseEvent(MouseEvent event)
property string selectedText
property color textColor
property int fileBorderWidth : 0
spacing: Math.round(5 * DefaultStyle.dp)
property int padding: Math.round(10 * DefaultStyle.dp)
// VOICE MESSAGES
// ListView {
// id: messagesVoicesList
// width: parent.width-2*mainItem.padding
// visible: count > 0
// spacing: 0
// clip: false
// model: ChatMessageContentProxy {
// filterType: ChatMessageContentProxy.FilterContentType.Voice
// chatMessageGui: mainItem.chatMessageGui
// }
// height: contentHeight
// boundsBehavior: Flickable.StopAtBounds
// interactive: false
// function updateBestWidth(){
// var newWidth = mainItem.updateListBestWidth(messagesVoicesList)
// mainItem.voicesCount = newWidth[0]
// mainItem.voicesBestWidth = newWidth[1]
// }
// delegate: ChatAudioMessage{
// id: audioMessage
// contentModel: $modelData
// visible: contentModel
// z: 1
// Component.onCompleted: messagesVoicesList.updateBestWidth()
// }
// Component.onCompleted: messagesVoicesList.updateBestWidth
// }
// CONFERENCE
Repeater {
id: conferenceList
visible: count > 0
model: ChatMessageContentProxy{
filterType: ChatMessageContentProxy.FilterContentType.Conference
chatMessageGui: mainItem.chatMessageGui
}
delegate: ChatMessageInvitationBubble {
Layout.fillWidth: true
conferenceInfoGui: modelData.core.conferenceInfo
// width: conferenceList.width
onMouseEvent: (event) => mainItem.mouseEvent(event)
}
}
// FILES
ChatFilesGridLayout {
id: messageFilesList
visible: itemCount > 0
Layout.fillWidth: true
maxWidth: Math.round(115*3 * DefaultStyle.dp)
Layout.fillHeight: true
// Layout.preferredHeight: contentHeight
chatMessageGui: mainItem.chatMessageGui
// onIsHoveringFileChanged: mainItem.isHoveringFile = isHoveringFile
onIsHoveringFileChanged: mainItem.isFileHoveringChanged(isHoveringFile)
// borderWidth: mainItem.fileBorderWidth
// property int availableSection: mainItem.availableWidth / mainItem.filesBestWidth
// property int bestFitSection: mainItem.bestWidth / mainItem.filesBestWidth
// columns: Math.max(1, Math.min(availableSection , bestFitSection))
// columnSpacing: 0
// rowSpacing: ChatStyle.entry.message.file.spacing
}
// TEXTS
Repeater {
id: messagesTextsList
visible: count > 0
model: ChatMessageContentProxy {
filterType: ChatMessageContentProxy.FilterContentType.Text
chatMessageGui: mainItem.chatMessageGui
}
delegate: ChatTextContent {
Layout.fillWidth: true
// height: implicitHeight
contentGui: modelData
onLastTextSelectedChanged: mainItem.selectedText = selectedText
// onRightClicked: mainItem.rightClicked()
}
}
}

View file

@ -16,7 +16,7 @@ Rectangle {
clip: true
antialiasing: true
property var conferenceInfoGui: ConferenceInfoGui
property ConferenceInfoGui conferenceInfoGui
property var conferenceInfo: conferenceInfoGui?.core
property string timeRangeText: ""
property bool linkHovered: false

View file

@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import Linphone
import UtilsCpp
// TODO : into Loader
// =============================================================================
TextEdit {
id: message
property ChatMessageContentGui contentGui
property string lastTextSelected : ''
color: DefaultStyle.main2_700
font {
pixelSize: (contentGui && UtilsCpp.isOnlyEmojis(contentGui.core.text)) ? Typography.h1.pixelSize : Typography.p1.pixelSize
weight: Typography.p1.weight
}
// property int removeWarningFromBindingLoop : implicitWidth // Just a dummy variable to remove meaningless binding loop on implicitWidth
visible: contentGui && contentGui.core.isText
textMargin: 0
readOnly: true
selectByMouse: true
text: visible ? UtilsCpp.encodeTextToQmlRichFormat(contentGui.core.utf8Text)
: ''
textFormat: Text.RichText // To supports links and imgs.
wrapMode: TextEdit.Wrap
onLinkActivated: (link) => {
if (link.startsWith('sip'))
UtilsCpp.createCall(link)
else
Qt.openUrlExternally(link)
}
onSelectedTextChanged:{
if(selectedText != '') lastTextSelected = selectedText
// else {
// if(mouseArea.keepLastSelection) {
// mouseArea.keepLastSelection = false
// }
// }
}
onActiveFocusChanged: {
if(activeFocus) {
lastTextSelected = ''
}
else mouseArea.keepLastSelection = false
// deselect()
}
MouseArea {
id: mouseArea
property bool keepLastSelection: false
property int lastStartSelection:0
property int lastEndSelection:0
anchors.fill: parent
propagateComposedEvents: true
hoverEnabled: true
scrollGestureEnabled: false
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
acceptedButtons: Qt.LeftButton
onPressed: (mouse) => {
// if(!keepLastSelection) {
// lastStartSelection = parent.selectionStart
// lastEndSelection = parent.selectionEnd
// }
keepLastSelection = true
mouse.accepted = false
}
}
}

View file

@ -0,0 +1,294 @@
import QtQuick
import QtQuick.Controls as Control
import QtQuick.Layouts
import QtMultimedia
import Linphone
import UtilsCpp
import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle
import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils
// =============================================================================
Item {
id: mainItem
property ChatMessageContentGui contentGui
property string thumbnail: contentGui && contentGui.core.thumbnail
property string name: contentGui && contentGui.core.name
property string filePath: contentGui && contentGui.core.filePath
property bool active: true
property real animationScale : FileViewStyle.animation.to
property alias imageScale: thumbnailProvider.scale
property bool wasDownloaded: contentGui && contentGui.core.wasDownloaded
property bool isAnimatedImage : contentGui && contentGui.core.wasDownloaded && UtilsCpp.isAnimatedImage(filePath)
property bool haveThumbnail: contentGui && UtilsCpp.canHaveThumbnail(filePath)
property int fileSize: contentGui ? contentGui.core.fileSize : 0
property bool isTransferring
Connections {
enabled: contentGui
target: contentGui.core
function onMsgStateChanged(state) {
isTransferring = state === LinphoneEnums.ChatMessageState.StateFileTransferInProgress
|| state === LinphoneEnums.ChatMessageState.StateInProgress
}
}
property bool isHovering: thumbnailProvider.state == 'hovered'
property bool isOutgoing: false
MouseArea {
hoverEnabled: true
propagateComposedEvents: true
// Changing of cursor seems not to work with the Loader
// Use override cursor for this case
onEntered: {
UtilsCpp.setGlobalCursor(Qt.PointingHandCursor)
thumbnailProvider.state = 'hovered'
}
onExited: {
UtilsCpp.restoreGlobalCursor()
thumbnailProvider.state = ''
}
anchors.fill: parent
onClicked: (mouse) => {
mouse.accepted = false
if(mainItem.isTransferring) {
mainItem.contentGui.core.lCancelDownloadFile()
mouse.accepted = true
}
else if(!mainItem.contentGui.core.wasDownloaded) {
mouse.accepted = true
thumbnailProvider.state = ''
mainItem.contentGui.core.lDownloadFile()
} else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) {
mouse.accepted = true
// if(SettingsModel.isVfsEncrypted){
// window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), {
// contentGui: mainItem.contentGui,
// }, function (status) {
// })
// }else
mainItem.contentGui.core.lOpenFile()
} else if (mainItem.contentGui) {
mouse.accepted = true
thumbnailProvider.state = ''
mainItem.contentGui.core.lOpenFile(true)// Show directory
}
}
}
// ---------------------------------------------------------------------
// Thumbnail
// ---------------------------------------------------------------------
Component {
id: thumbnailImage
Item {
id: thumbnailSource
property bool isVideo: UtilsCpp.isVideo(mainItem.filePath)
property bool isImage: UtilsCpp.isImage(mainItem.filePath)
property bool isPdf: UtilsCpp.isPdf(mainItem.filePath)
Image {
anchors.fill: parent
visible: thumbnailSource.isPdf
source: AppIcons.filePdf
sourceSize.width: mainItem.width
sourceSize.height: mainItem.height
fillMode: Image.PreserveAspectFit
}
Image {
visible: thumbnailSource.isImage
mipmap: false//SettingsModel.mipmapEnabled
source: mainItem.thumbnail
sourceSize.width: mainItem.width
sourceSize.height: mainItem.height
autoTransform: true
fillMode: Image.PreserveAspectCrop
anchors.fill: parent
Image {
anchors.fill: parent
visible: parent.status !== Image.Ready
source: AppIcons.fileImage
sourceSize.width: mainItem.width
sourceSize.height: mainItem.height
fillMode: Image.PreserveAspectFit
}
}
Rectangle {
visible: thumbnailSource.isVideo
color: DefaultStyle.grey_1000
anchors.fill: parent
Video {
id: videoThumbnail
anchors.fill: parent
position: 100
source: "file:///" + mainItem.filePath
fillMode: playbackState === MediaPlayer.PlayingState ? VideoOutput.PreserveAspectFit : VideoOutput.PreserveAspectCrop
MouseArea {
propagateComposedEvents: false
enabled: videoThumbnail.visible
anchors.fill: parent
hoverEnabled: false
acceptedButtons: Qt.LeftButton
onClicked: (mouse) => {
mouse.accepted = true
videoThumbnail.playbackState === MediaPlayer.PlayingState ? videoThumbnail.pause() : videoThumbnail.play()
}
}
EffectImage {
anchors.centerIn: parent
visible: videoThumbnail.playbackState !== MediaPlayer.PlayingState
width: Math.round(24 * DefaultStyle.dp)
height: Math.round(24 * DefaultStyle.dp)
imageSource: AppIcons.playFill
colorizationColor: DefaultStyle.main2_0
}
Text {
z: parent.z + 1
RectangleTest{anchors.fill: parent}
property int timeDisplayed: videoThumbnail.playbackState === MediaPlayer.PlayingState ? videoThumbnail.position : videoThumbnail.duration
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.bottomMargin: Math.round(6 * DefaultStyle.dp)
anchors.leftMargin: Math.round(6 * DefaultStyle.dp)
text: UtilsCpp.formatDuration(timeDisplayed)
color: DefaultStyle.grey_0
font {
pixelSize: Typography.d1.pixelSize
weight: Typography.d1.weight
}
}
}
}
}
}
Component {
id: animatedImage
AnimatedImage {
id: animatedImageSource
mipmap: false//SettingsModel.mipmapEnabled
source: 'file:/'+ mainItem.filePath
autoTransform: true
fillMode: Image.PreserveAspectFit
}
}
// ---------------------------------------------------------------------
// Extension
// ---------------------------------------------------------------------
Component {
id: defaultFileView
Control.Control {
id: defaultView
leftPadding: Math.round(4 * DefaultStyle.dp)
rightPadding: Math.round(4 * DefaultStyle.dp)
topPadding: Math.round(23 * DefaultStyle.dp)
bottomPadding: Math.round(4 * DefaultStyle.dp)
hoverEnabled: false
background: Rectangle {
anchors.fill: parent
color: FileViewStyle.extension.background.color
radius: FileViewStyle.extension.radius
Rectangle {
color: DefaultStyle.main2_200
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: Math.round(23 * DefaultStyle.dp)
EffectImage {
anchors.centerIn: parent
imageSource: contentGui
? UtilsCpp.isImage(mainItem.filePath)
? AppIcons.fileImage
: UtilsCpp.isPdf(mainItem.filePath)
? AppIcons.filePdf
: UtilsCpp.isText(mainItem.filePath)
? AppIcons.fileText
: AppIcons.file
: ''
imageWidth: Math.round(14 * DefaultStyle.dp)
imageHeight: Math.round(14 * DefaultStyle.dp)
colorizationColor: DefaultStyle.main2_600
}
}
}
contentItem: Item {
Text {
id: fileName
visible: !progressBar.visible
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
// visible: mainItem.contentGui && !mainItem.isAnimatedImage
font.pixelSize: Typography.f1.pixelSize
font.weight: Typography.f1l.weight
wrapMode: Text.WrapAnywhere
maximumLineCount: 2
text: mainItem.name
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
Text {
id: fileSizeText
visible: !progressBar.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
text: Utils.formatSize(mainItem.fileSize)
font.pixelSize: Typography.f1l.pixelSize
font.weight: Typography.f1l.weight
}
RoundProgressBar {
id: progressBar
anchors.centerIn: parent
to: 100
value: mainItem.contentGui ? (mainItem.fileSize>0 ? Math.floor(100 * mainItem.contentGui.core.fileOffset / mainItem.fileSize) : 0) : to
visible: mainItem.isTransferring && value != 0
/* Change format? Current is %
text: if(mainRow.contentGui){
var mainItem.fileSize = Utils.formatSize(mainRow.contentGui.core.mainItem.fileSize)
return progressBar.visible
? Utils.formatSize(mainRow.contentGui.core.fileOffset) + '/' + mainItem.fileSize
: mainItem.fileSize
}else
return ''
*/
}
Rectangle {
visible: thumbnailProvider.state === 'hovered' && mainItem.contentGui && (/*!mainItem.isOutgoing &&*/ !mainItem.contentGui.core.wasDownloaded)
color: DefaultStyle.grey_0
opacity: 0.5
anchors.fill: parent
}
EffectImage {
visible: thumbnailProvider.state === 'hovered' && mainItem.contentGui && (/*!mainItem.isOutgoing &&*/ !mainItem.contentGui.core.wasDownloaded)
anchors.centerIn: parent
imageSource: AppIcons.download
width: Math.round(24 * DefaultStyle.dp)
height: Math.round(24 * DefaultStyle.dp)
colorizationColor: DefaultStyle.main2_600
}
}
}
}
Loader {
id: thumbnailProvider
anchors.fill: parent
sourceComponent: mainItem.contentGui
? mainItem.isAnimatedImage
? animatedImage
: mainItem.haveThumbnail
? thumbnailImage
: defaultFileView
: undefined
states: State {
name: 'hovered'
}
}
}

View file

@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
import Linphone
ProgressBar{
id: mainItem
property string text: value + '%'
implicitHeight: 35
implicitWidth: 35
to: 100
value: 0
background: Item {}
Timer{
id: animationTest
repeat: true
onTriggered: value = (value + 1) % to
interval: 5
}
contentItem: Item{
Shape {
id: shape
anchors.fill: parent
anchors.margins: Math.round(2 * DefaultStyle.dp)
property real progressionRadius : Math.min(shape.width / 2, shape.height / 2) - Math.round(3 * DefaultStyle.dp) / 2
layer.enabled: true
layer.samples: 8
layer.smooth: true
vendorExtensionsEnabled: false
ShapePath {
id: pathDial
strokeColor: DefaultStyle.main1_100
fillColor: 'transparent'
strokeWidth: Math.round(3 * DefaultStyle.dp)
capStyle: Qt.RoundCap
PathAngleArc {
radiusX: shape.progressionRadius
radiusY: shape.progressionRadius
centerX: shape.width / 2
centerY: shape.height / 2
startAngle: -90 // top start
sweepAngle: 360
}
}
ShapePath {
id: pathProgress
strokeColor: DefaultStyle.main1_500_main
fillColor: 'transparent'
strokeWidth: Math.round(3 * DefaultStyle.dp)
capStyle: Qt.RoundCap
PathAngleArc {
radiusX: shape.progressionRadius
radiusY: shape.progressionRadius
centerX: shape.width / 2
centerY: shape.height / 2
startAngle: -90 // top start
sweepAngle: (360/ mainItem.to * mainItem.value)
}
}
}
Text{
anchors.centerIn: parent
text: mainItem.text
color: DefaultStyle.main1_500_main
font.pixelSize: Typography.p4.pixelSize
font.weight: Typography.p2.weight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}

View file

@ -0,0 +1,223 @@
import QtQuick
import QtQuick.Controls.Basic as Control
import QtQuick.Layouts
import Linphone
import UtilsCpp
import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle
import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils
Control.Control {
id: mainItem
property alias placeholderText: sendingTextArea.placeholderText
property alias text: sendingTextArea.text
property alias textArea: sendingTextArea
property alias cursorPosition: sendingTextArea.cursorPosition
property alias emojiPickerButtonChecked: emojiPickerButton.checked
property bool dropEnabled: true
property string dropDisabledReason
property bool isEphemeral : false
property bool emojiVisible: false
// ---------------------------------------------------------------------------
signal dropped (var files)
signal validText (string text)
signal sendText()
signal audioRecordRequest()
signal emojiClicked()
signal composing()
// ---------------------------------------------------------------------------
function _emitFiles (files) {
// Filtering files, other urls are forbidden.
files = files.reduce(function (files, file) {
console.log("dropping", file.toString())
if (file.toString().startsWith("file:")) {
files.push(Utils.getSystemPathFromUri(file))
}
return files
}, [])
if (files.length > 0) {
dropped(files)
}
}
// width: mainItem.implicitWidth
// height: mainItem.height
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(24 * DefaultStyle.dp)
bottomPadding: Math.round(16 * DefaultStyle.dp)
background: Rectangle {
anchors.fill: parent
color: DefaultStyle.grey_100
MediumButton {
id: expandButton
anchors.top: parent.top
anchors.topMargin: Math.round(4 * DefaultStyle.dp)
anchors.horizontalCenter: parent.horizontalCenter
style: ButtonStyle.noBackgroundOrange
icon.source: checked ? AppIcons.downArrow : AppIcons.upArrow
checkable: true
}
}
contentItem: RowLayout {
spacing: Math.round(20 * DefaultStyle.dp)
RowLayout {
spacing: Math.round(16 * DefaultStyle.dp)
BigButton {
id: emojiPickerButton
style: ButtonStyle.noBackground
checkable: true
icon.source: checked ? AppIcons.closeX : AppIcons.smiley
}
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.paperclip
onClicked: {
console.log("TODO : open explorer to attach file")
}
}
Control.Control {
Layout.fillWidth: true
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(15 * DefaultStyle.dp)
bottomPadding: Math.round(15 * DefaultStyle.dp)
background: Rectangle {
id: inputBackground
anchors.fill: parent
radius: Math.round(35 * DefaultStyle.dp)
color: DefaultStyle.grey_0
MouseArea {
anchors.fill: parent
onPressed: sendingTextArea.forceActiveFocus()
cursorShape: Qt.IBeamCursor
}
}
contentItem: RowLayout {
Flickable {
id: sendingAreaFlickable
Layout.fillWidth: true
Layout.preferredWidth: parent.width - stackButton.width
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight)
Binding {
target: sendingAreaFlickable
when: expandButton.checked
property: "Layout.preferredHeight"
value: Math.round(250 * DefaultStyle.dp)
restoreMode: Binding.RestoreBindingOrValue
}
Layout.fillHeight: true
contentHeight: sendingTextArea.contentHeight
contentWidth: width
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY+height <= r.y+r.height)
contentY = r.y+r.height-height;
}
TextArea {
id: sendingTextArea
width: sendingAreaFlickable.width
height: sendingAreaFlickable.height
textFormat: TextEdit.AutoText
//: Say something : placeholder text for sending message text area
placeholderText: qsTr("chat_view_send_area_placeholder_text")
placeholderTextColor: DefaultStyle.main2_400
color: DefaultStyle.main2_700
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
}
onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle)
wrapMode: TextEdit.WordWrap
Keys.onPressed: (event) => {
if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return)
&& (!(event.modifier & Qt.ShiftModifier))) {
mainItem.sendText()
sendingTextArea.clear()
event.accepted = true
}
}
}
}
StackLayout {
id: stackButton
currentIndex: sendingTextArea.text.length === 0 ? 0 : 1
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.microphone
onClicked: {
console.log("TODO : go to record message")
}
}
BigButton {
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
onClicked: {
mainItem.sendText()
sendingTextArea.clear()
}
}
}
}
}
}
}
Rectangle {
id: hoverContent
anchors.fill: parent
color: DefaultStyle.main2_0
visible: false
radius: Math.round(20 * DefaultStyle.dp)
EffectImage {
anchors.centerIn: parent
imageSource: AppIcons.filePlus
width: Math.round(37 * DefaultStyle.dp)
height: Math.round(37 * DefaultStyle.dp)
colorizationColor: DefaultStyle.main2_500main
}
DashRectangle {
x: parent.x
y: parent.y
radius: hoverContent.radius
color: DefaultStyle.main2_500main
width: parent.width
height: parent.height
}
}
DropArea {
anchors.fill: parent
keys: [ 'text/uri-list' ]
visible: mainItem.dropEnabled
onDropped: (drop) => {
state = ''
if (drop.hasUrls) {
_emitFiles(drop.urls)
}
}
onEntered: state = 'hover'
onExited: state = ''
states: State {
name: 'hover'
PropertyChanges { target: hoverContent; visible: true }
}
}
}

View file

@ -76,210 +76,164 @@ RowLayout {
}
]
content: [
ChatMessagesListView {
id: chatMessagesListView
clip: true
height: contentHeight
backgroundColor: splitPanel.panelColor
width: parent.width - anchors.leftMargin - anchors.rightMargin
chat: mainItem.chat
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: messageSender.top
anchors.leftMargin: Math.round(18 * DefaultStyle.dp)
anchors.rightMargin: Math.round(18 * DefaultStyle.dp)
Control.ScrollBar.vertical: scrollbar
Popup {
id: emojiPickerPopup
y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp)
x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp)
width: Math.round(393 * DefaultStyle.dp)
height: Math.round(291 * DefaultStyle.dp)
visible: emojiPickerButton.checked
closePolicy: Popup.CloseOnPressOutside
onClosed: emojiPickerButton.checked = false
padding: 10 * DefaultStyle.dp
background: Item {
anchors.fill: parent
Rectangle {
id: buttonBackground
anchors.fill: parent
color: DefaultStyle.grey_0
radius: Math.round(20 * DefaultStyle.dp)
}
MultiEffect {
anchors.fill: buttonBackground
source: buttonBackground
shadowEnabled: true
shadowColor: DefaultStyle.grey_1000
shadowBlur: 0.1
shadowOpacity: 0.5
}
}
contentItem: EmojiPicker {
id: emojiPicker
editor: sendingTextArea
}
}
},
ScrollBar {
id: scrollbar
visible: chatMessagesListView.contentHeight > parent.height
active: visible
anchors.top: chatMessagesListView.top
anchors.bottom: chatMessagesListView.bottom
anchors.right: parent.right
anchors.rightMargin: Math.round(5 * DefaultStyle.dp)
policy: Control.ScrollBar.AsNeeded
},
Control.Control {
id: messageSender
visible: !mainItem.chat.core.isReadOnly
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(24 * DefaultStyle.dp)
bottomPadding: Math.round(16 * DefaultStyle.dp)
background: Rectangle {
content: ColumnLayout {
spacing: 0
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ChatMessagesListView {
id: chatMessagesListView
clip: true
height: contentHeight
backgroundColor: splitPanel.panelColor
width: parent.width - anchors.leftMargin - anchors.rightMargin
chat: mainItem.chat
anchors.fill: parent
color: DefaultStyle.grey_100
MediumButton {
id: expandButton
anchors.top: parent.top
anchors.topMargin: Math.round(4 * DefaultStyle.dp)
anchors.horizontalCenter: parent.horizontalCenter
style: ButtonStyle.noBackgroundOrange
icon.source: checked ? AppIcons.downArrow : AppIcons.upArrow
checkable: true
anchors.leftMargin: Math.round(18 * DefaultStyle.dp)
anchors.rightMargin: Math.round(18 * DefaultStyle.dp)
Control.ScrollBar.vertical: scrollbar
Popup {
id: emojiPickerPopup
y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp)
x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp)
width: Math.round(393 * DefaultStyle.dp)
height: Math.round(291 * DefaultStyle.dp)
visible: messageSender.emojiPickerButtonChecked
closePolicy: Popup.CloseOnPressOutside
onClosed: messageSender.emojiPickerButtonChecked = false
padding: 10 * DefaultStyle.dp
background: Item {
anchors.fill: parent
Rectangle {
id: buttonBackground
anchors.fill: parent
color: DefaultStyle.grey_0
radius: Math.round(20 * DefaultStyle.dp)
}
MultiEffect {
anchors.fill: buttonBackground
source: buttonBackground
shadowEnabled: true
shadowColor: DefaultStyle.grey_1000
shadowBlur: 0.1
shadowOpacity: 0.5
}
}
contentItem: EmojiPicker {
id: emojiPicker
editor: messageSender.textArea
}
}
}
contentItem: RowLayout {
spacing: Math.round(20 * DefaultStyle.dp)
RowLayout {
spacing: Math.round(16 * DefaultStyle.dp)
BigButton {
id: emojiPickerButton
style: ButtonStyle.noBackground
checkable: true
icon.source: checked ? AppIcons.closeX : AppIcons.smiley
}
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.paperclip
onClicked: {
console.log("TODO : open explorer to attach file")
}
}
Control.Control {
Layout.fillWidth: true
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(15 * DefaultStyle.dp)
bottomPadding: Math.round(15 * DefaultStyle.dp)
background: Rectangle {
id: inputBackground
anchors.fill: parent
radius: Math.round(35 * DefaultStyle.dp)
color: DefaultStyle.grey_0
MouseArea {
anchors.fill: parent
onPressed: sendingTextArea.forceActiveFocus()
cursorShape: Qt.IBeamCursor
}
}
contentItem: RowLayout {
Flickable {
id: sendingAreaFlickable
Layout.fillWidth: true
Layout.preferredWidth: parent.width - stackButton.width
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight)
Binding {
target: sendingAreaFlickable
when: expandButton.checked
property: "Layout.preferredHeight"
value: Math.round(250 * DefaultStyle.dp)
restoreMode: Binding.RestoreBindingOrValue
}
Layout.fillHeight: true
contentHeight: sendingTextArea.contentHeight
contentWidth: width
ScrollBar {
id: scrollbar
visible: chatMessagesListView.contentHeight > parent.height
active: visible
anchors.top: chatMessagesListView.top
anchors.bottom: chatMessagesListView.bottom
anchors.right: parent.right
anchors.rightMargin: Math.round(5 * DefaultStyle.dp)
policy: Control.ScrollBar.AsNeeded
}
}
Control.Control {
id: selectedFilesArea
visible: selectedFiles.count > 0
Layout.fillWidth: true
Layout.preferredHeight: Math.round(104 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(12 * DefaultStyle.dp)
leftPadding: Math.round(19 * DefaultStyle.dp)
rightPadding: Math.round(19 * DefaultStyle.dp)
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY+height <= r.y+r.height)
contentY = r.y+r.height-height;
}
function addFile(path) {
contents.addFile(path)
}
TextArea {
id: sendingTextArea
width: sendingAreaFlickable.width
height: sendingAreaFlickable.height
textFormat: TextEdit.AutoText
//: Say something : placeholder text for sending message text area
placeholderText: qsTr("chat_view_send_area_placeholder_text")
placeholderTextColor: DefaultStyle.main2_400
color: DefaultStyle.main2_700
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
}
onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle)
wrapMode: TextEdit.WordWrap
Component.onCompleted: {
if (mainItem.chat) text = mainItem.chat.core.sendingText
}
onTextChanged: {
if (text !== "" && mainItem.chat.core.composingName !== "") {
mainItem.chat.core.lCompose()
}
mainItem.chat.core.sendingText = text
}
Keys.onPressed: (event) => {
event.accepted = false
if (UtilsCpp.isEmptyMessage(sendingTextArea.text)) return
if (!(event.modifiers & Qt.ShiftModifier) && (event.key == Qt.Key_Return || event.key == Qt.Key_Enter)) {
mainItem.chat.core.lSendTextMessage(sendingTextArea.text)
sendingTextArea.clear()
event.accepted = true
}
}
}
}
StackLayout {
id: stackButton
currentIndex: sendingTextArea.text.length === 0 ? 0 : 1
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.microphone
onClicked: {
console.log("TODO : go to record message")
}
}
BigButton {
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
onClicked: {
mainItem.chat.core.lSendTextMessage(sendingTextArea.text)
sendingTextArea.clear()
}
}
}
}
Button {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: selectedFilesArea.topPadding
anchors.rightMargin: selectedFilesArea.rightPadding
icon.source: AppIcons.closeX
style: ButtonStyle.noBackground
onClicked: {
contents.clear()
}
}
background: Item{
anchors.fill: parent
Rectangle {
color: DefaultStyle.grey_0
border.color: DefaultStyle.main2_100
border.width: Math.round(2 * DefaultStyle.dp)
radius: Math.round(20 * DefaultStyle.dp)
height: parent.height / 2
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 2 * parent.height / 3
}
}
contentItem: ListView {
id: selectedFiles
orientation: ListView.Horizontal
spacing: Math.round(16 * DefaultStyle.dp)
model: ChatMessageContentProxy {
id: contents
filterType: ChatMessageContentProxy.FilterContentType.File
}
delegate: Item {
width: Math.round(80 * DefaultStyle.dp)
height: Math.round(80 * DefaultStyle.dp)
FileView {
contentGui: modelData
anchors.left: parent.left
anchors.bottom: parent.bottom
width: Math.round(69 * DefaultStyle.dp)
height: Math.round(69 * DefaultStyle.dp)
}
RoundButton {
icon.source: AppIcons.closeX
icon.width: Math.round(12 * DefaultStyle.dp)
icon.height: Math.round(12 * DefaultStyle.dp)
anchors.top: parent.top
anchors.right: parent.right
style: ButtonStyle.numericPad
shadowEnabled: true
padding: Math.round(3 * DefaultStyle.dp)
onClicked: contents.removeContent(modelData)
}
}
}
}
]
ChatDroppableTextArea {
id: messageSender
visible: !mainItem.chat.core.isReadOnly
Layout.fillWidth: true
Layout.preferredHeight: height
Component.onCompleted: {
if (mainItem.chat) text = mainItem.chat.core.sendingText
}
onTextChanged: {
if (text !== "" && mainItem.chat.core.composingName !== "") {
mainItem.chat.core.lCompose()
}
mainItem.chat.core.sendingText = text
}
onSendText: mainItem.chat.core.lSendTextMessage(text)
onDropped: (files) => {
files.forEach(selectedFilesArea.addFile)
}
}
}
}
Rectangle {

View file

@ -75,6 +75,13 @@ QtObject {
property string avatar: "image://internal/randomAvatar.png"
property string pause: "image://internal/pause.svg"
property string play: "image://internal/play.svg"
property string playFill: "image://internal/play-fill.svg"
property string filePdf: "image://internal/file-pdf.svg"
property string fileText: "image://internal/file-text.svg"
property string fileImage: "image://internal/file-image.svg"
property string filePlus: "image://internal/file-plus.svg"
property string download: "image://internal/download-simple.svg"
property string file: "image://internal/file.svg"
property string paperclip: "image://internal/paperclip.svg"
property string paperPlaneRight: "image://internal/paper-plane-right.svg"
property string smiley: "image://internal/smiley.svg"

View file

@ -51,6 +51,7 @@ QtObject {
onDpChanged: {
console.log("Screen ratio changed", dp)
AppCpp.setScreenRatio(dp)
}
// Warning: Qt 6.8.1 (current version) and previous versions, Qt only support COLRv0 fonts. Don't try to use v1.

View file

@ -0,0 +1,73 @@
pragma Singleton
import QtQml
import Linphone
// =============================================================================
QtObject {
property string sectionName : 'FileView'
property int height: 120
property int heightbetter: 200
property int iconSize: 18
property int margins: 8
property int spacing: 8
property int width: 100
property QtObject name: QtObject{
property int pointSize: Math.round(DefaultStyle.dp * 7)
}
property QtObject download: QtObject{
property string icon: AppIcons.download
property int height: 20
property int pointSize: Math.round(DefaultStyle.dp * 8)
property int iconSize: 30
}
property QtObject thumbnailVideoIcon: QtObject {
property int iconSize: 40
property string name : 'play'
property string icon : AppIcons.playFill
}
property QtObject animation: QtObject {
property int duration: 300
property real to: 1.7
property real thumbnailTo: 2
}
property QtObject extension: QtObject {
property string icon: AppIcons.file
property string imageIcon: AppIcons.fileImage
property int iconSize: 60
property int internalSize: 37
property int radius: Math.round(5 * DefaultStyle.dp)
property QtObject background: QtObject {
property var color: DefaultStyle.grey_0
property var borderColor: DefaultStyle.grey_0
}
property QtObject text: QtObject {
property var color: DefaultStyle.grey_0
property int pointSize: Math.round(DefaultStyle.dp * 9)
}
}
property QtObject status: QtObject {
property int spacing: 4
property QtObject bar: QtObject {
property int height: 6
property int radius: 3
property QtObject background: QtObject {
property var color: DefaultStyle.grey_0
}
property QtObject contentItem: QtObject {
property var color: DefaultStyle.grey_0
}
}
}
}

View file

@ -59,7 +59,7 @@ QtObject {
weight: Math.min(Math.round(700 * DefaultStyle.dp), 1000)
})
// Text/P2 - Large Bold, reduced paragraph text
// Text/P2l - Large Bold, reduced paragraph text
property font p2l: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Math.round(14 * DefaultStyle.dp),
@ -73,7 +73,7 @@ QtObject {
weight: Math.min(Math.round(400 * DefaultStyle.dp), 1000)
})
// Text/P1 - Paragraph text
// Text/P1s - Paragraph text
property font p1s: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Math.round(13 * DefaultStyle.dp),
@ -101,4 +101,24 @@ QtObject {
weight: Math.min(Math.round(600 * DefaultStyle.dp), 1000)
})
// FileView/F1 - File View name text
property font f1: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Math.round(11 * DefaultStyle.dp),
weight: Math.min(Math.round(700 * DefaultStyle.dp), 1000)
})
// FileView/F1light - File View size text
property font f1l: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Math.round(10 * DefaultStyle.dp),
weight: Math.min(Math.round(500 * DefaultStyle.dp), 1000)
})
// FileView/F1light - Duration text
property font d1: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Math.round(8 * DefaultStyle.dp),
weight: Math.min(Math.round(600 * DefaultStyle.dp), 1000)
})
}

View file

@ -127,6 +127,28 @@
}
}
// No background light
var noBackgroundLight = {
color: {
normal: "#00000000",
hovered: "#00000000",
pressed: "#00000000",
checked: "#00000000"
},
text: {
normal: Linphone.DefaultStyle.main2_200,
hovered: Linphone.DefaultStyle.main2_300,
pressed: Linphone.DefaultStyle.main2_400,
checked: Linphone.DefaultStyle.main1_500main
},
image: {
normal: Linphone.DefaultStyle.main2_200,
hovered: Linphone.DefaultStyle.main2_300,
pressed: Linphone.DefaultStyle.main2_400,
checked: Linphone.DefaultStyle.main1_500main,
}
}
// No background
var noBackground = {
color: {