fix unread messages

fix do not add chat in list if events intended for another account

chat notif in vertical bar / account list

fix cursor on hovered link

fix sending area height
This commit is contained in:
Gaelle Braud 2025-07-01 11:18:22 +02:00
parent 0655672c32
commit c015375ae9
10 changed files with 187 additions and 137 deletions

View file

@ -476,6 +476,14 @@ QList<QSharedPointer<EventLogCore>> ChatCore::getEventLogList() const {
}
void ChatCore::resetEventLogList(QList<QSharedPointer<EventLogCore>> list) {
for (auto &e : mEventLogList) {
disconnect(e.get());
}
for (auto &e : list) {
if (auto message = e->getChatMessageCore()) {
connect(message.get(), &ChatMessageCore::isReadChanged, this, [this] { emit lUpdateUnreadCount(); });
}
}
mEventLogList = list;
emit eventListChanged();
}
@ -484,6 +492,8 @@ void ChatCore::appendEventLogsToEventLogList(QList<QSharedPointer<EventLogCore>>
int nbAdded = 0;
for (auto &e : list) {
if (mEventLogList.contains(e)) continue;
if (auto message = e->getChatMessageCore())
connect(message.get(), &ChatMessageCore::isReadChanged, this, [this] { emit lUpdateUnreadCount(); });
mEventLogList.append(e);
++nbAdded;
}
@ -496,6 +506,8 @@ void ChatCore::appendEventLogToEventLogList(QSharedPointer<EventLogCore> e) {
return e->getEventLogId() == event->getEventLogId();
});
if (it == mEventLogList.end()) {
if (auto message = e->getChatMessageCore())
connect(message.get(), &ChatMessageCore::isReadChanged, this, [this] { emit lUpdateUnreadCount(); });
mEventLogList.append(e);
emit eventsInserted({e});
}
@ -505,6 +517,7 @@ void ChatCore::removeEventLogsFromEventLogList(QList<QSharedPointer<EventLogCore
int nbRemoved = 0;
for (auto &e : list) {
if (mEventLogList.contains(e)) {
if (auto message = e->getChatMessageCore()) disconnect(message.get());
mEventLogList.removeAll(e);
++nbRemoved;
}
@ -513,6 +526,9 @@ void ChatCore::removeEventLogsFromEventLogList(QList<QSharedPointer<EventLogCore
}
void ChatCore::clearEventLogList() {
for (auto &e : mEventLogList) {
disconnect(e.get());
}
mEventLogList.clear();
emit eventListChanged();
}

View file

@ -22,6 +22,7 @@
#include "ChatCore.hpp"
#include "ChatGui.hpp"
#include "core/App.hpp"
#include "model/tool/ToolModel.hpp"
#include <QSharedPointer>
#include <linphone++/linphone.hh>
@ -109,63 +110,60 @@ void ChatList::setSelf(QSharedPointer<ChatList> me) {
mModelConnection->makeConnectToModel(
&CoreModel::defaultAccountChanged,
[this](std::shared_ptr<linphone::Core> core, std::shared_ptr<linphone::Account> account) { lUpdate(); });
mModelConnection->makeConnectToModel(
&CoreModel::messageReceived,
[this](const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::ChatRoom> &room,
const std::shared_ptr<linphone::ChatMessage> &message) {
auto chatCore = ChatCore::create(room);
mModelConnection->invokeToCore([this, chatCore] {
auto chatList = getSharedList<ChatCore>();
auto it =
std::find_if(chatList.begin(), chatList.end(), [chatCore](const QSharedPointer<ChatCore> item) {
return item && chatCore && item->getModel() && chatCore->getModel() &&
item->getModel()->getMonitor() == chatCore->getModel()->getMonitor();
});
if (it == chatList.end()) {
connectItem(chatCore);
add(chatCore);
emit chatAdded();
}
});
});
auto addChatToList = [this](const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &room,
const std::shared_ptr<linphone::ChatMessage> &message) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto receiverAccount = ToolModel::findAccount(message->getToAddress());
if (!receiverAccount) {
qWarning() << log().arg("Receiver account not found in account list, return");
return;
}
auto receiverAddress = receiverAccount->getContactAddress();
if (!receiverAddress) {
qWarning() << log().arg("Receiver account has no address, return");
return;
}
auto defaultAddress = core->getDefaultAccount()->getContactAddress();
if (!defaultAddress->weakEqual(receiverAddress)) {
qDebug() << log().arg("Receiver account is not the default one, do not add chat to list");
return;
}
auto chatCore = ChatCore::create(room);
mModelConnection->invokeToCore([this, chatCore] {
auto chatList = getSharedList<ChatCore>();
auto it = std::find_if(chatList.begin(), chatList.end(), [chatCore](const QSharedPointer<ChatCore> item) {
return item && chatCore && item->getModel() && chatCore->getModel() &&
item->getModel()->getMonitor() == chatCore->getModel()->getMonitor();
});
if (it == chatList.end()) {
connectItem(chatCore);
add(chatCore);
emit chatAdded();
}
});
};
mModelConnection->makeConnectToModel(&CoreModel::messageReceived,
[this, addChatToList](const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &room,
const std::shared_ptr<linphone::ChatMessage> &message) {
addChatToList(core, room, message);
});
mModelConnection->makeConnectToModel(
&CoreModel::messagesReceived,
[this](const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::ChatRoom> &room,
const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) {
auto chatCore = ChatCore::create(room);
mModelConnection->invokeToCore([this, chatCore] {
auto chatList = getSharedList<ChatCore>();
auto it =
std::find_if(chatList.begin(), chatList.end(), [chatCore](const QSharedPointer<ChatCore> item) {
return item && chatCore && item->getModel() && chatCore->getModel() &&
item->getModel()->getMonitor() == chatCore->getModel()->getMonitor();
});
if (it == chatList.end()) {
connectItem(chatCore);
add(chatCore);
emit chatAdded();
}
});
[this, addChatToList](const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &room,
const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) {
addChatToList(core, room, messages.front());
});
mModelConnection->makeConnectToModel(
&CoreModel::newMessageReaction,
[this](const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::ChatRoom> &room,
const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ChatMessageReaction> &reaction) {
auto chatCore = ChatCore::create(room);
mModelConnection->invokeToCore([this, chatCore] {
auto chatList = getSharedList<ChatCore>();
auto it =
std::find_if(chatList.begin(), chatList.end(), [chatCore](const QSharedPointer<ChatCore> item) {
return item && chatCore && item->getModel() && chatCore->getModel() &&
item->getModel()->getMonitor() == chatCore->getModel()->getMonitor();
});
if (it == chatList.end()) {
connectItem(chatCore);
add(chatCore);
emit chatAdded();
}
});
[this, addChatToList](const std::shared_ptr<linphone::Core> &core,
const std::shared_ptr<linphone::ChatRoom> &room,
const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ChatMessageReaction> &reaction) {
addChatToList(core, room, message);
});
connect(this, &ChatList::filterChanged, [this](QString filter) {

View file

@ -1,7 +1,6 @@
list(APPEND _LINPHONEAPP_RC_FILES data/assistant/use-app-sip-account.rc
data/assistant/create-app-sip-account.rc
data/assistant/use-other-sip-account.rc
data/shaders/opacityMask.frag.qsb
data/shaders/roundEffect.frag.qsb
data/emoji/emoji.json
)

View file

@ -45,8 +45,7 @@ AccountModel::AccountModel(const std::shared_ptr<linphone::Account> &account, QO
});
connect(CoreModel::getInstance().get(), &CoreModel::unreadNotificationsChanged, this, [this]() {
emit unreadNotificationsChanged(0 /*mMonitor->getUnreadChatMessageCount()*/,
mMonitor->getMissedCallsCount()); // TODO
emit unreadNotificationsChanged(mMonitor->getUnreadChatMessageCount(), mMonitor->getMissedCallsCount());
});
connect(CoreModel::getInstance().get(), &CoreModel::accountRemoved, this,
[this](const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account) {

View file

@ -14,26 +14,24 @@ ListView {
spacing: Math.round(4 * DefaultStyle.dp)
property ChatGui chat
property color backgroundColor
property bool lastItemVisible: false
signal showReactionsForMessageRequested(ChatMessageGui chatMessage)
signal showImdnStatusForMessageRequested(ChatMessageGui chatMessage)
signal replyToMessageRequested(ChatMessageGui chatMessage)
Component.onCompleted: {
var index = eventLogProxy.findFirstUnreadIndex()
positionViewAtIndex(index, ListView.End)
var chatMessage = eventLogProxy.getEventAtIndex(index)?.core?.chatMessage
if (chatMessage && !chatMessage.isRead) chatMessage.lMarkAsRead()
Qt.callLater(function() {
var index = eventLogProxy.findFirstUnreadIndex()
positionViewAtIndex(index, ListView.End)
})
}
onCountChanged: if (atYEnd) {
var index = eventLogProxy.findFirstUnreadIndex()
mainItem.positionViewAtIndex(index, ListView.End)
var chatMessage = eventLogProxy.getEventAtIndex(index)?.core?.chatMessage
if (chatMessage && !chatMessage.isRead) chatMessage.lMarkAsRead()
positionViewAtEnd()
}
Button {
visible: !mainItem.atYEnd
visible: !mainItem.lastItemVisible
icon.source: AppIcons.downArrow
leftPadding: Math.round(16 * DefaultStyle.dp)
rightPadding: Math.round(16 * DefaultStyle.dp)
@ -46,11 +44,14 @@ ListView {
onClicked: {
var index = eventLogProxy.findFirstUnreadIndex()
mainItem.positionViewAtIndex(index, ListView.End)
var chatMessage = eventLogProxy.getEventAtIndex(index)?.core?.chatMessage
if (chatMessage && !chatMessage.isRead) chatMessage.lMarkAsRead()
}
}
onAtYEndChanged: if (atYEnd) {
if (eventLogProxy.haveMore)
eventLogProxy.displayMore()
}
model: EventLogProxy {
id: eventLogProxy
chatGui: mainItem.chat
@ -58,8 +59,6 @@ ListView {
onEventInserted: (index, gui) => {
if (!mainItem.visible) return
mainItem.positionViewAtIndex(index, ListView.End)
if (gui.core.chatMessage && !gui.core.chatMessage.isRead)
gui.core.chatMessage.lMarkAsRead()
}
}
@ -114,67 +113,92 @@ ListView {
}
delegate: DelegateChooser {
delegate: DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "chatMessage"
delegate:
ChatMessage {
chatMessage: modelData
chat: mainItem.chat
maxWidth: Math.round(mainItem.width * (3/4))
onVisibleChanged: {
if (visible && !modelData.core.isRead) modelData.core.lMarkAsRead()
}
width: mainItem.width
property var previousIndex: index - 1
property var previousFromAddress: eventLogProxy.getEventAtIndex(index-1)?.core.chatMessage?.fromAddress
backgroundColor: isRemoteMessage ? DefaultStyle.main2_100 : DefaultStyle.main1_100
isFirstMessage: !previousFromAddress || previousFromAddress !== modelData.core.fromAddress
anchors.right: !isRemoteMessage && parent
? parent.right
: undefined
delegate: ChatMessage {
id: chatMessageDelegate
property int yoff: Math.round(chatMessageDelegate.y - mainItem.contentY)
property bool isFullyVisible: (yoff > mainItem.y && yoff + height < mainItem.y + mainItem.height)
onIsFullyVisibleChanged: {
if (index === mainItem.count - 1) {
mainItem.lastItemVisible = isFullyVisible
}
if (isFullyVisible)
modelData.core.lMarkAsRead()
}
chatMessage: modelData
chat: mainItem.chat
maxWidth: Math.round(mainItem.width * (3/4))
onVisibleChanged: {
if (visible) {
modelData.core.lMarkAsRead()
}
}
width: mainItem.width
property var previousIndex: index - 1
property var previousFromAddress: eventLogProxy.getEventAtIndex(index-1)?.core.chatMessage?.fromAddress
backgroundColor: isRemoteMessage ? DefaultStyle.main2_100 : DefaultStyle.main1_100
isFirstMessage: !previousFromAddress || previousFromAddress !== modelData.core.fromAddress
anchors.right: !isRemoteMessage && parent
? parent.right
: undefined
onMessageDeletionRequested: modelData.core.lDelete()
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(modelData)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(modelData)
onReplyToMessageRequested: mainItem.replyToMessageRequested(modelData)
}
onMessageDeletionRequested: modelData.core.lDelete()
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(modelData)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(modelData)
onReplyToMessageRequested: mainItem.replyToMessageRequested(modelData)
}
}
DelegateChoice {
roleValue: "event"
delegate:
Item {
property bool showTopMargin: !header.visible && index == 0
width: mainItem.width
height: (showTopMargin ? 30 : 0 * DefaultStyle.dp) + eventItem.implicitHeight
Event {
id: eventItem
anchors.top: parent.top
anchors.topMargin: showTopMargin ? 30 : 0 * DefaultStyle.dp
width: parent.width
eventLogGui: modelData
}
}
delegate: Item {
id: eventDelegate
property int yoff: Math.round(eventDelegate.y - mainItem.contentY)
property bool isFullyVisible: (yoff > mainItem.y && yoff + height < mainItem.y + mainItem.height)
onIsFullyVisibleChanged: {
if (index === mainItem.count - 1) {
mainItem.lastItemVisible = isFullyVisible
}
}
property bool showTopMargin: !header.visible && index == 0
width: mainItem.width
height: (showTopMargin ? 30 : 0 * DefaultStyle.dp) + eventItem.implicitHeight
Event {
id: eventItem
anchors.top: parent.top
anchors.topMargin: showTopMargin ? 30 : 0 * DefaultStyle.dp
width: parent.width
eventLogGui: modelData
}
}
}
DelegateChoice {
roleValue: "ephemeralEvent"
delegate:
Item {
property bool showTopMargin: !header.visible && index == 0
width: mainItem.width
//height: 40 * DefaultStyle.dp
height: (showTopMargin ? 30 : 0 * DefaultStyle.dp) + eventItem.height
EphemeralEvent {
id: eventItem
anchors.top: parent.top
anchors.topMargin: showTopMargin ? 30 : 0 * DefaultStyle.dp
eventLogGui: modelData
}
}
delegate: Item {
id: ephemeralEventDelegate
property int yoff: Math.round(ephemeralEventDelegate.y - mainItem.contentY)
property bool isFullyVisible: (yoff > mainItem.y && yoff + height < mainItem.y + mainItem.height)
onIsFullyVisibleChanged: {
if (index === mainItem.count - 1) {
mainItem.lastItemVisible = isFullyVisible
}
}
property bool showTopMargin: !header.visible && index == 0
width: mainItem.width
//height: 40 * DefaultStyle.dp
height: (showTopMargin ? 30 : 0 * DefaultStyle.dp) + eventItem.height
EphemeralEvent {
id: eventItem
anchors.top: parent.top
anchors.topMargin: showTopMargin ? 30 : 0 * DefaultStyle.dp
eventLogGui: modelData
}
}
}
}

View file

@ -27,7 +27,7 @@ TextEdit {
property var encodeTextObj: visible ? UtilsCpp.encodeTextToQmlRichFormat(contentGui.core.utf8Text, {}, mainItem.chatGui)
: ''
text: encodeTextObj ? encodeTextObj.value : ""
text: encodeTextObj && encodeTextObj.value || ""
textFormat: Text.RichText // To supports links and imgs.
wrapMode: TextEdit.Wrap
@ -43,11 +43,10 @@ TextEdit {
}
onSelectedTextChanged:{
if(selectedText != '') lastTextSelected = selectedText
// else {
// if(mouseArea.keepLastSelection) {
// mouseArea.keepLastSelection = false
// }
// }
}
onLinkHovered: {
if (hoveredLink !== "") UtilsCpp.setGlobalCursor(Qt.PointingHandCursor)
else UtilsCpp.restoreGlobalCursor()
}
onActiveFocusChanged: {
if(activeFocus) {

View file

@ -17,20 +17,23 @@ Item {
property string thumbnail: contentGui && contentGui.core.thumbnail
property string name: contentGui && contentGui.core.name
property string filePath: contentGui && contentGui.core.filePath
property bool active: true
property real animationScale : FileViewStyle.animation.to
property alias imageScale: thumbnailProvider.scale
property bool wasDownloaded: contentGui && contentGui.core.wasDownloaded
property bool isAnimatedImage : contentGui && contentGui.core.wasDownloaded && UtilsCpp.isAnimatedImage(filePath)
property bool haveThumbnail: contentGui && UtilsCpp.canHaveThumbnail(filePath)
property int fileSize: contentGui ? contentGui.core.fileSize : 0
property bool isTransferring
property bool isVideo: UtilsCpp.isVideo(filePath)
property bool isImage: UtilsCpp.isImage(filePath)
property bool isPdf: UtilsCpp.isPdf(filePath)
property bool isThumbnail: isVideo || isImage || isPdf
property int overriddenWidth
property int overriddenHeight
Connections {
enabled: contentGui
target: contentGui.core
function onMsgStateChanged(state) {
isTransferring = state === LinphoneEnums.ChatMessageState.StateFileTransferInProgress
mainItem.isTransferring = state === LinphoneEnums.ChatMessageState.StateFileTransferInProgress
|| state === LinphoneEnums.ChatMessageState.StateInProgress
}
}

View file

@ -145,7 +145,7 @@ Control.Control{
Layout.preferredHeight: Math.round(26 * DefaultStyle.dp)
Layout.fillHeight: true
Layout.leftMargin: Math.round(40 * DefaultStyle.dp)
visible: mainItem.account.core.unreadCallNotifications > 0
visible: mainItem.account.core.unreadNotifications > 0
Rectangle{
id: unreadNotifications
anchors.verticalCenter: parent.verticalCenter
@ -166,7 +166,7 @@ Control.Control{
fontSizeMode: Text.Fit
font.pixelSize: Math.round(11 * DefaultStyle.dp)
font.weight: Math.round(700 * DefaultStyle.dp)
text: mainItem.account.core.unreadCallNotifications >= 100 ? '99+' : mainItem.account.core.unreadCallNotifications
text: mainItem.account.core.unreadNotifications >= 100 ? '99+' : mainItem.account.core.unreadNotifications
}
}
MultiEffect {

View file

@ -58,7 +58,7 @@ Control.Control {
// height: mainItem.height
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(24 * DefaultStyle.dp)
topPadding: Math.round(16 * DefaultStyle.dp)
bottomPadding: Math.round(16 * DefaultStyle.dp)
background: Rectangle {
anchors.fill: parent
@ -67,6 +67,10 @@ Control.Control {
contentItem: Control.StackView {
id: sendingAreaStackView
initialItem: textAreaComp
width: currentItem.width
onHeightChanged: {
mainItem.height = height + mainItem.topPadding + mainItem.bottomPadding
}
Component {
id: textAreaComp
RowLayout {
@ -97,11 +101,16 @@ Control.Control {
}
}
Control.Control {
id: sendingControl
onHeightChanged: {
sendingAreaStackView.height = height
}
Layout.fillWidth: true
leftPadding: Math.round(15 * DefaultStyle.dp)
rightPadding: Math.round(15 * DefaultStyle.dp)
topPadding: Math.round(15 * DefaultStyle.dp)
bottomPadding: Math.round(15 * DefaultStyle.dp)
Layout.alignment: Qt.AlignCenter
leftPadding: Math.round(24 * DefaultStyle.dp)
rightPadding: Math.round(20 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(12 * DefaultStyle.dp)
background: Rectangle {
id: inputBackground
anchors.fill: parent
@ -116,7 +125,7 @@ Control.Control {
contentItem: RowLayout {
Flickable {
id: sendingAreaFlickable
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight)
Layout.preferredHeight: Math.min(Math.round(100 * DefaultStyle.dp), contentHeight)
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: sendingTextArea.contentHeight
@ -224,6 +233,9 @@ Control.Control {
}
ChatAudioContent {
id: voiceMessage
onHeightChanged: {
sendingAreaStackView.height = height
}
recording: true
Layout.fillWidth: true
Layout.preferredHeight: Math.round(48 * DefaultStyle.dp)

View file

@ -213,7 +213,7 @@ RowLayout {
Control.Control {
id: participantListPopup
width: parent.width
height: Math.min(contentItem.height, Math.round(200 * DefaultStyle.dp))
height: Math.min(participantInfoList.height, Math.round(200 * DefaultStyle.dp))
visible: false
anchors.bottom: chatMessagesListView.bottom
anchors.left: chatMessagesListView.left
@ -378,7 +378,7 @@ RowLayout {
}
ChatDroppableTextArea {
id: messageSender
Control.SplitView.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
Control.SplitView.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : height
Control.SplitView.minimumHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp)
chat: mainItem.chat
onChatChanged: {