Skip to content

Commit 5a1b9be

Browse files
CopilotIvansss
andcommitted
feat: Complete Chat relay implementation
- Apply all existing PR #2404 changes to NCExternalSignalingController.swift: * Add hasChatRelay property * Advertise chat-relay feature in hello messages * Set hasChatRelay from server features * Post extSignalingDidReceiveChatMessage notification * Post extSignalingDidDisconnect notification on reconnect - Apply all existing PR #2404 changes to NCChatController.m: * Add chatRelayMessagesBuffer/Queue/externalSignalingController properties * Add commonInitForRoom: to share init logic * Register for chat relay notifications (scoped to specific controller instance) * Switch from long-polling to chat relay mode on 304 response * Add startProcessingChatRelayMessagesFromMessagesId: * Add flushChatRelayMessagesBuffer * Add handleChatRelayMessage: with gap sanity check - Add new: fetch messages when websocket disconnects * NCChatController observes extSignalingDidDisconnect * On disconnect: stop relay, clear buffer, resume long-polling * Polling catches up and auto-switches back to relay on 304 - Add new: sanity checks for flaky connections * In handleChatRelayMessage: detect gap (messageId > lastKnownId + 1) * On gap: fall back to long-polling which fills the gap * Guard against false positives when lastNewestMessageId == 0 - Code quality: * Thread-safe stopReceivingNewChatMessages via serial queue dispatch * Renamed shouldStartLongPolling to hasReachedLatest for clarity Co-authored-by: Ivansss <4638605+Ivansss@users.noreply.github.com> Agent-Logs-Url: https://github.com/nextcloud/talk-ios/sessions/b29edced-1384-4943-bc83-e345a364ccae
1 parent 7320047 commit 5a1b9be

2 files changed

Lines changed: 149 additions & 8 deletions

File tree

NextcloudTalk/Chat/NCChatController.m

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@
2929

3030
@interface NCChatController ()
3131

32+
@property (nonatomic, assign) BOOL startProcessingChatRelayMessages;
3233
@property (nonatomic, assign) BOOL stopChatMessagesPoll;
3334
@property (nonatomic, strong) TalkAccount *account;
3435
@property (nonatomic, strong) NSURLSessionTask *getHistoryTask;
3536
@property (nonatomic, strong) NSURLSessionTask *pullMessagesTask;
37+
@property (nonatomic, strong) NSMutableArray *chatRelayMessagesBuffer;
38+
@property (nonatomic, strong) dispatch_queue_t chatRelayMessagesQueue;
39+
@property (nonatomic, strong) NCExternalSignalingController *externalSignalingController;
3640

3741
@end
3842

@@ -42,9 +46,7 @@ - (instancetype)initForRoom:(NCRoom *)room
4246
{
4347
self = [super init];
4448
if (self) {
45-
_room = room;
46-
_account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:_room.accountId];
47-
49+
[self commonInitForRoom:room];
4850
[[AllocationTracker shared] addAllocation:@"NCChatController"];
4951
}
5052

@@ -55,19 +57,32 @@ - (instancetype)initForThreadId:(NSInteger)threadId inRoom:(NCRoom *)room
5557
{
5658
self = [super init];
5759
if (self) {
58-
_room = room;
5960
_threadId = threadId;
60-
_account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:_room.accountId];
61-
61+
[self commonInitForRoom:room];
6262
[[AllocationTracker shared] addAllocation:@"NCChatController"];
6363
}
6464

6565
return self;
6666
}
6767

68+
- (void)commonInitForRoom:(NCRoom *)room
69+
{
70+
_room = room;
71+
_account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:_room.accountId];
72+
73+
_externalSignalingController = [[NCSettingsController sharedInstance] externalSignalingControllerForAccountId:_account.accountId];
74+
if (_externalSignalingController.hasChatRelay) {
75+
_chatRelayMessagesQueue = dispatch_queue_create("chat.relay.message.queue", DISPATCH_QUEUE_SERIAL);
76+
_chatRelayMessagesBuffer = [NSMutableArray new];
77+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveChatMessageFromExternalSignaling:) name:NSNotification.ExtSignalingDidReceiveChatMessage object:_externalSignalingController];
78+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didDisconnectFromExternalSignaling:) name:NSNotification.ExtSignalingDidDisconnect object:_externalSignalingController];
79+
}
80+
}
81+
6882
- (void)dealloc
6983
{
7084
[[AllocationTracker shared] removeAllocation:@"NCChatController"];
85+
[[NSNotificationCenter defaultCenter] removeObserver:self];
7186
}
7287

7388
- (BOOL)isThreadController
@@ -404,6 +419,45 @@ - (NSArray *)sortedMessagesFromMessageArray:(NSArray *)messages
404419
return sortedMessages;
405420
}
406421

422+
#pragma mark - External Signaling Controller
423+
424+
- (void)didReceiveChatMessageFromExternalSignaling:(NSNotification *)notification
425+
{
426+
NSString *roomToken = [notification.userInfo objectForKey:@"roomid"];
427+
NSDictionary *messageDict = [[[notification.userInfo objectForKey:@"data"] objectForKey:@"chat"] objectForKey:@"comment"];
428+
if ([roomToken isEqualToString:_room.token]) {
429+
dispatch_async(_chatRelayMessagesQueue, ^{
430+
if (!self->_startProcessingChatRelayMessages) {
431+
[self->_chatRelayMessagesBuffer addObject:messageDict];
432+
return;
433+
}
434+
435+
[self handleChatRelayMessage:messageDict];
436+
});
437+
}
438+
}
439+
440+
- (void)didDisconnectFromExternalSignaling:(NSNotification *)notification
441+
{
442+
// If the signaling connection was lost while we were using chat relay,
443+
// stop relay processing and fall back to long-polling to catch up on missed messages.
444+
dispatch_async(_chatRelayMessagesQueue, ^{
445+
if (!self->_startProcessingChatRelayMessages) {
446+
return;
447+
}
448+
449+
self->_startProcessingChatRelayMessages = NO;
450+
[self->_chatRelayMessagesBuffer removeAllObjects];
451+
452+
// Resume long-polling to fetch any messages missed during the disconnect.
453+
// Once polling reaches 304 (no new messages), it will switch back to chat relay.
454+
dispatch_async(dispatch_get_main_queue(), ^{
455+
NCChatBlock *lastChatBlock = [self chatBlocksForRoomOrThread].lastObject;
456+
[self startReceivingChatMessagesFromMessagesId:lastChatBlock.newestMessageId withTimeout:NO];
457+
});
458+
});
459+
}
460+
407461
#pragma mark - Chat
408462

409463
- (NSArray<NCChatMessage *> * _Nonnull)getTemporaryMessages
@@ -901,12 +955,74 @@ - (void)startReceivingChatMessagesFromMessagesId:(NSInteger)messageId withTimeou
901955
[self checkLastCommonReadMessage:lastCommonReadMessage];
902956

903957
if (error.code != -999) {
958+
BOOL hasReachedLatest = statusCode == 304;
904959
NCChatBlock *lastChatBlock = [self chatBlocksForRoomOrThread].lastObject;
905-
[self startReceivingChatMessagesFromMessagesId:lastChatBlock.newestMessageId withTimeout:YES];
960+
961+
if (hasReachedLatest && self->_externalSignalingController.hasChatRelay) {
962+
[NCLog log:@"Starting chat relay"];
963+
[self startProcessingChatRelayMessagesFromMessagesId:lastChatBlock.newestMessageId];
964+
return;
965+
}
966+
967+
[self startReceivingChatMessagesFromMessagesId:lastChatBlock.newestMessageId withTimeout:hasReachedLatest];
906968
}
907969
}];
908970
}
909971

972+
- (void)startProcessingChatRelayMessagesFromMessagesId:(NSInteger)messageId
973+
{
974+
dispatch_async(self.chatRelayMessagesQueue, ^{
975+
self.startProcessingChatRelayMessages = YES;
976+
[self flushChatRelayMessagesBuffer];
977+
});
978+
}
979+
980+
- (void)flushChatRelayMessagesBuffer
981+
{
982+
if (self.chatRelayMessagesBuffer.count == 0) {
983+
[NCLog log:@"No messages stored in chat relay messages buffer"];
984+
return;
985+
}
986+
987+
[self.chatRelayMessagesBuffer sortUsingDescriptors:@[
988+
[NSSortDescriptor sortDescriptorWithKey:@"id" ascending:YES]
989+
]];
990+
991+
for (NSDictionary *messageDict in self.chatRelayMessagesBuffer) {
992+
[self handleChatRelayMessage:messageDict];
993+
}
994+
995+
[self.chatRelayMessagesBuffer removeAllObjects];
996+
}
997+
998+
- (void)handleChatRelayMessage:(NSDictionary *)messageDict
999+
{
1000+
NCChatBlock *lastChatBlock = [self chatBlocksForRoomOrThread].lastObject;
1001+
NSInteger lastNewestMessageId = lastChatBlock.newestMessageId;
1002+
NCChatMessage *message = [NCChatMessage messageWithDictionary:messageDict andAccountId:self->_account.accountId];
1003+
if (message) {
1004+
// Sanity check: if there's a gap between the last known message and the incoming
1005+
// message, some messages may have been missed on a flaky connection. Stop chat relay
1006+
// and fall back to long-polling, which will fetch missing messages from the server.
1007+
// Once polling catches up (304), it will switch back to chat relay automatically.
1008+
// Only check for gaps when we have a known last message (lastNewestMessageId > 0).
1009+
if (lastNewestMessageId > 0 && message.messageId > lastNewestMessageId + 1) {
1010+
[NCLog log:[NSString stringWithFormat:@"Chat relay gap detected: last=%ld, received=%ld – falling back to long-polling to fetch missing messages", (long)lastNewestMessageId, (long)message.messageId]];
1011+
self->_startProcessingChatRelayMessages = NO;
1012+
[self->_chatRelayMessagesBuffer removeAllObjects];
1013+
dispatch_async(dispatch_get_main_queue(), ^{
1014+
[self startReceivingChatMessagesFromMessagesId:lastNewestMessageId withTimeout:NO];
1015+
});
1016+
return;
1017+
}
1018+
1019+
[self updateLastChatBlockWithNewestKnown:message.messageId];
1020+
[self storeMessages:@[messageDict]];
1021+
1022+
[self checkForNewMessagesFromMessageId:lastNewestMessageId];
1023+
}
1024+
}
1025+
9101026
- (void)startReceivingNewChatMessages
9111027
{
9121028
NCChatBlock *lastChatBlock = [self chatBlocksForRoomOrThread].lastObject;
@@ -915,6 +1031,12 @@ - (void)startReceivingNewChatMessages
9151031

9161032
- (void)stopReceivingNewChatMessages
9171033
{
1034+
if (_chatRelayMessagesQueue) {
1035+
dispatch_async(_chatRelayMessagesQueue, ^{
1036+
self->_startProcessingChatRelayMessages = NO;
1037+
[self->_chatRelayMessagesBuffer removeAllObjects];
1038+
});
1039+
}
9181040
_stopChatMessagesPoll = YES;
9191041
[_pullMessagesTask cancel];
9201042
}

NextcloudTalk/WebRTC/NCExternalSignalingController.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ import Foundation
1313
@objc func externalSignalingController(_ externalSignalingController: NCExternalSignalingController, shouldSwitchToCall roomToken: String)
1414
}
1515

16+
@objc extension NSNotification {
17+
public static let ExtSignalingDidReceiveChatMessage = Notification.Name.extSignalingDidReceiveChatMessage
18+
public static let ExtSignalingDidDisconnect = Notification.Name.extSignalingDidDisconnect
19+
}
20+
1621
extension Notification.Name {
1722
static let extSignalingDidUpdateParticipants = Notification.Name(rawValue: "NCExternalSignalingControllerDidUpdateParticipantsNotification")
1823
static let extSignalingDidReceiveJoinOfParticipant = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveJoinOfParticipantNotification")
1924
static let extSignalingDidReceiveLeaveOfParticipant = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveLeaveOfParticipantNotification")
2025
static let extSignalingDidReceiveStartedTyping = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveStartedTypingNotification")
2126
static let extSignalingDidReceiveStoppedTyping = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveStoppedTypingNotification")
27+
static let extSignalingDidReceiveChatMessage = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveChatMessageNotification")
28+
static let extSignalingDidDisconnect = Notification.Name(rawValue: "NCExternalSignalingControllerDidDisconnectNotification")
2229
}
2330

2431
public typealias SendMessageCompletionBlock = (_ task: URLSessionWebSocketTask?, _ status: NCExternalSignalingSendMessageStatus) -> Void
@@ -38,6 +45,7 @@ public enum NCExternalSignalingSendMessageStatus {
3845
public private(set) var account: TalkAccount
3946
public private(set) var disconnected: Bool = true
4047
public private(set) var hasMCU: Bool = false
48+
public private(set) var hasChatRelay: Bool = false
4149
public private(set) var sessionId: String?
4250
public private(set) var participantsMap = [String: SignalingParticipant]()
4351

@@ -155,6 +163,9 @@ public enum NCExternalSignalingSendMessageStatus {
155163
message.executeCompletionBlock(withStatus: .socketError)
156164
}
157165

166+
// Notify chat controllers that the signaling connection was lost
167+
NotificationCenter.default.post(name: .extSignalingDidDisconnect, object: self)
168+
158169
self.setReconnectionTimer()
159170
}
160171

@@ -280,6 +291,9 @@ public enum NCExternalSignalingSendMessageStatus {
280291
"userid": account.userId,
281292
"ticket": ticket
282293
]
294+
],
295+
"features": [
296+
"chat-relay"
283297
]
284298
]
285299
]
@@ -289,7 +303,10 @@ public enum NCExternalSignalingSendMessageStatus {
289303
"type": "hello",
290304
"hello": [
291305
"version": "1.0",
292-
"resumeid": resumeId
306+
"resumeid": resumeId,
307+
"features": [
308+
"chat-relay"
309+
]
293310
]
294311
]
295312
}
@@ -333,6 +350,7 @@ public enum NCExternalSignalingSendMessageStatus {
333350
}
334351

335352
self.hasMCU = serverFeatures.contains(where: { $0 == "mcu" })
353+
self.hasChatRelay = serverFeatures.contains(where: { $0 == "chat-relay" })
336354

337355
DispatchQueue.main.async {
338356
let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCUpdateSignalingVersionTransaction")
@@ -616,6 +634,7 @@ public enum NCExternalSignalingSendMessageStatus {
616634

617635
if messageType == "chat" {
618636
print("Chat message received")
637+
NotificationCenter.default.post(name: .extSignalingDidReceiveChatMessage, object: self, userInfo: messageDict)
619638
} else if messageType == "recording" {
620639
self.delegate?.externalSignalingController(self, didReceivedSignalingMessage: messageDict)
621640
} else {

0 commit comments

Comments
 (0)