diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 042d7c815..e9ace355c 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -36,6 +36,11 @@ 1F61C767285E35A6004D74D8 /* DiagnosticsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F61C766285E35A6004D74D8 /* DiagnosticsTableViewController.swift */; }; 1F61C76B285F65E1004D74D8 /* SimpleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */; }; 1F628CBA2842BAAF0083A425 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = 1F628CB92842BAAF0083A425 /* QRCodeReader */; }; + 1F66B71F29FA703B003FB168 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */; }; + 1F66B72129FA7089003FB168 /* TypingIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */; }; + 1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */; }; + 1F66B72C29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */; }; + 1F66B72F29FABD01003FB168 /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1F66B72E29FABD01003FB168 /* SwiftyAttributes */; }; 1F7625E52901B0DB00834869 /* CallsFromOldAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7625E42901B0DB00834869 /* CallsFromOldAccountViewController.swift */; }; 1F7625E72901B0E800834869 /* CallsFromOldAccountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F7625E62901B0E800834869 /* CallsFromOldAccountViewController.xib */; }; 1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */; }; @@ -265,7 +270,6 @@ 2CB3041C2264775E0053078A /* SLKTextView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A32264775E0053078A /* SLKTextView+SLKAdditions.m */; }; 2CB3041D2264775E0053078A /* SLKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A52264775E0053078A /* SLKTextView.m */; }; 2CB3041E2264775E0053078A /* SLKTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303A72264775E0053078A /* SLKTextViewController.m */; }; - 2CB3041F2264775E0053078A /* SLKTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AA2264775E0053078A /* SLKTypingIndicatorView.m */; }; 2CB304202264775E0053078A /* UIResponder+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */; }; 2CB304212264775E0053078A /* UIScrollView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */; }; 2CB304222264775E0053078A /* UIView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB303B12264775E0053078A /* UIView+SLKAdditions.m */; }; @@ -414,6 +418,12 @@ 1F5CDF632584E78900B0026E /* NCChatFileStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatFileStatus.m; sourceTree = ""; }; 1F61C766285E35A6004D74D8 /* DiagnosticsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTableViewController.swift; sourceTree = ""; }; 1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTableViewController.swift; sourceTree = ""; }; + 1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; + 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TypingIndicatorView.xib; sourceTree = ""; }; + 1F66B72729FA936E003FB168 /* SLKDefaultReplyView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SLKDefaultReplyView.h; sourceTree = ""; }; + 1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SLKDefaultReplyView.m; sourceTree = ""; }; + 1F66B72A29FA9414003FB168 /* SLKDefaultTypingIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SLKDefaultTypingIndicatorView.h; sourceTree = ""; }; + 1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SLKDefaultTypingIndicatorView.m; sourceTree = ""; }; 1F7625E42901B0DB00834869 /* CallsFromOldAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallsFromOldAccountViewController.swift; sourceTree = ""; }; 1F7625E62901B0E800834869 /* CallsFromOldAccountViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallsFromOldAccountViewController.xib; sourceTree = ""; }; 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = ""; }; @@ -719,9 +729,7 @@ 2CB303A52264775E0053078A /* SLKTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextView.m; sourceTree = ""; }; 2CB303A62264775E0053078A /* SLKTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextViewController.h; sourceTree = ""; }; 2CB303A72264775E0053078A /* SLKTextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextViewController.m; sourceTree = ""; }; - 2CB303A82264775E0053078A /* SLKTypingIndicatorProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorProtocol.h; sourceTree = ""; }; - 2CB303A92264775E0053078A /* SLKTypingIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorView.h; sourceTree = ""; }; - 2CB303AA2264775E0053078A /* SLKTypingIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTypingIndicatorView.m; sourceTree = ""; }; + 2CB303A82264775E0053078A /* SLKVisibleViewProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKVisibleViewProtocol.h; sourceTree = ""; }; 2CB303AB2264775E0053078A /* SLKUIConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKUIConstants.h; sourceTree = ""; }; 2CB303AC2264775E0053078A /* UIResponder+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIResponder+SLKAdditions.h"; sourceTree = ""; }; 2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+SLKAdditions.m"; sourceTree = ""; }; @@ -840,6 +848,7 @@ 1F468E7628DCC6C60099597B /* Dynamic in Frameworks */, 1FDCC3D729EC326400DEB39B /* SDWebImage in Frameworks */, 2C38D4AC27BBAFCC00BAE015 /* WebRTC.xcframework in Frameworks */, + 1F66B72F29FABD01003FB168 /* SwiftyAttributes in Frameworks */, 1F7AE07829142CA1009F72AD /* NextcloudKit in Frameworks */, 9993261EDAC77481FF4EF58A /* libPods-NextcloudTalk.a in Frameworks */, ); @@ -1325,6 +1334,8 @@ 2CA52AC92670D02800619610 /* VoiceMessageRecordingView.h */, 2CA52ACA2670D02800619610 /* VoiceMessageRecordingView.m */, 2CA52ACC2670D07900619610 /* VoiceMessageRecordingView.xib */, + 1F66B71E29FA703B003FB168 /* TypingIndicatorView.swift */, + 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */, ); name = "Chat views"; sourceTree = ""; @@ -1417,9 +1428,7 @@ 2CB303A52264775E0053078A /* SLKTextView.m */, 2CB303A62264775E0053078A /* SLKTextViewController.h */, 2CB303A72264775E0053078A /* SLKTextViewController.m */, - 2CB303A82264775E0053078A /* SLKTypingIndicatorProtocol.h */, - 2CB303A92264775E0053078A /* SLKTypingIndicatorView.h */, - 2CB303AA2264775E0053078A /* SLKTypingIndicatorView.m */, + 2CB303A82264775E0053078A /* SLKVisibleViewProtocol.h */, 2CB303AB2264775E0053078A /* SLKUIConstants.h */, 2CB303AC2264775E0053078A /* UIResponder+SLKAdditions.h */, 2CB303AD2264775E0053078A /* UIResponder+SLKAdditions.m */, @@ -1427,6 +1436,10 @@ 2CB303AF2264775E0053078A /* UIScrollView+SLKAdditions.m */, 2CB303B02264775E0053078A /* UIView+SLKAdditions.h */, 2CB303B12264775E0053078A /* UIView+SLKAdditions.m */, + 1F66B72729FA936E003FB168 /* SLKDefaultReplyView.h */, + 1F66B72829FA936E003FB168 /* SLKDefaultReplyView.m */, + 1F66B72A29FA9414003FB168 /* SLKDefaultTypingIndicatorView.h */, + 1F66B72B29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m */, ); path = Source; sourceTree = ""; @@ -1559,6 +1572,7 @@ 1F7AE07729142CA1009F72AD /* NextcloudKit */, 1FDCC3D629EC326400DEB39B /* SDWebImage */, 1FDCC3D929EC367700DEB39B /* SDWebImageSVGCoder */, + 1F66B72E29FABD01003FB168 /* SwiftyAttributes */, ); productName = NextcloudTalk; productReference = 2C05747D1EDD9E8E00D9E7F2 /* NextcloudTalk.app */; @@ -1687,6 +1701,7 @@ 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */, 1FDCC3D529EC326400DEB39B /* XCRemoteSwiftPackageReference "SDWebImage" */, 1FDCC3D829EC367700DEB39B /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */, + 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */, ); productRefGroup = 2C05747E1EDD9E8E00D9E7F2 /* Products */; projectDirPath = ""; @@ -1715,6 +1730,7 @@ 2C0574A51EDDA2E300D9E7F2 /* LoginViewController.xib in Resources */, 1F46CE2B28E05B3C00E7D88E /* ReferenceDefaultView.xib in Resources */, 1F98DF9E28E7485000E05174 /* ReferenceDeckView.xib in Resources */, + 1F66B72129FA7089003FB168 /* TypingIndicatorView.xib in Resources */, 2C738158210613A200CDB8DB /* NCChatTitleView.xib in Resources */, 2CEDA88A26F10BB20044552B /* UserStatusMessageViewController.xib in Resources */, 2CA52ACD2670D07900619610 /* VoiceMessageRecordingView.xib in Resources */, @@ -2035,6 +2051,7 @@ 2C78E9E325120DE600E3D4CA /* NCUserStatus.m in Sources */, 2C0574A41EDDA2E300D9E7F2 /* LoginViewController.m in Sources */, 2C78EFA51F86FF4A008AFA74 /* CallParticipantViewCell.m in Sources */, + 1F66B72C29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m in Sources */, 1F46CE2928E05B3200E7D88E /* ReferenceDefaultView.swift in Sources */, 2C444706265E59B100DF1DBC /* ShareConfirmationCollectionViewCell.m in Sources */, 2C78EF991F80F81E008AFA74 /* NCSignalingController.m in Sources */, @@ -2074,7 +2091,6 @@ DA75580F278EEA1000A48A1B /* SettingsTableViewController.swift in Sources */, 2CB6ACD22640814100D3D641 /* LocationMessageTableViewCell.m in Sources */, 2CB3041D2264775E0053078A /* SLKTextView.m in Sources */, - 2CB3041F2264775E0053078A /* SLKTypingIndicatorView.m in Sources */, 2CB304212264775E0053078A /* UIScrollView+SLKAdditions.m in Sources */, 2C7A245B24FE7B5300921A21 /* ShareConfirmationViewController.m in Sources */, 2C4446D32658147900DF1DBC /* TalkAccount.m in Sources */, @@ -2091,9 +2107,11 @@ 2C8A2BC9221F094F00DE6D2C /* DirectoryTableViewController.m in Sources */, 1F61C76B285F65E1004D74D8 /* SimpleTableViewController.swift in Sources */, 2C5BFBF2288A97D800E75118 /* NCPoll.m in Sources */, + 1F66B71F29FA703B003FB168 /* TypingIndicatorView.swift in Sources */, 2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */, 2C4CDCD026A84AEA0023F403 /* ShareViewController.m in Sources */, 1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */, + 1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */, 1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */, 2C4446EC265D25BA00DF1DBC /* NCKeyChainController.m in Sources */, DA8801A427AC52AC009EF248 /* TextInputTableViewCell.swift in Sources */, @@ -2903,6 +2921,14 @@ minimumVersion = 10.1.1; }; }; + 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/eddiekaiger/SwiftyAttributes.git"; + requirement = { + kind = revision; + revision = 1ae513a1617309455a115c3fc2d558f744b43788; + }; + }; 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; @@ -2948,6 +2974,11 @@ package = 1F628CB82842BAAF0083A425 /* XCRemoteSwiftPackageReference "QRCodeReader" */; productName = QRCodeReader; }; + 1F66B72E29FABD01003FB168 /* SwiftyAttributes */ = { + isa = XCSwiftPackageProductDependency; + package = 1F66B72D29FABD01003FB168 /* XCRemoteSwiftPackageReference "SwiftyAttributes" */; + productName = SwiftyAttributes; + }; 1F7AE07729142CA1009F72AD /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; package = 1F7AE07629142CA1009F72AD /* XCRemoteSwiftPackageReference "NextcloudKit" */; diff --git a/NextcloudTalk/NCChatViewController.m b/NextcloudTalk/NCChatViewController.m index 4e645a883..ccec607a7 100644 --- a/NextcloudTalk/NCChatViewController.m +++ b/NextcloudTalk/NCChatViewController.m @@ -117,7 +117,8 @@ @interface NCChatViewController () + VLCKitVideoViewControllerDelegate, + UITextViewDelegate> @property (nonatomic, strong) NCChatTitleView *titleView; @property (nonatomic, strong) PlaceholderView *chatBackgroundView; @@ -165,6 +166,8 @@ @interface NCChatViewController () *)results @@ -2833,6 +2972,54 @@ - (void)didUpdateParticipants:(NSNotification *)notification } } +- (void)didReceiveStartedTyping:(NSNotification *)notification +{ + NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"]; + NSString *displayName = [notification.userInfo objectForKey:@"displayName"]; + NSString *userId = [notification.userInfo objectForKey:@"userId"]; + + if (![roomToken isEqualToString:_room.token] || !displayName || !userId) { + return; + } + + // Don't show a typing indicator for ourselves + TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:_room.accountId]; + if ([userId isEqualToString:activeAccount.userId]) { + return; + } + + [self addTypingIndicatorWithUserId:userId withDisplayName:displayName]; +} + +- (void)didReceiveStoppedTyping:(NSNotification *)notification +{ + NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"]; + NSString *userId = [notification.userInfo objectForKey:@"userId"]; + + if (![roomToken isEqualToString:_room.token] || !userId) { + return; + } + + [self removeTypingIndicatorWithUserId:userId]; +} + +- (void)didReceiveParticipantJoin:(NSNotification *)notification +{ + NSString *roomToken = [notification.userInfo objectForKey:@"roomToken"]; + NSString *sessionId = [notification.userInfo objectForKey:@"sessionId"]; + + if (![roomToken isEqualToString:_room.token] || !sessionId) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_isTyping) { + [self sendStartedTypingMessageToSessionId:sessionId]; + } + }); +} + + #pragma mark - Lobby functions - (void)startObservingRoomLobbyFlag diff --git a/NextcloudTalk/NCDatabaseManager.h b/NextcloudTalk/NCDatabaseManager.h index 0901b9a1e..e62e654a0 100644 --- a/NextcloudTalk/NCDatabaseManager.h +++ b/NextcloudTalk/NCDatabaseManager.h @@ -74,6 +74,7 @@ extern NSString * const kCapabilityRecordingV1; extern NSString * const kCapabilitySingleConvStatus; extern NSString * const kCapabilityChatKeepNotifications; extern NSString * const kCapabilityConversationAvatars; +extern NSString * const kCapabilityTypingIndicators; extern NSString * const kMinimumRequiredTalkCapability; diff --git a/NextcloudTalk/NCDatabaseManager.m b/NextcloudTalk/NCDatabaseManager.m index 6d7bf0de8..a085853e6 100644 --- a/NextcloudTalk/NCDatabaseManager.m +++ b/NextcloudTalk/NCDatabaseManager.m @@ -78,6 +78,7 @@ NSString * const kCapabilitySingleConvStatus = @"single-conversation-status"; NSString * const kCapabilityChatKeepNotifications = @"chat-keep-notifications"; NSString * const kCapabilityConversationAvatars = @"avatar"; +NSString * const kCapabilityTypingIndicators = @"typing-indicators"; NSString * const kMinimumRequiredTalkCapability = kCapabilitySystemMessages; // Talk 4.0 is the minimum required version diff --git a/NextcloudTalk/NCExternalSignalingController.h b/NextcloudTalk/NCExternalSignalingController.h index 197c381b9..ad418df19 100644 --- a/NextcloudTalk/NCExternalSignalingController.h +++ b/NextcloudTalk/NCExternalSignalingController.h @@ -28,6 +28,9 @@ @class TalkAccount; extern NSString * const NCExternalSignalingControllerDidUpdateParticipantsNotification; +extern NSString * const NCExternalSignalingControllerDidReceiveJoinOfParticipant; +extern NSString * const NCExternalSignalingControllerDidReceiveStartedTypingNotification; +extern NSString * const NCExternalSignalingControllerDidReceiveStoppedTypingNotification; typedef enum NCExternalSignalingSendMessageStatus { SendMessageSuccess = 0, @@ -65,6 +68,7 @@ typedef void (^JoinRoomExternalSignalingCompletionBlock)(NSError *error); - (void)requestOfferForSessionId:(NSString *)sessionId andRoomType:(NSString *)roomType; - (NSString *)getUserIdFromSessionId:(NSString *)sessionId; - (NSString *)getDisplayNameFromSessionId:(NSString *)sessionId; +- (NSMutableDictionary *)getParticipantMap; - (void)connect; - (void)forceConnect; - (void)disconnect; diff --git a/NextcloudTalk/NCExternalSignalingController.m b/NextcloudTalk/NCExternalSignalingController.m index 0db6e707a..d4b944808 100644 --- a/NextcloudTalk/NCExternalSignalingController.m +++ b/NextcloudTalk/NCExternalSignalingController.m @@ -34,7 +34,10 @@ static NSTimeInterval kMaxReconnectInterval = 16; static NSTimeInterval kWebSocketTimeoutInterval = 15; -NSString * const NCExternalSignalingControllerDidUpdateParticipantsNotification = @"NCExternalSignalingControllerDidUpdateParticipantsNotification"; +NSString * const NCExternalSignalingControllerDidUpdateParticipantsNotification = @"NCExternalSignalingControllerDidUpdateParticipantsNotification"; +NSString * const NCExternalSignalingControllerDidReceiveJoinOfParticipant = @"NCExternalSignalingControllerDidReceiveJoinOfParticipant"; +NSString * const NCExternalSignalingControllerDidReceiveStartedTypingNotification = @"NCExternalSignalingControllerDidReceiveStartedTypingNotification"; +NSString * const NCExternalSignalingControllerDidReceiveStoppedTypingNotification = @"NCExternalSignalingControllerDidReceiveStoppedTypingNotification"; @interface NCExternalSignalingController () @@ -477,15 +480,31 @@ - (void)processRoomEvent:(NSDictionary *)eventDict NSArray *joins = [eventDict objectForKey:@"join"]; for (NSDictionary *participant in joins) { NSString *participantId = [participant objectForKey:@"userid"]; + if (!participantId || [participantId isEqualToString:@""]) { NSLog(@"Guest joined room."); } else { + NSString *sessionId = [participant objectForKey:@"sessionid"]; + if ([participantId isEqualToString:_userId]) { NSLog(@"App user joined room."); } else { NSLog(@"Participant joined room."); + + // Only notify if another participant joined the room and not ourselves from a different device + NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; + + if (_currentRoom && sessionId){ + [userInfo setObject:_currentRoom forKey:@"roomToken"]; + [userInfo setObject:sessionId forKey:@"sessionId"]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveJoinOfParticipant + object:self + userInfo:userInfo]; } - [_participantsMap setObject:participant forKey:[participant objectForKey:@"sessionid"]]; + + [_participantsMap setObject:participant forKey:sessionId]; } } } else if ([eventType isEqualToString:@"leave"]) { @@ -553,8 +572,36 @@ - (void)processRoomParticipantsEvent:(NSDictionary *)eventDict - (void)messageReceived:(NSDictionary *)messageDict { - //NSLog(@"Message received"); - [self.delegate externalSignalingController:self didReceivedSignalingMessage:messageDict]; + NSString *messageType = [[messageDict objectForKey:@"data"] objectForKey:@"type"]; + if ([messageType isEqualToString:@"startedTyping"] || [messageType isEqualToString:@"stoppedTyping"]) { + NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; + NSString *fromSession = [[messageDict objectForKey:@"sender"] objectForKey:@"sessionid"]; + NSString *fromUser = [[messageDict objectForKey:@"sender"] objectForKey:@"userid"]; + + if (_currentRoom && fromSession && fromUser){ + [userInfo setObject:_currentRoom forKey:@"roomToken"]; + [userInfo setObject:fromSession forKey:@"sessionId"]; + [userInfo setObject:fromUser forKey:@"userId"]; + + NSString *displayName = [self getDisplayNameFromSessionId:fromSession]; + + if (displayName) { + [userInfo setObject:displayName forKey:@"displayName"]; + } + } + + if ([messageType isEqualToString:@"startedTyping"]) { + [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveStartedTypingNotification + object:self + userInfo:userInfo]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveStoppedTypingNotification + object:self + userInfo:userInfo]; + } + } else { + [self.delegate externalSignalingController:self didReceivedSignalingMessage:messageDict]; + } } #pragma mark - Completion blocks @@ -713,6 +760,11 @@ - (NSString *)getDisplayNameFromSessionId:(NSString *)sessionId return displayName; } +- (NSMutableDictionary *)getParticipantMap +{ + return _participantsMap; +} + - (NSDictionary *)getWebSocketMessageFromJSONData:(NSData *)jsonData { NSError *error; diff --git a/NextcloudTalk/NCSignalingMessage.h b/NextcloudTalk/NCSignalingMessage.h index 756801822..cec2b737c 100644 --- a/NextcloudTalk/NCSignalingMessage.h +++ b/NextcloudTalk/NCSignalingMessage.h @@ -40,7 +40,9 @@ typedef enum { kNCSignalingMessageTypeNickChanged, kNCSignalingMessageTypeRaiseHand, kNCSignalingMessageTypeRecording, - kNCSignalingMessageTypeReaction + kNCSignalingMessageTypeReaction, + kNCSignalingMessageTypeStartedTyping, + kNCSignalingMessageTypeStoppedTyping } NCSignalingMessageType; @@ -168,3 +170,25 @@ typedef enum { - (instancetype)initWithValues:(NSDictionary *)values; @end + +@interface NCStartedTypingMessage : NCSignalingMessage + +- (instancetype)initWithFrom:(NSString *)from + sendTo:(NSString *)to + withPayload:(NSDictionary *)payload + forRoomType:(NSString *)roomType; + +- (instancetype)initWithValues:(NSDictionary *)values; + +@end + +@interface NCStoppedTypingMessage : NCSignalingMessage + +- (instancetype)initWithFrom:(NSString *)from + sendTo:(NSString *)to + withPayload:(NSDictionary *)payload + forRoomType:(NSString *)roomType; + +- (instancetype)initWithValues:(NSDictionary *)values; + +@end diff --git a/NextcloudTalk/NCSignalingMessage.m b/NextcloudTalk/NCSignalingMessage.m index 75092c065..f9e8d5c9e 100644 --- a/NextcloudTalk/NCSignalingMessage.m +++ b/NextcloudTalk/NCSignalingMessage.m @@ -64,6 +64,8 @@ static NSString * const kNCSignalingMessageTypeRaiseHandKey = @"raiseHand"; static NSString * const kNCSignalingMessageTypeRecordingKey = @"recording"; static NSString * const kNCSignalingMessageTypeReactionKey = @"reaction"; +static NSString * const kNCSignalingMessageTypeStartedTypingKey = @"startedTyping"; +static NSString * const kNCSignalingMessageTypeStoppedTypingKey = @"stoppedTyping"; static NSString * const kNCSignalingMessageSdpKey = @"sdp"; @@ -891,3 +893,174 @@ - (NCSignalingMessageType)messageType { } @end + + +@implementation NCStartedTypingMessage + +- (instancetype)initWithFrom:(NSString *)from sendTo:(NSString *)to withPayload:(NSDictionary *)payload forRoomType:(NSString *)roomType { + + return [super initWithFrom:from + to:to + sid:[NCSignalingMessage getMessageSid] + type:kNCSignalingMessageTypeStartedTypingKey + payload:payload + roomType:roomType]; +} + +- (instancetype)initWithValues:(NSDictionary *)values { + NSDictionary *dataDict = [[NSDictionary alloc] initWithDictionary:values]; + NSDictionary *payload = [dataDict objectForKey:kNCSignalingMessagePayloadKey]; + NSString *from = [values objectForKey:kNCSignalingMessageFromKey]; + // Get 'from' value from 'sender' using External Signaling + NSDictionary *sender = [values objectForKey:kNCExternalSignalingMessageSenderKey]; + if (sender) { + from = [sender objectForKey:kNCExternalSignalingMessageSessionIdKey]; + dataDict = [values objectForKey:kNCExternalSignalingMessageDataKey]; + payload = [dataDict objectForKey:kNCSignalingMessagePayloadKey]; + } + return [super initWithFrom:from + to:[dataDict objectForKey:kNCSignalingMessageToKey] + sid:[dataDict objectForKey:kNCSignalingMessageSidKey] + type:kNCSignalingMessageTypeStartedTypingKey + payload:payload + roomType:[dataDict objectForKey:kNCSignalingMessageRoomTypeKey]]; +} + +- (NSData *)JSONData { + NSError *error = nil; + NSData *data = + [NSJSONSerialization dataWithJSONObject:[self messageDict] + options:0 + error:&error]; + if (error) { + RTCLogError(@"Error serializing JSON: %@", error); + return nil; + } + + return data; +} + +- (NSString *)functionJSONSerialization +{ + NSError *error; + NSString *jsonString = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[self functionDict] + options:0 + error:&error]; + + if (! jsonData) { + NSLog(@"Error serializing JSON: %@", error); + } else { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + + return jsonString; +} + +- (NSDictionary *)messageDict { + return @{ + kNCSignalingMessageEventKey: kNCSignalingMessageKey, + kNCSignalingMessageFunctionKey: [self functionJSONSerialization], + kNCSignalingMessageSessionIdKey: self.from + }; +} + +- (NSDictionary *)functionDict { + return @{ + kNCSignalingMessageToKey: self.to, + kNCSignalingMessageRoomTypeKey: self.roomType, + kNCSignalingMessageTypeKey: self.type, + kNCSignalingMessagePayloadKey: self.payload, + }; +} + +- (NCSignalingMessageType)messageType { + return kNCSignalingMessageTypeStartedTyping; +} + +@end + +@implementation NCStoppedTypingMessage + +- (instancetype)initWithFrom:(NSString *)from sendTo:(NSString *)to withPayload:(NSDictionary *)payload forRoomType:(NSString *)roomType { + + return [super initWithFrom:from + to:to + sid:[NCSignalingMessage getMessageSid] + type:kNCSignalingMessageTypeStoppedTypingKey + payload:payload + roomType:roomType]; +} + +- (instancetype)initWithValues:(NSDictionary *)values { + NSDictionary *dataDict = [[NSDictionary alloc] initWithDictionary:values]; + NSDictionary *payload = [dataDict objectForKey:kNCSignalingMessagePayloadKey]; + NSString *from = [values objectForKey:kNCSignalingMessageFromKey]; + // Get 'from' value from 'sender' using External Signaling + NSDictionary *sender = [values objectForKey:kNCExternalSignalingMessageSenderKey]; + if (sender) { + from = [sender objectForKey:kNCExternalSignalingMessageSessionIdKey]; + dataDict = [values objectForKey:kNCExternalSignalingMessageDataKey]; + payload = [dataDict objectForKey:kNCSignalingMessagePayloadKey]; + } + return [super initWithFrom:from + to:[dataDict objectForKey:kNCSignalingMessageToKey] + sid:[dataDict objectForKey:kNCSignalingMessageSidKey] + type:kNCSignalingMessageTypeStoppedTypingKey + payload:payload + roomType:[dataDict objectForKey:kNCSignalingMessageRoomTypeKey]]; +} + +- (NSData *)JSONData { + NSError *error = nil; + NSData *data = + [NSJSONSerialization dataWithJSONObject:[self messageDict] + options:0 + error:&error]; + if (error) { + RTCLogError(@"Error serializing JSON: %@", error); + return nil; + } + + return data; +} + +- (NSString *)functionJSONSerialization +{ + NSError *error; + NSString *jsonString = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[self functionDict] + options:0 + error:&error]; + + if (! jsonData) { + NSLog(@"Error serializing JSON: %@", error); + } else { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + + return jsonString; +} + +- (NSDictionary *)messageDict { + return @{ + kNCSignalingMessageEventKey: kNCSignalingMessageKey, + kNCSignalingMessageFunctionKey: [self functionJSONSerialization], + kNCSignalingMessageSessionIdKey: self.from + }; +} + +- (NSDictionary *)functionDict { + return @{ + kNCSignalingMessageToKey: self.to, + kNCSignalingMessageRoomTypeKey: self.roomType, + kNCSignalingMessageTypeKey: self.type, + kNCSignalingMessagePayloadKey: self.payload, + }; +} + +- (NCSignalingMessageType)messageType { + return kNCSignalingMessageTypeStoppedTyping; +} + +@end diff --git a/NextcloudTalk/ReplyMessageView.h b/NextcloudTalk/ReplyMessageView.h index 16f38f255..291c16c2e 100644 --- a/NextcloudTalk/ReplyMessageView.h +++ b/NextcloudTalk/ReplyMessageView.h @@ -21,14 +21,14 @@ */ #import -#import "SLKTypingIndicatorProtocol.h" +#import "SLKVisibleViewProtocol.h" NS_ASSUME_NONNULL_BEGIN @class NCChatMessage; @class QuotedMessageView; -@interface ReplyMessageView : UIView +@interface ReplyMessageView : UIView @property (nonatomic, strong) NCChatMessage *message; @property (nonatomic, strong) QuotedMessageView *quotedMessageView; diff --git a/NextcloudTalk/ReplyMessageView.m b/NextcloudTalk/ReplyMessageView.m index a6bdb9bb1..2d8e83741 100644 --- a/NextcloudTalk/ReplyMessageView.m +++ b/NextcloudTalk/ReplyMessageView.m @@ -90,7 +90,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection } -#pragma mark - SLKTypingIndicatorProtocol +#pragma mark - SLKReplyViewProtocol - (void)dismiss { diff --git a/NextcloudTalk/TypingIndicatorView.swift b/NextcloudTalk/TypingIndicatorView.swift new file mode 100644 index 000000000..261fa1a07 --- /dev/null +++ b/NextcloudTalk/TypingIndicatorView.swift @@ -0,0 +1,190 @@ +// +// Copyright (c) 2023 Marcel Müller +// +// Author Marcel Müller +// +// GNU GPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Combine +import SwiftyAttributes + +@objcMembers class TypingIndicatorView: UIView, SLKVisibleViewProtocol { + private class TypingUser { + var userId: String + var displayName: String + var sessionCount: Int = 0 + + init(userId: String, displayName: String) { + self.userId = userId + self.displayName = displayName + self.sessionCount = 1 + } + } + + dynamic var isVisible: Bool = false + + private var typingUsers: [TypingUser] = [] + private var previousUpdateTimestamp: TimeInterval = .zero + private var updateTimer: Timer? + + @IBOutlet var contentView: UIView! + @IBOutlet weak var typingLabel: UILabel! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + func commonInit() { + Bundle.main.loadNibNamed("TypingIndicatorView", owner: self, options: nil) + addSubview(contentView) + contentView.frame = frame + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + contentView.backgroundColor = .clear + + typingLabel.text = "" + } + + private func getUsersTypingString() -> NSAttributedString { + // Array keep the order of the elements, no need to sort here manually + if self.typingUsers.count == 1 { + // Alice + return self.typingUsers[0].displayName.withTextColor(.secondaryLabel) + + } else { + let separator = ", ".withTextColor(.tertiaryLabel) + let separatorSpace = NSAttributedString(string: " ") + let separatorLast = NSLocalizedString("and", comment: "Alice and Bob").withTextColor(.tertiaryLabel) + + if self.typingUsers.count == 2 { + // Alice and Bob + let user1 = self.typingUsers[0].displayName.withTextColor(.secondaryLabel) + let user2 = self.typingUsers[1].displayName.withTextColor(.secondaryLabel) + + return user1 + separatorSpace + separatorLast + separatorSpace + user2 + + } else if self.typingUsers.count == 3 { + // Alice, Bob and Charlie + let user1 = self.typingUsers[0].displayName.withTextColor(.secondaryLabel) + let user2 = self.typingUsers[1].displayName.withTextColor(.secondaryLabel) + let user3 = self.typingUsers[2].displayName.withTextColor(.secondaryLabel) + + return user1 + separator + user2 + separatorSpace + separatorLast + separatorSpace + user3 + + } else { + // Alice, Bob, Charlie + let user1 = self.typingUsers[0].displayName.withTextColor(.secondaryLabel) + let user2 = self.typingUsers[1].displayName.withTextColor(.secondaryLabel) + let user3 = self.typingUsers[2].displayName.withTextColor(.secondaryLabel) + + return user1 + separator + user2 + separator + user3 + } + } + } + + private func updateTypingIndicator() { + if self.typingUsers.isEmpty { + // Just hide the label to have a nice animation. Otherwise we would animate an empty label/space + self.isVisible = false + } else { + let attributedSpace = NSAttributedString(string: " ") + var localizedSuffix: NSAttributedString + + if self.typingUsers.count == 1 { + localizedSuffix = NSLocalizedString("is typing…", comment: "Alice is typing…").withTextColor(.tertiaryLabel) + + } else if self.typingUsers.count == 2 || self.typingUsers.count == 3 { + localizedSuffix = NSLocalizedString("are typing…", comment: "Alice and Bob are typing…").withTextColor(.tertiaryLabel) + + } else if self.typingUsers.count == 4 { + localizedSuffix = NSLocalizedString("and 1 other is typing…", comment: "Alice, Bob, Charlie and 1 other is typing…").withTextColor(.tertiaryLabel) + + } else { + let localizedString = NSLocalizedString("and %ld others are typing…", comment: "Alice, Bob, Charlie and 3 others are typing…") + let formattedString = String(format: localizedString, self.typingUsers.count - 3) + localizedSuffix = formattedString.withTextColor(.tertiaryLabel) + } + + UIView.transition(with: self.typingLabel, + duration: 0.2, + options: .transitionCrossDissolve, + animations: { + self.typingLabel.attributedText = self.getUsersTypingString() + attributedSpace + localizedSuffix + }, completion: nil) + + self.isVisible = true + } + + self.previousUpdateTimestamp = Date().timeIntervalSinceReferenceDate + } + + private func updateTypingIndicatorDebounced() { + // There's already an update planned, no need to do that again + if updateTimer != nil { + return + } + + let currentUpdateTimestamp: TimeInterval = Date().timeIntervalSinceReferenceDate + + // Update the typing indicator at max. every second + let timestampDiff = currentUpdateTimestamp - previousUpdateTimestamp + if timestampDiff < 1.0 { + self.updateTimer = Timer.scheduledTimer(withTimeInterval: 1.0 - timestampDiff, repeats: false, block: { _ in + self.updateTypingIndicator() + self.updateTimer = nil + }) + } else { + self.updateTypingIndicator() + } + } + + func addTyping(userId: String, displayName: String) { + let existingEntry = self.typingUsers.first(where: { $0.userId == userId}) + + if let existingEntry = existingEntry { + // We already have this userId in our array, so probably we received this from a different session + existingEntry.sessionCount += 1 + } else { + let newEntry = TypingUser(userId: userId, displayName: displayName) + self.typingUsers.append(newEntry) + } + + self.updateTypingIndicatorDebounced() + } + + func removeTyping(userId: String) { + let existingIndex = self.typingUsers.firstIndex(where: { $0.userId == userId}) + + if let existingIndex = existingIndex { + let existingEntry = self.typingUsers[existingIndex] + + if existingEntry.sessionCount == 1 { + self.typingUsers.remove(at: existingIndex) + } else { + existingEntry.sessionCount -= 1 + } + } + + self.updateTypingIndicatorDebounced() + } +} diff --git a/NextcloudTalk/TypingIndicatorView.xib b/NextcloudTalk/TypingIndicatorView.xib new file mode 100644 index 000000000..2ecb84d2f --- /dev/null +++ b/NextcloudTalk/TypingIndicatorView.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ThirdParty/SlackTextViewController b/ThirdParty/SlackTextViewController index f6edf21aa..6af8c0afc 160000 --- a/ThirdParty/SlackTextViewController +++ b/ThirdParty/SlackTextViewController @@ -1 +1 @@ -Subproject commit f6edf21aa3f972733e4a18b7fb7413b9806a716f +Subproject commit 6af8c0afc127b6d1ebb76a7cd1aa1ed63971f6f6