Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Monal/Classes/ActiveChatsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ -(void) viewDidLoad
[nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalMessageFiletransferUpdateNotice object:nil];
[nc addObserver:self selector:@selector(refreshContact:) name:kMonalContactRefresh object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalNewMessageNotice object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalUpdatedMessageNotice object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalDeletedMessageNotice object:nil];
[nc addObserver:self selector:@selector(messageSent:) name:kMLMessageSentToContact object:nil];
[nc addObserver:self selector:@selector(handleDeviceRotation) name:UIDeviceOrientationDidChangeNotification object:nil];
Expand Down
4 changes: 2 additions & 2 deletions Monal/Classes/BlockedUsers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct BlockedUsers: View {
showBlockingUnsupportedPlaceholder = blockingUnsupported
reloadBlocksFromDB()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalAccountDiscoDone")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalAccountDiscoDone)).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
Expand All @@ -62,7 +62,7 @@ struct BlockedUsers: View {
reloadBlocksFromDB()
hideLoadingOverlay(overlay)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalBlockListRefresh")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalBlockListRefresh)).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
Expand Down
2 changes: 1 addition & 1 deletion Monal/Classes/ChannelMemberList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ struct ChannelMemberList: View {
.onAppear {
updateParticipantList()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalMucParticipantsAndMembersUpdated)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if contact == channel {
Expand Down
52 changes: 42 additions & 10 deletions Monal/Classes/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ struct ChatView: View {
guard let newMLMessage = MLXMPPManager.sharedInstance().sendMessageAndAddToHistory(message: draft.text, havingType: kMessageTypeText, toContact: self.contact.obj, isEncrypted: self.contact.isEncrypted, uploadInfo: nil) else {
return
}
messages.append(ChatViewMessage(newMLMessage))
messages.append(ChatViewMessage(ObservableKVOWrapper(newMLMessage)))
} messageBuilder: { message, viewModel, positionInUserGroup, positionInMessagesSection, positionInCommentsGroup, showContextMenuClosure, messageActionClosure, showAttachmentClosure in
MessageView(message: (message as! ChatViewMessage).message, viewModel: viewModel, positionInUserGroup: positionInUserGroup, positionInMessagesSection: positionInMessagesSection)
}
.showNetworkConnectionProblem(false)
// .enableLoadMore(pageSize: 3) { message in
Expand Down Expand Up @@ -350,7 +352,7 @@ struct ChatView: View {
if messages.isEmpty {
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.

}
}
ChatViewHelpers.refreshCounter(for: self.contact.obj)
Expand Down Expand Up @@ -386,7 +388,7 @@ struct ChatView: View {
MLNotificationManager.sharedInstance().currentContact = nil
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalOmemoFetchingStateUpdate")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalOmemoFetchingStateUpdate)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let notificationJid = notification.userInfo?["jid"] as? String {
if xmppAccount.accountID == contact.accountID && notificationJid == contact.contactJid {
DDLogDebug("Got omemo fetching update: \(contact) --> \(String(describing:notification.userInfo))")
Expand All @@ -398,7 +400,7 @@ struct ChatView: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalNewMessageNotice")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalNewMessageNotice)).receive(on: RunLoop.main)) { notification in
DDLogVerbose("chat view got new message notice \(String(describing:notification.userInfo))")

guard let message = notification.userInfo?["message"] as? MLMessage else {
Expand All @@ -408,23 +410,24 @@ struct ChatView: View {
if message.isEqual(to: self.contact.obj) {
// Do not insert based on delay timestamp because that
// would make it possible to fake history entries.
messages.append(ChatViewMessage(message))
messages.append(ChatViewMessage(ObservableKVOWrapper(message)))
}
ChatViewHelpers.refreshCounter(for: self.contact.obj)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalRefresh")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalRefresh)).receive(on: RunLoop.main)) { notification in
ChatViewHelpers.refreshCounter(for: self.contact.obj)
}
}
}

class ChatViewMessage: ExyteChat.Message {
@Published public var message: MLMessage
init(_ message: MLMessage) {
let message: ObservableKVOWrapper<MLMessage>

init(_ message: ObservableKVOWrapper<MLMessage>) {
self.message = message
let messageText = message.retracted ? NSLocalizedString("This message got retracted", comment: "") : message.messageText
let user = ExyteChat.User(id: message.senderID, name: message.contactDisplayName, avatarURL: nil, isCurrentUser: !message.inbound)
super.init(id: message.id, user: user, createdAt: message.timestamp, text: message.messageText)
super.init(id: message.id, user: user, createdAt: message.timestamp, text: messageText)
}
}

Expand Down Expand Up @@ -481,3 +484,32 @@ public extension ExyteChat.MessageView {
// }
}
}*/
struct MessageView: View {
@StateObject var message: ObservableKVOWrapper<MLMessage>
@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?

_message = StateObject(wrappedValue: message)
self.viewModel = viewModel
self.positionInUserGroup = positionInUserGroup
self.positionInMessagesSection = positionInMessagesSection
}
var body: some View {
ExyteChat.MessageView(
viewModel: viewModel,
message: ChatViewMessage(message),
positionInUserGroup: positionInUserGroup,
positionInMessagesSection: positionInMessagesSection,
chatType: .conversation,
avatarSize: 32,
tapAvatarClosure: nil,
messageStyler: AttributedString.init,
shouldShowLinkPreview: { _ in true },
isDisplayingMessageMenu: false,
showMessageTimeView: true,
messageLinkPreviewLimit: 8,
font: UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 15))
)
}
}
2 changes: 1 addition & 1 deletion Monal/Classes/ContactDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ struct ContactDetails: View {
.onAppear {
self.updateRoleAndAffiliation()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalMucParticipantsAndMembersUpdated)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if notificationContact == contact {
Expand Down
4 changes: 2 additions & 2 deletions Monal/Classes/ContactRequestsMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ struct ContactRequestsMenu: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalContactRefresh)).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalContactRemoved)).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onAppear {
Expand Down
6 changes: 3 additions & 3 deletions Monal/Classes/ContactResources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ struct ContactResources: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalXmppUserSoftWareVersionRefresh")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalXmppUserSoftWareVersionRefresh)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let softwareInfo = notification.userInfo?["versionInfo"] as? MLContactSoftwareVersionInfo {
DDLogVerbose("Got software version info from account \(xmppAccount)...")
if softwareInfo.fromJid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
Expand All @@ -99,7 +99,7 @@ struct ContactResources: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalNewPresenceNotice")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalNewPresenceNotice)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, let available = notification.userInfo?["available"] as? NSNumber {
DDLogVerbose("Got presence update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
Expand All @@ -118,7 +118,7 @@ struct ContactResources: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalLastInteractionUpdatedNotice")).receive(on: RunLoop.main)) { notification in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(kMonalLastInteractionUpdatedNotice)).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, notification.userInfo?["lastInteraction"] as? NSDate != nil {
DDLogVerbose("Got lastInteraction update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
Expand Down
4 changes: 2 additions & 2 deletions Monal/Classes/ContactsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,10 @@ class Contacts: ObservableObject {
self.contacts = Set(DataLayer.sharedInstance().contactList())
self.requestCount = DataLayer.sharedInstance().allContactRequests().count
subscriptions = [
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh"))
NotificationCenter.default.publisher(for: NSNotification.Name(kMonalContactRefresh))
.receive(on: DispatchQueue.main)
.sink() { _ in self.refreshContacts() },
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved"))
NotificationCenter.default.publisher(for: NSNotification.Name(kMonalContactRemoved))
.receive(on: DispatchQueue.main)
.sink() { _ in self.refreshContacts() },
]
Expand Down
1 change: 0 additions & 1 deletion Monal/Classes/DataLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ extern NSString* const kMessageTypeFiletransfer;
returns messages with the provided local id number
*/
-(NSArray<MLMessage*>*) messagesForHistoryIDs:(NSArray<NSNumber*>*) historyIDs;
-(MLMessage* _Nullable) messageForHistoryID:(NSNumber* _Nullable) historyID;
-(NSNumber*) getSmallestHistoryId;
-(NSNumber*) getBiggestHistoryId;

Expand Down
Loading
Loading