Skip to content

New ChatView: Support incoming updates of messages#1426

Merged
tmolitor-stud-tu merged 9 commits into
monal-im:developfrom
lissine0:message-updates
Aug 17, 2025
Merged

New ChatView: Support incoming updates of messages#1426
tmolitor-stud-tu merged 9 commits into
monal-im:developfrom
lissine0:message-updates

Conversation

@lissine0

@lissine0 lissine0 commented May 7, 2025

Copy link
Copy Markdown
Member

Use kMonalUpdatedMessageNotice for message corrections and MUC message reflections, instead of using kMonalNewMessageNotice.

I added observing of this new notification in all the places that are observing kMonalNewMessageNotice, except for MonalAppDelegate.
I think that's not needed because message updates don't cause the unread counter to be changed.

Also, I'm not sure observing the new notification in MLContact is strictly necessary, but it's not harmful.

On a semi-related note, you once said that when posting notifications, MLNotificationQueue should always be used (as opposed to NSNotificationCenter)
It turns out there are some places in the code base that use NSNotificationCenter for posting notifications.
See them with grep --color=auto -in 'NotificationCenter.*post' Monal/Classes/*.{m,swift}
Should I use MLNotificationQueue in these places?
(The CFNotificationCenter ones should remain)

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Most of these notifications have a comment, explaining why NSNotificationCenter is used directly, someting linke this one: //don't queue this notification because it should be handled immediately.

kMonalFrozen and kMonalUnfrozen as well as kMonalIncomingIPC should be handled inline, too, but don't have such a comment.

Only Classes/PasswordMigration.swift should use MLNotificationQueue instead of NSNotificationCenter.

Background: We don't want incoming stanzas to have side effects through notifications while we are still inside the db transaction wrapping this stanza processing. So We use MLContificationQueue instead of NSNotificationCenter everywhere in the code. That class will forward the call directly to NSNotificationCenter unless a queue was created by its "context manager" (context manager in the python sense, using objc blocks). Then the notification is queued until the context manager exits. Context managers can be nested, which lets the queued notifications bubble up to the next context manager, if the inner one exits. Just to mention the obvious: the context manager only captures notifications created inside the thread the context manager runs in.

Incoming stanza processing is one place, we use that context manager to queue notifications. Another one is processing history fetches from MAM. We use it to suppress all notifications for "new" messages in this case and clean the queue instead of letting the notifications "bubble up".

There are only a few places (those you found) where we don't want to queue notifications and always handle them immediately.

@tmolitor-stud-tu

tmolitor-stud-tu commented May 8, 2025

Copy link
Copy Markdown
Member

I added observing of this new notification in all the places that are observing kMonalNewMessageNotice, except for MonalAppDelegate.
I think that's not needed because message updates don't cause the unread counter to be changed.

Yes, that makes sense :)

Also, I'm not sure observing the new notification in MLContact is strictly necessary, but it's not harmful.

No, that's not needed, you can remove that one :)

Comment thread Monal/Classes/ChatView.swift Outdated
func findAndUpdateMessage(with message: MLMessage) {
if message.isEqual(to: self.contact.obj) {
for index in messages.indices {
if messages[index].message.messageDBId == message.messageDBId {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, the proper implementation is to extend MLMessage to be a full fledged model class handling updates itself (and being a per-db-id singleton), just like MLContact.

Then use the KVO wrapper to automatically update the UI.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, if I just update the MLMessage object, for example:

if messages[index].message.messageDBId == message.messageDBId {
    messages[index].message.update(with: message)

Then SwiftUI doesn't detect that the messages @State variable changed, and doesn't re-render the view.
By inserting a fresh new entry in the messages array, SwiftUI figures this out correctly and updates the view. (the live update requires a SwiftPM dependencies bump to work correctly. But I did that in my other PR)

@tmolitor-stud-tu tmolitor-stud-tu May 11, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly, that you can update this PR to use the ObservableKVOWrapper and let MLMessage be a proper model class, once I merge your other PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following can work:
In the ChatViewMessage class, we make the MLMessage property an ObservedObject (using KVOObserver for example).
Then in that same class, we listen for changes to the MLMessage property, and when it does change, we update the properties of the class (the ones that are currently only set in the initializer e.g. the text property)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just override the properties of the base class in our ChatViewMessage class using computed properties? The could compute the right values using the MLMessage and even properly propagate property changes of the MLMessage properties up the chain (using the ObservableKVOWrapper or something similar).

@matthewrfennell what do you think? imho this should be possible, so that changing the messageText property inside an MLMessage should automatically bubble up the chain all the way to swiftui consuming that message text.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me, I haven't tried it, but I think it should work

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that should work, but it's not the question/solution I proposed. I want the MLMessage be a singleton over the db id, just like MLContact is a singleton over the (jid, accountid) tuple. Then The MLMessage should listen for all update notifications and change the values of its properties according to these updates.

The rest of our code in ChatView.swift (etc.?) should make sure that these property updates of the underlying MLMessage are automatically picked up by SwiftUI using our ObservableKVOWrapper etc. (just like in the contact details + MLContact case).

@matthewrfennell My question is now: could you (together with lissine) figure out, what changes/additions etc. need to be done to the ChatViewMessage class (and of course MLMessage) to realize my vision depicted above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional note: to test, you could use a timer (createTimer(1, (^{ ... }));) in MLMessage to automatically add a dot to the end of every message every second. That should automatically trigger an UI update, if everything works out as intended :)

@lissine0

lissine0 commented May 8, 2025

Copy link
Copy Markdown
Member Author

Also, I'm not sure observing the new notification in MLContact is strictly necessary, but it's not harmful.

No, that's not needed, you can remove that one :)

That was my intuition, but for some reason MLContact observes for kMonalDeletedMessageNotice even though message retraction don't cause the unread counter to be updated.
Edit: See d4c21167a8

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Also, I'm not sure observing the new notification in MLContact is strictly necessary, but it's not harmful.

No, that's not needed, you can remove that one :)

That was my intuition, but for some reason MLContact observes for kMonalDeletedMessageNotice even though message retraction don't cause the unread counter to be updated. Edit: See d4c21167a8

Well, a retraction should update the unread counter. I think we should set the unread flag to 1 in DataLayer.m method retractMessageHistory:. Could you add that to this PR and remove the kMonalUpdatedMessageNotice handling from MLContact.m?

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Also, I'm not sure observing the new notification in MLContact is strictly necessary, but it's not harmful.

No, that's not needed, you can remove that one :)

That was my intuition, but for some reason MLContact observes for kMonalDeletedMessageNotice even though message retraction don't cause the unread counter to be updated. Edit: See d4c21167a8

Well, a retraction should update the unread counter. I think we should set the unread flag to 1 in DataLayer.m method retractMessageHistory:. Could you add that to this PR and remove the kMonalUpdatedMessageNotice handling from MLContact.m?

Sorry, my fault, of course I meant unread=0, not unread=1 --> mark the retracted message as read because we also remove the push notification for a retracted message

@tmolitor-stud-tu tmolitor-stud-tu force-pushed the develop branch 8 times, most recently from d79dd24 to 0991475 Compare May 25, 2025 14:48
@tmolitor-stud-tu tmolitor-stud-tu force-pushed the develop branch 7 times, most recently from cbcb0d2 to 4544425 Compare June 8, 2025 04:58
@tmolitor-stud-tu tmolitor-stud-tu force-pushed the develop branch 2 times, most recently from 62cf0df to 1c354de Compare July 2, 2025 02:48
@tmolitor-stud-tu tmolitor-stud-tu force-pushed the develop branch 2 times, most recently from 21b949a to d90c975 Compare July 23, 2025 02:51
@tmolitor-stud-tu tmolitor-stud-tu force-pushed the develop branch 2 times, most recently from d168ab8 to 0f4c75d Compare August 2, 2025 03:07
@lissine0

lissine0 commented Aug 9, 2025

Copy link
Copy Markdown
Member Author

Well, a retraction should update the unread counter. I think we should set the unread flag to 1 in DataLayer.m method retractMessageHistory:. Could you add that to this PR and remove the kMonalUpdatedMessageNotice handling from MLContact.m?

Note that doing this will change the position of the "Unread Messages Below" indicator to under the most recent retracted message.
i.e. if someone sends multiple messages and then retracts one of them, the unread indicator will move down even if I didn't open the chat.

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Note that doing this will change the position of the "Unread Messages Below" indicator to under the most recent retracted message. i.e. if someone sends multiple messages and then retracts one of them, the unread indicator will move down even if I didn't open the chat.

Oh, that's an unintended side effect I didn't think of. That's unfortunate.
I think we should introduce a new "retracted" boolean db field and set this to 1 if a message gets retracted. Then, when counting the unread messages, we should not count retracted ones.

Other than that: is this PR ready to be merged?

@lissine0

Copy link
Copy Markdown
Member Author

I think we should introduce a new "retracted" boolean db field and set this to 1 if a message gets retracted.

We already have a retracted field in the message_history table, as well as a retracted field in MLMessage

Other than that: is this PR ready to be merged?

It's only missing the handling of Status message update notifications (message received, displayed, error etc.) in MLMessage
I didn't include it yet because I want to try to refactor all of the Status update notifications into one notification.
Other than that, the PR is ready

@lissine0

Copy link
Copy Markdown
Member Author

Note that the changes in ChatView.swift depend on changes in the ExyteChat fork: the MessageView and ChatViewModel had to become public

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Note that the changes in ChatView.swift depend on changes in the ExyteChat fork: the MessageView and ChatViewModel had to become public

@matthewrfennell will we be able to get this change upstream?

@lissine0

Copy link
Copy Markdown
Member Author

Note that the changes in ChatView.swift depend on changes in the ExyteChat fork: the MessageView and ChatViewModel had to become public

@matthewrfennell will we be able to get this change upstream?

I don't think so, as I made internal structures public.
But it's a small change, so it won't be hard to keep it when rebasing: monal-im/ExyteChat@a4fa92f
monal-im/ExyteChat@864c1f6

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

We already have a retracted field in the message_history table, as well as a retracted field in MLMessage

Oh great! Then only the calculation of the unread counter has to be updated to take that field into account (or does it already?).
Setting a retracted message to unread=0 should not be necessary anymore, you can remove that :)

It's only missing the handling of Status message update notifications (message received, displayed, error etc.) in MLMessage I didn't include it yet because I want to try to refactor all of the Status update notifications into one notification. Other than that, the PR is ready

Okay, would you like to create a new PR for these status notifications and me to do a full review and merge this PR now? Or do you want me to review and merge this only after the status handling is complete?

@lissine0

Copy link
Copy Markdown
Member Author

We already have a retracted field in the message_history table, as well as a retracted field in MLMessage

Oh great! Then only the calculation of the unread counter has to be updated to take that field into account (or does it already?). Setting a retracted message to unread=0 should not be necessary anymore, you can remove that :)

Unfortunately, the situation is a bit complicated.
The algorithm for the unread counter is as follows: we go through messages from the most recent to older, until we find a message that is read. And we increment a counter each time.
Then the "Unread messages below" is inserted based on the counter.
Now, if we ignore retracted messages (i.e. increment the counter regardless), then it causes a problem when the most recent message is retracted. In that situation, the unread counter will always be above it.

It's only missing the handling of Status message update notifications (message received, displayed, error etc.) in MLMessage I didn't include it yet because I want to try to refactor all of the Status update notifications into one notification. Other than that, the PR is ready

Okay, would you like to create a new PR for these status notifications and me to do a full review and merge this PR now? Or do you want me to review and merge this only after the status handling is complete?

I decided against the refactor. So, implementing the missing handling of those notifications shouldn't take a lot time. Thus I'd like to include it in this PR

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Hmm we'll have to split the counting for the unread badge from the counting for the unread below marker then. Two datalayer methods instead of one.

@tmolitor-stud-tu tmolitor-stud-tu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a full review of all changes now :)

let dbMessages = DataLayer.sharedInstance().messages(forContact:contact.contactJid, forAccount:contact.accountID) as! [MLMessage]
for msg in dbMessages {
messages.append(ChatViewMessage(msg))
messages.append(ChatViewMessage(ObservableKVOWrapper(msg)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe don't wrap it into ObservableKVOWrapper outside, but wrap it inside the constructor of ChatViewMessage. That seems a bit cleaner because it doesn't expose these implementation details to the caller.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing that causes the following error on L502:

message: ChatViewMessage(message),
                         ^^^^^^^ 
Cannot convert value of type 'ObservableKVOWrapper<MLMessage>' to expected argument type 'MLMessage'

If I replace message with message.obj, the automatic view updates won't work anymore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you should change the code to "bubble that change up", aka: only wrap the MLMessage object into the kvo observer once and pass around the kvo observer or some object containing it rather then recreating things over and over again on every render.

@ObservedObject var viewModel: ExyteChat.ChatViewModel
let positionInUserGroup: PositionInUserGroup
let positionInMessagesSection: PositionInMessagesSection
init(message: ObservableKVOWrapper<MLMessage>, viewModel: ChatViewModel, positionInUserGroup: PositionInUserGroup, positionInMessagesSection: PositionInMessagesSection) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you pass in the ChatViewMessage here? You can get the underlying MLMessage from it just fine and you won't have to instantiate a new ChatViewMessage in the view body every time this gets rendered.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this suggestion and the view auto updates stopped working.
If the initializer of ChatViewMessage doesn't run on every view render, how are changes from MLMessage going to be picked up?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are going to be picked up by the ObservableKVOWrapper which is emitting a objectWillChange event if a property of MLMessage (being used by the swiftui view) changes. That event is used by swiftui as a rerender signal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a property of MLMessage (being used by the swiftui view) changes

The MessageView isn't using any MLMessage properties directly.
The MessageView uses ChatViewMessage; and ChatViewMessage gets initialized with MLMessage properties.
Thus the need for the initializer of ChatViewMessage to run in order to get updates from MLMessage

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to write our custom MessageView, and use MLMessage properties in it directly, in order for it to work how you said

@tmolitor-stud-tu tmolitor-stud-tu Aug 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I alreay said in some review that you should overwrite the properties of Message in ChatViewMessage using computed properties returning the values of MLMessage and use the MLMessage as @StateObject. That should work.

See #1426 (comment)

That means something like this (maybe you could try a proof of concept first, before updating this PR to save some work in case it doesn't work like depicted below):

  1. A view using an @StateObject to a class named ChatViewMessage and using its text property in the view body
  2. The ChatViewMessage class being a child of the ExyteChat open class Message: ObservableObject, Identifiable
  3. The ChatViewMessage class having a property innerMessage of type ObservableKVOWrapper<MLMessage>
  4. The ChatViewMessage class having an @Published computed property of type String named text, that always returns the innerMessage.messageText value. This property overwrites the @Published public var text: String property of the base class

Okay, step 4 is a problem here, apparently you cannot overwrite a stored property with a computed one :(
I hate Swift, in ObjC that could easily be done :(

Solution:

  1. Check if the proof of concept above works while not deriving the ChatViewMessage class from anything. (Only check if the computed property properly forwards the change event of the ObservableKVOWrapper wrapped MLMessage object (use some timer to periodically change the text).
  2. If that works, then either:
    1. Change the stored properties in the base class to be computed ones (computed ones can be overwritten).
    2. Update the ExyteChat to define a protocol named MessageProtocol that defines all the properties of the ExyteChat Message class/struct; let the ExyteChat Message derive from that protocol and change all mentions of Message in the ExyteChat codebase to be MessageProtocol ones; then derive our implementation from that protocol, too, instead of deriving it from the Message class.

I think introducing a new protocol is the way to go here, because it makes the original ExyteChat stuff more extensible but doesn't change any behavior. I think we can even upstream that change (together with making Message a class rather than a struct).

btw: the public enum Status doesn't contain a state for received, too, so we'll have to update the base class anyways.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewrfennell what do you think, would that protocol be upstreamable?

Comment thread Monal/Classes/MLMessage.m
_singletonCache = [NSMutableDictionary new];
}

+(MLMessage*) createMessageFromHistoryID:(NSNumber*) historyID

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have MLMessage messageFromDictionary: which is used in various places by the DataLayer and this method here. That's a duplication I don't like. Especially because one of them has a cache and the other one doesn't. In MLContactfor example, the MLContact contactFromDictionary: method is only called internally in MLContact and isn't exposed publicly.

I know you want to preserve all these high level DataLayer methods returning ready made MLMessage objects and that's a good decision. But then the MLMessage createMessageFromHistoryID: method should be removed and the cache added to MLMessage messageFromDictionary: . Every code currently calling into createMessageFromHistoryID should use the appropriate DataLayer method which is messageForHistoryID:.

Even better: move that DataLayer messageForHistoryID: method to MLMessage and name it createMessageFromHistoryID: (that's possible because this DataLayer method doesn't use any database calls but merely forwards the call to DataLayer messagesForHistoryIDs:), then change every call to that DataLayer method to use the new MLMessage method instead.
Note: caching should be still moved to MLMessage messageFromDictionary:.

So to sum it up:

  1. Move the cache from to MLMessage messageFromDictionary:
  2. Replace the code of MLMessage createMessageFromHistoryID: with DataLayer messageForHistoryID:
  3. Remove DataLayer messageForHistoryID: and change all calls to use MLMessage createMessageFromHistoryID: now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you solve this one yet?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! :)

Comment thread Monal/Classes/MLNotificationManager.m Outdated
Comment thread Monal/Classes/chatViewController.m Outdated
[[DataLayer sharedInstance] retractMessageHistory:message.messageDBId];
[message updateWithMessage:[[[DataLayer sharedInstance] messagesForHistoryIDs:@[message.messageDBId]] firstObject]];

//update table entry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the deleted/retracted message still be removed from the messages list? Currently the message is still in there, but its text gets reset to an empty string.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retracted and moderated messages don't get deleted, but rather have their text changed to "This message got retracted".
See https://github.com/monal-im/Monal/blob/198778f9889469a/Monal/Classes/chatViewController.m#L2245
That string is not stored in the DB / MLMessage object, in order for it to be localizeable.

The notification name might be a bit confusing. kMonalDeletedMessageNotice is only related to message retraction and moderation, not local deletion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, on Conversations the moderated/retracted messages are removed from the ui...maybe we should use the opportunity to change Monal's behavior to be the same. What do you think?

@lissine0 lissine0 Aug 17, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversations doesn't support retraction yet.
I think the placeholder for retracted messages should remain. Because retraction is not limited by time, and your contact can retract a message that you already read. And later you can re-read the history and wonder if there a was a message there or you imagined it.

As for moderation, it usually is spam cleaning in public channels. Deletion is fine in this case, since the server removes the messages from the MAM archive anyway (on the other hand, no server currently removes retracted messages from the MAM archive)
Also, spam can come in very large number of messages, which can be annoying if we keep the "this message got retracted" placeholder.
I think Gajim and Cheogram compress all (subsequent?) moderated messages into one, containing the reason of the moderation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, yes, good point.

"revision": "4b8714a7fb84d42393314ce897127b3939885ec3",
"version": "3.8.5"
"revision": "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
"version": "3.9.0"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a good idea without adapting to the new release, from the release notes:

DDFileLogger: File Locking is now optional

We need to explicitly turn locking on. We'd get corrupted rawlog files without locking. That was the reason why I added locking upstream. But since some folks complained over here, the locking was made optional.

Please implement the shouldLockLogFile: method in MLLogFileManager.m always returning YES, see https://github.com/CocoaLumberjack/CocoaLumberjack/pull/1425/files

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already did so in 76ff3612a3279 :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, all right then :)

Comment thread Monal/Monal.xcworkspace/xcshareddata/swiftpm/Package.resolved Outdated
.padding()
.addLoadingOverlay(overlay)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kXMPPError")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kXMPPError)).receive(on: RunLoop.main)) { notification in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are all these new constants defined in? I know they are #define constants in ObjC, but these unfortunately don't automatically leak to Swift afaik, see the top of SwiftHelpers.swift and HelperTools getObjcDefinedValue:.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constants are defined in MLConstants.h, which is part of monalxmpp.
monalxmpp is globally imported in our Swift source files.
And since commit 12c4d23ed52b93, MLConstants.h has public visibilty.
Thus we don't need to manually import constants from objective-C to Swift anymore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that's great! :)
You can remove all these manual imports and the HelperTools method used for this, then :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, already done in 901d54a :)

MLConstants.h is part of monalxmpp, which is globally imported in
our Swift source files.
And as of commit 12c4d23, MLConstants.h has public visibility.
Thus we don't need to manually import Objc constants anymore.
Using constants allows the compiler to catch potential typos.
Use kMonalUpdatedMessageNotice for message corrections and
MUC message reflections, instead of using kMonalNewMessageNotice.
Also, delete MLMessage:updateWithMessage: as it's no longer used
Thanks to ObservableKVOWrapper, and to MLMessage being a model class,
this gives us automatic view updates when an MLMessage object changes,
while still using the default MessageView.

@tmolitor-stud-tu tmolitor-stud-tu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to do the ExyteChat Protocol change in a new PR or in this one?

let dbMessages = DataLayer.sharedInstance().messages(forContact:contact.contactJid, forAccount:contact.accountID) as! [MLMessage]
for msg in dbMessages {
messages.append(ChatViewMessage(msg))
messages.append(ChatViewMessage(ObservableKVOWrapper(msg)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you should change the code to "bubble that change up", aka: only wrap the MLMessage object into the kvo observer once and pass around the kvo observer or some object containing it rather then recreating things over and over again on every render.

@ObservedObject var viewModel: ExyteChat.ChatViewModel
let positionInUserGroup: PositionInUserGroup
let positionInMessagesSection: PositionInMessagesSection
init(message: ObservableKVOWrapper<MLMessage>, viewModel: ChatViewModel, positionInUserGroup: PositionInUserGroup, positionInMessagesSection: PositionInMessagesSection) {

@tmolitor-stud-tu tmolitor-stud-tu Aug 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I alreay said in some review that you should overwrite the properties of Message in ChatViewMessage using computed properties returning the values of MLMessage and use the MLMessage as @StateObject. That should work.

See #1426 (comment)

That means something like this (maybe you could try a proof of concept first, before updating this PR to save some work in case it doesn't work like depicted below):

  1. A view using an @StateObject to a class named ChatViewMessage and using its text property in the view body
  2. The ChatViewMessage class being a child of the ExyteChat open class Message: ObservableObject, Identifiable
  3. The ChatViewMessage class having a property innerMessage of type ObservableKVOWrapper<MLMessage>
  4. The ChatViewMessage class having an @Published computed property of type String named text, that always returns the innerMessage.messageText value. This property overwrites the @Published public var text: String property of the base class

Okay, step 4 is a problem here, apparently you cannot overwrite a stored property with a computed one :(
I hate Swift, in ObjC that could easily be done :(

Solution:

  1. Check if the proof of concept above works while not deriving the ChatViewMessage class from anything. (Only check if the computed property properly forwards the change event of the ObservableKVOWrapper wrapped MLMessage object (use some timer to periodically change the text).
  2. If that works, then either:
    1. Change the stored properties in the base class to be computed ones (computed ones can be overwritten).
    2. Update the ExyteChat to define a protocol named MessageProtocol that defines all the properties of the ExyteChat Message class/struct; let the ExyteChat Message derive from that protocol and change all mentions of Message in the ExyteChat codebase to be MessageProtocol ones; then derive our implementation from that protocol, too, instead of deriving it from the Message class.

I think introducing a new protocol is the way to go here, because it makes the original ExyteChat stuff more extensible but doesn't change any behavior. I think we can even upstream that change (together with making Message a class rather than a struct).

btw: the public enum Status doesn't contain a state for received, too, so we'll have to update the base class anyways.

[[DataLayer sharedInstance] retractMessageHistory:message.messageDBId];
[message updateWithMessage:[[[DataLayer sharedInstance] messagesForHistoryIDs:@[message.messageDBId]] firstObject]];

//update table entry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, yes, good point.

@lissine0

Copy link
Copy Markdown
Member Author

Do you want to do the ExyteChat Protocol change in a new PR or in this one?

Let's keep that in a separate PR

@tmolitor-stud-tu

Copy link
Copy Markdown
Member

Do you want to do the ExyteChat Protocol change in a new PR or in this one?

Let's keep that in a separate PR

Okay, I created an issue for it over here: #1462

@tmolitor-stud-tu tmolitor-stud-tu merged commit 0c72df2 into monal-im:develop Aug 17, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants