- Shortcut in Reply to message's origin.

- Avoid to load huge chat room at the start of a call.
- Asynchronous chat room load.
- Fix video freeze on network change.
This commit is contained in:
Julien Wadel 2022-01-28 16:14:57 +01:00
parent 4ca37adc60
commit 33f382d80a
12 changed files with 140 additions and 81 deletions

View file

@ -8,13 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Features:
* messages features : Reply, forward (to contact, to a SIP address or to a timeline), Vocal record and play, multi contents.
* Messages features : Reply, forward (to contact, to a SIP address or to a timeline), Vocal record and play, multi contents.
- Add a feedback on fetching remote provisioning when it failed.
- Option to enable message notifications.
- CPIM on basic chat rooms.
- New event on new messages in chat and a shortcut to go to the end of chat if last message is not shown.
- Device name can be changed from settings.
- New event on new messages in chat and a shortcut to go to the end of chat if last message is not shown.
- Shortcut in Reply to message's origin.
- Based on Linphone SDK 5.1
### Fixed
@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Take account of return key on Numpad
- Huge messages are better shown and with less flickering.
- Adapt UserAgent with device name.
- Video freeze on network change.
## 4.3.2

View file

@ -881,12 +881,48 @@ void ChatRoomModel::updateNewMessageNotice(const int& count){
beginInsertRows(QModelIndex(), 0, 0);
mEntries.prepend(mUnreadMessageNotice);
endInsertRows();
qWarning() << "New message notice timestamp to :" << lastUnreadMessage.toString();
qDebug() << "New message notice timestamp to :" << lastUnreadMessage.toString();
}
//emit layoutChanged();
}
}
int ChatRoomModel::loadTillMessage(ChatMessageModel * message){
if( message){
qDebug() << "Load history till message : " << message->getChatMessage()->getMessageId().c_str();
auto linphoneMessage = message->getChatMessage();
// First find on current list
auto entry = std::find_if(mEntries.begin(), mEntries.end(), [linphoneMessage](const std::shared_ptr<ChatEvent>& entry ){
return entry->mType == ChatRoomModel::EntryType::MessageEntry && dynamic_cast<ChatMessageModel*>(entry.get())->getChatMessage() == linphoneMessage;
});
// if not find, load more entries and find it in new entries.
if( entry == mEntries.end()){
int newEntries = loadMoreEntries();
while( newEntries > 0){// no more new entries
int entryCount = 0;
entry = mEntries.begin();
while(entryCount < newEntries &&
((*entry)->mType != ChatRoomModel::EntryType::MessageEntry || dynamic_cast<ChatMessageModel*>(entry->get())->getChatMessage() != linphoneMessage)
){
++entryCount;
++entry;
}
if( entryCount < newEntries){// We got it
qDebug() << "Find message at " << entryCount << " after loading new entries";
return entryCount;
}else
newEntries = loadMoreEntries();// continue
}
}else{
int entryCount = entry - mEntries.begin();
qDebug() << "Find message at " << entryCount;
return entryCount;
}
qWarning() << "Message has not been found in history";
}
return -1;
}
void ChatRoomModel::initEntries(){
qDebug() << "Internal Entries : Init";
// On call : reinitialize all entries. This allow to free up memory

View file

@ -97,7 +97,6 @@ signals:
void chatMessageShouldBeStored(const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message);
void chatMessageParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state);
};
class ChatRoomModel : public QAbstractListModel {
@ -119,9 +118,6 @@ public:
};
Q_ENUM(EntryType)
//Q_PROPERTY(QString participants READ getParticipants NOTIFY participantsChanged);
//Q_PROPERTY(ParticipantProxyModel participants READ getParticipants NOTIFY participantsChanged);
Q_PROPERTY(QString subject READ getSubject WRITE setSubject NOTIFY subjectChanged)
Q_PROPERTY(QDateTime lastUpdateTime MEMBER mLastUpdateTime WRITE setLastUpdateTime NOTIFY lastUpdateTimeChanged)
Q_PROPERTY(int unreadMessagesCount MEMBER mUnreadMessagesCount WRITE setUnreadMessagesCount NOTIFY unreadMessagesCountChanged)
@ -238,6 +234,7 @@ public:
Q_INVOKABLE int loadMoreEntries(); // return new entries count
void callEnded(std::shared_ptr<linphone::Call> call);
void updateNewMessageNotice(const int& count);
Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index. -1 if not found.
QDateTime mLastUpdateTime;
int mUnreadMessagesCount = 0;

View file

@ -38,47 +38,10 @@ using namespace std;
QString ChatRoomProxyModel::gCachedText;
// Fetch the L last filtered chat entries.
class ChatRoomProxyModel::ChatRoomModelFilter : public QSortFilterProxyModel {
public:
ChatRoomModelFilter (QObject *parent) : QSortFilterProxyModel(parent) {}
int getEntryTypeFilter () {
return mEntryTypeFilter;
}
void setEntryTypeFilter (int type) {
mEntryTypeFilter = type;
invalidate();
}
protected:
bool filterAcceptsRow (int sourceRow, const QModelIndex &) const override {
if (mEntryTypeFilter == ChatRoomModel::EntryType::GenericEntry)
return true;
QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex());
auto eventModel = sourceModel()->data(index);
if( mEntryTypeFilter == ChatRoomModel::EntryType::CallEntry && eventModel.value<ChatCallModel*>() != nullptr)
return true;
if( mEntryTypeFilter == ChatRoomModel::EntryType::MessageEntry && eventModel.value<ChatMessageModel*>() != nullptr)
return true;
if( mEntryTypeFilter == ChatRoomModel::EntryType::NoticeEntry && eventModel.value<ChatNoticeModel*>() != nullptr)
return true;
return false;
}
private:
int mEntryTypeFilter = ChatRoomModel::EntryType::GenericEntry;
};
// =============================================================================
ChatRoomProxyModel::ChatRoomProxyModel (QObject *parent) : QSortFilterProxyModel(parent) {
setSourceModel(new ChatRoomModelFilter(this));
mMarkAsReadEnabled = true;
//mIsSecure = false;
App *app = App::getInstance();
QObject::connect(app->getMainWindow(), &QWindow::activeChanged, this, [this]() {
@ -109,13 +72,12 @@ ChatRoomProxyModel::ChatRoomProxyModel (QObject *parent) : QSortFilterProxyModel
void ChatRoomProxyModel::METHOD (ARG_TYPE value) { \
GET_CHAT_MODEL()->METHOD(value); \
}
#define CREATE_PARENT_MODEL_FUNCTION_WITH_ID(METHOD) \
void ChatRoomProxyModel::METHOD (int id) { \
QModelIndex sourceIndex = mapToSource(index(id, 0)); \
GET_CHAT_MODEL()->METHOD( \
static_cast<ChatRoomModelFilter *>(sourceModel())->mapToSource(sourceIndex).row() \
); \
GET_CHAT_MODEL()->METHOD( \
mapFromSource(static_cast<ChatRoomModel*>(sourceModel())->index(id, 0)).row() \
); \
}
CREATE_PARENT_MODEL_FUNCTION(removeAllEntries)
@ -139,6 +101,10 @@ void ChatRoomProxyModel::compose (const QString& text) {
gCachedText = text;
}
int ChatRoomProxyModel::getEntryTypeFilter () {
return mEntryTypeFilter;
}
// -----------------------------------------------------------------------------
void ChatRoomProxyModel::loadMoreEntriesAsync(){
@ -155,10 +121,9 @@ void ChatRoomProxyModel::loadMoreEntries() {
}
void ChatRoomProxyModel::setEntryTypeFilter (int type) {
ChatRoomModelFilter *ChatRoomModelFilter = static_cast<ChatRoomProxyModel::ChatRoomModelFilter *>(sourceModel());
if (ChatRoomModelFilter->getEntryTypeFilter() != type) {
ChatRoomModelFilter->setEntryTypeFilter(type);
if (getEntryTypeFilter() != type) {
mEntryTypeFilter = type;
invalidate();
emit entryTypeFilterChanged(type);
}
}
@ -167,7 +132,21 @@ void ChatRoomProxyModel::setEntryTypeFilter (int type) {
bool ChatRoomProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const {
bool show = false;
if(mFilterText != ""){
if (mEntryTypeFilter == ChatRoomModel::EntryType::GenericEntry)
show = true;
else{
QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex());
auto eventModel = sourceModel()->data(index);
if( mEntryTypeFilter == ChatRoomModel::EntryType::CallEntry && eventModel.value<ChatCallModel*>() != nullptr)
show = true;
else if( mEntryTypeFilter == ChatRoomModel::EntryType::MessageEntry && eventModel.value<ChatMessageModel*>() != nullptr)
show = true;
else if( mEntryTypeFilter == ChatRoomModel::EntryType::NoticeEntry && eventModel.value<ChatNoticeModel*>() != nullptr)
show = true;
}
if( show && mFilterText != ""){
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
auto eventModel = sourceModel()->data(index);
ChatMessageModel * chatModel = eventModel.value<ChatMessageModel*>();
@ -175,11 +154,10 @@ bool ChatRoomProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex &sou
QRegularExpression search(QRegularExpression::escape(mFilterText), QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption);
show = chatModel->mContent.contains(search);
}
}else
show = true;
}
return show;
}
bool ChatRoomProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const {
auto l = sourceModel()->data(left);
auto r = sourceModel()->data(right);
@ -279,7 +257,6 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) {
QObject::disconnect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded);
}
mChatRoomModel = CoreManager::getInstance()->getTimelineListModel()->getChatRoomModel(chatRoomModel);
if (mChatRoomModel) {
@ -290,11 +267,12 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) {
QObject::connect(ChatRoomModel, &ChatRoomModel::messageSent, this, &ChatRoomProxyModel::handleMessageSent);
QObject::connect(ChatRoomModel, &ChatRoomModel::markAsReadEnabledChanged, this, &ChatRoomProxyModel::markAsReadEnabledChanged);
QObject::connect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded);
mChatRoomModel->initEntries();// This way, we don't load huge chat rooms (that lead to freeze GUI)
}
static_cast<ChatRoomModelFilter *>(sourceModel())->setSourceModel(mChatRoomModel.get());
setSourceModel(mChatRoomModel.get());
invalidate();
}
void ChatRoomProxyModel::resetMessageCount(){
if( mChatRoomModel){
mChatRoomModel->resetMessageCount();
@ -314,6 +292,15 @@ void ChatRoomProxyModel::setFilterText(const QString& text){
}
}
int ChatRoomProxyModel::loadTillMessage(ChatMessageModel * message){
int messageIndex = mChatRoomModel->loadTillMessage(message);
if( messageIndex>= 0 ) {
messageIndex = mapFromSource(static_cast<ChatRoomModel*>(sourceModel())->index(messageIndex, 0)).row();
}
qDebug() << "Message index from chat room proxy : " << messageIndex;
return messageIndex;
}
ChatRoomModel *ChatRoomProxyModel::getChatRoomModel () const{
return mChatRoomModel.get();

View file

@ -52,13 +52,15 @@ class ChatRoomProxyModel : public QSortFilterProxyModel {
public:
ChatRoomProxyModel (QObject *parent = Q_NULLPTR);
int getEntryTypeFilter ();
Q_INVOKABLE void setEntryTypeFilter (int type);
Q_INVOKABLE QString getDisplayNameComposers()const;
Q_INVOKABLE QVariant getAt(int row);
Q_INVOKABLE void loadMoreEntriesAsync ();
Q_INVOKABLE void loadMoreEntries ();
Q_INVOKABLE void setEntryTypeFilter (int type);
Q_INVOKABLE void removeAllEntries ();
Q_INVOKABLE void removeRow (int index);
@ -75,6 +77,8 @@ public:
Q_INVOKABLE void setFilterText(const QString& text);
Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index in displayed list (-1 if not found)
public slots:
void onMoreEntriesLoaded(const int& count);
@ -133,6 +137,7 @@ private:
void handleMessageSent (const std::shared_ptr<linphone::ChatMessage> &message);
int mMaxDisplayedEntries = EntriesChunkSize;
int mEntryTypeFilter = ChatRoomModel::EntryType::GenericEntry;
QString mPeerAddress;
QString mLocalAddress;

View file

@ -99,7 +99,6 @@ void TimelineModel::setSelected(const bool& selected){
<< ", isAdmin:"<< mChatRoomModel->isMeAdmin()
<< ", canHandleParticipants:"<< mChatRoomModel->canHandleParticipants()
<< ", hasBeenLeft:" << mChatRoomModel->hasBeenLeft();
mChatRoomModel->initEntries();
}
emit selectedChanged(mSelected);
}

View file

@ -31,6 +31,15 @@ Rectangle {
color: ChatStyle.color
function positionViewAtIndex(index){
chat.bindToEnd = false
chat.positionViewAtIndex(index, ListView.Beginning)
}
function goToMessage(message){
positionViewAtIndex(container.proxyModel.loadTillMessage(message))
}
ColumnLayout {
anchors.fill: parent
spacing: 0
@ -41,13 +50,15 @@ Rectangle {
// -----------------------------------------------------------------------
property bool bindToEnd: false
property bool displaying: false
property int loaderCount: 0
property int readyItems : 0
property bool loadingLoader: (readyItems != loaderCount)
property bool loadingEntries: container.proxyModel.chatRoomModel.entriesLoading || displaying
property bool tryToLoadMoreEntries: loadingEntries || loadingLoader
property bool loadingEntries: (container.proxyModel.chatRoomModel && container.proxyModel.chatRoomModel.entriesLoading) || displaying
property bool tryToLoadMoreEntries: loadingEntries || remainingLoadersCount>0
property bool isMoving : false // replace moving read-only property to allow using movement signals.
// Load optimizations
property int remainingLoadersCount: 0
property int syncLoaderBatch: 50 // batch of simultaneous loaders on synchronous mode
//------------------------------------
onLoadingEntriesChanged: {
if( loadingEntries && !displaying)
displaying = true
@ -60,10 +71,9 @@ Rectangle {
interval: 5000
repeat: false
running: false
onTriggered: container.proxyModel.chatRoomModel.resetMessageCount()
onTriggered: if(container.proxyModel.chatRoomModel) container.proxyModel.chatRoomModel.resetMessageCount()
}
//property var sipAddressObserver: SipAddressesModel.getSipAddressObserver(proxyModel.fullPeerAddress, proxyModel.fullLocalAddress)
// -----------------------------------------------------------------------
Layout.fillHeight: true
Layout.fillWidth: true
@ -250,16 +260,20 @@ Rectangle {
height: (item !== null && typeof(item)!== 'undefined')? item.height: 0
Layout.fillWidth: true
source: Logic.getComponentFromEntry($chatEntry)
property int count: 0
asynchronous: chat.count - count > 100
onStatusChanged: if( status == Loader.Ready) ++chat.readyItems
Component.onCompleted: count = ++chat.loaderCount
Component.onDestruction: {
--chat.loaderCount
if( status == Loader.Ready)
--chat.readyItems
}
property int loaderIndex: 0 // index of loader from remaining loaders
property int remainingIndex : loaderIndex % ((chat.remainingLoadersCount) / chat.syncLoaderBatch) != 0 // Check loader index to remaining loader.
onRemainingIndexChanged: if( remainingIndex == 0 && asynchronous) asynchronous = false
asynchronous: true
onStatusChanged: if( status == Loader.Ready) {
remainingIndex = -1 // overwrite to remove signal changed. That way, there is no more binding loops.
--chat.remainingLoadersCount // Loader is ready: remove one from remaining count.
}
Component.onCompleted: loaderIndex = ++chat.remainingLoadersCount // on new Loader : one more remaining
Component.onDestruction: if( status != Loader.Ready) --chat.remainingLoadersCount // Remove remaining count if not loaded
}
Connections{
target: loader.item
ignoreUnknownSignals: true
@ -289,6 +303,10 @@ Rectangle {
}
})
}
onGoToMessage:{
container.goToMessage(message) // sometimes, there is no access to chat id (maybe because of cleaning component while loading new items). Use a global intermediate.
}
}
}
}
@ -489,3 +507,4 @@ Rectangle {
}
}

View file

@ -36,6 +36,7 @@ Item {
height: fitHeight
onMainChatMessageModelChanged: if( mainChatMessageModel && mainChatMessageModel.replyChatMessageModel) chatMessageModel = mainChatMessageModel.replyChatMessageModel
signal goToMessage(ChatMessageModel message)
ColumnLayout{
anchors.fill: parent
@ -51,6 +52,10 @@ Item {
iconSize: ChatReplyMessageStyle.header.replyIcon.iconSize
height: iconSize
overwriteColor: ChatReplyMessageStyle.header.color
MouseArea{
anchors.fill: parent
onClicked: mainItem.goToMessage(mainItem.chatMessageModel)
}
}
Text{
id: headerText
@ -62,6 +67,10 @@ Item {
font.family: mainItem.customFont.family
font.pointSize: Units.dp * (mainItem.customFont.pointSize + ChatReplyMessageStyle.header.pointSizeOffset)
color: ChatReplyMessageStyle.header.color
MouseArea{
anchors.fill: parent
onClicked: mainItem.goToMessage(mainItem.chatMessageModel)
}
}
}
Rectangle{

View file

@ -17,6 +17,7 @@ RowLayout {
signal copySelectionDone()
signal replyClicked()
signal forwardClicked()
signal goToMessage(ChatMessageModel message)
implicitHeight: message.height
spacing: 0
@ -67,6 +68,7 @@ RowLayout {
onCopySelectionDone: parent.copySelectionDone()
onReplyClicked: parent.replyClicked()
onForwardClicked: parent.forwardClicked()
onGoToMessage: parent.goToMessage(message)
Layout.fillWidth: true

View file

@ -34,6 +34,7 @@ Item {
signal copySelectionDone()
signal replyClicked()
signal forwardClicked()
signal goToMessage(ChatMessageModel message)
// ---------------------------------------------------------------------------
property string lastTextSelected
@ -87,6 +88,7 @@ Item {
onFitWidthChanged:{
rectangle.updateWidth()
}
onGoToMessage: container.goToMessage(message)
}
ListView {

View file

@ -19,6 +19,7 @@ Item {
signal copySelectionDone()
signal replyClicked()
signal forwardClicked()
signal goToMessage(ChatMessageModel message)
Message {
id: message
@ -27,6 +28,7 @@ Item {
onCopySelectionDone: parent.copySelectionDone()
onReplyClicked: parent.replyClicked()
onForwardClicked: parent.forwardClicked()
onGoToMessage: parent.goToMessage(message)
anchors {
left: parent.left

@ -1 +1 @@
Subproject commit 938bcf6d288eba2ff430165fe3da22f8cde9d42e
Subproject commit 9cc416ad81725beb1d04baa9c6e1dc3d23db11ac