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/phone-number/PhoneNumber.hpp"
#include "core/phone-number/PhoneNumberProxy.hpp"
#include "core/recorder/RecorderGui.hpp"
#include "core/register/RegisterPage.hpp"
#include "core/screen/ScreenList.hpp"
#include "core/screen/ScreenProxy.hpp"
@ -686,6 +687,7 @@ void App::initCppInterfaces() {
qmlRegisterType<FPSCounter>(Constants::MainQmlUri, 1, 0, "FPSCounter");
qmlRegisterType<EmojiModel>(Constants::MainQmlUri, 1, 0, "EmojiModel");
qmlRegisterType<SoundPlayerGui>(Constants::MainQmlUri, 1, 0, "SoundPlayerGui");
qmlRegisterType<RecorderGui>(Constants::MainQmlUri, 1, 0, "RecorderGui");
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/SoundPlayerGui.cpp
core/recorder/RecorderCore.cpp
core/recorder/RecorderGui.cpp
core/videoSource/VideoSourceDescriptorCore.cpp
core/videoSource/VideoSourceDescriptorGui.cpp

View file

@ -117,6 +117,15 @@ void ChatCore::setSelf(QSharedPointer<ChatCore> me) {
mChatModelConnection->makeConnectToCore(&ChatCore::lDeleteHistory, [this]() {
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(
&ChatCore::lLeave, [this]() { mChatModelConnection->invokeToModel([this]() { mChatModel->leave(); }); });
mChatModelConnection->makeConnectToModel(&ChatModel::historyDeleted, [this]() {
@ -458,8 +467,13 @@ void ChatCore::appendEventLogsToEventLogList(QList<QSharedPointer<EventLogCore>>
void ChatCore::appendEventLogToEventLogList(QSharedPointer<EventLogCore> e) {
if (mEventLogList.contains(e)) return;
mEventLogList.append(e);
emit eventsInserted({e});
auto it = std::find_if(mEventLogList.begin(), mEventLogList.end(), [e](QSharedPointer<EventLogCore> event) {
return e->getEventLogId() == event->getEventLogId();
});
if (it == mEventLogList.end()) {
mEventLogList.append(e);
emit eventsInserted({e});
}
}
void ChatCore::removeEventLogsFromEventLogList(QList<QSharedPointer<EventLogCore>> list) {

View file

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

View file

@ -90,6 +90,12 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
for (auto content : chatmessage->getContents()) {
auto contentCore = ChatMessageContentCore::create(content, mChatMessageModel);
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();
mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString();
@ -122,11 +128,6 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mIsForward = chatmessage->isForward();
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() {
@ -156,6 +157,9 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lRemoveReaction, [this]() {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->removeReaction(); });
});
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lSend, [this]() {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->send(); });
});
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::newMessageReaction,
[this](const std::shared_ptr<linphone::ChatMessage> &message,
@ -431,3 +435,7 @@ std::shared_ptr<ChatMessageModel> ChatMessageCore::getModel() const {
// ConferenceInfoGui *ChatMessageCore::getConferenceInfoGui() const {
// 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/>.
*/
#ifndef CHATMESSAGECORE_H_
#define CHATMESSAGECORE_H_
#ifndef CHAT_MESSAGE_CORE_H_
#define CHAT_MESSAGE_CORE_H_
#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/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp"
@ -118,7 +118,7 @@ public:
void setMessageState(LinphoneEnums::ChatMessageState state);
std::shared_ptr<ChatMessageModel> getModel() const;
// ConferenceInfoGui *getConferenceInfoGui() const;
Q_INVOKABLE ChatMessageContentGui *getVoiceRecordingContent() const;
signals:
void timestampChanged(QDateTime timestamp);
@ -136,6 +136,7 @@ signals:
void readChanged();
void lSendReaction(const QString &reaction);
void lRemoveReaction();
void lSend();
private:
DECLARE_ABSTRACT_OBJECT
@ -164,10 +165,12 @@ private:
bool mIsOutgoing = false;
LinphoneEnums::ChatMessageState mMessageState;
QList<QSharedPointer<ChatMessageContentCore>> mChatMessageContentList;
// for voice recording creation message
QSharedPointer<ChatMessageContentCore> mVoiceRecordingContent;
// QSharedPointer<ConferenceInfoCore> mConferenceInfo = nullptr;
std::shared_ptr<ChatMessageModel> mChatMessageModel;
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 (mFilterType == (int)FilterContentType::Unknown) return false;
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::Voice) return contentCore->isVoiceRecording();
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) {
shared_ptr<linphone::ChatMessage> message = messages.front();
auto receiverAccount = ToolModel::findAccount(message->getToAddress());
if (receiverAccount) {
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();
remoteAddr->clean();
remoteAddress = Utils::coreStringToAppString(remoteAddr->asStringUriOnly());
@ -356,9 +356,22 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr<linphone::ChatRoom>
if (txt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation received!' : Notification about receiving an invitation to a conference.
txt = tr("new_conference_invitation");
};
if (messages.size() == 1) { // Display only sender on mono message.
getMessage(message);
} else {
//: 'New messages received!' Notification that warn the user of new messages.
txt = tr("new_chat_room_messages");
int unreadCount = 0;
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);

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
mVideoEnabled = settingsModel->getVideoEnabled();
mEchoCancellationEnabled = settingsModel->getEchoCancellationEnabled();
mAutoDownloadReceivedFiles = settingsModel->getAutoDownloadReceivedFiles();
mAutomaticallyRecordCallsEnabled = settingsModel->getAutomaticallyRecordCallsEnabled();
// Audio
@ -143,6 +144,7 @@ SettingsCore::SettingsCore(const SettingsCore &settingsCore) {
// Call
mVideoEnabled = settingsCore.mVideoEnabled;
mEchoCancellationEnabled = settingsCore.mEchoCancellationEnabled;
mAutoDownloadReceivedFiles = settingsCore.mAutoDownloadReceivedFiles;
mAutomaticallyRecordCallsEnabled = settingsCore.mAutomaticallyRecordCallsEnabled;
// Audio
@ -233,6 +235,12 @@ void SettingsCore::setSelf(QSharedPointer<SettingsCore> me) {
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
mSettingsModelConnection->makeConnectToModel(
&SettingsModel::automaticallyRecordCallsEnabledChanged, [this](const bool enabled) {
@ -462,6 +470,7 @@ void SettingsCore::reset(const SettingsCore &settingsCore) {
setEchoCancellationEnabled(settingsCore.mEchoCancellationEnabled);
setAutomaticallyRecordCallsEnabled(settingsCore.mAutomaticallyRecordCallsEnabled);
setAutoDownloadReceivedFiles(settingsCore.mAutoDownloadReceivedFiles);
// Audio
setCaptureDevices(settingsCore.mCaptureDevices);
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) {
if (mAutomaticallyRecordCallsEnabled != enabled) {
mAutomaticallyRecordCallsEnabled = enabled;
@ -960,6 +977,9 @@ void SettingsCore::writeIntoModel(std::shared_ptr<SettingsModel> model) const {
model->setEchoCancellationEnabled(mEchoCancellationEnabled);
model->setAutomaticallyRecordCallsEnabled(mAutomaticallyRecordCallsEnabled);
// Chat
model->setAutoDownloadReceivedFiles(mAutoDownloadReceivedFiles);
// Audio
model->setRingerDevice(mRingerDevice);
model->setCaptureDevice(mCaptureDevice);
@ -1022,6 +1042,9 @@ void SettingsCore::writeFromModel(const std::shared_ptr<SettingsModel> &model) {
mEchoCancellationEnabled = model->getEchoCancellationEnabled();
mAutomaticallyRecordCallsEnabled = model->getAutomaticallyRecordCallsEnabled();
// Chat
mAutoDownloadReceivedFiles = model->getAutoDownloadReceivedFiles();
// Audio
mCaptureDevices = model->getCaptureDevices();
mPlaybackDevices = model->getPlaybackDevices();

View file

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

View file

@ -29,14 +29,6 @@
DEFINE_ABSTRACT_OBJECT(SoundPlayerCore)
// =============================================================================
using namespace std;
namespace {
int ForceCloseTimerInterval = 20;
}
// -----------------------------------------------------------------------------
QSharedPointer<SoundPlayerCore> SoundPlayerCore::create() {
@ -114,7 +106,7 @@ void SoundPlayerCore::buildInternalPlayer(QSharedPointer<SoundPlayerCore> me) {
mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->play(mSource); });
});
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->invokeToCore([this, pos] { setPosition(pos); });
@ -126,12 +118,15 @@ void SoundPlayerCore::buildInternalPlayer(QSharedPointer<SoundPlayerCore> me) {
});
});
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::eofReached,
[this](const shared_ptr<linphone::Player> &player) {
[this](const std::shared_ptr<linphone::Player> &player) {
mSoundPlayerModelConnection->invokeToCore([this] {
mForceClose = true;
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::stopped, this, &SoundPlayerGui::stopped);
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) {
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);

View file

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

View file

@ -823,6 +823,19 @@
"puppy eyes"
]
},
{
"code": "1f979",
"char": "🥹",
"name": "face holding back tears",
"keywords": [
"tears",
"emotive",
"admiration",
"face with tears",
"gratitude",
"admiration"
]
},
{
"code": "1f626",
"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

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

View file

@ -40,4 +40,4 @@ void CallHistoryModel::removeCallHistory() {
mustBeInLinphoneThread(getClassName() + "::removeCallHistory");
qInfo() << "Removing call log: " << Utils::coreStringToAppString(callLog->getCallId());
CoreModel::getInstance()->getCore()->removeCallLog(callLog);
}
}

View file

@ -119,6 +119,10 @@ void ChatModel::deleteHistory() {
emit historyDeleted();
}
void ChatModel::deleteMessage(std::shared_ptr<linphone::ChatMessage> message) {
mMonitor->deleteMessage(message);
}
void ChatModel::leave() {
mMonitor->leave();
}
@ -128,6 +132,11 @@ void ChatModel::deleteChatRoom() {
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) {
return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text));
}

View file

@ -47,8 +47,11 @@ public:
std::list<std::shared_ptr<linphone::ChatMessage>> getHistory() const;
QString getIdentifier() const;
void deleteHistory();
void deleteMessage(std::shared_ptr<linphone::ChatMessage> message);
void deleteChatRoom();
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> createMessage(QString text,
QList<std::shared_ptr<ChatMessageContentModel>> filesContent);

View file

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

View file

@ -59,6 +59,8 @@ public:
void removeReaction();
void send();
linphone::ChatMessage::State getState() 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 {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return CoreModel::getInstance()->getCore()->echoCancellationEnabled();
@ -558,6 +569,18 @@ QString SettingsModel::getLogsFolder(const shared_ptr<linphone::Config> &config)
: 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 {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto core = CoreModel::getInstance()->getCore();

View file

@ -61,6 +61,9 @@ public:
bool getEchoCancellationEnabled() const;
void setEchoCancellationEnabled(bool enabled);
void setAutoDownloadReceivedFiles(bool enabled);
bool getAutoDownloadReceivedFiles() const;
// Audio. --------------------------------------------------------------------
bool getIsInCall() const;
@ -137,6 +140,7 @@ public:
QString getLogsFolder() const;
void setLogsFolder(const QString &folder);
static QString getLogsFolder(const std::shared_ptr<linphone::Config> &config);
QString getSavedCallsFolder() const;
QString getLogsUploadUrl() const;
void setLogsUploadUrl(const QString &url);
@ -238,6 +242,9 @@ signals:
void dndChanged(bool value);
// Messages. --------------------------------------------------------------------
void autoDownloadReceivedFilesChanged(bool enabled);
private:
void notifyConfigReady();
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);
emit positionChanged(mMonitor->getCurrentPosition());
}

View file

@ -46,7 +46,7 @@ public:
void pause();
bool play(QString source);
void stop(bool force = false);
void seek(int offset);
void seek(QString source, int offset);
int getPosition() const;
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/path/Paths.hpp"
#include "core/payload-type/DownloadablePayloadTypeCore.hpp"
#include "core/recorder/RecorderGui.hpp"
#include "model/object/VariantObject.hpp"
#include "model/tool/ToolModel.hpp"
#include "tool/providers/AvatarProvider.hpp"
@ -1948,6 +1949,56 @@ QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) {
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) {
if (path.isEmpty()) return false;
return QMimeDatabase().mimeTypeForFile(path).name().contains("video/");

View file

@ -52,6 +52,7 @@ class ConferenceCore;
class ParticipantDeviceCore;
class DownloadablePayloadTypeCore;
class ChatGui;
class RecorderGui;
class Utils : public QObject, public AbstractObject {
Q_OBJECT
@ -174,6 +175,9 @@ public:
static QDateTime getOffsettedUTC(const QDateTime &date);
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);
static QString getApplicationProduct();

View file

@ -149,6 +149,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Page/Layout/Settings/DebugSettingsLayout.qml
view/Page/Layout/Settings/LdapSettingsLayout.qml
view/Page/Layout/Settings/CarddavSettingsLayout.qml
view/Page/Layout/Settings/ChatSettingsLayout.qml
view/Page/Layout/Settings/SecuritySettingsLayout.qml
view/Page/Layout/Settings/NetworkSettingsLayout.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 borderColor: style?.borderColor || "transparent"
ToolTip.visible: hovered && ToolTip.text != ""
ToolTip.delay: 1000
ToolTip.delay: 500
property color disabledFilterColor: color.hslLightness > 0.5
? DefaultStyle.grey_0
: DefaultStyle.grey_400
@ -199,9 +199,20 @@ Control.Button {
}
Component{
id: imageComponent
ButtonImage{
Item {
width: stacklayout.width
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{

View file

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

View file

@ -7,41 +7,45 @@ import UtilsCpp
// =============================================================================
Loader{
Item {
id: mainItem
property ChatMessageContentGui chatMessageContentGui
property int availableWidth : parent.width
// 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 : soundPlayerGui && soundPlayerGui.core.playbackState === LinphoneEnums.PlaybackState.PlayingState
onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop()
width: mainItem.width
height: mainItem.height
clip: false
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
SoundPlayerGui {
signal voiceRecordingMessageCreationRequested(RecorderGui recorderGui)
signal stopRecording()
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)
}
}
Loader {
id: soudPlayerLoader
property int duration: mainItem.chatMessageContentGui
? mainItem.chatMessageContentGui.core.fileDuration
: item
? item.core.duration
: 0
property int position: item?.core.position || 0
active: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.isVoiceRecording
sourceComponent: 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
function play(){
if(loadedItem.isPlaying){// Pause the play
if(mainItem.isPlaying){// Pause the play
soundPlayerGui.core.lPause()
}else{// Play the audio
soundPlayerGui.core.lPlay()
@ -51,41 +55,82 @@ Loader{
mediaProgressBar.value = 101
}
onPositionChanged: {
mediaProgressBar.progressPosition = position
mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / duration)
mediaProgressBar.progressPosition = soudPlayerLoader.position
mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / soudPlayerLoader.duration)
}
onSourceChanged: if (source != "") {
// core.lPlay()// This will open the file and allow seeking
// core.lPause()
core.lOpen()
core.lOpen() // Open the file and allow seeking
mediaProgressBar.value = 0
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
MediaProgressBar{
id: mediaProgressBar
anchors.fill: parent
progressDuration: soundPlayerGui ? soundPlayerGui.duration : chatMessageContentGui.core.fileDuration
progressPosition: 0
value: 0
function refresh(){
if(soundPlayerGui){
soundPlayerGui.core.lRefreshPosition()
}
Connections {
target: mainItem
function onStopRecording() {
recorderLoader.item.core.lStop()
}
onEndReached:{
if(soundPlayerGui)
soundPlayerGui.core.lStop()
}
onPlayStopButtonToggled: soundPlayerGui.play()
onRefreshPositionRequested: refresh()
onSeekRequested: (ms) => {
if(soundPlayerGui) {
soundPlayerGui.core.lSeek(ms)
}
sourceComponent: RecorderGui {
id: recorderGui
onReady: core.lStart()
onStateChanged: (state) => {
if (state === LinphoneEnums.RecorderState.Running) mediaProgressBar.start()
if (state === LinphoneEnums.RecorderState.Closed) {
mediaProgressBar.stop()
mainItem.voiceRecordingMessageCreationRequested(recorderGui)
}
}
}
}
}
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)
Layout.fillWidth: true
onClicked: {
//: Delete the chat ?
//: Delete the conversation ?
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"),
"",
function(confirmed) {

View file

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

View file

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

View file

@ -11,26 +11,27 @@ 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 alias placeholderText: sendingTextArea.placeholderText
property string text
property var textArea
// property alias cursorPosition: sendingTextArea.cursorPosition
property bool emojiPickerButtonChecked
property bool dropEnabled: true
property string dropDisabledReason
property bool isEphemeral : false
property bool emojiVisible: false
property ChatGui chat
// ---------------------------------------------------------------------------
signal dropped (var files)
signal validText (string text)
signal sendText()
signal audioRecordRequest()
signal sendMessage()
signal emojiClicked()
signal composing()
// ---------------------------------------------------------------------------
function _emitFiles (files) {
@ -63,118 +64,196 @@ Control.Control {
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: {
fileDialog.open()
contentItem: Control.StackView {
id: sendingAreaStackView
initialItem: textAreaComp
Component {
id: textAreaComp
RowLayout {
// disable record button if call ongoing
CallProxy {
id: callsModel
sourceModel: AppCpp.calls
}
}
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
spacing: Math.round(16 * DefaultStyle.dp)
BigButton {
id: emojiPickerButton
style: ButtonStyle.noBackground
checkable: true
icon.source: checked ? AppIcons.closeX : AppIcons.smiley
onCheckedChanged: mainItem.emojiPickerButtonChecked = checked
Connections {
target: mainItem
function onEmojiPickerButtonCheckedChanged() {
emojiPickerButton.checked = mainItem.emojiPickerButtonChecked
}
}
}
contentItem: RowLayout {
Flickable {
id: sendingAreaFlickable
Layout.fillWidth: true
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
BigButton {
style: ButtonStyle.noBackground
icon.source: AppIcons.paperclip
onClicked: {
fileDialog.open()
}
}
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
}
Layout.fillHeight: true
contentHeight: sendingTextArea.contentHeight
contentWidth: width
}
contentItem: RowLayout {
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) {
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
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;
}
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.sendText()
event.accepted = true
TextArea {
id: sendingTextArea
width: sendingAreaFlickable.width
height: sendingAreaFlickable.height
textFormat: TextEdit.AutoText
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 {
visible: sendingTextArea.text.length === 0
style: ButtonStyle.noBackground
icon.source: AppIcons.microphone
onClicked: {
console.log("TODO : go to record message")
}
}
}
}
Component {
id: voiceMessageRecordComp
RowLayout {
spacing: Math.round(16 * DefaultStyle.dp)
RoundButton {
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 {
visible: sendingTextArea.text.length !== 0
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
onClicked: {
mainItem.sendText()
else {
sendVoiceRecordingOnCreated = true
voiceMessage.stopRecording()
}
}
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: {
mainWindow.showConfirmationLambdaPopup(qsTr(""),
mainWindow.showConfirmationLambdaPopup("",
qsTr("chat_view_group_call_toast_message"),
"",
function(confirmed) {
@ -115,153 +115,163 @@ RowLayout {
}
]
content: ColumnLayout {
spacing: 0
content: Control.SplitView {
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
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
}
orientation: Qt.Vertical
handle: Rectangle {
implicitHeight: Math.round(8 * DefaultStyle.dp)
color: Control.SplitHandle.hovered ? DefaultStyle.grey_200 : DefaultStyle.grey_100
}
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)
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()
ColumnLayout {
spacing: 0
Control.SplitView.fillHeight: true
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
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
}
}
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
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)
Button {
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)
anchors.topMargin: selectedFilesArea.topPadding
anchors.rightMargin: selectedFilesArea.rightPadding
icon.source: AppIcons.closeX
style: ButtonStyle.noBackground
onClicked: {
contents.clear()
}
RoundButton {
icon.source: AppIcons.closeX
icon.width: Math.round(12 * DefaultStyle.dp)
icon.height: Math.round(12 * DefaultStyle.dp)
}
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
style: ButtonStyle.numericPad
shadowEnabled: true
padding: Math.round(3 * DefaultStyle.dp)
onClicked: contents.removeContent(modelData)
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 2 * parent.height / 3
}
}
Control.ScrollBar.horizontal: selectedFilesScrollbar
}
ScrollBar {
id: selectedFilesScrollbar
active: true
anchors.bottom: selectedFilesArea.bottom
anchors.left: selectedFilesArea.left
anchors.right: selectedFilesArea.right
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)
}
}
Control.ScrollBar.horizontal: selectedFilesScrollbar
}
ScrollBar {
id: selectedFilesScrollbar
active: true
anchors.bottom: selectedFilesArea.bottom
anchors.left: selectedFilesArea.left
anchors.right: selectedFilesArea.right
}
}
}
ChatDroppableTextArea {
id: messageSender
Layout.fillWidth: true
Layout.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : height
Control.SplitView.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
Control.SplitView.minimumHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
chat: mainItem.chat
Component.onCompleted: {
if (mainItem.chat) text = mainItem.chat.core.sendingText
}
onTextChanged: {
@ -270,12 +280,11 @@ RowLayout {
}
mainItem.chat.core.sendingText = text
}
onSendText: {
onSendMessage: {
var filesContents = contents.getAll()
if (filesContents.length === 0)
mainItem.chat.core.lSendTextMessage(text)
else mainItem.chat.core.lSendMessage(text, filesContents)
messageSender.textArea.clear()
contents.clear()
}
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.Layouts
import QtQuick.Controls.Basic as Control
import SettingsCpp 1.0
import SettingsCpp
import Linphone
AbstractSettingsLayout {

View file

@ -1,5 +1,6 @@
pragma Singleton
import QtQuick
import Linphone
QtObject {
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
var tertiary = {
color: {