#import "TGSecretModernConversationCompanion.h" #import "ActionStage.h" #import "SGraphObjectNode.h" #import "TGInterfaceManager.h" #import "TGTelegraph.h" #import "TGActionSheet.h" #import "TGAlertView.h" #import "TGNavigationBar.h" #import "TGSecretChatUserInfoController.h" #import "TGMessageModernConversationItem.h" #import "TGMessage.h" #import "TGModernConversationController.h" #import "TGModernConversationTitleIcon.h" #import "TGSecretConversationHandshakeStatusPanel.h" #import "TGModernConversationActionInputPanel.h" #import "TGSecretConversationEmptyListView.h" #import "TGSecretModernConversationAccessoryTimerView.h" #import "TGDatabase.h" #import "TGAppDelegate.h" #import "TGDialogListController.h" #import "TGTelegraphDialogListCompanion.h" #import "TGPopoverController.h" #import "TGModernGalleryController.h" #import "TGSecretTimerValueController.h" #import "TGStringUtils.h" #import "TGModernConversationUpgradeStateTitlePanel.h" #import "TGModernSendSecretMessageActor.h" #import "TGPickerSheet.h" @interface TGSecretModernConversationCompanion () { int64_t _encryptedConversationId; int64_t _accessHash; bool _encryptedConversationIsIncoming; NSString *_encryptedConversationUserName; NSTimeInterval _lastTypingActivity; TGConversation *_conversation; // Main Thread int _encryptionState; // Main Thread TGSecretModernConversationAccessoryTimerView *_selfDestructTimerView; int _selfDestructTimer; // Main Thread TGPickerSheet *_pickerSheet; } @end @implementation TGSecretModernConversationCompanion - (instancetype)initWithEncryptedConversationId:(int64_t)encryptedConversationId accessHash:(int64_t)accessHash conversationId:(int64_t)conversationId uid:(int)uid activity:(NSString *)activity mayHaveUnreadMessages:(bool)mayHaveUnreadMessages { self = [super initWithConversationId:conversationId uid:uid activity:activity mayHaveUnreadMessages:mayHaveUnreadMessages]; if (self != nil) { _encryptedConversationId = encryptedConversationId; _accessHash = accessHash; _conversation = [TGDatabaseInstance() loadConversationWithIdCached:_conversationId]; _encryptedConversationIsIncoming = _conversation.chatParticipants.chatAdminId != TGTelegraphInstance.clientUserId; _encryptedConversationUserName = [TGDatabaseInstance() loadUser:uid].displayFirstName; } return self; } - (void)dealloc { UIView *selfDestructTimerView = _selfDestructTimerView; TGPickerSheet *pickerSheet = _pickerSheet; dispatch_async(dispatch_get_main_queue(), ^ { [selfDestructTimerView alpha]; [pickerSheet dismiss]; }); } #pragma mark - - (void)loadInitialState { _selfDestructTimer = [TGDatabaseInstance() messageLifetimeForPeerId:_conversationId]; _selfDestructTimerView.timerValue = _selfDestructTimer; [self updateLayer:[TGDatabaseInstance() peerLayer:_conversationId]];\ [super loadInitialState]; TGModernConversationTitleIcon *lockIcon = [[TGModernConversationTitleIcon alloc] init]; lockIcon.bounds = CGRectMake(0.0f, 0.0f, 16, 16); lockIcon.offsetWeight = 0.5f; lockIcon.imageOffset = CGPointMake(3.0f, 5.0f); static UIImage *lockImage = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^ { lockImage = [UIImage imageNamed:@"ModernConversationTitleIconLock.png"]; }); lockIcon.image = lockImage; lockIcon.iconPosition = TGModernConversationTitleIconPositionBeforeTitle; [self setAdditionalTitleIcons:@[lockIcon]]; [self _updateEncryptionState:_conversation.encryptedData.handshakeState]; } #pragma mark - - (bool)controllerShouldStoreCapturedAssets { return false; } - (bool)controllerShouldCacheServerAssets { return false; } - (bool)controllerShouldLiveUploadVideo { return false; } - (bool)allowMessageForwarding { return false; } - (bool)allowReplies { return [self layer] >= 45; } - (bool)allowMessageEntities { return [self layer] >= 45; } - (bool)allowContactSharing { return false; } - (bool)allowVenueSharing { return [self layer] >= 45; } - (bool)allowCaptionedMedia { return [self layer] >= 45; } - (bool)encryptUploads { return true; } - (void)_controllerAvatarPressed { TGModernConversationController *controller = self.controller; if (controller.currentSizeClass == UIUserInterfaceSizeClassCompact) { [[TGInterfaceManager instance] navigateToProfileOfUser:_uid encryptedConversationId:_encryptedConversationId]; } else { if (controller != nil) { TGSecretChatUserInfoController *secretChatInfoController = [[TGSecretChatUserInfoController alloc] initWithUid:_uid encryptedConversationId:_encryptedConversationId]; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[secretChatInfoController] navigationBarClass:[TGWhiteNavigationBar class]]; navigationController.presentationStyle = TGNavigationControllerPresentationStyleRootInPopover; TGPopoverController *popoverController = [[TGPopoverController alloc] initWithContentViewController:navigationController]; navigationController.parentPopoverController = popoverController; navigationController.detachFromPresentingControllerInCompactMode = true; [popoverController setContentSize:CGSizeMake(320.0f, 528.0f)]; controller.associatedPopoverController = popoverController; [popoverController presentPopoverFromBarButtonItem:controller.navigationItem.rightBarButtonItem permittedArrowDirections:UIPopoverArrowDirectionAny animated:true]; secretChatInfoController.collectionView.contentOffset = CGPointMake(0.0f, -secretChatInfoController.collectionView.contentInset.top); } } } - (TGModernConversationEmptyListPlaceholderView *)_conversationEmptyListPlaceholder { TGSecretConversationEmptyListView *placeholder = [[TGSecretConversationEmptyListView alloc] initWithIncoming:_encryptedConversationIsIncoming userName:_encryptedConversationUserName]; return placeholder; } - (UIView *)_controllerInputTextPanelAccessoryView { if (_selfDestructTimerView == nil) { _selfDestructTimerView = [[TGSecretModernConversationAccessoryTimerView alloc] init]; _selfDestructTimerView.delegate = self; _selfDestructTimerView.timerValue = _selfDestructTimer; } return _selfDestructTimerView; } - (void)accessoryTimerViewPressed:(TGSecretModernConversationAccessoryTimerView *)__unused accessoryTimerView { NSMutableArray *timerValues = [[NSMutableArray alloc] init]; [timerValues addObject:@(0)]; for (int i = 1; i < 16; i++) { [timerValues addObject:@(i)]; } [timerValues addObject:@(30)]; [timerValues addObject:@(1 * 60)]; [timerValues addObject:@(1 * 60 * 60)]; [timerValues addObject:@(1 * 60 * 60 * 24)]; [timerValues addObject:@(1 * 60 * 60 * 24 * 7)]; NSUInteger selectedIndex = 5; if (_selfDestructTimer != 0) { NSInteger closestMatchIndex = 5; NSInteger index = -1; for (NSNumber *nValue in timerValues) { index++; if ([nValue intValue] != 0 && ABS([nValue intValue] - _selfDestructTimer) < ABS([timerValues[closestMatchIndex] intValue] - _selfDestructTimer)) { closestMatchIndex = index; } } selectedIndex = closestMatchIndex; } __weak TGSecretModernConversationCompanion *weakSelf = self; _pickerSheet = [[TGPickerSheet alloc] initWithItems:timerValues selectedIndex:selectedIndex action:^(NSNumber *timerValue) { __strong TGSecretModernConversationCompanion *strongSelf = weakSelf; [strongSelf _commitSetSelfDestructTimer:[timerValue intValue]]; }]; TGModernConversationController *controller = self.controller; if (TGAppDelegateInstance.rootController.currentSizeClass == UIUserInterfaceSizeClassRegular) { CGRect windowRect = [_selfDestructTimerView convertRect:_selfDestructTimerView.bounds toView:controller.view]; [_pickerSheet showFromRect:windowRect inView:controller.view]; } else [_pickerSheet show]; } - (void)_commitSetSelfDestructTimer:(int)value { if (value != _selfDestructTimer) { _selfDestructTimer = value; _selfDestructTimerView.timerValue = _selfDestructTimer; if (_selfDestructTimer > 0 && _selfDestructTimer <= 60 && [self layer] < 17) { TGUser *user = [TGDatabaseInstance() loadUser:_uid]; TGDispatchOnMainThread(^ { NSString *text = [[NSString alloc] initWithFormat:TGLocalized(@"Compatibility.SecretMediaVersionTooLow"), user.displayFirstName, user.displayFirstName]; [[[TGAlertView alloc] initWithTitle:text message:nil cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; }); } [TGDatabaseInstance() setMessageLifetimeForPeerId:_conversationId encryptedConversationId:_encryptedConversationId messageLifetime:value writeToActionQueue:true]; [ActionStageInstance() requestActor:@"/tg/service/synchronizeserviceactions/(settings)" options:nil watcher:TGTelegraphInstance]; } } - (bool)shouldDisplayContactLinkPanel { return false; } #pragma mark - - (void)_updateEncryptionState:(int)encryptionState { if (_encryptionState != encryptionState) { _encryptionState = encryptionState; TGModernConversationInputPanel *panel = nil; if (encryptionState == 1 || encryptionState == 2) { TGSecretConversationHandshakeStatusPanel *statusPanel = [[TGSecretConversationHandshakeStatusPanel alloc] init]; statusPanel.delegate = self.controller; panel = statusPanel; if (encryptionState == 1) // awaiting { NSString *formatText = TGLocalized(@"Conversation.EncryptionWaiting"); NSString *baseText = [[NSString alloc] initWithFormat:formatText, [TGDatabaseInstance() loadUser:_uid].displayFirstName]; [statusPanel setText:baseText]; } else [statusPanel setText:TGLocalized(@"Conversation.EncryptionProcessing")]; } else if (encryptionState == 3) // cancelled { TGModernConversationActionInputPanel *deleteAndExitPanel = [[TGModernConversationActionInputPanel alloc] init]; [deleteAndExitPanel setActionWithTitle:TGLocalized(@"ConversationProfile.LeaveDeleteAndExit") action:@"deleteAndExit"]; deleteAndExitPanel.delegate = self.controller; deleteAndExitPanel.companionHandle = self.actionHandle; panel = deleteAndExitPanel; } TGModernConversationController *controller = self.controller; [controller setCustomInputPanel:panel]; } } - (void)updateLayer:(NSUInteger)layer { self.layer = layer; [self updateDebugPanel]; } - (void)updateDebugPanel { /*#ifdef INTERNAL_RELEASE TGDispatchOnMainThread(^ { TGModernConversationController *controller = self.controller; TGModernConversationUpgradeStateTitlePanel *panel = [controller.secondaryTitlePanel isKindOfClass:[TGModernConversationUpgradeStateTitlePanel class]] ? (TGModernConversationUpgradeStateTitlePanel *)controller.secondaryTitlePanel : nil; if (panel == nil) { panel = [[TGModernConversationUpgradeStateTitlePanel alloc] init]; __weak TGSecretModernConversationCompanion *weakSelf = self; panel.rekey = ^ { __strong TGSecretModernConversationCompanion *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf _maybeRekey]; } }; } int64_t keyId = 0; [TGDatabaseInstance() encryptionKeyForConversationId:_conversationId requestedKeyFingerprint:0 outKeyFingerprint:&keyId]; [panel setCurrentLayer:self.layer keyId:keyId rekeySessionId:_conversation.encryptedData.currentRekeyExchangeId canRekey:self.layer >= 20 && _conversation.encryptedData.keyFingerprint != 0 && _conversation.encryptedData.currentRekeyExchangeId == 0]; [controller setSecondaryTitlePanel:panel animated:false]; }); #endif*/ } - (NSDictionary *)userActivityData { return nil; } #pragma mark - - (void)_maybeRekey { [TGModernSendSecretMessageActor maybeRekeyPeerId:_conversationId]; } #pragma mark - - (void)controllerDidUpdateTypingActivity { [ActionStageInstance() dispatchOnStageQueue:^ { CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent(); if (ABS(currentTime - _lastTypingActivity) >= 4.0) { _lastTypingActivity = currentTime; [ActionStageInstance() requestActor:[NSString stringWithFormat:@"/tg/conversation/(%lld)/activity/(typing)", _conversationId] options:[self _optionsForMessageActions] watcher:self]; } }]; } - (void)controllerDidCancelTypingActivity { } #pragma mark - - (bool)imageDownloadsShouldAutosavePhotos { return false; } - (bool)_shouldCacheRemoteAssetUris { return false; } - (int)messageLifetime { return [TGDatabaseInstance() messageLifetimeForPeerId:_conversationId]; } - (NSString *)_sendMessagePathForMessageId:(int32_t)mid { return [[NSString alloc] initWithFormat:@"/tg/sendSecretMessage/(%@)/(%d)", [self _conversationIdPathComponent], mid]; } - (NSString *)_sendMessagePathPrefix { return [[NSString alloc] initWithFormat:@"/tg/sendSecretMessage/(%@)/", [self _conversationIdPathComponent]]; } - (void)subscribeToUpdates { [ActionStageInstance() watchForPaths:@[ [[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/conversation", _conversationId], [[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/readByDateMessages", _conversationId], [[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/messageFlagChanges", _conversationId], [[NSString alloc] initWithFormat:@"/tg/conversation/messageViewDateChanges"], [[NSString alloc] initWithFormat:@"/tg/encrypted/messageLifetime/(%" PRId64 ")", _conversationId], [[NSString alloc] initWithFormat:@"/tg/peerLayerUpdates/(%" PRId64 ")", _conversationId] ] watcher:self]; [super subscribeToUpdates]; } - (NSDictionary *)_optionsForMessageActions { return @{ @"conversationId": @(_conversationId), @"encryptedConversationId": @(_encryptedConversationId), @"accessHash": @(_accessHash), @"isEncrypted": @(true) }; } - (bool)_messagesNeedRandomId { return true; } - (void)serviceNotificationsForMessageIds:(NSArray *)messageIds { [TGModernConversationCompanion dispatchOnMessageQueue:^ { NSMutableDictionary *messageFlagChanges = [[NSMutableDictionary alloc] init]; NSMutableArray *randomIds = [[NSMutableArray alloc] init]; for (NSNumber *nMid in messageIds) { int32_t messageId = [nMid intValue]; int messageFlags = [TGDatabaseInstance() secretMessageFlags:messageId]; //if ((messageFlags & TGSecretMessageFlagScreenshot) == 0) { messageFlags |= TGSecretMessageFlagScreenshot; TGMessage *message = [TGDatabaseInstance() loadMessageWithMid:messageId peerId:_conversationId]; if (message != nil) { messageFlagChanges[@(messageId)] = @(messageFlags); int64_t randomId = [TGDatabaseInstance() randomIdForMessageId:messageId]; if (randomId != 0) [randomIds addObject:@(randomId)]; } } } int64_t encryptedConversationId = [TGDatabaseInstance() encryptedConversationIdForPeerId:_conversationId]; if (encryptedConversationId != 0 && randomIds.count != 0) { int64_t actionRandomId = 0; arc4random_buf(&actionRandomId, 8); NSUInteger peerLayer = [TGDatabaseInstance() peerLayer:_conversationId]; NSData *messageData = [TGModernSendSecretMessageActor decryptedServiceMessageActionWithLayer:MIN(peerLayer, [TGModernSendSecretMessageActor currentLayer]) screenshotMessagesWithRandomIds:randomIds randomId:actionRandomId]; if (messageData != nil) { [TGModernSendSecretMessageActor enqueueOutgoingServiceMessageForPeerId:_conversationId layer:MIN(peerLayer, [TGModernSendSecretMessageActor currentLayer]) keyId:0 randomId:actionRandomId messageData:messageData]; } [ActionStageInstance() dispatchResource:[[NSString alloc] initWithFormat:@"/tg/conversation/(%" PRId64 ")/messageFlagChanges", _conversationId] resource:messageFlagChanges]; } }]; } - (void)markMessagesAsViewed:(NSArray *)messageIds { [TGDatabaseInstance() dispatchOnDatabaseThread:^ { NSMutableArray *readMesageIds = [[NSMutableArray alloc] init]; for (NSNumber *nMessageId in messageIds) { TGMessage *message = [TGDatabaseInstance() loadMessageWithMid:[nMessageId intValue] peerId:_conversationId]; if (!message.outgoing && message.messageLifetime > 0 && message.messageLifetime <= 60 && message.layer >= 17) { bool initiatedCountdown = false; [TGDatabaseInstance() messageCountdownLocalTime:[nMessageId intValue] enqueueIfNotQueued:true initiatedCountdown:&initiatedCountdown]; if (initiatedCountdown) [readMesageIds addObject:nMessageId]; } } if (readMesageIds.count != 0) { [ActionStageInstance() requestActor:@"/tg/service/synchronizeserviceactions/(settings)" options:nil watcher:TGTelegraphInstance]; } } synchronous:false]; } #pragma mark - - (TGMessageModernConversationItem *)_updateMediaStatusData:(TGMessageModernConversationItem *)item { if (item->_message.mediaAttachments.count != 0) { bool canBeRead = false; for (TGMediaAttachment *attachment in item->_message.mediaAttachments) { switch (attachment.type) { case TGImageMediaAttachmentType: case TGVideoMediaAttachmentType: canBeRead = item->_message.messageLifetime > 0 && item->_message.messageLifetime <= 60; break; case TGAudioMediaAttachmentType: canBeRead = true; break; case TGDocumentMediaAttachmentType: for (id attribute in ((TGDocumentMediaAttachment *)attachment).attributes) { if ([attribute isKindOfClass:[TGDocumentAttributeAudio class]]) { canBeRead = ((TGDocumentAttributeAudio *)attribute).isVoice; break; } } break; default: break; } } if (canBeRead) { int flags = [TGDatabaseInstance() secretMessageFlags:item->_message.mid]; NSTimeInterval viewDate = [TGDatabaseInstance() messageCountdownLocalTime:item->_message.mid enqueueIfNotQueued:false initiatedCountdown:NULL]; if (flags != 0 || ABS(viewDate - DBL_EPSILON) > 0.0) [self _setMessageFlagsAndViewDate:item->_message.mid flags:flags viewDate:viewDate]; } } return [super _updateMediaStatusData:item]; } #pragma mark - - (void)actionStageActionRequested:(NSString *)action options:(id)options { if ([action isEqualToString:@"actionPanelAction"]) { NSString *panelAction = options[@"action"]; if ([panelAction isEqualToString:@"deleteAndExit"]) { TGModernConversationController *controller = self.controller; UINavigationController *navigationController = controller.navigationController; NSUInteger index = [navigationController.viewControllers indexOfObject:controller]; if (index != NSNotFound) { [TGAppDelegateInstance.rootController.dialogListController.dialogListCompanion deleteItem:[[TGConversation alloc] initWithConversationId:_conversationId unreadCount:0 serviceUnreadCount:0] animated:false]; [self _dismissController]; } } } [super actionStageActionRequested:action options:options]; } - (void)actionStageResourceDispatched:(NSString *)path resource:(id)resource arguments:(id)arguments { if ([path isEqualToString:[[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/readByDateMessages", _conversationId]]) { int maxDate = [resource[@"maxDate"] intValue]; [TGModernConversationCompanion dispatchOnMessageQueue:^ { NSMutableArray *messageIds = [[NSMutableArray alloc] init]; for (TGMessageModernConversationItem *messageItem in _items) { if (messageItem->_message.deliveryState == TGMessageDeliveryStateDelivered) { if (messageItem->_additionalDate != 0) { if (messageItem->_additionalDate <= maxDate) [messageIds addObject:[[NSNumber alloc] initWithInt:messageItem->_message.mid]]; } else if (messageItem->_message.date <= maxDate) [messageIds addObject:[[NSNumber alloc] initWithInt:messageItem->_message.mid]]; } } if (messageIds.count != 0) [self _updateMessagesRead:messageIds]; }]; } else if ([path isEqualToString:[[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/conversation", _conversationId]]) { TGDispatchOnMainThread(^ { _conversation = ((SGraphObjectNode *)resource).object; [self _updateEncryptionState:_conversation.encryptedData.handshakeState]; [self updateDebugPanel]; }); } else if ([path isEqualToString:[[NSString alloc] initWithFormat:@"/tg/conversation/(%lld)/messageFlagChanges", _conversationId]]) { TGDispatchOnMainThread(^ { [(NSMutableDictionary *)resource enumerateKeysAndObjectsUsingBlock:^(NSNumber *nMessageId, NSNumber *nFlags, __unused BOOL *stop) { [self _setMessageFlags:(int32_t)[nMessageId intValue] flags:[nFlags intValue]]; }]; }); } else if ([path isEqualToString:[[NSString alloc] initWithFormat:@"/tg/conversation/messageViewDateChanges"]]) { TGDispatchOnMainThread(^ { [(NSMutableDictionary *)resource enumerateKeysAndObjectsUsingBlock:^(NSNumber *nMessageId, NSNumber *nViewDate, __unused BOOL *stop) { [self _setMessageViewDate:(int32_t)[nMessageId intValue] viewDate:[nViewDate doubleValue]]; }]; }); } else if ([path hasPrefix:@"/tg/encrypted/messageLifetime/"]) { TGDispatchOnMainThread(^ { _selfDestructTimer = [resource intValue]; _selfDestructTimerView.timerValue = _selfDestructTimer; }); } else if ([path isEqualToString:[[NSString alloc] initWithFormat:@"/tg/peerLayerUpdates/(%" PRId64 ")", _conversationId]]) { [self updateLayer:[resource[@"layer"] unsignedIntegerValue]]; } [super actionStageResourceDispatched:path resource:resource arguments:arguments]; } - (void)controllerDidChangeInputText:(NSString *)inputText { if (self.layer >= 23) [super controllerDidChangeInputText:inputText]; } - (bool)allowExternalContent { return true; } @end