ChatViewHeader.m 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2014-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import <Contacts/Contacts.h>
  21. #import "ChatViewHeader.h"
  22. #import "Contact.h"
  23. #import "ImageData.h"
  24. #import "MWPhotoBrowser.h"
  25. #import "ImageMessage.h"
  26. #import "NibUtil.h"
  27. #import "ChatViewController.h"
  28. #import "EntityManager.h"
  29. #import "RectUtil.h"
  30. #import "BallotListTableViewController.h"
  31. #import "AvatarMaker.h"
  32. #import "HairlineView.h"
  33. #import "VideoCaptionView.h"
  34. #import "FileCaptionView.h"
  35. #import "PhotoCaptionView.h"
  36. #import "MediaBrowserPhoto.h"
  37. #import "MediaBrowserVideo.h"
  38. #import "MediaBrowserFile.h"
  39. #import "StatusNavigationBar.h"
  40. #import "UIDefines.h"
  41. #import "ChatViewSearchHeader.h"
  42. #import "BundleUtil.h"
  43. #import "UIImage+ColoredImage.h"
  44. #import "FileMessagePreview.h"
  45. #import "PreviewActionNavigationController.h"
  46. #import "FeatureMask.h"
  47. #import "ServerConnector.h"
  48. #import "UserSettings.h"
  49. #import "Utils.h"
  50. #import "ImageUtils.h"
  51. #import "Threema-Swift.h"
  52. #define CONVERSATION_KEYPATHS @[@"contact.verificationLevel", @"contact.cnContactId", @"contact.imageData", @"messages", @"displayName", @"groupImage"]
  53. @interface ChatViewHeader () <MWPhotoBrowserDelegate, MWVideoDelegate, MWFileDelegate, ChatViewSearchHeaderDelegate, MaterialShowcaseDelegate>
  54. @property NSArray *mediaMessages;
  55. @property MWPhotoBrowser *photoBrowser;
  56. @property NSMutableArray *callNumbers;
  57. @property NSUInteger deletePhotoIndex;
  58. @property UIScrollView *groupImagesView;
  59. @property EntityManager *entityManager;
  60. @property NSMutableSet *photoSelection;
  61. @property ChatViewSearchHeader *searchView;
  62. @property UIVisualEffectView *effectView;
  63. @property FileMessagePreview *fileMessagPreview;
  64. @property MaterialShowcase *showCase;
  65. @end
  66. @implementation ChatViewHeader
  67. - (void)awakeFromNib {
  68. [self fixLinePosition:self.horizontalDividerLine1];
  69. [self fixLinePosition:self.horizontalDividerLine2];
  70. [self setupButtons];
  71. _callButton.accessibilityLabel = NSLocalizedString(@"call", nil);
  72. _entityManager = [[EntityManager alloc] init];
  73. _searchButton.accessibilityLabel = [BundleUtil localizedStringForKey:@"search"];
  74. _notificationsSettingsButton.accessibilityLabel = NSLocalizedString(@"pushSetting_title", @"");
  75. _threemaTypeIcon.image = [Utils threemaTypeIcon];
  76. [self setupColors];
  77. [super awakeFromNib];
  78. }
  79. - (void)setupColors {
  80. _callButton.tintColor = [Colors main];
  81. _mediaButton.tintColor = [Colors main];
  82. _ballotsButton.tintColor = [Colors main];
  83. UIImage *image = [UIImage imageNamed:@"Search" inColor:[Colors main]];
  84. [_searchButton setImage:image forState:UIControlStateNormal];
  85. PushSetting *pushSetting = [PushSetting findPushSettingForConversation:_conversation];
  86. UIImage *pushSettingIcon = [UIImage imageNamed:@"Bell"];
  87. if (pushSetting) {
  88. pushSettingIcon = [pushSetting imageForPushSetting];
  89. }
  90. [_notificationsSettingsButton setImage:[pushSettingIcon imageWithTint:[Colors main]] forState:UIControlStateNormal];
  91. UIImage *phoneImage = [ImageUtils imageWithImage:[UIImage imageNamed:@"ThreemaPhone" inColor:[Colors main]] scaledToSize:CGSizeMake(30, 30)];
  92. [_callButton setImage:phoneImage forState:UIControlStateNormal];
  93. _callButton.imageView.contentMode = UIViewContentModeCenter;
  94. _callButton.enabled = [UserSettings sharedUserSettings].enableThreemaCall && is64Bit == 1;
  95. _verticalDividerLine1.backgroundColor = [Colors hairline];
  96. _verticalDividerLine2.backgroundColor = [Colors hairline];
  97. _verticalDividerLine3.backgroundColor = [Colors hairline];
  98. _horizontalDividerLine1.backgroundColor = [Colors hairline];
  99. _horizontalDividerLine2.backgroundColor = [Colors hairline];
  100. if (@available(iOS 11.0, *)) {
  101. _avatarButton.accessibilityIgnoresInvertColors = true;
  102. }
  103. [self addBlur];
  104. }
  105. - (void)refresh {
  106. [self setupColors];
  107. [self setup];
  108. }
  109. - (void)cancelSearch {
  110. [_searchView cancelAction:self];
  111. }
  112. - (void)dealloc {
  113. [self removeObservers];
  114. }
  115. - (void)layoutSubviews {
  116. CGFloat currentHeight = CGRectGetHeight(self.frame);
  117. CGFloat height = [self getHeight];
  118. self.frame = [RectUtil setHeightOf:self.frame height:height];
  119. if (_chatViewController.searching == NO) {
  120. _mainView.hidden = NO;
  121. _horizontalDividerLine1.hidden = NO;
  122. [self layoutButtons];
  123. } else {
  124. _mainView.hidden = YES;
  125. _optionalButtonsView.hidden = YES;
  126. _horizontalDividerLine1.hidden = YES;
  127. }
  128. if (currentHeight != height && _delegate != nil) {
  129. [_delegate didChangeHeightTo:height];
  130. }
  131. _searchView.frame = [RectUtil setWidthOf:_searchView.frame width:self.frame.size.width];
  132. }
  133. - (void)layoutButtons {
  134. if ([self showOptionalButtons] == NO) {
  135. _optionalButtonsView.hidden = YES;
  136. } else {
  137. _optionalButtonsView.hidden = NO;
  138. CGFloat x = CGRectGetMaxX(_notificationsSettingsButton.frame);
  139. CGFloat buttonsTotalWidth = CGRectGetWidth(_optionalButtonsView.frame) - _searchButton.frame.size.width - _notificationsSettingsButton.frame.size.width;
  140. if (_mediaButton.hidden) {
  141. _ballotsButton.frame = [RectUtil setWidthOf:_ballotsButton.frame width:buttonsTotalWidth];
  142. _ballotsButton.frame = [RectUtil setXPositionOf:_mediaButton.frame x:x];
  143. } else if (_ballotsButton.hidden) {
  144. _mediaButton.frame = [RectUtil setWidthOf:_mediaButton.frame width:buttonsTotalWidth];
  145. _mediaButton.frame = [RectUtil setXPositionOf:_mediaButton.frame x:x];
  146. } else {
  147. CGFloat buttonWidth = round(buttonsTotalWidth/2.0);
  148. _mediaButton.frame = [RectUtil setWidthOf:_mediaButton.frame width:buttonWidth];
  149. _mediaButton.frame = [RectUtil setXPositionOf:_mediaButton.frame x:x];
  150. _ballotsButton.frame = [RectUtil setWidthOf:_ballotsButton.frame width:buttonWidth];
  151. CGFloat ballotButtonOffset = CGRectGetMaxX(_mediaButton.frame);
  152. _ballotsButton.frame = [RectUtil setPositionOf:_ballotsButton.frame x: ballotButtonOffset y: _ballotsButton.frame.origin.y];
  153. CGFloat verticalDividerLine3Offset = CGRectGetMaxX(_mediaButton.frame);
  154. _verticalDividerLine3.frame = [RectUtil setPositionOf:_verticalDividerLine3.frame x: verticalDividerLine3Offset y: _verticalDividerLine3.frame.origin.y];
  155. }
  156. _verticalDividerLine3.hidden = _ballotsButton.hidden || _mediaButton.hidden;
  157. CGFloat ballotTextRightEdge = CGRectGetMaxX([_ballotsButton titleRectForContentRect:_ballotsButton.frame]);
  158. CGFloat badgeXOffset = fminf(ballotTextRightEdge, CGRectGetMaxX(_ballotsButton.frame) - CGRectGetWidth(_ballotBadge.frame));
  159. if (_mediaButton.hidden == true) {
  160. badgeXOffset = badgeXOffset + (_ballotBadge.frame.size.width/2);
  161. }
  162. _ballotBadge.frame = [RectUtil setXPositionOf:_ballotBadge.frame x:badgeXOffset];
  163. }
  164. if (_conversation.isGroup == true) {
  165. GroupProxy *group = [GroupProxy groupProxyForConversation:_conversation];
  166. _notificationsSettingsButton.enabled = group.isSelfMember;
  167. } else {
  168. _notificationsSettingsButton.enabled = true;
  169. }
  170. }
  171. - (BOOL)resignFirstResponder {
  172. return [_searchView resignFirstResponder];
  173. }
  174. - (BOOL)showOptionalButtons {
  175. return YES;
  176. }
  177. - (CGFloat)getHeight {
  178. if (_searchView) {
  179. return _searchView.frame.size.height;
  180. } else if ([self showOptionalButtons]) {
  181. return CGRectGetMaxY(_optionalButtonsView.frame);
  182. } else {
  183. return CGRectGetHeight(_mainView.frame);
  184. }
  185. }
  186. - (void)fixLinePosition:(UIView*)view {
  187. CGRect frame = view.frame;
  188. frame.origin.y -= 0.5;
  189. view.frame = frame;
  190. }
  191. - (void)addBlur {
  192. if (_effectView) {
  193. [_effectView removeFromSuperview];
  194. _effectView = nil;
  195. }
  196. UIBlurEffectStyle blurStyle = UIBlurEffectStyleExtraLight;
  197. switch ([Colors getTheme]) {
  198. case ColorThemeDark:
  199. case ColorThemeDarkWork:
  200. blurStyle = UIBlurEffectStyleDark;
  201. break;
  202. case ColorThemeLight:
  203. case ColorThemeLightWork:
  204. case ColorThemeUndefined:
  205. break;
  206. }
  207. UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:blurStyle];
  208. _effectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
  209. _effectView.frame = self.bounds;
  210. _effectView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  211. self.wrapperView.backgroundColor = [UIColor clearColor];
  212. [_effectView.contentView addSubview:self.wrapperView];
  213. [self addSubview:_effectView];
  214. }
  215. - (void)setup {
  216. if (_conversation.groupId != nil) {
  217. [self setupForGroup];
  218. } else {
  219. [self setupForIndividual];
  220. }
  221. [self updateBallotButton];
  222. [self updateMediaButton];
  223. [self checkEnableCallButtons];
  224. }
  225. - (void)setupButtons {
  226. [_avatarButton addTarget:self action:@selector(showContactDetails) forControlEvents:UIControlEventTouchUpInside];
  227. [_verificationLevel addTarget:self action:@selector(showContactDetails) forControlEvents:UIControlEventTouchUpInside];
  228. [_mediaButton setTitle:NSLocalizedString(@"media_overview", nil) forState:UIControlStateNormal];
  229. [_ballotsButton setTitle:NSLocalizedStringFromTable(@"ballots", @"Ballot", nil) forState:UIControlStateNormal];
  230. }
  231. - (void)updateMediaButton {
  232. NSInteger numMedia = [_entityManager.entityFetcher countMediaMessagesForConversation:_conversation];
  233. _mediaButton.hidden = numMedia < 1;
  234. [self setNeedsLayout];
  235. }
  236. - (void)updateBallotButton {
  237. NSInteger numBallots = [_entityManager.entityFetcher countBallotsForConversation:_conversation];
  238. if (numBallots > 0) {
  239. NSInteger numOpenBallots = [_entityManager.entityFetcher countOpenBallotsForConversation:_conversation];
  240. if (numOpenBallots > 0) {
  241. _ballotBadge.value = numOpenBallots;
  242. _ballotBadge.hidden = NO;
  243. } else {
  244. _ballotBadge.hidden = YES;
  245. }
  246. _ballotsButton.hidden = NO;
  247. } else {
  248. _ballotBadge.hidden = YES;
  249. _ballotsButton.hidden = YES;
  250. }
  251. [self setNeedsLayout];
  252. }
  253. - (void)setupForGroup {
  254. _verificationLevel.hidden = YES;
  255. _callButton.hidden = YES;
  256. _avatarButton.hidden = YES;
  257. if (_groupImagesView) {
  258. [_groupImagesView removeFromSuperview];
  259. }
  260. UIView *imageContainer = [self makeContactImagesForGroup];
  261. _groupImagesView = [[UIScrollView alloc] initWithFrame: _mainView.bounds];
  262. _groupImagesView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  263. [_groupImagesView addSubview: imageContainer];
  264. _groupImagesView.contentSize = imageContainer.bounds.size;
  265. _groupImagesView.accessibilityLabel = [_conversation sortedMemberNames];
  266. _groupImagesView.accessibilityTraits = UIAccessibilityTraitButton;
  267. _groupImagesView.isAccessibilityElement = YES;
  268. if (@available(iOS 11.0, *)) {
  269. _groupImagesView.accessibilityIgnoresInvertColors = true;
  270. }
  271. //otherwise containing scrollview does not get this event
  272. _groupImagesView.scrollsToTop = NO;
  273. UITapGestureRecognizer *photoTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showContactDetails)];
  274. [_groupImagesView addGestureRecognizer:photoTapGesture];
  275. _groupImagesView.accessibilityIdentifier = @"GroupImageView";
  276. [_mainView addSubview:_groupImagesView];
  277. _threemaTypeIcon.hidden = YES;
  278. [self setNeedsLayout];
  279. }
  280. - (UIView *)makeContactImagesForGroup {
  281. CGFloat width = _avatarButton.bounds.size.width;
  282. CGFloat margin = 6.0f;
  283. NSInteger memberCount = [_conversation.members count];
  284. CGFloat height = _mainView.bounds.size.height;
  285. CGFloat totalWidth = memberCount * width + (memberCount + 1) * margin;
  286. CGRect containerRect = CGRectMake(0.0, 0.0, totalWidth, height);
  287. UIView *imageContainer = [[UIView alloc] initWithFrame: containerRect];
  288. CGRect imageRect = CGRectMake(margin, 0.0, width, width);
  289. imageRect = [RectUtil rect:imageRect centerVerticalIn:imageContainer.frame];
  290. for (Contact *contact in _conversation.sortedMembers) {
  291. if (contact.state.intValue == kStateInvalid)
  292. continue;
  293. UIImageView *imageView = [[UIImageView alloc] initWithFrame:imageRect];
  294. imageView.image = [[AvatarMaker sharedAvatarMaker] avatarForContact:contact size:width masked:YES];
  295. [imageContainer addSubview: imageView];
  296. if (![Utils hideThreemaTypeIconForContact:contact]) {
  297. UIImageView *littleThreemaTypeIcon = [[UIImageView alloc] initWithImage:[Utils threemaTypeIcon]];
  298. littleThreemaTypeIcon.frame = CGRectMake(imageRect.origin.x - 3.0, (imageRect.origin.y + imageRect.size.height) - (imageRect.size.width / 2.5) - 1.0, imageRect.size.width / 2.5, imageRect.size.height / 2.5);
  299. [imageContainer addSubview:littleThreemaTypeIcon];
  300. }
  301. imageRect = [RectUtil offsetRect:imageRect byX:width + margin byY:0.0];
  302. }
  303. return imageContainer;
  304. }
  305. - (void)setupForIndividual {
  306. [_verificationLevel setImage:[_conversation.contact verificationLevelImage] forState:UIControlStateNormal];
  307. _verificationLevel.hidden = NO;
  308. _verificationLevel.accessibilityLabel = [_conversation.contact verificationLevelAccessibilityLabel];
  309. [_avatarButton setImage:[[AvatarMaker sharedAvatarMaker] avatarForContact:_conversation.contact size:_avatarButton.frame.size.width masked:YES] forState:UIControlStateNormal];
  310. _avatarButton.accessibilityLabel = _conversation.contact.displayName;
  311. _callButton.alpha = 1.0;
  312. _callButton.enabled = [UserSettings sharedUserSettings].enableThreemaCall && is64Bit == 1;
  313. _threemaTypeIcon.hidden = [Utils hideThreemaTypeIconForContact:_conversation.contact];
  314. }
  315. - (void)setConversation:(Conversation *)newConversation {
  316. if (_conversation != newConversation) {
  317. [self removeObservers];
  318. _conversation = newConversation;
  319. [self setup];
  320. [self addObservers];
  321. }
  322. }
  323. - (void)addObservers {
  324. for (NSString *keyPath in CONVERSATION_KEYPATHS) {
  325. [_conversation addObserver:self forKeyPath:keyPath options:0 context:nil];
  326. }
  327. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(newMessageReceived:) name:@"ThreemaNewMessageReceived" object:nil];
  328. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(avatarChanged:) name:kNotificationIdentityAvatarChanged object:nil];
  329. // Listen for connection status changes so we can enable/disable the call button
  330. [[ServerConnector sharedServerConnector] addObserver:self forKeyPath:@"connectionState" options:0 context:nil];
  331. }
  332. - (void)removeObservers {
  333. for (NSString *keyPath in CONVERSATION_KEYPATHS) {
  334. [_conversation removeObserver:self forKeyPath:keyPath];
  335. }
  336. @try {
  337. [[ServerConnector sharedServerConnector] removeObserver:self forKeyPath:@"connectionState"];
  338. } @catch(id anException) {
  339. // ServerConnector observer does not exist
  340. }
  341. [[NSNotificationCenter defaultCenter] removeObserver: self];
  342. }
  343. - (void)cleanupMedia {
  344. _mediaMessages = nil;
  345. _photoBrowser = nil;
  346. }
  347. - (void)checkEnableCallButtons {
  348. if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
  349. _callButton.enabled = YES;
  350. } else {
  351. _callButton.enabled = [UserSettings sharedUserSettings].enableThreemaCall && is64Bit == 1 && [ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn;
  352. }
  353. }
  354. - (NSUInteger)mediaSelectionCount {
  355. return _photoSelection.count;
  356. }
  357. - (NSSet *)mediaPhotoSelection {
  358. return _photoSelection;
  359. }
  360. - (void)startVoipCall:(BOOL)withVideo {
  361. if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
  362. VoIPCallUserAction *action = [[VoIPCallUserAction alloc] initWithAction:withVideo ? ActionCallWithVideo : ActionCall contact:self.conversation.contact callId:nil completion:nil];
  363. [[VoIPCallStateManager shared] processUserAction:action];
  364. } else {
  365. if ([UserSettings sharedUserSettings].enableThreemaCall && is64Bit == 1) {
  366. [_chatViewController startVoipCall:withVideo];
  367. }
  368. }
  369. }
  370. - (void)showThreemaVideoCallInfo {
  371. if (_conversation.isGroup == false) {
  372. if ([UserSettings sharedUserSettings].videoCallInChatInfoShown == false && [UserSettings sharedUserSettings].enableVideoCall && self.conversation.contact.isVideoCallAvailable) {
  373. if (_showCase == nil) {
  374. _showCase = [[MaterialShowcase alloc] init];
  375. [_showCase setTargetViewWithView:_callButton];
  376. _showCase.primaryText = [BundleUtil localizedStringForKey:@"call_threema_video_in_chat_info_title"];
  377. _showCase.secondaryText = [BundleUtil localizedStringForKey:@"call_threema_video_in_chat_info_description"];
  378. _showCase.backgroundPromptColor = [Colors main];
  379. _showCase.backgroundPromptColorAlpha = 0.93;
  380. _showCase.primaryTextSize = 24.0;
  381. _showCase.secondaryTextSize = 18.0;
  382. _showCase.primaryTextColor = [Colors white];
  383. _showCase.delegate = self;
  384. }
  385. if (!_chatViewController.showHeader) {
  386. [_chatViewController showHeaderWithDuration:0.3 completion:^(BOOL finished) {
  387. [_showCase showWithAnimated:true hasShadow:true hasSkipButton:false completion:nil];
  388. }];
  389. } else {
  390. [_showCase showWithAnimated:true hasShadow:true hasSkipButton:false completion:nil];
  391. }
  392. }
  393. }
  394. }
  395. #pragma mark - actions
  396. - (IBAction)callAction:(id)sender {
  397. [self startVoipCall:false];
  398. }
  399. - (IBAction)videoCallAction:(id)sender {
  400. [self startVoipCall:true];
  401. }
  402. - (IBAction)mediaAction:(id)sender {
  403. [self showPhotoBrowser];
  404. }
  405. - (IBAction)ballotAction:(id)sender {
  406. UIViewController *viewController = [BallotListTableViewController ballotListViewControllerForConversation: _conversation];
  407. [_chatViewController.navigationController pushViewController:viewController animated:YES];
  408. }
  409. - (IBAction)searchAction:(id)sender {
  410. [self showSearchView:YES];
  411. }
  412. - (IBAction)notificationsSettingsAction:(id)sender {
  413. [_chatViewController openPushSettings];
  414. }
  415. - (void)showSearchView:(BOOL)show {
  416. if (show) {
  417. _searchView = (ChatViewSearchHeader *)[NibUtil loadViewFromNibWithName:@"ChatViewSearchHeader"];
  418. _searchView.chatViewController = _chatViewController;
  419. _searchView.delegate = self;
  420. [_wrapperView addSubview:_searchView];
  421. [self setNeedsLayout];
  422. _chatViewController.searching = YES;
  423. [_searchView becomeFirstResponder];
  424. } else {
  425. _chatViewController.searching = NO;
  426. [_searchView resignFirstResponder];
  427. [_searchView removeFromSuperview];
  428. _searchView = nil;
  429. [self setNeedsLayout];
  430. }
  431. }
  432. - (void)showContactDetails {
  433. if (_conversation.groupId != nil) {
  434. [_chatViewController performSegueWithIdentifier:@"ShowGroupInfo" sender:_chatViewController];
  435. } else {
  436. [_chatViewController performSegueWithIdentifier:@"ShowContact" sender:_chatViewController];
  437. }
  438. }
  439. - (void)prepareMediaMessages {
  440. NSArray *imageMessages = [_entityManager.entityFetcher imageMessagesForConversation: _conversation];
  441. NSArray *videoMessages = [_entityManager.entityFetcher videoMessagesForConversation: _conversation];
  442. NSArray *fileMessages = [_entityManager.entityFetcher fileMessagesForConversation: _conversation];
  443. NSMutableArray *allMediaMessages = [NSMutableArray arrayWithArray:imageMessages];
  444. [allMediaMessages addObjectsFromArray:videoMessages];
  445. [allMediaMessages addObjectsFromArray:fileMessages];
  446. _mediaMessages = [allMediaMessages sortedArrayWithOptions:0 usingComparator:^NSComparisonResult(BaseMessage *msg1, BaseMessage *msg2) {
  447. return [msg1.date compare:msg2.date];
  448. }];
  449. }
  450. - (UIViewController *)getPhotoBrowserAtMessage:(BaseMessage*)msg forPeeking:(BOOL)peeking {
  451. [self prepareMediaMessages];
  452. NSUInteger initialIndex = [_mediaMessages indexOfObject:msg];
  453. if (initialIndex == NSNotFound) {
  454. initialIndex = 0;
  455. }
  456. [self setupPhotoBrowser];
  457. _photoBrowser.enableSwipeToDismiss = YES;
  458. _photoBrowser.currentPhotoIndex = initialIndex;
  459. if (peeking) {
  460. _photoBrowser.peeking = YES;
  461. if (@available(iOS 13.0, *)) {
  462. return _photoBrowser;
  463. }
  464. }
  465. UINavigationController *navigationController = [[PreviewActionNavigationController alloc] initWithRootViewController:_photoBrowser];
  466. return navigationController;
  467. }
  468. - (void)showPhotoBrowser {
  469. [self prepareMediaMessages];
  470. [self setupPhotoBrowser];
  471. _photoBrowser.startOnGrid = [_mediaMessages count] > 1;
  472. [_photoBrowser setCurrentPhotoIndex:[_mediaMessages count]];
  473. UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:_photoBrowser];
  474. navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
  475. [_chatViewController presentViewController:navigationController animated:YES completion:nil];
  476. }
  477. - (void)setupPhotoBrowser {
  478. _photoBrowser = [[MWPhotoBrowser alloc] initWithDelegate:self];
  479. _photoBrowser.displayDeleteButton = YES;
  480. _photoSelection = [NSMutableSet set];
  481. }
  482. - (NSURL*)makeTelUrlForPhone:(NSString*)phoneNumber {
  483. return [NSURL URLWithString:[NSString stringWithFormat:@"tel:%@", [phoneNumber stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]];
  484. }
  485. #pragma mark - key value observer
  486. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  487. dispatch_async(dispatch_get_main_queue(), ^{
  488. if (object == _conversation) {
  489. [self setup];
  490. }
  491. if (object == [ServerConnector sharedServerConnector] && [keyPath isEqualToString:@"connectionState"]) {
  492. [self checkEnableCallButtons];
  493. }
  494. });
  495. }
  496. - (void)newMessageReceived:(NSNotification*)notification {
  497. [self setup];
  498. }
  499. #pragma mark - Photo browser delegate
  500. - (NSUInteger)numberOfPhotosInPhotoBrowser:(MWPhotoBrowser *)photoBrowser {
  501. return _mediaMessages.count;
  502. }
  503. - (id<MWPhoto>)photoBrowser:(MWPhotoBrowser *)photoBrowser photoAtIndex:(NSUInteger)index {
  504. return [self mwPhotoAtIndex:index forThumbnail:NO];
  505. }
  506. - (id<MWPhoto>)photoBrowser:(MWPhotoBrowser *)photoBrowser thumbPhotoAtIndex:(NSUInteger)index {
  507. id<MWPhoto> photo = [self mwPhotoAtIndex:index forThumbnail:YES];
  508. [photo loadUnderlyingImageAndNotify]; // ensure the underlying image is set to keep loading indicator from appearing
  509. return photo;
  510. }
  511. - (id<MWPhoto>)mwPhotoAtIndex:(NSUInteger)index forThumbnail:(BOOL)thumbnail {
  512. id<MWPhoto> media = nil;
  513. if (index < _mediaMessages.count) {
  514. BaseMessage *message = _mediaMessages[index];
  515. if ([message isKindOfClass:[VideoMessage class]]) {
  516. MediaBrowserVideo *video = [MediaBrowserVideo videoWithThumbnail: ((VideoMessage *)message).thumbnail.uiImage];
  517. video.delegate = self;
  518. video.sourceReference = (VideoMessage *)message;
  519. video.caption = [DateFormatter shortStyleDateTime:message.remoteSentDate];
  520. media = video;
  521. } else if ([message isKindOfClass:[ImageMessage class]]) {
  522. MediaBrowserPhoto *photo = [MediaBrowserPhoto photoWithImageMessage:(ImageMessage*)message thumbnail:thumbnail];
  523. photo.caption = [DateFormatter shortStyleDateTime:message.remoteSentDate];
  524. media = photo;
  525. } else if ([message isKindOfClass:[FileMessage class]]) {
  526. MediaBrowserFile *file;
  527. file = [MediaBrowserFile fileWithFileMessage:(FileMessage *)message thumbnail:thumbnail];
  528. file.delegate = self;
  529. file.caption = [DateFormatter shortStyleDateTime:message.remoteSentDate];
  530. media = file;
  531. }
  532. }
  533. return media;
  534. }
  535. - (MWCaptionView *)photoBrowser:(MWPhotoBrowser *)photoBrowser captionViewForPhotoAtIndex:(NSUInteger)index {
  536. id<MWPhoto> media = [photoBrowser photoAtIndex: index];
  537. if ([media isKindOfClass:[MediaBrowserVideo class]]) {
  538. VideoCaptionView *videoCaption = [[VideoCaptionView alloc] initWithPhoto:media];
  539. return videoCaption;
  540. } else if ([media isKindOfClass:[MediaBrowserPhoto class]]) {
  541. PhotoCaptionView *photoCaptionView = [[PhotoCaptionView alloc] initWithPhoto:media];
  542. return photoCaptionView;
  543. } else if ([media isKindOfClass:[MediaBrowserFile class]]) {
  544. FileCaptionView *fileCaptionView = [[FileCaptionView alloc] initWithPhoto:media];
  545. return fileCaptionView;
  546. } else {
  547. return nil;
  548. }
  549. }
  550. - (void)photoBrowser:(MWPhotoBrowser *)photoBrowser deleteButton:(UIBarButtonItem *)deleteButton pressedForPhotoAtIndex:(NSUInteger)index {
  551. _deletePhotoIndex = index;
  552. id<MWPhoto> media = [photoBrowser photoAtIndex: index];
  553. NSString *deleteButtonTitle = nil;
  554. if ([media isKindOfClass:[MediaBrowserVideo class]]) {
  555. deleteButtonTitle = NSLocalizedString(@"delete_video", nil);
  556. } else if ([media isKindOfClass:[MediaBrowserPhoto class]]) {
  557. deleteButtonTitle = NSLocalizedString(@"delete_photo", nil);
  558. } else if ([media isKindOfClass:[MediaBrowserFile class]]) {
  559. deleteButtonTitle = NSLocalizedString(@"delete_file", nil);
  560. }
  561. UIAlertController *deletePhotoActionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
  562. [deletePhotoActionSheet addAction:[UIAlertAction actionWithTitle:deleteButtonTitle style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
  563. [_entityManager performSyncBlockAndSafe:^{
  564. ImageMessage *imageMessage = _mediaMessages[_deletePhotoIndex];
  565. imageMessage.conversation = nil;
  566. [[_entityManager entityDestroyer] deleteObjectWithObject:imageMessage];
  567. [_chatViewController updateConversationLastMessage];
  568. }];
  569. [_chatViewController updateConversation];
  570. [self prepareMediaMessages];
  571. if (_mediaMessages.count == 0) {
  572. [_chatViewController dismissViewControllerAnimated:YES completion:nil];
  573. } else {
  574. [_photoBrowser reloadData:true];
  575. }
  576. }]];
  577. [deletePhotoActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
  578. if (SYSTEM_IS_IPAD) {
  579. deletePhotoActionSheet.popoverPresentationController.barButtonItem = deleteButton;
  580. }
  581. [photoBrowser presentViewController:deletePhotoActionSheet animated:YES completion:nil];
  582. }
  583. - (BOOL)photoBrowser:(MWPhotoBrowser *)photoBrowser isPhotoSelectedAtIndex:(NSUInteger)index {
  584. return [_photoSelection containsObject:[NSNumber numberWithInteger:index]];
  585. }
  586. - (void)photoBrowser:(MWPhotoBrowser *)photoBrowser photoAtIndex:(NSUInteger)index selectedChanged:(BOOL)selected {
  587. if (selected) {
  588. [_photoSelection addObject:[NSNumber numberWithInteger:index]];
  589. } else {
  590. [_photoSelection removeObject:[NSNumber numberWithInteger:index]];
  591. }
  592. }
  593. - (void)photoBrowserResetSelection:(MWPhotoBrowser *)photoBrowser {
  594. [_photoSelection removeAllObjects];
  595. }
  596. - (void)photoBrowserSelectAll:(MWPhotoBrowser *)photoBrowser {
  597. [_photoSelection removeAllObjects];
  598. for (int i = 0; i < _mediaMessages.count; i++) {
  599. [_photoSelection addObject:[NSNumber numberWithInt:i]];
  600. }
  601. }
  602. - (void)photoBrowser:(MWPhotoBrowser *)photoBrowser deleteButton:(UIBarButtonItem *)deleteButton {
  603. if (_photoSelection.count > 0) {
  604. _chatViewController.deleteMediaTotal = (int)_photoSelection.count;
  605. [_entityManager performSyncBlockAndSafe:^{
  606. [_photoSelection enumerateObjectsUsingBlock:^(NSNumber *index, BOOL * _Nonnull stop) {
  607. ImageMessage *imageMessage = _mediaMessages[[index integerValue]];
  608. imageMessage.conversation = nil;
  609. [[_entityManager entityDestroyer] deleteObjectWithObject:imageMessage];
  610. }];
  611. [_chatViewController updateConversationLastMessage];
  612. }];
  613. } else {
  614. _chatViewController.deleteMediaTotal = (int)[self numberOfPhotosInPhotoBrowser:photoBrowser];
  615. [_entityManager performSyncBlockAndSafe:^{
  616. for (int i = 0; i < [self numberOfPhotosInPhotoBrowser:photoBrowser]; i++ ) {
  617. ImageMessage *imageMessage = _mediaMessages[i];
  618. imageMessage.conversation = nil;
  619. [[_entityManager entityDestroyer] deleteObjectWithObject:imageMessage];
  620. };
  621. [_chatViewController updateConversationLastMessage];
  622. }];
  623. }
  624. [_chatViewController updateConversation];
  625. [self prepareMediaMessages];
  626. if (_mediaMessages.count == 0) {
  627. [_chatViewController dismissViewControllerAnimated:YES completion:nil];
  628. } else {
  629. [_photoBrowser finishedDeleteMedia];
  630. }
  631. }
  632. - (void)photoBrowser:(MWPhotoBrowser *)photoBrowser actionButtonPressedForPhotoAtIndex:(NSUInteger)index {
  633. MWPhoto *item;
  634. if ([_mediaMessages[index] isKindOfClass:[FileMessage class]]) {
  635. FileMessage *fileMessage = _mediaMessages[index];
  636. if (fileMessage.data != nil) {
  637. item = [photoBrowser photoAtIndex:index];
  638. }
  639. }
  640. else if ([_mediaMessages[index] isKindOfClass:[ImageMessage class]]) {
  641. ImageMessage *imageMessage = _mediaMessages[index];
  642. if (imageMessage.image != nil) {
  643. item = [photoBrowser photoAtIndex:index];
  644. }
  645. }
  646. else if ([_mediaMessages[index] isKindOfClass:[VideoMessage class]]) {
  647. VideoMessage *videoMessage = _mediaMessages[index];
  648. if (videoMessage.video != nil) {
  649. item = [photoBrowser photoAtIndex:index];
  650. }
  651. }
  652. if (item != nil) {
  653. [photoBrowser shareMedia:item];
  654. }
  655. else {
  656. [photoBrowser showAlert:@"" message:[BundleUtil localizedStringForKey:@"media_file_not_found"]];
  657. }
  658. }
  659. #pragma mark - MWVideoDelegate
  660. - (void)playVideo:(MediaBrowserVideo *)video {
  661. VideoMessage *message = (VideoMessage *)video.sourceReference;
  662. if (message) {
  663. [_chatViewController videoMessageTapped:message];
  664. }
  665. }
  666. #pragma mark - MWFileDelegate
  667. - (void)showFile:(FileMessage *)fileMessage {
  668. if (fileMessage) {
  669. _fileMessagPreview = [FileMessagePreview fileMessagePreviewFor:fileMessage];
  670. [_fileMessagPreview showOn:_photoBrowser];
  671. }
  672. }
  673. - (void)playFileVideo:(FileMessage *)fileMessage {
  674. if (fileMessage) {
  675. [_chatViewController fileVideoMessageTapped:fileMessage];
  676. }
  677. }
  678. - (void)toggleControls {
  679. [_photoBrowser toggleControls];
  680. }
  681. #pragma mark - ChatViewSearchHeaderDelegate
  682. - (void)didCancelSearch {
  683. [self showSearchView:NO];
  684. }
  685. #pragma mark - MaterialShowCaseDelegate
  686. - (void)showCaseDidDismissWithShowcase:(MaterialShowcase *)showcase didTapTarget:(BOOL)didTapTarget {
  687. [[UserSettings sharedUserSettings] setVideoCallInChatInfoShown:true];
  688. }
  689. #pragma mark - Notification
  690. - (void)avatarChanged:(NSNotification*)notification
  691. {
  692. if (notification.object && [self needsUpdateAvatarsForNotification:notification]) {
  693. [self setup];
  694. }
  695. }
  696. - (BOOL)needsUpdateAvatarsForNotification:(NSNotification *)notification {
  697. if (_conversation.isGroup) {
  698. for (Contact *contact in _conversation.members) {
  699. if ([contact.identity isEqualToString:notification.object]) {
  700. return YES;
  701. }
  702. }
  703. return NO;
  704. } else {
  705. return [_conversation.contact.identity isEqualToString:notification.object];
  706. }
  707. }
  708. @end