#import "TGNeoConversationController.h" #import "TGNeoChatsController.h" #import "TGStringUtils.h" #import "TGDateUtils.h" #import "WKInterfaceTable+TGDataDrivenTable.h" #import "TGTableDeltaUpdater.h" #import "TGInterfaceMenu.h" #import "TGBridgeClient.h" #import "TGBridgeContext.h" #import "TGBridgeUser.h" #import "TGBridgeChat.h" #import "TGBridgeChatMessageListView.h" #import "TGBridgeMessage+TGTableItem.h" #import "TGBridgeBotInfo.h" #import "TGBridgeBotReplyMarkup.h" #import "TGBridgeUserCache.h" #import "TGChatInfo.h" #import "TGBridgeChatMessageListSignals.h" #import "TGBridgeConversationSignals.h" #import "TGBridgePeerSettingsSignals.h" #import "TGBridgeSendMessageSignals.h" #import "TGBridgeBotSignals.h" #import "TGBridgeStateSignal.h" #import "TGBridgeRemoteSignals.h" #import "TGBridgeAudioSignals.h" #import "TGNeoConversationRowController.h" #import "TGNeoConversationStaticRowController.h" #import "TGNeoConversationTimeRowController.h" #import "TGConversationFooterController.h" #import "TGUserInfoController.h" #import "TGGroupInfoController.h" #import "TGBotCommandController.h" #import "TGBotKeyboardController.h" #import "TGStickersController.h" #import "TGLocationController.h" #import "TGInputController.h" #import "TGMessageViewController.h" #import "TGAudioMicAlertController.h" NSString *const TGNeoConversationControllerIdentifier = @"TGNeoConversationController"; const NSInteger TGNeoConversationControllerDefaultBatchLimit = 8; const NSInteger TGNeoConversationControllerMaximumBatchLimit = 20; const NSInteger TGNeoConversationControllerInitialRenderCount = 4; @interface TGNeoConversationControllerContext () { int64_t _peerId; SVariable *_messages; } @property (nonatomic, readonly) SSignal *signal; @property (nonatomic, readonly) bool shouldReadMessages; @end @implementation TGNeoConversationControllerContext - (instancetype)initWithChat:(TGBridgeChat *)chat { self = [super init]; if (self != nil) { _chat = chat; [self initialize]; } return self; } - (instancetype)initWithPeerId:(int64_t)peerId { self = [super init]; if (self != nil) { _peerId = peerId; [self initialize]; } return self; } - (void)initialize { _shouldReadMessages = true; NSInteger rangeCount = TGNeoConversationControllerDefaultBatchLimit; NSInteger initialUnreadCount = _chat.unreadCount; if (initialUnreadCount > 0) { rangeCount = MAX(TGNeoConversationControllerDefaultBatchLimit, MIN(TGNeoConversationControllerMaximumBatchLimit, initialUnreadCount)); if (initialUnreadCount > TGNeoConversationControllerMaximumBatchLimit) _shouldReadMessages = false; } _messages = [[SVariable alloc] init]; [_messages set:[[TGBridgeChatMessageListSignals chatMessageListViewWithPeerId:self.peerId atMessageId:0 rangeMessageCount:rangeCount] deliverOn:[SQueue mainQueue]]]; } - (int64_t)peerId { if (_peerId == 0) return _chat.identifier; return _peerId; } - (SSignal *)signal { return _messages.signal; } @end @interface TGNeoConversationController () { TGNeoConversationControllerContext *_context; SMetaDisposable *_messagesListDisposable; SMetaDisposable *_chatGroupDisposable; SMetaDisposable *_sendMessageDisposable; SMetaDisposable *_readMessagesDisposable; SMetaDisposable *_peerSettingsDisposable; SMetaDisposable *_updateSettingsDisposable; SMetaDisposable *_botInfoDisposable; SMetaDisposable *_botReplyMarkupDisposable; SMetaDisposable *_remoteActionDisposable; SMetaDisposable *_sentMediaDisposable; SMetaDisposable *_playAudioDisposable; TGBridgeMediaAttachment *_pendingAudioAttachment; TGBridgeChat *_chatModel; TGBridgeChatMessageListView *_messageListView; TGBridgeBotInfo *_botInfo; TGBridgeBotReplyMarkup *_botReplyMarkup; NSDictionary *_peerModels; NSMutableArray *_pendingSentMessages; bool _shouldReadMessages; bool _muted; bool _blocked; bool _hasBots; NSArray *_rowModels; bool _initialized; bool _initialRendering; bool _shouldScrollToBottom; TGInterfaceMenu *_menu; TGConversationFooterOptions _footerOptions; bool _dontAnimateFooterTransition; } @end @implementation TGNeoConversationController - (instancetype)init { self = [super init]; if (self != nil) { _messagesListDisposable = [[SMetaDisposable alloc] init]; _chatGroupDisposable = [[SMetaDisposable alloc] init]; _sendMessageDisposable = [[SMetaDisposable alloc] init]; _readMessagesDisposable = [[SMetaDisposable alloc] init]; _peerSettingsDisposable = [[SMetaDisposable alloc] init]; _updateSettingsDisposable = [[SMetaDisposable alloc] init]; _botInfoDisposable = [[SMetaDisposable alloc] init]; _botReplyMarkupDisposable = [[SMetaDisposable alloc] init]; _remoteActionDisposable = [[SMetaDisposable alloc] init]; _sentMediaDisposable = [[SMetaDisposable alloc] init]; _playAudioDisposable = [[SMetaDisposable alloc] init]; _pendingSentMessages = [[NSMutableArray alloc] init]; _dontAnimateFooterTransition = true; self.table.reloadDataReversed = true; self.table.tableDataSource = self; [self.table _setInitialHidden:true]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(synchronizationStateUpdated:) name:TGSynchronizationStateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextUpdated:) name:TGContextNotification object:nil]; } return self; } - (void)dealloc { [_messagesListDisposable dispose]; [_chatGroupDisposable dispose]; [_sendMessageDisposable dispose]; [_readMessagesDisposable dispose]; [_peerSettingsDisposable dispose]; [_updateSettingsDisposable dispose]; [_botInfoDisposable dispose]; [_botReplyMarkupDisposable dispose]; [_remoteActionDisposable dispose]; [_sentMediaDisposable dispose]; [_playAudioDisposable dispose]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)configureWithContext:(TGNeoConversationControllerContext *)context { _context = context; if (context.finished != nil) context.finished(); if (_context.chat.identifier < 0) _chatModel = _context.chat; self.title = [self conversationTitle]; _shouldReadMessages = context.shouldReadMessages; __weak TGNeoConversationController *weakSelf = self; [_messagesListDisposable setDisposable:[context.signal startWithNext:^(NSDictionary *models) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_shouldScrollToBottom = (strongSelf->_messageListView == nil); TGBridgeChatMessageListView *messageListView = models[TGBridgeChatMessageListViewKey]; strongSelf->_messageListView = messageListView; [[TGBridgeUserCache instance] storeUsers:[models[TGBridgeUsersDictionaryKey] allValues]]; strongSelf->_peerModels = models[TGBridgeUsersDictionaryKey]; [strongSelf _readMessagesIfNeeded]; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; if ([self peerIsAnyGroup]) { [_chatGroupDisposable setDisposable:[[TGBridgeConversationSignals conversationWithPeerId:[self peerId]] startWithNext:^(NSDictionary *next) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_chatModel = next[TGBridgeChatKey]; [[TGBridgeUserCache instance] storeUsers:[next[TGBridgeUsersDictionaryKey] allValues]]; [strongSelf _updateBots]; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; } else { [self _updateBots]; if (_hasBots) { [_botInfoDisposable setDisposable:[[TGBridgeBotSignals botInfoForUserId:(int32_t)[self peerId]] startWithNext:^(TGBridgeBotInfo *next) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_botInfo = next; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; } } if ([self peerIsAnyGroup] || _hasBots) { [_botReplyMarkupDisposable setDisposable:[[TGBridgeBotSignals botReplyMarkupForPeerId:[self peerId]] startWithNext:^(TGBridgeBotReplyMarkup *next) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_botReplyMarkup = next; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; } [_peerSettingsDisposable setDisposable:[[TGBridgePeerSettingsSignals peerSettingsWithPeerId:[self peerId]] startWithNext:^(NSDictionary *next) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGBridgePeerNotificationSettings *settings = next[@"notifications"]; bool blocked = [next[@"blocked"] boolValue]; bool muted = (settings.muteFor > 0); strongSelf->_blocked = blocked; strongSelf->_muted = muted; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; [_sentMediaDisposable setDisposable:[[TGBridgeAudioSignals sentAudioForConversationId:[self peerId]] startWithNext:^(id next) { }]]; [self configureHandoff]; } - (void)reloadData { NSArray *currentRowModels = _rowModels; NSMutableArray *rowModels = [TGNeoConversationController reversedMessagesArray:_messageListView.messages]; if (rowModels == nil) return; TGConversationFooterOptions oldFooterOptions = _footerOptions; _footerOptions = TGConversationFooterOptionsSendMessage; if (_chatModel.isKickedFromGroup || _chatModel.hasLeftGroup) _footerOptions = TGConversationFooterOptionsInactive; else if (_blocked) _footerOptions = [self _userIsBot] ? TGConversationFooterOptionsRestartBot : TGConversationFooterOptionsUnblock; else if (![self peerIsGroup] && _hasBots && _messageListView != nil && _messageListView.messages.count == 0) _footerOptions = TGConversationFooterOptionsStartBot; if (_footerOptions == TGConversationFooterOptionsSendMessage && _hasBots) { if (_botReplyMarkup.rows.count > 0) _footerOptions |= TGConversationFooterOptionsBotKeyboard; else _footerOptions |= TGConversationFooterOptionsBotCommands; } if ([self peerIsAnyGroup] || !_hasBots) _footerOptions |= TGConversationFooterOptionsVoice; NSMutableArray *pendingSentMessages = [[NSMutableArray alloc] init]; for (TGBridgeMessage *message in _pendingSentMessages) { TGBridgeDocumentMediaAttachment *documentAttachment = nil; TGBridgeLocationMediaAttachment *locationAttachment = nil; TGBridgeAudioMediaAttachment *audioAttachment = nil; for (TGBridgeMediaAttachment *attachment in message.media) { if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; } bool skip = false; for (TGBridgeMessage *realMessage in rowModels) { if (!realMessage.outgoing) continue; if (fabs(realMessage.date - message.date) > 4.0) continue; if ([realMessage.text isEqualToString:message.text]) { skip = true; } else { TGBridgeDocumentMediaAttachment *realDocumentAttachment = nil; TGBridgeLocationMediaAttachment *realLocationAttachment = nil; TGBridgeAudioMediaAttachment *realAudioAttachment = nil; for (TGBridgeMediaAttachment *attachment in message.media) { if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) realDocumentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) realLocationAttachment = (TGBridgeLocationMediaAttachment *)attachment; else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) realAudioAttachment = (TGBridgeAudioMediaAttachment *)attachment; } if ([realDocumentAttachment isEqual:documentAttachment] || [realLocationAttachment isEqual:locationAttachment] || [realAudioAttachment isEqual:audioAttachment]) { skip = true; } } } if (!skip) [pendingSentMessages addObject:message]; } _pendingSentMessages = pendingSentMessages; for (TGBridgeMessage *message in pendingSentMessages) [rowModels addObject:message]; if (_botInfo.botDescription.length > 0) { TGChatInfo *chatInfo = [[TGChatInfo alloc] init]; chatInfo.title = TGLocalized(@"Bot.DescriptionTitle"); chatInfo.text = _botInfo.botDescription; [rowModels insertObject:chatInfo atIndex:0]; } _rowModels = [TGNeoConversationController timestampedModelsArray:rowModels]; bool initial = (currentRowModels == nil); if (!initial) { [TGTableDeltaUpdater updateTable:self.table oldData:currentRowModels newData:_rowModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) { return [self table:self.table rowControllerClassAtIndexPath:indexPath]; }]; if (oldFooterOptions != _footerOptions) [self.table reloadFooter]; } else { _initialRendering = true; [self.table reloadData]; self.activityIndicator.hidden = true; self.table.hidden = false; } if (_shouldScrollToBottom) { _shouldScrollToBottom = false; if (!_initialized) { [self animateWithDuration:0.4 animations:^ { self.table.alpha = 1.0f; }]; _initialized = true; } [self.table scrollToRowAtIndexPath:[TGIndexPath indexPathForRow:_rowModels.count - 1 inSection:0]]; if (_initialRendering) { TGDispatchAfter(1.7, dispatch_get_main_queue(), ^ { _initialRendering = false; [self.table reloadAllRows]; }); } } [self updateMenuItems]; } - (NSString *)conversationTitle { if ([self peerIsGroup] || [self peerIsChannel]) return _chatModel.groupTitle; else return [[[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]] displayName]; } - (void)configureHandoff { int64_t peerId = [self peerId]; bool isGroup = [self peerIsGroup] || [self peerIsChannel]; if (isGroup) peerId = -peerId; NSMutableDictionary *peerDict = [[NSMutableDictionary alloc] init]; peerDict[@"type"] = isGroup ? @"group" : @"user"; peerDict[@"id"] = @(peerId); NSDictionary *userInfo = @{@"user_id": @(_context.context.userId), @"peer": peerDict}; [self updateUserActivity:@"org.telegram.conversation" userInfo:userInfo webpageURL:[NSURL URLWithString:@"https://telegram.org/dl"]]; } #pragma mark - - (void)synchronizationStateUpdated:(NSNotification *)notification { //TGBridgeSynchronizationStateValue value = (TGBridgeSynchronizationStateValue)[notification.userInfo[TGSynchronizationStateKey] integerValue]; //if (self.isVisible) // [self updateTitleWithState:value]; } - (void)contextUpdated:(NSNotification *)notification { TGBridgeContext *context = notification.userInfo[TGContextNotificationKey]; if (context != nil) _context.context = context; } - (void)updateTitleWithState:(TGBridgeSynchronizationStateValue)value { NSString *state = [TGNeoChatsController stringForSyncState:value]; if (_context.context == nil || state == nil) self.title = [self conversationTitle]; else self.title = state; } - (void)updateMenuItems { [_menu clearItems]; if (_context.chat.isKickedFromGroup || _context.chat.hasLeftGroup) return; if (_menu == nil) _menu = [[TGInterfaceMenu alloc] initForInterfaceController:self]; NSMutableArray *menuItems = [[NSMutableArray alloc] init]; __weak TGNeoConversationController *weakSelf = self; TGInterfaceMenuItem *infoItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:WKMenuItemIconInfo title:[self peerIsAnyGroup] ? TGLocalized(@"Watch.Conversation.GroupInfo") : TGLocalized(@"Watch.Conversation.UserInfo") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; if ([strongSelf peerIsGroup]) { TGGroupInfoControllerContext *context = [[TGGroupInfoControllerContext alloc] initWithGroupChat:strongSelf->_context.chat]; [controller pushControllerWithClass:[TGGroupInfoController class] context:context]; } else if ([strongSelf peerIsChannel]) { TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithChannel:strongSelf->_chatModel]; context.disallowCompose = true; [controller pushControllerWithClass:[TGUserInfoController class] context:context]; } else { TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)[strongSelf peerId]]; context.disallowCompose = true; [controller pushControllerWithClass:[TGUserInfoController class] context:context]; } }]; [menuItems addObject:infoItem]; bool muted = _muted; bool blocked = _blocked; bool muteForever = [self peerIsAnyGroup]; int32_t muteFor = muteForever ? INT_MAX : 1; NSString *muteTitle = muteForever ? TGLocalized(@"Watch.UserInfo.Mute") : [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Watch.UserInfo.Mute_" value:muteFor]), muteFor]; TGInterfaceMenuItem *muteItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:muted ? WKMenuItemIconSpeaker : WKMenuItemIconMute title:muted ? TGLocalized(@"Watch.UserInfo.Unmute") : muteTitle actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGBridgePeerNotificationSettings *settings = [[TGBridgePeerNotificationSettings alloc] init]; settings.muteFor = muted ? 0 : (muteFor == INT_MAX ? INT_MAX : muteFor * 60 * 60); [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateNotificationSettingsWithPeerId:[strongSelf peerId] settings:settings] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_muted = !muted; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; }]; [menuItems addObject:muteItem]; if (![self peerIsGroup] && ![self peerIsChannel]) { TGInterfaceMenuItem *blockItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:WKMenuItemIconBlock title:blocked ? TGLocalized(@"Watch.UserInfo.Unblock") : TGLocalized(@"Watch.UserInfo.Block") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:!blocked] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_blocked = !blocked; [strongSelf performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; }]]; }]; [menuItems addObject:blockItem]; } [_menu addItems:menuItems]; } #pragma mark - Peer - (int64_t)peerId { return _context.peerId; } - (bool)peerIsGroup { if (_chatModel != nil) return _chatModel.isGroup; else return _context.peerId < 0; } - (bool)peerIsChannel { if (_chatModel != nil) return _chatModel.isChannel; else return false; } - (bool)peerIsChannelGroup { if (_chatModel != nil) return _chatModel.isChannelGroup; else return false; } - (bool)peerIsAnyGroup { return [self peerIsGroup] || [self peerIsChannelGroup]; } #pragma mark - Bots - (SSignal *)botCommandListSignal { if (!_hasBots) return nil; if ([self peerIsAnyGroup]) { NSMutableArray *botInfoSignals = [[NSMutableArray alloc] init]; NSMutableArray *botUsers = [[NSMutableArray alloc] init]; NSMutableArray *initialStates = [[NSMutableArray alloc] init]; [_chatModel.participantsUserIds enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)idx]; if ([user isBot]) { [botUsers addObject:user]; [initialStates addObject:@[]]; [botInfoSignals addObject:[[TGBridgeBotSignals botInfoForUserId:user.identifier] map:^NSArray *(TGBridgeBotInfo *botInfo) { if (botInfo.commandList == nil) return @[]; return botInfo.commandList; }]]; } }]; return [[SSignal combineSignals:botInfoSignals withInitialStates:initialStates] map:^id(NSArray *commandLists) { NSMutableArray *commands = [[NSMutableArray alloc] init]; NSInteger index = 0; for (NSArray *commandList in commandLists) { [commands addObject:@{ TGBotCommandUserKey: botUsers[index], TGBotCommandListKey: commandList } ]; index++; } return commands; }]; } else if ([self _userIsBot]) { int32_t userId = (int32_t)[self peerId]; return [[TGBridgeBotSignals botInfoForUserId:userId] map:^NSArray *(TGBridgeBotInfo *botInfo) { if (botInfo != nil) { TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:userId]; return @[ @{ TGBotCommandUserKey: user, TGBotCommandListKey: botInfo.commandList } ]; } return nil; }]; } return nil; } - (bool)_userIsBot { if ([self peerId] < 0) return false; TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; return [user isBot]; } - (void)_updateBots { _hasBots = false; if ([self peerIsAnyGroup]) { [_chatModel.participantsUserIds enumerateIndexesUsingBlock:^(NSUInteger userId, BOOL * _Nonnull stop) { TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)userId]; if ([user isBot]) { _hasBots = true; *stop = true; } }]; } else { TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; _hasBots = [user isBot]; } } #pragma mark - - (void)sendMessageWithText:(NSString *)text { [self sendMessageWithText:text replyToMessage:nil]; } - (void)sendMessageWithText:(NSString *)text replyToMessage:(TGBridgeMessage *)replyToMessage { _shouldReadMessages = true; _shouldScrollToBottom = true; [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForText:text userId:_context.context.userId replyToMessage:replyToMessage]]; __weak TGNeoConversationController *weakSelf = self; [self performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] text:text replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; } - (void)sendMessageWithStickerAttachment:(TGBridgeDocumentMediaAttachment *)sticker { _shouldReadMessages = true; _shouldScrollToBottom = true; [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForSticker:sticker userId:_context.context.userId]]; __weak TGNeoConversationController *weakSelf = self; [self performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] sticker:sticker replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; } - (void)sendMessageWithLocationAttachment:(TGBridgeLocationMediaAttachment *)location { _shouldReadMessages = true; _shouldScrollToBottom = true; [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForLocation:location userId:_context.context.userId]]; __weak TGNeoConversationController *weakSelf = self; [self performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] location:location replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; } - (void)sendAudioWithUniqueId:(int64_t)uniqueId duration:(int32_t)duration url:(NSURL *)url { _shouldReadMessages = true; _shouldScrollToBottom = true; [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForAudioWithDuration:duration userId:_context.context.userId localAudioId:uniqueId]]; __weak TGNeoConversationController *weakSelf = self; [self performInterfaceUpdate:^(bool animated) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf reloadData]; }]; NSDictionary *metadata = @ { TGBridgeIncomingFileTypeKey: TGBridgeIncomingFileTypeAudio, TGBridgeIncomingFileRandomIdKey: @(uniqueId), TGBridgeIncomingFilePeerIdKey: @([self peerId]), TGBridgeIncomingFileReplyToMidKey: @(0) }; [[TGBridgeClient instance] sendFileWithURL:url metadata:metadata]; } - (TGBridgeMessage *)_latestIncomingMessage { __block TGBridgeMessage *incomingMessage = nil; [_messageListView.messages enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(TGBridgeMessage *message, NSUInteger index, BOOL *stop) { if (!message.outgoing) { *stop = true; incomingMessage = message; } }]; return incomingMessage; } - (void)_readMessagesIfNeeded { bool hasUnreadMessages = false; for (TGBridgeMessage *message in _messageListView.messages) { if (!message.outgoing && message.unread) { hasUnreadMessages = true; break; } } if (hasUnreadMessages && _shouldReadMessages) { [_readMessagesDisposable setDisposable:[[TGBridgeChatMessageListSignals readChatMessageListWithPeerId:[self peerId]] startWithNext:nil completed:nil]]; } } #pragma mark - Table Data Source & Delegate - (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath { id model = _rowModels[indexPath.row]; if ([model isKindOfClass:[TGBridgeMessage class]]) { return [TGNeoRowController rowControllerClassForMessage:(TGBridgeMessage *)model]; } else if ([model isKindOfClass:[TGChatInfo class]]) { return [TGNeoConversationStaticRowController class]; } else if ([model isKindOfClass:[TGChatTimestamp class]]) { return [TGNeoConversationTimeRowController class]; } return nil; } - (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section { return _rowModels.count; } - (void)table:(WKInterfaceTable *)table updateRowController:(TGTableRowController *)controller forIndexPath:(TGIndexPath *)indexPath { __weak TGNeoConversationController *weakSelf = self; id model = _rowModels[indexPath.row]; NSUInteger index = [self numberOfRowsInTable:self.table section:0] - indexPath.row - 1; if ([model isKindOfClass:[TGChatTimestamp class]]) { TGNeoConversationTimeRowController *timeController = (TGNeoConversationTimeRowController *)controller; [timeController updateWithTimestamp:model]; return; } TGNeoRowController *rowController = (TGNeoRowController *)controller; rowController.shouldRenderContent = ^bool { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf != nil && strongSelf->_initialRendering && index >= TGNeoConversationControllerInitialRenderCount) return false; return true; }; if ([model isKindOfClass:[TGBridgeMessage class]]) { TGBridgeMessage *message = (TGBridgeMessage *)model; TGNeoConversationRowController *conversationRow = (TGNeoConversationRowController *)controller; __weak TGNeoConversationRowController *weakConversationRow = conversationRow; conversationRow.animate = ^(void (^animations)(void)) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf animateWithDuration:0.25 animations:animations]; }; conversationRow.buttonPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGBridgeMediaAttachment *audioAttachment = nil; for (TGBridgeMediaAttachment *attachment in message.media) { if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) { audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; } else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) { TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; if (documentAttachment.isVoice) audioAttachment = documentAttachment; } } if (audioAttachment != nil) { __strong TGNeoConversationRowController *strongConversationRow = weakConversationRow; if ([strongSelf->_pendingAudioAttachment isEqual:audioAttachment]) { if (strongConversationRow != nil) [strongConversationRow setProcessingState:false]; strongSelf->_pendingAudioAttachment = nil; [strongSelf->_playAudioDisposable setDisposable:nil]; } else { if (strongConversationRow != nil) [strongConversationRow setProcessingState:true]; strongSelf->_pendingAudioAttachment = audioAttachment; [strongSelf->_playAudioDisposable setDisposable:[[[TGBridgeAudioSignals audioForAttachment:audioAttachment conversationId:[strongSelf peerId] messageId:message.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSURL *url) { if (url == nil) return; __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; __strong TGNeoConversationRowController *strongConversationRow = weakConversationRow; if (strongConversationRow != nil) [strongConversationRow setProcessingState:false]; strongSelf->_pendingAudioAttachment = nil; [strongSelf presentMediaPlayerControllerWithURL:url options:@{ WKMediaPlayerControllerOptionsAutoplayKey: @true } completion:^(BOOL didPlayToEnd, NSTimeInterval endTime, NSError *error) {}]; }]]; } } else { [strongSelf->_remoteActionDisposable setDisposable:[[TGBridgeRemoteSignals openRemoteMessageWithPeerId:[strongSelf peerId] messageId:message.identifier type:0 autoPlay:true] startWithNext:nil]]; } }; TGNeoMessageType type = TGNeoMessageTypeGeneric; if ([self peerIsAnyGroup]) type = TGNeoMessageTypeGroup; else if ([self peerIsChannel]) type = TGNeoMessageTypeChannel; conversationRow.additionalPeers = _peerModels; [conversationRow updateWithMessage:message context:_context.context index:index type:type]; } else if ([model isKindOfClass:[TGChatInfo class]]) { TGChatInfo *chatInfo = (TGChatInfo *)model; TGNeoConversationStaticRowController *conversationRow = (TGNeoConversationStaticRowController *)controller; [conversationRow updateWithChatInfo:chatInfo]; } } - (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath { TGBridgeMessage *message = _rowModels[indexPath.row]; TGMessageViewControllerContext *context = nil; if ([self peerIsChannel] && ![self peerIsChannelGroup]) context = [[TGMessageViewControllerContext alloc] initWithMessage:message channel:_chatModel]; else context = [[TGMessageViewControllerContext alloc] initWithMessage:message peerId:[self peerId]]; context.additionalPeers = _peerModels; [self pushControllerWithClass:[TGMessageViewController class] context:context]; } - (Class)footerControllerClassForTable:(WKInterfaceTable *)table { if ([self peerIsChannel] && ![self peerIsChannelGroup]) return nil; return [TGConversationFooterController class]; } - (void)table:(WKInterfaceTable *)table updateFooterController:(TGConversationFooterController *)controller { __weak TGNeoConversationController *weakSelf = self; controller.animate = ^(void (^animations)(void)) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf animateWithDuration:0.3 animations:animations]; }; [controller setOptions:_footerOptions animated:!_dontAnimateFooterTransition]; _dontAnimateFooterTransition = false; controller.stickerPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGStickersControllerContext *context = [[TGStickersControllerContext alloc] init]; context.completionBlock = ^(TGBridgeDocumentMediaAttachment *sticker) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendMessageWithStickerAttachment:sticker]; }; [strongSelf presentControllerWithClass:[TGStickersController class] context:context]; }; controller.locationPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGLocationControllerContext *context = [[TGLocationControllerContext alloc] init]; context.completionBlock = ^(TGBridgeLocationMediaAttachment *location) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendMessageWithLocationAttachment:location]; }; [strongSelf presentControllerWithClass:[TGLocationController class] context:context]; }; controller.voicePressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; if (strongSelf->_context.context.micAccessAllowed) { [TGInputController presentAudioControllerForInterfaceController:strongSelf completion:^(int64_t uniqueId, int32_t duration, NSURL *url) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendAudioWithUniqueId:uniqueId duration:duration url:url]; }]; } else { [strongSelf presentControllerWithClass:[TGAudioMicAlertController class] context:nil]; } }; controller.commandsPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; if (strongSelf->_botReplyMarkup != nil) { TGBridgeBotReplyMarkup *replyMarkup = strongSelf->_botReplyMarkup; TGBotKeyboardControllerContext *context = [[TGBotKeyboardControllerContext alloc] init]; context.replyMarkup = replyMarkup; context.completionBlock = ^(NSString *command) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendMessageWithText:command replyToMessage:replyMarkup.message]; }; [strongSelf presentControllerWithClass:[TGBotKeyboardController class] context:context]; } else { TGBotCommandControllerContext *context = [[TGBotCommandControllerContext alloc] init]; context.commandListSignal = [strongSelf botCommandListSignal]; context.context = strongSelf->_context.context; context.completionBlock = ^(NSString *command) { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendMessageWithText:command]; }; [strongSelf presentControllerWithClass:[TGBotCommandController class] context:context]; } }; controller.unblockPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:false] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_blocked = false; [strongSelf reloadData]; }]]; }; controller.replyPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [TGInputController presentInputControllerForInterfaceController:strongSelf suggestionsForText:[strongSelf _latestIncomingMessage].text completion:^(NSString *text) { [strongSelf sendMessageWithText:text]; }]; }; controller.startPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf sendMessageWithText:@"/start"]; }; controller.restartPressed = ^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:false] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ { __strong TGNeoConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_blocked = false; [strongSelf sendMessageWithText:@"/start"]; }]]; }; } #pragma mark - + (NSMutableArray *)reversedMessagesArray:(NSArray *)array { if (array == nil) return nil; NSMutableArray *reversedArray = [[NSMutableArray alloc] init]; for (id object in array) [reversedArray insertObject:object atIndex:0]; return reversedArray; } + (NSArray *)timestampedModelsArray:(NSArray *)models { NSMutableArray *newModels = [[NSMutableArray alloc] init]; TGChatTimestamp *lastTimestamp = nil; for (id model in models) { if ([model isKindOfClass:[TGChatTimestamp class]]) { continue; } else if ([model isKindOfClass:[TGBridgeMessage class]]) { TGBridgeMessage *message = (TGBridgeMessage *)model; TGChatTimestamp *timestamp = [TGDateUtils timestampForDateIfNeeded:message.date previousDate:lastTimestamp ? @(lastTimestamp.date) : nil]; if (timestamp != nil) { lastTimestamp = timestamp; [newModels addObject:timestamp]; } } [newModels addObject:model]; } return newModels; } #pragma mark - + (NSString *)identifier { return TGNeoConversationControllerIdentifier; } @end