12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2012-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- @import SafariServices;
- #import <AVKit/AVKit.h>
- #import <UIKit/UIKit.h>
- #import "ChatViewController.h"
- #import "AppDelegate.h"
- #import "ChatBar.h"
- #import "ChatDefines.h"
- #import "MessageSender.h"
- #import "ContactDetailsViewController.h"
- #import <QuartzCore/QuartzCore.h>
- #import <MediaPlayer/MediaPlayer.h>
- #import "ProtocolDefines.h"
- #import "UserSettings.h"
- #import "VideoMessageLoader.h"
- #import "PreviewImageViewController.h"
- #import "LocationViewController.h"
- #import "MessageDetailsViewController.h"
- #import "ImageMessageLoader.h"
- #import "GroupDetailsViewController.h"
- #import "PlayRecordAudioViewController.h"
- #import "NonFirstResponderActionSheet.h"
- #import "EntityManager.h"
- #import "BallotDispatcher.h"
- #import "RectUtil.h"
- #import "MessageFetcher.h"
- #import "PermissionChecker.h"
- #import "StatusNavigationBar.h"
- #import "ModalPresenter.h"
- #import "BundleUtil.h"
- #import "DocumentPicker.h"
- #import "Utils.h"
- #import "ChatMessageCell.h"
- #import "ModalNavigationController.h"
- #import "LicenseStore.h"
- #import "FeatureMask.h"
- #import "MessageDraftStore.h"
- #import "MWPhotoBrowser.h"
- #import "AppGroup.h"
- #import "VoIPHelper.h"
- #import "NotificationManager.h"
- #import "NSString+Hex.h"
- #import "NibUtil.h"
- #import "ChatDeleteAction.h"
- #import "SendMediaAction.h"
- #import "SendLocationAction.h"
- #import "ChatTableDataSource.h"
- #import "BallotResultViewController.h"
- #import "BallotVoteViewController.h"
- #import "QuoteParser.h"
- #import "FeatureMask.h"
- #import "Threema-Swift.h"
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- @interface ChatViewController () <UIViewControllerPreviewingDelegate, PPAssetsActionHelperDelegate, GroupDetailsViewControllerDelegate>
- @property ChatTableDataSource *tableDataSource;
- @property UIImageView *backgroundView;
- @property BOOL isDirty;
- @property NSMutableArray *imageMessageObserverList;
- @property NSMutableArray *locationMessageObserverList;
- @property UILabel *titleLabel;
- @end
- @implementation ChatViewController {
- BOOL visible;
- BOOL shouldScrollDown;
- UIView *containerView;
- UIView *chatBarWrapper;
- CGFloat wrapperBottomPadding;
- NSMutableArray *readReceiptQueue;
- BOOL inhibitScrollBottom;
- BOOL haveNewMessages;
- BOOL typingIndicatorSent;
- UIButton *scrollDownButton;
-
- LocationMessage* locationToShow;
-
- int numberOfPages;
-
- BaseMessage *detailsMessage;
-
- NSURL *tmpAudioVideoUrl;
- NSIndexPath *lastIndexPathBeforeRotation;
- UIInterfaceOrientation lastInterfaceOrientation;
- NSString *prevAudioCategory;
- AVPlayerViewController *player;
-
- NSString *initialMessageText;
-
- BOOL ignoreNextTap;
-
- MessageFetcher *messageFetcher;
-
- PlayRecordAudioViewController *audioRecorder;
-
- CGFloat lastKeyboardHeight;
-
- BOOL isScrollingToTop;
- BOOL isScrollingToUnreadMessages;
- BOOL isNewMessageReceivedInActiveChat;
- BOOL isFirstAppearance;
- CGPoint lastScrollOffset;
-
- EntityManager *entityManager;
-
- ChatViewControllerAction *currentAction;
-
- NSInteger currentOffset;
- BOOL forceTouching;
-
- NSIndexPath *selectedAudioMessage;
-
- PPAssetsActionHelper *assetActionHelper;
-
- CGRect lastKeyboardEndFrame;
- NSTimeInterval lastAnimationDuration;
- UIViewAnimationCurve lastAnimationCurve;
-
- BOOL _cancelShowQuotedMessage;
-
- UITapGestureRecognizer *tapGestureRecognizer;
-
- int _deleteMediaCount;
-
- /// When was the table fully reloaded last time?
- NSDate *lastFullConversationUpdate;
-
- BOOL _assetActionHelperWillPresent;
- }
- @synthesize sentMessageSound;
- @synthesize chatContent;
- @synthesize chatBar;
- @synthesize headerView;
- @synthesize conversation;
- @synthesize imageDataToSend;
- @synthesize deleteMediaTotal;
- @synthesize showHeader;
- #pragma mark NSObject
- - (void)dealloc {
- for (ImageMessage *message in _imageMessageObserverList) {
- [message removeObserver:self forKeyPath:@"thumbnail"];
- }
- [_imageMessageObserverList removeAllObjects];
-
- for (LocationMessage *message in _locationMessageObserverList) {
- [message removeObserver:self forKeyPath:@"reverseGeocodingResult"];
- }
- [_locationMessageObserverList removeAllObjects];
-
- if (sentMessageSound) {
- AudioServicesDisposeSystemSoundID(sentMessageSound);
- }
-
- [self removeConversationObservers];
-
- chatContent.delegate = nil;
- chatContent.dataSource = nil;
- chatBar.delegate = nil;
-
- _tableDataSource = nil;
-
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
- - (id)initWithCoder:(NSCoder *)aDecoder {
- self = [super initWithCoder:aDecoder];
- if (self) {
- readReceiptQueue = [NSMutableArray array];
-
- entityManager = [[EntityManager alloc] init];
-
- _imageMessageObserverList = [NSMutableArray new];
- _locationMessageObserverList = [NSMutableArray new];
-
- _isOpenWithForceTouch = NO;
- _assetActionHelperWillPresent = false;
- }
- return self;
- }
- - (BOOL)shouldAutorotate {
- return NO;
- }
- - (void)setSearching:(BOOL)searching {
- _searching = searching;
- _tableDataSource.searching = searching;
-
- CGFloat barHeight = 0.0;
- if (_searching) {
- chatBarWrapper.hidden = YES;
- } else {
- barHeight = chatBarWrapper.frame.size.height;
- chatBarWrapper.hidden = NO;
- }
-
- chatContent.frame = [RectUtil setHeightOf:chatContent.frame height: containerView.frame.size.height - barHeight - [self tabBarHeight]];
- }
- - (void)setSearchPattern:(NSString *)searchPattern {
- _searchPattern = searchPattern;
- _tableDataSource.searchPattern = searchPattern;
- }
- - (NSIndexPath *)indexPathForMessage:(BaseMessage *)message {
- return [_tableDataSource indexPathForMessage:message];
- }
- - (id)objectAtIndexPath:(NSIndexPath *)indexPath {
- return [_tableDataSource objectForIndexPath:indexPath];
- }
- - (BOOL)hasAlpha : (UIImage*) img {
- CGImageAlphaInfo alpha = CGImageGetAlphaInfo(img.CGImage);
- return (
- alpha == kCGImageAlphaFirst ||
- alpha == kCGImageAlphaLast ||
- alpha == kCGImageAlphaPremultipliedFirst ||
- alpha == kCGImageAlphaPremultipliedLast
- );
- }
- #pragma mark UIViewController
- - (void)viewWillLayoutSubviews {
- UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;
- if (self.view.frame.size.width > self.view.frame.size.height) {
- orientation = UIInterfaceOrientationLandscapeLeft;
- }
-
- [self updateConversationClearContent:NO];
-
- [self updateBackgroundForOrientation:orientation duration:0.0];
- }
- - (void)viewDidLayoutSubviews {
- CGFloat top;
- if (@available(iOS 11.0, *)) {
- top = self.view.safeAreaLayoutGuide.layoutFrame.origin.y;
- } else {
- top = self.topLayoutGuide.length;
- }
- if (showHeader) {
- headerView.frame = [RectUtil setYPositionOf:headerView.frame y:top];
- }
-
- // self.topLayoutGuide is only available after view was added -> make sure offset is set
- [self updateChatContentInset];
- }
- - (void)viewDidLoad {
- [super viewDidLoad];
-
- [[UserSettings sharedUserSettings] checkWallpaper];
-
- self.navigationController.interactivePopGestureRecognizer.enabled = YES;
- self.navigationController.interactivePopGestureRecognizer.delegate = nil;
-
- /* Load sounds */
- NSString *sendPath = [BundleUtil pathForResource:@"sent_message" ofType:@"caf"];
- CFURLRef baseURL = (__bridge CFURLRef)[NSURL fileURLWithPath:sendPath];
- AudioServicesCreateSystemSoundID(baseURL, &sentMessageSound);
-
- self.navigationController.tabBarItem.image = [UIImage imageNamed:@"TabBar-Chats"];
- self.navigationController.tabBarItem.selectedImage = [UIImage imageNamed:@"TabBar-Chats"];
- self.automaticallyAdjustsScrollViewInsets = NO;
-
- self.view.backgroundColor = [Colors backgroundChat]; // shown during rotation
-
- if (@available(iOS 11.0, *)) {
- containerView = [[UIView alloc] initWithFrame:self.view.safeAreaLayoutGuide.layoutFrame];
- } else {
- containerView = [[UIView alloc] initWithFrame:self.view.frame];
- }
-
- containerView.backgroundColor = [UIColor clearColor];
- containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- [self.view addSubview:containerView];
-
- // Calculate initial height based on font size (ugly hack)
- float fontSize = [UserSettings sharedUserSettings].chatFontSize;
- float initialChatBarHeight = kChatBarHeight1;
- if (fontSize >= 36)
- initialChatBarHeight = 64.0;
- else if (fontSize >= 30)
- initialChatBarHeight = 57.0;
- else if (fontSize >= 28)
- initialChatBarHeight = 55.0;
- else if (fontSize >= 24)
- initialChatBarHeight = 50.0;
- else if (fontSize >= 20)
- initialChatBarHeight = 45.0;
-
- CGFloat initialChatBarWrapperPadding = 0.0f;
- if ([AppDelegate hasBottomSafeAreaInsets]) {
- initialChatBarWrapperPadding += kIphoneXChatBarBottomPadding;
- wrapperBottomPadding = kIphoneXChatBarBottomPadding;
- }
-
- // Create chatContent
- CGFloat chatContentHeight = containerView.frame.size.height - initialChatBarHeight - [self tabBarHeight] - initialChatBarWrapperPadding;
- CGRect chatContectRect = CGRectMake(0.0f, 0.0f, containerView.frame.size.width, chatContentHeight);
- chatContent = [[UITableView alloc] initWithFrame:chatContectRect];
- chatContent.clearsContextBeforeDrawing = NO;
- chatContent.backgroundColor = [UIColor clearColor];
- chatContent.separatorStyle = UITableViewCellSeparatorStyleNone;
- chatContent.separatorColor = [UIColor clearColor];
- chatContent.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- chatContent.allowsSelection = NO;
- chatContent.allowsSelectionDuringEditing = YES;
- chatContent.allowsMultipleSelectionDuringEditing = YES;
- [chatContent registerNib:[UINib nibWithNibName:@"UnreadMessageLineCell" bundle:nil] forCellReuseIdentifier:@"UnreadMessageLineCell"];
-
- tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(chatContentTapped:)];
- tapGestureRecognizer.numberOfTapsRequired = 1;
- tapGestureRecognizer.delaysTouchesEnded = false;
- tapGestureRecognizer.cancelsTouchesInView = false;
- [chatContent addGestureRecognizer:tapGestureRecognizer];
- [containerView addSubview:chatContent];
-
- [self setupHeaderView];
-
- chatContent.tableHeaderView = self.chatContentHeader;
- [self updateChatContentInset];
-
- CGRect chatBarWrapperRect = CGRectMake(0.0f, chatContentHeight, containerView.frame.size.width, initialChatBarHeight + initialChatBarWrapperPadding);
- chatBarWrapper = [[UIView alloc] initWithFrame:chatBarWrapperRect];
- chatBarWrapper.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
-
- CGRect chatBarRect = CGRectMake(0.0f, 0.0f, chatBarWrapperRect.size.width, initialChatBarHeight);
-
- chatBar = [[ChatBar alloc] initWithFrame: chatBarRect];
- chatBar.delegate = self;
- chatBar.canSendAudio = [PlayRecordAudioViewController canRecordAudio];
- if (conversation.isGroup == true) {
- [chatBar setupMentions:conversation.sortedMembers];
- }
-
- /* Put chat bar in a wrapper so we can adjust the bottom offset for iPhone X */
- [chatBarWrapper addSubview:chatBar];
- [containerView addSubview:chatBarWrapper];
- [containerView sendSubviewToBack:chatBarWrapper];
-
- [self setupNavigationBar];
-
- /* Scroll down button */
- scrollDownButton = [UIButton buttonWithType:UIButtonTypeCustom];
- [scrollDownButton setAccessibilityLabel:NSLocalizedString(@"scoll_down_text", @"")];
- [scrollDownButton addTarget:self action:@selector(scrollDownButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
- [containerView addSubview:scrollDownButton];
-
- isFirstAppearance = YES;
- [self updateContactDisplay];
-
- lastInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
-
- [self registerForPreviewingWithDelegate:self sourceView:self.view];
-
- [self setupColors];
-
- self.deleteMediaTotal = 0;
- _deleteMediaCount = 0;
- }
- - (void)setupHeaderView {
- headerView = (ChatViewHeader *)[NibUtil loadViewFromNibWithName:@"ChatViewHeader"];
- headerView.chatViewController = self;
- headerView.hidden = YES;
- headerView.delegate = self;
- [self.view addSubview: headerView];
- }
- - (void)setupNavigationBar {
- self.navigationItem.rightBarButtonItems = @[self.editButtonItem];
-
- _titleLabel = [[UILabel alloc] init];
- _titleLabel.font = [UIFont boldSystemFontOfSize:17.0f];
- _titleLabel.frame = CGRectMake(0, 0, 40, 28);
- UITapGestureRecognizer *titleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(titleTapped:)];
- [_titleLabel addGestureRecognizer:titleTapRecognizer];
- _titleLabel.userInteractionEnabled = YES;
- _titleLabel.text = conversation.displayName;
- _titleLabel.accessibilityIdentifier = @"TapHeaderView";
-
- self.navigationItem.titleView = _titleLabel;
- }
- - (void)setupColors {
- _titleLabel.textColor = [Colors fontNormal];
-
- self.loadEarlierMessages.titleLabel.font = [UIFont systemFontOfSize:17.0];
-
- self.loadEarlierMessages.backgroundColor = [[Colors backgroundBaseColor] colorWithAlphaComponent:0.3];
- self.loadEarlierMessages.layer.cornerRadius = 4.0;
-
- [self.loadEarlierMessages setTitleColor:[Colors fontLink] forState:UIControlStateNormal];
- [self.loadEarlierMessages setTitleColor:[Colors fontLight] forState:UIControlStateHighlighted];
-
- [self.navigationController.view setBackgroundColor:[Colors backgroundDark]];
- chatBarWrapper.backgroundColor = [Colors chatBarBackground];
- [self.view setBackgroundColor:[Colors backgroundChat]];
-
- // Set scroll down button image
- [scrollDownButton setImage:StyleKit.scrollDownButtonIcon forState:UIControlStateNormal];
- }
- - (void)refresh {
- [self setupBackground];
- [self setupColors];
-
- [headerView refresh];
-
- [chatBar refresh];
-
- [self.chatContent reloadData];
-
- self.navigationController.navigationBar.topItem.leftBarButtonItem.title = @"Back";
- }
- - (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated]; // below: work around for [chatContent flashScrollIndicators]
-
- _tableDataSource.openTableView = YES;
-
- DDLogVerbose(@"viewWillAppear, composing = %d", self.composing);
-
- [self registerForNotifications];
-
- [self updateConversationIfNeeded];
-
- [chatContent performSelector:@selector(flashScrollIndicators) withObject:nil afterDelay:0.0];
-
- /* update typing indicator on last cell */
- if ([_tableDataSource hasData]) {
- NSIndexPath *pathToLastCell = [_tableDataSource indexPathForLastCell];
- [self updateTypingIndicatorAtIndexPath:pathToLastCell];
- }
-
- if (initialMessageText) {
- chatBar.text = initialMessageText;
- initialMessageText = nil;
- }
-
- /* remove temporary audio/video file? */
- if (tmpAudioVideoUrl != nil) {
- NSError *error = nil;
- [[NSFileManager defaultManager] removeItemAtURL:tmpAudioVideoUrl error:&error];
- DDLogVerbose(@"Removing temporary audio/video file %@: %@", tmpAudioVideoUrl, error);
- tmpAudioVideoUrl = nil;
- }
-
- if (player != nil) {
- player = nil;
- }
-
- /* was there a rotation while we were hidden? */
- if ([[UIApplication sharedApplication] statusBarOrientation] != lastInterfaceOrientation) {
- [self updateTableForRotationToInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
- dispatch_async(dispatch_get_main_queue(), ^{
- /* Workaround as chatContent.frame won't be updated yet when we reposition the button below */
- [self repositionScrollDownButton];
- });
- }
-
- [self repositionScrollDownButton];
- [self updateScrollDownButtonAnimated:NO];
-
- /* send notification (e.g. for hiding toasts that apply to this conversation) */
- [[NSNotificationCenter defaultCenter] postNotificationName:@"ThreemaConversationOpened" object:conversation userInfo:nil];
-
- [self registerCustomMenuItems];
- isFirstAppearance = NO;
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showProfilePictureChanged:) name:kNotificationShowProfilePictureChanged object:nil];
-
- /* Load draft, if any */
- NSString *draft = [MessageDraftStore loadDraftForConversation:self.conversation];
- if (draft.length != 0 && chatBar.text.length == 0) {
- [chatBar updateMentionsFromDraft:draft];
- // chatBar.text = mentionsString;
- if ([self canBecomeFirstResponder]) {
- [chatBar becomeFirstResponder];
- }
- }
-
- /* correct the width of the headerView */
- if (@available(iOS 11.0, *)) {
- headerView.frame = CGRectMake(headerView.frame.origin.x, headerView.frame.origin.y, self.view.safeAreaLayoutGuide.layoutFrame.size.width, headerView.frame.size.height);
- } else {
- headerView.frame = CGRectMake(headerView.frame.origin.x, headerView.frame.origin.y, self.view.frame.size.width, headerView.frame.size.height);
- }
-
- // Remove unread line if unread count is 0
- if (conversation.unreadMessageCount.integerValue == 0) {
- [self removeUnreadLine:NO];
- }
-
- [headerView refresh];
-
- if (!_backgroundView) {
- [self setupBackground];
- }
-
- if (headerView.hidden) {
- [self hideHeaderWithDuration:0.3];
- } else {
- [self showHeaderWithDuration:0.3 completion:nil];
- }
-
- [self setupNavigationBar];
- [self setupColors];
-
- [self loadImagesIfNeeded];
-
- if (SYSTEM_IS_IPAD == true) {
- [_delegate pushSettingChanged:self.conversation];
- }
-
- [chatBar setupMentions:conversation.sortedMembers];
- }
- - (void)registerForNotifications {
- // Listen for keyboard.
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputModeDidChange:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
-
- // Listen for resign active notification
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resignActive:) name:UIApplicationWillResignActiveNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuWillHide:) name:UIMenuControllerWillHideMenuNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuDidHide:) name:UIMenuControllerDidHideMenuNotification object:nil];
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDirtyObjects:) name:kNotificationDBRefreshedDirtyObject object:nil];
- }
- - (void)showKeyboardConditionally {
- if (self.composing) {
- if ([self canBecomeFirstResponder]) {
- [chatBar becomeFirstResponder];
- }
- }
- }
- - (void)hideKeyboardTemporarily:(BOOL)temporarily {
- if (self.composing) {
- // can only be set to NO
- self.composing = temporarily;
- }
- dispatch_async(dispatch_get_main_queue(), ^{
- [chatBar resignFirstResponder];
- });
- }
- - (void)viewDidAppear:(BOOL)animated {
- [super viewDidAppear:animated];
- DDLogVerbose(@"viewDidAppear");
-
- [self resetUnreadMessageCount];
- ((StatusNavigationBar*)self.navigationController.navigationBar).ignoreSetItems = NO;
-
- [self scrollToUnreadMessage:animated];
-
- [self showKeyboardConditionally];
-
- visible = YES;
-
- [self processReadReceiptQueue];
-
- // free up memory in case we came back from photo browser
- [headerView cleanupMedia];
-
- /* send pending image */
- if (imageDataToSend != nil) {
- [self chatBar:chatBar didSendImageData:imageDataToSend];
- imageDataToSend = nil;
- }
-
- /* restore audio category? */
- NSInteger state = [[VoIPCallStateManager shared] currentCallState];
- if (prevAudioCategory != nil && state == CallStateIdle) {
- [[AVAudioSession sharedInstance] setCategory:prevAudioCategory error:nil];
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- });
- prevAudioCategory = nil;
- }
- [headerView showThreemaVideoCallInfo];
- }
- - (void)viewWillDisappear:(BOOL)animated {
- DDLogVerbose(@"viewWillDisappear, composing = %d", self.composing);
-
- /* Send stop typing indicator now, as it may be too late once we've deleted the conversation below */
- [chatBar stopTyping];
-
- /* Save draft in case we get killed */
- NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
- if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
- [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
- } else {
- [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
- }
-
- lastInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
- lastIndexPathBeforeRotation = [[self.chatContent indexPathsForVisibleRows] lastObject];
-
- // When the app is closed in this screen and has passlock enabled this gehts called while opening
- // the locked app. But we want to keep the unread line until unlock. (IOS-1463)
- if ([KKPasscodeLock sharedLock].isPasscodeRequired) {
- if (![AppDelegate sharedAppDelegate].isAppLocked && [AppDelegate sharedAppDelegate].isLockscreenDismissed) {
- [self removeUnreadLine:YES];
- }
- } else {
- [self removeUnreadLine:YES];
- }
-
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDirtyObjects:) name:kNotificationDBRefreshedDirtyObject object:nil];
-
- _tableDataSource.openTableView = NO;
-
- [super viewWillDisappear:animated];
- }
- - (void)viewDidDisappear:(BOOL)animated {
- DDLogVerbose(@"viewDidDisappear");
- visible = NO;
-
- /* Are we going back to ConversationsViewController, or to another view (e.g. contact details)? */
- if (self.navigationController.viewControllers == nil) {
- /* If our Conversation is still empty (no messages) and not a group conversation, delete it */
- if (conversation.messages.count == 0 && conversation.groupId == nil) {
- [entityManager performSyncBlockAndSafe:^{
- [[entityManager entityDestroyer] deleteObjectWithObject:conversation];
- }];
- }
- }
-
- [super viewDidDisappear:animated];
- }
- - (void)resignActive:(NSNotification*)notification {
- /* stop typing as the user is leaving the app */
- [chatBar stopTyping];
-
- /* Save draft in case we get killed */
- NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
- if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
- [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
- } else {
- [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
- }
-
- /* Remove unread line in active chat */
- [self removeUnreadLine:YES];
- }
- - (void)didBecomeActive:(NSNotification*)notification {
- [self updateConversationIfNeeded];
- [self resetUnreadMessageCount];
- [self processReadReceiptQueue];
-
- /* scroll the newest message if there is one */
- [self scrollToUnreadMessage:YES];
-
- [chatBar resetKeyboardType:NO];
-
- [self loadImagesIfNeeded];
- }
- - (void)removeUnreadLine:(BOOL)animated {
- dispatch_async(dispatch_get_main_queue(), ^{
- NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
- if (indexPath) {
-
- BOOL removed = [_tableDataSource removeUnreadLine];
- if (removed) {
- [chatContent beginUpdates];
- if (animated) {
- [chatContent deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
- } else {
- [chatContent deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
- }
- [chatContent endUpdates];
- }
-
- }
- });
- }
- - (void)scrollToUnreadMessage:(BOOL)animated {
- /* scroll the newest message if there is one */
- NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
- if (indexPath) {
- @try {
- isScrollingToUnreadMessages = YES;
- NSIndexPath *unreadLineIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
- [chatContent scrollToRowAtIndexPath:unreadLineIndexPath atScrollPosition:UITableViewScrollPositionTop animated:animated];
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, [chatContent cellForRowAtIndexPath:indexPath]);
- }
- @catch (NSException *exception) {
- ;//ignore
- }
-
- [self updateScrollDownButtonAnimated:NO];
- }
- }
- - (void)showContentAfterForceTouch {
- _isOpenWithForceTouch = NO;
- chatBarWrapper.hidden = NO;
-
- chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, chatBarWrapper.frame.origin.y);
- }
- - (void)updateLayoutAfterCall {
- if (@available(iOS 11.0, *)) {
- _backgroundView.frame = self.view.safeAreaLayoutGuide.layoutFrame;
- } else {
- _backgroundView.frame = self.view.frame;
- }
-
- if (headerView.hidden) {
- [self hideHeaderWithDuration:0.3];
- } else {
- headerView.hidden = YES;
- [self showHeaderWithDuration:0.3 completion:nil];
- }
- }
- - (void)openPushSettings {
- [self performSegueWithIdentifier:@"ShowPushSetting" sender:nil];
- }
- - (void)loadImagesIfNeeded {
- // check if there are messages with not loaded images
- NSArray *lastMessages = [messageFetcher last20Messages];
- for (BaseMessage *message in lastMessages) {
- if ([message isKindOfClass:[ImageMessage class]]) {
- ImageMessage *imageMessage = (ImageMessage *)message;
- if (imageMessage.image == nil) {
- /* Start loading image */
- ImageMessageLoader *loader = [[ImageMessageLoader alloc] init];
-
- [loader startWithMessage:imageMessage onCompletion:^(BaseMessage *message) {
- } onError:^(NSError *error) {
- DDLogError(@"Image message blob load failed with error: %@", error);
- }];
- }
- }
- }
- }
- - (void)setCurrentAction:(ChatViewControllerAction *)newAction {
- currentAction = newAction;
- }
- #pragma mark - notification observer
- - (void)refreshDirtyObjects:(NSNotification*)notification {
- NSManagedObjectID *objectID = [notification.userInfo objectForKey:kKeyObjectID];
- if (objectID && [objectID isEqual:self.conversation.objectID]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self updateConversation];
- });
- }
- }
- - (void)menuWillHide:(NSNotification*)notification {
- DDLogVerbose(@"menuWillHide");
- ignoreNextTap = YES;
- }
- - (void)menuDidHide:(NSNotification*)notification {
- DDLogVerbose(@"menuDidHide");
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- ignoreNextTap = NO;
- });
- [self registerCustomMenuItems];
- }
- - (void)registerCustomMenuItems {
- UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"scan_qr", nil) action:@selector(scanQrCode:)];
- [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
- }
- - (void)scanQrCode:(id)sender {
- /* dummy to avoid compiler warning */
- }
- - (void)scrollDownButtonPressed:(id)sender {
- [self scrollToBottomAnimated:YES];
- }
- - (void)repositionScrollDownButton {
- // Icon should be cached and quadratic
- CGFloat buttonSize = StyleKit.scrollDownButtonIcon.size.height / UIScreen.mainScreen.scale;
- CGFloat padding = 8;
-
- CGFloat chatWidth = self.chatContent.frame.size.width;
- if (@available(iOS 11.0, *)) {
- // Adhere safe area insets on X-devices
- chatWidth -= self.view.safeAreaInsets.right;
- }
-
- scrollDownButton.frame = CGRectMake((chatWidth - (buttonSize + padding)),
- (self.chatContent.frame.origin.y + self.chatContent.frame.size.height) - (buttonSize + padding),
- buttonSize, buttonSize);
- }
- - (void)updateScrollDownButtonAnimated:(BOOL)animated {
- CGFloat targetAlpha;
-
- if ([self isScrolledAtBottom]) {
- targetAlpha = 0;
- haveNewMessages = NO;
- } else
- targetAlpha = kScrollButtonAlpha;
-
- if (scrollDownButton.alpha == targetAlpha)
- return;
-
- if (animated) {
- [UIView animateWithDuration:0.5f animations:^{
- scrollDownButton.alpha = targetAlpha;
- }];
- } else {
- scrollDownButton.alpha = targetAlpha;
- }
- }
- -(UIInterfaceOrientationMask)supportedInterfaceOrientations {
- if (SYSTEM_IS_IPAD) {
- return UIInterfaceOrientationMaskAll;
- }
-
- return UIInterfaceOrientationMaskAllButUpsideDown;
- }
- - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
- lastIndexPathBeforeRotation = [[self.chatContent indexPathsForVisibleRows] lastObject];
-
- // override assumed table width for heightForRowAtIndexPath during rotation to get a smooth animation
- _tableDataSource.rotationOverrideTableWidth = self.chatContent.frame.size.width;
-
- _tableDataSource.rotationOverrideTableWidth = 0;
-
- [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
- {
- UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
- [self updateTableForRotationToInterfaceOrientation:orientation];
- [self repositionScrollDownButton];
- } completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
- {
- if (lastIndexPathBeforeRotation != nil) {
- @try {
- [self.chatContent scrollToRowAtIndexPath:lastIndexPathBeforeRotation atScrollPosition:UITableViewScrollPositionBottom animated:NO];
- } @catch (NSException *exception) {}
- }
-
- [self updateScrollDownButtonAnimated:YES];
- }];
-
- [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
- }
- - (void)setupBackground {
- UIImage *bgImage = nil;
- if ([UserSettings sharedUserSettings].wallpaper) {
- bgImage = [UserSettings sharedUserSettings].wallpaper;
- } else {
- if (![LicenseStore requiresLicenseKey]) {
- UIImage *chatBackground = [BundleUtil imageNamed:@"ChatBackground"];
- bgImage = [chatBackground drawImageWithTintColor:[Colors chatBackgroundLines]];
- }
- }
-
- if (_backgroundView) {
- [_backgroundView removeFromSuperview];
- }
-
- if (bgImage != nil) {
- if (@available(iOS 11.0, *)) {
- _backgroundView = [[UIImageView alloc] initWithFrame:self.view.safeAreaLayoutGuide.layoutFrame];
- } else {
- _backgroundView = [[UIImageView alloc] initWithFrame:self.view.frame];
- }
- _backgroundView.contentMode = UIViewContentModeScaleAspectFill;
- _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- _backgroundView.clipsToBounds = YES;
-
- if ([UserSettings sharedUserSettings].wallpaper) {
- _backgroundView.backgroundColor = [UIColor clearColor];
- _backgroundView.image = bgImage;
- } else {
- _backgroundView.backgroundColor = [[UIColor alloc] initWithPatternImage:bgImage];
- _backgroundView.image = nil;
- }
-
- [containerView addSubview:_backgroundView];
- [containerView sendSubviewToBack:_backgroundView];
- }
- }
- - (void)updateBackgroundForOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
-
- if (_backgroundView == nil) {
- [self setupBackground];
- }
-
- if ([UserSettings sharedUserSettings].wallpaper || _backgroundView == nil) {
- // do not rotate for custom wallpapers
- return;
- }
-
- CGFloat rotation;
- if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation== UIInterfaceOrientationLandscapeRight) {
- rotation = M_PI/2;
- } else {
- rotation = 0;
- }
-
- [UIView animateWithDuration:duration animations:^{
- _backgroundView.transform = CGAffineTransformMakeRotation(rotation);
- _backgroundView.frame = self.view.frame;
- }];
-
- }
- - (void)updateTableForRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
- [self checkShouldShowHeader];
-
- [self.chatContent beginUpdates];
- [self updateChatContentInset];
- [self.chatContent endUpdates];
-
- if (lastIndexPathBeforeRotation != nil) {
- dispatch_async(dispatch_get_main_queue(), ^{
- @try {
- [self.chatContent scrollToRowAtIndexPath:lastIndexPathBeforeRotation atScrollPosition:UITableViewScrollPositionBottom animated:NO];
- } @catch (NSException *exception) {}
- });
- }
-
- if (_searching == NO) {
- [chatBar resizeChatInput];
- }
- }
- - (void)moveContainerViewForKeyboardFrame:(CGRect)keyboardFrameInView willHide:(BOOL)willHide {
- CGRect containerViewFrame = containerView.frame;
- CGFloat keyboardHeight = willHide ? 0.0f : keyboardFrameInView.size.height - [self tabBarHeight];
-
- if (SYSTEM_IS_IPAD && !willHide) {
- // iPad with external keyboard needs special treatment, as it will be shown as a collapsed bar with
- // some buttons, but the height is still the same. Therefore we need to calculate the height from
- // the y offset of the keyboard frame and the screen height
- keyboardHeight = self.view.frame.size.height - keyboardFrameInView.origin.y - [self tabBarHeight];
- }
-
- if ([AppDelegate hasBottomSafeAreaInsets]) {
- if (willHide) {
- // Must add padding to chat bar wrapper for iPhone X
- wrapperBottomPadding = kIphoneXChatBarBottomPadding;
- } else {
- wrapperBottomPadding = 0;
- }
- [self chatBar:chatBar didChangeHeight:chatBar.frame.size.height];
- }
-
- containerViewFrame.origin.y = -keyboardHeight;
- containerView.frame = containerViewFrame;
-
- lastKeyboardHeight = keyboardHeight;
-
- [self updateChatContentInset];
-
- if (willHide == NO) {
- [self checkShouldShowHeader];
- }
- }
- - (void)removeConversationObservers {
- @try {
- [conversation removeObserver:self forKeyPath:@"messages"];
- [conversation removeObserver:self forKeyPath:@"unreadMessageCount"];
- [conversation removeObserver:self forKeyPath:@"typing"];
- [conversation removeObserver:self forKeyPath:@"displayName"];
- [conversation removeObserver:self forKeyPath:@"groupId"];
- [conversation removeObserver:self forKeyPath:@"members"];
- } @catch (NSException * __unused exception) {}
-
- [conversation.members enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
- @try {
- [contact removeObserver:self forKeyPath:@"displayName"];
- }
- @catch (NSException * __unused exception) {}
- }];
- }
- - (void)addConversationObservers {
- @try {
- /* observe this conversation in case new messages are added to it while we're open */
- [conversation addObserver:self forKeyPath:@"messages" options:NSKeyValueObservingOptionNew context:nil];
- [conversation addObserver:self forKeyPath:@"unreadMessageCount" options:0 context:nil];
- [conversation addObserver:self forKeyPath:@"typing" options:0 context:nil];
- [conversation addObserver:self forKeyPath:@"displayName" options:0 context:nil];
- [conversation addObserver:self forKeyPath:@"groupId" options:0 context:nil];
- [conversation addObserver:self forKeyPath:@"members" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- } @catch (NSException * __unused exception) {}
-
- [conversation.members enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
- @try {
- [contact addObserver:self forKeyPath:@"displayName" options:0 context:nil];
- }
- @catch (NSException * __unused exception) {}
- }];
- }
- - (CGFloat)topOffsetForVisibleContent {
- CGFloat topOffset;
- if (@available(iOS 11.0, *)) {
- topOffset = self.view.safeAreaLayoutGuide.layoutFrame.origin.y;
- } else {
- topOffset = self.topLayoutGuide.length;
- }
- return topOffset;
- }
- - (CGFloat)topOffsetForVisibleChatContent {
- CGFloat topOffset = [self topOffsetForVisibleContent];
-
- if (showHeader) {
- topOffset += [headerView getHeight];
- }
-
- return topOffset;
- }
- - (void)updateContactDisplay {
- [self updateConversation];
- }
- - (void)setConversation:(Conversation *)newConversation {
- if (conversation == newConversation)
- return;
-
- [self removeConversationObservers];
- conversation = newConversation;
-
- [self addConversationObservers];
-
- numberOfPages = 1;
-
- shouldScrollDown = YES;
- _isDirty = YES;
-
- messageFetcher = [MessageFetcher messageFetcherFor:conversation withEntityFetcher:entityManager.entityFetcher];
-
- currentOffset = -1;
- }
- - (NSInteger)messageOffset {
- return currentOffset;
- }
- - (void)cleanCellHeightCache {
- [_tableDataSource cleanCellHeightCache];
- }
- // Update conversation if last update was not today
- // This is needed to update the realtive table view headers
- - (void)updateConversationIfNeeded {
- if (lastFullConversationUpdate != nil) {
- NSCalendar *calendar = [NSCalendar currentCalendar];
- if (![calendar isDateInToday:lastFullConversationUpdate]) {
- [self updateConversation];
- }
- }
- }
- - (void)resetLastFullConversationUpdate {
- lastFullConversationUpdate = [NSDate date];
- }
- - (void)updateConversation {
- _isDirty = YES;
- [self updateConversationClearContent:YES];
- }
- - (void)updateConversationClearContent:(BOOL)clearContent {
- if (conversation == nil || self.isViewLoaded == NO || _isDirty == NO) {
- return;
- }
-
- _isDirty = NO;
- if (self.editing == YES) {
- self.editing = NO;
- }
-
- [self updateConversationLastMessage];
-
- headerView.conversation = conversation;
- [self setupNavigationBar];
- [self setupColors];
-
- NSInteger newOffset;
- NSInteger numberOfMessagesToLoad;
- ChatTableDataSource *previousDataSource;
- if (clearContent == NO && currentOffset != -1) {
- previousDataSource = _tableDataSource;
- newOffset = currentOffset - LOAD_MESSAGES_PER_PAGE;
- if (newOffset < 0) {
- newOffset = 0;
- }
- numberOfMessagesToLoad = currentOffset - newOffset;
- } else {
- int messagesAtStart = MESSAGES_AT_START;
- if ([conversation.unreadMessageCount intValue] > messagesAtStart - 5) {
- messagesAtStart = [conversation.unreadMessageCount intValue] + 5;
- }
- NSInteger numberOfMessages = messagesAtStart;
- if (numberOfPages > 1)
- numberOfMessages += (numberOfPages - 1) * LOAD_MESSAGES_PER_PAGE;
- newOffset = messageFetcher.count - numberOfMessages;
- numberOfMessagesToLoad = numberOfMessages;
- if (newOffset < 0) {
- newOffset = 0;
- numberOfMessagesToLoad = messageFetcher.count;
- }
- }
-
- ChatTableDataSource *tmpDatasource = [[ChatTableDataSource alloc] init];
- tmpDatasource.chatVC = self;
- tmpDatasource.backgroundColor = [Colors background];
-
- self.chatContent.dataSource = tmpDatasource;
- self.chatContent.delegate = tmpDatasource;
-
- BOOL didHideHeader = NO;
- if (newOffset == 0) {
- if (!self.chatContentHeader.hidden) {
- self.chatContent.tableHeaderView = nil;
- self.chatContentHeader.hidden = YES;
- didHideHeader = YES;
- }
- } else {
- if (self.chatContentHeader.hidden) {
- self.chatContent.tableHeaderView = self.headerView;
- self.chatContentHeader.hidden = NO;
- }
- }
-
- NSArray *pagedMessages = [messageFetcher messagesAtOffset:newOffset count:numberOfMessagesToLoad];
- currentOffset = newOffset;
-
- for (int i = 0; i < [pagedMessages count]; i++) {
- BaseMessage *curMessage = [pagedMessages objectAtIndex:i];
- [tmpDatasource addMessage:curMessage newSections:nil newRows:nil visible:visible];
-
- if (!curMessage.isOwn.boolValue && !curMessage.read.boolValue) {
- [readReceiptQueue addObject:curMessage];
- }
- }
-
- [self processReadReceiptQueue];
-
- CGFloat contentOffsetFromBottom = self.chatContent.contentOffset.y + self.chatContent.frame.size.height - self.chatContent.contentSize.height;
-
- if (previousDataSource) {
- [tmpDatasource addObjectsFrom:previousDataSource];
- tmpDatasource.searching = previousDataSource.searching;
- tmpDatasource.searchPattern = previousDataSource.searchPattern;
- }
-
- _tableDataSource = tmpDatasource;
- [self.chatContent reloadData];
- [self.chatContent layoutIfNeeded];
- [chatBar setupMentions:conversation.sortedMembers];
-
- CGFloat newContentOffset = contentOffsetFromBottom - self.chatContent.frame.size.height + self.chatContent.contentSize.height;
-
- if (newContentOffset < -self.chatContent.contentInset.top) {
- newContentOffset = -self.chatContent.contentInset.top;
- }
-
- if (didHideHeader) {
- newContentOffset -= 40;
- }
-
- self.chatContent.contentOffset = CGPointMake(0, newContentOffset);
-
- if (shouldScrollDown) {
- shouldScrollDown = NO;
- NSIndexPath *indexPath = [_tableDataSource getUnreadLineIndexPath];
- if (indexPath) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self scrollToUnreadMessage:YES];
- });
- } else {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self scrollToBottomAnimated:NO];
- });
- }
- }
-
- // Full reload
- if (clearContent == YES) {
- [_tableDataSource refreshSectionHeadersInTableView:chatContent];
- [self resetLastFullConversationUpdate];
- }
- }
- - (void)updateConversationLastMessage {
- BaseMessage *message = [messageFetcher lastMessage];
- if ([message isKindOfClass:[SystemMessage class]]) {
- SystemMessage *systemMessage = (SystemMessage *)message;
- switch ([systemMessage.type intValue]) {
- case kSystemMessageCallMissed:
- case kSystemMessageCallRejected:
- case kSystemMessageCallRejectedBusy:
- case kSystemMessageCallRejectedTimeout:
- case kSystemMessageCallEnded:
- case kSystemMessageCallRejectedDisabled:
- case kSystemMessageCallRejectedUnknown:
- // call messages should add as last message, all other types should not
- break;
- default:
- return;
- }
- }
- conversation.lastMessage = [messageFetcher lastMessage];
- }
- - (void)presentActivityViewController:(UIActivityViewController *)viewControllerToPresent animated:(BOOL)flag fromView:(UIView *)view {
- /* hide keyboard before showing UIActivityViewController to keep keyboard from popping up and down
- repeatedly, and to prevent missed keyboard event that gets sent after viewDidDisappear but
- before viewWillAppear */
- [self hideKeyboardTemporarily:YES];
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults setDouble:[Utils systemUptime] forKey:@"UIActivityViewControllerOpenTime"];
- [defaults synchronize];
-
- [viewControllerToPresent setCompletionWithItemsHandler:^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) {
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults removeObjectForKey:@"UIActivityViewControllerOpenTime"];
- }];
-
-
- CGRect rect = [self.view convertRect:view.frame fromView:view.superview];
- [ModalPresenter present:viewControllerToPresent on:self fromRect:rect inView:self.view];
- }
- - (void)titleTapped:(UITapGestureRecognizer*)sender {
- [self toggleHeader];
- }
- - (CGFloat)tabBarHeight {
- if (SYSTEM_IS_IPAD) {
- return self.tabBarController.tabBar.frame.size.height;
- } else {
- return 0.0;
- }
- }
- #pragma mark - key value observer
- - (void)observeUpdatesForMessage:(BaseMessage *)message {
- /* workaround for image messages: if this image hasn't been loaded yet, we must observe it
- and refresh the cell when the image becomes available (height changes). This cannot be done
- in ChatImageMessageCell due to race condition issues */
- if ([message isKindOfClass:[ImageMessage class]]) {
- ImageMessage *imageMessage = (ImageMessage*)message;
- if (imageMessage.thumbnail == nil) {
-
- [imageMessage addObserver:self forKeyPath:@"thumbnail" options:0 context:nil];
- [_imageMessageObserverList addObject:imageMessage];
- }
- } else if ([message isKindOfClass:[FileMessage class]]) {
- FileMessage *fileMessage = (FileMessage*)message;
- if (fileMessage.data == nil) {
- [fileMessage addObserver:self forKeyPath:@"thumbnail" options:0 context:nil];
- [_imageMessageObserverList addObject:fileMessage];
- }
- } else if ([message isKindOfClass:[LocationMessage class]]) {
- LocationMessage *locationMessage = (LocationMessage*)message;
- if (locationMessage.poiName == nil && locationMessage.reverseGeocodingResult == nil) {
- [locationMessage addObserver:self forKeyPath:@"reverseGeocodingResult" options:0 context:nil];
- [_locationMessageObserverList addObject:locationMessage];
- }
- }
- }
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
- //DDLogVerbose(@"observeValueForKeyPath:%@ ofObject:%@ change:%@", keyPath, object, change);
- // objects in the change dictionary can get lost between here and the dispatch block -> copy
- NSDictionary *changeCopy = [change copy];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- if (object == conversation) {
- if ([keyPath isEqualToString:@"messages"]) {
- switch ([(NSNumber*)[changeCopy objectForKey:NSKeyValueChangeKindKey] intValue]) {
- case NSKeyValueChangeInsertion: {
- NSArray *newMessages = (NSArray*)[changeCopy objectForKey:NSKeyValueChangeNewKey];
- [self insertMessages: newMessages];
- break;
- }
- case NSKeyValueChangeRemoval: {
- if (deleteMediaTotal > 0) {
- _deleteMediaCount++;
- if (deleteMediaTotal == _deleteMediaCount) {
- deleteMediaTotal = 0;
- _deleteMediaCount = 0;
- [self updateConversation];
- }
- }
- break;
- }
- }
- } else if ([keyPath isEqualToString:@"unreadMessageCount"]) {
- if (visible) {
- [self resetUnreadMessageCount];
- }
- } else if ([keyPath isEqualToString:@"typing"]) {
- /* update typing indicator */
- if ([_tableDataSource hasData]) {
- NSIndexPath *pathToLastCell = [_tableDataSource indexPathForLastCell];
- [self updateTypingIndicatorAtIndexPath:pathToLastCell];
- }
- } else if ([keyPath isEqualToString:@"displayName"]) {
- [self updateContactDisplay];
- } else if ([keyPath isEqualToString:@"groupId"]) {
- if (conversation.groupId == nil) {
- [self.navigationController dismissViewControllerAnimated:YES completion:^{
- [self.navigationController popToRootViewControllerAnimated:true];
- }];
- }
- } else if ([keyPath isEqualToString:@"members"]) {
-
- NSSet *oldMembers = changeCopy[NSKeyValueChangeOldKey];
- NSSet *newMembers = changeCopy[NSKeyValueChangeNewKey];
- [self updateMembersObserver:oldMembers newMembers:newMembers];
- }
-
- } else if ([object isKindOfClass:[ImageMessage class]] && [keyPath isEqualToString:@"thumbnail"]) {
- [self updateObject:object];
- } else if ([object isKindOfClass:[LocationMessage class]] && [keyPath isEqualToString:@"reverseGeocodingResult"]) {
- [self updateObject:object];
- } else if ([object isKindOfClass:[FileMessage class]] && [keyPath isEqualToString:@"thumbnail"]) {
- [self updateObject:object];
- } else if ([object isKindOfClass:[Contact class]] && [keyPath isEqualToString:@"displayName"]) {
- [self updateConversation];
- }
- });
- }
- - (void)updateObject:(id)object {
- /* find cell in cell map and call table view update */
- NSIndexPath *indexPath = [_tableDataSource indexPathForMessage:object];
- if (indexPath) {
- [_tableDataSource removeObjectFromCellHeightCache:indexPath];
- [self.chatContent reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
- [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
- }
- }
- - (void)insertMessages:(NSArray *)newMessages {
- if (newMessages != nil) {
- [self updateConversationIfNeeded];
-
- BOOL isScrolledAtBottom = [self isScrolledAtBottom];
- dispatch_async(dispatch_get_main_queue(), ^{
-
- /* this simply assumes that the inserted message is newer than any existing
- messages, and so can be added at the end of the list */
- NSIndexPath *prevLastIndexPath = [_tableDataSource indexPathForLastCell];
-
- NSMutableIndexSet *newSections = [NSMutableIndexSet new];
- NSMutableArray *newRows = [[NSMutableArray alloc] initWithCapacity:newMessages.count*2];
- BOOL newSentMessages = NO;
- BOOL newReceivedMessages = NO;
-
- [self.chatContent beginUpdates];
-
- for (BaseMessage *message in newMessages) {
- /* check if we have already added this message – this can happen as KVO sometimes
- sends an NSKeyValueChangeInsertion event for the same messages twice on iOS 8 */
- NSIndexPath *indexPathForMessage = [_tableDataSource indexPathForMessage:message];
- if (indexPathForMessage != nil) {
- [self.chatContent endUpdates];
- if (indexPathForMessage == prevLastIndexPath) {
- if (newSentMessages || isScrolledAtBottom) {
- inhibitScrollBottom = YES;
- isNewMessageReceivedInActiveChat = YES;
- [self performSelector:@selector(scrollToBottomScheduled) withObject:nil afterDelay:0.2f];
- } else if (newReceivedMessages) {
- haveNewMessages = YES;
- [self updateScrollDownButtonAnimated:YES];
- }
- }
- return;
- }
-
- [_tableDataSource addMessage:message newSections:newSections newRows:newRows visible:visible];
-
- if (!message.isOwn.boolValue && !message.read.boolValue) { // not read, so queue read receipt for sending the next time we appear
- [readReceiptQueue addObject:message];
- newReceivedMessages = YES;
- }
-
- if (message.isOwn.boolValue)
- newSentMessages = YES;
- }
-
- if (newSections.count > 0) {
- [chatContent insertSections:newSections withRowAnimation:UITableViewRowAnimationNone];
- }
-
- if (newRows.count > 0) {
- [chatContent insertRowsAtIndexPaths:newRows withRowAnimation:UITableViewRowAnimationNone];
- }
-
- [self.chatContent endUpdates];
-
- /* must update/remove the typing indicator on the previously last row */
- if (prevLastIndexPath != nil)
- [self updateTypingIndicatorAtIndexPath:prevLastIndexPath];
-
- if (newSentMessages || isScrolledAtBottom) {
- inhibitScrollBottom = YES;
- isNewMessageReceivedInActiveChat = YES;
- [self performSelector:@selector(scrollToBottomScheduled) withObject:nil afterDelay:0.2f];
- } else if (newReceivedMessages) {
- haveNewMessages = YES;
- [self updateScrollDownButtonAnimated:YES];
- }
-
- if (visible) {
- [self processReadReceiptQueue];
- }
- });
- }
- }
- - (void)resetUnreadMessageCount {
- if (![AppDelegate sharedAppDelegate].active)
- return;
-
- if ([conversation.unreadMessageCount intValue] != 0) {
- /* mark conversation as read */
- [entityManager performSyncBlockAndSafe:^{
- conversation.unreadMessageCount = [NSNumber numberWithInt:0];
- }];
- }
- [[NotificationManager sharedInstance] updateUnreadMessagesCount:NO];
- }
- - (void)processReadReceiptQueue {
- if (conversation.groupId != nil) {
- /* no read receipts for groups, but we have to set the read field in database for new message line */
- /* fix for update from 2.8.0 to new version --> set all messages to read if first message of group is not read */
- id firstMessage = [_tableDataSource objectForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
- if ([firstMessage isKindOfClass:[BaseMessage class]]) {
- if (!((BaseMessage *)firstMessage).read.boolValue) {
- // add all visible messages to readReceiptQueue
- NSArray *visibleMessages = [messageFetcher messagesAtOffset:currentOffset count:(messageFetcher.count - currentOffset)];
-
- for (int i = 0; i < [visibleMessages count]; i++) {
- BaseMessage *curMessage = [visibleMessages objectAtIndex:i];
- [readReceiptQueue addObject:curMessage];
- }
-
- }
- }
-
- NSMutableArray *tmpReadReceiptQueue = [NSMutableArray arrayWithArray:readReceiptQueue];
-
- [entityManager performAsyncBlockAndSafe:^{
- for (BaseMessage *message in tmpReadReceiptQueue) {
- @try {
- message.read = [NSNumber numberWithBool:YES];
- message.readDate = [NSDate date];
- }
- @catch (NSException *exception) {
- // intended to catch NSObjectInaccessibleException, which may happen
- // if the message has been deleted in the meantime
- DDLogError(@"Exception while marking message as read: %@", exception);
- }
- }
- }];
- [readReceiptQueue removeAllObjects];
- return;
- }
-
- /* do not send read receipts while app is in the background */
- if (![AppDelegate sharedAppDelegate].active)
- return;
-
- if (readReceiptQueue.count > 0) {
- NSMutableArray *tmpReadReceiptQueue = [NSMutableArray arrayWithArray:readReceiptQueue];
- [MessageSender sendReadReceiptForMessages:tmpReadReceiptQueue toIdentity:conversation.contact.identity async:YES quickReply:NO];
-
- [entityManager performAsyncBlockAndSafe:^{
- for (BaseMessage *message in tmpReadReceiptQueue) {
- @try {
- message.read = [NSNumber numberWithBool:YES];
- message.readDate = [NSDate date];
- }
- @catch (NSException *exception) {
- // intended to catch NSObjectInaccessibleException, which may happen
- // if the message has been deleted in the meantime
- DDLogError(@"Exception while marking message as read: %@", exception);
- }
- }
- }];
-
- [readReceiptQueue removeAllObjects];
- }
- }
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
- if ([segue.identifier isEqualToString:@"ShowContact"]) {
- ContactDetailsViewController *detailsView = (ContactDetailsViewController*)segue.destinationViewController;
- if ([sender isKindOfClass:[Contact class]]) {
- detailsView.contact = (Contact *)sender;
- } else {
- detailsView.contact = conversation.contact;
- }
- } else if ([segue.identifier isEqualToString:@"ShowLocation"]) {
- LocationViewController *locationView = (LocationViewController*)segue.destinationViewController;
- locationView.locationMessage = locationToShow;
- } else if ([segue.identifier isEqualToString:@"ShowDetails"]) {
- MessageDetailsViewController *detailsView = (MessageDetailsViewController*)segue.destinationViewController;
- detailsView.message = detailsMessage;
- } else if ([segue.identifier isEqualToString:@"ShowGroupInfo"]) {
- GroupDetailsViewController *detailsView = (GroupDetailsViewController*)segue.destinationViewController;
- detailsView.delegate = self;
- detailsView.group = [GroupProxy groupProxyForConversation:conversation];
- } else if ([segue.identifier isEqualToString:@"ShowPushSetting"]) {
- NotificationSettingViewController *settingsView = (NotificationSettingViewController*)segue.destinationViewController;
- if (conversation.isGroup) {
- settingsView.identity = [NSString stringWithHexData:conversation.groupId];
- settingsView.isGroup = YES;
- settingsView.conversation = conversation;
- } else {
- settingsView.identity = conversation.contact.identity;
- settingsView.isGroup = NO;
- settingsView.conversation = conversation;
- }
- }
- }
- - (void)cancelAction:(id)sender {
- self.editing = NO;
- }
- - (void)deleteAction:(id)sender {
-
- NSString *actionTitle;
- NSUInteger numSelected = [[self.chatContent indexPathsForSelectedRows] count];
- if (numSelected == 0) {
- /* clear all */
- actionTitle = NSLocalizedString(@"messages_delete_all_confirm", nil);
- } else {
- actionTitle = NSLocalizedString(@"messages_delete_selected_confirm", nil);
- }
-
- UIAlertController *deleteActionSheet = [UIAlertController alertControllerWithTitle:actionTitle message:nil preferredStyle:UIAlertControllerStyleAlert];
- [deleteActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"delete", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
- [_tableDataSource cleanCellHeightCache];
- ChatDeleteAction *deleteAction = [ChatDeleteAction actionForChatViewController:self];
- deleteAction.entityManager = entityManager;
- currentAction = deleteAction;
- [deleteAction executeAction];
- }]];
- [deleteActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
- deleteActionSheet.popoverPresentationController.sourceView = self.view;
- [self presentViewController:deleteActionSheet animated:YES completion:nil];
- }
- - (IBAction)loadEarlierMessagesAction:(id)sender {
- numberOfPages++;
- _isDirty = YES;
- [self updateConversationClearContent:NO];
- }
- - (void)scrollToBottomScheduled {
- inhibitScrollBottom = NO;
- [self scrollToBottomAnimated:YES];
- }
- - (void)scrollToBottomAnimated:(BOOL)animated {
- if (inhibitScrollBottom || (chatContent.contentSize.height - chatContent.contentOffset.y - chatContent.frame.size.height) < 0)
- return;
-
- NSIndexPath *bottomRow = [_tableDataSource indexPathForLastCell];
- if (bottomRow) {
- [self checkShouldShowHeader];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- @try {
-
- [chatContent scrollToRowAtIndexPath:bottomRow atScrollPosition:UITableViewScrollPositionBottom animated:animated];
- [self repositionScrollDownButton];
- [self updateScrollDownButtonAnimated:NO];
-
- }
- @catch (NSException *exception) {
- ;//ignore
- }
- });
- }
- }
- - (BOOL)isScrolledAtBottom {
- return ((chatContent.contentSize.height - chatContent.contentOffset.y - chatContent.frame.size.height) < 25);
- }
- - (NSString *)messageText {
- if (chatBar != nil) {
- return chatBar.text;
- } else {
- return initialMessageText;
- }
- }
- - (void)setMessageText:(NSString *)messageText {
- if (chatBar != nil) {
- [self showKeyboardConditionally];
-
- chatBar.text = messageText;
- } else {
- initialMessageText = messageText;
- }
- }
- - (void)setImageDataToSend:(NSData *)newImageToSend {
- imageDataToSend = newImageToSend;
-
- /* if we're currently visible, trigger send as there will be no viewDidAppear */
- if (visible) {
- [self chatBar:chatBar didSendImageData:imageDataToSend];
- imageDataToSend = nil;
- }
- }
- - (void)chatContentTapped:(UITapGestureRecognizer*)sender {
- DDLogVerbose(@"chatContentTapped, ignoreNextTap = %d, sender = %@", ignoreNextTap, sender);
-
- if (ignoreNextTap) {
- ignoreNextTap = NO;
- return;
- }
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideKeyboardTemporarily:NO];
- });
- }
- - (void)messageBackgroundTapped:(BaseMessage *)message {
- DDLogVerbose(@"messageBackgroundTapped");
-
- if (ignoreNextTap)
- return;
-
- [self hideKeyboardTemporarily:NO];
- }
- - (void)startRecordingAudio {
- [PlayRecordAudioViewController requestMicrophoneAccessOnCompletion:^{
- UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
- selectedAudioMessage = [_tableDataSource indexPathForLastCell];
- audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
- audioRecorder.delegate = self;
- selectedAudioMessage = nil;
- [audioRecorder startRecordingForConversation: conversation];
- }];
- }
- - (void)createBallot {
- [BallotDispatcher showBallotCreateViewControllerForConversation:conversation onNavigationController:self.navigationController];
- }
- - (void)sendFile {
- DocumentPicker *documentPicker = [DocumentPicker documentPickerForViewController:self conversation:self.conversation];
- documentPicker.popoverSourceRect = [self.view convertRect:self.chatBar.addButton.frame fromView:self.chatBar];
- [documentPicker show];
- }
- - (void)playAudioMessage:(AudioMessage*)message {
- /* write audio to temp. file */
- [self createTmpAVFileFrom:message];
-
- if (tmpAudioVideoUrl) {
- [self hideKeyboardTemporarily:YES];
-
- UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
- selectedAudioMessage = [_tableDataSource indexPathForMessage:message];
- audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
- audioRecorder.delegate = self;
- [audioRecorder startPlaying: tmpAudioVideoUrl];
- }
- }
- - (void)playFileAudioMessage:(FileMessage*)message {
- /* write audio to temp. file */
- [self createTmpAVFileFrom:message];
-
- if (tmpAudioVideoUrl) {
- [self hideKeyboardTemporarily:YES];
-
- UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, audioRecorder);
- selectedAudioMessage = [_tableDataSource indexPathForMessage:message];
- audioRecorder = [PlayRecordAudioViewController playRecordAudioViewControllerIn: self];
- audioRecorder.delegate = self;
- [audioRecorder startPlaying: tmpAudioVideoUrl];
- }
- }
- - (void)updateTypingIndicatorAtIndexPath:(NSIndexPath*)indexPath {
- UITableViewCell *cell = [self.chatContent cellForRowAtIndexPath:indexPath];
- if (cell != nil && [cell isKindOfClass:[ChatMessageCell class]]) {
- ChatMessageCell *chatMessageCell = (ChatMessageCell*)cell;
-
- NSIndexPath *currentLastIndexPath = [_tableDataSource indexPathForLastCell];
- if (conversation.typing.boolValue && [indexPath isEqual:currentLastIndexPath]) {
- chatMessageCell.typing = YES;
- } else {
- chatMessageCell.typing = NO;
- }
- }
- }
- - (void)updateChatContentInset {
- float statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
-
- if (@available(iOS 11.0, *)) {
- chatContent.contentInset = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] - self.navigationController.navigationBar.frame.size.height - statusBarHeight + 4.0f, 0, 0, 0);
- chatContent.scrollIndicatorInsets = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] - self.navigationController.navigationBar.frame.size.height - statusBarHeight, 0, 0, 0);
- } else {
- chatContent.contentInset = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent] + 4.0f, 0, 0, 0);
- chatContent.scrollIndicatorInsets = UIEdgeInsetsMake(lastKeyboardHeight + [self topOffsetForVisibleChatContent], 0, 0, 0);
- }
- }
- # pragma mark - Keyboard Notifications
- - (void)keyboardWillShow:(NSNotification *)notification {
- DDLogVerbose(@"keyboardWillShow");
- forceTouching = NO;
- [self processKeyboardNotification:notification willHide:NO];
-
- self.composing = YES;
- }
- - (void)keyboardWillHide:(NSNotification *)notification {
- DDLogVerbose(@"keyboardWillHide");
- [self processKeyboardNotification:notification willHide:YES];
-
- [_tableDataSource refreshSectionHeadersInTableView:self.chatContent];
- }
- - (void)processKeyboardNotification:(NSNotification*)notification willHide:(BOOL)willHide {
- if (notification == nil) {
- CGRect newKeyboardEndFrame = lastKeyboardEndFrame;
- if (lastKeyboardHeight == 162 && [UIScreen mainScreen].bounds.size.height == 320.0) {
- newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height;
- }
- else if (lastKeyboardHeight == 162 || lastKeyboardHeight == 216) {
- newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height + 32.0;
- }
- else {
- newKeyboardEndFrame.size.height = lastKeyboardEndFrame.size.height + 42.0;
- }
-
- [UIView animateWithDuration:lastAnimationDuration delay:0 options:(lastAnimationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState) animations:^{
- [self moveContainerViewForKeyboardFrame:newKeyboardEndFrame willHide:willHide];
- } completion:^(BOOL finished) {}];
- } else {
- CGRect keyboardEndFrame;
- [notification.userInfo[UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
- CGRect keyboardEndFrameRelative = [self.view convertRect:keyboardEndFrame fromView:nil];
-
- CGSize keyboardSize = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
-
- DDLogVerbose(@"keyboardEndFrame: %@", NSStringFromCGRect(keyboardEndFrame));
- DDLogVerbose(@"keyboardEndFrameRelative: %@", NSStringFromCGRect(keyboardEndFrameRelative));
- DDLogVerbose(@"Keyboardsize height: %f", keyboardSize.height);
-
- NSNumber *durationValue = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];
- NSTimeInterval animationDuration = durationValue.doubleValue;
-
- NSNumber *curveValue = notification.userInfo[UIKeyboardAnimationCurveUserInfoKey];
- UIViewAnimationCurve animationCurve = curveValue.intValue;
-
- lastKeyboardEndFrame = keyboardEndFrameRelative;
- lastAnimationDuration = animationDuration;
- lastAnimationCurve = animationCurve;
-
- if (visible) {
- [UIView animateWithDuration:animationDuration delay:0 options:(animationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState) animations:^{
- [self moveContainerViewForKeyboardFrame:keyboardEndFrameRelative willHide:willHide];
- } completion:^(BOOL finished) {}];
- } else {
- [self moveContainerViewForKeyboardFrame:keyboardEndFrameRelative willHide:willHide];
- }
- }
- }
- - (void)inputModeDidChange:(NSNotification *)notification {
- // iPhone X fix
- if (@available(iOS 11.0, *)) {
- if([[UITextInputMode currentInputMode].primaryLanguage isEqualToString:@"emoji"]) {
- if (SYSTEM_IS_IPHONE_X && (lastKeyboardHeight == 291 || lastKeyboardHeight == 171)) { // iPhone X
- [self processKeyboardNotification:nil willHide:NO];
- } else if (lastKeyboardHeight == 226) { // Portrait iPhone 5.5'
- [self processKeyboardNotification:nil willHide:NO];
- } else if (lastKeyboardHeight == 216) { // Portrait iPhone 4' iPhone 4.7'
- [self processKeyboardNotification:nil willHide:NO];
- } else if (lastKeyboardHeight == 162) { // Landscape iPhone 5.5' iPhone 4.7' iPhone 4'
- [self processKeyboardNotification:nil willHide:NO];
- } else if (SYSTEM_IS_IPAD == YES && lastKeyboardHeight == 304) { // iPad
- [self processKeyboardNotification:nil willHide:NO];
- } else if (SYSTEM_IS_IPAD == YES && (lastKeyboardHeight == 279 || lastKeyboardHeight == 374)) { // iPad Pro 12.9
- [self processKeyboardNotification:nil willHide:NO];
- }
- }
- }
- }
- - (void)setEditing:(BOOL)editing animated:(BOOL)animated {
- DDLogVerbose(@"setEditing");
- [super setEditing:editing animated:animated];
- [chatContent setEditing:editing animated:animated];
-
- tapGestureRecognizer.enabled = !editing;
-
- chatContent.separatorStyle = editing ?
- UITableViewCellSeparatorStyleSingleLine : UITableViewCellSeparatorStyleNone;
-
- if (editing) {
- UIBarButtonItem *deleteButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"delete_all", nil) style:UIBarButtonItemStylePlain target:self action:@selector(deleteAction:)];
- deleteButton.tintColor = [UIColor redColor];
- self.navigationItem.leftBarButtonItem = deleteButton;
- self.navigationItem.rightBarButtonItems = @[[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction:)]];
-
- self.loadEarlierMessages.hidden = YES;
- } else {
- self.navigationItem.leftBarButtonItem = nil;
- self.navigationItem.rightBarButtonItems = @[self.editButtonItem];
-
- self.loadEarlierMessages.hidden = NO;
- }
-
- if (editing) {
- [self checkShouldShowHeader];
- [self hideKeyboardTemporarily:NO];
- }
- }
- #pragma mark - ChatBarDelegate
- - (void)chatBar:(ChatBar *)curChatBar didChangeHeight:(CGFloat)height {
- BOOL wasScrolledAtBottom = [self isScrolledAtBottom];
-
- CGRect chatContentFrame = chatContent.frame;
- chatContentFrame.size.height = containerView.frame.size.height - height - [self tabBarHeight] - wrapperBottomPadding;
- [UIView beginAnimations:nil context:NULL];
- [UIView setAnimationDuration:0.1f];
- chatContent.frame = chatContentFrame;
- chatBarWrapper.frame = CGRectMake(chatBarWrapper.frame.origin.x, chatContentFrame.size.height, containerView.frame.size.width, height + wrapperBottomPadding);
- chatBar.frame = CGRectMake(0, 0, chatBarWrapper.frame.size.width, height);
- [self repositionScrollDownButton];
- [UIView commitAnimations];
-
- if (wasScrolledAtBottom)
- [self scrollToBottomAnimated:YES];
- }
- - (void)chatBar:(ChatBar *)curChatBar didSendText:(NSString *)text {
- if (text.length == 0 && curChatBar.canSendAudio) {
- if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
- return;
- }
-
- /* microphone button pressed */
- [self hideKeyboardTemporarily:YES];
- [self startRecordingAudio];
- return;
- }
-
- NSString *trimmedMessage = nil;
- NSString *quotedIdentity = nil;
- NSData *quoteMessageId = nil;
- NSString *remainingBody = nil;
- NSString *quotedText = nil;
- if ([[UserSettings sharedUserSettings] quoteV2Active]) {
- quoteMessageId = [QuoteParser parseQuoteV2FromMessage:text remainingBody:&remainingBody];
- } else {
- quotedText = [QuoteParser parseQuoteFromMessage:text quotedIdentity:"edIdentity remainingBody:&remainingBody];
- }
-
- if (quoteMessageId || quotedText) {
- remainingBody = [remainingBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
- }
-
- trimmedMessage = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
-
- // Don't send blank messages.
- if (quoteMessageId || quotedText) {
- if (remainingBody == nil || remainingBody.length == 0 || [remainingBody isEqualToString:@"\ufffc"]) {
- [chatBar clearChatInput];
- return;
- }
- } else {
- if (trimmedMessage == nil || trimmedMessage.length == 0 || [trimmedMessage isEqualToString:@"\ufffc"]) {
- [chatBar clearChatInput];
- return;
- }
- }
-
- if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
- return;
- }
-
- NSArray *trimmedMessages = [Utils getTrimmedMessages:trimmedMessage];
-
- [chatBar checkEnableSendButton];
-
- if (!trimmedMessages) {
- [MessageSender sendMessage:trimmedMessage inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
- [chatBar clearChatInput];
- [MessageDraftStore deleteDraftForConversation:self.conversation];
- }];
- } else {
- [trimmedMessages enumerateObjectsUsingBlock:^(NSString *separatedTrimmedMessage, NSUInteger idx, BOOL * _Nonnull stop) {
- [MessageSender sendMessage:separatedTrimmedMessage inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
- if (idx == trimmedMessages.count - 1) {
- [chatBar clearChatInput];
- [MessageDraftStore deleteDraftForConversation:self.conversation];
- }
- }];
- }];
- }
-
- if ([UserSettings sharedUserSettings].inAppSounds) {
- AudioServicesPlaySystemSound(sentMessageSound);
- }
- }
- - (void)chatBar:(ChatBar *)chatBar didSendImageData:(NSData *)image {
- if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
- return;
- }
-
- [self hideKeyboardTemporarily:YES];
-
- UINavigationController *previewNavVc = [self.storyboard instantiateViewControllerWithIdentifier:@"PreviewImageNav"];
- PreviewImageViewController *previewVc = previewNavVc.viewControllers[0];
- previewVc.delegate = self;
- previewVc.image = image;
- previewVc.hasCancelButton = YES;
- [self presentViewController:previewNavVc animated:YES completion:nil];
- }
- - (void)chatBar:(ChatBar *)chatBar2 didSendGIF:(NSData *)gifData fallbackImage:(UIImage *)image {
- if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
- return;
- }
-
- // Check if we can send file messages in this conversation
- NSSet *conversations = [NSSet setWithObject:self.conversation];
- [FeatureMask checkFeatureMask:FEATURE_MASK_FILE_TRANSFER forConversations:conversations onCompletion:^(NSArray *unsupportedContacts) {
- [self hideKeyboardTemporarily:YES];
-
- UINavigationController *previewNavVc = [self.storyboard instantiateViewControllerWithIdentifier:@"PreviewImageNav"];
- PreviewImageViewController *previewVc = previewNavVc.viewControllers[0];
- previewVc.delegate = self;
-
- if ([unsupportedContacts count] > 0) {
- previewVc.image = UIImageJPEGRepresentation(image, 1.0);
- } else {
- previewVc.gifData = gifData;
- }
-
- previewVc.hasCancelButton = YES;
- [self presentViewController:previewNavVc animated:YES completion:nil];
- }];
- }
- - (void)chatBarWillStartTyping:(ChatBar *)chatBar {
- if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
- if (typingIndicatorSent)
- return;
- if (conversation.groupId == nil && [[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil])
- [MessageSender sendTypingIndicatorMessage:YES toIdentity:conversation.contact.identity];
- typingIndicatorSent = YES;
- }
- }
- - (void)chatBarDidStopTyping:(ChatBar *)theChatBar {
- if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
- if (!typingIndicatorSent)
- return;
- if (conversation.groupId == nil)
- [MessageSender sendTypingIndicatorMessage:NO toIdentity:conversation.contact.identity];
- typingIndicatorSent = NO;
-
- NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
- if ([[chatBar.text stringByTrimmingCharactersInSet: set] length] > 0) {
- [MessageDraftStore saveDraft:[chatBar formattedMentionText] forConversation:self.conversation];
- } else {
- [MessageDraftStore saveDraft:@"" forConversation:self.conversation];
- }
- }
- }
- - (void)chatBarDidPushAddButton:(ChatBar *)_chatBar {
-
- if (![[PermissionChecker permissionCheckerPresentingAlarmsOn:self] canSendIn:conversation entityManager:nil]) {
- return;
- }
-
- if (SYSTEM_IS_IPAD == true) {
- [_delegate cancelSwipeGestureFromConversations];
- }
-
- CGRect rect = [self.view convertRect:chatBar.addButton.frame fromView:_chatBar];
- [self showAddActionAlertControllerFrom:rect inView:self.view];
- }
- - (void)showAddActionAlertControllerFrom:(CGRect)rect inView:(UIView *)view {
- if (_assetActionHelperWillPresent) {
- return;
- }
- _assetActionHelperWillPresent = true;
- [self hideKeyboardTemporarily:YES];
-
- if (assetActionHelper == nil) {
- assetActionHelper = [[PPAssetsActionHelper alloc] init];
- assetActionHelper.delegate = self;
- }
- PPAssetsActionController *assetActionController = [assetActionHelper buildAction];
- if ([[UserSettings sharedUserSettings] showGalleryPreview]) {
- [[UserSettings sharedUserSettings] setOpenPlusIconInChat:YES];
- }
- [self presentViewController:assetActionController animated:YES completion:^{
- _assetActionHelperWillPresent = false;
- if ([[UserSettings sharedUserSettings] showGalleryPreview]) {
- [[UserSettings sharedUserSettings] setOpenPlusIconInChat:NO];
- }
- }];
- }
- - (UIInterfaceOrientation)interfaceOrientationForChatBar:(ChatBar *)chatBar {
- return [[UIApplication sharedApplication] statusBarOrientation];
- }
- - (BOOL)canBecomeFirstResponder {
- return ![self.presentedViewController isKindOfClass:[CallViewController class]] && self.presentedViewController == nil;
- }
- - (void)chatBarTapped:(ChatBar *)chatBar {
- if (SYSTEM_IS_IPAD == true) {
- [_delegate cancelSwipeGestureFromConversations];
- }
- }
- - (void)chatBarDidAddQuote {
- if (_searching) {
- [headerView cancelSearch];
- }
- }
- - (UIView *)chatContainterView {
- return self.view;
- }
- #pragma mark - Chat message cell delegate
- - (void)imageMessageTapped:(ImageMessage *)message {
- [self hideKeyboardTemporarily:YES];
-
- UIViewController *vc = [headerView getPhotoBrowserAtMessage:message forPeeking:NO];
- vc.modalPresentationStyle = UIModalPresentationFullScreen;
-
- [self presentViewController:vc animated:YES completion:nil];
- }
- - (void)fileImageMessageTapped:(FileMessage *)message {
- [self hideKeyboardTemporarily:true];
-
- UIViewController *vc = [headerView getPhotoBrowserAtMessage:message forPeeking:NO];
- vc.modalPresentationStyle = UIModalPresentationFullScreen;
-
- [self presentViewController:vc animated:YES completion:nil];
- }
- - (void)fileVideoMessageTapped:(FileMessage *)message {
- if (message.data == nil) {
- /* need to download this video first */
- BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
- [loader startWithMessage:message onCompletion:^(BaseMessage<BlobData> *loadedMessage) {
- if (visible) {
- [self playFileVideoMessage:message];
- }
- } onError:^(NSError *error) {
- [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- }];
- } else {
- /* can show/play this video right now */
- [self playFileVideoMessage:message];
- }
- }
- - (void)fileAudioMessageTapped:(FileMessage *)message {
- if (message.data == nil) {
- [PlayRecordAudioViewController activateProximityMonitoring];
- /* need to download this audio first */
- BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
- [loader startWithMessage:message onCompletion:^(BaseMessage *loadedMessage) {
- if (visible) {
- [self playFileAudioMessage:message];
- }
- } onError:^(NSError *error) {
- [PlayRecordAudioViewController deactivateProximityMonitoring];
- [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- }];
- } else {
- /* can show/play this audio right now */
- [self playFileAudioMessage:message];
- }
- }
- - (void)locationMessageTapped:(LocationMessage *)message {
- [self hideKeyboardTemporarily:YES];
-
- locationToShow = message;
- [self performSegueWithIdentifier:@"ShowLocation" sender:self];
- }
- - (void)videoMessageTapped:(VideoMessage*)message {
- if (message.video == nil) {
- /* need to download this video first */
- VideoMessageLoader *loader = [[VideoMessageLoader alloc] init];
- [loader startWithMessage:message onCompletion:^(BaseMessage<BlobData> *loadedMessage) {
- if (visible) {
- [self playVideoMessage:message];
- }
- } onError:^(NSError *error) {
- [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- }];
- } else {
- /* can show/play this video right now */
- [self playVideoMessage:message];
- }
- }
- - (void)startPlayer {
- [self hideKeyboardTemporarily:YES];
-
- AppDelegate *appDelegate = [AppDelegate sharedAppDelegate];
-
- /* ignore mute switch */
- NSInteger state = [[VoIPCallStateManager shared] currentCallState];
- if (state == CallStateIdle) {
- prevAudioCategory = [[AVAudioSession sharedInstance] category];
- [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
- }
- AVPlayer *p = [AVPlayer playerWithURL:tmpAudioVideoUrl];
- player = [AVPlayerViewController new];
- player.player = p;
- if (self.isViewLoaded && self.view.window) {
- //self is visible
- [self presentViewController:player animated:YES completion:^{
- [player.player play];
- if (state != CallStateIdle) {
- [[VoIPCallStateManager shared] activateRTCAudio];
- }
- }];
- } else {
- [appDelegate.window.rootViewController.presentedViewController presentViewController:player animated:YES completion:^{
- [player.player play];
- }];
- }
- }
- - (void)createTmpAVFileFrom:(id<BlobData>)message {
- NSURL *tmpDirUrl = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
- tmpAudioVideoUrl = [[tmpDirUrl URLByAppendingPathComponent:@"av"] URLByAppendingPathExtension: MEDIA_EXTENSION_VIDEO];
- DDLogInfo(@"fileURL: %@", [tmpAudioVideoUrl path]);
-
- NSData *data = [message blobGetData];
- if (![data writeToURL:tmpAudioVideoUrl atomically:NO]) {
- DDLogWarn(@"Writing audio/video data to temporary file failed");
- return;
- }
- }
- - (void)playVideoMessage:(VideoMessage*)message {
- /* write video to temp. file */
- [self createTmpAVFileFrom:message];
-
- if (tmpAudioVideoUrl) {
- [self startPlayer];
- }
- }
- - (void)playFileVideoMessage:(FileMessage *)message {
- /* write video to temp. file */
- [self createTmpAVFileFrom:message];
-
- if (tmpAudioVideoUrl) {
- [self startPlayer];
- }
- }
- - (void)showMessageDetails:(BaseMessage *)message {
- detailsMessage = message;
- [self performSegueWithIdentifier:@"ShowDetails" sender:self];
- }
- - (void)audioMessageTapped:(AudioMessage*)message {
- if (message.audio == nil) {
- [PlayRecordAudioViewController activateProximityMonitoring];
- /* need to download this audio first */
- BlobMessageLoader *loader = [[BlobMessageLoader alloc] init];
- [loader startWithMessage:message onCompletion:^(BaseMessage *loadedMessage) {
- if (visible) {
- [self playAudioMessage:message];
- }
- } onError:^(NSError *error) {
- [PlayRecordAudioViewController deactivateProximityMonitoring];
- [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- }];
- } else {
- /* can show/play this audio right now */
- [self playAudioMessage:message];
- }
- }
- - (void)ballotMessageTapped:(BallotMessage*)message {
- [self hideKeyboardTemporarily:YES];
-
- [BallotDispatcher showViewControllerForBallot:message.ballot onNavigationController:self.navigationController];
- }
- - (void)mentionTapped:(id)mentionObject {
- [self hideKeyboardTemporarily:NO];
- if ([mentionObject isKindOfClass:[Contact class]]) {
- [self performSegueWithIdentifier:@"ShowContact" sender:(Contact *)mentionObject];
- } else {
- [self performSegueWithIdentifier:@"ShowMeContact" sender:(Contact *)mentionObject];
- }
- }
- - (void)showQuotedMessage:(BaseMessage *)message {
- _cancelShowQuotedMessage = NO;
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- __block NSIndexPath *indexPath = nil;
- while (_cancelShowQuotedMessage == NO) {
- indexPath = [self indexPathForMessage:message];
-
- if (indexPath) {
- // found message
- break;
- } else {
- NSInteger offset = [self messageOffset];
-
- if (offset > 0) {
- dispatch_sync(dispatch_get_main_queue(), ^{
- [self addLoadEarlierMessagesHUD];
-
- [self loadEarlierMessagesAction:nil];
-
- if (_cancelShowQuotedMessage) {
- return;
- }
-
- NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
- [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
- });
- } else {
- break;
- }
- }
- }
-
- dispatch_sync(dispatch_get_main_queue(), ^{
- [MBProgressHUD hideHUDForView:self.view animated:YES];
- });
-
- dispatch_async(dispatch_get_main_queue(), ^{
-
- if (indexPath) {
- // safety check if indexPath is still valid
- if ([self isValidIndexPath:indexPath] == NO) {
- return;
- }
-
- [self.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
-
- __block ChatMessageCell *currentCell = (ChatMessageCell *)[self.chatContent cellForRowAtIndexPath:indexPath];
-
- // Deselect all currently selected cells
- for (ChatMessageCell *visibleCell in self.chatContent.visibleCells) {
- if ([visibleCell respondsToSelector:@selector(setBubbleHighlighted:)]) {
- [visibleCell setBubbleHighlighted:NO];
- }
- }
-
- CGFloat delayMs;
- if (currentCell) {
- delayMs = 100.0;
- } else {
- // cell not visible yet
- delayMs = 400.0;
- }
-
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
- currentCell = (ChatMessageCell *)[self.chatContent cellForRowAtIndexPath:indexPath];
- [currentCell setBubbleHighlighted:YES];
-
- if (UIAccessibilityIsVoiceOverRunning()) {
- NSString *text = currentCell.accessibilityLabel;
- UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text);
- }
- });
- }
- });
- });
- }
- - (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
- NSInteger sectionCount = [self.chatContent.dataSource numberOfSectionsInTableView:self.chatContent];
- if (indexPath.section >= sectionCount) {
- return NO;
- }
-
- NSInteger rowCount = [self.chatContent.dataSource tableView:self.chatContent numberOfRowsInSection:indexPath.section];
- if (indexPath.row >= rowCount) {
- return NO;
- }
-
- return YES;
- }
- - (void)addLoadEarlierMessagesHUD {
- if ([MBProgressHUD HUDForView:self.view] != nil) {
- return;
- }
-
- MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
- hud.label.text = [BundleUtil localizedStringForKey:@"load_earlier_messages"];
- [hud.button setTitle:NSLocalizedString(@"cancel", nil) forState:UIControlStateNormal];
- [hud.button addTarget:self action:@selector(cancelShowQuotedMessage) forControlEvents:UIControlEventTouchUpInside];
- }
- - (void)cancelShowQuotedMessage {
- _cancelShowQuotedMessage = YES;
- }
- #pragma mark - Preview image delegate
- - (void)previewImageControllerDidChooseToSend:(PreviewImageViewController *)previewController imageData:(NSData *)image {
- [self sendImageData:image];
- [self dismissViewControllerAnimated:YES completion:nil];
- }
- - (void)previewImageControllerDidChooseToSend:(PreviewImageViewController *)previewController gif:(NSData *)gifData {
- [self previewImageControllerDidChooseToSend:previewController imageData:gifData];
- }
- - (void)previewImageControllerDidChooseToCancel:(PreviewImageViewController *)previewController {
- [self dismissViewControllerAnimated:YES completion:nil];
- }
- - (void)sendImageData:(NSData *)imageData {
- if (imageData == nil)
- return;
-
- ImageURLSenderItemCreator *imageSender = [[ImageURLSenderItemCreator alloc] init];
-
- CFStringRef uti = [ImageURLSenderItemCreator getUTIFor:imageData];
- if (uti == nil) {
- uti = kUTTypeJPEG;
- }
- URLSenderItem *item = [imageSender senderItemFrom:imageData uti:(__bridge NSString *)uti];
-
- FileMessageSender *sender = [[FileMessageSender alloc] init];
- [sender sendItem:item inConversation:conversation];
- }
- #pragma mark - scroll view delegate
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
- if (visible) {
- [self updateScrollDownButtonAnimated:YES];
-
- CGFloat yDiff = scrollView.contentOffset.y - lastScrollOffset.y;
- if (yDiff > 32.0 && lastScrollOffset.y != 0.0) {
- if (scrollView.isDragging) {
- [self hideHeaderWithDuration:0.2];
- lastScrollOffset = scrollView.contentOffset;
- }
- }
- }
- }
- -(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
-
- lastScrollOffset = scrollView.contentOffset;
-
- if (self.searching) {
- [headerView resignFirstResponder];
- }
- }
- -(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
-
- if (velocity.y == 0.0) {
- return;
- }
-
- BOOL velocityTriggerUp = velocity.y < -0.8;
- BOOL velocityTriggerDown = velocity.y > 0.0;
-
- CGFloat topOffset = [self topOffsetForVisibleContent];
-
- BOOL offsetTrigger = targetContentOffset->y <= -topOffset;
-
- // scrollViewWillEndDragging velocity is points/milliseconds
- CGFloat duration = fabs(scrollView.contentOffset.y - targetContentOffset->y)/fabs(velocity.y*1000);
- duration = fminf(duration, 0.8);
- duration = fmaxf(duration, 0.2);
-
- if (velocityTriggerUp) {
- [self showHeaderWithDuration:duration completion:nil];
- } else if (offsetTrigger && velocity.y < 0.0) {
- [self showHeaderWithDuration:duration completion:nil];
- } else if (velocityTriggerDown) {
- [self hideHeaderWithDuration:duration];
- }
- }
- -(BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
- isScrollingToTop = YES;
- [self hideKeyboardTemporarily:NO];
-
- if ([self checkShouldShowHeader]) {
- CGFloat yOffset = scrollView.contentOffset.y - [headerView getHeight];
-
- UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
- [UIView animateWithDuration:0.3 delay:0.0 options:options animations:^{
- scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, yOffset);
- } completion:^(BOOL finished) {
- isScrollingToTop = NO;
- }];
- };
-
- return YES;
- }
- #pragma mark - header view show/hide
- - (void)showHeaderWithDuration:(CGFloat)duration completion:(void (^ __nullable)(BOOL finished))completion {
-
- CGFloat targetOffset = [self topOffsetForVisibleContent];
- CGRect targetRect = [RectUtil setYPositionOf:headerView.frame y:targetOffset];
-
- if (headerView.hidden == NO && CGRectEqualToRect(targetRect, headerView.frame)) {
- return;
- }
-
- if ([self shouldShowHeader] == NO) {
- return;
- }
-
- CGFloat headerHeight = [headerView getHeight];
- headerView.frame = [RectUtil setYPositionOf:headerView.frame y: -headerHeight];
- headerView.hidden = NO;
- showHeader = YES;
-
- UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
- [UIView animateWithDuration:duration delay:0.0 options:options animations:^{
- headerView.frame = targetRect;
- [self updateChatContentInset];
- } completion:^(BOOL finished) {
- headerView.hidden = NO;
- if (completion != nil) {
- completion(YES);
- }
- }];
- }
- - (void)hideHeaderWithDuration:(CGFloat)duration {
- CGFloat headerHeight = [headerView getHeight];
- CGRect targetRect = [RectUtil setYPositionOf:headerView.frame y: -headerHeight];
- if (headerView.hidden == YES && CGRectEqualToRect(targetRect, headerView.frame)) {
- return;
- }
-
- if ([self shouldShowHeader]) {
- return;
- }
-
- showHeader = NO;
-
- UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
- [UIView animateWithDuration:duration delay:0.0 options:options animations:^{
- headerView.frame = targetRect;
- } completion:^(BOOL finished) {
- [self updateChatContentInset];
- headerView.hidden = YES;
- }];
- }
- - (BOOL)shouldShowHeader {
- if (_isOpenWithForceTouch) {
- chatBarWrapper.hidden = YES;
- if (@available(iOS 13.0, *)) {
- if ([self chatContentSmallerThanVisibleArea]) {
- chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, [self unvisibleChatHeight]);
- } else {
- chatContent.frame = CGRectMake(chatContent.frame.origin.x, CGRectGetHeight(chatContent.frame) - [self unvisibleChatHeight], chatContent.frame.size.width, [self unvisibleChatHeight]);
- }
- } else {
- if ([self chatContentSmallerThanVisibleArea]) {
- chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y, chatContent.frame.size.width, [self unvisibleChatHeight]);
- } else {
- chatContent.frame = CGRectMake(chatContent.frame.origin.x, chatContent.frame.origin.y - CGRectGetHeight(headerView.frame), chatContent.frame.size.width, [self unvisibleChatHeight] + CGRectGetHeight(headerView.frame));
- }
- }
- return NO;
- }
- else if (_searching) {
- // always show when searching
- return YES;
- } else if (SYSTEM_IS_IPAD == NO && UIDeviceOrientationIsLandscape((UIDeviceOrientation)[[UIApplication sharedApplication] statusBarOrientation])) {
- //hide for landscape - only on iPhone
- return NO;
- } else if (lastKeyboardHeight > 0.0 && !isScrollingToTop) {
- //hide if not enough space
- return NO;
- } else if ([self chatContentSmallerThanVisibleArea]) {
- // show if area not filled with chat content
- return YES;
- } else if (self.editing) {
- // hide when editing
- return NO;
- } else if (isFirstAppearance) {
- // initially hidden
- return NO;
- } else if (isScrollingToTop) {
- // keep when scrolling to top
- return YES;
- } else if (shouldScrollDown) {
- // don't show header when initially scrolling down
- return NO;
- } else if (isScrollingToUnreadMessages) {
- // don't show header when scrolling to first unread messages
- isScrollingToUnreadMessages = NO;
- return NO;
- } else if (isNewMessageReceivedInActiveChat) {
- // don't show header if receive new message in a active chat
- isNewMessageReceivedInActiveChat = NO;
- return NO;
- }
-
- // otherwise toggle
- return headerView.hidden;
- }
- - (BOOL)checkShouldShowHeader {
- if ([self shouldShowHeader]) {
- [self showHeaderWithDuration:0.3 completion:nil];
- return YES;
- } else {
- [self hideHeaderWithDuration:0.3];
- return NO;
- }
- }
- - (void)toggleHeader {
- if (headerView.hidden) {
- [self hideKeyboardTemporarily:NO];
- [self showHeaderWithDuration:0.3 completion:nil];
- } else {
- [self hideHeaderWithDuration:0.3];
- }
- }
- - (BOOL)visible {
- return visible;
- }
- - (CGFloat)visibleChatHeight {
- return CGRectGetHeight(self.chatContent.frame) - chatContent.contentInset.top;
- }
- - (CGFloat)unvisibleChatHeight {
- return CGRectGetHeight(self.chatContent.frame) + chatBarWrapper.frame.size.height;
- }
- - (BOOL)chatContentSmallerThanVisibleArea {
- CGFloat heightOfVisibleChatView = [self visibleChatHeight] - CGRectGetHeight(headerView.frame);
- return heightOfVisibleChatView - chatContent.contentSize.height >= 0.0;
- }
- #pragma mark - ChatViewHeaderDelegate
- -(void)didChangeHeightTo:(CGFloat)newHeight {
- [self updateChatContentInset];
- }
- #pragma mark - GroupDetailsViewControllerDelegate
- - (void)presentGroupDetails:(GroupDetailsViewController *)groupDetailsViewController onCompletion:(GroupDetailsCompletionBlock)onCompletion {
- // not used
- }
- - (void)updateMembersObserver:(NSSet *)oldMembers newMembers:(NSSet *)newMembers {
- if (oldMembers != nil && oldMembers != (id)[NSNull null]) {
- [oldMembers enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
- @try {
- [contact removeObserver:self forKeyPath:@"displayName"];
- }
- @catch (NSException * __unused exception) {}
- }];
- }
- if (newMembers != nil && newMembers != (id)[NSNull null]) {
- [newMembers enumerateObjectsUsingBlock:^(Contact *contact, BOOL * _Nonnull stop) {
- @try {
- [contact addObserver:self forKeyPath:@"displayName" options:0 context:nil];
- }
- @catch (NSException * __unused exception) {}
- }];
- }
- }
- # pragma mark - preview actions
- - (NSArray<id<UIPreviewActionItem>> *)previewActionItems {
- NSMutableArray *previewActions = [NSMutableArray array];
-
- if (_delegate == nil) {
- return previewActions;
- }
-
- if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
- NSString *actionTitle = NSLocalizedString(@"take_photo_or_video", nil);
- UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
- SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController];
- sendMediaAction.mediaPickerType = MediaPickerTakePhoto;
-
- currentAction = sendMediaAction;
- [sendMediaAction executeAction];
- }];
- }];
-
- [previewActions addObject:shareAction];
- }
-
- if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
- NSString *actionTitle = NSLocalizedString(@"choose_existing", nil);
- UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
-
- SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController];
- sendMediaAction.mediaPickerType = MediaPickerChooseExisting;
-
- currentAction = sendMediaAction;
- [sendMediaAction executeAction];
- }];
- }];
-
- [previewActions addObject:shareAction];
- }
-
- if ([CLLocationManager locationServicesEnabled]) {
- NSString *actionTitle = NSLocalizedString(@"share_location", nil);
- UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
- SendLocationAction *sendLocationAction = [SendLocationAction actionForChatViewController:chatViewController];
- currentAction = sendLocationAction;
- [sendLocationAction executeAction];
- }];
- }];
-
- [previewActions addObject:shareAction];
- }
-
- if ([PlayRecordAudioViewController canRecordAudio]) {
- NSString *actionTitle = NSLocalizedString(@"record_audio", nil);
- UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
- [chatViewController startRecordingAudio];
- }];
- }];
-
- [previewActions addObject:shareAction];
- }
-
- NSString *actionTitle = NSLocalizedStringFromTable(@"ballot_create", @"Ballot", nil);
- UIPreviewAction *shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
-
- [chatViewController createBallot];
- }];
- }];
-
- [previewActions addObject:shareAction];
-
- actionTitle = NSLocalizedString(@"share_file", nil);
- shareAction = [UIPreviewAction actionWithTitle:actionTitle style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
-
- [_delegate presentChatViewController:self onCompletion:^(ChatViewController *chatViewController) {
- [chatViewController sendFile];
- }];
- }];
-
- [previewActions addObject:shareAction];
-
- return previewActions;
- }
- #pragma mark - UIViewControllerPreviewingDelegate
- - (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
-
- if ([viewControllerToCommit isKindOfClass:[UINavigationController class]]) {
- UINavigationController *navigationController = (UINavigationController *)viewControllerToCommit;
- navigationController.navigationBar.hidden = NO;
-
- if ([navigationController.topViewController isKindOfClass:[MWPhotoBrowser class]]) {
- ((MWPhotoBrowser*)navigationController.topViewController).peeking = NO;
- }
-
- [self presentViewController:viewControllerToCommit animated:YES completion:nil];
- }
- else if ([viewControllerToCommit isKindOfClass:[ThreemaSafariViewController class]]) {
- [self.navigationController presentViewController:viewControllerToCommit animated:false completion:^{
- [viewControllerToCommit dismissViewControllerAnimated:false completion:^{
- [[UIApplication sharedApplication] openURL:((ThreemaSafariViewController *)viewControllerToCommit).url options:@{} completionHandler:nil];
- }];
- }];
-
- } else {
- [self.navigationController pushViewController:viewControllerToCommit animated:YES];
- }
- }
- - (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
-
- UIView *view = [self.view hitTest:location withEvent:nil];
-
- ChatMessageCell *cell = (ChatMessageCell *)[Utils view:view getSuperviewOfKind:[ChatMessageCell class]];
- if (cell) {
- forceTouching = YES;
- UIViewController *previewController = [cell previewViewController];
-
- if ([[cell previewViewControllerFor:previewingContext viewControllerForLocation:location] isKindOfClass:[ThreemaSafariViewController class]]) {
- previewController = [cell previewViewControllerFor:previewingContext viewControllerForLocation:location];
- if (!previewController || UIAccessibilityIsVoiceOverRunning()) {
- return nil;
- }
- _Bool legalURL = [IDNSafetyHelper isLegalURLWithUrl:((ThreemaSafariViewController *)previewController).url];
- if (!legalURL) {
- return nil;
- }
- }
- else {
- if ([previewController isKindOfClass:[UINavigationController class]]) {
- UINavigationController *navigationController = (UINavigationController *)previewController;
- navigationController.navigationBar.hidden = YES;
- }
- }
-
- previewingContext.sourceRect = [self.view convertRect:cell.frame fromView:self.chatContent];
-
- return previewController;
- }
-
- return nil;
- }
- #pragma mark - Audio player/recorder delegate
- - (void)audioPlayerDidHide {
- UITableViewCell *selectedCell = [self.chatContent cellForRowAtIndexPath:selectedAudioMessage];
-
- if (selectedCell) {
- UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, selectedCell);
- } else {
- UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
- }
- audioRecorder = nil;
- selectedAudioMessage = nil;
- }
- #pragma mark - Notifications
- - (void)showProfilePictureChanged:(NSNotification*)notification {
- [headerView refresh];
- }
- #pragma mark - PPAssetsActionHelperDelegate
- - (void)assetsActionHelperDidCancel:(PPAssetsActionHelper *)picker {
- // do nothing
- }
- - (void)assetsActionHelper:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
-
- }
- - (void)assetActionHelperDidSelectOwnOption:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
-
- }
- - (void)assetsActionHelperDidSelectOwnSnapButton:(PPAssetsActionHelper *)picker didFinishPicking:(NSArray *)assets {
- [self dismissViewControllerAnimated:YES completion:nil];
- if (assets && assets.count) {
- SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
- [action showPreviewForAssets:assets];
- } else {
- SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
- action.mediaPickerType = MediaPickerChooseExisting;
-
- currentAction = action;
- [action executeAction];
- }
- }
- - (void)assetsActionHelperDidSelectLiveCameraCell:(PPAssetsActionHelper *)picker {
- [self dismissViewControllerAnimated:YES completion:nil];
- SendMediaAction *action = [SendMediaAction actionForChatViewController:self];
- action.mediaPickerType = MediaPickerTakePhoto;
-
- currentAction = action;
- [action executeAction];
- }
- - (void)assetsActionHelperDidSelectLocation:(PPAssetsActionHelper *)picker {
- [self dismissViewControllerAnimated:YES completion:nil];
- SendLocationAction *action = [SendLocationAction actionForChatViewController:self];
- currentAction = action;
- [action executeAction];
- }
- - (void)assetsActionHelperDidSelectRecordAudio:(PPAssetsActionHelper *)picker {
- [self dismissViewControllerAnimated:YES completion:^{
- [self startRecordingAudio];
- }];
- }
- - (void)assetsActionHelperDidSelectCreateBallot:(PPAssetsActionHelper *)picker {
- [self dismissViewControllerAnimated:YES completion:nil];
- [self createBallot];
- }
- - (void)assetsActionHelperDidSelectShareFile:(PPAssetsActionHelper *)picker {
- [self dismissViewControllerAnimated:YES completion:nil];
- [self sendFile];
- }
- #pragma mark - Voip
- - (void)startVoipCall:(BOOL)withVideo {
- [self hideHeaderWithDuration:0.0];
- [FeatureMask checkFeatureMask:FEATURE_MASK_VOIP forContacts:[NSSet setWithObjects:self.conversation.contact, nil] onCompletion:^(NSArray *unsupportedContacts) {
- if (unsupportedContacts.count == 0) {
- VoIPCallUserAction *action = [[VoIPCallUserAction alloc] initWithAction:withVideo ? ActionCallWithVideo : ActionCall contact:conversation.contact callId:nil completion:nil];
- [[VoIPCallStateManager shared] processUserAction:action];
- } else {
- [UIAlertTemplate showAlertWithOwner:self title:NSLocalizedString(@"call_voip_not_supported_title", nil) message:NSLocalizedString(@"call_voip_not_supported_text", nil) actionOk:nil];
- }
- }];
- }
- @end
|