From 8fb42c333c6b292315ea58be3e50b49024bb2e3e Mon Sep 17 00:00:00 2001 From: Gaelle Braud Date: Tue, 22 Apr 2025 09:25:17 +0200 Subject: [PATCH] chat list chat messages view update sdk --- Linphone/CMakeLists.txt | 2 +- Linphone/core/App.cpp | 10 + Linphone/core/CMakeLists.txt | 8 + Linphone/core/call/CallList.cpp | 4 +- Linphone/core/call/CallProxy.cpp | 2 +- Linphone/core/chat/ChatCore.cpp | 195 ++++++++++++ Linphone/core/chat/ChatCore.hpp | 100 ++++++ Linphone/core/chat/ChatGui.cpp | 38 +++ Linphone/core/chat/ChatGui.hpp | 41 +++ Linphone/core/chat/ChatList.cpp | 110 +++++++ Linphone/core/chat/ChatList.hpp | 54 ++++ Linphone/core/chat/ChatProxy.cpp | 60 ++++ Linphone/core/chat/ChatProxy.hpp | 47 +++ .../core/chat/message/ChatMessageCore.cpp | 84 +++++ .../core/chat/message/ChatMessageCore.hpp | 67 ++++ Linphone/core/chat/message/ChatMessageGui.cpp | 38 +++ Linphone/core/chat/message/ChatMessageGui.hpp | 41 +++ .../core/chat/message/ChatMessageList.cpp | 109 +++++++ .../core/chat/message/ChatMessageList.hpp | 63 ++++ .../core/chat/message/ChatMessageProxy.cpp | 70 ++++ .../core/chat/message/ChatMessageProxy.hpp | 56 ++++ Linphone/data/image/paper-plane-right.svg | 1 + Linphone/data/languages/de.ts | 301 +++++++++++------- Linphone/data/languages/en.ts | 301 +++++++++++------- Linphone/data/languages/fr_FR.ts | 301 +++++++++++------- Linphone/model/CMakeLists.txt | 3 + Linphone/model/chat/ChatModel.cpp | 262 +++++++++++++++ Linphone/model/chat/ChatModel.hpp | 194 +++++++++++ .../model/chat/message/ChatMessageModel.cpp | 57 ++++ .../model/chat/message/ChatMessageModel.hpp | 51 +++ Linphone/model/setting/SettingsModel.cpp | 2 +- Linphone/model/tool/ToolModel.cpp | 16 + Linphone/model/tool/ToolModel.hpp | 3 + Linphone/tool/Utils.cpp | 4 +- Linphone/tool/Utils.hpp | 2 +- Linphone/view/CMakeLists.txt | 5 + Linphone/view/Control/Button/BigButton.qml | 12 +- Linphone/view/Control/Button/Button.qml | 41 ++- .../view/Control/Button/CalendarComboBox.qml | 2 +- .../view/Control/Container/VerticalTabBar.qml | 1 - .../Control/Display/Chat/ChatListView.qml | 244 ++++++++++++++ .../view/Control/Display/Chat/ChatMessage.qml | 74 +++++ .../Display/Chat/ChatMessagesListView.qml | 34 ++ .../view/Control/Display/Contact/Avatar.qml | 173 +++++----- .../Conversation/ConversationListView.qml | 247 ++++++++++++++ Linphone/view/Control/Input/TextField.qml | 6 +- .../view/Page/Form/Chat/SelectedChatView.qml | 186 +++++++++++ Linphone/view/Page/Layout/Main/MainLayout.qml | 3 +- Linphone/view/Page/Main/Call/CallPage.qml | 3 +- Linphone/view/Page/Main/Chat/ChatPage.qml | 253 +++++++++++++++ Linphone/view/Style/AppIcons.qml | 6 +- Linphone/view/Style/Typography.qml | 7 + Linphone/view/Style/buttonStyle.js | 9 +- external/linphone-sdk | 2 +- 54 files changed, 3508 insertions(+), 497 deletions(-) create mode 100644 Linphone/core/chat/ChatCore.cpp create mode 100644 Linphone/core/chat/ChatCore.hpp create mode 100644 Linphone/core/chat/ChatGui.cpp create mode 100644 Linphone/core/chat/ChatGui.hpp create mode 100644 Linphone/core/chat/ChatList.cpp create mode 100644 Linphone/core/chat/ChatList.hpp create mode 100644 Linphone/core/chat/ChatProxy.cpp create mode 100644 Linphone/core/chat/ChatProxy.hpp create mode 100644 Linphone/core/chat/message/ChatMessageCore.cpp create mode 100644 Linphone/core/chat/message/ChatMessageCore.hpp create mode 100644 Linphone/core/chat/message/ChatMessageGui.cpp create mode 100644 Linphone/core/chat/message/ChatMessageGui.hpp create mode 100644 Linphone/core/chat/message/ChatMessageList.cpp create mode 100644 Linphone/core/chat/message/ChatMessageList.hpp create mode 100644 Linphone/core/chat/message/ChatMessageProxy.cpp create mode 100644 Linphone/core/chat/message/ChatMessageProxy.hpp create mode 100644 Linphone/data/image/paper-plane-right.svg create mode 100644 Linphone/model/chat/ChatModel.cpp create mode 100644 Linphone/model/chat/ChatModel.hpp create mode 100644 Linphone/model/chat/message/ChatMessageModel.cpp create mode 100644 Linphone/model/chat/message/ChatMessageModel.hpp create mode 100644 Linphone/view/Control/Display/Chat/ChatListView.qml create mode 100644 Linphone/view/Control/Display/Chat/ChatMessage.qml create mode 100644 Linphone/view/Control/Display/Chat/ChatMessagesListView.qml create mode 100644 Linphone/view/Control/Display/Conversation/ConversationListView.qml create mode 100644 Linphone/view/Page/Form/Chat/SelectedChatView.qml create mode 100644 Linphone/view/Page/Main/Chat/ChatPage.qml diff --git a/Linphone/CMakeLists.txt b/Linphone/CMakeLists.txt index 0fee56f86..f693f59f7 100644 --- a/Linphone/CMakeLists.txt +++ b/Linphone/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(Linphone VERSION 6.0.0 LANGUAGES CXX) +project(Linphone VERSION 6.1.0 LANGUAGES CXX) ################################################################ # PACKAGES diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 1a4281f49..be897a337 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -52,6 +52,10 @@ #include "core/call/CallGui.hpp" #include "core/call/CallList.hpp" #include "core/call/CallProxy.hpp" +#include "core/chat/ChatProxy.hpp" +#include "core/chat/message/ChatMessageProxy.hpp" +#include "core/chat/message/ChatMessageList.hpp" +#include "core/chat/message/ChatMessageGui.hpp" #include "core/camera/CameraGui.hpp" #include "core/conference/ConferenceGui.hpp" #include "core/conference/ConferenceInfoGui.hpp" @@ -652,6 +656,12 @@ void App::initCppInterfaces() { qmlRegisterType(Constants::MainQmlUri, 1, 0, "CallHistoryProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "CallGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "CallProxy"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatList"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatProxy"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatGui"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageGui"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageList"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ChatMessageProxy"); qmlRegisterUncreatableType(Constants::MainQmlUri, 1, 0, "ConferenceCore", QLatin1String("Uncreatable")); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ConferenceGui"); diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index c40492f8c..e7c649d9c 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -19,6 +19,14 @@ list(APPEND _LINPHONEAPP_SOURCES core/camera/CameraGui.cpp core/camera/CameraDummy.cpp core/camera/PreviewManager.cpp + core/chat/ChatCore.cpp + core/chat/ChatGui.cpp + core/chat/ChatList.cpp + core/chat/ChatProxy.cpp + core/chat/message/ChatMessageCore.cpp + core/chat/message/ChatMessageGui.cpp + core/chat/message/ChatMessageList.cpp + core/chat/message/ChatMessageProxy.cpp core/fps-counter/FPSCounter.cpp core/friend/FriendCore.cpp core/friend/FriendGui.cpp diff --git a/Linphone/core/call/CallList.cpp b/Linphone/core/call/CallList.cpp index 96ac873fb..083a19f3a 100644 --- a/Linphone/core/call/CallList.cpp +++ b/Linphone/core/call/CallList.cpp @@ -129,7 +129,7 @@ void CallList::setSelf(QSharedPointer me) { }); mModelConnection->makeConnectToModel(&CoreModel::firstCallStarted, - [this]() { mModelConnection->invokeToCore([this]() { lUpdate(); }); }); + [this]() { mModelConnection->invokeToCore([this]() { lUpdate(); }); }); mModelConnection->makeConnectToModel(&CoreModel::lastCallEnded, [this]() { mModelConnection->invokeToCore([this]() { setHaveCall(false); @@ -158,7 +158,7 @@ CallGui *CallList::getCurrentCall() const { else return nullptr; } -void CallList::setCurrentCall(CallGui* callGui) { +void CallList::setCurrentCall(CallGui *callGui) { auto callCore = callGui ? callGui->mCore : nullptr; if (mCurrentCall != callCore) { mCurrentCall = callCore; diff --git a/Linphone/core/call/CallProxy.cpp b/Linphone/core/call/CallProxy.cpp index c52bae809..03a12bdde 100644 --- a/Linphone/core/call/CallProxy.cpp +++ b/Linphone/core/call/CallProxy.cpp @@ -63,7 +63,7 @@ void CallProxy::setSourceModel(QAbstractItemModel *model) { connect(newCallList, &CallList::haveCallChanged, this, &CallProxy::haveCallChanged, Qt::QueuedConnection); connect(this, &CallProxy::lMergeAll, newCallList, &CallList::lMergeAll); } - setSourceModels(new SortFilterList(model, Qt::AscendingOrder)); + setSourceModels(new SortFilterList(model)); } bool CallProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { diff --git a/Linphone/core/chat/ChatCore.cpp b/Linphone/core/chat/ChatCore.cpp new file mode 100644 index 000000000..20b83fcab --- /dev/null +++ b/Linphone/core/chat/ChatCore.cpp @@ -0,0 +1,195 @@ +/* + * 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 . + */ + +#include "ChatCore.hpp" +#include "core/App.hpp" +#include "core/friend/FriendCore.hpp" +#include "core/setting/SettingsCore.hpp" +#include "model/tool/ToolModel.hpp" +#include "tool/Utils.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatCore) + +/***********************************************************************/ + +QSharedPointer ChatCore::create(const std::shared_ptr &chatRoom) { + auto sharedPointer = QSharedPointer(new ChatCore(chatRoom), &QObject::deleteLater); + sharedPointer->setSelf(sharedPointer); + sharedPointer->moveToThread(App::getInstance()->thread()); + return sharedPointer; +} + +ChatCore::ChatCore(const std::shared_ptr &chatRoom) : QObject(nullptr) { + lDebug() << "[ChatCore] new" << this; + mustBeInLinphoneThread(getClassName()); + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); + mLastUpdatedTime = QDateTime::fromSecsSinceEpoch(chatRoom->getLastUpdateTime()); + if (chatRoom->hasCapability((int)linphone::ChatRoom::Capabilities::Basic)) { + mTitle = ToolModel::getDisplayName(chatRoom->getPeerAddress()->clone()); + mAvatarUri = Utils::coreStringToAppString(chatRoom->getPeerAddress()->asStringUriOnly()); + auto peerAddress = chatRoom->getPeerAddress(); + mPeerAddress = Utils::coreStringToAppString(peerAddress->asStringUriOnly()); + } else { + if (chatRoom->hasCapability((int)linphone::ChatRoom::Capabilities::OneToOne)) { + auto peer = chatRoom->getParticipants().front(); + if (peer) mTitle = ToolModel::getDisplayName(peer->getAddress()->clone()); + mAvatarUri = Utils::coreStringToAppString(peer->getAddress()->asStringUriOnly()); + auto participants = chatRoom->getParticipants(); + if (participants.size() == 1) { + auto peerAddress = participants.front()->getAddress(); + if (peerAddress) mPeerAddress = Utils::coreStringToAppString(peerAddress->asStringUriOnly()); + } + } else if (chatRoom->hasCapability((int)linphone::ChatRoom::Capabilities::Conference)) { + mTitle = Utils::coreStringToAppString(chatRoom->getSubject()); + mAvatarUri = Utils::coreStringToAppString(chatRoom->getSubject()); + } + } + mUnreadMessagesCount = chatRoom->getUnreadMessagesCount(); + mChatModel = Utils::makeQObject_ptr(chatRoom); + mChatModel->setSelf(mChatModel); + mLastMessageInHistory = mChatModel->getLastMessageInHistory(); + auto history = chatRoom->getHistory(0, (int)linphone::ChatRoom::HistoryFilter::ChatMessage); + std::list> linHistory; + for (auto &eventLog : history) { + if (eventLog->getChatMessage()) linHistory.push_back(eventLog->getChatMessage()); + } + for (auto &message : linHistory) { + if (!message) continue; + auto chatMessage = ChatMessageCore::create(message); + mChatMessageList.append(chatMessage); + } +} + +ChatCore::~ChatCore() { + lDebug() << "[ChatCore] delete" << this; + mustBeInMainThread("~" + getClassName()); + emit mChatModel->removeListener(); +} + +void ChatCore::setSelf(QSharedPointer me) { + mChatModelConnection = SafeConnection::create(me, mChatModel); + // mChatModelConnection->makeConnectToCore(&ChatCore::lSetMicrophoneMuted, [this](bool isMuted) { + // mChatModelConnection->invokeToModel( + // [this, isMuted]() { mChatModel->setMicrophoneMuted(isMuted); }); + // }); + mChatModelConnection->makeConnectToModel(&ChatModel::chatMessageReceived, + [this](const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + if (mChatModel->getMonitor() != chatRoom) return; + qDebug() << "MESSAGE RECEIVED IN CHATROOM" << mChatModel->getTitle(); + // mChatModelConnection->invokeToCore([this, isMuted]() { + // setMicrophoneMuted(isMuted); }); + }); +} + +QDateTime ChatCore::getLastUpdatedTime() const { + return mLastUpdatedTime; +} + +void ChatCore::setLastUpdatedTime(QDateTime time) { + if (mLastUpdatedTime != time) { + mLastUpdatedTime = time; + emit lastUpdatedTimeChanged(time); + } +} + +QString ChatCore::getTitle() const { + return mTitle; +} + +void ChatCore::setTitle(QString title) { + if (mTitle != title) { + mTitle = title; + emit titleChanged(title); + } +} + +QString ChatCore::getPeerAddress() const { + return mPeerAddress; +} + +void ChatCore::setPeerAddress(QString peerAddress) { + if (mPeerAddress != peerAddress) { + mPeerAddress = peerAddress; + emit peerAddressChanged(peerAddress); + } +} + +QString ChatCore::getAvatarUri() const { + return mAvatarUri; +} + +void ChatCore::setAvatarUri(QString avatarUri) { + if (mAvatarUri != avatarUri) { + mAvatarUri = avatarUri; + emit avatarUriChanged(); + } +} + +QString ChatCore::getLastMessageInHistory() const { + return mLastMessageInHistory; +} + +void ChatCore::setLastMessageInHistory(QString lastMessageInHistory) { + if (mLastMessageInHistory != lastMessageInHistory) { + mLastMessageInHistory = lastMessageInHistory; + emit lastMessageInHistoryChanged(lastMessageInHistory); + } +} + +int ChatCore::getUnreadMessagesCount() const { + return mUnreadMessagesCount; +} + +void ChatCore::setUnreadMessagesCount(int count) { + if (mUnreadMessagesCount != count) { + mUnreadMessagesCount = count; + emit unreadMessagesCountChanged(count); + } +} + +QList> ChatCore::getChatMessageList() const { + return mChatMessageList; +} +void ChatCore::resetChatMessageList(QList> list) { + mChatMessageList = list; + emit messageListChanged(); +} + +void ChatCore::appendMessagesToMessageList(QList> list) { + int nbAdded = 0; + for (auto &message : list) { + if (mChatMessageList.contains(message)) continue; + mChatMessageList.append(message); + ++nbAdded; + } + if (nbAdded > 0) emit messageListChanged(); +} + +void ChatCore::removeMessagesFromMessageList(QList> list) { + int nbRemoved = 0; + for (auto &message : list) { + if (mChatMessageList.contains(message)) { + mChatMessageList.removeAll(message); + ++nbRemoved; + } + } + if (nbRemoved > 0) emit messageListChanged(); +} diff --git a/Linphone/core/chat/ChatCore.hpp b/Linphone/core/chat/ChatCore.hpp new file mode 100644 index 000000000..89c22b255 --- /dev/null +++ b/Linphone/core/chat/ChatCore.hpp @@ -0,0 +1,100 @@ +/* + * 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 . + */ + +#ifndef CHAT_CORE_H_ +#define CHAT_CORE_H_ + +#include "core/chat/message/ChatMessageCore.hpp" +#include "model/chat/ChatModel.hpp" +#include "model/search/MagicSearchModel.hpp" +#include "tool/LinphoneEnums.hpp" +#include "tool/thread/SafeConnection.hpp" +#include +#include +#include + +class ChatCore : public QObject, public AbstractObject { + Q_OBJECT + +public: + Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) + Q_PROPERTY(QString peerAddress READ getPeerAddress WRITE setPeerAddress NOTIFY peerAddressChanged) + Q_PROPERTY(QString avatarUri READ getAvatarUri WRITE setAvatarUri NOTIFY avatarUriChanged) + Q_PROPERTY(QDateTime lastUpdatedTime READ getLastUpdatedTime WRITE setLastUpdatedTime NOTIFY lastUpdatedTimeChanged) + Q_PROPERTY(QString lastMessageInHistory READ getLastMessageInHistory WRITE setLastMessageInHistory NOTIFY + lastMessageInHistoryChanged) + Q_PROPERTY(int unreadMessagesCount READ getUnreadMessagesCount WRITE setUnreadMessagesCount NOTIFY + unreadMessagesCountChanged) + // Q_PROPERTY(VideoStats videoStats READ getVideoStats WRITE setVideoStats NOTIFY videoStatsChanged) + + // Should be call from model Thread. Will be automatically in App thread after initialization + static QSharedPointer create(const std::shared_ptr &chatRoom); + ChatCore(const std::shared_ptr &chatRoom); + ~ChatCore(); + void setSelf(QSharedPointer me); + + QDateTime getLastUpdatedTime() const; + void setLastUpdatedTime(QDateTime time); + + QString getTitle() const; + void setTitle(QString title); + + QString getLastMessageInHistory() const; + void setLastMessageInHistory(QString message); + + int getUnreadMessagesCount() const; + void setUnreadMessagesCount(int count); + + QString getPeerAddress() const; + void setPeerAddress(QString peerAddress); + + QList> getChatMessageList() const; + void resetChatMessageList(QList> list); + void appendMessagesToMessageList(QList> list); + void removeMessagesFromMessageList(QList> list); + + QString getAvatarUri() const; + void setAvatarUri(QString avatarUri); + +signals: + void lastUpdatedTimeChanged(QDateTime time); + void lastMessageInHistoryChanged(QString time); + void titleChanged(QString title); + void peerAddressChanged(QString address); + void unreadMessagesCountChanged(int count); + void messageListChanged(); + void avatarUriChanged(); + +private: + QString id; + QDateTime mLastUpdatedTime; + QString mLastMessageInHistory; + QString mPeerAddress; + QString mTitle; + QString mAvatarUri; + int mUnreadMessagesCount; + std::shared_ptr mChatModel; + QList> mChatMessageList; + QSharedPointer> mChatModelConnection; + + DECLARE_ABSTRACT_OBJECT +}; +Q_DECLARE_METATYPE(ChatCore *) +#endif diff --git a/Linphone/core/chat/ChatGui.cpp b/Linphone/core/chat/ChatGui.cpp new file mode 100644 index 000000000..a02bb5f33 --- /dev/null +++ b/Linphone/core/chat/ChatGui.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatGui.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatGui) + +ChatGui::ChatGui(QSharedPointer core) { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); + mCore = core; + if (isInLinphoneThread()) moveToThread(App::getInstance()->thread()); +} + +ChatGui::~ChatGui() { + mustBeInMainThread("~" + getClassName()); +} + +ChatCore *ChatGui::getCore() const { + return mCore.get(); +} diff --git a/Linphone/core/chat/ChatGui.hpp b/Linphone/core/chat/ChatGui.hpp new file mode 100644 index 000000000..9bad5226f --- /dev/null +++ b/Linphone/core/chat/ChatGui.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHAT_GUI_H_ +#define CHAT_GUI_H_ + +#include "ChatCore.hpp" +#include +#include + +class ChatGui : public QObject, public AbstractObject { + Q_OBJECT + + Q_PROPERTY(ChatCore *core READ getCore CONSTANT) + +public: + ChatGui(QSharedPointer core); + ~ChatGui(); + ChatCore *getCore() const; + QSharedPointer mCore; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/ChatList.cpp b/Linphone/core/chat/ChatList.cpp new file mode 100644 index 000000000..7429290b9 --- /dev/null +++ b/Linphone/core/chat/ChatList.cpp @@ -0,0 +1,110 @@ +/* + * 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 . + */ + +#include "ChatList.hpp" +#include "ChatCore.hpp" +#include "ChatGui.hpp" +#include "core/App.hpp" + +#include +#include + +// ============================================================================= + +DEFINE_ABSTRACT_OBJECT(ChatList) + +QSharedPointer ChatList::create() { + auto model = QSharedPointer(new ChatList(), &QObject::deleteLater); + model->moveToThread(App::getInstance()->thread()); + model->setSelf(model); + return model; +} + +QSharedPointer ChatList::createChatCore(const std::shared_ptr &chatroom) { + auto chatCore = ChatCore::create(chatroom); + return chatCore; +} + +ChatList::ChatList(QObject *parent) : ListProxy(parent) { + mustBeInMainThread(getClassName()); + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ChatList::~ChatList() { + mustBeInMainThread("~" + getClassName()); + mModelConnection = nullptr; +} + +void ChatList::setSelf(QSharedPointer me) { + mModelConnection = SafeConnection::create(me, CoreModel::getInstance()); + + mModelConnection->makeConnectToCore(&ChatList::lUpdate, [this]() { + mModelConnection->invokeToModel([this]() { + mustBeInLinphoneThread(getClassName()); + // Avoid copy to lambdas + QList> *chats = new QList>(); + auto currentAccount = CoreModel::getInstance()->getCore()->getDefaultAccount(); +// auto linphoneChatRooms = currentAccount->filterChatRooms(Utils::appStringToCoreString(mFilter)); + auto linphoneChatRooms = currentAccount->getChatRooms(); + for (auto it : linphoneChatRooms) { + auto model = createChatCore(it); + chats->push_back(model); + } + mModelConnection->invokeToCore([this, chats]() { + mustBeInMainThread(getClassName()); + resetData(*chats); + delete chats; + }); + }); + }); + + mModelConnection->makeConnectToModel(&CoreModel::chatRoomStateChanged, + [this](const std::shared_ptr &core, + const std::shared_ptr &chatRoom, + linphone::ChatRoom::State state) { + // check account, filtre, puis ajout si c'est bon + bool toCreate = false; + if (toCreate) { + auto model = createChatCore(chatRoom); + mModelConnection->invokeToCore([this, model]() { + // We set the current here and not on firstChatStarted event + // because we don't want to add unicity check while keeping the + // same model between list and current chat. + add(model); + }); + } + }); + mModelConnection->makeConnectToModel(&CoreModel::defaultAccountChanged, [this] (std::shared_ptr core, std::shared_ptr account) { + lUpdate(); + }); + + connect(this, &ChatList::filterChanged, [this](QString filter) { + mFilter = filter; + lUpdate(); + }); + lUpdate(); +} + +QVariant ChatList::data(const QModelIndex &index, int role) const { + int row = index.row(); + if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant(); + if (role == Qt::DisplayRole) return QVariant::fromValue(new ChatGui(mList[row].objectCast())); + return QVariant(); +} diff --git a/Linphone/core/chat/ChatList.hpp b/Linphone/core/chat/ChatList.hpp new file mode 100644 index 000000000..8202482a3 --- /dev/null +++ b/Linphone/core/chat/ChatList.hpp @@ -0,0 +1,54 @@ +/* + * 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 . + */ + +#ifndef CHAT_LIST_H_ +#define CHAT_LIST_H_ + +#include "../proxy/ListProxy.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" +#include + +class ChatGui; +class ChatCore; +// ============================================================================= + +class ChatList : public ListProxy, public AbstractObject { + Q_OBJECT +public: + static QSharedPointer create(); + // Create a ChatCore and make connections to List. + QSharedPointer createChatCore(const std::shared_ptr &chatroom); + ChatList(QObject *parent = Q_NULLPTR); + ~ChatList(); + + void setSelf(QSharedPointer me); + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; +signals: + void lUpdate(); + void filterChanged(QString filter); + +private: + QString mFilter; + QSharedPointer> mModelConnection; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/ChatProxy.cpp b/Linphone/core/chat/ChatProxy.cpp new file mode 100644 index 000000000..589c853d9 --- /dev/null +++ b/Linphone/core/chat/ChatProxy.cpp @@ -0,0 +1,60 @@ +/* + * 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 . + */ + +#include "ChatProxy.hpp" +#include "ChatGui.hpp" +#include "ChatList.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatProxy) + +ChatProxy::ChatProxy(QObject *parent) : LimitProxy(parent) { + mList = ChatList::create(); + setSourceModel(mList.get()); +} + +ChatProxy::~ChatProxy() { +} + +void ChatProxy::setSourceModel(QAbstractItemModel *model) { + auto oldChatList = getListModel(); + if (oldChatList) { + disconnect(oldChatList); + } + auto newChatList = dynamic_cast(model); + if (newChatList) { +// connect(this, &ChatProxy::filterTextChanged, newChatList, &ChatList::filterChanged); + } + setSourceModels(new SortFilterList(model)); + sort(0); +} + +bool ChatProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { +// auto l = getItemAtSource(sourceRow); +// return l != nullptr; + return true; +} + +bool ChatProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { + auto l = getItemAtSource(sourceLeft.row()); + auto r = getItemAtSource(sourceRight.row()); + if (l && r) return l->getLastUpdatedTime() >= r->getLastUpdatedTime(); + else return true; +} diff --git a/Linphone/core/chat/ChatProxy.hpp b/Linphone/core/chat/ChatProxy.hpp new file mode 100644 index 000000000..f68de29d5 --- /dev/null +++ b/Linphone/core/chat/ChatProxy.hpp @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +#ifndef CHAT_PROXY_H_ +#define CHAT_PROXY_H_ + +#include "../proxy/LimitProxy.hpp" +#include "core/chat/ChatGui.hpp" +#include "core/chat/ChatList.hpp" +#include "tool/AbstractObject.hpp" + +// ============================================================================= + +class ChatProxy : public LimitProxy, public AbstractObject { + Q_OBJECT + +public: + DECLARE_SORTFILTER_CLASS() + + ChatProxy(QObject *parent = Q_NULLPTR); + ~ChatProxy(); + + void setSourceModel(QAbstractItemModel *sourceModel) override; + +protected: + QSharedPointer mList; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp new file mode 100644 index 000000000..ed076725b --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +#include "ChatMessageCore.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageCore) + +QSharedPointer ChatMessageCore::create(const std::shared_ptr &chatmessage) { + auto sharedPointer = QSharedPointer(new ChatMessageCore(chatmessage), &QObject::deleteLater); + sharedPointer->setSelf(sharedPointer); + sharedPointer->moveToThread(App::getInstance()->thread()); + return sharedPointer; +} + +ChatMessageCore::ChatMessageCore(const std::shared_ptr &chatmessage) { + lDebug() << "[ChatMessageCore] new" << this; + mustBeInLinphoneThread(getClassName()); + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); + mChatMessageModel = Utils::makeQObject_ptr(chatmessage); + mChatMessageModel->setSelf(mChatMessageModel); + mText = mChatMessageModel->getText(); + mTimestamp = QDateTime::fromSecsSinceEpoch(chatmessage->getTime()); + auto from = chatmessage->getFromAddress(); + auto to = chatmessage->getLocalAddress(); + mIsRemoteMessage = !from->weakEqual(to); +} + +ChatMessageCore::~ChatMessageCore() { +} + +void ChatMessageCore::setSelf(QSharedPointer me) { + mChatMessageModelConnection = SafeConnection::create(me, mChatMessageModel); +} + +QDateTime ChatMessageCore::getTimestamp() const { + return mTimestamp; +} + +void ChatMessageCore::setTimestamp(QDateTime timestamp) { + if (mTimestamp != timestamp) { + mTimestamp = timestamp; + emit timestampChanged(timestamp); + } +} + +QString ChatMessageCore::getText() const { + return mText; +} + +void ChatMessageCore::setText(QString text) { + if (mText != text) { + mText = text; + emit textChanged(text); + } +} + +bool ChatMessageCore::isRemoteMessage() const { + return mIsRemoteMessage; +} + +void ChatMessageCore::setIsRemoteMessage(bool isRemote) { + if (mIsRemoteMessage != isRemote) { + mIsRemoteMessage = isRemote; + emit isRemoteMessageChanged(isRemote); + } +} diff --git a/Linphone/core/chat/message/ChatMessageCore.hpp b/Linphone/core/chat/message/ChatMessageCore.hpp new file mode 100644 index 000000000..646781ea6 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageCore.hpp @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +#ifndef CHATMESSAGECORE_H_ +#define CHATMESSAGECORE_H_ + +#include "model/chat/message/ChatMessageModel.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" +#include +#include + +#include + +class ChatMessageCore : public QObject, public AbstractObject { + Q_OBJECT + Q_PROPERTY(QDateTime timestamp READ getTimestamp WRITE setTimestamp NOTIFY timestampChanged) + Q_PROPERTY(QString text READ getText WRITE setText NOTIFY textChanged) + Q_PROPERTY(bool isRemoteMessage READ isRemoteMessage WRITE setIsRemoteMessage NOTIFY isRemoteMessageChanged) + +public: + static QSharedPointer create(const std::shared_ptr &chatmessage); + ChatMessageCore(const std::shared_ptr &chatmessage); + ~ChatMessageCore(); + void setSelf(QSharedPointer me); + + QDateTime getTimestamp() const; + void setTimestamp(QDateTime timestamp); + + QString getText() const; + void setText(QString text); + + bool isRemoteMessage() const; + void setIsRemoteMessage(bool isRemote); + +signals: + void timestampChanged(QDateTime timestamp); + void textChanged(QString text); + void isRemoteMessageChanged(bool isRemote); + +private: + DECLARE_ABSTRACT_OBJECT + QString mText; + QDateTime mTimestamp; + bool mIsRemoteMessage = false; + std::shared_ptr mChatMessageModel; + QSharedPointer> mChatMessageModelConnection; +}; + +#endif // CHATMESSAGECORE_H_ diff --git a/Linphone/core/chat/message/ChatMessageGui.cpp b/Linphone/core/chat/message/ChatMessageGui.cpp new file mode 100644 index 000000000..cbfbfa062 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageGui.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatMessageGui.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageGui) + +ChatMessageGui::ChatMessageGui(QSharedPointer core) { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); + mCore = core; + if (isInLinphoneThread()) moveToThread(App::getInstance()->thread()); +} + +ChatMessageGui::~ChatMessageGui() { + mustBeInMainThread("~" + getClassName()); +} + +ChatMessageCore *ChatMessageGui::getCore() const { + return mCore.get(); +} diff --git a/Linphone/core/chat/message/ChatMessageGui.hpp b/Linphone/core/chat/message/ChatMessageGui.hpp new file mode 100644 index 000000000..f5b0be030 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageGui.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHAT_MESSAGE_GUI_H_ +#define CHAT_MESSAGE_GUI_H_ + +#include "ChatMessageCore.hpp" +#include +#include + +class ChatMessageGui : public QObject, public AbstractObject { + Q_OBJECT + + Q_PROPERTY(ChatMessageCore *core READ getCore CONSTANT) + +public: + ChatMessageGui(QSharedPointer core); + ~ChatMessageGui(); + ChatMessageCore *getCore() const; + QSharedPointer mCore; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/message/ChatMessageList.cpp b/Linphone/core/chat/message/ChatMessageList.cpp new file mode 100644 index 000000000..8ca118944 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageList.cpp @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +#include "ChatMessageList.hpp" +#include "ChatMessageCore.hpp" +#include "ChatMessageGui.hpp" +#include "core/chat/ChatCore.hpp" +#include "core/chat/ChatGui.hpp" +#include "core/App.hpp" + +#include +#include + +// ============================================================================= + +DEFINE_ABSTRACT_OBJECT(ChatMessageList) + +QSharedPointer ChatMessageList::create() { + auto model = QSharedPointer(new ChatMessageList(), &QObject::deleteLater); + model->moveToThread(App::getInstance()->thread()); + model->setSelf(model); + return model; +} + +QSharedPointer ChatMessageList::createChatMessageCore(const std::shared_ptr &chatMessage) { + auto chatMessageCore = ChatMessageCore::create(chatMessage); + return chatMessageCore; +} + +ChatMessageList::ChatMessageList(QObject *parent) : ListProxy(parent) { + mustBeInMainThread(getClassName()); + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); + connect(this, &ChatMessageList::chatChanged, this, &ChatMessageList::lUpdate); +} + +ChatMessageList::~ChatMessageList() { + mustBeInMainThread("~" + getClassName()); + mModelConnection = nullptr; +} + +ChatGui* ChatMessageList::getChat() const { + if (mChatCore) return new ChatGui(mChatCore); + else return nullptr; +} + +QSharedPointer ChatMessageList::getChatCore() const { + return mChatCore; +} + +void ChatMessageList::setChatCore(QSharedPointer core) { + if (mChatCore != core) { + mChatCore = core; + emit chatChanged(); + } +} + +void ChatMessageList::setChatGui(ChatGui* chat) { + auto chatCore = chat ? chat->mCore : nullptr; + setChatCore(chatCore); +} + +void ChatMessageList::setSelf(QSharedPointer me) { + mModelConnection = SafeConnection::create(me, CoreModel::getInstance()); + + mModelConnection->makeConnectToCore(&ChatMessageList::lUpdate, [this]() { +// mModelConnection->invokeToModel([this]() { +// // Avoid copy to lambdas +// QList> *calls = new QList>(); +// mustBeInLinphoneThread(getClassName()); +// mModelConnection->invokeToCore([this, calls, currentCallCore]() { +// mustBeInMainThread(getClassName()); +// resetData(*calls); +// }); +// }); + if (!mChatCore) return; + auto messages = mChatCore->getChatMessageList(); + resetData(messages); + }); + + connect(this, &ChatMessageList::filterChanged, [this](QString filter) { + mFilter = filter; + lUpdate(); + }); + lUpdate(); +} + +QVariant ChatMessageList::data(const QModelIndex &index, int role) const { + int row = index.row(); + if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant(); + if (role == Qt::DisplayRole) return QVariant::fromValue(new ChatMessageGui(mList[row].objectCast())); + return QVariant(); +} diff --git a/Linphone/core/chat/message/ChatMessageList.hpp b/Linphone/core/chat/message/ChatMessageList.hpp new file mode 100644 index 000000000..9459784c5 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageList.hpp @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +#ifndef CHAT_MESSAGE_LIST_H_ +#define CHAT_MESSAGE_LIST_H_ + +#include "core/proxy/ListProxy.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/thread/SafeConnection.hpp" +#include + +class ChatMessageGui; +class ChatMessageCore; +class ChatCore; +class ChatGui; +// ============================================================================= + +class ChatMessageList : public ListProxy, public AbstractObject { + Q_OBJECT +public: + static QSharedPointer create(); + // Create a ChatMessageCore and make connections to List. + QSharedPointer createChatMessageCore(const std::shared_ptr &chatMessage); + ChatMessageList(QObject *parent = Q_NULLPTR); + ~ChatMessageList(); + + QSharedPointer getChatCore() const; + ChatGui* getChat() const; + void setChatCore(QSharedPointer core); + void setChatGui(ChatGui* chat); + + void setSelf(QSharedPointer me); + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; +signals: + void lUpdate(); + void filterChanged(QString filter); + void chatChanged(); + +private: + QString mFilter; + QSharedPointer mChatCore; + QSharedPointer> mModelConnection; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/core/chat/message/ChatMessageProxy.cpp b/Linphone/core/chat/message/ChatMessageProxy.cpp new file mode 100644 index 000000000..ef0790a87 --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageProxy.cpp @@ -0,0 +1,70 @@ +/* + * 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 . + */ + +#include "ChatMessageProxy.hpp" +#include "ChatMessageGui.hpp" +//#include "core/chat/ChatGui.hpp" +#include "core/App.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageProxy) + +ChatMessageProxy::ChatMessageProxy(QObject *parent) : LimitProxy(parent) { + mList = ChatMessageList::create(); + setSourceModel(mList.get()); +} + +ChatMessageProxy::~ChatMessageProxy() { +} + +void ChatMessageProxy::setSourceModel(QAbstractItemModel *model) { + auto oldChatMessageList = getListModel(); + if (oldChatMessageList) { + disconnect(oldChatMessageList); + } + auto newChatMessageList = dynamic_cast(model); + if (newChatMessageList) { + connect(newChatMessageList, &ChatMessageList::chatChanged, this, &ChatMessageProxy::chatChanged); + } + setSourceModels(new SortFilterList(model)); + sort(0); +} + +ChatGui* ChatMessageProxy::getChatGui() { + auto model = getListModel(); + if (!mChatGui && model) mChatGui = model->getChat(); + return mChatGui; +} + +void ChatMessageProxy::setChatGui(ChatGui* chat) { + getListModel()->setChatGui(chat); +} + +bool ChatMessageProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { +// auto l = getItemAtSource(sourceRow); +// return l != nullptr; + return true; +} + +bool ChatMessageProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { + auto l = getItemAtSource(sourceLeft.row()); + auto r = getItemAtSource(sourceRight.row()); + if (l && r) return l->getTimestamp() <= r->getTimestamp(); + else return true; +} diff --git a/Linphone/core/chat/message/ChatMessageProxy.hpp b/Linphone/core/chat/message/ChatMessageProxy.hpp new file mode 100644 index 000000000..382fb654a --- /dev/null +++ b/Linphone/core/chat/message/ChatMessageProxy.hpp @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +#ifndef CHAT_MESSAGE_PROXY_H_ +#define CHAT_MESSAGE_PROXY_H_ + +#include "core/proxy/LimitProxy.hpp" +#include "ChatMessageList.hpp" +#include "tool/AbstractObject.hpp" + +// ============================================================================= + +class ChatGui; + +class ChatMessageProxy : public LimitProxy, public AbstractObject { + Q_OBJECT + Q_PROPERTY(ChatGui* chatGui READ getChatGui WRITE setChatGui NOTIFY chatChanged) + +public: + DECLARE_SORTFILTER_CLASS() + + ChatMessageProxy(QObject *parent = Q_NULLPTR); + ~ChatMessageProxy(); + + ChatGui* getChatGui(); + void setChatGui(ChatGui* chat); + + void setSourceModel(QAbstractItemModel *sourceModel) override; + +signals: + void chatChanged(); + +protected: + QSharedPointer mList; + ChatGui* mChatGui = nullptr; + DECLARE_ABSTRACT_OBJECT +}; + +#endif diff --git a/Linphone/data/image/paper-plane-right.svg b/Linphone/data/image/paper-plane-right.svg new file mode 100644 index 000000000..48a2fcafc --- /dev/null +++ b/Linphone/data/image/paper-plane-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/data/languages/de.ts b/Linphone/data/languages/de.ts index ee17f2b29..feb2f26b0 100644 --- a/Linphone/data/languages/de.ts +++ b/Linphone/data/languages/de.ts @@ -513,74 +513,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? - + application_description "A free and open source SIP video-phone." - + command_line_arg_order "Send an order to the application towards a command line" - + command_line_option_show_help - + command_line_option_show_app_version - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." - + command_line_option_config_to_fetch_arg "URL, path or file" - + command_line_option_minimized - + command_line_option_log_to_stdout - + command_line_option_print_app_logs_only "Print only logs from the application" - + hide_action "Cacher" "Afficher" - + show_action - + quit_action "Quitter" @@ -915,7 +915,7 @@ - + menu_delete_history "Supprimer l'historique" @@ -979,7 +979,7 @@ - + information_popup_error_title @@ -996,55 +996,55 @@ - + menu_see_existing_contact - "Voir le contact" + "Show contact" - + menu_add_address_to_contacts - "Ajouter aux contacts" + "Add to contacts" - + menu_copy_sip_address "Copier l'adresse SIP" - + sip_address_copied_to_clipboard_toast Adresse copiée - + sip_address_copied_to_clipboard_message L'adresse a été copié dans le presse_papiers - + sip_address_copy_to_clipboard_error "Erreur lors de la copie de l'adresse" - + notification_missed_call_title "Appel manqué" - + call_outgoing "Appel sortant" - + call_audio_incoming "Appel entrant" @@ -1222,276 +1222,276 @@ - + conference_user_is_recording "Vous enregistrez la réunion" - + call_user_is_recording "Vous enregistrez l'appel" - + conference_remote_is_recording "Un participant enregistre la réunion" - + call_remote_recording "%1 enregistre l'appel" - + call_stop_recording "Arrêter l'enregistrement" - + add - + call_transfer_current_call_title "Transférer %1 à…" - - + + call_transfer_confirm_dialog_tittle "Confirmer le transfert" - - + + call_transfer_confirm_dialog_message "Vous allez transférer %1 à %2." - + call_action_start_new_call "Nouvel appel" - - + + call_action_show_dialer "Pavé numérique" - + call_action_change_layout "Modifier la disposition" - + call_action_go_to_calls_list "Liste d'appel" - + Merger tous les appels call_action_merge_calls - - + + call_action_go_to_settings "Paramètres" - + conference_action_screen_sharing "Partage de votre écran" - + conference_share_link_title Partager le lien de la réunion - + copied Copié - + information_popup_meeting_address_copied_to_clipboard Le lien de la réunion a été copié dans le presse-papier - - + + conference_participants_list_title "Participants (%1)" - - + + group_call_participant_selected - + meeting_schedule_add_participants_title - + call_encryption_title Chiffrement - + call_stats_title Statistiques - + call_action_end_call "Terminer l'appel" - + call_action_resume_call "Reprendre l'appel" - + call_action_pause_call "Mettre l'appel en pause" - + call_action_transfer_call "Transférer l'appel" - + call_action_start_new_call_hint "Initier un nouvel appel" - + call_display_call_list_hint "Afficher la liste d'appels" - + call_deactivate_video_hint "Désactiver la vidéo" "Activer la vidéo" - + call_activate_video_hint - + call_activate_microphone "Activer le micro" - + call_deactivate_microphone "Désactiver le micro" - + call_share_screen_hint Partager l'écran… - + call_rise_hand_hint "Lever la main" - + call_send_reaction_hint "Envoyer une réaction" - + call_manage_participants_hint "Gérer les participants" - + call_more_options_hint "Plus d'options…" - + call_action_change_conference_layout "Modifier la disposition" - + call_action_full_screen "Mode Plein écran" - + call_action_stop_recording "Terminer l'enregistrement" - + call_action_record "Enregistrer l'appel" - + call_activate_speaker_hint "Activer le son" - + call_deactivate_speaker_hint "Désactiver le son" @@ -1603,6 +1603,63 @@ + + ChatPage + + + chat_start_title + "Nouvelle conversation" + + + + + chat_empty_title + "Aucune conversation" + + + + + chat_dialog_delete_chat_title + Supprimer la conversation ? + + + + + chat_dialog_delete_chat_message + "La conversation et tous ses messages seront supprimés." + + + + + chat_list_title + "Conversations" + + + + + chat_search_in_history + "Rechercher une conversation" + + + + + list_filter_no_result_found + "Aucun résultat…" + + + + + chat_list_empty_history + "Aucune conversation dans votre historique" + + + + + chat_action_start_new_chat + "New chat" + + + CliModel @@ -1742,38 +1799,38 @@ - + contact_editor_first_name "Prénom" - + contact_editor_last_name "Nom" - + contact_editor_company "Entreprise" - + contact_editor_job_title "Fonction" - - + + sip_address - - + + phone "Téléphone" @@ -1782,53 +1839,53 @@ ContactListItem - + contact_details_remove_from_favourites "Enlever des favoris" - + contact_details_add_to_favourites "Ajouter aux favoris" - + Partager - + information_popup_error_title - + information_popup_vcard_creation_error La création du fichier vcard a échoué - + information_popup_vcard_creation_title VCard créée - + information_popup_vcard_creation_success "VCard du contact enregistrée dans %1" - + contact_sharing_email_title Partage de contact - + contact_details_delete "Supprimer" @@ -2385,9 +2442,9 @@ - - - + + + sip_address "Adresse SIP" @@ -2395,18 +2452,18 @@ - + device_id "Téléphone" - + information_popup_error_title - + information_popup_invalid_address_message "Adresse invalide" @@ -3275,13 +3332,13 @@ - + search_bar_look_for_contact_text "Rechercher un contact" - + call_start_group_call_title @@ -3321,97 +3378,97 @@ - + oidc_authentication_granted_message Authentication granted - + oidc_authentication_not_authenticated_message Not authenticated - + oidc_authentication_refresh_message Refreshing token - + oidc_authentication_temporary_credentials_message Temporary credentials received - + oidc_authentication_network_error Network error - + oidc_authentication_server_error Server error - + oidc_authentication_token_not_found_error OAuth token not found - + oidc_authentication_token_secret_not_found_error OAuth token secret not found - + oidc_authentication_callback_not_verified_error OAuth callback not verified - + oidc_authentication_request_auth_message Requesting authorization from browser - + oidc_authentication_request_token_message Requesting access token - + oidc_authentication_refresh_token_message Refreshing access token - + oidc_authentication_request_authorization_message Requesting authorization - + oidc_authentication_request_temporary_credentials_message Requesting temporary credentials - + oidc_authentication_no_auth_found_in_config_error No authorization endpoint found in OpenID configuration - + oidc_authentication_no_token_found_in_config_error No token endpoint found in OpenID configuration @@ -4145,25 +4202,25 @@ Pour les activer dans un projet commercial, merci de nous contacter. - + call_dialog_zrtp_validate_trust_letters_do_not_match_text "Le code fourni ne correspond pas." - + call_dialog_zrtp_security_alert_message "La confidentialité de votre appel peut être compromise !" - + call_dialog_zrtp_validate_trust_letters_do_not_match "Aucune correspondance" - + call_action_hang_up "Raccrocher" diff --git a/Linphone/data/languages/en.ts b/Linphone/data/languages/en.ts index 012424c4d..d2d3b792a 100644 --- a/Linphone/data/languages/en.ts +++ b/Linphone/data/languages/en.ts @@ -513,74 +513,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Do you want to download and apply remote provisioning from this address ? - + application_description "A free and open source SIP video-phone." A free and open source SIP video-phone. - + command_line_arg_order "Send an order to the application towards a command line" Send an order to the application towards a command line - + command_line_option_show_help Show this help - + command_line_option_show_app_version Show app version - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." Specify the linphone configuration file to be fetched. It will be merged with the current configuration. - + command_line_option_config_to_fetch_arg "URL, path or file" URL, path or file - + command_line_option_minimized Minimize - + command_line_option_log_to_stdout Log to stdout some debug information while running - + command_line_option_print_app_logs_only "Print only logs from the application" Print only logs from the application - + hide_action "Cacher" "Afficher" Hide - + show_action Show - + quit_action "Quitter" Quit @@ -915,7 +915,7 @@ - + menu_delete_history "Supprimer l'historique" Delete history @@ -980,7 +980,7 @@ - + information_popup_error_title Error @@ -997,55 +997,55 @@ You are not connected - + menu_see_existing_contact - "Voir le contact" + "Show contact" Show contact - + menu_add_address_to_contacts - "Ajouter aux contacts" + "Add to contacts" Add to contacts - + menu_copy_sip_address "Copier l'adresse SIP" Copy SIP address - + sip_address_copied_to_clipboard_toast Adresse copiée SIP address copied - + sip_address_copied_to_clipboard_message L'adresse a été copié dans le presse_papiers The address has been copied to the clipboard - + sip_address_copy_to_clipboard_error "Erreur lors de la copie de l'adresse" Error copying address - + notification_missed_call_title "Appel manqué" Missed call - + call_outgoing "Appel sortant" Outgoing call - + call_audio_incoming "Appel entrant" Incoming call @@ -1235,133 +1235,133 @@ Call paused by remote - + conference_user_is_recording "Vous enregistrez la réunion" You are recording the meeting - + call_user_is_recording "Vous enregistrez l'appel" You are recording the call - + conference_remote_is_recording "Un participant enregistre la réunion" Someone is recording the meeting - + call_remote_recording "%1 enregistre l'appel" %1 records the call - + call_stop_recording "Arrêter l'enregistrement" Stop recording - + add Add - + call_transfer_current_call_title "Transférer %1 à…" Transfer %1 to… - - + + call_transfer_confirm_dialog_tittle "Confirmer le transfert" Confirm transfer - - + + call_transfer_confirm_dialog_message "Vous allez transférer %1 à %2." You are going to transfer %1 to %2. - + call_action_start_new_call "Nouvel appel" New call - - + + call_action_show_dialer "Pavé numérique" Dialer - + call_action_change_layout "Modifier la disposition" Change layout - + call_action_go_to_calls_list "Liste d'appel" Call list - + Merger tous les appels call_action_merge_calls Merge all calls - - + + call_action_go_to_settings "Paramètres" Settings - + conference_action_screen_sharing "Partage de votre écran" Share your screen - + conference_share_link_title Partager le lien de la réunion Share meeting link - + copied Copié Copied - + information_popup_meeting_address_copied_to_clipboard Le lien de la réunion a été copié dans le presse-papier Meeting link has been copied to the clipboard - - + + conference_participants_list_title "Participants (%1)" Participants (%1) - - + + group_call_participant_selected 1 selected participant @@ -1369,143 +1369,143 @@ - + meeting_schedule_add_participants_title Add participants - + call_encryption_title Chiffrement Encryption - + call_stats_title Statistiques Statistics - + call_action_end_call "Terminer l'appel" End call - + call_action_resume_call "Reprendre l'appel" Resume call - + call_action_pause_call "Mettre l'appel en pause" Pause call - + call_action_transfer_call "Transférer l'appel" Transfer call - + call_action_start_new_call_hint "Initier un nouvel appel" Start new call - + call_display_call_list_hint "Afficher la liste d'appels" View call list - + call_deactivate_video_hint "Désactiver la vidéo" "Activer la vidéo" Turn off video - + call_activate_video_hint Enable video - + call_activate_microphone "Activer le micro" Activate microphone - + call_deactivate_microphone "Désactiver le micro" Mute microphone - + call_share_screen_hint Partager l'écran… Share screen… - + call_rise_hand_hint "Lever la main" Rise hand - + call_send_reaction_hint "Envoyer une réaction" Send reaction - + call_manage_participants_hint "Gérer les participants" Manage participants - + call_more_options_hint "Plus d'options…" More options… - + call_action_change_conference_layout "Modifier la disposition" Change layout - + call_action_full_screen "Mode Plein écran" Full screen mode - + call_action_stop_recording "Terminer l'enregistrement" End recording - + call_action_record "Enregistrer l'appel" Record call - + call_activate_speaker_hint "Activer le son" Activate speaker - + call_deactivate_speaker_hint "Désactiver le son" Mute speaker @@ -1617,6 +1617,63 @@ Audio only + + ChatPage + + + chat_start_title + "Nouvelle conversation" + New conversation + + + + chat_empty_title + "Aucune conversation" + No conversation + + + + chat_dialog_delete_chat_title + Supprimer la conversation ? + Delete chat ? + + + + chat_dialog_delete_chat_message + "La conversation et tous ses messages seront supprimés." + This chat and all its messages will be deleted. + + + + chat_list_title + "Conversations" + Chat + + + + chat_search_in_history + "Rechercher une conversation" + Search for a chat + + + + list_filter_no_result_found + "Aucun résultat…" + No result… + + + + chat_list_empty_history + "Aucune conversation dans votre historique" + No chat in history + + + + chat_action_start_new_chat + "New chat" + New chat + + CliModel @@ -1756,38 +1813,38 @@ Delete - + contact_editor_first_name "Prénom" First name - + contact_editor_last_name "Nom" Last name - + contact_editor_company "Entreprise" Company - + contact_editor_job_title "Fonction" Job - - + + sip_address SIP address - - + + phone "Téléphone" Phone @@ -1796,53 +1853,53 @@ ContactListItem - + contact_details_remove_from_favourites "Enlever des favoris" Remove from favorites - + contact_details_add_to_favourites "Ajouter aux favoris" Add to favorites - + Partager Share - + information_popup_error_title Error - + information_popup_vcard_creation_error La création du fichier vcard a échoué VCard creation failed - + information_popup_vcard_creation_title VCard créée VCard created - + information_popup_vcard_creation_success "VCard du contact enregistrée dans %1" VCard has been saved in %1 - + contact_sharing_email_title Partage de contact Share contact - + contact_details_delete "Supprimer" Delete @@ -2404,9 +2461,9 @@ - - - + + + sip_address "Adresse SIP" SIP address @@ -2414,18 +2471,18 @@ - + device_id "Téléphone" Phone - + information_popup_error_title Error - + information_popup_invalid_address_message "Adresse invalide" Invalid address @@ -3303,13 +3360,13 @@ Ongoing call - + search_bar_look_for_contact_text "Rechercher un contact" Find contact - + call_start_group_call_title Group call @@ -3349,97 +3406,97 @@ Timeout: Not authenticated - + oidc_authentication_granted_message Authentication granted Authentication granted - + oidc_authentication_not_authenticated_message Not authenticated Not authenticated - + oidc_authentication_refresh_message Refreshing token Refreshing token - + oidc_authentication_temporary_credentials_message Temporary credentials received Temporary credentials received - + oidc_authentication_network_error Network error Network error - + oidc_authentication_server_error Server error Server error - + oidc_authentication_token_not_found_error OAuth token not found OAuth token not found - + oidc_authentication_token_secret_not_found_error OAuth token secret not found OAuth token secret not found - + oidc_authentication_callback_not_verified_error OAuth callback not verified OAuth callback not verified - + oidc_authentication_request_auth_message Requesting authorization from browser Requesting authorization from browser - + oidc_authentication_request_token_message Requesting access token Requesting access token - + oidc_authentication_refresh_token_message Refreshing access token Refreshing access token - + oidc_authentication_request_authorization_message Requesting authorization Requesting authorization - + oidc_authentication_request_temporary_credentials_message Requesting temporary credentials Requesting temporary credentials - + oidc_authentication_no_auth_found_in_config_error No authorization endpoint found in OpenID configuration No authorization endpoint found in OpenID configuration - + oidc_authentication_no_token_found_in_config_error No token endpoint found in OpenID configuration No token endpoint found in OpenID configuration @@ -4181,25 +4238,25 @@ To enable them in a commercial project, please contact us. Corresponding code : - + call_dialog_zrtp_validate_trust_letters_do_not_match_text "Le code fourni ne correspond pas." The provided code does not match. - + call_dialog_zrtp_security_alert_message "La confidentialité de votre appel peut être compromise !" The confidentiality of your call may be compromised! - + call_dialog_zrtp_validate_trust_letters_do_not_match "Aucune correspondance" No match - + call_action_hang_up "Raccrocher" Hang up diff --git a/Linphone/data/languages/fr_FR.ts b/Linphone/data/languages/fr_FR.ts index 49d8ec2a9..ab8a3aae7 100644 --- a/Linphone/data/languages/fr_FR.ts +++ b/Linphone/data/languages/fr_FR.ts @@ -513,74 +513,74 @@ App - + remote_provisioning_dialog Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? - + application_description "A free and open source SIP video-phone." A free and open source SIP video-phone. - + command_line_arg_order "Send an order to the application towards a command line" Send an order to the application towards a command line - + command_line_option_show_help Show this help - + command_line_option_show_app_version Afficher la version de l'application - + command_line_option_config_to_fetch "Specify the linphone configuration file to be fetched. It will be merged with the current configuration." Specify the linphone configuration file to be fetched. It will be merged with the current configuration. - + command_line_option_config_to_fetch_arg "URL, path or file" URL, path or file - + command_line_option_minimized Minimiser - + command_line_option_log_to_stdout Log to stdout some debug information while running - + command_line_option_print_app_logs_only "Print only logs from the application" Print only logs from the application - + hide_action "Cacher" "Afficher" Cacher - + show_action Afficher - + quit_action "Quitter" Quitter @@ -915,7 +915,7 @@ - + menu_delete_history "Supprimer l'historique" Supprimer l'historique @@ -980,7 +980,7 @@ - + information_popup_error_title Erreur @@ -997,55 +997,55 @@ Vous n'etes pas connecté - + menu_see_existing_contact - "Voir le contact" + "Show contact" Voir le contact - + menu_add_address_to_contacts - "Ajouter aux contacts" + "Add to contacts" Ajouter aux contacts - + menu_copy_sip_address "Copier l'adresse SIP" Copier l'adresse SIP - + sip_address_copied_to_clipboard_toast Adresse copiée Adresse copiée - + sip_address_copied_to_clipboard_message L'adresse a été copié dans le presse_papiers L'adresse a été copié dans le presse-papiers - + sip_address_copy_to_clipboard_error "Erreur lors de la copie de l'adresse" Erreur lors de la copie de l'adresse - + notification_missed_call_title "Appel manqué" Appel manqué - + call_outgoing "Appel sortant" Appel sortant - + call_audio_incoming "Appel entrant" Appel entrant @@ -1235,133 +1235,133 @@ Appel mis en pause par votre correspondant - + conference_user_is_recording "Vous enregistrez la réunion" Vous enregistrez la réunion - + call_user_is_recording "Vous enregistrez l'appel" Vous enregistrez l'appel - + conference_remote_is_recording "Un participant enregistre la réunion" Un participant enregistre la réunion - + call_remote_recording "%1 enregistre l'appel" %1 enregistre l'appel - + call_stop_recording "Arrêter l'enregistrement" Arrêter l'enregistrement - + add Ajouter - + call_transfer_current_call_title "Transférer %1 à…" Transférer %1 à… - - + + call_transfer_confirm_dialog_tittle "Confirmer le transfert" Confirmer le transfert - - + + call_transfer_confirm_dialog_message "Vous allez transférer %1 à %2." Vous allez transférer %1 à %2. - + call_action_start_new_call "Nouvel appel" Nouvel appel - - + + call_action_show_dialer "Pavé numérique" Pavé numérique - + call_action_change_layout "Modifier la disposition" Modifier la disposition - + call_action_go_to_calls_list "Liste d'appel" Liste d'appel - + Merger tous les appels call_action_merge_calls Merger tous les appels - - + + call_action_go_to_settings "Paramètres" Paramètres - + conference_action_screen_sharing "Partage de votre écran" Partage de votre écran - + conference_share_link_title Partager le lien de la réunion Partager le lien de la réunion - + copied Copié Copié - + information_popup_meeting_address_copied_to_clipboard Le lien de la réunion a été copié dans le presse-papier Le lien de la réunion a été copié dans le presse-papier - - + + conference_participants_list_title "Participants (%1)" Participants (%1) - - + + group_call_participant_selected un participant sélectionné @@ -1369,143 +1369,143 @@ - + meeting_schedule_add_participants_title Ajouter des participants - + call_encryption_title Chiffrement Chiffrement - + call_stats_title Statistiques Statistiques - + call_action_end_call "Terminer l'appel" Terminer l'appel - + call_action_resume_call "Reprendre l'appel" Reprendre l'appel - + call_action_pause_call "Mettre l'appel en pause" Mettre l'appel en pause - + call_action_transfer_call "Transférer l'appel" Transférer l'appel - + call_action_start_new_call_hint "Initier un nouvel appel" Initier un nouvel appel - + call_display_call_list_hint "Afficher la liste d'appels" Afficher la liste d'appels - + call_deactivate_video_hint "Désactiver la vidéo" "Activer la vidéo" Désactiver la vidéo - + call_activate_video_hint Activer la vidéo - + call_activate_microphone "Activer le micro" Activer le micro - + call_deactivate_microphone "Désactiver le micro" Désactiver le micro - + call_share_screen_hint Partager l'écran… Partager l'écran… - + call_rise_hand_hint "Lever la main" Lever la main - + call_send_reaction_hint "Envoyer une réaction" Envoyer une réaction - + call_manage_participants_hint "Gérer les participants" Gérer les participants - + call_more_options_hint "Plus d'options…" Plus d'options… - + call_action_change_conference_layout "Modifier la disposition" Modifier la disposition - + call_action_full_screen "Mode Plein écran" Mode Plein écran - + call_action_stop_recording "Terminer l'enregistrement" Terminer l'enregistrement - + call_action_record "Enregistrer l'appel" Enregistrer l'appel - + call_activate_speaker_hint "Activer le son" Activer le son - + call_deactivate_speaker_hint "Désactiver le son" Désactiver le son @@ -1617,6 +1617,63 @@ Audio uniquement + + ChatPage + + + chat_start_title + "Nouvelle conversation" + Nouvelle conversation + + + + chat_empty_title + "Aucune conversation" + Aucune conversation + + + + chat_dialog_delete_chat_title + Supprimer la conversation ? + Supprimer la conversation ? + + + + chat_dialog_delete_chat_message + "La conversation et tous ses messages seront supprimés." + La conversation et tous ses messages seront supprimés. + + + + chat_list_title + "Conversations" + Conversations + + + + chat_search_in_history + "Rechercher une conversation" + Rechercher une conversation + + + + list_filter_no_result_found + "Aucun résultat…" + Aucun résultat… + + + + chat_list_empty_history + "Aucune conversation dans votre historique" + Aucune conversation dans votre historique + + + + chat_action_start_new_chat + "New chat" + Nouvelle conversation + + CliModel @@ -1756,38 +1813,38 @@ Supprimer - + contact_editor_first_name "Prénom" Prénom - + contact_editor_last_name "Nom" Nom - + contact_editor_company "Entreprise" Entreprise - + contact_editor_job_title "Fonction" Fonction - - + + sip_address Adresse SIP - - + + phone "Téléphone" Téléphone @@ -1796,53 +1853,53 @@ ContactListItem - + contact_details_remove_from_favourites "Enlever des favoris" Enlever des favoris - + contact_details_add_to_favourites "Ajouter aux favoris" Ajouter aux favoris - + Partager Partager - + information_popup_error_title Erreur - + information_popup_vcard_creation_error La création du fichier vcard a échoué La création du fichier vcard a échoué - + information_popup_vcard_creation_title VCard créée VCard créée - + information_popup_vcard_creation_success "VCard du contact enregistrée dans %1" VCard du contact enregistrée dans %1 - + contact_sharing_email_title Partage de contact Partage de contact - + contact_details_delete "Supprimer" Supprimer @@ -2399,9 +2456,9 @@ - - - + + + sip_address "Adresse SIP" Adresse SIP @@ -2409,18 +2466,18 @@ - + device_id "Téléphone" Téléphone - + information_popup_error_title Erreur - + information_popup_invalid_address_message "Adresse invalide" Adresse invalide @@ -3313,13 +3370,13 @@ Appels en cours - + search_bar_look_for_contact_text "Rechercher un contact" Rechercher un contact - + call_start_group_call_title Appel de groupe @@ -3359,97 +3416,97 @@ Timeout : non authentifié - + oidc_authentication_granted_message Authentication granted Authentification accordée - + oidc_authentication_not_authenticated_message Not authenticated Non authentifié - + oidc_authentication_refresh_message Refreshing token Token en cours de rafraîchissement - + oidc_authentication_temporary_credentials_message Temporary credentials received Identifiants temporaires reçus - + oidc_authentication_network_error Network error Erreur réseau - + oidc_authentication_server_error Server error Erreur de serveur - + oidc_authentication_token_not_found_error OAuth token not found Token OAuth non trouvé - + oidc_authentication_token_secret_not_found_error OAuth token secret not found Token OAuth secret non trouvé - + oidc_authentication_callback_not_verified_error OAuth callback not verified Retour OAuth non vérifié - + oidc_authentication_request_auth_message Requesting authorization from browser En attente d'autorisation du navigateur - + oidc_authentication_request_token_message Requesting access token En attente du token d'accès - + oidc_authentication_refresh_token_message Refreshing access token Token en cours de rafraîchissement - + oidc_authentication_request_authorization_message Requesting authorization Autorisation en cours - + oidc_authentication_request_temporary_credentials_message Requesting temporary credentials En attente d'identifiants temporaires - + oidc_authentication_no_auth_found_in_config_error No authorization endpoint found in OpenID configuration Pas d'autorisation trouvé dans la configuration OpenID - + oidc_authentication_no_token_found_in_config_error No token endpoint found in OpenID configuration Pas de token trouvé dans la configuration OpenID @@ -4191,25 +4248,25 @@ Pour les activer dans un projet commercial, merci de nous contacter.Code correspondant : - + call_dialog_zrtp_validate_trust_letters_do_not_match_text "Le code fourni ne correspond pas." Le code fourni ne correspond pas. - + call_dialog_zrtp_security_alert_message "La confidentialité de votre appel peut être compromise !" La confidentialité de votre appel peut être compromise ! - + call_dialog_zrtp_validate_trust_letters_do_not_match "Aucune correspondance" Aucune correspondance - + call_action_hang_up "Raccrocher" Raccrocher diff --git a/Linphone/model/CMakeLists.txt b/Linphone/model/CMakeLists.txt index b7d394a30..0d4c6e0c1 100644 --- a/Linphone/model/CMakeLists.txt +++ b/Linphone/model/CMakeLists.txt @@ -11,6 +11,9 @@ list(APPEND _LINPHONEAPP_SOURCES model/call-history/CallHistoryModel.cpp + model/chat/ChatModel.cpp + model/chat/message/ChatMessageModel.cpp + model/conference/ConferenceInfoModel.cpp model/conference/ConferenceModel.cpp model/conference/ConferenceSchedulerModel.cpp diff --git a/Linphone/model/chat/ChatModel.cpp b/Linphone/model/chat/ChatModel.cpp new file mode 100644 index 000000000..1d34005eb --- /dev/null +++ b/Linphone/model/chat/ChatModel.cpp @@ -0,0 +1,262 @@ +/* + * 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 . + */ + +#include "ChatModel.hpp" + +#include + +#include "core/path/Paths.hpp" +#include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" +#include "model/tool/ToolModel.hpp" +#include "tool/Utils.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatModel) + +ChatModel::ChatModel(const std::shared_ptr &chatroom, QObject *parent) + : ::Listener(chatroom, parent) { + lDebug() << "[ChatModel] new" << this << " / SDKModel=" << chatroom.get(); + mustBeInLinphoneThread(getClassName()); +} + +ChatModel::~ChatModel() { + mustBeInLinphoneThread("~" + getClassName()); +} + +QDateTime ChatModel::getLastUpdateTime() { + // TODO : vérifier unité + return QDateTime::fromSecsSinceEpoch(mMonitor->getLastUpdateTime()); +} + +std::list> ChatModel::getHistory() const { + auto history = mMonitor->getHistory(0, (int)linphone::ChatRoom::HistoryFilter::ChatMessage); + std::list> res; + for (auto &eventLog : history) { + if (!eventLog->getChatMessage()) res.push_back(eventLog->getChatMessage()); + } + return res; +} + +QString ChatModel::getTitle() { + if (mMonitor->hasCapability((int)linphone::ChatRoom::Capabilities::Basic)) { + return ToolModel::getDisplayName(mMonitor->getPeerAddress()->clone()); + } else { + if (mMonitor->hasCapability((int)linphone::ChatRoom::Capabilities::OneToOne)) { + auto peer = mMonitor->getParticipants().front(); + return peer ? ToolModel::getDisplayName(peer->getAddress()->clone()) : "Chat"; + } else if (mMonitor->hasCapability((int)linphone::ChatRoom::Capabilities::Conference)) { + return Utils::coreStringToAppString(mMonitor->getSubject()); + } + } +} + +QString ChatModel::getPeerAddress() const { + return Utils::coreStringToAppString(mMonitor->getPeerAddress()->asStringUriOnly()); +} + +QString ChatModel::getLastMessageInHistory(std::list> startList) const { + if (startList.empty()) { + auto lastMessage = mMonitor->getLastMessageInHistory(); + if (lastMessage) startList = lastMessage->getContents(); + } + for (auto &content : startList) { + if (content->isText()) { + return Utils::coreStringToAppString(content->getUtf8Text()); + } else if (content->isFile()) { + return Utils::coreStringToAppString(content->getName()); + } else if (content->isIcalendar()) { + return QString("Invitation à une réunion"); + } else if (content->isMultipart()) { + return getLastMessageInHistory(content->getParts()); + } + } + return QString(""); +} + +int ChatModel::getUnreadMessagesCount() const { + return mMonitor->getUnreadMessagesCount(); +} +//---------------------------------------------------------------// + +void ChatModel::onIsComposingReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &remoteAddress, + bool isComposing) { + emit isComposingReceived(chatRoom, remoteAddress, isComposing); +} + +// Do not use this api, only manipulate EventLogs +void ChatModel::onMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) { + // emit messageReceived(chatRoom, message); +} + +void ChatModel::onMessagesReceived(const std::shared_ptr &chatRoom, + const std::list> &chatMessages) { + // emit messagesReceived(chatRoom, chatMessages); +} + +void ChatModel::onNewEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit newEvent(chatRoom, eventLog); +} + +void ChatModel::onNewEvents(const std::shared_ptr &chatRoom, + const std::list> &eventLogs) { + emit newEvents(chatRoom, eventLogs); +} + +void ChatModel::onChatMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit chatMessageReceived(chatRoom, eventLog); +} + +void ChatModel::onChatMessagesReceived(const std::shared_ptr &chatRoom, + const std::list> &eventLogs) { + emit chatMessagesReceived(chatRoom, eventLogs); +} + +void ChatModel::onChatMessageSending(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit chatMessageSending(chatRoom, eventLog); +} + +void ChatModel::onChatMessageSent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit chatMessageSent(chatRoom, eventLog); +} + +void ChatModel::onParticipantAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantAdded(chatRoom, eventLog); +} + +void ChatModel::onParticipantRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantRemoved(chatRoom, eventLog); +} + +void ChatModel::onParticipantAdminStatusChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantAdminStatusChanged(chatRoom, eventLog); +} + +void ChatModel::onStateChanged(const std::shared_ptr &chatRoom, + linphone::ChatRoom::State newState) { + emit stateChanged(chatRoom, newState); +} + +void ChatModel::onSecurityEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit securityEvent(chatRoom, eventLog); +} + +void ChatModel::onSubjectChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit subjectChanged(chatRoom, eventLog); +} + +void ChatModel::onUndecryptableMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) { + emit undecryptableMessageReceived(chatRoom, message); +} + +void ChatModel::onParticipantDeviceAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantDeviceAdded(chatRoom, eventLog); +} + +void ChatModel::onParticipantDeviceRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantDeviceRemoved(chatRoom, eventLog); +} + +void ChatModel::onParticipantDeviceStateChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog, + linphone::ParticipantDevice::State state) { + emit participantDeviceStateChanged(chatRoom, eventLog, state); +} + +void ChatModel::onParticipantDeviceMediaAvailabilityChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit participantDeviceMediaAvailabilityChanged(chatRoom, eventLog); +} + +void ChatModel::onConferenceJoined(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit conferenceJoined(chatRoom, eventLog); +} + +void ChatModel::onConferenceLeft(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit conferenceLeft(chatRoom, eventLog); +} + +void ChatModel::onEphemeralEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit ephemeralEvent(chatRoom, eventLog); +} + +void ChatModel::onEphemeralMessageTimerStarted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit ephemeralMessageTimerStarted(chatRoom, eventLog); +} + +void ChatModel::onEphemeralMessageDeleted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) { + emit onEphemeralMessageDeleted(chatRoom, eventLog); +} + +void ChatModel::onConferenceAddressGeneration(const std::shared_ptr &chatRoom) { + emit conferenceAddressGeneration(chatRoom); +} + +void ChatModel::onParticipantRegistrationSubscriptionRequested( + const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress) { + emit participantRegistrationSubscriptionRequested(chatRoom, participantAddress); +} + +void ChatModel::onParticipantRegistrationUnsubscriptionRequested( + const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress) { + emit participantRegistrationUnsubscriptionRequested(chatRoom, participantAddress); +} + +void ChatModel::onChatMessageShouldBeStored(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) { + emit chatMessageShouldBeStored(chatRoom, message); +} + +void ChatModel::onChatMessageParticipantImdnStateChanged( + const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &state) { + emit chatMessageParticipantImdnStateChanged(chatRoom, message, state); +} + +void ChatModel::onChatRoomRead(const std::shared_ptr &chatRoom) { + emit chatRoomRead(chatRoom); +} + +void ChatModel::onNewMessageReaction(const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &reaction) { + emit onNewMessageReaction(chatRoom, message, reaction); +} diff --git a/Linphone/model/chat/ChatModel.hpp b/Linphone/model/chat/ChatModel.hpp new file mode 100644 index 000000000..eb3ef7170 --- /dev/null +++ b/Linphone/model/chat/ChatModel.hpp @@ -0,0 +1,194 @@ +/* + * 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 . + */ + +#ifndef CHAT_MODEL_H_ +#define CHAT_MODEL_H_ + +#include "model/listener/Listener.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/LinphoneEnums.hpp" + +#include +#include +#include + +class ChatModel : public ::Listener, + public linphone::ChatRoomListener, + public AbstractObject { + Q_OBJECT +public: + ChatModel(const std::shared_ptr &chatRoom, QObject *parent = nullptr); + ~ChatModel(); + + QDateTime getLastUpdateTime(); + QString getTitle(); + QString getPeerAddress() const; + QString getLastMessageInHistory(std::list> startList = {}) const; + int getUnreadMessagesCount() const; + std::list> getHistory() const; + +private: + DECLARE_ABSTRACT_OBJECT + + //-------------------------------------------------------------------------------- + // LINPHONE + //-------------------------------------------------------------------------------- + virtual void onIsComposingReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &remoteAddress, + bool isComposing) override; + virtual void onMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) override; + virtual void onMessagesReceived(const std::shared_ptr &chatRoom, + const std::list> &chatMessages) override; + virtual void onNewEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onNewEvents(const std::shared_ptr &chatRoom, + const std::list> &eventLogs) override; + virtual void onChatMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onChatMessagesReceived(const std::shared_ptr &chatRoom, + const std::list> &eventLogs) override; + virtual void onChatMessageSending(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onChatMessageSent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onParticipantAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onParticipantRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onParticipantAdminStatusChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onStateChanged(const std::shared_ptr &chatRoom, + linphone::ChatRoom::State newState) override; + virtual void onSecurityEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onSubjectChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onUndecryptableMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) override; + virtual void onParticipantDeviceAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onParticipantDeviceRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onParticipantDeviceStateChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog, + linphone::ParticipantDevice::State state) override; + virtual void + onParticipantDeviceMediaAvailabilityChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onConferenceJoined(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onConferenceLeft(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onEphemeralEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onEphemeralMessageTimerStarted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onEphemeralMessageDeleted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog) override; + virtual void onConferenceAddressGeneration(const std::shared_ptr &chatRoom) override; + virtual void onParticipantRegistrationSubscriptionRequested( + const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress) override; + virtual void onParticipantRegistrationUnsubscriptionRequested( + const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress) override; + virtual void onChatMessageShouldBeStored(const std::shared_ptr &chatRoom, + const std::shared_ptr &message) override; + virtual void onChatMessageParticipantImdnStateChanged( + const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &state) override; + virtual void onChatRoomRead(const std::shared_ptr &chatRoom) override; + virtual void onNewMessageReaction(const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &reaction) override; + +signals: + void isComposingReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &remoteAddress, + bool isComposing); + void messageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message); + void messagesReceived(const std::shared_ptr &chatRoom, + const std::list> &chatMessages); + void newEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void newEvents(const std::shared_ptr &chatRoom, + const std::list> &eventLogs); + void chatMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void chatMessagesReceived(const std::shared_ptr &chatRoom, + const std::list> &eventLogs); + void chatMessageSending(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void chatMessageSent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void participantAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void participantRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void participantAdminStatusChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void stateChanged(const std::shared_ptr &chatRoom, linphone::ChatRoom::State newState); + void securityEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void subjectChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void undecryptableMessageReceived(const std::shared_ptr &chatRoom, + const std::shared_ptr &message); + void participantDeviceAdded(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void participantDeviceRemoved(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void participantDeviceStateChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog, + linphone::ParticipantDevice::State state); + void participantDeviceMediaAvailabilityChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void conferenceJoined(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void conferenceLeft(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void ephemeralEvent(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void ephemeralMessageTimerStarted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void ephemeralMessageDeleted(const std::shared_ptr &chatRoom, + const std::shared_ptr &eventLog); + void conferenceAddressGeneration(const std::shared_ptr &chatRoom); + void + participantRegistrationSubscriptionRequested(const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress); + void + participantRegistrationUnsubscriptionRequested(const std::shared_ptr &chatRoom, + const std::shared_ptr &participantAddress); + void chatMessageShouldBeStored(const std::shared_ptr &chatRoom, + const std::shared_ptr &message); + void chatMessageParticipantImdnStateChanged(const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &state); + void chatRoomRead(const std::shared_ptr &chatRoom); + void newMessageReaction(const std::shared_ptr &chatRoom, + const std::shared_ptr &message, + const std::shared_ptr &reaction); +}; + +#endif diff --git a/Linphone/model/chat/message/ChatMessageModel.cpp b/Linphone/model/chat/message/ChatMessageModel.cpp new file mode 100644 index 000000000..da20a9503 --- /dev/null +++ b/Linphone/model/chat/message/ChatMessageModel.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatMessageModel.hpp" + +#include + +#include "core/path/Paths.hpp" +#include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" +#include "model/tool/ToolModel.hpp" +#include "tool/Utils.hpp" + +DEFINE_ABSTRACT_OBJECT(ChatMessageModel) + +ChatMessageModel::ChatMessageModel(const std::shared_ptr &chatMessage, QObject *parent) + : ::Listener(chatMessage, parent) { + lDebug() << "[ChatMessageModel] new" << this << " / SDKModel=" << chatMessage.get(); + mustBeInLinphoneThread(getClassName()); +} + +ChatMessageModel::~ChatMessageModel() { + mustBeInLinphoneThread("~" + getClassName()); +} + +QString ChatMessageModel::getText() const { + return ToolModel::getMessageFromContent(mMonitor->getContents()); +} + +QDateTime ChatMessageModel::getTimestamp() const { + return QDateTime::fromSecsSinceEpoch(mMonitor->getTime()); +} + +std::shared_ptr +ChatMessageModel::onFileTransferSend(const std::shared_ptr &message, + const std::shared_ptr &content, + size_t offset, + size_t size) { + return nullptr; +} diff --git a/Linphone/model/chat/message/ChatMessageModel.hpp b/Linphone/model/chat/message/ChatMessageModel.hpp new file mode 100644 index 000000000..d1a3fe041 --- /dev/null +++ b/Linphone/model/chat/message/ChatMessageModel.hpp @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +#ifndef CHAT_MESSAGE_MODEL_H_ +#define CHAT_MESSAGE_MODEL_H_ + +#include "model/listener/Listener.hpp" +#include "tool/AbstractObject.hpp" +#include "tool/LinphoneEnums.hpp" + +#include +#include +#include + +class ChatMessageModel : public ::Listener, + public linphone::ChatMessageListener, + public AbstractObject { + Q_OBJECT +public: + ChatMessageModel(const std::shared_ptr &chatMessage, QObject *parent = nullptr); + ~ChatMessageModel(); + + QString getText() const; + QDateTime getTimestamp() const; + +private: + DECLARE_ABSTRACT_OBJECT + virtual std::shared_ptr onFileTransferSend(const std::shared_ptr &message, + const std::shared_ptr &content, + size_t offset, + size_t size) override; +}; + +#endif diff --git a/Linphone/model/setting/SettingsModel.cpp b/Linphone/model/setting/SettingsModel.cpp index 8d19ad926..0fae2411c 100644 --- a/Linphone/model/setting/SettingsModel.cpp +++ b/Linphone/model/setting/SettingsModel.cpp @@ -736,7 +736,7 @@ void SettingsModel::notifyConfigReady(){ DEFINE_NOTIFY_CONFIG_READY(disableCommandLine, DisableCommandLine) } -DEFINE_GETSET_CONFIG(SettingsModel, bool, Bool, disableChatFeature, DisableChatFeature, "disable_chat_feature", true) +DEFINE_GETSET_CONFIG(SettingsModel, bool, Bool, disableChatFeature, DisableChatFeature, "disable_chat_feature", false) DEFINE_GETSET_CONFIG( SettingsModel, bool, Bool, disableMeetingsFeature, DisableMeetingsFeature, "disable_meetings_feature", false) DEFINE_GETSET_CONFIG(SettingsModel, diff --git a/Linphone/model/tool/ToolModel.cpp b/Linphone/model/tool/ToolModel.cpp index b3e8a9ba9..effd8f035 100644 --- a/Linphone/model/tool/ToolModel.cpp +++ b/Linphone/model/tool/ToolModel.cpp @@ -384,6 +384,22 @@ bool ToolModel::friendIsInFriendList(const std::shared_ptr return (it != friends.end()); } + +QString ToolModel::getMessageFromContent(std::list> contents) { + for (auto &content : contents) { + if (content->isText()) { + return Utils::coreStringToAppString(content->getUtf8Text()); + } else if (content->isFile()) { + return Utils::coreStringToAppString(content->getName()); + } else if (content->isIcalendar()) { + return QString("Invitation à une réunion"); + } else if (content->isMultipart()) { + return getMessageFromContent(content->getParts()); + } + } + return QString(""); +} + // Load downloaded codecs like OpenH264 (needs to be after core is created and has loaded its plugins, as // reloadMsPlugins modifies plugin path for the factory) void ToolModel::loadDownloadedCodecs() { diff --git a/Linphone/model/tool/ToolModel.hpp b/Linphone/model/tool/ToolModel.hpp index bc35cd70f..f6c4b40a8 100644 --- a/Linphone/model/tool/ToolModel.hpp +++ b/Linphone/model/tool/ToolModel.hpp @@ -69,6 +69,9 @@ public: static bool friendIsInFriendList(const std::shared_ptr &friendList, const std::shared_ptr &f); + + static QString getMessageFromContent(std::list> contents); + static void loadDownloadedCodecs(); static void updateCodecs(); diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index be4ac176f..050d9cf8b 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -318,7 +318,7 @@ QString Utils::formatElapsedTime(int seconds, bool dotsSeparator) { else return (h == 0 ? "" : hours + "h ") + (m == 0 ? "" : min + "min ") + sec + "s"; } -QString Utils::formatDate(const QDateTime &date, bool includeTime, QString format) { +QString Utils::formatDate(const QDateTime &date, bool includeTime, bool includeDateIfToday, QString format) { QString dateDay; //: "Aujourd'hui" if (date.date() == QDate::currentDate()) dateDay = tr("today"); @@ -333,6 +333,8 @@ QString Utils::formatDate(const QDateTime &date, bool includeTime, QString forma if (!includeTime) return dateDay; auto time = date.time().toString("hh:mm"); + if (!includeDateIfToday && date.date() == QDate::currentDate()) return time; + return dateDay + " | " + time; } diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index c8b60b5fe..c6062e129 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -89,7 +89,7 @@ public: Q_INVOKABLE static QString formatElapsedTime(int seconds, bool dotsSeparator = true); // Return the elapsed time formated Q_INVOKABLE static QString - formatDate(const QDateTime &date, bool includeTime = true, QString format = ""); // Return the date formated + formatDate(const QDateTime &date, bool includeTime = true, bool includeDateIfToday = true, QString format = ""); // Return the date formated Q_INVOKABLE static QString formatDateElapsedTime(const QDateTime &date); Q_INVOKABLE static QString formatTime(const QDateTime &date); // Return the time formated Q_INVOKABLE static QStringList generateSecurityLettersArray(int arraySize, int correctIndex, QString correctCode); diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index cae06cce3..087dec775 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -48,6 +48,9 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/Call/CallListView.qml view/Control/Display/Call/CallHistoryListView.qml view/Control/Display/Call/CallStatistics.qml + view/Control/Display/Chat/ChatListView.qml + view/Control/Display/Chat/ChatMessage.qml + view/Control/Display/Chat/ChatMessagesListView.qml view/Control/Display/Contact/Avatar.qml view/Control/Display/Contact/Contact.qml view/Control/Display/Contact/ContactListItem.qml @@ -98,6 +101,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Tool/Prototype/PhoneNumberPrototype.qml view/Page/Form/Call/NewCallForm.qml + view/Page/Form/Chat/SelectedChatView.qml view/Page/Form/Contact/ContactDescription.qml view/Page/Form/Contact/ContactEdition.qml view/Page/Form/Login/LoginPage.qml @@ -132,6 +136,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Page/Main/Call/CallPage.qml view/Page/Main/Call/CallSettingsPanel.qml view/Page/Main/Call/WaitingRoom.qml + view/Page/Main/Chat/ChatPage.qml view/Page/Main/Contact/ContactPage.qml view/Page/Main/Help/HelpPage.qml view/Page/Main/Meeting/MeetingPage.qml diff --git a/Linphone/view/Control/Button/BigButton.qml b/Linphone/view/Control/Button/BigButton.qml index 78a81251c..f83318996 100644 --- a/Linphone/view/Control/Button/BigButton.qml +++ b/Linphone/view/Control/Button/BigButton.qml @@ -8,10 +8,10 @@ Button { id: mainItem textSize: Typography.b1.pixelSize textWeight: Typography.b1.weight - leftPadding: Math.round(20.04 * DefaultStyle.dp) - rightPadding: Math.round(20.04 * DefaultStyle.dp) - topPadding: Math.round(11.2 * DefaultStyle.dp) - bottomPadding: Math.round(11.2 * DefaultStyle.dp) - icon.width: Math.round(24.89 * DefaultStyle.dp) - icon.height: Math.round(24.89 * DefaultStyle.dp) + leftPadding: Math.round(20 * DefaultStyle.dp) + rightPadding: Math.round(20 * DefaultStyle.dp) + topPadding: Math.round(11 * DefaultStyle.dp) + bottomPadding: Math.round(11 * DefaultStyle.dp) + icon.width: Math.round(24 * DefaultStyle.dp) + icon.height: Math.round(24 * DefaultStyle.dp) } diff --git a/Linphone/view/Control/Button/Button.qml b/Linphone/view/Control/Button/Button.qml index 2a152b1a8..58650ecdf 100644 --- a/Linphone/view/Control/Button/Button.qml +++ b/Linphone/view/Control/Button/Button.qml @@ -12,8 +12,9 @@ Control.Button { property var style property color color: style?.color?.normal || DefaultStyle.main1_500_main property color hoveredColor: style?.color?.hovered || Qt.darker(color, 1.05) - property color pressedColor: style?.color?.pressed || Qt.darker(color, 1.1) - property color textColor: style?.text?.normal || DefaultStyle.grey_0 + property color pressedColor: style?.color?.pressed || Qt.darker(color, 1.1) + property color checkedColor: style?.color?.checked || style?.color?.pressed || Qt.darker(color, 1.1) + property color textColor: style?.text?.normal || DefaultStyle.grey_0 property color hoveredTextColor: style?.text?.hovered || Qt.darker(textColor, 1.05) property color pressedTextColor: style?.text?.pressed || Qt.darker(textColor, 1.1) property color borderColor: style?.borderColor || "transparent" @@ -53,11 +54,13 @@ Control.Button { Rectangle { id: buttonBackground anchors.fill: parent - color: mainItem.pressed - ? mainItem.pressedColor - : mainItem.hovered || mainItem.hasNavigationFocus - ? mainItem.hoveredColor - : mainItem.color + color: mainItem.checkable && mainItem.checked + ? mainItem.checkedColor || mainItem.pressedColor + : mainItem.pressed + ? mainItem.pressedColor + : mainItem.hovered || mainItem.hasNavigationFocus + ? mainItem.hoveredColor + : mainItem.color radius: mainItem.radius border.color: mainItem.borderColor } @@ -95,11 +98,13 @@ Control.Button { wrapMode: Text.WrapAnywhere text: mainItem.text maximumLineCount: 1 - color: pressed - ? mainItem.pressedTextColor - : mainItem.hovered - ? mainItem.hoveredTextColor - : mainItem.textColor + color: mainItem.checkable && mainItem.checked + ? mainItem.checkedColor || mainItem.pressedColor + : mainItem.pressed + ? mainItem.pressedTextColor + : mainItem.hovered + ? mainItem.hoveredTextColor + : mainItem.textColor font { pixelSize: mainItem.textSize weight: mainItem.textWeight @@ -120,11 +125,13 @@ Control.Button { imageSource: mainItem.icon.source imageWidth: mainItem.icon.width imageHeight: mainItem.icon.height - colorizationColor: mainItem.pressed - ? mainItem.pressedImageColor - : mainItem.hovered - ? mainItem.hoveredImageColor - : mainItem.contentImageColor + colorizationColor: mainItem.checkable && mainItem.checked + ? mainItem.checkedColor || mainItem.pressedColor + : mainItem.pressed + ? mainItem.pressedImageColor + : mainItem.hovered + ? mainItem.hoveredImageColor + : mainItem.contentImageColor } contentItem: Control.StackView{ diff --git a/Linphone/view/Control/Button/CalendarComboBox.qml b/Linphone/view/Control/Button/CalendarComboBox.qml index 765b54675..4afcddc78 100644 --- a/Linphone/view/Control/Button/CalendarComboBox.qml +++ b/Linphone/view/Control/Button/CalendarComboBox.qml @@ -13,7 +13,7 @@ ComboBox { property alias contentText: contentText contentItem: Text { id: contentText - text: UtilsCpp.formatDate(calendar.selectedDate, false, "ddd d, MMMM") + text: UtilsCpp.formatDate(calendar.selectedDate, false, true, "ddd d, MMMM") anchors.fill: parent anchors.leftMargin: Math.round(15 * DefaultStyle.dp) anchors.verticalCenter: parent.verticalCenter diff --git a/Linphone/view/Control/Container/VerticalTabBar.qml b/Linphone/view/Control/Container/VerticalTabBar.qml index 2113934de..a896123a4 100644 --- a/Linphone/view/Control/Container/VerticalTabBar.qml +++ b/Linphone/view/Control/Container/VerticalTabBar.qml @@ -36,7 +36,6 @@ Control.TabBar { } component UnreadNotification: Rectangle { - id: unreadNotifications property int unread: 0 visible: unread > 0 width: Math.round(15 * DefaultStyle.dp) diff --git a/Linphone/view/Control/Display/Chat/ChatListView.qml b/Linphone/view/Control/Display/Chat/ChatListView.qml new file mode 100644 index 000000000..627e8b0b1 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatListView.qml @@ -0,0 +1,244 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone +import UtilsCpp +import SettingsCpp +import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle +import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils + +ListView { + id: mainItem + clip: true + + property SearchBar searchBar + property bool loading: false + property string searchText: searchBar?.text + property real busyIndicatorSize: Math.round(60 * DefaultStyle.dp) + + signal resultsReceived + + onResultsReceived: { + loading = false + // contentY = 0 + } + + model: ChatProxy { + id: chatProxy + Component.onCompleted: { + loading = true + } + filterText: mainItem.searchText + onFilterTextChanged: maxDisplayItems = initialDisplayItems + initialDisplayItems: Math.max( + 20, + 2 * mainItem.height / (Math.round(56 * DefaultStyle.dp))) + displayItemsStep: 3 * initialDisplayItems / 2 + onModelReset: { + mainItem.resultsReceived() + } + } + // flickDeceleration: 10000 + spacing: Math.round(10 * DefaultStyle.dp) + + Component.onCompleted: cacheBuffer = Math.max(contentHeight, 0) //contentHeight>0 ? contentHeight : 0// cache all items + // remove binding loop + onContentHeightChanged: Qt.callLater(function () { + if (mainItem) + mainItem.cacheBuffer = Math?.max(contentHeight, 0) || 0 + }) + + onActiveFocusChanged: if (activeFocus && currentIndex < 0 && count > 0) + currentIndex = 0 + onCountChanged: { + if (currentIndex < 0 && count > 0) { + mainItem.currentIndex = 0 // Select first item after loading model + } + if (atYBeginning) + positionViewAtBeginning() // Stay at beginning + } + + onAtYEndChanged: { + if (atYEnd && count > 0) { + chatProxy.displayMore() + } + } + //---------------------------------------------------------------- + function moveToCurrentItem() { + if (mainItem.currentIndex >= 0) + Utils.updatePosition(mainItem, mainItem) + } + onCurrentItemChanged: { + moveToCurrentItem() + } + // Update position only if we are moving to current item and its position is changing. + property var _currentItemY: currentItem?.y + on_CurrentItemYChanged: if (_currentItemY && moveAnimation.running) { + moveToCurrentItem() + } + Behavior on contentY { + NumberAnimation { + id: moveAnimation + duration: 500 + easing.type: Easing.OutExpo + alwaysRunToEnd: true + } + } + +// //---------------------------------------------------------------- + onVisibleChanged: { +// if (!visible) +// currentIndex = -1 + } + + BusyIndicator { + anchors.horizontalCenter: mainItem.horizontalCenter + visible: mainItem.loading + height: visible ? mainItem.busyIndicatorSize : 0 + width: mainItem.busyIndicatorSize + indicatorHeight: mainItem.busyIndicatorSize + indicatorWidth: mainItem.busyIndicatorSize + indicatorColor: DefaultStyle.main1_500_main + } + + // Qt bug: sometimes, containsMouse may not be send and update on each MouseArea. + // So we need to use this variable to switch off all hovered items. + property int lastMouseContainsIndex: -1 + + component UnreadNotification: Item { + id: unreadNotif + property int unread: 0 + width: Math.round(22 * DefaultStyle.dp) + height: Math.round(22 * DefaultStyle.dp) + visible: unread > 0 + Rectangle { + id: background + anchors.fill: parent + radius: width/2 + color: DefaultStyle.danger_500main + Text{ + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + color: DefaultStyle.grey_0 + fontSizeMode: Text.Fit + font.pixelSize: Typography.p3.pixelSize + text: parent.unreadNotif > 100 ? '99+' : unreadNotif.unread + } + } + MultiEffect { + id: shadow + anchors.fill: background + source: background + // Crash : https://bugreports.qt.io/browse/QTBUG-124730? + shadowEnabled: true + shadowColor: DefaultStyle.grey_1000 + shadowBlur: 1 + shadowOpacity: 0.15 + z: unreadNotif.z - 1 + } + } + + delegate: FocusScope { + width: mainItem.width + height: Math.round(63 * DefaultStyle.dp) + RowLayout { + z: 1 + anchors.fill: parent + anchors.leftMargin: Math.round(11 * DefaultStyle.dp) + anchors.rightMargin: Math.round(11 * DefaultStyle.dp) + anchors.topMargin: Math.round(9 * DefaultStyle.dp) + anchors.bottomMargin: Math.round(9 * DefaultStyle.dp) + spacing: Math.round(10 * DefaultStyle.dp) + Avatar { + id: historyAvatar + property var contactObj: UtilsCpp.findFriendByAddress(modelData.core.peerAddress) + contact: contactObj?.value || null + onContactChanged: { + if (contact) console.log("found contact", contact.core.defaultAddress) + else console.log("no contact for peer address", modelData.core.peerAddress, modelData.core.avatarUri) + } + displayNameVal: contact ? undefined : modelData.core.avatarUri + // secured: securityLevel === LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified + Layout.preferredWidth: Math.round(45 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(45 * DefaultStyle.dp) + // isConference: modelData.core.isConference + shadowEnabled: false + asynchronous: false + } + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + spacing: Math.round(5 * DefaultStyle.dp) + Text { + id: friendAddress + Layout.fillWidth: true + maximumLineCount: 1 + text: modelData.core.title + color: DefaultStyle.main2_800 + font { + pixelSize: Typography.p1.pixelSize + weight: unreadCount.unread > 0 ? Typography.p2.weight : Typography.p1.weight + capitalization: Font.Capitalize + } + } + Text { + Layout.fillWidth: true + maximumLineCount: 1 + text: modelData.core.lastMessageInHistory + color: DefaultStyle.main2_400 + font { + pixelSize: Typography.p1.pixelSize + weight: unreadCount.unread > 0 ? Typography.p2.weight : Typography.p1.weight + } + } + + } + + ColumnLayout { + Text { + color: DefaultStyle.main2_500main + text: UtilsCpp.formatDate(modelData.core.lastUpdatedTime, true, false) + font { + pixelSize: Typography.p3.pixelSize + weight: Typography.p3.weight + capitalization: Font.Capitalize + } + } + + RowLayout { + UnreadNotification { + id: unreadCount + unread: modelData.core.unreadMessagesCount + } + //sourdine, éphémère, IMDN + } + } + } + MouseArea { + hoverEnabled: true + anchors.fill: parent + focus: true + onContainsMouseChanged: { + if (containsMouse) + mainItem.lastMouseContainsIndex = index + else if (mainItem.lastMouseContainsIndex == index) + mainItem.lastMouseContainsIndex = -1 + } + Rectangle { + anchors.fill: parent + opacity: 0.7 + radius: Math.round(8 * DefaultStyle.dp) + color: mainItem.currentIndex + === index ? DefaultStyle.main2_200 : DefaultStyle.main2_100 + visible: mainItem.lastMouseContainsIndex === index + || mainItem.currentIndex === index + } + onPressed: { + mainItem.currentIndex = model.index + mainItem.forceActiveFocus() + } + } + } +} diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml new file mode 100644 index 000000000..7868823e7 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone +import UtilsCpp +import SettingsCpp +import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle +import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils + +Control.Control { + id: mainItem + property color backgroundColor + property bool isRemoteMessage + property bool isFirstMessage + + property string imgUrl + property string contentText + topPadding: Math.round(12 * DefaultStyle.dp) + bottomPadding: Math.round(12 * DefaultStyle.dp) + leftPadding: Math.round(18 * DefaultStyle.dp) + rightPadding: Math.round(18 * DefaultStyle.dp) + + background: Item { + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: mainItem.backgroundColor + radius: Math.round(16 * DefaultStyle.dp) + } + Rectangle { + visible: mainItem.isFirstMessage && mainItem.isRemoteMessage + anchors.top: parent.top + anchors.left: parent.left + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } + Rectangle { + visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage + anchors.bottom: parent.bottom + anchors.right: parent.right + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } + } + contentItem: ColumnLayout { + id: contentLayout + Image { + visible: mainItem.imgUrl != undefined + id: contentimage + } + Text { + visible: mainItem.contentText != undefined + text: mainItem.contentText + Layout.fillWidth: true + color: DefaultStyle.main2_700 + font { + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + } + } + Text { + Layout.alignment: Qt.AlignRight + text: UtilsCpp.formatDate(modelData.core.timestamp, true, false) + color: DefaultStyle.main2_500main + font { + pixelSize: Typography.p3.pixelSize + weight: Typography.p3.weight + } + } + } +} diff --git a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml new file mode 100644 index 000000000..d1b9f94f0 --- /dev/null +++ b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone +import UtilsCpp +import SettingsCpp +import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle +import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils + +ListView { + id: mainItem + property ChatGui chat + spacing: Math.round(20 * DefaultStyle.dp) + + Component.onCompleted: positionViewAtEnd() + + model: ChatMessageProxy { + chatGui: mainItem.chat + } + + delegate: ChatMessage { + id: chatMessage + width: Math.min(implicitWidth, Math.round(mainItem.width * (3/4))) + // height: childrenRect.height + backgroundColor: isRemoteMessage ? DefaultStyle.main2_100 : DefaultStyle.main1_100 + contentText: modelData.core.text + isFirstMessage: true + isRemoteMessage: modelData.core.isRemoteMessage + anchors.right: !isRemoteMessage && parent + ? parent.right + : undefined + } +} diff --git a/Linphone/view/Control/Display/Contact/Avatar.qml b/Linphone/view/Control/Display/Contact/Avatar.qml index c2dac732e..44895f06c 100644 --- a/Linphone/view/Control/Display/Contact/Avatar.qml +++ b/Linphone/view/Control/Display/Contact/Avatar.qml @@ -141,94 +141,95 @@ Loader{ } } - Component{ - id: initials - Item { - id: avatarItem - height: stackView.height - width: height - Rectangle { - id: initialItem - property string initials: mainItem.isConference ? "" : UtilsCpp.getInitials(mainItem.displayNameVal) - radius: width / 2 - color: DefaultStyle.main2_200 - height: stackView.height - width: height - Text { - anchors.fill: parent - anchors.centerIn: parent - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: initialItem.initials - font { - pixelSize: initialItem.height * 36 / 120 - weight: Typography.h4.weight - capitalization: Font.AllUppercase - } - } - EffectImage { - id: initialImg - visible: initialItem.initials == '' - width: stackView.width/2 - height: width - colorizationColor: DefaultStyle.main2_600 - imageSource: mainItem.isConference ? AppIcons.usersThree : AppIcons.profile - anchors.centerIn: parent - } - } - MultiEffect { - source: initialItem - anchors.fill: initialItem - shadowEnabled: true - shadowBlur: 0.1 - shadowColor: DefaultStyle.grey_1000 - shadowOpacity: 0.1 - } - Connections { - target: mainItem.call?.core ? mainItem.call.core : null - onRemoteNameChanged: initialItem.initials = UtilsCpp.getInitials(mainItem.call.core.remoteName) - } - } - } - Component{ - id: avatar - Item { - id: avatarItem - height: stackView.height - width: height - Image { - id: image - z: 200 - visible: false - width: parent.width - height: parent.height - sourceSize.width: avatarItem.width - sourceSize.height: avatarItem.height - fillMode: Image.PreserveAspectCrop - anchors.centerIn: parent - source: mainItem.account - ? mainItem.account.core.pictureUri - : mainItem.contact - ? mainItem.contact.core.pictureUri - : computedAvatarUri - mipmap: true - layer.enabled: true - } - ShaderEffect { - id: roundEffect - property variant src: image - property real edge: 0.9 - property real edgeSoftness: 0.9 - property real radius: width / 2.0 - property real shadowSoftness: 0.5 - property real shadowOffset: 0.01 - anchors.fill: parent - fragmentShader: 'qrc:/data/shaders/roundEffect.frag.qsb' - } - } - } } + + Component{ + id: initials + Item { + id: avatarItem + height: stackView.height + width: height + Rectangle { + id: initialItem + property string initials: mainItem.isConference ? "" : UtilsCpp.getInitials(mainItem.displayNameVal) + radius: width / 2 + color: DefaultStyle.main2_200 + height: stackView.height + width: height + Text { + anchors.fill: parent + anchors.centerIn: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: initialItem.initials + font { + pixelSize: initialItem.height * 36 / 120 + weight: Typography.h4.weight + capitalization: Font.AllUppercase + } + } + EffectImage { + id: initialImg + visible: initialItem.initials == '' + width: stackView.width/2 + height: width + colorizationColor: DefaultStyle.main2_600 + imageSource: mainItem.isConference ? AppIcons.usersThree : AppIcons.profile + anchors.centerIn: parent + } + } + MultiEffect { + source: initialItem + anchors.fill: initialItem + shadowEnabled: true + shadowBlur: 0.1 + shadowColor: DefaultStyle.grey_1000 + shadowOpacity: 0.1 + } + Connections { + target: mainItem.call?.core ? mainItem.call.core : null + onRemoteNameChanged: initialItem.initials = UtilsCpp.getInitials(mainItem.call.core.remoteName) + } + } + } + Component{ + id: avatar + Item { + id: avatarItem + height: stackView.height + width: height + Image { + id: image + z: 200 + visible: false + width: parent.width + height: parent.height + sourceSize.width: avatarItem.width + sourceSize.height: avatarItem.height + fillMode: Image.PreserveAspectCrop + anchors.centerIn: parent + source: mainItem.account + ? mainItem.account.core.pictureUri + : mainItem.contact + ? mainItem.contact.core.pictureUri + : computedAvatarUri + mipmap: true + layer.enabled: true + } + ShaderEffect { + id: roundEffect + property variant src: image + property real edge: 0.9 + property real edgeSoftness: 0.9 + property real radius: width / 2.0 + property real shadowSoftness: 0.5 + property real shadowOffset: 0.01 + anchors.fill: parent + fragmentShader: 'qrc:/data/shaders/roundEffect.frag.qsb' + } + } + } } } } diff --git a/Linphone/view/Control/Display/Conversation/ConversationListView.qml b/Linphone/view/Control/Display/Conversation/ConversationListView.qml new file mode 100644 index 000000000..088b81996 --- /dev/null +++ b/Linphone/view/Control/Display/Conversation/ConversationListView.qml @@ -0,0 +1,247 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone +import UtilsCpp +import SettingsCpp +import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle +import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils + +ListView { + id: mainItem + clip: true + + property SearchBar searchBar + property bool loading: false + property string searchText: searchBar?.text + property real busyIndicatorSize: Math.round(60 * DefaultStyle.dp) + + signal resultsReceived + + onResultsReceived: { + loading = false + // contentY = 0 + } + + model: CallHistoryProxy { + id: callHistoryProxy + Component.onCompleted: { + loading = true + } + filterText: mainItem.searchText + onFilterTextChanged: maxDisplayItems = initialDisplayItems + initialDisplayItems: Math.max( + 20, + 2 * mainItem.height / (Math.round(56 * DefaultStyle.dp))) + displayItemsStep: 3 * initialDisplayItems / 2 + onModelReset: { + mainItem.resultsReceived() + } + } + flickDeceleration: 10000 + spacing: Math.round(10 * DefaultStyle.dp) + + Keys.onPressed: event => { + if (event.key == Qt.Key_Escape) { + console.log("Back") + searchBar.forceActiveFocus() + event.accepted = true + } + } + + Component.onCompleted: cacheBuffer = Math.max( + contentHeight, + 0) //contentHeight>0 ? contentHeight : 0// cache all items + // remove binding loop + onContentHeightChanged: Qt.callLater(function () { + if (mainItem) + mainItem.cacheBuffer = Math?.max(contentHeight, 0) || 0 + }) + + onActiveFocusChanged: if (activeFocus && currentIndex < 0 && count > 0) + currentIndex = 0 + onCountChanged: { + if (currentIndex < 0 && count > 0) { + mainItem.currentIndex = 0 // Select first item after loading model + } + if (atYBeginning) + positionViewAtBeginning() // Stay at beginning + } + Connections { + target: deleteHistoryPopup + function onAccepted() { + mainItem.model.removeAllEntries() + } + } + + onAtYEndChanged: { + if (atYEnd && count > 0) { + callHistoryProxy.displayMore() + } + } + //---------------------------------------------------------------- + function moveToCurrentItem() { + if (mainItem.currentIndex >= 0) + Utils.updatePosition(mainItem, mainItem) + } + onCurrentItemChanged: { + moveToCurrentItem() + } + // Update position only if we are moving to current item and its position is changing. + property var _currentItemY: currentItem?.y + on_CurrentItemYChanged: if (_currentItemY && moveAnimation.running) { + moveToCurrentItem() + } + Behavior on contentY { + NumberAnimation { + id: moveAnimation + duration: 500 + easing.type: Easing.OutExpo + alwaysRunToEnd: true + } + } + + //---------------------------------------------------------------- + onVisibleChanged: { +// if (!visible) +// currentIndex = -1 + } + + BusyIndicator { + anchors.horizontalCenter: mainItem.horizontalCenter + visible: mainItem.loading + height: visible ? mainItem.busyIndicatorSize : 0 + width: mainItem.busyIndicatorSize + indicatorHeight: mainItem.busyIndicatorSize + indicatorWidth: mainItem.busyIndicatorSize + indicatorColor: DefaultStyle.main1_500_main + } + + // Qt bug: sometimes, containsMouse may not be send and update on each MouseArea. + // So we need to use this variable to switch off all hovered items. + property int lastMouseContainsIndex: -1 + delegate: FocusScope { + width: mainItem.width + height: Math.round(56 * DefaultStyle.dp) + RowLayout { + z: 1 + anchors.fill: parent + anchors.leftMargin: Math.round(10 * DefaultStyle.dp) + spacing: Math.round(10 * DefaultStyle.dp) + Avatar { + id: historyAvatar + property var contactObj: UtilsCpp.findFriendByAddress( + modelData.core.remoteAddress) + contact: contactObj?.value || null + displayNameVal: modelData.core.displayName + secured: securityLevel === LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified + width: Math.round(45 * DefaultStyle.dp) + height: Math.round(45 * DefaultStyle.dp) + isConference: modelData.core.isConference + shadowEnabled: false + asynchronous: false + } + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + spacing: Math.round(5 * DefaultStyle.dp) + Text { + id: friendAddress + Layout.fillWidth: true + maximumLineCount: 1 + text: historyAvatar.displayNameVal + font { + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + capitalization: Font.Capitalize + } + } + RowLayout { + spacing: Math.round(6 * DefaultStyle.dp) + EffectImage { + id: statusIcon + imageSource: modelData.core.status === LinphoneEnums.CallStatus.Declined + || modelData.core.status + === LinphoneEnums.CallStatus.DeclinedElsewhere + || modelData.core.status === LinphoneEnums.CallStatus.Aborted + || modelData.core.status === LinphoneEnums.CallStatus.EarlyAborted ? AppIcons.arrowElbow : modelData.core.isOutgoing ? AppIcons.arrowUpRight : AppIcons.arrowDownLeft + colorizationColor: modelData.core.status + === LinphoneEnums.CallStatus.Declined + || modelData.core.status + === LinphoneEnums.CallStatus.DeclinedElsewhere + || modelData.core.status + === LinphoneEnums.CallStatus.Aborted + || modelData.core.status + === LinphoneEnums.CallStatus.EarlyAborted + || modelData.core.status === LinphoneEnums.CallStatus.Missed ? DefaultStyle.danger_500main : modelData.core.isOutgoing ? DefaultStyle.info_500_main : DefaultStyle.success_500main + Layout.preferredWidth: Math.round(12 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(12 * DefaultStyle.dp) + transform: Rotation { + angle: modelData.core.isOutgoing + && (modelData.core.status === LinphoneEnums.CallStatus.Declined + || modelData.core.status + === LinphoneEnums.CallStatus.DeclinedElsewhere + || modelData.core.status === LinphoneEnums.CallStatus.Aborted + || modelData.core.status + === LinphoneEnums.CallStatus.EarlyAborted) ? 180 : 0 + origin { + x: statusIcon.width / 2 + y: statusIcon.height / 2 + } + } + } + Text { + // text: modelData.core.date + text: UtilsCpp.formatDate(modelData.core.date) + font { + pixelSize: Math.round(12 * DefaultStyle.dp) + weight: Math.round(300 * DefaultStyle.dp) + } + } + } + } + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.phone + focus: true + activeFocusOnTab: false + asynchronous: false + onClicked: { + if (modelData.core.isConference) { + var callsWindow = UtilsCpp.getCallsWindow() + callsWindow.setupConference( + modelData.core.conferenceInfo) + UtilsCpp.smartShowWindow(callsWindow) + } else { + UtilsCpp.createCall(modelData.core.remoteAddress) + } + } + } + } + MouseArea { + hoverEnabled: true + anchors.fill: parent + focus: true + onContainsMouseChanged: { + if (containsMouse) + mainItem.lastMouseContainsIndex = index + else if (mainItem.lastMouseContainsIndex == index) + mainItem.lastMouseContainsIndex = -1 + } + Rectangle { + anchors.fill: parent + opacity: 0.7 + radius: Math.round(8 * DefaultStyle.dp) + color: mainItem.currentIndex + === index ? DefaultStyle.main2_200 : DefaultStyle.main2_100 + visible: mainItem.lastMouseContainsIndex === index + || mainItem.currentIndex === index + } + onPressed: { + mainItem.currentIndex = model.index + mainItem.forceActiveFocus() + } + } + } +} diff --git a/Linphone/view/Control/Input/TextField.qml b/Linphone/view/Control/Input/TextField.qml index 674ecb66a..5dbd0ecdf 100644 --- a/Linphone/view/Control/Input/TextField.qml +++ b/Linphone/view/Control/Input/TextField.qml @@ -167,9 +167,9 @@ Control.TextField { } } Keys.onReleased: event => { - if (event.jey == Qt.Key_Control) - mainItem.controlIsDown = false - } + if (event.jey == Qt.Key_Control) + mainItem.controlIsDown = false + } Button { id: eyeButton diff --git a/Linphone/view/Page/Form/Chat/SelectedChatView.qml b/Linphone/view/Page/Form/Chat/SelectedChatView.qml new file mode 100644 index 000000000..30dcc9b29 --- /dev/null +++ b/Linphone/view/Page/Form/Chat/SelectedChatView.qml @@ -0,0 +1,186 @@ +import QtCore +import QtQuick +import QtQuick.Controls.Basic as Control +import QtQuick.Dialogs +import QtQuick.Effects +import QtQuick.Layouts +import Linphone +import UtilsCpp +import SettingsCpp +import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle + +RowLayout { + id: mainItem + property ChatGui chat + spacing: 0 + MainRightPanel { + Layout.fillWidth: true + Layout.fillHeight: true + panelColor: DefaultStyle.grey_0 + clip: true + headerContent: [ + RowLayout { + anchors.left: parent.left + anchors.leftMargin: Math.round(31 * DefaultStyle.dp) + anchors.verticalCenter: parent.verticalCenter + spacing: Math.round(12 * DefaultStyle.dp) + Avatar { + property var contactObj: mainItem.chat ? UtilsCpp.findFriendByAddress(mainItem.chat.core.peerAddress) : null + contact: contactObj?.value || null + Layout.preferredWidth: Math.round(45 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(45 * DefaultStyle.dp) + } + Text { + text: mainItem.chat?.core.title || "" + color: DefaultStyle.main2_600 + Layout.fillWidth: true + maximumLineCount: 1 + font { + pixelSize: Typography.h4.pixelSize + weight: Math.round(400 * DefaultStyle.dp) + } + } + }, + RowLayout { + anchors.right: parent.right + anchors.rightMargin: Math.round(41 * DefaultStyle.dp) + anchors.verticalCenter: parent.verticalCenter + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.phone + } + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.camera + } + BigButton { + style: ButtonStyle.noBackground + checkable: true + icon.source: AppIcons.info + onCheckedChanged: { + + } + } + } + ] + + content: [ + ChatMessagesListView { + id: chatMessagesListView + height: contentHeight + width: parent.width - anchors.leftMargin - anchors.rightMargin + chat: mainItem.chat + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: messageSender.top + anchors.leftMargin: Math.round(18 * DefaultStyle.dp) + anchors.rightMargin: Math.round(18 * DefaultStyle.dp) + anchors.bottomMargin: Math.round(18 * DefaultStyle.dp) + Control.ScrollBar.vertical: scrollbar + }, + ScrollBar { + id: scrollbar + visible: chatMessagesListView.contentHeight > parent.height + active: visible + anchors.top: parent.top + anchors.bottom: chatMessagesListView.bottom + anchors.right: parent.right + anchors.rightMargin: Math.round(5 * DefaultStyle.dp) + policy: Control.ScrollBar.AsNeeded + }, + Control.Control { + id: messageSender + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 79 * DefaultStyle.dp + leftPadding: Math.round(15 * DefaultStyle.dp) + rightPadding: Math.round(15 * DefaultStyle.dp) + topPadding: Math.round(16 * DefaultStyle.dp) + bottomPadding: Math.round(16 * DefaultStyle.dp) + background: Rectangle { + color: DefaultStyle.grey_100 + } + contentItem: RowLayout { + spacing: Math.round(20 * DefaultStyle.dp) + RowLayout { + spacing: Math.round(16 * DefaultStyle.dp) + BigButton { + style: ButtonStyle.noBackground + checkable: true + icon.source: AppIcons.smiley + onCheckedChanged: { + console.log("TODO : emoji") + } + } + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.paperclip + onClicked: { + console.log("TODO : open explorer to attach file") + } + } + Control.Control { + Layout.fillWidth: true + Layout.preferredHeight: Math.round(48 * DefaultStyle.dp) + background: Rectangle { + id: inputBackground + anchors.fill: parent + radius: Math.round(30 * DefaultStyle.dp) + color: DefaultStyle.grey_0 + } + contentItem: RowLayout { + TextArea { + id: sendingTextArea + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: parent.width - stackButton.width + } + StackLayout { + id: stackButton + currentIndex: sendingTextArea.text.length === 0 ? 0 : 1 + BigButton { + style: ButtonStyle.noBackground + icon.source: AppIcons.microphone + onClicked: { + console.log("TODO : go to record message") + } + } + BigButton { + style: ButtonStyle.noBackgroundOrange + icon.source: AppIcons.paperPlaneRight + onClicked: { + console.log("TODO : send message") + } + } + } + } + } + } + } + } + ] + + } + Rectangle { + visible: detailsPanel.visible + color: DefaultStyle.main2_200 + Layout.preferredWidth: Math.round(1 * DefaultStyle.dp) + Layout.fillHeight: true + } + Control.Control { + id: detailsPanel + visible: false + Layout.fillHeight: true + Layout.preferredWidth: Math.round(387 * DefaultStyle.dp) + background: Rectangle { + color: DefaultStyle.grey_0 + anchors.fill: parent + } + contentItem: ColumnLayout { + + } + } +} + diff --git a/Linphone/view/Page/Layout/Main/MainLayout.qml b/Linphone/view/Page/Layout/Main/MainLayout.qml index 9b581a28b..1f3c0e368 100644 --- a/Linphone/view/Page/Layout/Main/MainLayout.qml +++ b/Linphone/view/Page/Layout/Main/MainLayout.qml @@ -616,8 +616,7 @@ Item { } } } - Item {} - //ConversationPage{} + ChatPage{} MeetingPage {} } } diff --git a/Linphone/view/Page/Main/Call/CallPage.qml b/Linphone/view/Page/Main/Call/CallPage.qml index 5afd1e585..c00547fc4 100644 --- a/Linphone/view/Page/Main/Call/CallPage.qml +++ b/Linphone/view/Page/Main/Call/CallPage.qml @@ -674,8 +674,7 @@ AbstractMainPage { } } Text { - text: UtilsCpp.formatDate( - modelData.core.date) + text: UtilsCpp.formatDate(modelData.core.date) color: modelData.core.status === LinphoneEnums.CallStatus.Missed ? DefaultStyle.danger_500main : DefaultStyle.main2_500main font { pixelSize: Math.round(12 * DefaultStyle.dp) diff --git a/Linphone/view/Page/Main/Chat/ChatPage.qml b/Linphone/view/Page/Main/Chat/ChatPage.qml new file mode 100644 index 000000000..85dcb0fd1 --- /dev/null +++ b/Linphone/view/Page/Main/Chat/ChatPage.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control +import Linphone +import UtilsCpp +import SettingsCpp +import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle +import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils + +AbstractMainPage { + id: mainItem + //: "Nouvelle conversation" + noItemButtonText: qsTr("chat_start_title") + //: "Aucune conversation" + emptyListText: qsTr("chat_empty_title") + newItemIconSource: AppIcons.plusCircle + + property var selectedChatGui + + onSelectedChatGuiChanged: { + if (selectedChatGui) + rightPanelStackView.replace(currentChatComp, + Control.StackView.Immediate) + else + rightPanelStackView.replace(emptySelection, + Control.StackView.Immediate) + } + + rightPanelStackView.initialItem: emptySelection + + onNoItemButtonPressed: goToNewChat() + + showDefaultItem: listStackView.currentItem + && listStackView.currentItem.objectName == "chatListItem" + && listStackView.currentItem.listView.count === 0 || false + + function goToNewChat() { + if (listStackView.currentItem + && listStackView.currentItem.objectName != "newChatItem") + listStackView.push(newChatItem) + } + + Dialog { + id: deleteChatPopup + width: Math.round(637 * DefaultStyle.dp) + //: Supprimer la conversation ? + title: qsTr("chat_dialog_delete_chat_title") + //: "La conversation et tous ses messages seront supprimés." + text: qsTr("chat_dialog_delete_chat_message") + } + + leftPanelContent: Control.StackView { + id: listStackView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: Math.round(45 * DefaultStyle.dp) + clip: true + initialItem: chatListItem + focus: true + onActiveFocusChanged: if (activeFocus) { + currentItem.forceActiveFocus() + } + } + + Component { + id: chatListItem + FocusScope { + objectName: "chatListItem" + property alias listView: chatListView + ColumnLayout { + anchors.fill: parent + spacing: 0 + RowLayout { + spacing: Math.round(16 * DefaultStyle.dp) + Text { + Layout.fillWidth: true + //: "Conversations" + text: qsTr("chat_list_title") + color: DefaultStyle.main2_700 + font.pixelSize: Typography.h2.pixelSize + font.weight: Typography.h2.weight + } + Item { + Layout.fillWidth: true + } + Button { + id: newChatButton + style: ButtonStyle.noBackground + icon.source: AppIcons.plusCircle + Layout.preferredWidth: Math.round(28 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(28 * DefaultStyle.dp) + Layout.rightMargin: Math.round(39 * DefaultStyle.dp) + icon.width: Math.round(28 * DefaultStyle.dp) + icon.height: Math.round(28 * DefaultStyle.dp) + KeyNavigation.down: searchBar + onClicked: { + console.debug("[ChatPage]User: create new chat") + mainItem.goToNewChat() + } + } + } + SearchBar { + id: searchBar + Layout.fillWidth: true + Layout.topMargin: Math.round(18 * DefaultStyle.dp) + Layout.rightMargin: Math.round(39 * DefaultStyle.dp) + //: "Rechercher une conversation" + placeholderText: qsTr("chat_search_in_history") + visible: chatListView.count !== 0 || text.length !== 0 + focus: true + KeyNavigation.up: newChatButton + KeyNavigation.down: chatListView + Binding { + target: mainItem + property: "showDefaultItem" + when: searchBar.text.length != 0 + value: false + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColumnLayout { + anchors.fill: parent + anchors.rightMargin: Math.round(39 * DefaultStyle.dp) + Text { + visible: chatListView.count === 0 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Math.round(137 * DefaultStyle.dp) + //: "Aucun résultat…" + text: searchBar.text.length != 0 ? qsTr("list_filter_no_result_found") + //: "Aucune conversation dans votre historique" + : qsTr("chat_list_empty_history") + font { + pixelSize: Typography.h4.pixelSize + weight: Typography.h4.weight + } + } + ChatListView { + id: chatListView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Math.round(39 * DefaultStyle.dp) + searchBar: searchBar + Control.ScrollBar.vertical: scrollbar + + onCurrentIndexChanged: { + mainItem.selectedChatGui = model.getAt(currentIndex) + } + onCountChanged: { + mainItem.selectedChatGui = model.getAt(currentIndex) + } + } + } + ScrollBar { + id: scrollbar + visible: chatListView.contentHeight > parent.height + active: visible + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: Math.round(8 * DefaultStyle.dp) + policy: Control.ScrollBar.AsNeeded + } + } + } + } + } + + Component { + id: newChatItem + FocusScope { + objectName: "newChatItem" + width: parent?.width + height: parent?.height + Control.StackView.onActivated: { + callContactsList.forceActiveFocus() + } + ColumnLayout { + anchors.fill: parent + spacing: 0 + RowLayout { + spacing: Math.round(10 * DefaultStyle.dp) + Button { + Layout.preferredWidth: Math.round(24 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(24 * DefaultStyle.dp) + style: ButtonStyle.noBackground + icon.source: AppIcons.leftArrow + focus: true + KeyNavigation.down: listStackView + onClicked: { + console.debug( + "[CallPage]User: return to call history") + listStackView.pop() + listStackView.forceActiveFocus() + } + } + Text { + Layout.fillWidth: true + //: "New chat" + text: qsTr("chat_action_start_new_chat") + color: DefaultStyle.main2_700 + font.pixelSize: Typography.h2.pixelSize + font.weight: Typography.h2.weight + } + Item { + Layout.fillWidth: true + } + } + // NewCallForm { + // id: callContactsList + // Layout.topMargin: Math.round(18 * DefaultStyle.dp) + // Layout.fillWidth: true + // Layout.fillHeight: true + // focus: true + // numPadPopup: numericPadPopupItem + // groupCallVisible: true + // searchBarColor: DefaultStyle.grey_100 + // onContactClicked: contact => { + // mainWindow.startCallWithContact(contact, false, callContactsList) + // } + // onGroupCallCreationRequested: { + // console.log("groupe call requetsed") + // listStackView.push(groupCallItem) + // } + // Connections { + // target: mainItem + // function onCreateCallFromSearchBarRequested() { + // UtilsCpp.createCall(callContactsList.searchBar.text) + // } + // } + // } + } + } + } + + Component { + id: emptySelection + Item { + objectName: "emptySelection" + } + } + Component { + id: currentChatComp + FocusScope { + SelectedChatView { + anchors.fill: parent + chat: mainItem.selectedChatGui + } + } + } +} diff --git a/Linphone/view/Style/AppIcons.qml b/Linphone/view/Style/AppIcons.qml index 42652f102..1027b4475 100644 --- a/Linphone/view/Style/AppIcons.qml +++ b/Linphone/view/Style/AppIcons.qml @@ -72,8 +72,10 @@ QtObject { property string trustedMask: "image://internal/trusted-mask.svg" property string avatar: "image://internal/randomAvatar.png" property string pause: "image://internal/pause.svg" - property string play: "image://internal/play.svg" - property string smiley: "image://internal/smiley.svg" + property string play: "image://internal/play.svg" + property string paperclip: "image://internal/paperclip.svg" + property string paperPlaneRight: "image://internal/paper-plane-right.svg" + property string smiley: "image://internal/smiley.svg" property string smileySad: "image://internal/smiley-sad.svg" property string trashCan: "image://internal/trash-simple.svg" property string copy: "image://internal/copy.svg" diff --git a/Linphone/view/Style/Typography.qml b/Linphone/view/Style/Typography.qml index bd8375733..ad94c6dce 100644 --- a/Linphone/view/Style/Typography.qml +++ b/Linphone/view/Style/Typography.qml @@ -30,6 +30,13 @@ QtObject { pixelSize: Math.round(36 * DefaultStyle.dp), weight: Math.min(Math.round(800 * DefaultStyle.dp), 1000) }) + + // Text/P3 - Reduced paragraph text + property font p3: Qt.font( { + family: DefaultStyle.defaultFont, + pixelSize: Math.round(12 * DefaultStyle.dp), + weight: Math.min(Math.round(300 * DefaultStyle.dp), 1000) + }) // Text/P2 - Bold, reduced paragraph text property font p2: Qt.font( { diff --git a/Linphone/view/Style/buttonStyle.js b/Linphone/view/Style/buttonStyle.js index 7aa784029..0f24865b0 100644 --- a/Linphone/view/Style/buttonStyle.js +++ b/Linphone/view/Style/buttonStyle.js @@ -130,17 +130,20 @@ color: { normal: "#00000000", hovered: "#00000000", - pressed: "#00000000" + pressed: "#00000000", + checked: Linphone.DefaultStyle.main1_500main }, text: { normal: Linphone.DefaultStyle.main2_600, hovered: Linphone.DefaultStyle.main2_700, - pressed: Linphone.DefaultStyle.main2_800 + pressed: Linphone.DefaultStyle.main2_800, + checked: Linphone.DefaultStyle.main1_500main }, image: { normal: Linphone.DefaultStyle.main2_600, hovered: Linphone.DefaultStyle.main2_700, - pressed: Linphone.DefaultStyle.main2_800 + pressed: Linphone.DefaultStyle.main2_800, + checked: Linphone.DefaultStyle.main1_500main } } diff --git a/external/linphone-sdk b/external/linphone-sdk index 96a6385bf..c66673ac4 160000 --- a/external/linphone-sdk +++ b/external/linphone-sdk @@ -1 +1 @@ -Subproject commit 96a6385bfdb3e6c0ed85df42bda99467e8f6fc5f +Subproject commit c66673ac4b844d4b147c3629629a65f83efb3b38