ChatViewController.m 112 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-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 SafariServices;
  21. #import <AVKit/AVKit.h>
  22. #import <UIKit/UIKit.h>
  23. #import "ChatViewController.h"
  24. #import "AppDelegate.h"
  25. #import "ChatBar.h"
  26. #import "ChatDefines.h"
  27. #import "MessageSender.h"
  28. #import "ContactDetailsViewController.h"
  29. #import <QuartzCore/QuartzCore.h>
  30. #import <MediaPlayer/MediaPlayer.h>
  31. #import "ProtocolDefines.h"
  32. #import "UserSettings.h"
  33. #import "VideoMessageLoader.h"
  34. #import "PreviewImageViewController.h"
  35. #import "LocationViewController.h"
  36. #import "MessageDetailsViewController.h"
  37. #import "ImageMessageLoader.h"
  38. #import "GroupDetailsViewController.h"
  39. #import "PlayRecordAudioViewController.h"
  40. #import "NonFirstResponderActionSheet.h"
  41. #import "EntityManager.h"
  42. #import "BallotDispatcher.h"
  43. #import "RectUtil.h"
  44. #import "MessageFetcher.h"
  45. #import "PermissionChecker.h"
  46. #import "StatusNavigationBar.h"
  47. #import "ModalPresenter.h"
  48. #import "BundleUtil.h"
  49. #import "DocumentPicker.h"
  50. #import "Utils.h"
  51. #import "ChatMessageCell.h"
  52. #import "ModalNavigationController.h"
  53. #import "LicenseStore.h"
  54. #import "FeatureMask.h"
  55. #import "MessageDraftStore.h"
  56. #import "MWPhotoBrowser.h"
  57. #import "AppGroup.h"
  58. #import "VoIPHelper.h"
  59. #import "NotificationManager.h"
  60. #import "NSString+Hex.h"
  61. #import "NibUtil.h"
  62. #import "ChatDeleteAction.h"
  63. #import "SendMediaAction.h"
  64. #import "SendLocationAction.h"
  65. #import "ChatTableDataSource.h"
  66. #import "BallotResultViewController.h"
  67. #import "BallotVoteViewController.h"
  68. #import "QuoteParser.h"
  69. #import "FeatureMask.h"
  70. #import "Threema-Swift.h"
  71. #ifdef DEBUG
  72. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  73. #else
  74. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  75. #endif
  76. @interface ChatViewController () <UIViewControllerPreviewingDelegate, PPAssetsActionHelperDelegate, GroupDetailsViewControllerDelegate>
  77. @property ChatTableDataSource *tableDataSource;
  78. @property UIImageView *backgroundView;
  79. @property BOOL isDirty;
  80. @property NSMutableArray *imageMessageObserverList;
  81. @property NSMutableArray *locationMessageObserverList;
  82. @property UILabel *titleLabel;
  83. @end
  84. @implementation ChatViewController {
  85. BOOL visible;
  86. BOOL shouldScrollDown;
  87. UIView *containerView;
  88. UIView *chatBarWrapper;
  89. CGFloat wrapperBottomPadding;
  90. NSMutableArray *readReceiptQueue;
  91. BOOL inhibitScrollBottom;
  92. BOOL haveNewMessages;
  93. BOOL typingIndicatorSent;
  94. UIButton *scrollDownButton;
  95. LocationMessage* locationToShow;
  96. int numberOfPages;
  97. BaseMessage *detailsMessage;
  98. NSURL *tmpAudioVideoUrl;
  99. NSIndexPath *lastIndexPathBeforeRotation;
  100. UIInterfaceOrientation lastInterfaceOrientation;
  101. NSString *prevAudioCategory;
  102. AVPlayerViewController *player;
  103. NSString *initialMessageText;
  104. BOOL ignoreNextTap;
  105. MessageFetcher *messageFetcher;
  106. PlayRecordAudioViewController *audioRecorder;
  107. CGFloat lastKeyboardHeight;
  108. BOOL isScrollingToTop;
  109. BOOL isScrollingToUnreadMessages;
  110. BOOL isNewMessageReceivedInActiveChat;
  111. BOOL isFirstAppearance;
  112. CGPoint lastScrollOffset;
  113. EntityManager *entityManager;
  114. ChatViewControllerAction *currentAction;
  115. NSInteger currentOffset;
  116. BOOL forceTouching;
  117. NSIndexPath *selectedAudioMessage;
  118. PPAssetsActionHelper *assetActionHelper;
  119. CGRect lastKeyboardEndFrame;
  120. NSTimeInterval lastAnimationDuration;
  121. UIViewAnimationCurve lastAnimationCurve;
  122. BOOL _cancelShowQuotedMessage;
  123. UITapGestureRecognizer *tapGestureRecognizer;
  124. int _deleteMediaCount;
  125. /// When was the table fully reloaded last time?
  126. NSDate *lastFullConversationUpdate;
  127. BOOL _assetActionHelperWillPresent;
  128. }
  129. @synthesize sentMessageSound;
  130. @synthesize chatContent;
  131. @synthesize chatBar;
  132. @synthesize headerView;
  133. @synthesize conversation;
  134. @synthesize imageDataToSend;
  135. @synthesize deleteMediaTotal;
  136. @synthesize showHeader;
  137. #pragma mark NSObject
  138. - (void)dealloc {
  139. for (ImageMessage *message in _imageMessageObserverList) {
  140. [message removeObserver:self forKeyPath:@"thumbnail"];
  141. }
  142. [_imageMessageObserverList removeAllObjects];
  143. for (LocationMessage *message in _locationMessageObserverList) {
  144. [message removeObserver:self forKeyPath:@"reverseGeocodingResult"];
  145. }
  146. [_locationMessageObserverList removeAllObjects];
  147. if (sentMessageSound) {
  148. AudioServicesDisposeSystemSoundID(sentMessageSound);
  149. }
  150. [self removeConversationObservers];
  151. chatContent.delegate = nil;
  152. chatContent.dataSource = nil;
  153. chatBar.delegate = nil;
  154. _tableDataSource = nil;
  155. [[NSNotificationCenter defaultCenter] removeObserver:self];
  156. }
  157. - (id)initWithCoder:(NSCoder *)aDecoder {
  158. self = [super initWithCoder:aDecoder];
  159. if (self) {
  160. readReceiptQueue = [NSMutableArray array];
  161. entityManager = [[EntityManager alloc] init];
  162. _imageMessageObserverList = [NSMutableArray new];
  163. _locationMessageObserverList = [NSMutableArray new];
  164. _isOpenWithForceTouch = NO;
  165. _assetActionHelperWillPresent = false;
  166. }
  167. return self;
  168. }
  169. - (BOOL)shouldAutorotate {
  170. return NO;
  171. }
  172. - (void)setSearching:(BOOL)searching {
  173. _searching = searching;
  174. _tableDataSource.searching = searching;
  175. CGFloat barHeight = 0.0;
  176. if (_searching) {
  177. chatBarWrapper.hidden = YES;
  178. } else {
  179. barHeight = chatBarWrapper.frame.size.height;
  180. chatBarWrapper.hidden = NO;
  181. }
  182. chatContent.frame = [RectUtil setHeightOf:chatContent.frame height: containerView.frame.size.height - barHeight - [self tabBarHeight]];
  183. }
  184. - (void)setSearchPattern:(NSString *)searchPattern {
  185. _searchPattern = searchPattern;
  186. _tableDataSource.searchPattern = searchPattern;
  187. }
  188. - (NSIndexPath *)indexPathForMessage:(BaseMessage *)message {
  189. return [_tableDataSource indexPathForMessage:message];
  190. }
  191. - (id)objectAtIndexPath:(NSIndexPath *)indexPath {
  192. return [_tableDataSource objectForIndexPath:indexPath];
  193. }
  194. - (BOOL)hasAlpha : (UIImage*) img {
  195. CGImageAlphaInfo alpha = CGImageGetAlphaInfo(img.CGImage);
  196. return (
  197. alpha == kCGImageAlphaFirst ||
  198. alpha == kCGImageAlphaLast ||
  199. alpha == kCGImageAlphaPremultipliedFirst ||
  200. alpha == kCGImageAlphaPremultipliedLast
  201. );
  202. }
  203. #pragma mark UIViewController
  204. - (void)viewWillLayoutSubviews {
  205. UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;
  206. if (self.view.frame.size.width > self.view.frame.size.height) {
  207. orientation = UIInterfaceOrientationLandscapeLeft;
  208. }
  209. [self updateConversationClearContent:NO];
  210. [self updateBackgroundForOrientation:orientation duration:0.0];
  211. }
  212. - (void)viewDidLayoutSubviews {
  213. CGFloat top;
  214. if (@available(iOS 11.0, *)) {
  215. top = self.view.safeAreaLayoutGuide.layoutFrame.origin.y;
  216. } else {
  217. top = self.topLayoutGuide.length;
  218. }
  219. if (showHeader) {
  220. headerView.frame = [RectUtil setYPositionOf:headerView.frame y:top];
  221. }
  222. // self.topLayoutGuide is only available after view was added -> make sure offset is set
  223. [self updateChatContentInset];
  224. }
  225. - (void)viewDidLoad {
  226. [super viewDidLoad];
  227. [[UserSettings sharedUserSettings] checkWallpaper];
  228. self.navigationController.interactivePopGestureRecognizer.enabled = YES;
  229. self.navigationController.interactivePopGestureRecognizer.delegate = nil;
  230. /* Load sounds */
  231. NSString *sendPath = [BundleUtil pathForResource:@"sent_message" ofType:@"caf"];
  232. CFURLRef baseURL = (__bridge CFURLRef)[NSURL fileURLWithPath:sendPath];
  233. AudioServicesCreateSystemSoundID(baseURL, &sentMessageSound);
  234. self.navigationController.tabBarItem.image = [UIImage imageNamed:@"TabBar-Chats"];
  235. self.navigationController.tabBarItem.selectedImage = [UIImage imageNamed:@"TabBar-Chats"];
  236. self.automaticallyAdjustsScrollViewInsets = NO;
  237. self.view.backgroundColor = [Colors backgroundChat]; // shown during rotation
  238. if (@available(iOS 11.0, *)) {
  239. containerView = [[UIView alloc] initWithFrame:self.view.safeAreaLayoutGuide.layoutFrame];
  240. } else {
  241. containerView = [[UIView alloc] initWithFrame:self.view.frame];
  242. }
  243. containerView.backgroundColor = [UIColor clearColor];
  244. containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  245. [self.view addSubview:containerView];
  246. // Calculate initial height based on font size (ugly hack)
  247. float fontSize = [UserSettings sharedUserSettings].chatFontSize;
  248. float initialChatBarHeight = kChatBarHeight1;
  249. if (fontSize >= 36)
  250. initialChatBarHeight = 64.0;
  251. else if (fontSize >= 30)
  252. initialChatBarHeight = 57.0;
  253. else if (fontSize >= 28)
  254. initialChatBarHeight = 55.0;
  255. else if (fontSize >= 24)
  256. initialChatBarHeight = 50.0;
  257. else if (fontSize >= 20)
  258. initialChatBarHeight = 45.0;
  259. CGFloat initialChatBarWrapperPadding = 0.0f;
  260. if ([AppDelegate hasBottomSafeAreaInsets]) {
  261. initialChatBarWrapperPadding += kIphoneXChatBarBottomPadding;
  262. wrapperBottomPadding = kIphoneXChatBarBottomPadding;
  263. }
  264. // Create chatContent
  265. CGFloat chatContentHeight = containerView.frame.size.height - initialChatBarHeight - [self tabBarHeight] - initialChatBarWrapperPadding;
  266. CGRect chatContectRect = CGRectMake(0.0f, 0.0f, containerView.frame.size.width, chatContentHeight);
  267. chatContent = [[UITableView alloc] initWithFrame:chatContectRect];
  268. chatContent.clearsContextBeforeDrawing = NO;
  269. chatContent.backgroundColor = [UIColor clearColor];
  270. chatContent.separatorStyle = UITableViewCellSeparatorStyleNone;
  271. chatContent.separatorColor = [UIColor clearColor];
  272. chatContent.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  273. chatContent.allowsSelection = NO;
  274. chatContent.allowsSelectionDuringEditing = YES;
  275. chatContent.allowsMultipleSelectionDuringEditing = YES;
  276. [chatContent registerNib:[UINib nibWithNibName:@"UnreadMessageLineCell" bundle:nil] forCellReuseIdentifier:@"UnreadMessageLineCell"];
  277. tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(chatContentTapped:)];
  278. tapGestureRecognizer.numberOfTapsRequired = 1;
  279. tapGestureRecognizer.delaysTouchesEnded = false;
  280. tapGestureRecognizer.cancelsTouchesInView = false;
  281. [chatContent addGestureRecognizer:tapGestureRecognizer];
  282. [containerView addSubview:chatContent];
  283. [self setupHeaderView];
  284. chatContent.tableHeaderView = self.chatContentHeader;
  285. [self updateChatContentInset];
  286. CGRect chatBarWrapperRect = CGRectMake(0.0f, chatContentHeight, containerView.frame.size.width, initialChatBarHeight + initialChatBarWrapperPadding);
  287. chatBarWrapper = [[UIView alloc] initWithFrame:chatBarWrapperRect];
  288. chatBarWrapper.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
  289. CGRect chatBarRect = CGRectMake(0.0f, 0.0f, chatBarWrapperRect.size.width, initialChatBarHeight);
  290. chatBar = [[ChatBar alloc] initWithFrame: chatBarRect];
  291. chatBar.delegate = self;
  292. chatBar.canSendAudio = [PlayRecordAudioViewController canRecordAudio];
  293. if (conversation.isGroup == true) {
  294. [chatBar setupMentions:conversation.sortedMembers];
  295. }
  296. /* Put chat bar in a wrapper so we can adjust the bottom offset for iPhone X */
  297. [chatBarWrapper addSubview:chatBar];
  298. [containerView addSubview:chatBarWrapper];
  299. [containerView sendSubviewToBack:chatBarWrapper];
  300. [self setupNavigationBar];
  301. /* Scroll down button */
  302. scrollDownButton = [UIButton buttonWithType:UIButtonTypeCustom];
  303. [scrollDownButton setAccessibilityLabel:NSLocalizedString(@"scoll_down_text", @"")];
  304. [scrollDownButton addTarget:self action:@selector(scrollDownButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
  305. [containerView addSubview:scrollDownButton];
  306. isFirstAppearance = YES;
  307. [self updateContactDisplay];
  308. lastInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
  309. [self registerForPreviewingWithDelegate:self sourceView:self.view];
  310. [self setupColors];
  311. self.deleteMediaTotal = 0;
  312. _deleteMediaCount = 0;
  313. }
  314. - (void)setupHeaderView {
  315. headerView = (ChatViewHeader *)[NibUtil loadViewFromNibWithName:@"ChatViewHeader"];
  316. headerView.chatViewController = self;
  317. headerView.hidden = YES;
  318. headerView.delegate = self;
  319. [self.view addSubview: headerView];
  320. }
  321. - (void)setupNavigationBar {
  322. self.navigationItem.rightBarButtonItems = @[self.editButtonItem];
  323. _titleLabel = [[UILabel alloc] init];
  324. _titleLabel.font = [UIFont boldSystemFontOfSize:17.0f];
  325. _titleLabel.frame = CGRectMake(0, 0, 40, 28);
  326. UITapGestureRecognizer *titleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(titleTapped:)];
  327. [_titleLabel addGestureRecognizer:titleTapRecognizer];
  328. _titleLabel.userInteractionEnabled = YES;
  329. _titleLabel.text = conversation.displayName;
  330. _titleLabel.accessibilityIdentifier = @"TapHeaderView";
  331. self.navigationItem.titleView = _titleLabel;
  332. }
  333. - (void)setupColors {
  334. _titleLabel.textColor = [Colors fontNormal];
  335. self.loadEarlierMessages.titleLabel.font = [UIFont systemFontOfSize:17.0];
  336. self.loadEarlierMessages.backgroundColor = [[Colors backgroundBaseColor] colorWithAlphaComponent:0.3];
  337. self.loadEarlierMessages.layer.cornerRadius = 4.0;
  338. [self.loadEarlierMessages setTitleColor:[Colors fontLink] forState:UIControlStateNormal];
  339. [self.loadEarlierMessages setTitleColor:[Colors fontLight] forState:UIControlStateHighlighted];
  340. [self.navigationController.view setBackgroundColor:[Colors backgroundDark]];
  341. chatBarWrapper.backgroundColor = [Colors chatBarBackground];
  342. [self.view setBackgroundColor:[Colors backgroundChat]];
  343. // Set scroll down button image
  344. [scrollDownButton setImage:StyleKit.scrollDownButtonIcon forState:UIControlStateNormal];
  345. }
  346. - (void)refresh {
  347. [self setupBackground];
  348. [self setupColors];
  349. [headerView refresh];
  350. [chatBar refresh];
  351. [self.chatContent reloadData];
  352. self.navigationController.navigationBar.topItem.leftBarButtonItem.title = @"Back";
  353. }
  354. - (void)viewWillAppear:(BOOL)animated {
  355. [super viewWillAppear:animated]; // below: work around for [chatContent flashScrollIndicators]
  356. _tableDataSource.openTableView = YES;
  357. DDLogVerbose(@"viewWillAppear, composing = %d", self.composing);
  358. [self registerForNotifications];
  359. [self updateConversationIfNeeded];
  360. [chatContent performSelector:@selector(flashScrollIndicators) withObject:nil afterDelay:0.0];
  361. /* update typing indicator on last cell */
  362. if ([_tableDataSource hasData]) {
  363. NSIndexPath *pathToLastCell = [_tableDataSource indexPathForLastCell];
  364. [self updateTypingIndicatorAtIndexPath:pathToLastCell];
  365. }
  366. if (initialMessageText) {
  367. chatBar.text = initialMessageText;
  368. initialMessageText = nil;
  369. }
  370. /* remove temporary audio/video file? */
  371. if (tmpAudioVideoUrl != nil) {
  372. NSError *error = nil;
  373. [[NSFileManager defaultManager] removeItemAtURL:tmpAudioVideoUrl error:&error];
  374. DDLogVerbose(@"Removing temporary audio/video file %@: %@", tmpAudioVideoUrl, error);
  375. tmpAudioVideoUrl = nil;
  376. }
  377. if (player != nil) {
  378. player = nil;
  379. }
  380. /* was there a rotation while we were hidden? */
  381. if ([[UIApplication sharedApplication] statusBarOrientation] != lastInterfaceOrientation) {
  382. [self updateTableForRotationToInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
  383. dispatch_async(dispatch_get_main_queue(), ^{
  384. /* Workaround as chatContent.frame won't be updated yet when we reposition the button below */
  385. [self repositionScrollDownButton];
  386. });
  387. }
  388. [self repositionScrollDownButton];
  389. [self updateScrollDownButtonAnimated:NO];
  390. /* send notification (e.g. for hiding toasts that apply to this conversation) */
  391. [[NSNotificationCenter defaultCenter] postNotificationName:@"ThreemaConversationOpened" object:conversation userInfo:nil];
  392. [self registerCustomMenuItems];
  393. isFirstAppearance = NO;
  394. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showProfilePictureChanged:) name:kNotificationShowProfilePictureChanged object:nil];
  395. /* Load draft, if any */
  396. NSString *draft = [MessageDraftStore loadDraftForConversation:self.conversation];
  397. if (draft.length != 0 && chatBar.text.length == 0) {
  398. [chatBar updateMentionsFromDraft:draft];
  399. // chatBar.text = mentionsString;
  400. if ([self canBecomeFirstResponder]) {
  401. [chatBar becomeFirstResponder];
  402. }
  403. }
  404. /* correct the width of the headerView */
  405. if (@available(iOS 11.0, *)) {
  406. headerView.frame = CGRectMake(headerView.frame.origin.x, headerView.frame.origin.y, self.view.safeAreaLayoutGuide.layoutFrame.size.width, headerView.frame.size.height);
  407. } else {
  408. headerView.frame = CGRectMake(headerView.frame.origin.x, headerView.frame.origin.y, self.view.frame.size.width, headerView.frame.size.height);
  409. }
  410. // Remove unread line if unread count is 0
  411. if (conversation.unreadMessageCount.integerValue == 0) {
  412. [self removeUnreadLine:NO];
  413. }
  414. [headerView refresh];
  415. if (!_backgroundView) {
  416. [self setupBackground];
  417. }
  418. if (headerView.hidden) {
  419. [self hideHeaderWithDuration:0.3];
  420. } else {
  421. [self showHeaderWithDuration:0.3 completion:nil];
  422. }
  423. [self setupNavigationBar];
  424. [self setupColors];
  425. [self loadImagesIfNeeded];
  426. if (SYSTEM_IS_IPAD == true) {
  427. [_delegate pushSettingChanged:self.conversation];
  428. }
  429. [chatBar setupMentions:conversation.sortedMembers];
  430. }
  431. - (void)registerForNotifications {
  432. // Listen for keyboard.
  433. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
  434. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
  435. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputModeDidChange:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
  436. // Listen for resign active notification
  437. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resignActive:) name:UIApplicationWillResignActiveNotification object:nil];
  438. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
  439. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuWillHide:) name:UIMenuControllerWillHideMenuNotification object:nil];
  440. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuDidHide:) name:UIMenuControllerDidHideMenuNotification object:nil];
  441. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDirtyObjects:) name:kNotificationDBRefreshedDirtyObject object:nil];
  442. }
  443. - (void)showKeyboardConditionally {
  444. if (self.composing) {
  445. if ([self canBecomeFirstResponder]) {
  446. [chatBar becomeFirstResponder];
  447. }
  448. }
  449. }
  450. - (void)hideKeyboardTemporarily:(BOOL)temporarily {
  451. if (self.composing) {
  452. // can only be set to NO
  453. self.composing = temporarily;
  454. }
  455. dispatch_async(dispatch_get_main_queue(), ^{
  456. [chatBar resignFirstResponder];
  457. });
  458. }
  459. - (void)viewDidAppear:(BOOL)animated {
  460. [super viewDidAppear:animated];
  461. DDLogVerbose(@"viewDidAppear");
  462. [self resetUnreadMessageCount];
  463. ((StatusNavigationBar*)self.navigationController.navigationBar).ignoreSetItems = NO;
  464. [self scrollToUnreadMessage:animated];
  465. [self showKeyboardConditionally];
  466. visible = YES;
  467. [self processReadReceiptQueue];
  468. // free up memory in case we came back from photo browser
  469. [headerView cleanupMedia];
  470. /* send pending image */
  471. if (imageDataToSend != nil) {
  472. [self chatBar:chatBar didSendImageData:imageDataToSend];
  473. imageDataToSend = nil;
  474. }
  475. /* restore audio category? */
  476. NSInteger state = [[VoIPCallStateManager shared] currentCallState];
  477. if (prevAudioCategory != nil && state == CallStateIdle) {
  478. [[AVAudioSession sharedInstance] setCategory:prevAudioCategory error:nil];
  479. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  480. [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
  481. });
  482. prevAudioCategory = nil;
  483. }
  484. [headerView showThreemaVideoCallInfo];
  485. }
  486. - (void)viewWillDisappear:(BOOL)animated {
  487. DDLogVerbose(@"viewWillDisappear, composing = %d", self.composing);
  488. /* Send stop typing indicator now, as it may be too late once we've deleted the conversation below */
  489. [chatBar stopTyping];
  490. /* Save draft in case we get killed */
  491. NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
  492. if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
  493. [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
  494. } else {
  495. [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
  496. }
  497. lastInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
  498. lastIndexPathBeforeRotation = [[self.chatContent indexPathsForVisibleRows] lastObject];
  499. // When the app is closed in this screen and has passlock enabled this gehts called while opening
  500. // the locked app. But we want to keep the unread line until unlock. (IOS-1463)
  501. if ([KKPasscodeLock sharedLock].isPasscodeRequired) {
  502. if (![AppDelegate sharedAppDelegate].isAppLocked && [AppDelegate sharedAppDelegate].isLockscreenDismissed) {
  503. [self removeUnreadLine:YES];
  504. }
  505. } else {
  506. [self removeUnreadLine:YES];
  507. }
  508. [[NSNotificationCenter defaultCenter] removeObserver:self];
  509. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDirtyObjects:) name:kNotificationDBRefreshedDirtyObject object:nil];
  510. _tableDataSource.openTableView = NO;
  511. [super viewWillDisappear:animated];
  512. }
  513. - (void)viewDidDisappear:(BOOL)animated {
  514. DDLogVerbose(@"viewDidDisappear");
  515. visible = NO;
  516. /* Are we going back to ConversationsViewController, or to another view (e.g. contact details)? */
  517. if (self.navigationController.viewControllers == nil) {
  518. /* If our Conversation is still empty (no messages) and not a group conversation, delete it */
  519. if (conversation.messages.count == 0 && conversation.groupId == nil) {
  520. [entityManager performSyncBlockAndSafe:^{
  521. [[entityManager entityDestroyer] deleteObjectWithObject:conversation];
  522. }];
  523. }
  524. }
  525. [super viewDidDisappear:animated];
  526. }
  527. - (void)resignActive:(NSNotification*)notification {
  528. /* stop typing as the user is leaving the app */
  529. [chatBar stopTyping];
  530. /* Save draft in case we get killed */
  531. NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
  532. if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
  533. [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
  534. } else {
  535. [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
  536. }
  537. /* Remove unread line in active chat */
  538. [self removeUnreadLine:YES];
  539. }
  540. - (void)didBecomeActive:(NSNotification*)notification {
  541. [self updateConversationIfNeeded];
  542. [self resetUnreadMessageCount];
  543. [self processReadReceiptQueue];
  544. /* scroll the newest message if there is one */
  545. [self scrollToUnreadMessage:YES];
  546. [chatBar resetKeyboardType:NO];
  547. [self loadImagesIfNeeded];
  548. }
  549. - (void)removeUnreadLine:(BOOL)animated {
  550. dispatch_async(dispatch_get_main_queue(), ^{
  551. NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
  552. if (indexPath) {
  553. BOOL removed = [_tableDataSource removeUnreadLine];
  554. if (removed) {
  555. [chatContent beginUpdates];
  556. if (animated) {
  557. [chatContent deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
  558. } else {
  559. [chatContent deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
  560. }
  561. [chatContent endUpdates];
  562. }
  563. }
  564. });
  565. }
  566. - (void)scrollToUnreadMessage:(BOOL)animated {
  567. /* scroll the newest message if there is one */
  568. NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
  569. if (indexPath) {
  570. @try {
  571. isScrollingToUnreadMessages = YES;
  572. NSIndexPath *unreadLineIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
  573. [chatContent scrollToRowAtIndexPath:unreadLineIndexPath atScrollPosition:UITableViewScrollPositionTop animated:animated];
  574. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, [chatContent cellForRowAtIndexPath:indexPath]);
  575. }
  576. @catch (NSException *exception) {
  577. ;//ignore
  578. }
  579. [self updateScrollDownButtonAnimated:NO];
  580. }
  581. }
  582. - (void)showContentAfterForceTouch {
  583. _isOpenWithForceTouch = NO;
  584. chatBarWrapper.hidden = NO;
  585. chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, chatBarWrapper.frame.origin.y);
  586. }
  587. - (void)updateLayoutAfterCall {
  588. if (@available(iOS 11.0, *)) {
  589. _backgroundView.frame = self.view.safeAreaLayoutGuide.layoutFrame;
  590. } else {
  591. _backgroundView.frame = self.view.frame;
  592. }
  593. if (headerView.hidden) {
  594. [self hideHeaderWithDuration:0.3];
  595. } else {
  596. headerView.hidden = YES;
  597. [self showHeaderWithDuration:0.3 completion:nil];
  598. }
  599. }
  600. - (void)openPushSettings {
  601. [self performSegueWithIdentifier:@"ShowPushSetting" sender:nil];
  602. }
  603. - (void)loadImagesIfNeeded {
  604. // check if there are messages with not loaded images
  605. NSArray *lastMessages = [messageFetcher last20Messages];
  606. for (BaseMessage *message in lastMessages) {
  607. if ([message isKindOfClass:[ImageMessage class]]) {
  608. ImageMessage *imageMessage = (ImageMessage *)message;
  609. if (imageMessage.image == nil) {
  610. /* Start loading image */
  611. ImageMessageLoader *loader = [[ImageMessageLoader alloc] init];
  612. [loader startWithMessage:imageMessage onCompletion:^(BaseMessage *message) {
  613. } onError:^(NSError *error) {
  614. DDLogError(@"Image message blob load failed with error: %@", error);
  615. }];
  616. }
  617. }
  618. }
  619. }
  620. - (void)setCurrentAction:(ChatViewControllerAction *)newAction {
  621. currentAction = newAction;
  622. }
  623. #pragma mark - notification observer
  624. - (void)refreshDirtyObjects:(NSNotification*)notification {
  625. NSManagedObjectID *objectID = [notification.userInfo objectForKey:kKeyObjectID];
  626. if (objectID && [objectID isEqual:self.conversation.objectID]) {
  627. dispatch_async(dispatch_get_main_queue(), ^{
  628. [self updateConversation];
  629. });
  630. }
  631. }
  632. - (void)menuWillHide:(NSNotification*)notification {
  633. DDLogVerbose(@"menuWillHide");
  634. ignoreNextTap = YES;
  635. }
  636. - (void)menuDidHide:(NSNotification*)notification {
  637. DDLogVerbose(@"menuDidHide");
  638. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  639. ignoreNextTap = NO;
  640. });
  641. [self registerCustomMenuItems];
  642. }
  643. - (void)registerCustomMenuItems {
  644. UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"scan_qr", nil) action:@selector(scanQrCode:)];
  645. [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
  646. }
  647. - (void)scanQrCode:(id)sender {
  648. /* dummy to avoid compiler warning */
  649. }
  650. - (void)scrollDownButtonPressed:(id)sender {
  651. [self scrollToBottomAnimated:YES];
  652. }
  653. - (void)repositionScrollDownButton {
  654. // Icon should be cached and quadratic
  655. CGFloat buttonSize = StyleKit.scrollDownButtonIcon.size.height / UIScreen.mainScreen.scale;
  656. CGFloat padding = 8;
  657. CGFloat chatWidth = self.chatContent.frame.size.width;
  658. if (@available(iOS 11.0, *)) {
  659. // Adhere safe area insets on X-devices
  660. chatWidth -= self.view.safeAreaInsets.right;
  661. }
  662. scrollDownButton.frame = CGRectMake((chatWidth - (buttonSize + padding)),
  663. (self.chatContent.frame.origin.y + self.chatContent.frame.size.height) - (buttonSize + padding),
  664. buttonSize, buttonSize);
  665. }
  666. - (void)updateScrollDownButtonAnimated:(BOOL)animated {
  667. CGFloat targetAlpha;
  668. if ([self isScrolledAtBottom]) {
  669. targetAlpha = 0;
  670. haveNewMessages = NO;
  671. } else
  672. targetAlpha = kScrollButtonAlpha;
  673. if (scrollDownButton.alpha == targetAlpha)
  674. return;
  675. if (animated) {
  676. [UIView animateWithDuration:0.5f animations:^{
  677. scrollDownButton.alpha = targetAlpha;
  678. }];
  679. } else {
  680. scrollDownButton.alpha = targetAlpha;
  681. }
  682. }
  683. -(UIInterfaceOrientationMask)supportedInterfaceOrientations {
  684. if (SYSTEM_IS_IPAD) {
  685. return UIInterfaceOrientationMaskAll;
  686. }
  687. return UIInterfaceOrientationMaskAllButUpsideDown;
  688. }
  689. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
  690. lastIndexPathBeforeRotation = [[self.chatContent indexPathsForVisibleRows] lastObject];
  691. // override assumed table width for heightForRowAtIndexPath during rotation to get a smooth animation
  692. _tableDataSource.rotationOverrideTableWidth = self.chatContent.frame.size.width;
  693. _tableDataSource.rotationOverrideTableWidth = 0;
  694. [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
  695. {
  696. UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
  697. [self updateTableForRotationToInterfaceOrientation:orientation];
  698. [self repositionScrollDownButton];
  699. } completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
  700. {
  701. if (lastIndexPathBeforeRotation != nil) {
  702. @try {
  703. [self.chatContent scrollToRowAtIndexPath:lastIndexPathBeforeRotation atScrollPosition:UITableViewScrollPositionBottom animated:NO];
  704. } @catch (NSException *exception) {}
  705. }
  706. [self updateScrollDownButtonAnimated:YES];
  707. }];
  708. [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  709. }
  710. - (void)setupBackground {
  711. UIImage *bgImage = nil;
  712. if ([UserSettings sharedUserSettings].wallpaper) {
  713. bgImage = [UserSettings sharedUserSettings].wallpaper;
  714. } else {
  715. if (![LicenseStore requiresLicenseKey]) {
  716. UIImage *chatBackground = [BundleUtil imageNamed:@"ChatBackground"];
  717. bgImage = [chatBackground drawImageWithTintColor:[Colors chatBackgroundLines]];
  718. }
  719. }
  720. if (_backgroundView) {
  721. [_backgroundView removeFromSuperview];
  722. }
  723. if (bgImage != nil) {
  724. if (@available(iOS 11.0, *)) {
  725. _backgroundView = [[UIImageView alloc] initWithFrame:self.view.safeAreaLayoutGuide.layoutFrame];
  726. } else {
  727. _backgroundView = [[UIImageView alloc] initWithFrame:self.view.frame];
  728. }
  729. _backgroundView.contentMode = UIViewContentModeScaleAspectFill;
  730. _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  731. _backgroundView.clipsToBounds = YES;
  732. if ([UserSettings sharedUserSettings].wallpaper) {
  733. _backgroundView.backgroundColor = [UIColor clearColor];
  734. _backgroundView.image = bgImage;
  735. } else {
  736. _backgroundView.backgroundColor = [[UIColor alloc] initWithPatternImage:bgImage];
  737. _backgroundView.image = nil;
  738. }
  739. [containerView addSubview:_backgroundView];
  740. [containerView sendSubviewToBack:_backgroundView];
  741. }
  742. }
  743. - (void)updateBackgroundForOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
  744. if (_backgroundView == nil) {
  745. [self setupBackground];
  746. }
  747. if ([UserSettings sharedUserSettings].wallpaper || _backgroundView == nil) {
  748. // do not rotate for custom wallpapers
  749. return;
  750. }
  751. CGFloat rotation;
  752. if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation== UIInterfaceOrientationLandscapeRight) {
  753. rotation = M_PI/2;
  754. } else {
  755. rotation = 0;
  756. }
  757. [UIView animateWithDuration:duration animations:^{
  758. _backgroundView.transform = CGAffineTransformMakeRotation(rotation);
  759. _backgroundView.frame = self.view.frame;
  760. }];
  761. }
  762. - (void)updateTableForRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
  763. [self checkShouldShowHeader];
  764. [self.chatContent beginUpdates];
  765. [self updateChatContentInset];
  766. [self.chatContent endUpdates];
  767. if (lastIndexPathBeforeRotation != nil) {
  768. dispatch_async(dispatch_get_main_queue(), ^{
  769. @try {
  770. [self.chatContent scrollToRowAtIndexPath:lastIndexPathBeforeRotation atScrollPosition:UITableViewScrollPositionBottom animated:NO];
  771. } @catch (NSException *exception) {}
  772. });
  773. }
  774. if (_searching == NO) {
  775. [chatBar resizeChatInput];
  776. }
  777. }
  778. - (void)moveContainerViewForKeyboardFrame:(CGRect)keyboardFrameInView willHide:(BOOL)willHide {
  779. CGRect containerViewFrame = containerView.frame;
  780. CGFloat keyboardHeight = willHide ? 0.0f : keyboardFrameInView.size.height - [self tabBarHeight];
  781. if (SYSTEM_IS_IPAD && !willHide) {
  782. // iPad with external keyboard needs special treatment, as it will be shown as a collapsed bar with
  783. // some buttons, but the height is still the same. Therefore we need to calculate the height from
  784. // the y offset of the keyboard frame and the screen height
  785. keyboardHeight = self.view.frame.size.height - keyboardFrameInView.origin.y - [self tabBarHeight];
  786. }
  787. if ([AppDelegate hasBottomSafeAreaInsets]) {
  788. if (willHide) {
  789. // Must add padding to chat bar wrapper for iPhone X
  790. wrapperBottomPadding = kIphoneXChatBarBottomPadding;
  791. } else {
  792. wrapperBottomPadding = 0;
  793. }
  794. [self chatBar:chatBar didChangeHeight:chatBar.frame.size.height];
  795. }
  796. containerViewFrame.origin.y = -keyboardHeight;
  797. containerView.frame = containerViewFrame;
  798. lastKeyboardHeight = keyboardHeight;
  799. [self updateChatContentInset];
  800. if (willHide == NO) {
  801. [self checkShouldShowHeader];
  802. }
  803. }
  804. - (void)removeConversationObservers {
  805. @try {
  806. [conversation removeObserver:self forKeyPath:@"messages"];
  807. [conversation removeObserver:self forKeyPath:@"unreadMessageCount"];
  808. [conversation removeObserver:self forKeyPath:@"typing"];
  809. [conversation removeObserver:self forKeyPath:@"displayName"];
  810. [conversation removeObserver:self forKeyPath:@"groupId"];
  811. [conversation removeObserver:self forKeyPath:@"members"];
  812. } @catch (NSException * __unused exception) {}
  813. [conversation.members enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
  814. @try {
  815. [contact removeObserver:self forKeyPath:@"displayName"];
  816. }
  817. @catch (NSException * __unused exception) {}
  818. }];
  819. }
  820. - (void)addConversationObservers {
  821. @try {
  822. /* observe this conversation in case new messages are added to it while we're open */
  823. [conversation addObserver:self forKeyPath:@"messages" options:NSKeyValueObservingOptionNew context:nil];
  824. [conversation addObserver:self forKeyPath:@"unreadMessageCount" options:0 context:nil];
  825. [conversation addObserver:self forKeyPath:@"typing" options:0 context:nil];
  826. [conversation addObserver:self forKeyPath:@"displayName" options:0 context:nil];
  827. [conversation addObserver:self forKeyPath:@"groupId" options:0 context:nil];
  828. [conversation addObserver:self forKeyPath:@"members" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
  829. } @catch (NSException * __unused exception) {}
  830. [conversation.members enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
  831. @try {
  832. [contact addObserver:self forKeyPath:@"displayName" options:0 context:nil];
  833. }
  834. @catch (NSException * __unused exception) {}
  835. }];
  836. }
  837. - (CGFloat)topOffsetForVisibleContent {
  838. CGFloat topOffset;
  839. if (@available(iOS 11.0, *)) {
  840. topOffset = self.view.safeAreaLayoutGuide.layoutFrame.origin.y;
  841. } else {
  842. topOffset = self.topLayoutGuide.length;
  843. }
  844. return topOffset;
  845. }
  846. - (CGFloat)topOffsetForVisibleChatContent {
  847. CGFloat topOffset = [self topOffsetForVisibleContent];
  848. if (showHeader) {
  849. topOffset += [headerView getHeight];
  850. }
  851. return topOffset;
  852. }
  853. - (void)updateContactDisplay {
  854. [self updateConversation];
  855. }
  856. - (void)setConversation:(Conversation *)newConversation {
  857. if (conversation == newConversation)
  858. return;
  859. [self removeConversationObservers];
  860. conversation = newConversation;
  861. [self addConversationObservers];
  862. numberOfPages = 1;
  863. shouldScrollDown = YES;
  864. _isDirty = YES;
  865. messageFetcher = [MessageFetcher messageFetcherFor:conversation withEntityFetcher:entityManager.entityFetcher];
  866. currentOffset = -1;
  867. }
  868. - (NSInteger)messageOffset {
  869. return currentOffset;
  870. }
  871. - (void)cleanCellHeightCache {
  872. [_tableDataSource cleanCellHeightCache];
  873. }
  874. // Update conversation if last update was not today
  875. // This is needed to update the realtive table view headers
  876. - (void)updateConversationIfNeeded {
  877. if (lastFullConversationUpdate != nil) {
  878. NSCalendar *calendar = [NSCalendar currentCalendar];
  879. if (![calendar isDateInToday:lastFullConversationUpdate]) {
  880. [self updateConversation];
  881. }
  882. }
  883. }
  884. - (void)resetLastFullConversationUpdate {
  885. lastFullConversationUpdate = [NSDate date];
  886. }
  887. - (void)updateConversation {
  888. _isDirty = YES;
  889. [self updateConversationClearContent:YES];
  890. }
  891. - (void)updateConversationClearContent:(BOOL)clearContent {
  892. if (conversation == nil || self.isViewLoaded == NO || _isDirty == NO) {
  893. return;
  894. }
  895. _isDirty = NO;
  896. if (self.editing == YES) {
  897. self.editing = NO;
  898. }
  899. [self updateConversationLastMessage];
  900. headerView.conversation = conversation;
  901. [self setupNavigationBar];
  902. [self setupColors];
  903. NSInteger newOffset;
  904. NSInteger numberOfMessagesToLoad;
  905. ChatTableDataSource *previousDataSource;
  906. if (clearContent == NO && currentOffset != -1) {
  907. previousDataSource = _tableDataSource;
  908. newOffset = currentOffset - LOAD_MESSAGES_PER_PAGE;
  909. if (newOffset < 0) {
  910. newOffset = 0;
  911. }
  912. numberOfMessagesToLoad = currentOffset - newOffset;
  913. } else {
  914. int messagesAtStart = MESSAGES_AT_START;
  915. if ([conversation.unreadMessageCount intValue] > messagesAtStart - 5) {
  916. messagesAtStart = [conversation.unreadMessageCount intValue] + 5;
  917. }
  918. NSInteger numberOfMessages = messagesAtStart;
  919. if (numberOfPages > 1)
  920. numberOfMessages += (numberOfPages - 1) * LOAD_MESSAGES_PER_PAGE;
  921. newOffset = messageFetcher.count - numberOfMessages;
  922. numberOfMessagesToLoad = numberOfMessages;
  923. if (newOffset < 0) {
  924. newOffset = 0;
  925. numberOfMessagesToLoad = messageFetcher.count;
  926. }
  927. }
  928. ChatTableDataSource *tmpDatasource = [[ChatTableDataSource alloc] init];
  929. tmpDatasource.chatVC = self;
  930. tmpDatasource.backgroundColor = [Colors background];
  931. self.chatContent.dataSource = tmpDatasource;
  932. self.chatContent.delegate = tmpDatasource;
  933. BOOL didHideHeader = NO;
  934. if (newOffset == 0) {
  935. if (!self.chatContentHeader.hidden) {
  936. self.chatContent.tableHeaderView = nil;
  937. self.chatContentHeader.hidden = YES;
  938. didHideHeader = YES;
  939. }
  940. } else {
  941. if (self.chatContentHeader.hidden) {
  942. self.chatContent.tableHeaderView = self.headerView;
  943. self.chatContentHeader.hidden = NO;
  944. }
  945. }
  946. NSArray *pagedMessages = [messageFetcher messagesAtOffset:newOffset count:numberOfMessagesToLoad];
  947. currentOffset = newOffset;
  948. for (int i = 0; i < [pagedMessages count]; i++) {
  949. BaseMessage *curMessage = [pagedMessages objectAtIndex:i];
  950. [tmpDatasource addMessage:curMessage newSections:nil newRows:nil visible:visible];
  951. if (!curMessage.isOwn.boolValue && !curMessage.read.boolValue) {
  952. [readReceiptQueue addObject:curMessage];
  953. }
  954. }
  955. [self processReadReceiptQueue];
  956. CGFloat contentOffsetFromBottom = self.chatContent.contentOffset.y + self.chatContent.frame.size.height - self.chatContent.contentSize.height;
  957. if (previousDataSource) {
  958. [tmpDatasource addObjectsFrom:previousDataSource];
  959. tmpDatasource.searching = previousDataSource.searching;
  960. tmpDatasource.searchPattern = previousDataSource.searchPattern;
  961. }
  962. _tableDataSource = tmpDatasource;
  963. [self.chatContent reloadData];
  964. [self.chatContent layoutIfNeeded];
  965. [chatBar setupMentions:conversation.sortedMembers];
  966. CGFloat newContentOffset = contentOffsetFromBottom - self.chatContent.frame.size.height + self.chatContent.contentSize.height;
  967. if (newContentOffset < -self.chatContent.contentInset.top) {
  968. newContentOffset = -self.chatContent.contentInset.top;
  969. }
  970. if (didHideHeader) {
  971. newContentOffset -= 40;
  972. }
  973. self.chatContent.contentOffset = CGPointMake(0, newContentOffset);
  974. if (shouldScrollDown) {
  975. shouldScrollDown = NO;
  976. NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
  977. if (indexPath) {
  978. dispatch_async(dispatch_get_main_queue(), ^{
  979. [self scrollToUnreadMessage:YES];
  980. });
  981. } else {
  982. dispatch_async(dispatch_get_main_queue(), ^{
  983. [self scrollToBottomAnimated:NO];
  984. });
  985. }
  986. }
  987. // Full reload
  988. if (clearContent == YES) {
  989. [_tableDataSource refreshSectionHeadersInTableView:chatContent];
  990. [self resetLastFullConversationUpdate];
  991. }
  992. }
  993. - (void)updateConversationLastMessage {
  994. BaseMessage *message = [messageFetcher lastMessage];
  995. if ([message isKindOfClass:[SystemMessage class]]) {
  996. SystemMessage *systemMessage = (SystemMessage *)message;
  997. switch ([systemMessage.type intValue]) {
  998. case kSystemMessageCallMissed:
  999. case kSystemMessageCallRejected:
  1000. case kSystemMessageCallRejectedBusy:
  1001. case kSystemMessageCallRejectedTimeout:
  1002. case kSystemMessageCallEnded:
  1003. case kSystemMessageCallRejectedDisabled:
  1004. case kSystemMessageCallRejectedUnknown:
  1005. // call messages should add as last message, all other types should not
  1006. break;
  1007. default:
  1008. return;
  1009. }
  1010. }
  1011. conversation.lastMessage = [messageFetcher lastMessage];
  1012. }
  1013. - (void)presentActivityViewController:(UIActivityViewController *)viewControllerToPresent animated:(BOOL)flag fromView:(UIView *)view {
  1014. /* hide keyboard before showing UIActivityViewController to keep keyboard from popping up and down
  1015. repeatedly, and to prevent missed keyboard event that gets sent after viewDidDisappear but
  1016. before viewWillAppear */
  1017. [self hideKeyboardTemporarily:YES];
  1018. NSUserDefaults *defaults = [AppGroup userDefaults];
  1019. [defaults setDouble:[Utils systemUptime] forKey:@"UIActivityViewControllerOpenTime"];
  1020. [defaults synchronize];
  1021. [viewControllerToPresent setCompletionWithItemsHandler:^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) {
  1022. NSUserDefaults *defaults = [AppGroup userDefaults];
  1023. [defaults removeObjectForKey:@"UIActivityViewControllerOpenTime"];
  1024. }];
  1025. CGRect rect = [self.view convertRect:view.frame fromView:view.superview];
  1026. [ModalPresenter present:viewControllerToPresent on:self fromRect:rect inView:self.view];
  1027. }
  1028. - (void)titleTapped:(UITapGestureRecognizer*)sender {
  1029. [self toggleHeader];
  1030. }
  1031. - (CGFloat)tabBarHeight {
  1032. if (SYSTEM_IS_IPAD) {
  1033. return self.tabBarController.tabBar.frame.size.height;
  1034. } else {
  1035. return 0.0;
  1036. }
  1037. }
  1038. #pragma mark - key value observer
  1039. - (void)observeUpdatesForMessage:(BaseMessage *)message {
  1040. /* workaround for image messages: if this image hasn't been loaded yet, we must observe it
  1041. and refresh the cell when the image becomes available (height changes). This cannot be done
  1042. in ChatImageMessageCell due to race condition issues */
  1043. if ([message isKindOfClass:[ImageMessage class]]) {
  1044. ImageMessage *imageMessage = (ImageMessage*)message;
  1045. if (imageMessage.thumbnail == nil) {
  1046. [imageMessage addObserver:self forKeyPath:@"thumbnail" options:0 context:nil];
  1047. [_imageMessageObserverList addObject:imageMessage];
  1048. }
  1049. } else if ([message isKindOfClass:[FileMessage class]]) {
  1050. FileMessage *fileMessage = (FileMessage*)message;
  1051. if (fileMessage.data == nil) {
  1052. [fileMessage addObserver:self forKeyPath:@"thumbnail" options:0 context:nil];
  1053. [_imageMessageObserverList addObject:fileMessage];
  1054. }
  1055. } else if ([message isKindOfClass:[LocationMessage class]]) {
  1056. LocationMessage *locationMessage = (LocationMessage*)message;
  1057. if (locationMessage.poiName == nil && locationMessage.reverseGeocodingResult == nil) {
  1058. [locationMessage addObserver:self forKeyPath:@"reverseGeocodingResult" options:0 context:nil];
  1059. [_locationMessageObserverList addObject:locationMessage];
  1060. }
  1061. }
  1062. }
  1063. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  1064. //DDLogVerbose(@"observeValueForKeyPath:%@ ofObject:%@ change:%@", keyPath, object, change);
  1065. // objects in the change dictionary can get lost between here and the dispatch block -> copy
  1066. NSDictionary *changeCopy = [change copy];
  1067. dispatch_async(dispatch_get_main_queue(), ^{
  1068. if (object == conversation) {
  1069. if ([keyPath isEqualToString:@"messages"]) {
  1070. switch ([(NSNumber*)[changeCopy objectForKey:NSKeyValueChangeKindKey] intValue]) {
  1071. case NSKeyValueChangeInsertion: {
  1072. NSArray *newMessages = (NSArray*)[changeCopy objectForKey:NSKeyValueChangeNewKey];
  1073. [self insertMessages: newMessages];
  1074. break;
  1075. }
  1076. case NSKeyValueChangeRemoval: {
  1077. if (deleteMediaTotal > 0) {
  1078. _deleteMediaCount++;
  1079. if (deleteMediaTotal == _deleteMediaCount) {
  1080. deleteMediaTotal = 0;
  1081. _deleteMediaCount = 0;
  1082. [self updateConversation];
  1083. }
  1084. }
  1085. break;
  1086. }
  1087. }
  1088. } else if ([keyPath isEqualToString:@"unreadMessageCount"]) {
  1089. if (visible) {
  1090. [self resetUnreadMessageCount];
  1091. }
  1092. } else if ([keyPath isEqualToString:@"typing"]) {
  1093. /* update typing indicator */
  1094. if ([_tableDataSource hasData]) {
  1095. NSIndexPath *pathToLastCell = [_tableDataSource indexPathForLastCell];
  1096. [self updateTypingIndicatorAtIndexPath:pathToLastCell];
  1097. }
  1098. } else if ([keyPath isEqualToString:@"displayName"]) {
  1099. [self updateContactDisplay];
  1100. } else if ([keyPath isEqualToString:@"groupId"]) {
  1101. if (conversation.groupId == nil) {
  1102. [self.navigationController dismissViewControllerAnimated:YES completion:^{
  1103. [self.navigationController popToRootViewControllerAnimated:true];
  1104. }];
  1105. }
  1106. } else if ([keyPath isEqualToString:@"members"]) {
  1107. NSSet *oldMembers = changeCopy[NSKeyValueChangeOldKey];
  1108. NSSet *newMembers = changeCopy[NSKeyValueChangeNewKey];
  1109. [self updateMembersObserver:oldMembers newMembers:newMembers];
  1110. }
  1111. } else if ([object isKindOfClass:[ImageMessage class]] && [keyPath isEqualToString:@"thumbnail"]) {
  1112. [self updateObject:object];
  1113. } else if ([object isKindOfClass:[LocationMessage class]] && [keyPath isEqualToString:@"reverseGeocodingResult"]) {
  1114. [self updateObject:object];
  1115. } else if ([object isKindOfClass:[FileMessage class]] && [keyPath isEqualToString:@"thumbnail"]) {
  1116. [self updateObject:object];
  1117. } else if ([object isKindOfClass:[Contact class]] && [keyPath isEqualToString:@"displayName"]) {
  1118. [self updateConversation];
  1119. }
  1120. });
  1121. }
  1122. - (void)updateObject:(id)object {
  1123. /* find cell in cell map and call table view update */
  1124. NSIndexPath *indexPath = [_tableDataSource indexPathForMessage:object];
  1125. if (indexPath) {
  1126. [_tableDataSource removeObjectFromCellHeightCache:indexPath];
  1127. [self.chatContent reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
  1128. [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
  1129. }
  1130. }
  1131. - (void)insertMessages:(NSArray *)newMessages {
  1132. if (newMessages != nil) {
  1133. [self updateConversationIfNeeded];
  1134. BOOL isScrolledAtBottom = [self isScrolledAtBottom];
  1135. dispatch_async(dispatch_get_main_queue(), ^{
  1136. /* this simply assumes that the inserted message is newer than any existing
  1137. messages, and so can be added at the end of the list */
  1138. NSIndexPath *prevLastIndexPath = [_tableDataSource indexPathForLastCell];
  1139. NSMutableIndexSet *newSections = [NSMutableIndexSet new];
  1140. NSMutableArray *newRows = [[NSMutableArray alloc] initWithCapacity:newMessages.count*2];
  1141. BOOL newSentMessages = NO;
  1142. BOOL newReceivedMessages = NO;
  1143. [self.chatContent beginUpdates];
  1144. for (BaseMessage *message in newMessages) {
  1145. /* check if we have already added this message – this can happen as KVO sometimes
  1146. sends an NSKeyValueChangeInsertion event for the same messages twice on iOS 8 */
  1147. NSIndexPath *indexPathForMessage = [_tableDataSource indexPathForMessage:message];
  1148. if (indexPathForMessage != nil) {
  1149. [self.chatContent endUpdates];
  1150. if (indexPathForMessage == prevLastIndexPath) {
  1151. if (newSentMessages || isScrolledAtBottom) {
  1152. inhibitScrollBottom = YES;
  1153. isNewMessageReceivedInActiveChat = YES;
  1154. [self performSelector:@selector(scrollToBottomScheduled) withObject:nil afterDelay:0.2f];
  1155. } else if (newReceivedMessages) {
  1156. haveNewMessages = YES;
  1157. [self updateScrollDownButtonAnimated:YES];
  1158. }
  1159. }
  1160. return;
  1161. }
  1162. [_tableDataSource addMessage:message newSections:newSections newRows:newRows visible:visible];
  1163. if (!message.isOwn.boolValue && !message.read.boolValue) { // not read, so queue read receipt for sending the next time we appear
  1164. [readReceiptQueue addObject:message];
  1165. newReceivedMessages = YES;
  1166. }
  1167. if (message.isOwn.boolValue)
  1168. newSentMessages = YES;
  1169. }
  1170. if (newSections.count > 0) {
  1171. [chatContent insertSections:newSections withRowAnimation:UITableViewRowAnimationNone];
  1172. }
  1173. if (newRows.count > 0) {
  1174. [chatContent insertRowsAtIndexPaths:newRows withRowAnimation:UITableViewRowAnimationNone];
  1175. }
  1176. [self.chatContent endUpdates];
  1177. /* must update/remove the typing indicator on the previously last row */
  1178. if (prevLastIndexPath != nil)
  1179. [self updateTypingIndicatorAtIndexPath:prevLastIndexPath];
  1180. if (newSentMessages || isScrolledAtBottom) {
  1181. inhibitScrollBottom = YES;
  1182. isNewMessageReceivedInActiveChat = YES;
  1183. [self performSelector:@selector(scrollToBottomScheduled) withObject:nil afterDelay:0.2f];
  1184. } else if (newReceivedMessages) {
  1185. haveNewMessages = YES;
  1186. [self updateScrollDownButtonAnimated:YES];
  1187. }
  1188. if (visible) {
  1189. [self processReadReceiptQueue];
  1190. }
  1191. });
  1192. }
  1193. }
  1194. - (void)resetUnreadMessageCount {
  1195. if (![AppDelegate sharedAppDelegate].active)
  1196. return;
  1197. if ([conversation.unreadMessageCount intValue] != 0) {
  1198. /* mark conversation as read */
  1199. [entityManager performSyncBlockAndSafe:^{
  1200. conversation.unreadMessageCount = [NSNumber numberWithInt:0];
  1201. }];
  1202. }
  1203. [[NotificationManager sharedInstance] updateUnreadMessagesCount:NO];
  1204. }
  1205. - (void)processReadReceiptQueue {
  1206. if (conversation.groupId != nil) {
  1207. /* no read receipts for groups, but we have to set the read field in database for new message line */
  1208. /* fix for update from 2.8.0 to new version --> set all messages to read if first message of group is not read */
  1209. id firstMessage = [_tableDataSource objectForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
  1210. if ([firstMessage isKindOfClass:[BaseMessage class]]) {
  1211. if (!((BaseMessage *)firstMessage).read.boolValue) {
  1212. // add all visible messages to readReceiptQueue
  1213. NSArray *visibleMessages = [messageFetcher messagesAtOffset:currentOffset count:(messageFetcher.count - currentOffset)];
  1214. for (int i = 0; i < [visibleMessages count]; i++) {
  1215. BaseMessage *curMessage = [visibleMessages objectAtIndex:i];
  1216. [readReceiptQueue addObject:curMessage];
  1217. }
  1218. }
  1219. }
  1220. NSMutableArray *tmpReadReceiptQueue = [NSMutableArray arrayWithArray:readReceiptQueue];
  1221. [entityManager performAsyncBlockAndSafe:^{
  1222. for (BaseMessage *message in tmpReadReceiptQueue) {
  1223. @try {
  1224. message.read = [NSNumber numberWithBool:YES];
  1225. message.readDate = [NSDate date];
  1226. }
  1227. @catch (NSException *exception) {
  1228. // intended to catch NSObjectInaccessibleException, which may happen
  1229. // if the message has been deleted in the meantime
  1230. DDLogError(@"Exception while marking message as read: %@", exception);
  1231. }
  1232. }
  1233. }];
  1234. [readReceiptQueue removeAllObjects];
  1235. return;
  1236. }
  1237. /* do not send read receipts while app is in the background */
  1238. if (![AppDelegate sharedAppDelegate].active)
  1239. return;
  1240. if (readReceiptQueue.count > 0) {
  1241. NSMutableArray *tmpReadReceiptQueue = [NSMutableArray arrayWithArray:readReceiptQueue];
  1242. [MessageSender sendReadReceiptForMessages:tmpReadReceiptQueue toIdentity:conversation.contact.identity async:YES quickReply:NO];
  1243. [entityManager performAsyncBlockAndSafe:^{
  1244. for (BaseMessage *message in tmpReadReceiptQueue) {
  1245. @try {
  1246. message.read = [NSNumber numberWithBool:YES];
  1247. message.readDate = [NSDate date];
  1248. }
  1249. @catch (NSException *exception) {
  1250. // intended to catch NSObjectInaccessibleException, which may happen
  1251. // if the message has been deleted in the meantime
  1252. DDLogError(@"Exception while marking message as read: %@", exception);
  1253. }
  1254. }
  1255. }];
  1256. [readReceiptQueue removeAllObjects];
  1257. }
  1258. }
  1259. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  1260. if ([segue.identifier isEqualToString:@"ShowContact"]) {
  1261. ContactDetailsViewController *detailsView = (ContactDetailsViewController*)segue.destinationViewController;
  1262. if ([sender isKindOfClass:[Contact class]]) {
  1263. detailsView.contact = (Contact *)sender;
  1264. } else {
  1265. detailsView.contact = conversation.contact;
  1266. }
  1267. } else if ([segue.identifier isEqualToString:@"ShowLocation"]) {
  1268. LocationViewController *locationView = (LocationViewController*)segue.destinationViewController;
  1269. locationView.locationMessage = locationToShow;
  1270. } else if ([segue.identifier isEqualToString:@"ShowDetails"]) {
  1271. MessageDetailsViewController *detailsView = (MessageDetailsViewController*)segue.destinationViewController;
  1272. detailsView.message = detailsMessage;
  1273. } else if ([segue.identifier isEqualToString:@"ShowGroupInfo"]) {
  1274. GroupDetailsViewController *detailsView = (GroupDetailsViewController*)segue.destinationViewController;
  1275. detailsView.delegate = self;
  1276. detailsView.group = [GroupProxy groupProxyForConversation:conversation];
  1277. } else if ([segue.identifier isEqualToString:@"ShowPushSetting"]) {
  1278. NotificationSettingViewController *settingsView = (NotificationSettingViewController*)segue.destinationViewController;
  1279. if (conversation.isGroup) {
  1280. settingsView.identity = [NSString stringWithHexData:conversation.groupId];
  1281. settingsView.isGroup = YES;
  1282. settingsView.conversation = conversation;
  1283. } else {
  1284. settingsView.identity = conversation.contact.identity;
  1285. settingsView.isGroup = NO;
  1286. settingsView.conversation = conversation;
  1287. }
  1288. }
  1289. }
  1290. - (void)cancelAction:(id)sender {
  1291. self.editing = NO;
  1292. }
  1293. - (void)deleteAction:(id)sender {
  1294. NSString *actionTitle;
  1295. NSUInteger numSelected = [[self.chatContent indexPathsForSelectedRows] count];
  1296. if (numSelected == 0) {
  1297. /* clear all */
  1298. actionTitle = NSLocalizedString(@"messages_delete_all_confirm", nil);
  1299. } else {
  1300. actionTitle = NSLocalizedString(@"messages_delete_selected_confirm", nil);
  1301. }
  1302. UIAlertController *deleteActionSheet = [UIAlertController alertControllerWithTitle:actionTitle message:nil preferredStyle:UIAlertControllerStyleAlert];
  1303. [deleteActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"delete", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
  1304. [_tableDataSource cleanCellHeightCache];
  1305. ChatDeleteAction *deleteAction = [ChatDeleteAction actionForChatViewController:self];
  1306. deleteAction.entityManager = entityManager;
  1307. currentAction = deleteAction;
  1308. [deleteAction executeAction];
  1309. }]];
  1310. [deleteActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
  1311. deleteActionSheet.popoverPresentationController.sourceView = self.view;
  1312. [self presentViewController:deleteActionSheet animated:YES completion:nil];
  1313. }
  1314. - (IBAction)loadEarlierMessagesAction:(id)sender {
  1315. numberOfPages++;
  1316. _isDirty = YES;
  1317. [self updateConversationClearContent:NO];
  1318. }
  1319. - (void)scrollToBottomScheduled {
  1320. inhibitScrollBottom = NO;
  1321. [self scrollToBottomAnimated:YES];
  1322. }
  1323. - (void)scrollToBottomAnimated:(BOOL)animated {
  1324. if (inhibitScrollBottom || (chatContent.contentSize.height - chatContent.contentOffset.y - chatContent.frame.size.height) < 0)
  1325. return;
  1326. NSIndexPath *bottomRow = [_tableDataSource indexPathForLastCell];
  1327. if (bottomRow) {
  1328. [self checkShouldShowHeader];
  1329. dispatch_async(dispatch_get_main_queue(), ^{
  1330. @try {
  1331. [chatContent scrollToRowAtIndexPath:bottomRow atScrollPosition:UITableViewScrollPositionBottom animated:animated];
  1332. [self repositionScrollDownButton];
  1333. [self updateScrollDownButtonAnimated:NO];
  1334. }
  1335. @catch (NSException *exception) {
  1336. ;//ignore
  1337. }
  1338. });
  1339. }
  1340. }
  1341. - (BOOL)isScrolledAtBottom {
  1342. return ((chatContent.contentSize.height - chatContent.contentOffset.y - chatContent.frame.size.height) < 25);
  1343. }
  1344. - (NSString *)messageText {
  1345. if (chatBar != nil) {
  1346. return chatBar.text;
  1347. } else {
  1348. return initialMessageText;
  1349. }
  1350. }
  1351. - (void)setMessageText:(NSString *)messageText {
  1352. if (chatBar != nil) {
  1353. [self showKeyboardConditionally];
  1354. chatBar.text = messageText;
  1355. } else {
  1356. initialMessageText = messageText;
  1357. }
  1358. }
  1359. - (void)setImageDataToSend:(NSData *)newImageToSend {
  1360. imageDataToSend = newImageToSend;
  1361. /* if we're currently visible, trigger send as there will be no viewDidAppear */
  1362. if (visible) {
  1363. [self chatBar:chatBar didSendImageData:imageDataToSend];
  1364. imageDataToSend = nil;
  1365. }
  1366. }
  1367. - (void)chatContentTapped:(UITapGestureRecognizer*)sender {
  1368. DDLogVerbose(@"chatContentTapped, ignoreNextTap = %d, sender = %@", ignoreNextTap, sender);
  1369. if (ignoreNextTap) {
  1370. ignoreNextTap = NO;
  1371. return;
  1372. }
  1373. dispatch_async(dispatch_get_main_queue(), ^{
  1374. [self hideKeyboardTemporarily:NO];
  1375. });
  1376. }
  1377. - (void)messageBackgroundTapped:(BaseMessage *)message {
  1378. DDLogVerbose(@"messageBackgroundTapped");
  1379. if (ignoreNextTap)
  1380. return;
  1381. [self hideKeyboardTemporarily:NO];
  1382. }
  1383. - (void)startRecordingAudio {
  1384. [PlayRecordAudioViewController requestMicrophoneAccessOnCompletion:^{
  1385. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
  1386. selectedAudioMessage = [_tableDataSource indexPathForLastCell];
  1387. audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
  1388. audioRecorder.delegate = self;
  1389. selectedAudioMessage = nil;
  1390. [audioRecorder startRecordingForConversation: conversation];
  1391. }];
  1392. }
  1393. - (void)createBallot {
  1394. [BallotDispatcher showBallotCreateViewControllerForConversation:conversation onNavigationController:self.navigationController];
  1395. }
  1396. - (void)sendFile {
  1397. DocumentPicker *documentPicker = [DocumentPicker documentPickerForViewController:self conversation:self.conversation];
  1398. documentPicker.popoverSourceRect = [self.view convertRect:self.chatBar.addButton.frame fromView:self.chatBar];
  1399. [documentPicker show];
  1400. }
  1401. - (void)playAudioMessage:(AudioMessage*)message {
  1402. /* write audio to temp. file */
  1403. [self createTmpAVFileFrom:message];
  1404. if (tmpAudioVideoUrl) {
  1405. [self hideKeyboardTemporarily:YES];
  1406. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
  1407. selectedAudioMessage = [_tableDataSource indexPathForMessage:message];
  1408. audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
  1409. audioRecorder.delegate = self;
  1410. [audioRecorder startPlaying: tmpAudioVideoUrl];
  1411. }
  1412. }
  1413. - (void)playFileAudioMessage:(FileMessage*)message {
  1414. /* write audio to temp. file */
  1415. [self createTmpAVFileFrom:message];
  1416. if (tmpAudioVideoUrl) {
  1417. [self hideKeyboardTemporarily:YES];
  1418. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
  1419. selectedAudioMessage = [_tableDataSource indexPathForMessage:message];
  1420. audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
  1421. audioRecorder.delegate = self;
  1422. [audioRecorder startPlaying: tmpAudioVideoUrl];
  1423. }
  1424. }
  1425. - (void)updateTypingIndicatorAtIndexPath:(NSIndexPath*)indexPath {
  1426. UITableViewCell *cell = [self.chatContent cellForRowAtIndexPath:indexPath];
  1427. if (cell != nil && [cell isKindOfClass:[ChatMessageCell class]]) {
  1428. ChatMessageCell *chatMessageCell = (ChatMessageCell*)cell;
  1429. NSIndexPath *currentLastIndexPath = [_tableDataSource indexPathForLastCell];
  1430. if (conversation.typing.boolValue && [indexPath isEqual:currentLastIndexPath]) {
  1431. chatMessageCell.typing = YES;
  1432. } else {
  1433. chatMessageCell.typing = NO;
  1434. }
  1435. }
  1436. }
  1437. - (void)updateChatContentInset {
  1438. float statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
  1439. if (@available(iOS 11.0, *)) {
  1440. chatContent.contentInset = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] - self.navigationController.navigationBar.frame.size.height - statusBarHeight + 4.0f, 0, 0, 0);
  1441. chatContent.scrollIndicatorInsets = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] - self.navigationController.navigationBar.frame.size.height - statusBarHeight, 0, 0, 0);
  1442. } else {
  1443. chatContent.contentInset = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] + 4.0f, 0, 0, 0);
  1444. chatContent.scrollIndicatorInsets = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent], 0, 0, 0);
  1445. }
  1446. }
  1447. # pragma mark - Keyboard Notifications
  1448. - (void)keyboardWillShow:(NSNotification *)notification {
  1449. DDLogVerbose(@"keyboardWillShow");
  1450. forceTouching = NO;
  1451. [self processKeyboardNotification:notification willHide:NO];
  1452. self.composing = YES;
  1453. }
  1454. - (void)keyboardWillHide:(NSNotification *)notification {
  1455. DDLogVerbose(@"keyboardWillHide");
  1456. [self processKeyboardNotification:notification willHide:YES];
  1457. [_tableDataSource refreshSectionHeadersInTableView:self.chatContent];
  1458. }
  1459. - (void)processKeyboardNotification:(NSNotification*)notification willHide:(BOOL)willHide {
  1460. if (notification == nil) {
  1461. CGRect newKeyboardEndFrame = lastKeyboardEndFrame;
  1462. if (lastKeyboardHeight == 162 && [UIScreen mainScreen].bounds.size.height == 320.0) {
  1463. newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height;
  1464. }
  1465. else if (lastKeyboardHeight == 162 || lastKeyboardHeight == 216) {
  1466. newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height + 32.0;
  1467. }
  1468. else {
  1469. newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height + 42.0;
  1470. }
  1471. [UIView animateWithDuration:lastAnimationDuration delay:0 options:(lastAnimationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState) animations:^{
  1472. [self moveContainerViewForKeyboardFrame:newKeyboardEndFrame willHide:willHide];
  1473. } completion:^(BOOL finished) {}];
  1474. } else {
  1475. CGRect keyboardEndFrame;
  1476. [notification.userInfo[UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
  1477. CGRect keyboardEndFrameRelative = [self.view convertRect:keyboardEndFrame fromView:nil];
  1478. CGSize keyboardSize = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
  1479. DDLogVerbose(@"keyboardEndFrame: %@", NSStringFromCGRect(keyboardEndFrame));
  1480. DDLogVerbose(@"keyboardEndFrameRelative: %@", NSStringFromCGRect(keyboardEndFrameRelative));
  1481. DDLogVerbose(@"Keyboardsize height: %f", keyboardSize.height);
  1482. NSNumber *durationValue = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];
  1483. NSTimeInterval animationDuration = durationValue.doubleValue;
  1484. NSNumber *curveValue = notification.userInfo[UIKeyboardAnimationCurveUserInfoKey];
  1485. UIViewAnimationCurve animationCurve = curveValue.intValue;
  1486. lastKeyboardEndFrame = keyboardEndFrameRelative;
  1487. lastAnimationDuration = animationDuration;
  1488. lastAnimationCurve = animationCurve;
  1489. if (visible) {
  1490. [UIView animateWithDuration:animationDuration delay:0 options:(animationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState) animations:^{
  1491. [self moveContainerViewForKeyboardFrame:keyboardEndFrameRelative willHide:willHide];
  1492. } completion:^(BOOL finished) {}];
  1493. } else {
  1494. [self moveContainerViewForKeyboardFrame:keyboardEndFrameRelative willHide:willHide];
  1495. }
  1496. }
  1497. }
  1498. - (void)inputModeDidChange:(NSNotification *)notification {
  1499. // iPhone X fix
  1500. if (@available(iOS 11.0, *)) {
  1501. if([[UITextInputMode currentInputMode].primaryLanguage isEqualToString:@"emoji"]) {
  1502. if (SYSTEM_IS_IPHONE_X && (lastKeyboardHeight == 291 || lastKeyboardHeight == 171)) { // iPhone X
  1503. [self processKeyboardNotification:nil willHide:NO];
  1504. } else if (lastKeyboardHeight == 226) { // Portrait iPhone 5.5'
  1505. [self processKeyboardNotification:nil willHide:NO];
  1506. } else if (lastKeyboardHeight == 216) { // Portrait iPhone 4' iPhone 4.7'
  1507. [self processKeyboardNotification:nil willHide:NO];
  1508. } else if (lastKeyboardHeight == 162) { // Landscape iPhone 5.5' iPhone 4.7' iPhone 4'
  1509. [self processKeyboardNotification:nil willHide:NO];
  1510. } else if (SYSTEM_IS_IPAD == YES && lastKeyboardHeight == 304) { // iPad
  1511. [self processKeyboardNotification:nil willHide:NO];
  1512. } else if (SYSTEM_IS_IPAD == YES && (lastKeyboardHeight == 279 || lastKeyboardHeight == 374)) { // iPad Pro 12.9
  1513. [self processKeyboardNotification:nil willHide:NO];
  1514. }
  1515. }
  1516. }
  1517. }
  1518. - (void)setEditing:(BOOL)editing animated:(BOOL)animated {
  1519. DDLogVerbose(@"setEditing");
  1520. [super setEditing:editing animated:animated];
  1521. [chatContent setEditing:editing animated:animated];
  1522. tapGestureRecognizer.enabled = !editing;
  1523. chatContent.separatorStyle = editing ?
  1524. UITableViewCellSeparatorStyleSingleLine : UITableViewCellSeparatorStyleNone;
  1525. if (editing) {
  1526. UIBarButtonItem *deleteButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"delete_all", nil) style:UIBarButtonItemStylePlain target:self action:@selector(deleteAction:)];
  1527. deleteButton.tintColor = [UIColor redColor];
  1528. self.navigationItem.leftBarButtonItem = deleteButton;
  1529. self.navigationItem.rightBarButtonItems = @[[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction:)]];
  1530. self.loadEarlierMessages.hidden = YES;
  1531. } else {
  1532. self.navigationItem.leftBarButtonItem = nil;
  1533. self.navigationItem.rightBarButtonItems = @[self.editButtonItem];
  1534. self.loadEarlierMessages.hidden = NO;
  1535. }
  1536. if (editing) {
  1537. [self checkShouldShowHeader];
  1538. [self hideKeyboardTemporarily:NO];
  1539. }
  1540. }
  1541. #pragma mark - ChatBarDelegate
  1542. - (void)chatBar:(ChatBar *)curChatBar didChangeHeight:(CGFloat)height {
  1543. BOOL wasScrolledAtBottom = [self isScrolledAtBottom];
  1544. CGRect chatContentFrame = chatContent.frame;
  1545. chatContentFrame.size.height = containerView.frame.size.height - height - [self tabBarHeight] - wrapperBottomPadding;
  1546. [UIView beginAnimations:nil context:NULL];
  1547. [UIView setAnimationDuration:0.1f];
  1548. chatContent.frame = chatContentFrame;
  1549. chatBarWrapper.frame = CGRectMake(chatBarWrapper.frame.origin.x, chatContentFrame.size.height, containerView.frame.size.width, height + wrapperBottomPadding);
  1550. chatBar.frame = CGRectMake(0, 0, chatBarWrapper.frame.size.width, height);
  1551. [self repositionScrollDownButton];
  1552. [UIView commitAnimations];
  1553. if (wasScrolledAtBottom)
  1554. [self scrollToBottomAnimated:YES];
  1555. }
  1556. - (void)chatBar:(ChatBar *)curChatBar didSendText:(NSString *)text {
  1557. if (text.length == 0 && curChatBar.canSendAudio) {
  1558. if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
  1559. return;
  1560. }
  1561. /* microphone button pressed */
  1562. [self hideKeyboardTemporarily:YES];
  1563. [self startRecordingAudio];
  1564. return;
  1565. }
  1566. NSString *trimmedMessage = nil;
  1567. NSString *quotedIdentity = nil;
  1568. NSData *quoteMessageId = nil;
  1569. NSString *remainingBody = nil;
  1570. NSString *quotedText = nil;
  1571. if ([[UserSettings sharedUserSettings] quoteV2Active]) {
  1572. quoteMessageId = [QuoteParser parseQuoteV2FromMessage:text remainingBody:&remainingBody];
  1573. } else {
  1574. quotedText = [QuoteParser parseQuoteFromMessage:text quotedIdentity:&quotedIdentity remainingBody:&remainingBody];
  1575. }
  1576. if (quoteMessageId || quotedText) {
  1577. remainingBody = [remainingBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  1578. }
  1579. trimmedMessage = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  1580. // Don't send blank messages.
  1581. if (quoteMessageId || quotedText) {
  1582. if (remainingBody == nil || remainingBody.length == 0 || [remainingBody isEqualToString:@"\ufffc"]) {
  1583. [chatBar clearChatInput];
  1584. return;
  1585. }
  1586. } else {
  1587. if (trimmedMessage == nil || trimmedMessage.length == 0 || [trimmedMessage isEqualToString:@"\ufffc"]) {
  1588. [chatBar clearChatInput];
  1589. return;
  1590. }
  1591. }
  1592. if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
  1593. return;
  1594. }
  1595. NSArray *trimmedMessages = [Utils getTrimmedMessages:trimmedMessage];
  1596. [chatBar checkEnableSendButton];
  1597. if (!trimmedMessages) {
  1598. [MessageSender sendMessage:trimmedMessage inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1599. [chatBar clearChatInput];
  1600. [MessageDraftStore deleteDraftForConversation:self.conversation];
  1601. }];
  1602. } else {
  1603. [trimmedMessages enumerateObjectsUsingBlock:^(NSString *separatedTrimmedMessage, NSUInteger idx, BOOL * _Nonnull stop) {
  1604. [MessageSender sendMessage:separatedTrimmedMessage inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1605. if (idx == trimmedMessages.count - 1) {
  1606. [chatBar clearChatInput];
  1607. [MessageDraftStore deleteDraftForConversation:self.conversation];
  1608. }
  1609. }];
  1610. }];
  1611. }
  1612. if ([UserSettings sharedUserSettings].inAppSounds) {
  1613. AudioServicesPlaySystemSound(sentMessageSound);
  1614. }
  1615. }
  1616. - (void)chatBar:(ChatBar *)chatBar didSendImageData:(NSData *)image {
  1617. if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
  1618. return;
  1619. }
  1620. [self hideKeyboardTemporarily:YES];
  1621. UINavigationController *previewNavVc = [self.storyboard instantiateViewControllerWithIdentifier:@"PreviewImageNav"];
  1622. PreviewImageViewController *previewVc = previewNavVc.viewControllers[0];
  1623. previewVc.delegate = self;
  1624. previewVc.image = image;
  1625. previewVc.hasCancelButton = YES;
  1626. [self presentViewController:previewNavVc animated:YES completion:nil];
  1627. }
  1628. - (void)chatBar:(ChatBar *)chatBar2 didSendGIF:(NSData *)gifData fallbackImage:(UIImage *)image {
  1629. if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
  1630. return;
  1631. }
  1632. // Check if we can send file messages in this conversation
  1633. NSSet *conversations = [NSSet setWithObject:self.conversation];
  1634. [FeatureMask checkFeatureMask:FEATURE_MASK_FILE_TRANSFER forConversations:conversations onCompletion:^(NSArray *unsupportedContacts) {
  1635. [self hideKeyboardTemporarily:YES];
  1636. UINavigationController *previewNavVc = [self.storyboard instantiateViewControllerWithIdentifier:@"PreviewImageNav"];
  1637. PreviewImageViewController *previewVc = previewNavVc.viewControllers[0];
  1638. previewVc.delegate = self;
  1639. if ([unsupportedContacts count] > 0) {
  1640. previewVc.image = UIImageJPEGRepresentation(image, 1.0);
  1641. } else {
  1642. previewVc.gifData = gifData;
  1643. }
  1644. previewVc.hasCancelButton = YES;
  1645. [self presentViewController:previewNavVc animated:YES completion:nil];
  1646. }];
  1647. }
  1648. - (void)chatBarWillStartTyping:(ChatBar *)chatBar {
  1649. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  1650. if (typingIndicatorSent)
  1651. return;
  1652. if (conversation.groupId == nil && [[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil])
  1653. [MessageSender sendTypingIndicatorMessage:YES toIdentity:conversation.contact.identity];
  1654. typingIndicatorSent = YES;
  1655. }
  1656. }
  1657. - (void)chatBarDidStopTyping:(ChatBar *)theChatBar {
  1658. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  1659. if (!typingIndicatorSent)
  1660. return;
  1661. if (conversation.groupId == nil)
  1662. [MessageSender sendTypingIndicatorMessage:NO toIdentity:conversation.contact.identity];
  1663. typingIndicatorSent = NO;
  1664. NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
  1665. if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
  1666. [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
  1667. } else {
  1668. [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
  1669. }
  1670. }
  1671. }
  1672. - (void)chatBarDidPushAddButton:(ChatBar *)_chatBar {
  1673. if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
  1674. return;
  1675. }
  1676. if (SYSTEM_IS_IPAD == true) {
  1677. [_delegate cancelSwipeGestureFromConversations];
  1678. }
  1679. CGRect rect = [self.view convertRect:chatBar.addButton.frame fromView:_chatBar];
  1680. [self showAddActionAlertControllerFrom:rect inView:self.view];
  1681. }
  1682. - (void)showAddActionAlertControllerFrom:(CGRect)rect inView:(UIView *)view {
  1683. if (_assetActionHelperWillPresent) {
  1684. return;
  1685. }
  1686. _assetActionHelperWillPresent = true;
  1687. [self hideKeyboardTemporarily:YES];
  1688. if (assetActionHelper == nil) {
  1689. assetActionHelper = [[PPAssetsActionHelper alloc] init];
  1690. assetActionHelper.delegate = self;
  1691. }
  1692. PPAssetsActionController *assetActionController = [assetActionHelper buildAction];
  1693. if ([[UserSettings sharedUserSettings] showGalleryPreview]) {
  1694. [[UserSettings sharedUserSettings] setOpenPlusIconInChat:YES];
  1695. }
  1696. [self presentViewController:assetActionController animated:YES completion:^{
  1697. _assetActionHelperWillPresent = false;
  1698. if ([[UserSettings sharedUserSettings] showGalleryPreview]) {
  1699. [[UserSettings sharedUserSettings] setOpenPlusIconInChat:NO];
  1700. }
  1701. }];
  1702. }
  1703. - (UIInterfaceOrientation)interfaceOrientationForChatBar:(ChatBar *)chatBar {
  1704. return [[UIApplication sharedApplication] statusBarOrientation];
  1705. }
  1706. - (BOOL)canBecomeFirstResponder {
  1707. return ![self.presentedViewController isKindOfClass:[CallViewController class]] && self.presentedViewController == nil;
  1708. }
  1709. - (void)chatBarTapped:(ChatBar *)chatBar {
  1710. if (SYSTEM_IS_IPAD == true) {
  1711. [_delegate cancelSwipeGestureFromConversations];
  1712. }
  1713. }
  1714. - (void)chatBarDidAddQuote {
  1715. if (_searching) {
  1716. [headerView cancelSearch];
  1717. }
  1718. }
  1719. - (UIView *)chatContainterView {
  1720. return self.view;
  1721. }
  1722. #pragma mark - Chat message cell delegate
  1723. - (void)imageMessageTapped:(ImageMessage *)message {
  1724. [self hideKeyboardTemporarily:YES];
  1725. UIViewController *vc = [headerView getPhotoBrowserAtMessage:message forPeeking:NO];
  1726. vc.modalPresentationStyle = UIModalPresentationFullScreen;
  1727. [self presentViewController:vc animated:YES completion:nil];
  1728. }
  1729. - (void)fileImageMessageTapped:(FileMessage *)message {
  1730. [self hideKeyboardTemporarily:true];
  1731. UIViewController *vc = [headerView getPhotoBrowserAtMessage:message forPeeking:NO];
  1732. vc.modalPresentationStyle = UIModalPresentationFullScreen;
  1733. [self presentViewController:vc animated:YES completion:nil];
  1734. }
  1735. - (void)fileVideoMessageTapped:(FileMessage *)message {
  1736. if (message.data == nil) {
  1737. /* need to download this video first */
  1738. BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
  1739. [loader startWithMessage:message onCompletion:^(BaseMessage<BlobData> *loadedMessage) {
  1740. if (visible) {
  1741. [self playFileVideoMessage:message];
  1742. }
  1743. } onError:^(NSError *error) {
  1744. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  1745. }];
  1746. } else {
  1747. /* can show/play this video right now */
  1748. [self playFileVideoMessage:message];
  1749. }
  1750. }
  1751. - (void)fileAudioMessageTapped:(FileMessage *)message {
  1752. if (message.data == nil) {
  1753. [PlayRecordAudioViewController activateProximityMonitoring];
  1754. /* need to download this audio first */
  1755. BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
  1756. [loader startWithMessage:message onCompletion:^(BaseMessage *loadedMessage) {
  1757. if (visible) {
  1758. [self playFileAudioMessage:message];
  1759. }
  1760. } onError:^(NSError *error) {
  1761. [PlayRecordAudioViewController deactivateProximityMonitoring];
  1762. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  1763. }];
  1764. } else {
  1765. /* can show/play this audio right now */
  1766. [self playFileAudioMessage:message];
  1767. }
  1768. }
  1769. - (void)locationMessageTapped:(LocationMessage *)message {
  1770. [self hideKeyboardTemporarily:YES];
  1771. locationToShow = message;
  1772. [self performSegueWithIdentifier:@"ShowLocation" sender:self];
  1773. }
  1774. - (void)videoMessageTapped:(VideoMessage*)message {
  1775. if (message.video == nil) {
  1776. /* need to download this video first */
  1777. VideoMessageLoader *loader = [[VideoMessageLoader alloc] init];
  1778. [loader startWithMessage:message onCompletion:^(BaseMessage<BlobData> *loadedMessage) {
  1779. if (visible) {
  1780. [self playVideoMessage:message];
  1781. }
  1782. } onError:^(NSError *error) {
  1783. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  1784. }];
  1785. } else {
  1786. /* can show/play this video right now */
  1787. [self playVideoMessage:message];
  1788. }
  1789. }
  1790. - (void)startPlayer {
  1791. [self hideKeyboardTemporarily:YES];
  1792. AppDelegate *appDelegate = [AppDelegate sharedAppDelegate];
  1793. /* ignore mute switch */
  1794. NSInteger state = [[VoIPCallStateManager shared] currentCallState];
  1795. if (state == CallStateIdle) {
  1796. prevAudioCategory = [[AVAudioSession sharedInstance] category];
  1797. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
  1798. }
  1799. AVPlayer *p = [AVPlayer playerWithURL:tmpAudioVideoUrl];
  1800. player = [AVPlayerViewController new];
  1801. player.player = p;
  1802. if (self.isViewLoaded && self.view.window) {
  1803. //self is visible
  1804. [self presentViewController:player animated:YES completion:^{
  1805. [player.player play];
  1806. if (state != CallStateIdle) {
  1807. [[VoIPCallStateManager shared] activateRTCAudio];
  1808. }
  1809. }];
  1810. } else {
  1811. [appDelegate.window.rootViewController.presentedViewController presentViewController:player animated:YES completion:^{
  1812. [player.player play];
  1813. }];
  1814. }
  1815. }
  1816. - (void)createTmpAVFileFrom:(id<BlobData>)message {
  1817. NSURL *tmpDirUrl = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
  1818. tmpAudioVideoUrl = [[tmpDirUrl URLByAppendingPathComponent:@"av"] URLByAppendingPathExtension: MEDIA_EXTENSION_VIDEO];
  1819. DDLogInfo(@"fileURL: %@", [tmpAudioVideoUrl path]);
  1820. NSData *data = [message blobGetData];
  1821. if (![data writeToURL:tmpAudioVideoUrl atomically:NO]) {
  1822. DDLogWarn(@"Writing audio/video data to temporary file failed");
  1823. return;
  1824. }
  1825. }
  1826. - (void)playVideoMessage:(VideoMessage*)message {
  1827. /* write video to temp. file */
  1828. [self createTmpAVFileFrom:message];
  1829. if (tmpAudioVideoUrl) {
  1830. [self startPlayer];
  1831. }
  1832. }
  1833. - (void)playFileVideoMessage:(FileMessage *)message {
  1834. /* write video to temp. file */
  1835. [self createTmpAVFileFrom:message];
  1836. if (tmpAudioVideoUrl) {
  1837. [self startPlayer];
  1838. }
  1839. }
  1840. - (void)showMessageDetails:(BaseMessage *)message {
  1841. detailsMessage = message;
  1842. [self performSegueWithIdentifier:@"ShowDetails" sender:self];
  1843. }
  1844. - (void)audioMessageTapped:(AudioMessage*)message {
  1845. if (message.audio == nil) {
  1846. [PlayRecordAudioViewController activateProximityMonitoring];
  1847. /* need to download this audio first */
  1848. BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
  1849. [loader startWithMessage:message onCompletion:^(BaseMessage *loadedMessage) {
  1850. if (visible) {
  1851. [self playAudioMessage:message];
  1852. }
  1853. } onError:^(NSError *error) {
  1854. [PlayRecordAudioViewController deactivateProximityMonitoring];
  1855. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  1856. }];
  1857. } else {
  1858. /* can show/play this audio right now */
  1859. [self playAudioMessage:message];
  1860. }
  1861. }
  1862. - (void)ballotMessageTapped:(BallotMessage*)message {
  1863. [self hideKeyboardTemporarily:YES];
  1864. [BallotDispatcher showViewControllerForBallot:message.ballot onNavigationController:self.navigationController];
  1865. }
  1866. - (void)mentionTapped:(id)mentionObject {
  1867. [self hideKeyboardTemporarily:NO];
  1868. if ([mentionObject isKindOfClass:[Contact class]]) {
  1869. [self performSegueWithIdentifier:@"ShowContact" sender:(Contact *)mentionObject];
  1870. } else {
  1871. [self performSegueWithIdentifier:@"ShowMeContact" sender:(Contact *)mentionObject];
  1872. }
  1873. }
  1874. - (void)showQuotedMessage:(BaseMessage *)message {
  1875. _cancelShowQuotedMessage = NO;
  1876. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  1877. __block NSIndexPath *indexPath = nil;
  1878. while (_cancelShowQuotedMessage == NO) {
  1879. indexPath = [self indexPathForMessage:message];
  1880. if (indexPath) {
  1881. // found message
  1882. break;
  1883. } else {
  1884. NSInteger offset = [self messageOffset];
  1885. if (offset > 0) {
  1886. dispatch_sync(dispatch_get_main_queue(), ^{
  1887. [self addLoadEarlierMessagesHUD];
  1888. [self loadEarlierMessagesAction:nil];
  1889. if (_cancelShowQuotedMessage) {
  1890. return;
  1891. }
  1892. NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  1893. [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
  1894. });
  1895. } else {
  1896. break;
  1897. }
  1898. }
  1899. }
  1900. dispatch_sync(dispatch_get_main_queue(), ^{
  1901. [MBProgressHUD hideHUDForView:self.view animated:YES];
  1902. });
  1903. dispatch_async(dispatch_get_main_queue(), ^{
  1904. if (indexPath) {
  1905. // safety check if indexPath is still valid
  1906. if ([self isValidIndexPath:indexPath] == NO) {
  1907. return;
  1908. }
  1909. [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
  1910. __block ChatMessageCell *currentCell = (ChatMessageCell *)[self.chatContent cellForRowAtIndexPath:indexPath];
  1911. // Deselect all currently selected cells
  1912. for (ChatMessageCell *visibleCell in self.chatContent.visibleCells) {
  1913. if ([visibleCell respondsToSelector:@selector(setBubbleHighlighted:)]) {
  1914. [visibleCell setBubbleHighlighted:NO];
  1915. }
  1916. }
  1917. CGFloat delayMs;
  1918. if (currentCell) {
  1919. delayMs = 100.0;
  1920. } else {
  1921. // cell not visible yet
  1922. delayMs = 400.0;
  1923. }
  1924. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  1925. currentCell = (ChatMessageCell *)[self.chatContent cellForRowAtIndexPath:indexPath];
  1926. [currentCell setBubbleHighlighted:YES];
  1927. if (UIAccessibilityIsVoiceOverRunning()) {
  1928. NSString *text = currentCell.accessibilityLabel;
  1929. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text);
  1930. }
  1931. });
  1932. }
  1933. });
  1934. });
  1935. }
  1936. - (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
  1937. NSInteger sectionCount = [self.chatContent.dataSource numberOfSectionsInTableView:self.chatContent];
  1938. if (indexPath.section >= sectionCount) {
  1939. return NO;
  1940. }
  1941. NSInteger rowCount = [self.chatContent.dataSource tableView:self.chatContent numberOfRowsInSection:indexPath.section];
  1942. if (indexPath.row >= rowCount) {
  1943. return NO;
  1944. }
  1945. return YES;
  1946. }
  1947. - (void)addLoadEarlierMessagesHUD {
  1948. if ([MBProgressHUD HUDForView:self.view] != nil) {
  1949. return;
  1950. }
  1951. MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
  1952. hud.label.text = [BundleUtil localizedStringForKey:@"load_earlier_messages"];
  1953. [hud.button setTitle:NSLocalizedString(@"cancel", nil) forState:UIControlStateNormal];
  1954. [hud.button addTarget:self action:@selector(cancelShowQuotedMessage) forControlEvents:UIControlEventTouchUpInside];
  1955. }
  1956. - (void)cancelShowQuotedMessage {
  1957. _cancelShowQuotedMessage = YES;
  1958. }
  1959. #pragma mark - Preview image delegate
  1960. - (void)previewImageControllerDidChooseToSend:(PreviewImageViewController *)previewController imageData:(NSData *)image {
  1961. [self sendImageData:image];
  1962. [self dismissViewControllerAnimated:YES completion:nil];
  1963. }
  1964. - (void)previewImageControllerDidChooseToSend:(PreviewImageViewController *)previewController gif:(NSData *)gifData {
  1965. [self previewImageControllerDidChooseToSend:previewController imageData:gifData];
  1966. }
  1967. - (void)previewImageControllerDidChooseToCancel:(PreviewImageViewController *)previewController {
  1968. [self dismissViewControllerAnimated:YES completion:nil];
  1969. }
  1970. - (void)sendImageData:(NSData *)imageData {
  1971. if (imageData == nil)
  1972. return;
  1973. ImageURLSenderItemCreator *imageSender = [[ImageURLSenderItemCreator alloc] init];
  1974. CFStringRef uti = [ImageURLSenderItemCreator getUTIFor:imageData];
  1975. if (uti == nil) {
  1976. uti = kUTTypeJPEG;
  1977. }
  1978. URLSenderItem *item = [imageSender senderItemFrom:imageData uti:(__bridge NSString *)uti];
  1979. FileMessageSender *sender = [[FileMessageSender alloc] init];
  1980. [sender sendItem:item inConversation:conversation];
  1981. }
  1982. #pragma mark - scroll view delegate
  1983. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  1984. if (visible) {
  1985. [self updateScrollDownButtonAnimated:YES];
  1986. CGFloat yDiff = scrollView.contentOffset.y - lastScrollOffset.y;
  1987. if (yDiff > 32.0 && lastScrollOffset.y != 0.0) {
  1988. if (scrollView.isDragging) {
  1989. [self hideHeaderWithDuration:0.2];
  1990. lastScrollOffset = scrollView.contentOffset;
  1991. }
  1992. }
  1993. }
  1994. }
  1995. -(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  1996. lastScrollOffset = scrollView.contentOffset;
  1997. if (self.searching) {
  1998. [headerView resignFirstResponder];
  1999. }
  2000. }
  2001. -(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
  2002. if (velocity.y == 0.0) {
  2003. return;
  2004. }
  2005. BOOL velocityTriggerUp = velocity.y < -0.8;
  2006. BOOL velocityTriggerDown = velocity.y > 0.0;
  2007. CGFloat topOffset = [self topOffsetForVisibleContent];
  2008. BOOL offsetTrigger = targetContentOffset->y <= -topOffset;
  2009. // scrollViewWillEndDragging velocity is points/milliseconds
  2010. CGFloat duration = fabs(scrollView.contentOffset.y - targetContentOffset->y)/fabs(velocity.y*1000);
  2011. duration = fminf(duration, 0.8);
  2012. duration = fmaxf(duration, 0.2);
  2013. if (velocityTriggerUp) {
  2014. [self showHeaderWithDuration:duration completion:nil];
  2015. } else if (offsetTrigger && velocity.y < 0.0) {
  2016. [self showHeaderWithDuration:duration completion:nil];
  2017. } else if (velocityTriggerDown) {
  2018. [self hideHeaderWithDuration:duration];
  2019. }
  2020. }
  2021. -(BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
  2022. isScrollingToTop = YES;
  2023. [self hideKeyboardTemporarily:NO];
  2024. if ([self checkShouldShowHeader]) {
  2025. CGFloat yOffset = scrollView.contentOffset.y - [headerView getHeight];
  2026. UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
  2027. [UIView animateWithDuration:0.3 delay:0.0 options:options animations:^{
  2028. scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, yOffset);
  2029. } completion:^(BOOL finished) {
  2030. isScrollingToTop = NO;
  2031. }];
  2032. };
  2033. return YES;
  2034. }
  2035. #pragma mark - header view show/hide
  2036. - (void)showHeaderWithDuration:(CGFloat)duration completion:(void (^ __nullable)(BOOL finished))completion {
  2037. CGFloat targetOffset = [self topOffsetForVisibleContent];
  2038. CGRect targetRect = [RectUtil setYPositionOf:headerView.frame y:targetOffset];
  2039. if (headerView.hidden == NO && CGRectEqualToRect(targetRect, headerView.frame)) {
  2040. return;
  2041. }
  2042. if ([self shouldShowHeader] == NO) {
  2043. return;
  2044. }
  2045. CGFloat headerHeight = [headerView getHeight];
  2046. headerView.frame = [RectUtil setYPositionOf:headerView.frame y: -headerHeight];
  2047. headerView.hidden = NO;
  2048. showHeader = YES;
  2049. UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
  2050. [UIView animateWithDuration:duration delay:0.0 options:options animations:^{
  2051. headerView.frame = targetRect;
  2052. [self updateChatContentInset];
  2053. } completion:^(BOOL finished) {
  2054. headerView.hidden = NO;
  2055. if (completion != nil) {
  2056. completion(YES);
  2057. }
  2058. }];
  2059. }
  2060. - (void)hideHeaderWithDuration:(CGFloat)duration {
  2061. CGFloat headerHeight = [headerView getHeight];
  2062. CGRect targetRect = [RectUtil setYPositionOf:headerView.frame y: -headerHeight];
  2063. if (headerView.hidden == YES && CGRectEqualToRect(targetRect, headerView.frame)) {
  2064. return;
  2065. }
  2066. if ([self shouldShowHeader]) {
  2067. return;
  2068. }
  2069. showHeader = NO;
  2070. UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
  2071. [UIView animateWithDuration:duration delay:0.0 options:options animations:^{
  2072. headerView.frame = targetRect;
  2073. } completion:^(BOOL finished) {
  2074. [self updateChatContentInset];
  2075. headerView.hidden = YES;
  2076. }];
  2077. }
  2078. - (BOOL)shouldShowHeader {
  2079. if (_isOpenWithForceTouch) {
  2080. chatBarWrapper.hidden = YES;
  2081. if (@available(iOS 13.0, *)) {
  2082. if ([self chatContentSmallerThanVisibleArea]) {
  2083. chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, [self unvisibleChatHeight]);
  2084. } else {
  2085. chatContent.frame = CGRectMake(chatContent.frame.origin.x, CGRectGetHeight(chatContent.frame) - [self unvisibleChatHeight], chatContent.frame.size.width, [self unvisibleChatHeight]);
  2086. }
  2087. } else {
  2088. if ([self chatContentSmallerThanVisibleArea]) {
  2089. chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, [self unvisibleChatHeight]);
  2090. } else {
  2091. chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y - CGRectGetHeight(headerView.frame), chatContent.frame.size.width, [self unvisibleChatHeight] + CGRectGetHeight(headerView.frame));
  2092. }
  2093. }
  2094. return NO;
  2095. }
  2096. else if (_searching) {
  2097. // always show when searching
  2098. return YES;
  2099. } else if (SYSTEM_IS_IPAD == NO && UIDeviceOrientationIsLandscape((UIDeviceOrientation)[[UIApplication sharedApplication] statusBarOrientation])) {
  2100. //hide for landscape - only on iPhone
  2101. return NO;
  2102. } else if (lastKeyboardHeight > 0.0 && !isScrollingToTop) {
  2103. //hide if not enough space
  2104. return NO;
  2105. } else if ([self chatContentSmallerThanVisibleArea]) {
  2106. // show if area not filled with chat content
  2107. return YES;
  2108. } else if (self.editing) {
  2109. // hide when editing
  2110. return NO;
  2111. } else if (isFirstAppearance) {
  2112. // initially hidden
  2113. return NO;
  2114. } else if (isScrollingToTop) {
  2115. // keep when scrolling to top
  2116. return YES;
  2117. } else if (shouldScrollDown) {
  2118. // don't show header when initially scrolling down
  2119. return NO;
  2120. } else if (isScrollingToUnreadMessages) {
  2121. // don't show header when scrolling to first unread messages
  2122. isScrollingToUnreadMessages = NO;
  2123. return NO;
  2124. } else if (isNewMessageReceivedInActiveChat) {
  2125. // don't show header if receive new message in a active chat
  2126. isNewMessageReceivedInActiveChat = NO;
  2127. return NO;
  2128. }
  2129. // otherwise toggle
  2130. return headerView.hidden;
  2131. }
  2132. - (BOOL)checkShouldShowHeader {
  2133. if ([self shouldShowHeader]) {
  2134. [self showHeaderWithDuration:0.3 completion:nil];
  2135. return YES;
  2136. } else {
  2137. [self hideHeaderWithDuration:0.3];
  2138. return NO;
  2139. }
  2140. }
  2141. - (void)toggleHeader {
  2142. if (headerView.hidden) {
  2143. [self hideKeyboardTemporarily:NO];
  2144. [self showHeaderWithDuration:0.3 completion:nil];
  2145. } else {
  2146. [self hideHeaderWithDuration:0.3];
  2147. }
  2148. }
  2149. - (BOOL)visible {
  2150. return visible;
  2151. }
  2152. - (CGFloat)visibleChatHeight {
  2153. return CGRectGetHeight(self.chatContent.frame) - chatContent.contentInset.top;
  2154. }
  2155. - (CGFloat)unvisibleChatHeight {
  2156. return CGRectGetHeight(self.chatContent.frame) + chatBarWrapper.frame.size.height;
  2157. }
  2158. - (BOOL)chatContentSmallerThanVisibleArea {
  2159. CGFloat heightOfVisibleChatView = [self visibleChatHeight] - CGRectGetHeight(headerView.frame);
  2160. return heightOfVisibleChatView - chatContent.contentSize.height >= 0.0;
  2161. }
  2162. #pragma mark - ChatViewHeaderDelegate
  2163. -(void)didChangeHeightTo:(CGFloat)newHeight {
  2164. [self updateChatContentInset];
  2165. }
  2166. #pragma mark - GroupDetailsViewControllerDelegate
  2167. - (void)presentGroupDetails:(GroupDetailsViewController *)groupDetailsViewController onCompletion:(GroupDetailsCompletionBlock)onCompletion {
  2168. // not used
  2169. }
  2170. - (void)updateMembersObserver:(NSSet *)oldMembers newMembers:(NSSet *)newMembers {
  2171. if (oldMembers != nil && oldMembers != (id)[NSNull null]) {
  2172. [oldMembers enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
  2173. @try {
  2174. [contact removeObserver:self forKeyPath:@"displayName"];
  2175. }
  2176. @catch (NSException * __unused exception) {}
  2177. }];
  2178. }
  2179. if (newMembers != nil && newMembers != (id)[NSNull null]) {
  2180. [newMembers enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
  2181. @try {
  2182. [contact addObserver:self forKeyPath:@"displayName" options:0 context:nil];
  2183. }
  2184. @catch (NSException * __unused exception) {}
  2185. }];
  2186. }
  2187. }
  2188. # pragma mark - preview actions
  2189. - (NSArray<id<UIPreviewActionItem>> *)previewActionItems {
  2190. NSMutableArray *previewActions = [NSMutableArray array];
  2191. if (_delegate == nil) {
  2192. return previewActions;
  2193. }
  2194. if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
  2195. NSString *actionTitle = NSLocalizedString(@"take_photo_or_video", nil);
  2196. UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2197. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2198. SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController];
  2199. sendMediaAction.mediaPickerType = MediaPickerTakePhoto;
  2200. currentAction = sendMediaAction;
  2201. [sendMediaAction executeAction];
  2202. }];
  2203. }];
  2204. [previewActions addObject:shareAction];
  2205. }
  2206. if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
  2207. NSString *actionTitle = NSLocalizedString(@"choose_existing", nil);
  2208. UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2209. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2210. SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController];
  2211. sendMediaAction.mediaPickerType = MediaPickerChooseExisting;
  2212. currentAction = sendMediaAction;
  2213. [sendMediaAction executeAction];
  2214. }];
  2215. }];
  2216. [previewActions addObject:shareAction];
  2217. }
  2218. if ([CLLocationManager locationServicesEnabled]) {
  2219. NSString *actionTitle = NSLocalizedString(@"share_location", nil);
  2220. UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2221. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2222. SendLocationAction *sendLocationAction = [SendLocationAction actionForChatViewController:chatViewController];
  2223. currentAction = sendLocationAction;
  2224. [sendLocationAction executeAction];
  2225. }];
  2226. }];
  2227. [previewActions addObject:shareAction];
  2228. }
  2229. if ([PlayRecordAudioViewController canRecordAudio]) {
  2230. NSString *actionTitle = NSLocalizedString(@"record_audio", nil);
  2231. UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2232. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2233. [chatViewController startRecordingAudio];
  2234. }];
  2235. }];
  2236. [previewActions addObject:shareAction];
  2237. }
  2238. NSString *actionTitle = NSLocalizedStringFromTable(@"ballot_create", @"Ballot", nil);
  2239. UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2240. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2241. [chatViewController createBallot];
  2242. }];
  2243. }];
  2244. [previewActions addObject:shareAction];
  2245. actionTitle = NSLocalizedString(@"share_file", nil);
  2246. shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
  2247. [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
  2248. [chatViewController sendFile];
  2249. }];
  2250. }];
  2251. [previewActions addObject:shareAction];
  2252. return previewActions;
  2253. }
  2254. #pragma mark - UIViewControllerPreviewingDelegate
  2255. - (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
  2256. if ([viewControllerToCommit isKindOfClass:[UINavigationController class]]) {
  2257. UINavigationController *navigationController = (UINavigationController *)viewControllerToCommit;
  2258. navigationController.navigationBar.hidden = NO;
  2259. if ([navigationController.topViewController isKindOfClass:[MWPhotoBrowser class]]) {
  2260. ((MWPhotoBrowser*)navigationController.topViewController).peeking = NO;
  2261. }
  2262. [self presentViewController:viewControllerToCommit animated:YES completion:nil];
  2263. }
  2264. else if ([viewControllerToCommit isKindOfClass:[ThreemaSafariViewController class]]) {
  2265. [self.navigationController presentViewController:viewControllerToCommit animated:false completion:^{
  2266. [viewControllerToCommit dismissViewControllerAnimated:false completion:^{
  2267. [[UIApplication sharedApplication] openURL:((ThreemaSafariViewController *)viewControllerToCommit).url options:@{} completionHandler:nil];
  2268. }];
  2269. }];
  2270. } else {
  2271. [self.navigationController pushViewController:viewControllerToCommit animated:YES];
  2272. }
  2273. }
  2274. - (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
  2275. UIView *view = [self.view hitTest:location withEvent:nil];
  2276. ChatMessageCell *cell = (ChatMessageCell *)[Utils view:view getSuperviewOfKind:[ChatMessageCell class]];
  2277. if (cell) {
  2278. forceTouching = YES;
  2279. UIViewController *previewController = [cell previewViewController];
  2280. if ([[cell previewViewControllerFor:previewingContext viewControllerForLocation:location] isKindOfClass:[ThreemaSafariViewController class]]) {
  2281. previewController = [cell previewViewControllerFor:previewingContext viewControllerForLocation:location];
  2282. if (!previewController || UIAccessibilityIsVoiceOverRunning()) {
  2283. return nil;
  2284. }
  2285. _Bool legalURL = [IDNSafetyHelper isLegalURLWithUrl:((ThreemaSafariViewController *)previewController).url];
  2286. if (!legalURL) {
  2287. return nil;
  2288. }
  2289. }
  2290. else {
  2291. if ([previewController isKindOfClass:[UINavigationController class]]) {
  2292. UINavigationController *navigationController = (UINavigationController *)previewController;
  2293. navigationController.navigationBar.hidden = YES;
  2294. }
  2295. }
  2296. previewingContext.sourceRect = [self.view convertRect:cell.frame fromView:self.chatContent];
  2297. return previewController;
  2298. }
  2299. return nil;
  2300. }
  2301. #pragma mark - Audio player/recorder delegate
  2302. - (void)audioPlayerDidHide {
  2303. UITableViewCell *selectedCell = [self.chatContent cellForRowAtIndexPath:selectedAudioMessage];
  2304. if (selectedCell) {
  2305. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, selectedCell);
  2306. } else {
  2307. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
  2308. }
  2309. audioRecorder = nil;
  2310. selectedAudioMessage = nil;
  2311. }
  2312. #pragma mark - Notifications
  2313. - (void)showProfilePictureChanged:(NSNotification*)notification {
  2314. [headerView refresh];
  2315. }
  2316. #pragma mark - PPAssetsActionHelperDelegate
  2317. - (void)assetsActionHelperDidCancel:(PPAssetsActionHelper *)picker {
  2318. // do nothing
  2319. }
  2320. - (void)assetsActionHelper:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
  2321. }
  2322. - (void)assetActionHelperDidSelectOwnOption:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
  2323. }
  2324. - (void)assetsActionHelperDidSelectOwnSnapButton:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
  2325. [self dismissViewControllerAnimated:YES completion:nil];
  2326. if (assets && assets.count) {
  2327. SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
  2328. [action showPreviewForAssets:assets];
  2329. } else {
  2330. SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
  2331. action.mediaPickerType = MediaPickerChooseExisting;
  2332. currentAction = action;
  2333. [action executeAction];
  2334. }
  2335. }
  2336. - (void)assetsActionHelperDidSelectLiveCameraCell:(PPAssetsActionHelper *)picker {
  2337. [self dismissViewControllerAnimated:YES completion:nil];
  2338. SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
  2339. action.mediaPickerType = MediaPickerTakePhoto;
  2340. currentAction = action;
  2341. [action executeAction];
  2342. }
  2343. - (void)assetsActionHelperDidSelectLocation:(PPAssetsActionHelper *)picker {
  2344. [self dismissViewControllerAnimated:YES completion:nil];
  2345. SendLocationAction *action = [SendLocationAction actionForChatViewController:self];
  2346. currentAction = action;
  2347. [action executeAction];
  2348. }
  2349. - (void)assetsActionHelperDidSelectRecordAudio:(PPAssetsActionHelper *)picker {
  2350. [self dismissViewControllerAnimated:YES completion:^{
  2351. [self startRecordingAudio];
  2352. }];
  2353. }
  2354. - (void)assetsActionHelperDidSelectCreateBallot:(PPAssetsActionHelper *)picker {
  2355. [self dismissViewControllerAnimated:YES completion:nil];
  2356. [self createBallot];
  2357. }
  2358. - (void)assetsActionHelperDidSelectShareFile:(PPAssetsActionHelper *)picker {
  2359. [self dismissViewControllerAnimated:YES completion:nil];
  2360. [self sendFile];
  2361. }
  2362. #pragma mark - Voip
  2363. - (void)startVoipCall:(BOOL)withVideo {
  2364. [self hideHeaderWithDuration:0.0];
  2365. [FeatureMask checkFeatureMask:FEATURE_MASK_VOIP forContacts:[NSSet setWithObjects:self.conversation.contact, nil] onCompletion:^(NSArray *unsupportedContacts) {
  2366. if (unsupportedContacts.count == 0) {
  2367. VoIPCallUserAction *action = [[VoIPCallUserAction alloc] initWithAction:withVideo ? ActionCallWithVideo : ActionCall contact:conversation.contact callId:nil completion:nil];
  2368. [[VoIPCallStateManager shared] processUserAction:action];
  2369. } else {
  2370. [UIAlertTemplate showAlertWithOwner:self title:NSLocalizedString(@"call_voip_not_supported_title", nil) message:NSLocalizedString(@"call_voip_not_supported_text", nil) actionOk:nil];
  2371. }
  2372. }];
  2373. }
  2374. @end