record message

auto download attached files setting
This commit is contained in:
Gaelle Braud 2025-06-13 17:16:09 +02:00
parent 6d4506c5ae
commit f82931d6c6
49 changed files with 3097 additions and 1984 deletions

View file

@ -78,6 +78,7 @@
#include "core/payload-type/PayloadTypeProxy.hpp" #include "core/payload-type/PayloadTypeProxy.hpp"
#include "core/phone-number/PhoneNumber.hpp" #include "core/phone-number/PhoneNumber.hpp"
#include "core/phone-number/PhoneNumberProxy.hpp" #include "core/phone-number/PhoneNumberProxy.hpp"
#include "core/recorder/RecorderGui.hpp"
#include "core/register/RegisterPage.hpp" #include "core/register/RegisterPage.hpp"
#include "core/screen/ScreenList.hpp" #include "core/screen/ScreenList.hpp"
#include "core/screen/ScreenProxy.hpp" #include "core/screen/ScreenProxy.hpp"
@ -686,6 +687,7 @@ void App::initCppInterfaces() {
qmlRegisterType<FPSCounter>(Constants::MainQmlUri, 1, 0, "FPSCounter"); qmlRegisterType<FPSCounter>(Constants::MainQmlUri, 1, 0, "FPSCounter");
qmlRegisterType<EmojiModel>(Constants::MainQmlUri, 1, 0, "EmojiModel"); qmlRegisterType<EmojiModel>(Constants::MainQmlUri, 1, 0, "EmojiModel");
qmlRegisterType<SoundPlayerGui>(Constants::MainQmlUri, 1, 0, "SoundPlayerGui"); qmlRegisterType<SoundPlayerGui>(Constants::MainQmlUri, 1, 0, "SoundPlayerGui");
qmlRegisterType<RecorderGui>(Constants::MainQmlUri, 1, 0, "RecorderGui");
qmlRegisterType<TimeZoneProxy>(Constants::MainQmlUri, 1, 0, "TimeZoneProxy"); qmlRegisterType<TimeZoneProxy>(Constants::MainQmlUri, 1, 0, "TimeZoneProxy");

View file

@ -86,6 +86,9 @@ list(APPEND _LINPHONEAPP_SOURCES
core/sound-player/SoundPlayerCore.cpp core/sound-player/SoundPlayerCore.cpp
core/sound-player/SoundPlayerGui.cpp core/sound-player/SoundPlayerGui.cpp
core/recorder/RecorderCore.cpp
core/recorder/RecorderGui.cpp
core/videoSource/VideoSourceDescriptorCore.cpp core/videoSource/VideoSourceDescriptorCore.cpp
core/videoSource/VideoSourceDescriptorGui.cpp core/videoSource/VideoSourceDescriptorGui.cpp

View file

@ -117,6 +117,15 @@ void ChatCore::setSelf(QSharedPointer<ChatCore> me) {
mChatModelConnection->makeConnectToCore(&ChatCore::lDeleteHistory, [this]() { mChatModelConnection->makeConnectToCore(&ChatCore::lDeleteHistory, [this]() {
mChatModelConnection->invokeToModel([this]() { mChatModel->deleteHistory(); }); mChatModelConnection->invokeToModel([this]() { mChatModel->deleteHistory(); });
}); });
mChatModelConnection->makeConnectToCore(&ChatCore::lDeleteMessage, [this](ChatMessageGui *message) {
mChatModelConnection->invokeToModel([this, core = message ? message->mCore : nullptr]() {
auto messageModel = core ? core->getModel() : nullptr;
if (messageModel) {
mChatModel->deleteMessage(messageModel->getMonitor());
}
});
});
mChatModelConnection->makeConnectToCore( mChatModelConnection->makeConnectToCore(
&ChatCore::lLeave, [this]() { mChatModelConnection->invokeToModel([this]() { mChatModel->leave(); }); }); &ChatCore::lLeave, [this]() { mChatModelConnection->invokeToModel([this]() { mChatModel->leave(); }); });
mChatModelConnection->makeConnectToModel(&ChatModel::historyDeleted, [this]() { mChatModelConnection->makeConnectToModel(&ChatModel::historyDeleted, [this]() {
@ -458,8 +467,13 @@ void ChatCore::appendEventLogsToEventLogList(QList<QSharedPointer<EventLogCore>>
void ChatCore::appendEventLogToEventLogList(QSharedPointer<EventLogCore> e) { void ChatCore::appendEventLogToEventLogList(QSharedPointer<EventLogCore> e) {
if (mEventLogList.contains(e)) return; if (mEventLogList.contains(e)) return;
mEventLogList.append(e); auto it = std::find_if(mEventLogList.begin(), mEventLogList.end(), [e](QSharedPointer<EventLogCore> event) {
emit eventsInserted({e}); return e->getEventLogId() == event->getEventLogId();
});
if (it == mEventLogList.end()) {
mEventLogList.append(e);
emit eventsInserted({e});
}
} }
void ChatCore::removeEventLogsFromEventLogList(QList<QSharedPointer<EventLogCore>> list) { void ChatCore::removeEventLogsFromEventLogList(QList<QSharedPointer<EventLogCore>> list) {

View file

@ -150,7 +150,7 @@ signals:
void meAdminChanged(); void meAdminChanged();
void participantsChanged(); void participantsChanged();
void lDeleteMessage(); void lDeleteMessage(ChatMessageGui *message);
void lDelete(); void lDelete();
void lDeleteHistory(); void lDeleteHistory();
void lMarkAsRead(); void lMarkAsRead();
@ -159,6 +159,7 @@ signals:
void lUpdateLastUpdatedTime(); void lUpdateLastUpdatedTime();
void lSendTextMessage(QString message); void lSendTextMessage(QString message);
void lSendMessage(QString message, QVariantList files); void lSendMessage(QString message, QVariantList files);
void lSendVoiceMessage();
void lCompose(); void lCompose();
void lLeave(); void lLeave();
void lSetMuted(bool muted); void lSetMuted(bool muted);

View file

@ -90,6 +90,12 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
for (auto content : chatmessage->getContents()) { for (auto content : chatmessage->getContents()) {
auto contentCore = ChatMessageContentCore::create(content, mChatMessageModel); auto contentCore = ChatMessageContentCore::create(content, mChatMessageModel);
mChatMessageContentList.push_back(contentCore); mChatMessageContentList.push_back(contentCore);
if (content->isFile() && !content->isVoiceRecording()) mHasFileContent = true;
if (content->isIcalendar()) mIsCalendarInvite = true;
if (content->isVoiceRecording()) {
mIsVoiceRecording = true;
mVoiceRecordingContent = contentCore;
}
} }
auto reac = chatmessage->getOwnReaction(); auto reac = chatmessage->getOwnReaction();
mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString(); mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString();
@ -122,11 +128,6 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mIsForward = chatmessage->isForward(); mIsForward = chatmessage->isForward();
mIsReply = chatmessage->isReply(); mIsReply = chatmessage->isReply();
for (auto &content : chatmessage->getContents()) {
if (content->isFile() && !content->isVoiceRecording()) mHasFileContent = true;
if (content->isIcalendar()) mIsCalendarInvite = true;
if (content->isVoiceRecording()) mIsVoiceRecording = true;
}
} }
ChatMessageCore::~ChatMessageCore() { ChatMessageCore::~ChatMessageCore() {
@ -156,6 +157,9 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lRemoveReaction, [this]() { mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lRemoveReaction, [this]() {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->removeReaction(); }); mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->removeReaction(); });
}); });
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lSend, [this]() {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->send(); });
});
mChatMessageModelConnection->makeConnectToModel( mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::newMessageReaction, &ChatMessageModel::newMessageReaction,
[this](const std::shared_ptr<linphone::ChatMessage> &message, [this](const std::shared_ptr<linphone::ChatMessage> &message,
@ -431,3 +435,7 @@ std::shared_ptr<ChatMessageModel> ChatMessageCore::getModel() const {
// ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const { // ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const {
// return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr; // return mConferenceInfo ? new ConferenceInfoGui(mConferenceInfo) : nullptr;
// } // }
ChatMessageContentGui *ChatMessageCore::getVoiceRecordingContent() const {
return new ChatMessageContentGui(mVoiceRecordingContent);
}

View file

@ -18,11 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef CHATMESSAGECORE_H_ #ifndef CHAT_MESSAGE_CORE_H_
#define CHATMESSAGECORE_H_ #define CHAT_MESSAGE_CORE_H_
#include "EventLogCore.hpp" #include "EventLogCore.hpp"
#include "core/chat/message/content/ChatMessageContentCore.hpp" #include "core/chat/message/content/ChatMessageContentGui.hpp"
#include "core/chat/message/content/ChatMessageContentProxy.hpp" #include "core/chat/message/content/ChatMessageContentProxy.hpp"
#include "core/conference/ConferenceInfoCore.hpp" #include "core/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp" #include "core/conference/ConferenceInfoGui.hpp"
@ -118,7 +118,7 @@ public:
void setMessageState(LinphoneEnums::ChatMessageState state); void setMessageState(LinphoneEnums::ChatMessageState state);
std::shared_ptr<ChatMessageModel> getModel() const; std::shared_ptr<ChatMessageModel> getModel() const;
// ConferenceInfoGui *getConferenceInfoGui() const; Q_INVOKABLE ChatMessageContentGui *getVoiceRecordingContent() const;
signals: signals:
void timestampChanged(QDateTime timestamp); void timestampChanged(QDateTime timestamp);
@ -136,6 +136,7 @@ signals:
void readChanged(); void readChanged();
void lSendReaction(const QString &reaction); void lSendReaction(const QString &reaction);
void lRemoveReaction(); void lRemoveReaction();
void lSend();
private: private:
DECLARE_ABSTRACT_OBJECT DECLARE_ABSTRACT_OBJECT
@ -164,10 +165,12 @@ private:
bool mIsOutgoing = false; bool mIsOutgoing = false;
LinphoneEnums::ChatMessageState mMessageState; LinphoneEnums::ChatMessageState mMessageState;
QList<QSharedPointer<ChatMessageContentCore>> mChatMessageContentList; QList<QSharedPointer<ChatMessageContentCore>> mChatMessageContentList;
// for voice recording creation message
QSharedPointer<ChatMessageContentCore> mVoiceRecordingContent;
// QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr; // QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr;
std::shared_ptr<ChatMessageModel> mChatMessageModel; std::shared_ptr<ChatMessageModel> mChatMessageModel;
QSharedPointer<SafeConnection<ChatMessageCore, ChatMessageModel>> mChatMessageModelConnection; QSharedPointer<SafeConnection<ChatMessageCore, ChatMessageModel>> mChatMessageModelConnection;
}; };
#endif // CHATMESSAGECORE_H_ #endif // CHAT_MESSAGE_CORE_H_

View file

@ -88,7 +88,7 @@ bool ChatMessageContentProxy::SortFilterList::filterAcceptsRow(int sourceRow, co
if (contentCore) { if (contentCore) {
if (mFilterType == (int)FilterContentType::Unknown) return false; if (mFilterType == (int)FilterContentType::Unknown) return false;
else if (mFilterType == (int)FilterContentType::File) { else if (mFilterType == (int)FilterContentType::File) {
return contentCore->isFile() || contentCore->isFileTransfer(); return !contentCore->isVoiceRecording() && (contentCore->isFile() || contentCore->isFileTransfer());
} else if (mFilterType == (int)FilterContentType::Text) return contentCore->isText(); } else if (mFilterType == (int)FilterContentType::Text) return contentCore->isText();
else if (mFilterType == (int)FilterContentType::Voice) return contentCore->isVoiceRecording(); else if (mFilterType == (int)FilterContentType::Voice) return contentCore->isVoiceRecording();
else if (mFilterType == (int)FilterContentType::Conference) return contentCore->isCalendar(); else if (mFilterType == (int)FilterContentType::Conference) return contentCore->isCalendar();

View file

@ -324,7 +324,6 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr<linphone::ChatRoom>
if (messages.size() > 0) { if (messages.size() > 0) {
shared_ptr<linphone::ChatMessage> message = messages.front(); shared_ptr<linphone::ChatMessage> message = messages.front();
auto receiverAccount = ToolModel::findAccount(message->getToAddress()); auto receiverAccount = ToolModel::findAccount(message->getToAddress());
if (receiverAccount) { if (receiverAccount) {
auto senderAccount = ToolModel::findAccount(message->getFromAddress()); auto senderAccount = ToolModel::findAccount(message->getFromAddress());
@ -340,7 +339,8 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr<linphone::ChatRoom>
} }
} }
if (messages.size() == 1) { // Display only sender on mono message. auto getMessage = [this, &remoteAddress, &txt](const shared_ptr<linphone::ChatMessage> &message) {
if (message->isRead()) return;
auto remoteAddr = message->getFromAddress()->clone(); auto remoteAddr = message->getFromAddress()->clone();
remoteAddr->clean(); remoteAddr->clean();
remoteAddress = Utils::coreStringToAppString(remoteAddr->asStringUriOnly()); remoteAddress = Utils::coreStringToAppString(remoteAddr->asStringUriOnly());
@ -356,9 +356,22 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr<linphone::ChatRoom>
if (txt.isEmpty() && message->hasConferenceInvitationContent()) if (txt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation received!' : Notification about receiving an invitation to a conference. //: 'Conference invitation received!' : Notification about receiving an invitation to a conference.
txt = tr("new_conference_invitation"); txt = tr("new_conference_invitation");
};
if (messages.size() == 1) { // Display only sender on mono message.
getMessage(message);
} else { } else {
//: 'New messages received!' Notification that warn the user of new messages. int unreadCount = 0;
txt = tr("new_chat_room_messages"); for (auto &message : messages) {
if (!message->isRead()) {
++unreadCount;
if (unreadCount == 1) getMessage(message);
}
}
if (unreadCount == 0) return;
if (unreadCount > 1)
//: 'New messages received!' Notification that warn the user of new messages.
txt = tr("new_chat_room_messages");
} }
auto chatCore = ChatCore::create(room); auto chatCore = ChatCore::create(room);

View file

@ -0,0 +1,150 @@
/*
* 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 <QFile>
#include "core/App.hpp"
#include "core/path/Paths.hpp"
#include "model/core/CoreModel.hpp"
#include "model/setting/SettingsModel.hpp"
#include "tool/Utils.hpp"
#include "RecorderCore.hpp"
DEFINE_ABSTRACT_OBJECT(RecorderCore)
// =============================================================================
QSharedPointer<RecorderCore> RecorderCore::create(QObject *parent) {
auto sharedPointer = QSharedPointer<RecorderCore>(new RecorderCore(), &QObject::deleteLater);
sharedPointer->setSelf(sharedPointer);
sharedPointer->moveToThread(App::getInstance()->thread());
return sharedPointer;
}
RecorderCore::RecorderCore(QObject *parent) : QObject(parent) {
App::getInstance()->mEngine->setObjectOwnership(
this, QQmlEngine::CppOwnership); // Avoid QML to destroy it when passing by Q_INVOKABLE
}
RecorderCore::~RecorderCore() {
}
void RecorderCore::buildRecorder(QSharedPointer<RecorderCore> me) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto core = CoreModel::getInstance()->getCore();
std::shared_ptr<linphone::RecorderParams> params = core->createRecorderParams();
params->setFileFormat(linphone::MediaFileFormat::Mkv);
params->setVideoCodec("");
auto recorder = core->createRecorder(params);
if (recorder) {
mDuration = recorder->getDuration();
mCaptureVolume = recorder->getCaptureVolume();
if (mRecorderModelConnection) mRecorderModelConnection->disconnect();
mRecorderModel = Utils::makeQObject_ptr<RecorderModel>(recorder);
mRecorderModelConnection = SafeConnection<RecorderCore, RecorderModel>::create(me, mRecorderModel);
mRecorderModelConnection->makeConnectToCore(&RecorderCore::lStart, [this] {
mRecorderModelConnection->invokeToModel([this] { mRecorderModel->start(); });
});
mRecorderModelConnection->makeConnectToCore(&RecorderCore::lPause, [this] {
mRecorderModelConnection->invokeToModel([this] { mRecorderModel->pause(); });
});
mRecorderModelConnection->makeConnectToCore(&RecorderCore::lStop, [this] {
mRecorderModelConnection->invokeToModel([this] { mRecorderModel->stop(); });
});
mRecorderModelConnection->makeConnectToCore(&RecorderCore::lRefresh, [this] {
mRecorderModelConnection->invokeToModel([this] {
auto duration = mRecorderModel->getDuration();
auto volume = mRecorderModel->getCaptureVolume();
mRecorderModelConnection->invokeToModel([this, duration, volume] {
setDuration(duration);
setCaptureVolume(volume);
});
});
});
mRecorderModelConnection->makeConnectToModel(&RecorderModel::stateChanged, [this] {
auto state = LinphoneEnums::fromLinphone(mRecorderModel->getState());
mRecorderModelConnection->invokeToCore([this, state] { setState(state); });
});
mRecorderModelConnection->makeConnectToModel(&RecorderModel::fileChanged, [this] {
auto file = mRecorderModel->getFile();
mRecorderModelConnection->invokeToCore([this, file] { setFile(file); });
});
mRecorderModelConnection->makeConnectToModel(&RecorderModel::errorChanged, [this](QString error) {
mRecorderModelConnection->invokeToCore([this, error] { emit errorChanged(error); });
});
emit ready();
}
}
void RecorderCore::setSelf(QSharedPointer<RecorderCore> me) {
auto coreModel = CoreModel::getInstance();
mCoreModelConnection = SafeConnection<RecorderCore, CoreModel>::create(me, coreModel);
mCoreModelConnection->invokeToModel([this, me, coreModel] { buildRecorder(me); });
}
void RecorderCore::setCaptureVolume(float volume) {
if (mCaptureVolume != volume) {
mCaptureVolume = volume;
emit captureVolumeChanged();
}
}
int RecorderCore::getDuration() const {
return mDuration;
}
void RecorderCore::setDuration(int duration) {
if (mDuration != duration) {
mDuration = duration;
emit durationChanged();
}
}
float RecorderCore::getCaptureVolume() const {
return mCaptureVolume;
}
LinphoneEnums::RecorderState RecorderCore::getState() const {
return mState;
}
void RecorderCore::setState(LinphoneEnums::RecorderState state) {
if (mState != state) {
mState = state;
emit stateChanged(state);
}
}
QString RecorderCore::getFile() const {
return mFile;
}
void RecorderCore::setFile(QString file) {
if (mFile != file) {
mFile = file;
emit fileChanged();
}
}
const std::shared_ptr<RecorderModel> &RecorderCore::getModel() const {
return mRecorderModel;
}

View file

@ -0,0 +1,82 @@
/*
* 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 RECORDER_CORE_H
#define RECORDER_CORE_H
#include "model/recorder/RecorderModel.hpp"
#include "tool/LinphoneEnums.hpp"
#include "tool/thread/SafeConnection.hpp"
#include <linphone++/linphone.hh>
// =============================================================================
class RecorderCore : public QObject, public AbstractObject {
Q_OBJECT
public:
static QSharedPointer<RecorderCore> create(QObject *parent = nullptr);
RecorderCore(QObject *parent = nullptr);
~RecorderCore();
void setSelf(QSharedPointer<RecorderCore> me);
Q_PROPERTY(LinphoneEnums::RecorderState state READ getState NOTIFY stateChanged)
Q_PROPERTY(QString file READ getFile NOTIFY fileChanged)
Q_PROPERTY(int duration READ getDuration NOTIFY durationChanged)
Q_PROPERTY(int captureVolume READ getCaptureVolume NOTIFY captureVolumeChanged)
void buildRecorder(QSharedPointer<RecorderCore> me);
int getDuration() const;
void setDuration(int duration);
float getCaptureVolume() const;
void setCaptureVolume(float volume);
LinphoneEnums::RecorderState getState() const;
void setState(LinphoneEnums::RecorderState state);
QString getFile() const;
void setFile(QString file);
const std::shared_ptr<RecorderModel> &getModel() const;
signals:
void lStart();
void lPause();
void lStop();
void lRefresh();
void stateChanged(LinphoneEnums::RecorderState state);
void fileChanged();
void durationChanged();
void captureVolumeChanged();
void errorChanged(QString error);
void ready();
private:
DECLARE_ABSTRACT_OBJECT
std::shared_ptr<RecorderModel> mRecorderModel;
QString mFile;
LinphoneEnums::RecorderState mState;
int mDuration = 0;
int mCaptureVolume = 0;
bool mIsReady = false;
QSharedPointer<SafeConnection<RecorderCore, RecorderModel>> mRecorderModelConnection;
QSharedPointer<SafeConnection<RecorderCore, CoreModel>> mCoreModelConnection;
};
#endif

View file

@ -0,0 +1,49 @@
/*
* 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 "RecorderGui.hpp"
#include "core/App.hpp"
DEFINE_ABSTRACT_OBJECT(RecorderGui)
RecorderGui::RecorderGui(QObject *parent) : QObject(parent) {
mustBeInMainThread(getClassName());
mCore = RecorderCore::create();
if (mCore) connect(mCore.get(), &RecorderCore::errorChanged, this, &RecorderGui::errorChanged);
if (mCore) connect(mCore.get(), &RecorderCore::stateChanged, this, &RecorderGui::stateChanged);
if (mCore) connect(mCore.get(), &RecorderCore::ready, this, &RecorderGui::ready);
}
RecorderGui::RecorderGui(QSharedPointer<RecorderCore> core) {
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);
mCore = core;
if (isInLinphoneThread()) moveToThread(App::getInstance()->thread());
}
LinphoneEnums::RecorderState RecorderGui::getState() const {
return mCore ? mCore->getState() : LinphoneEnums::RecorderState::Closed;
}
RecorderGui::~RecorderGui() {
mustBeInMainThread("~" + getClassName());
}
RecorderCore *RecorderGui::getCore() const {
return mCore.get();
}

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 RECORDER_GUI_H_
#define RECORDER_GUI_H_
#include "RecorderCore.hpp"
#include "tool/AbstractObject.hpp"
#include <QObject>
#include <QSharedPointer>
class RecorderGui : public QObject, public AbstractObject {
Q_OBJECT
Q_PROPERTY(RecorderCore *core READ getCore CONSTANT)
public:
RecorderGui(QObject *parent = nullptr);
RecorderGui(QSharedPointer<RecorderCore> core);
~RecorderGui();
RecorderCore *getCore() const;
LinphoneEnums::RecorderState getState() const;
QSharedPointer<RecorderCore> mCore;
signals:
void errorChanged(QString error);
void ready();
void stateChanged(LinphoneEnums::RecorderState state);
private:
DECLARE_ABSTRACT_OBJECT
};
#endif

View file

@ -49,6 +49,7 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) {
// Call // Call
mVideoEnabled = settingsModel->getVideoEnabled(); mVideoEnabled = settingsModel->getVideoEnabled();
mEchoCancellationEnabled = settingsModel->getEchoCancellationEnabled(); mEchoCancellationEnabled = settingsModel->getEchoCancellationEnabled();
mAutoDownloadReceivedFiles = settingsModel->getAutoDownloadReceivedFiles();
mAutomaticallyRecordCallsEnabled = settingsModel->getAutomaticallyRecordCallsEnabled(); mAutomaticallyRecordCallsEnabled = settingsModel->getAutomaticallyRecordCallsEnabled();
// Audio // Audio
@ -143,6 +144,7 @@ SettingsCore::SettingsCore(const SettingsCore &settingsCore) {
// Call // Call
mVideoEnabled = settingsCore.mVideoEnabled; mVideoEnabled = settingsCore.mVideoEnabled;
mEchoCancellationEnabled = settingsCore.mEchoCancellationEnabled; mEchoCancellationEnabled = settingsCore.mEchoCancellationEnabled;
mAutoDownloadReceivedFiles = settingsCore.mAutoDownloadReceivedFiles;
mAutomaticallyRecordCallsEnabled = settingsCore.mAutomaticallyRecordCallsEnabled; mAutomaticallyRecordCallsEnabled = settingsCore.mAutomaticallyRecordCallsEnabled;
// Audio // Audio
@ -233,6 +235,12 @@ void SettingsCore::setSelf(QSharedPointer<SettingsCore> me) {
mSettingsModelConnection->invokeToCore([this, enabled]() { setEchoCancellationEnabled(enabled); }); mSettingsModelConnection->invokeToCore([this, enabled]() { setEchoCancellationEnabled(enabled); });
}); });
// Auto download incoming files
mSettingsModelConnection->makeConnectToModel(
&SettingsModel::autoDownloadReceivedFilesChanged, [this](const bool enabled) {
mSettingsModelConnection->invokeToCore([this, enabled]() { setAutoDownloadReceivedFiles(enabled); });
});
// Auto recording // Auto recording
mSettingsModelConnection->makeConnectToModel( mSettingsModelConnection->makeConnectToModel(
&SettingsModel::automaticallyRecordCallsEnabledChanged, [this](const bool enabled) { &SettingsModel::automaticallyRecordCallsEnabledChanged, [this](const bool enabled) {
@ -462,6 +470,7 @@ void SettingsCore::reset(const SettingsCore &settingsCore) {
setEchoCancellationEnabled(settingsCore.mEchoCancellationEnabled); setEchoCancellationEnabled(settingsCore.mEchoCancellationEnabled);
setAutomaticallyRecordCallsEnabled(settingsCore.mAutomaticallyRecordCallsEnabled); setAutomaticallyRecordCallsEnabled(settingsCore.mAutomaticallyRecordCallsEnabled);
setAutoDownloadReceivedFiles(settingsCore.mAutoDownloadReceivedFiles);
// Audio // Audio
setCaptureDevices(settingsCore.mCaptureDevices); setCaptureDevices(settingsCore.mCaptureDevices);
setPlaybackDevices(settingsCore.mPlaybackDevices); setPlaybackDevices(settingsCore.mPlaybackDevices);
@ -576,6 +585,14 @@ void SettingsCore::setEchoCancellationEnabled(bool enabled) {
} }
} }
void SettingsCore::setAutoDownloadReceivedFiles(bool enabled) {
if (mAutoDownloadReceivedFiles != enabled) {
mAutoDownloadReceivedFiles = enabled;
emit autoDownloadReceivedFilesChanged();
setIsSaved(false);
}
}
void SettingsCore::setAutomaticallyRecordCallsEnabled(bool enabled) { void SettingsCore::setAutomaticallyRecordCallsEnabled(bool enabled) {
if (mAutomaticallyRecordCallsEnabled != enabled) { if (mAutomaticallyRecordCallsEnabled != enabled) {
mAutomaticallyRecordCallsEnabled = enabled; mAutomaticallyRecordCallsEnabled = enabled;
@ -960,6 +977,9 @@ void SettingsCore::writeIntoModel(std::shared_ptr<SettingsModel> model) const {
model->setEchoCancellationEnabled(mEchoCancellationEnabled); model->setEchoCancellationEnabled(mEchoCancellationEnabled);
model->setAutomaticallyRecordCallsEnabled(mAutomaticallyRecordCallsEnabled); model->setAutomaticallyRecordCallsEnabled(mAutomaticallyRecordCallsEnabled);
// Chat
model->setAutoDownloadReceivedFiles(mAutoDownloadReceivedFiles);
// Audio // Audio
model->setRingerDevice(mRingerDevice); model->setRingerDevice(mRingerDevice);
model->setCaptureDevice(mCaptureDevice); model->setCaptureDevice(mCaptureDevice);
@ -1022,6 +1042,9 @@ void SettingsCore::writeFromModel(const std::shared_ptr<SettingsModel> &model) {
mEchoCancellationEnabled = model->getEchoCancellationEnabled(); mEchoCancellationEnabled = model->getEchoCancellationEnabled();
mAutomaticallyRecordCallsEnabled = model->getAutomaticallyRecordCallsEnabled(); mAutomaticallyRecordCallsEnabled = model->getAutomaticallyRecordCallsEnabled();
// Chat
mAutoDownloadReceivedFiles = model->getAutoDownloadReceivedFiles();
// Audio // Audio
mCaptureDevices = model->getCaptureDevices(); mCaptureDevices = model->getCaptureDevices();
mPlaybackDevices = model->getPlaybackDevices(); mPlaybackDevices = model->getPlaybackDevices();

View file

@ -40,6 +40,8 @@ public:
Q_PROPERTY(bool videoEnabled READ getVideoEnabled WRITE setVideoEnabled NOTIFY videoEnabledChanged) Q_PROPERTY(bool videoEnabled READ getVideoEnabled WRITE setVideoEnabled NOTIFY videoEnabledChanged)
Q_PROPERTY(bool echoCancellationEnabled READ getEchoCancellationEnabled WRITE setEchoCancellationEnabled NOTIFY Q_PROPERTY(bool echoCancellationEnabled READ getEchoCancellationEnabled WRITE setEchoCancellationEnabled NOTIFY
echoCancellationEnabledChanged) echoCancellationEnabledChanged)
Q_PROPERTY(bool autoDownloadReceivedFiles READ getAutoDownloadReceivedFiles WRITE setAutoDownloadReceivedFiles
NOTIFY autoDownloadReceivedFilesChanged)
Q_PROPERTY( Q_PROPERTY(
int echoCancellationCalibration READ getEchoCancellationCalibration NOTIFY echoCancellationCalibrationChanged) int echoCancellationCalibration READ getEchoCancellationCalibration NOTIFY echoCancellationCalibrationChanged)
Q_PROPERTY(bool automaticallyRecordCallsEnabled READ getAutomaticallyRecordCallsEnabled WRITE Q_PROPERTY(bool automaticallyRecordCallsEnabled READ getAutomaticallyRecordCallsEnabled WRITE
@ -127,6 +129,11 @@ public:
} }
void setEchoCancellationEnabled(bool enabled); void setEchoCancellationEnabled(bool enabled);
bool getAutoDownloadReceivedFiles() {
return mAutoDownloadReceivedFiles;
}
void setAutoDownloadReceivedFiles(bool enabled);
bool getAutomaticallyRecordCallsEnabled() { bool getAutomaticallyRecordCallsEnabled() {
return mAutomaticallyRecordCallsEnabled; return mAutomaticallyRecordCallsEnabled;
} }
@ -248,6 +255,7 @@ signals:
void videoEnabledChanged(); void videoEnabledChanged();
void echoCancellationEnabledChanged(); void echoCancellationEnabledChanged();
void autoDownloadReceivedFilesChanged();
void automaticallyRecordCallsEnabledChanged(); void automaticallyRecordCallsEnabledChanged();
@ -327,6 +335,7 @@ private:
// Call // Call
bool mVideoEnabled; bool mVideoEnabled;
bool mEchoCancellationEnabled; bool mEchoCancellationEnabled;
bool mAutoDownloadReceivedFiles;
bool mAutomaticallyRecordCallsEnabled; bool mAutomaticallyRecordCallsEnabled;
// Audio // Audio

View file

@ -29,14 +29,6 @@
DEFINE_ABSTRACT_OBJECT(SoundPlayerCore) DEFINE_ABSTRACT_OBJECT(SoundPlayerCore)
// =============================================================================
using namespace std;
namespace {
int ForceCloseTimerInterval = 20;
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
QSharedPointer<SoundPlayerCore> SoundPlayerCore::create() { QSharedPointer<SoundPlayerCore> SoundPlayerCore::create() {
@ -114,7 +106,7 @@ void SoundPlayerCore::buildInternalPlayer(QSharedPointer<SoundPlayerCore> me) {
mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->play(mSource); }); mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->play(mSource); });
}); });
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lSeek, [this](int offset) { mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lSeek, [this](int offset) {
mSoundPlayerModelConnection->invokeToModel([this, offset] { mSoundPlayerModel->seek(offset); }); mSoundPlayerModelConnection->invokeToModel([this, offset] { mSoundPlayerModel->seek(mSource, offset); });
}); });
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::positionChanged, [this](int pos) { mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::positionChanged, [this](int pos) {
mSoundPlayerModelConnection->invokeToCore([this, pos] { setPosition(pos); }); mSoundPlayerModelConnection->invokeToCore([this, pos] { setPosition(pos); });
@ -126,12 +118,15 @@ void SoundPlayerCore::buildInternalPlayer(QSharedPointer<SoundPlayerCore> me) {
}); });
}); });
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::eofReached, mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::eofReached,
[this](const shared_ptr<linphone::Player> &player) { [this](const std::shared_ptr<linphone::Player> &player) {
mSoundPlayerModelConnection->invokeToCore([this] { mSoundPlayerModelConnection->invokeToCore([this] {
mForceClose = true; mForceClose = true;
handleEof(); handleEof();
}); });
}); });
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::errorChanged, [this](QString error) {
mSoundPlayerModelConnection->invokeToCore([this, error] { setError(error); });
});
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View file

@ -29,6 +29,7 @@ SoundPlayerGui::SoundPlayerGui(QObject *parent) : QObject(parent) {
if (mCore) connect(mCore.get(), &SoundPlayerCore::sourceChanged, this, &SoundPlayerGui::sourceChanged); if (mCore) connect(mCore.get(), &SoundPlayerCore::sourceChanged, this, &SoundPlayerGui::sourceChanged);
if (mCore) connect(mCore.get(), &SoundPlayerCore::stopped, this, &SoundPlayerGui::stopped); if (mCore) connect(mCore.get(), &SoundPlayerCore::stopped, this, &SoundPlayerGui::stopped);
if (mCore) connect(mCore.get(), &SoundPlayerCore::positionChanged, this, &SoundPlayerGui::positionChanged); if (mCore) connect(mCore.get(), &SoundPlayerCore::positionChanged, this, &SoundPlayerGui::positionChanged);
if (mCore) connect(mCore.get(), &SoundPlayerCore::errorChanged, this, &SoundPlayerGui::errorChanged);
} }
SoundPlayerGui::SoundPlayerGui(QSharedPointer<SoundPlayerCore> core) { SoundPlayerGui::SoundPlayerGui(QSharedPointer<SoundPlayerCore> core) {
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);

View file

@ -44,6 +44,7 @@ signals:
void sourceChanged(); void sourceChanged();
void stopped(); void stopped();
void positionChanged(); void positionChanged();
void errorChanged(QString error);
private: private:
DECLARE_ABSTRACT_OBJECT DECLARE_ABSTRACT_OBJECT

View file

@ -823,6 +823,19 @@
"puppy eyes" "puppy eyes"
] ]
}, },
{
"code": "1f979",
"char": "🥹",
"name": "face holding back tears",
"keywords": [
"tears",
"emotive",
"admiration",
"face with tears",
"gratitude",
"admiration"
]
},
{ {
"code": "1f626", "code": "1f626",
"char": "😦", "char": "😦",

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Raised-Hand" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<radialGradient id="face_1_" cx="63.6" cy="7861.3501" r="56.9597" gradientTransform="matrix(1 0 0 1 0 -7798.4497)" gradientUnits="userSpaceOnUse">
<stop offset="0.5" style="stop-color:#FDE030"/>
<stop offset="0.92" style="stop-color:#F7C02B"/>
<stop offset="1" style="stop-color:#F4A223"/>
</radialGradient>
<path id="face" style="fill:url(#face_1_);" d="M63.6,118.8c-27.9,0-58-17.5-58-55.9S35.7,7,63.6,7c15.5,0,29.8,5.1,40.4,14.4 c11.5,10.2,17.6,24.6,17.6,41.5s-6.1,31.2-17.6,41.4C93.4,113.6,79,118.8,63.6,118.8z"/>
<g id="eyes">
<path style="fill:#FFFFFF;" d="M43,47.7c9.58,0.03,17.33,7.82,17.3,17.4s-7.82,17.33-17.4,17.3c-9.58-0.03-17.33-7.82-17.3-17.4 C25.66,55.43,33.43,47.71,43,47.7"/>
<circle style="fill:#422B0D;" cx="42.7" cy="62.8" r="15.4"/>
<ellipse transform="matrix(0.7659 -0.6429 0.6429 0.7659 -32.8541 47.0076)" style="fill:#FFFFFF;" cx="48.13" cy="68.62" rx="2.6" ry="2.4"/>
<ellipse transform="matrix(0.8022 -0.5971 0.5971 0.8022 -26.9821 34.5782)" style="fill:#FFFFFF;" cx="38.69" cy="58.01" rx="9" ry="7.3"/>
<path style="fill:#FFFFFF;" d="M86,47.7c9.58,0.03,17.33,7.82,17.3,17.4c-0.03,9.58-7.82,17.33-17.4,17.3 c-9.58-0.03-17.33-7.82-17.3-17.4C68.66,55.43,76.43,47.71,86,47.7"/>
<circle style="fill:#422B0D;" cx="85.7" cy="62.8" r="15.4"/>
<ellipse transform="matrix(0.7659 -0.6429 0.6429 0.7659 -22.7305 74.6885)" style="fill:#FFFFFF;" cx="91.21" cy="68.56" rx="2.6" ry="2.4"/>
<ellipse transform="matrix(0.8022 -0.5971 0.5971 0.8022 -18.4797 60.2586)" style="fill:#FFFFFF;" cx="81.7" cy="58.02" rx="9" ry="7.3"/>
</g>
<path style="fill:none;" d="M43,47.7c-9.58,0.03-17.33,7.82-17.3,17.4c0.03,9.58,7.82,17.33,17.4,17.3 c9.58-0.03,17.33-7.82,17.3-17.4C60.34,55.43,52.57,47.71,43,47.7"/>
<circle style="fill:none;" cx="43.3" cy="62.8" r="15.4"/>
<ellipse transform="matrix(0.6429 -0.7659 0.7659 0.6429 -39.0221 53.4261)" style="fill:none;" cx="37.79" cy="68.56" rx="2.4" ry="2.6"/>
<ellipse transform="matrix(0.5971 -0.8022 0.8022 0.5971 -27.4803 61.3103)" style="fill:none;" cx="47.29" cy="58.01" rx="7.3" ry="9"/>
<g>
<defs>
<path id="SVGID_1_" d="M86,47.7c-9.58,0.03-17.33,7.82-17.3,17.4c0.03,9.58,7.82,17.33,17.4,17.3c9.58-0.03,17.33-7.82,17.3-17.4 C103.34,55.43,95.57,47.71,86,47.7"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_2_);">
<path style="fill:#29B6F6;" d="M102.35,68.47c-2.16-0.36-4.35,0.48-5.71,2.21c-0.97,1.27-2.49,1.99-4.09,1.95h-0.27 c-1.1,0-2.19,0.3-3.13,0.87c-1.48,0.9-3.35,0.9-4.83,0c-0.94-0.57-2.03-0.87-3.13-0.87c-0.24-0.02-0.47-0.02-0.71,0 c-1.71,0.25-3.42-0.43-4.48-1.79c-1.14-1.55-2.95-2.46-4.88-2.45c-3.31,0.08-5.96,2.76-6,6.07c0.03,3.3,2.7,5.97,6,6 c0.28,0,0.57-0.02,0.85-0.06c1.65-0.27,3.31,0.4,4.31,1.74c1.85,2.65,5.46,3.36,8.17,1.61c1.41-0.89,3.2-0.89,4.61,0 c2.63,1.68,6.1,1.07,8-1.4c0.91-1.25,2.38-1.96,3.93-1.9h0.38c3.34-0.05,6.01-2.8,5.96-6.14c-0.04-2.89-2.12-5.34-4.96-5.86 L102.35,68.47z"/>
</g>
</g>
<g>
<defs>
<path id="SVGID_3_" d="M43,47.7c-9.58,0.03-17.33,7.82-17.3,17.4c0.03,9.58,7.82,17.33,17.4,17.3c9.58-0.03,17.33-7.82,17.3-17.4 C60.34,55.43,52.57,47.71,43,47.7"/>
</defs>
<clipPath id="SVGID_4_">
<use xlink:href="#SVGID_3_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_4_);">
<path style="fill:#29B6F6;" d="M59.9,68.47c-2.16-0.36-4.35,0.49-5.7,2.21c-0.98,1.27-2.5,1.99-4.1,1.95h-0.26 c-1.1-0.01-2.19,0.29-3.13,0.87c-1.49,0.9-3.35,0.9-4.84,0c-0.94-0.58-2.03-0.88-3.13-0.87c-0.23-0.01-0.47-0.01-0.7,0 c-1.73,0.26-3.47-0.44-4.53-1.83c-1.14-1.55-2.95-2.46-4.87-2.45c-3.31,0.08-5.96,2.76-6,6.07c0,3.31,2.69,6,6,6 c0.02,0,0.03,0,0.05,0c0.28,0,0.56-0.02,0.84-0.06c1.65-0.27,3.32,0.4,4.32,1.74c1.82,2.66,5.42,3.4,8.15,1.68 c1.4-0.89,3.2-0.89,4.6,0c2.63,1.68,6.1,1.07,8-1.4c0.9-1.25,2.38-1.96,3.92-1.9h0.39c3.31,0.28,6.22-2.19,6.5-5.5 S63.22,68.76,59.9,68.47L59.9,68.47z"/>
</g>
</g>
<g id="eyebrows">
<path style="fill:#422B0D;" d="M27.4,39.8c-2.2,0.4-2.3,3.6,0.1,3.7c5.3,0.07,10.42-1.9,14.3-5.5c1.48-1.28,2.73-2.8,3.7-4.5 c0.58-0.83,0.38-1.97-0.45-2.55c-0.83-0.58-1.97-0.38-2.55,0.45l-0.1,0.1C38.48,35.88,33.19,38.81,27.4,39.8z"/>
<path style="fill:#422B0D;" d="M84.5,31.4c-0.58-0.83-1.72-1.03-2.55-0.45c-0.83,0.58-1.03,1.72-0.45,2.55 c0.97,1.7,2.22,3.22,3.7,4.5c3.9,3.57,9.01,5.54,14.3,5.5c2.5-0.1,2.3-3.3,0.1-3.7C93.74,38.84,88.41,35.87,84.5,31.4L84.5,31.4"/>
</g>
<path style="fill:#EB8F00;" d="M111.49,29.67c5.33,8.6,8.11,18.84,8.11,30.23c0,16.9-6.1,31.2-17.6,41.4 c-10.6,9.3-25,14.5-40.4,14.5c-18.06,0-37-7.35-48.18-22.94c10.76,17.66,31,25.94,50.18,25.94c15.4,0,29.8-5.2,40.4-14.5 c11.5-10.2,17.6-24.5,17.6-41.4C121.6,50.16,118.13,38.84,111.49,29.67z"/>
<path id="mouth" style="fill:#422B0D;" d="M64,103.2c10.8,0,17.8-7.9,19.7-11.6c0.7-1.4,0.7-2.6,0.1-3.1c-0.64-0.4-1.46-0.4-2.1,0 c-0.32,0.13-0.62,0.3-0.9,0.5c-4.9,3.52-10.77,5.44-16.8,5.5c-6.01-0.08-11.87-1.96-16.8-5.4c-0.28-0.2-0.58-0.37-0.9-0.5 c-0.64-0.4-1.46-0.4-2.1,0c-0.6,0.6-0.6,1.7,0.1,3.1C46.2,95.3,53.2,103.2,64,103.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

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

@ -43,6 +43,8 @@ list(APPEND _LINPHONEAPP_SOURCES
model/sound-player/SoundPlayerModel.cpp model/sound-player/SoundPlayerModel.cpp
model/recorder/RecorderModel.cpp
model/tool/ToolModel.cpp model/tool/ToolModel.cpp
model/tool/VfsUtils.cpp model/tool/VfsUtils.cpp

View file

@ -119,6 +119,10 @@ void ChatModel::deleteHistory() {
emit historyDeleted(); emit historyDeleted();
} }
void ChatModel::deleteMessage(std::shared_ptr<linphone::ChatMessage> message) {
mMonitor->deleteMessage(message);
}
void ChatModel::leave() { void ChatModel::leave() {
mMonitor->leave(); mMonitor->leave();
} }
@ -128,6 +132,11 @@ void ChatModel::deleteChatRoom() {
emit deleted(); emit deleted();
} }
std::shared_ptr<linphone::ChatMessage>
ChatModel::createVoiceRecordingMessage(const std::shared_ptr<linphone::Recorder> &recorder) {
return mMonitor->createVoiceRecordingMessage(recorder);
}
std::shared_ptr<linphone::ChatMessage> ChatModel::createTextMessageFromText(QString text) { std::shared_ptr<linphone::ChatMessage> ChatModel::createTextMessageFromText(QString text) {
return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text)); return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text));
} }

View file

@ -47,8 +47,11 @@ public:
std::list<std::shared_ptr<linphone::ChatMessage>> getHistory() const; std::list<std::shared_ptr<linphone::ChatMessage>> getHistory() const;
QString getIdentifier() const; QString getIdentifier() const;
void deleteHistory(); void deleteHistory();
void deleteMessage(std::shared_ptr<linphone::ChatMessage> message);
void deleteChatRoom(); void deleteChatRoom();
void leave(); void leave();
std::shared_ptr<linphone::ChatMessage>
createVoiceRecordingMessage(const std::shared_ptr<linphone::Recorder> &recorder);
std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text); std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text);
std::shared_ptr<linphone::ChatMessage> createMessage(QString text, std::shared_ptr<linphone::ChatMessage> createMessage(QString text,
QList<std::shared_ptr<ChatMessageContentModel>> filesContent); QList<std::shared_ptr<ChatMessageContentModel>> filesContent);

View file

@ -102,6 +102,10 @@ void ChatMessageModel::removeReaction() {
sendReaction(QString()); sendReaction(QString());
} }
void ChatMessageModel::send() {
mMonitor->send();
}
QString ChatMessageModel::getOwnReaction() const { QString ChatMessageModel::getOwnReaction() const {
auto reaction = mMonitor->getOwnReaction(); auto reaction = mMonitor->getOwnReaction();
return reaction ? Utils::coreStringToAppString(reaction->getBody()) : QString(); return reaction ? Utils::coreStringToAppString(reaction->getBody()) : QString();

View file

@ -59,6 +59,8 @@ public:
void removeReaction(); void removeReaction();
void send();
linphone::ChatMessage::State getState() const; linphone::ChatMessage::State getState() const;
QString getOwnReaction() const; QString getOwnReaction() const;

View file

@ -0,0 +1,118 @@
/*
* 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 "core/App.hpp"
#include "model/core/CoreModel.hpp"
#include "model/setting/SettingsModel.hpp"
#include "tool/Utils.hpp"
#include <QFile>
#include <QQmlApplicationEngine>
#include "RecorderModel.hpp"
DEFINE_ABSTRACT_OBJECT(RecorderModel)
// =============================================================================
RecorderModel::RecorderModel(std::shared_ptr<linphone::Recorder> recorder, QObject *parent) : QObject(parent) {
mustBeInLinphoneThread(getClassName());
mRecorder = recorder;
}
RecorderModel::~RecorderModel() {
}
std::shared_ptr<linphone::Recorder> RecorderModel::getRecorder() {
return mRecorder;
}
int RecorderModel::getDuration() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return mRecorder->getDuration();
}
float RecorderModel::getCaptureVolume() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return mRecorder->getCaptureVolume();
}
linphone::Recorder::State RecorderModel::getState() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return mRecorder->getState();
}
QString RecorderModel::getFile() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return Utils::coreStringToAppString(mRecorder->getFile());
}
QStringList RecorderModel::splitSavedFilename(const QString &filename) {
QStringList fields = filename.split('_');
if (fields.size() == 3 && fields[0] == "vocal" && fields[1].split('-').size() == 3 &&
fields[2].split('-').size() == 4) {
return fields;
} else return QStringList(filename);
}
QDateTime RecorderModel::getDateTimeSavedFilename(const QString &filename) {
auto fields = splitSavedFilename(filename);
if (fields.size() > 1) return QDateTime::fromString(fields[1] + "_" + fields[2], "yyyy-MM-dd_hh-mm-ss-zzz");
else return QDateTime();
}
void RecorderModel::start() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
bool soFarSoGood;
QString filename =
QStringLiteral("vocal_%1.mkv").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss-zzz"));
const QString safeFilePath = Utils::getSafeFilePath(
QStringLiteral("%1%2").arg(SettingsModel::getInstance()->getSavedCallsFolder()).arg(filename), &soFarSoGood);
if (!soFarSoGood) {
qWarning() << QStringLiteral("Unable to create safe file path for: %1.").arg(filename);
emit errorChanged(QString("Unable to create safe file path for : %1.").arg(filename));
} else if (mRecorder->open(Utils::appStringToCoreString(safeFilePath)) < 0) {
qWarning() << QStringLiteral("Unable to open safe file path for: %1.").arg(filename);
emit errorChanged(QString("Unable to open safe file path for : %1.").arg(filename));
} else if (mRecorder->start() < 0) {
qWarning() << QStringLiteral("Unable to start recording to : %1.").arg(filename);
emit errorChanged(QString("Unable to start recording to : %1.").arg(filename));
}
emit stateChanged();
emit fileChanged();
}
void RecorderModel::pause() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
mRecorder->pause();
emit stateChanged();
}
void RecorderModel::stop() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
// if (mRecorder->getState() == linphone::Recorder::State::Running) // Remove these tests when the SDK do them.
// mRecorder->pause();
// if (mRecorder->getState() == linphone::Recorder::State::Paused) {
mRecorder->close();
emit stateChanged();
}
//--------------------------------------------------------------------------------------------------------------------------

View file

@ -0,0 +1,60 @@
/*
* 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 RECORDER_MODEL_H
#define RECORDER_MODEL_H
#include "tool/AbstractObject.hpp"
#include <linphone++/linphone.hh>
// =============================================================================
class RecorderModel : public QObject, public AbstractObject {
Q_OBJECT
public:
RecorderModel(std::shared_ptr<linphone::Recorder> recorder, QObject *parent = nullptr);
virtual ~RecorderModel();
std::shared_ptr<linphone::Recorder> getRecorder();
int getDuration() const;
float getCaptureVolume() const;
linphone::Recorder::State getState() const;
QString getFile() const;
static QStringList
splitSavedFilename(const QString &filename); // If doesn't match to generateSavedFilename, return filename
static QDateTime getDateTimeSavedFilename(const QString &filename);
void start();
void pause();
void stop();
signals:
void stateChanged();
void fileChanged();
void errorChanged(QString error);
private:
DECLARE_ABSTRACT_OBJECT
std::shared_ptr<linphone::Recorder> mRecorder;
};
#endif

View file

@ -428,6 +428,17 @@ void SettingsModel::setVideoEnabled(const bool enabled) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
bool SettingsModel::getAutoDownloadReceivedFiles() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return CoreModel::getInstance()->getCore()->getMaxSizeForAutoDownloadIncomingFiles() == 0;
}
void SettingsModel::setAutoDownloadReceivedFiles(bool status) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
CoreModel::getInstance()->getCore()->setMaxSizeForAutoDownloadIncomingFiles(status ? 0 : -1);
emit autoDownloadReceivedFilesChanged(status);
}
bool SettingsModel::getEchoCancellationEnabled() const { bool SettingsModel::getEchoCancellationEnabled() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return CoreModel::getInstance()->getCore()->echoCancellationEnabled(); return CoreModel::getInstance()->getCore()->echoCancellationEnabled();
@ -558,6 +569,18 @@ QString SettingsModel::getLogsFolder(const shared_ptr<linphone::Config> &config)
: Paths::getLogsDirPath(); : Paths::getLogsDirPath();
} }
static inline std::string getLegacySavedCallsFolder(const shared_ptr<linphone::Config> &config) {
auto path = config->getString(SettingsModel::UiSection, "saved_videos_folder", "");
if (path == "") path = Utils::appStringToCoreString(Paths::getCapturesDirPath());
return path;
}
QString SettingsModel::getSavedCallsFolder() const {
auto path = mConfig->getString(UiSection, "saved_calls_folder", ""); // Avoid to call default function if exist.
if (path == "") path = getLegacySavedCallsFolder(mConfig);
return QDir::cleanPath(Utils::coreStringToAppString(path)) + QDir::separator();
}
QString SettingsModel::getLogsUploadUrl() const { QString SettingsModel::getLogsUploadUrl() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto core = CoreModel::getInstance()->getCore(); auto core = CoreModel::getInstance()->getCore();

View file

@ -61,6 +61,9 @@ public:
bool getEchoCancellationEnabled() const; bool getEchoCancellationEnabled() const;
void setEchoCancellationEnabled(bool enabled); void setEchoCancellationEnabled(bool enabled);
void setAutoDownloadReceivedFiles(bool enabled);
bool getAutoDownloadReceivedFiles() const;
// Audio. -------------------------------------------------------------------- // Audio. --------------------------------------------------------------------
bool getIsInCall() const; bool getIsInCall() const;
@ -137,6 +140,7 @@ public:
QString getLogsFolder() const; QString getLogsFolder() const;
void setLogsFolder(const QString &folder); void setLogsFolder(const QString &folder);
static QString getLogsFolder(const std::shared_ptr<linphone::Config> &config); static QString getLogsFolder(const std::shared_ptr<linphone::Config> &config);
QString getSavedCallsFolder() const;
QString getLogsUploadUrl() const; QString getLogsUploadUrl() const;
void setLogsUploadUrl(const QString &url); void setLogsUploadUrl(const QString &url);
@ -238,6 +242,9 @@ signals:
void dndChanged(bool value); void dndChanged(bool value);
// Messages. --------------------------------------------------------------------
void autoDownloadReceivedFilesChanged(bool enabled);
private: private:
void notifyConfigReady(); void notifyConfigReady();
MediastreamerUtils::SimpleCaptureGraph *mSimpleCaptureGraph = nullptr; MediastreamerUtils::SimpleCaptureGraph *mSimpleCaptureGraph = nullptr;

View file

@ -99,7 +99,13 @@ bool SoundPlayerModel::play(QString source) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void SoundPlayerModel::seek(int offset) { void SoundPlayerModel::seek(QString source, int offset) {
if (!open(source)) {
qWarning() << QStringLiteral("Unable to open: `%1`").arg(source);
//: Unable to open: `%1`
emit errorChanged(QString("sound_player_open_error").arg(source));
return;
}
mMonitor->seek(offset); mMonitor->seek(offset);
emit positionChanged(mMonitor->getCurrentPosition()); emit positionChanged(mMonitor->getCurrentPosition());
} }

View file

@ -46,7 +46,7 @@ public:
void pause(); void pause();
bool play(QString source); bool play(QString source);
void stop(bool force = false); void stop(bool force = false);
void seek(int offset); void seek(QString source, int offset);
int getPosition() const; int getPosition() const;
bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope. bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope.

View file

@ -32,6 +32,7 @@
#include "core/participant/ParticipantDeviceCore.hpp" #include "core/participant/ParticipantDeviceCore.hpp"
#include "core/path/Paths.hpp" #include "core/path/Paths.hpp"
#include "core/payload-type/DownloadablePayloadTypeCore.hpp" #include "core/payload-type/DownloadablePayloadTypeCore.hpp"
#include "core/recorder/RecorderGui.hpp"
#include "model/object/VariantObject.hpp" #include "model/object/VariantObject.hpp"
#include "model/tool/ToolModel.hpp" #include "model/tool/ToolModel.hpp"
#include "tool/providers/AvatarProvider.hpp" #include "tool/providers/AvatarProvider.hpp"
@ -1948,6 +1949,56 @@ QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) {
return QString(""); return QString("");
} }
VariantObject *Utils::createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui) {
VariantObject *data = new VariantObject("createVoiceRecordingMessage");
if (!data) return nullptr;
data->makeRequest([recorderCore = recorderGui ? recorderGui->getCore() : nullptr,
chatCore = chatGui ? chatGui->getCore() : nullptr]() {
if (!recorderCore || !chatCore) return QVariant();
auto model = recorderCore->getModel();
auto chatModel = chatCore->getModel();
if (!model || !chatModel) return QVariant();
auto recorder = model->getRecorder();
auto linMessage = chatModel->createVoiceRecordingMessage(recorder);
if (linMessage) {
auto messageCore = ChatMessageCore::create(linMessage);
return QVariant::fromValue(new ChatMessageGui(messageCore));
}
return QVariant();
});
data->requestValue();
return data;
}
void Utils::sendVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui) {
auto chatModel = chatGui && chatGui->mCore ? chatGui->mCore->getModel() : nullptr;
auto recorderModel = recorderGui && recorderGui->mCore ? recorderGui->mCore->getModel() : nullptr;
if (!chatModel || !recorderModel) {
//: Error with the recorder
QString error = !recorderModel ? tr("recorder_error")
//: Error in the chat
: tr("chat_error");
//: Error
showInformationPopup(tr("info_popup_error_title"),
//: Could not send voice message : %1
tr("info_popup_send_voice_message_error_message").arg(error));
return;
}
App::postModelAsync([chatModel, recorderModel] {
mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO));
auto chat = chatModel->getMonitor();
auto recorder = recorderModel->getRecorder();
auto linMessage = chatModel->createVoiceRecordingMessage(recorder);
if (linMessage) {
linMessage->send();
} else
//: Error
showInformationPopup(tr("info_popup_error_title"),
//: Failed to create message from record
tr("info_popup_send_voice_message_sending_error_message"));
});
}
bool Utils::isVideo(const QString &path) { bool Utils::isVideo(const QString &path) {
if (path.isEmpty()) return false; if (path.isEmpty()) return false;
return QMimeDatabase().mimeTypeForFile(path).name().contains("video/"); return QMimeDatabase().mimeTypeForFile(path).name().contains("video/");

View file

@ -52,6 +52,7 @@ class ConferenceCore;
class ParticipantDeviceCore; class ParticipantDeviceCore;
class DownloadablePayloadTypeCore; class DownloadablePayloadTypeCore;
class ChatGui; class ChatGui;
class RecorderGui;
class Utils : public QObject, public AbstractObject { class Utils : public QObject, public AbstractObject {
Q_OBJECT Q_OBJECT
@ -174,6 +175,9 @@ public:
static QDateTime getOffsettedUTC(const QDateTime &date); static QDateTime getOffsettedUTC(const QDateTime &date);
Q_INVOKABLE static QString toTimeString(QDateTime date, const QString &format = "hh:mm:ss"); Q_INVOKABLE static QString toTimeString(QDateTime date, const QString &format = "hh:mm:ss");
Q_INVOKABLE static VariantObject *createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui);
Q_INVOKABLE static void sendVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui);
// QDir findDirectoryByName(QString startPath, QString name); // QDir findDirectoryByName(QString startPath, QString name);
static QString getApplicationProduct(); static QString getApplicationProduct();

View file

@ -149,6 +149,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Page/Layout/Settings/DebugSettingsLayout.qml view/Page/Layout/Settings/DebugSettingsLayout.qml
view/Page/Layout/Settings/LdapSettingsLayout.qml view/Page/Layout/Settings/LdapSettingsLayout.qml
view/Page/Layout/Settings/CarddavSettingsLayout.qml view/Page/Layout/Settings/CarddavSettingsLayout.qml
view/Page/Layout/Settings/ChatSettingsLayout.qml
view/Page/Layout/Settings/SecuritySettingsLayout.qml view/Page/Layout/Settings/SecuritySettingsLayout.qml
view/Page/Layout/Settings/NetworkSettingsLayout.qml view/Page/Layout/Settings/NetworkSettingsLayout.qml
view/Page/Layout/Settings/AdvancedSettingsLayout.qml view/Page/Layout/Settings/AdvancedSettingsLayout.qml

View file

@ -19,7 +19,7 @@ Control.Button {
property color pressedTextColor: style?.text?.pressed || Qt.darker(textColor, 1.1) property color pressedTextColor: style?.text?.pressed || Qt.darker(textColor, 1.1)
property color borderColor: style?.borderColor || "transparent" property color borderColor: style?.borderColor || "transparent"
ToolTip.visible: hovered && ToolTip.text != "" ToolTip.visible: hovered && ToolTip.text != ""
ToolTip.delay: 1000 ToolTip.delay: 500
property color disabledFilterColor: color.hslLightness > 0.5 property color disabledFilterColor: color.hslLightness > 0.5
? DefaultStyle.grey_0 ? DefaultStyle.grey_0
: DefaultStyle.grey_400 : DefaultStyle.grey_400
@ -199,9 +199,20 @@ Control.Button {
} }
Component{ Component{
id: imageComponent id: imageComponent
ButtonImage{ Item {
width: stacklayout.width width: stacklayout.width
height: stacklayout.height height: stacklayout.height
ButtonImage {
id: buttonIcon
anchors.fill: parent
}
ButtonImage {
z: buttonIcon.z + 1
visible: !mainItem.enabled
anchors.fill: parent
colorizationColor: DefaultStyle.grey_0
opacity: 0.5
}
} }
} }
Component{ Component{

View file

@ -12,8 +12,8 @@ Button {
// bottomPadding: Math.round(16 * DefaultStyle.dp) // bottomPadding: Math.round(16 * DefaultStyle.dp)
// leftPadding: Math.round(16 * DefaultStyle.dp) // leftPadding: Math.round(16 * DefaultStyle.dp)
// rightPadding: Math.round(16 * DefaultStyle.dp) // rightPadding: Math.round(16 * DefaultStyle.dp)
icon.width: width // icon.width: width
icon.height: width // icon.height: width
radius: width * 2 radius: width * 2
// width: Math.round(24 * DefaultStyle.dp) // width: Math.round(24 * DefaultStyle.dp)
height: width height: width

View file

@ -7,41 +7,45 @@ import UtilsCpp
// ============================================================================= // =============================================================================
Loader{ Item {
id: mainItem id: mainItem
property ChatMessageContentGui chatMessageContentGui property ChatMessageContentGui chatMessageContentGui
property int availableWidth : parent.width property var chatMessageObj
property ChatMessageGui chatMessage: chatMessageObj && chatMessageObj.value || null
property bool isPlaying : soudPlayerLoader.item && soudPlayerLoader.item.core.playbackState === LinphoneEnums.PlaybackState.PlayingState
onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop()
property bool recording: false
property RecorderGui recorderGui: recorderLoader.item || null
// property string filePath : tempFile.filePath signal voiceRecordingMessageCreationRequested(RecorderGui recorderGui)
signal stopRecording()
active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording function createVoiceMessageInChat(chat) {
if (recorderLoader.item) {
mainItem.chatMessageObj = UtilsCpp.createVoiceRecordingMessage(recorderLoader.item, chat)
} else {
//: Error
UtilsCpp.showInformationPopup(qsTr("information_popup_error_title"),
//: Failed to create voice message : error in recorder
qsTr("information_popup_voice_message_error_message"), false)
}
}
// onChatMessageContentGuiChanged: if(chatMessageContentGui){ Loader {
// tempFile.createFileFromContentModel(chatMessageContentGui, false); id: soudPlayerLoader
// } property int duration: mainItem.chatMessageContentGui
? mainItem.chatMessageContentGui.core.fileDuration
// TemporaryFile { : item
// id: tempFile ? item.core.duration
// } : 0
property int position: item?.core.position || 0
sourceComponent: Item { active: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.isVoiceRecording
id: loadedItem sourceComponent: SoundPlayerGui {
property bool isPlaying : soundPlayerGui && soundPlayerGui.core.playbackState === LinphoneEnums.PlaybackState.PlayingState
onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop()
width: mainItem.width
height: mainItem.height
clip: false
SoundPlayerGui {
id: soundPlayerGui id: soundPlayerGui
property int duration: mainItem.chatMessageContentGui ? mainItem.chatMessageContentGui.core.fileDuration : core.duration
property int position: core.position
source: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.filePath source: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.filePath
function play(){ function play(){
if(loadedItem.isPlaying){// Pause the play if(mainItem.isPlaying){// Pause the play
soundPlayerGui.core.lPause() soundPlayerGui.core.lPause()
}else{// Play the audio }else{// Play the audio
soundPlayerGui.core.lPlay() soundPlayerGui.core.lPlay()
@ -51,41 +55,82 @@ Loader{
mediaProgressBar.value = 101 mediaProgressBar.value = 101
} }
onPositionChanged: { onPositionChanged: {
mediaProgressBar.progressPosition = position mediaProgressBar.progressPosition = soudPlayerLoader.position
mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / duration) mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / soudPlayerLoader.duration)
} }
onSourceChanged: if (source != "") { onSourceChanged: if (source != "") {
// core.lPlay()// This will open the file and allow seeking core.lOpen() // Open the file and allow seeking
// core.lPause()
core.lOpen()
mediaProgressBar.value = 0 mediaProgressBar.value = 0
mediaProgressBar.refresh() mediaProgressBar.refresh()
} }
onErrorChanged: (error) => {
//: Error
UtilsCpp.showInformationPopup(qsTr("information_popup_error_title"), error, false)
}
}
}
Loader {
id: recorderLoader
active: mainItem.recording && !mainItem.chatMessageContentGui
property int duration: item?.core.duration || 0
property int captureVolume: item?.core.captureVolume || 0
property var state: item?.core.state
Connections {
target: mainItem
function onStopRecording() {
recorderLoader.item.core.lStop()
}
} }
sourceComponent: RecorderGui {
MediaProgressBar{ id: recorderGui
id: mediaProgressBar onReady: core.lStart()
anchors.fill: parent onStateChanged: (state) => {
progressDuration: soundPlayerGui ? soundPlayerGui.duration : chatMessageContentGui.core.fileDuration if (state === LinphoneEnums.RecorderState.Running) mediaProgressBar.start()
progressPosition: 0 if (state === LinphoneEnums.RecorderState.Closed) {
value: 0 mediaProgressBar.stop()
function refresh(){ mainItem.voiceRecordingMessageCreationRequested(recorderGui)
if(soundPlayerGui){
soundPlayerGui.core.lRefreshPosition()
}
}
onEndReached:{
if(soundPlayerGui)
soundPlayerGui.core.lStop()
}
onPlayStopButtonToggled: soundPlayerGui.play()
onRefreshPositionRequested: refresh()
onSeekRequested: (ms) => {
if(soundPlayerGui) {
soundPlayerGui.core.lSeek(ms)
} }
} }
} }
} }
MediaProgressBar{
id: mediaProgressBar
anchors.fill: parent
progressDuration: soudPlayerLoader.active
? soudPlayerLoader.duration
: recorderLoader
? recorderLoader.duration
: chatMessageContentGui.core.fileDuration
progressPosition: 0
value: 0
recording: recorderLoader.state === LinphoneEnums.RecorderState.Running
function refresh(){
if(soudPlayerLoader.item){
soudPlayerLoader.item.core.lRefreshPosition()
} else if (recorderLoader.item) {
recorderLoader.item.core.lRefresh()
}
}
onEndReached:{
if(soudPlayerLoader.item)
soudPlayerLoader.item.core.lStop()
}
onPlayStopButtonToggled: {
if(soudPlayerLoader.item) {
soudPlayerLoader.item.play()
} else if (recorderLoader.item) {
recorderLoader.item.core.lStop()
}
}
onRefreshPositionRequested: refresh()
onSeekRequested: (ms) => {
if(soudPlayerLoader.active) {
soudPlayerLoader.item.core.lSeek(ms)
}
}
}
} }

View file

@ -342,9 +342,9 @@ ListView {
spacing: Math.round(10 * DefaultStyle.dp) spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true Layout.fillWidth: true
onClicked: { onClicked: {
//: Delete the chat ? //: Delete the conversation ?
mainWindow.showConfirmationLambdaPopup(qsTr("chat_list_delete_chat_popup_title"), mainWindow.showConfirmationLambdaPopup(qsTr("chat_list_delete_chat_popup_title"),
//: This chat and all its messages will be deleted. Do You want to continue ? //: This conversation and all its messages will be deleted. Do You want to continue ?
qsTr("chat_list_delete_chat_popup_message"), qsTr("chat_list_delete_chat_popup_message"),
"", "",
function(confirmed) { function(confirmed) {

View file

@ -28,7 +28,7 @@ ColumnLayout {
// VOICE MESSAGES // VOICE MESSAGES
Repeater { Repeater {
id: messagesVoicesList id: messagesVoicesList
visible: mainItem.chatMessageGui.core.isVoiceRecording && count > 0 visible: count > 0
model: ChatMessageContentProxy{ model: ChatMessageContentProxy{
filterType: ChatMessageContentProxy.FilterContentType.Voice filterType: ChatMessageContentProxy.FilterContentType.Voice
chatMessageGui: mainItem.chatMessageGui chatMessageGui: mainItem.chatMessageGui

View file

@ -30,7 +30,7 @@ ProgressBar {
animationTest.start() animationTest.start()
} }
function resume(){ function resume(){
if(mainItem.value >= 100) if (mainItem.value >= 100)
mainItem.value = 0 mainItem.value = 0
animationTest.start() animationTest.start()
} }
@ -41,7 +41,7 @@ ProgressBar {
signal endReached() signal endReached()
signal refreshPositionRequested() signal refreshPositionRequested()
signal seekRequested(int ms) signal seekRequested(int ms)
Timer{ Timer {
id: animationTest id: animationTest
repeat: true repeat: true
onTriggered: mainItem.refreshPositionRequested() onTriggered: mainItem.refreshPositionRequested()
@ -60,7 +60,6 @@ ProgressBar {
mainItem.value = 100// Stay at 100 mainItem.value = 100// Stay at 100
progressPosition = progressDuration progressPosition = progressDuration
} }
console.log("end reached")
mainItem.endReached() mainItem.endReached()
} }
} }
@ -128,8 +127,7 @@ ProgressBar {
onClicked: { onClicked: {
mainItem.playStopButtonToggled() mainItem.playStopButtonToggled()
} }
borderColor: "transparent" style: ButtonStyle.player
style: ButtonStyle.secondary
} }
Control.Control { Control.Control {
anchors.right: parent.right anchors.right: parent.right
@ -150,6 +148,8 @@ ProgressBar {
visible: mainItem.recording visible: mainItem.recording
colorizationColor: DefaultStyle.danger_500main colorizationColor: DefaultStyle.danger_500main
imageSource: AppIcons.recordFill imageSource: AppIcons.recordFill
Layout.preferredWidth: Math.round(14 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(14 * DefaultStyle.dp)
} }
Text { Text {
id: durationText id: durationText

View file

@ -11,23 +11,24 @@ import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils
Control.Control { Control.Control {
id: mainItem id: mainItem
property alias placeholderText: sendingTextArea.placeholderText // property alias placeholderText: sendingTextArea.placeholderText
property alias text: sendingTextArea.text property string text
property alias textArea: sendingTextArea property var textArea
property alias cursorPosition: sendingTextArea.cursorPosition // property alias cursorPosition: sendingTextArea.cursorPosition
property alias emojiPickerButtonChecked: emojiPickerButton.checked property bool emojiPickerButtonChecked
property bool dropEnabled: true property bool dropEnabled: true
property string dropDisabledReason property string dropDisabledReason
property bool isEphemeral : false property bool isEphemeral : false
property bool emojiVisible: false property bool emojiVisible: false
property ChatGui chat
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
signal dropped (var files) signal dropped (var files)
signal validText (string text) signal validText (string text)
signal sendText() signal sendMessage()
signal audioRecordRequest()
signal emojiClicked() signal emojiClicked()
signal composing() signal composing()
@ -63,118 +64,196 @@ Control.Control {
background: Rectangle { background: Rectangle {
anchors.fill: parent anchors.fill: parent
color: DefaultStyle.grey_100 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 { contentItem: Control.StackView {
spacing: Math.round(20 * DefaultStyle.dp) id: sendingAreaStackView
RowLayout { initialItem: textAreaComp
spacing: Math.round(16 * DefaultStyle.dp) Component {
BigButton { id: textAreaComp
id: emojiPickerButton RowLayout {
style: ButtonStyle.noBackground // disable record button if call ongoing
checkable: true CallProxy {
icon.source: checked ? AppIcons.closeX : AppIcons.smiley id: callsModel
} sourceModel: AppCpp.calls
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.paperclip
onClicked: {
fileDialog.open()
} }
} spacing: Math.round(16 * DefaultStyle.dp)
Control.Control { BigButton {
Layout.fillWidth: true id: emojiPickerButton
leftPadding: Math.round(15 * DefaultStyle.dp) style: ButtonStyle.noBackground
rightPadding: Math.round(15 * DefaultStyle.dp) checkable: true
topPadding: Math.round(15 * DefaultStyle.dp) icon.source: checked ? AppIcons.closeX : AppIcons.smiley
bottomPadding: Math.round(15 * DefaultStyle.dp) onCheckedChanged: mainItem.emojiPickerButtonChecked = checked
background: Rectangle { Connections {
id: inputBackground target: mainItem
anchors.fill: parent function onEmojiPickerButtonCheckedChanged() {
radius: Math.round(35 * DefaultStyle.dp) emojiPickerButton.checked = mainItem.emojiPickerButtonChecked
color: DefaultStyle.grey_0 }
MouseArea {
anchors.fill: parent
onPressed: sendingTextArea.forceActiveFocus()
cursorShape: Qt.IBeamCursor
} }
} }
contentItem: RowLayout { BigButton {
Flickable { style: ButtonStyle.noBackground
id: sendingAreaFlickable icon.source: AppIcons.paperclip
Layout.fillWidth: true onClicked: {
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight) fileDialog.open()
Binding { }
target: sendingAreaFlickable }
when: expandButton.checked Control.Control {
property: "Layout.preferredHeight" Layout.fillWidth: true
value: Math.round(250 * DefaultStyle.dp) leftPadding: Math.round(15 * DefaultStyle.dp)
restoreMode: Binding.RestoreBindingOrValue 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
} }
Layout.fillHeight: true }
contentHeight: sendingTextArea.contentHeight contentItem: RowLayout {
contentWidth: width Flickable {
id: sendingAreaFlickable
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight)
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: sendingTextArea.contentHeight
contentWidth: width
function ensureVisible(r) { function ensureVisible(r) {
if (contentX >= r.x) if (contentX >= r.x)
contentX = r.x; contentX = r.x;
else if (contentX+width <= r.x+r.width) else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width; contentX = r.x+r.width-width;
if (contentY >= r.y) if (contentY >= r.y)
contentY = r.y; contentY = r.y;
else if (contentY+height <= r.y+r.height) else if (contentY+height <= r.y+r.height)
contentY = r.y+r.height-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 TextArea {
Keys.onPressed: (event) => { id: sendingTextArea
if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return)) width: sendingAreaFlickable.width
if(!(event.modifiers & Qt.ShiftModifier)) { height: sendingAreaFlickable.height
mainItem.sendText() textFormat: TextEdit.AutoText
event.accepted = true onTextChanged: mainItem.text = text
Component.onCompleted: mainItem.textArea = sendingTextArea
//: 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))
if(!(event.modifiers & Qt.ShiftModifier)) {
mainItem.sendMessage()
event.accepted = true
}
}
Connections {
target: mainItem
function onTextChanged() {
if (mainItem.text !== text) text = mainItem.text
}
function onSendMessage() {
sendingTextArea.clear()
}
}
}
}
RowLayout {
id: stackButton
spacing: 0
BigButton {
id: recordButton
enabled: !callsModel.currentCall
ToolTip.visible: !enabled && hovered
//: Cannot record a message while a call is ongoing
ToolTip.text: qsTr("cannot_record_while_in_call_tooltip")
visible: sendingTextArea.text.length === 0
style: ButtonStyle.noBackground
hoverEnabled: true
icon.source: AppIcons.microphone
onClicked: {
sendingAreaStackView.push(voiceMessageRecordComp)
}
}
BigButton {
visible: sendingTextArea.text.length !== 0
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
onClicked: {
mainItem.sendMessage()
} }
} }
} }
} }
RowLayout { }
id: stackButton }
spacing: 0 }
BigButton { Component {
visible: sendingTextArea.text.length === 0 id: voiceMessageRecordComp
style: ButtonStyle.noBackground RowLayout {
icon.source: AppIcons.microphone spacing: Math.round(16 * DefaultStyle.dp)
onClicked: { RoundButton {
console.log("TODO : go to record message") style: ButtonStyle.player
} shadowEnabled: true
padding: Math.round(4 * DefaultStyle.dp)
icon.width: Math.round(22 * DefaultStyle.dp)
icon.height: Math.round(22 * DefaultStyle.dp)
icon.source: AppIcons.closeX
width: Math.round(30 * DefaultStyle.dp)
Layout.preferredWidth: width
Layout.preferredHeight: height
onClicked: {
if (voiceMessage.chatMessage) mainItem.chat.core.lDeleteMessage(voiceMessage.chatMessage)
sendingAreaStackView.pop()
}
}
ChatAudioContent {
id: voiceMessage
recording: true
Layout.fillWidth: true
Layout.preferredHeight: Math.round(48 * DefaultStyle.dp)
chatMessageContentGui: chatMessage ? chatMessage.core.getVoiceRecordingContent() : null
onVoiceRecordingMessageCreationRequested: (recorderGui) => {
chatMessageObj = UtilsCpp.createVoiceRecordingMessage(recorderGui, mainItem.chat)
}
}
BigButton {
id: sendButton
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
icon.width: Math.round(22 * DefaultStyle.dp)
icon.height: Math.round(22 * DefaultStyle.dp)
// Layout.preferredWidth: icon.width
// Layout.preferredHeight: icon.height
property bool sendVoiceRecordingOnCreated: false
onClicked: {
if (voiceMessage.chatMessage) {
voiceMessage.chatMessage.core.lSend()
sendingAreaStackView.pop()
} }
BigButton { else {
visible: sendingTextArea.text.length !== 0 sendVoiceRecordingOnCreated = true
style: ButtonStyle.noBackgroundOrange voiceMessage.stopRecording()
icon.source: AppIcons.paperPlaneRight }
onClicked: { }
mainItem.sendText() Connections {
target: voiceMessage
function onChatMessageChanged() {
if (sendButton.sendVoiceRecordingOnCreated) {
voiceMessage.chatMessage.core.lSend()
sendButton.sendVoiceRecordingOnCreated = false
sendingAreaStackView.pop()
} }
} }
} }

View file

@ -29,7 +29,7 @@ RowLayout {
} }
onGroupCall: { onGroupCall: {
mainWindow.showConfirmationLambdaPopup(qsTr(""), mainWindow.showConfirmationLambdaPopup("",
qsTr("chat_view_group_call_toast_message"), qsTr("chat_view_group_call_toast_message"),
"", "",
function(confirmed) { function(confirmed) {
@ -115,153 +115,163 @@ RowLayout {
} }
] ]
content: ColumnLayout { content: Control.SplitView {
spacing: 0
anchors.fill: parent anchors.fill: parent
Item { orientation: Qt.Vertical
Layout.fillWidth: true handle: Rectangle {
Layout.fillHeight: true implicitHeight: Math.round(8 * DefaultStyle.dp)
ChatMessagesListView { color: Control.SplitHandle.hovered ? DefaultStyle.grey_200 : DefaultStyle.grey_100
id: chatMessagesListView
clip: true
height: contentHeight
backgroundColor: splitPanel.panelColor
width: parent.width - anchors.leftMargin - anchors.rightMargin
chat: mainItem.chat
anchors.fill: parent
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
}
}
}
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 { ColumnLayout {
id: selectedFilesArea spacing: 0
visible: selectedFiles.count > 0 Control.SplitView.fillHeight: true
Layout.fillWidth: true Item {
Layout.preferredHeight: Math.round(104 * DefaultStyle.dp) Layout.fillWidth: true
topPadding: Math.round(12 * DefaultStyle.dp) Layout.fillHeight: true
bottomPadding: Math.round(12 * DefaultStyle.dp) ChatMessagesListView {
leftPadding: Math.round(19 * DefaultStyle.dp) id: chatMessagesListView
rightPadding: Math.round(19 * DefaultStyle.dp) clip: true
height: contentHeight
backgroundColor: splitPanel.panelColor
width: parent.width - anchors.leftMargin - anchors.rightMargin
chat: mainItem.chat
anchors.fill: parent
anchors.leftMargin: Math.round(18 * DefaultStyle.dp)
anchors.rightMargin: Math.round(18 * DefaultStyle.dp)
Control.ScrollBar.vertical: scrollbar
Button { Popup {
anchors.top: parent.top id: emojiPickerPopup
anchors.right: parent.right y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp)
anchors.topMargin: selectedFilesArea.topPadding x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp)
anchors.rightMargin: selectedFilesArea.rightPadding width: Math.round(393 * DefaultStyle.dp)
icon.source: AppIcons.closeX height: Math.round(291 * DefaultStyle.dp)
style: ButtonStyle.noBackground visible: messageSender.emojiPickerButtonChecked
onClicked: { closePolicy: Popup.CloseOnPressOutside
contents.clear() 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
}
}
}
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
} }
} }
background: Item{ Control.Control {
anchors.fill: parent id: selectedFilesArea
Rectangle { visible: selectedFiles.count > 0
color: DefaultStyle.grey_0 Layout.fillWidth: true
border.color: DefaultStyle.main2_100 Layout.preferredHeight: Math.round(104 * DefaultStyle.dp)
border.width: Math.round(2 * DefaultStyle.dp) topPadding: Math.round(12 * DefaultStyle.dp)
radius: Math.round(20 * DefaultStyle.dp) bottomPadding: Math.round(12 * DefaultStyle.dp)
height: parent.height / 2 leftPadding: Math.round(19 * DefaultStyle.dp)
rightPadding: Math.round(19 * DefaultStyle.dp)
Button {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
} anchors.topMargin: selectedFilesArea.topPadding
Rectangle { anchors.rightMargin: selectedFilesArea.rightPadding
anchors.bottom: parent.bottom icon.source: AppIcons.closeX
anchors.left: parent.left style: ButtonStyle.noBackground
anchors.right: parent.right onClicked: {
height: 2 * parent.height / 3 contents.clear()
}
}
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 background: Item{
icon.width: Math.round(12 * DefaultStyle.dp) anchors.fill: parent
icon.height: Math.round(12 * DefaultStyle.dp) 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.top: parent.top
anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
style: ButtonStyle.numericPad }
shadowEnabled: true Rectangle {
padding: Math.round(3 * DefaultStyle.dp) anchors.bottom: parent.bottom
onClicked: contents.removeContent(modelData) anchors.left: parent.left
anchors.right: parent.right
height: 2 * parent.height / 3
} }
} }
Control.ScrollBar.horizontal: selectedFilesScrollbar contentItem: ListView {
} id: selectedFiles
ScrollBar { orientation: ListView.Horizontal
id: selectedFilesScrollbar spacing: Math.round(16 * DefaultStyle.dp)
active: true model: ChatMessageContentProxy {
anchors.bottom: selectedFilesArea.bottom id: contents
anchors.left: selectedFilesArea.left filterType: ChatMessageContentProxy.FilterContentType.File
anchors.right: selectedFilesArea.right }
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)
}
}
Control.ScrollBar.horizontal: selectedFilesScrollbar
}
ScrollBar {
id: selectedFilesScrollbar
active: true
anchors.bottom: selectedFilesArea.bottom
anchors.left: selectedFilesArea.left
anchors.right: selectedFilesArea.right
}
} }
} }
ChatDroppableTextArea { ChatDroppableTextArea {
id: messageSender id: messageSender
Layout.fillWidth: true Control.SplitView.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
Layout.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : height Control.SplitView.minimumHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
chat: mainItem.chat
Component.onCompleted: { Component.onCompleted: {
if (mainItem.chat) text = mainItem.chat.core.sendingText if (mainItem.chat) text = mainItem.chat.core.sendingText
} }
onTextChanged: { onTextChanged: {
@ -270,12 +280,11 @@ RowLayout {
} }
mainItem.chat.core.sendingText = text mainItem.chat.core.sendingText = text
} }
onSendText: { onSendMessage: {
var filesContents = contents.getAll() var filesContents = contents.getAll()
if (filesContents.length === 0) if (filesContents.length === 0)
mainItem.chat.core.lSendTextMessage(text) mainItem.chat.core.lSendTextMessage(text)
else mainItem.chat.core.lSendMessage(text, filesContents) else mainItem.chat.core.lSendMessage(text, filesContents)
messageSender.textArea.clear()
contents.clear() contents.clear()
} }
onDropped: (files) => { onDropped: (files) => {

View file

@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic as Control
import SettingsCpp
import Linphone
AbstractSettingsLayout {
id: mainItem
width: parent?.width
contentModel: [
{
//: Attached files
title: qsTr("settings_chat_attached_files_title"),
subTitle: "",
contentComponent: attachedFilesParamComp,
// hideTopMargin: true
}
]
Component {
id: attachedFilesParamComp
SwitchSetting {
//: "Automatic download"
titleText: qsTr("settings_chat_attached_files_auto_download_title")
//: "Automatically download transferred or received files in conversations"
subTitleText: qsTr("settings_chat_attached_files_auto_download_subtitle")
propertyName: "autoDownloadReceivedFiles"
propertyOwner: SettingsCpp
Connections {
target: mainItem
function onSave() {
SettingsCpp.save()
}
}
}
}
}

View file

@ -2,7 +2,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls.Basic as Control import QtQuick.Controls.Basic as Control
import SettingsCpp 1.0 import SettingsCpp
import Linphone import Linphone
AbstractSettingsLayout { AbstractSettingsLayout {

View file

@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
import QtQuick import QtQuick
import Linphone
QtObject { QtObject {
property color main1_100: "#FFEACB" property color main1_100: "#FFEACB"

View file

@ -38,6 +38,23 @@
} }
} }
// White with orange icon
var player = {
color: {
normal: Linphone.DefaultStyle.grey_0,
hovered: Linphone.DefaultStyle.main1_100,
pressed: Linphone.DefaultStyle.main1_500_main
},
text: {
normal: Linphone.DefaultStyle.main1_500_main,
pressed: Linphone.DefaultStyle.main1_500_main
},
image: {
normal: Linphone.DefaultStyle.main1_500_main,
pressed: Linphone.DefaultStyle.main1_500_main
}
}
// Light orange // Light orange
var tertiary = { var tertiary = {
color: { color: {