From 40eb0d52086a02e73745b9298a97f8606221b8b0 Mon Sep 17 00:00:00 2001 From: Benjamin Verdier Date: Fri, 29 Jun 2018 13:58:37 +0200 Subject: [PATCH] Add support for multiple image and text + image sending --- Classes/Base.lproj/ChatConversationView.xib | 24 +++ Classes/ChatConversationView.h | 10 +- Classes/ChatConversationView.m | 171 +++++++++++++++++++- Classes/LinphoneUI/UIChatBubblePhotoCell.m | 42 ++++- Classes/LinphoneUI/UIImageViewDeletable.h | 26 +++ Classes/LinphoneUI/UIImageViewDeletable.m | 59 +++++++ Classes/LinphoneUI/UIImageViewDeletable.xib | 50 ++++++ Classes/PhoneMainView.m | 2 + Classes/Utils/FileTransferDelegate.h | 1 + Classes/Utils/FileTransferDelegate.m | 2 +- Resources/images/delete_img.png | Bin 0 -> 1184 bytes 11 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 Classes/LinphoneUI/UIImageViewDeletable.h create mode 100644 Classes/LinphoneUI/UIImageViewDeletable.m create mode 100644 Classes/LinphoneUI/UIImageViewDeletable.xib create mode 100644 Resources/images/delete_img.png diff --git a/Classes/Base.lproj/ChatConversationView.xib b/Classes/Base.lproj/ChatConversationView.xib index 604c03dc1..1bb0fba72 100644 --- a/Classes/Base.lproj/ChatConversationView.xib +++ b/Classes/Base.lproj/ChatConversationView.xib @@ -18,6 +18,8 @@ + + @@ -269,6 +271,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Classes/ChatConversationView.h b/Classes/ChatConversationView.h index 7a264ca8e..0a2f99c0c 100644 --- a/Classes/ChatConversationView.h +++ b/Classes/ChatConversationView.h @@ -27,12 +27,13 @@ #import "UIRoundedImageView.h" #import "UIBackToCallButton.h" #import "Utils/HPGrowingTextView/HPGrowingTextView.h" +#import "UIImageViewDeletable.h" #include "linphone/linphonecore.h" @interface ChatConversationView : TPMultiLayoutViewController { + UIDocumentInteractionControllerDelegate, UISearchBarDelegate, UIImageViewDeletableDelegate, UICollectionViewDataSource> { OrderedDictionary *imageQualities; BOOL scrollOnGrowingEnabled; BOOL composingVisible; @@ -60,6 +61,12 @@ @property (weak, nonatomic) IBOutlet UIIconButton *infoButton; @property (weak, nonatomic) IBOutlet UILabel *particpantsLabel; @property (nonatomic, strong) UIDocumentInteractionController *documentInteractionController; +@property NSMutableArray *imagesArray; +@property NSMutableArray *assetIdsArray; +@property NSMutableArray *qualitySettingsArray; +@property (weak, nonatomic) IBOutlet UICollectionView *imagesCollectionView; +@property (weak, nonatomic) IBOutlet UIView *imagesView; + + (void)markAsRead:(LinphoneChatRoom *)chatRoom; - (void)configureForRoom:(BOOL)editing; @@ -74,5 +81,6 @@ - (IBAction)onEditionChangeClick:(id)sender; - (void)update; - (void)openResults:(NSString *) filePath; +- (void)clearMessageView; @end diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index 54077999b..6008b869e 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -101,6 +101,8 @@ static UICompositeViewDescription *compositeDescription = nil; _messageField.contentInset = UIEdgeInsetsMake(-15, 0, 0, 0); // _messageField.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, 0, 10); [_tableController setChatRoomDelegate:self]; + [_imagesCollectionView registerClass:[UIImageViewDeletable class] forCellWithReuseIdentifier:NSStringFromClass([UIImageViewDeletable class])]; + [_imagesCollectionView setDataSource:self]; } - (void)viewWillAppear:(BOOL)animated { @@ -125,6 +127,24 @@ static UICompositeViewDescription *compositeDescription = nil; selector:@selector(callUpdateEvent:) name:kLinphoneCallUpdate object:nil]; + + if ([_imagesArray count] > 0) { + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - 100; + imagesFrame.size.height = 100; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height -= 100; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + } } - (void)viewWillDisappear:(BOOL)animated { @@ -203,7 +223,6 @@ static UICompositeViewDescription *compositeDescription = nil; _messageField.editable = !linphone_chat_room_has_been_left(_chatRoom); _pictureButton.enabled = !linphone_chat_room_has_been_left(_chatRoom); _messageView.userInteractionEnabled = !linphone_chat_room_has_been_left(_chatRoom); - [_messageField setText:@""]; [_tableController setChatRoom:_chatRoom]; _chatView.hidden = NO; @@ -222,7 +241,12 @@ static UICompositeViewDescription *compositeDescription = nil; //share photo NSData *data = dict[@"nsData"]; UIImage *image = [[UIImage alloc] initWithData:data]; - [self chooseImageQuality:image assetId:nil]; + NSString *filename = dict[@"url"]; + if (filename) { + NSMutableDictionary * assetDict = [LinphoneUtils photoAssetsDictionary]; + [self chooseImageQuality:image assetId:[[assetDict objectForKey:filename] localIdentifier]]; + } else + [self chooseImageQuality:image assetId:@""]; [defaults removeObjectForKey:@"img"]; } else if (dictWeb) { //share url, if local file, then upload file @@ -311,7 +335,11 @@ static UICompositeViewDescription *compositeDescription = nil; } - (void)saveAndSend:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality{ - [self startImageUpload:image assetId:phAssetId withQuality:quality]; + + [_imagesArray addObject:image]; + [_assetIdsArray addObject:phAssetId]; + [_qualitySettingsArray addObject:@(quality)]; + [self addImageToDrawer:image withAssetId:phAssetId]; } - (void)chooseImageQuality:(UIImage *)image assetId:(NSString *)phAssetId { @@ -456,6 +484,12 @@ static UICompositeViewDescription *compositeDescription = nil; messageRect.size.height += diff; [_messageView setFrame:messageRect]; + if ([_imagesArray count] > 0) { + CGRect _imagesRect = [_imagesView frame]; + _imagesRect.origin.y -= diff; + [_imagesView setFrame:_imagesRect]; + } + // Always stay at bottom if (scrollOnGrowingEnabled) { CGRect tableFrame = [_tableController.view frame]; @@ -493,6 +527,15 @@ static UICompositeViewDescription *compositeDescription = nil; } - (IBAction)onSendClick:(id)event { + if ([_imagesArray count] > 0) { + int i = 0; + for (i = 0; i < [_imagesArray count] - 1; ++i) { + [self startImageUpload:[_imagesArray objectAtIndex:i] assetId:[_assetIdsArray objectAtIndex:i] withQuality:[_qualitySettingsArray objectAtIndex:i].floatValue]; + } + [self startImageUpload:[_imagesArray objectAtIndex:i] assetId:[_assetIdsArray objectAtIndex:i] withQuality:[_qualitySettingsArray objectAtIndex:i].floatValue andMessage:[self.messageField text]]; + [self clearMessageView]; + return; + } if ([self sendMessage:[_messageField text] withExterlBodyUrl:nil withInternalURL:nil]) { scrollOnGrowingEnabled = FALSE; [_messageField setText:@""]; @@ -583,6 +626,14 @@ static UICompositeViewDescription *compositeDescription = nil; return TRUE; } +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality andMessage:(NSString *)message { + FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; + [fileTransfer setText:message]; + [fileTransfer upload:image withassetId:phAssetId forChatRoom:_chatRoom withQuality:quality]; + [_tableController scrollToBottom:true]; + return TRUE; +} + - (BOOL)startFileUpload:(NSData *)data withUrl:(NSURL *)url { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; [fileTransfer uploadFile:data forChatRoom:_chatRoom withUrl:url]; @@ -611,6 +662,28 @@ static UICompositeViewDescription *compositeDescription = nil; [VIEW(ImagePickerView).popoverController dismissPopoverAnimated:TRUE]; } [self chooseImageQuality:image assetId:phAssetId]; + //[self chooseImageQuality:image assetId:phAssetId]; +} + +- (void)addImageToDrawer:(UIImage *)img withAssetId:(NSString *)assetId { + if ([_imagesArray count] == 1) { // We resize chatView to display the image + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - 100; + imagesFrame.size.height = 100; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height -= 100; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + } + [_imagesCollectionView reloadData]; } - (void)tableViewIsScrolling { @@ -667,6 +740,19 @@ static UICompositeViewDescription *compositeDescription = nil; } } } + + + if ([_imagesArray count] > 0){ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - 100; + imagesFrame.size.height = 100; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height -= 100; + [_tableController.tableView setFrame:tableViewFrame]; + } } completion:^(BOOL finished){ @@ -722,6 +808,18 @@ static UICompositeViewDescription *compositeDescription = nil; [_messageView frame].origin.y - tableFrame.origin.y - composeIndicatorCompensation; [_tableController.view setFrame:tableFrame]; } + + if ([_imagesArray count] > 0){ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - 100; + imagesFrame.size.height = 100; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height -= 100; + [_tableController.tableView setFrame:tableViewFrame]; + } // Scroll NSInteger lastSection = [_tableController.tableView numberOfSections] - 1; @@ -734,6 +832,7 @@ static UICompositeViewDescription *compositeDescription = nil; animated:FALSE]; } } + } completion:^(BOOL finished){ }]; @@ -839,4 +938,70 @@ void on_chat_room_conference_left(LinphoneChatRoom *cr, const LinphoneEventLog * } } +- (void)deleteImageWithAssetId:(NSString *)assetId { + NSUInteger key = [_assetIdsArray indexOfObject:assetId]; + [_imagesArray removeObjectAtIndex:key]; + [_assetIdsArray removeObjectAtIndex:key]; + if ([_imagesArray count] == 0) { + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y; + imagesFrame.size.height = 0; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height += 100; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + + [_sendButton setEnabled:FALSE]; + } + [_imagesCollectionView reloadData]; +} + +- (void)clearMessageView { + [_messageField setText:@""]; + _imagesArray = [NSMutableArray array]; + _assetIdsArray = [NSMutableArray array]; + + // resizing imagesView + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y; + imagesFrame.size.height = 0; + [_imagesView setFrame:imagesFrame]; + + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + CGFloat composeIndicatorCompensation = composingVisible ? _composeIndicatorView.frame.size.height : 0.0f; + tableViewFrame.size.height = [_messageView frame].origin.y - tableViewFrame.origin.y - composeIndicatorCompensation; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + + [_imagesCollectionView reloadData]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return [_imagesArray count]; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + UIImageViewDeletable *imgView = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([UIImageViewDeletable class]) forIndexPath:indexPath]; + [imgView.image setImage:[UIImage resizeImage:[_imagesArray objectAtIndex:[indexPath item]] withMaxWidth:50 andMaxHeight:100]]; + [imgView setAssetId:[_assetIdsArray objectAtIndex:[indexPath item]]]; + [imgView setDeleteDelegate:self]; + [_sendButton setEnabled:TRUE]; + return imgView; +} + @end diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.m b/Classes/LinphoneUI/UIChatBubblePhotoCell.m index 99c85c91e..d81c69c3b 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.m +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.m @@ -38,7 +38,6 @@ - (id)initWithIdentifier:(NSString *)identifier { if ((self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]) != nil) { - // TODO: remove text cell subview NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; // resize cell to match .nib size. It is needed when resized the cell to @@ -119,7 +118,7 @@ [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage *image, NSDictionary * info) { if (image) { - imageSize = [UIChatBubbleTextCell getMediaMessageSizefromOriginalSize:[image size] withWidth:chatTableView.tableView.frame.size.width]; + imageSize = [UIChatBubbleTextCell getMediaMessageSizefromOriginalSize:[image size] withWidth:chatTableView.tableView.frame.size.width - 40]; UIImage *newImage = [UIImage UIImageResize:image toSize:imageSize]; [chatTableView.imagesInChatroom setObject:newImage forKey:[asset localIdentifier]]; [self loadImageAsset:asset image:newImage]; @@ -167,13 +166,24 @@ }); } +- (void) loadPlaceholder { + dispatch_async(dispatch_get_main_queue(), ^{ + //[_finalImage setImage:image]; + //[_messageImageView setAsset:asset]; + [_messageImageView stopLoading]; + _messageImageView.hidden = YES; + _imageGestureRecognizer.enabled = YES; + _finalImage.hidden = NO; + [self layoutSubviews]; + }); +} + - (void)update { if (self.message == nil) { LOGW(@"Cannot update message room cell: NULL message"); return; } - [super update]; - + [super update]; const char *url = linphone_chat_message_get_external_body_url(self.message); BOOL is_external = (url && (strstr(url, "http") == url)) || linphone_chat_message_get_file_transfer_information(self.message); @@ -203,7 +213,7 @@ PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localImage] options:nil]; UIImage *img = [chatTableView.imagesInChatroom objectForKey:localImage]; if (![assets firstObject]) - return; + [self loadPlaceholder]; PHAsset *asset = [assets firstObject]; if (img) [self loadImageAsset:asset image:img]; @@ -415,6 +425,28 @@ bubbleFrame.origin.x = origin_x; super.bubbleView.frame = bubbleFrame; + + // Resizing Image view + if (_finalImage.image) { + CGRect imgFrame = self.finalAssetView.frame; + imgFrame.size = [UIChatBubbleTextCell getMediaMessageSizefromOriginalSize:[_finalImage.image size] withWidth:chatTableView.tableView.frame.size.width - 40]; + imgFrame.origin.x = (bubbleFrame.size.width - imgFrame.size.width)/2; + self.finalAssetView.frame = imgFrame; + + // Positioning text message + const char *utf8Text = linphone_chat_message_get_text_content(self.message); + + CGRect textFrame = self.messageText.frame; + textFrame.origin = CGPointMake(textFrame.origin.x, self.finalAssetView.frame.origin.y + self.finalAssetView.frame.size.height); + if (!utf8Text) { + textFrame.size.height = 0; + } else { + textFrame.size.height = bubbleFrame.size.height - textFrame.origin.x; + } + + self.messageText.frame = textFrame; + LOGD([NSString stringWithFormat:@"Text of the photoCell: %@, size of the text of the photoCell: %@", [self.messageText text], NSStringFromCGSize(textFrame.size)]); + } } @end diff --git a/Classes/LinphoneUI/UIImageViewDeletable.h b/Classes/LinphoneUI/UIImageViewDeletable.h new file mode 100644 index 000000000..f036fd6b7 --- /dev/null +++ b/Classes/LinphoneUI/UIImageViewDeletable.h @@ -0,0 +1,26 @@ +// +// UIImageViewDeletable.h +// linphone +// +// Created by benjamin_verdier on 28/06/2018. +// + +#import + +@protocol UIImageViewDeletableDelegate + +@required + +- (void)deleteImageWithAssetId:(NSString *)assetId; + +@end + +@interface UIImageViewDeletable : UICollectionViewCell + +@property NSString *assetId; +@property(nonatomic, strong) id deleteDelegate; +@property (weak, nonatomic) IBOutlet UIImageView *image; + +- (IBAction)onDeletePressed; + +@end diff --git a/Classes/LinphoneUI/UIImageViewDeletable.m b/Classes/LinphoneUI/UIImageViewDeletable.m new file mode 100644 index 000000000..157dd640a --- /dev/null +++ b/Classes/LinphoneUI/UIImageViewDeletable.m @@ -0,0 +1,59 @@ +// +// UIImageViewDeletable.m +// linphone +// +// Created by benjamin_verdier on 28/06/2018. +// + +#import "UIImageViewDeletable.h" + +@interface UIImageViewDeletable () + +@end + +@implementation UIImageViewDeletable + +- (UIImageViewDeletable *)init { + self = [super init]; + if (self) { + NSArray *arrayOfViews = + [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; + // resize cell to match .nib size. It is needed when resized the cell to + // correctly adapt its height too + UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:arrayOfViews.count - 1]); + [self setFrame:CGRectMake(0, 0, sub.frame.size.width, sub.frame.size.height)]; + [self addSubview:sub]; + } + return self; +} + + +- (UIImageViewDeletable *)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + NSArray *arrayOfViews = + [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; + // resize cell to match .nib size. It is needed when resized the cell to + // correctly adapt its height too + UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:arrayOfViews.count - 1]); + [self setFrame:frame]; + [self addSubview:sub]; + } + return self; +} + +- (IBAction)onDeletePressed { + [_deleteDelegate deleteImageWithAssetId:_assetId]; +} + +/* +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. +} +*/ + +@end diff --git a/Classes/LinphoneUI/UIImageViewDeletable.xib b/Classes/LinphoneUI/UIImageViewDeletable.xib new file mode 100644 index 000000000..2fa5d17e1 --- /dev/null +++ b/Classes/LinphoneUI/UIImageViewDeletable.xib @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Classes/PhoneMainView.m b/Classes/PhoneMainView.m index 860c0599a..1f35fae2a 100644 --- a/Classes/PhoneMainView.m +++ b/Classes/PhoneMainView.m @@ -934,6 +934,8 @@ static RootViewManager *rootViewManagerInstance = nil; linphone_chat_room_remove_callbacks(view.chatRoom, view.chatRoomCbs); view.chatRoomCbs = NULL; + if (view.chatRoom != cr) + [view clearMessageView]; view.chatRoom = cr; self.currentRoom = view.chatRoom; if (PhoneMainView.instance.currentView == view.compositeViewDescription) diff --git a/Classes/Utils/FileTransferDelegate.h b/Classes/Utils/FileTransferDelegate.h index 34a63df2a..17a4bcc5a 100644 --- a/Classes/Utils/FileTransferDelegate.h +++ b/Classes/Utils/FileTransferDelegate.h @@ -19,4 +19,5 @@ - (void)stopAndDestroy; @property() LinphoneChatMessage *message; +@property() NSString *text; @end diff --git a/Classes/Utils/FileTransferDelegate.m b/Classes/Utils/FileTransferDelegate.m index 7b620e216..9268ff0d6 100644 --- a/Classes/Utils/FileTransferDelegate.m +++ b/Classes/Utils/FileTransferDelegate.m @@ -208,8 +208,8 @@ static LinphoneBuffer *linphone_iphone_file_transfer_send(LinphoneChatMessage *m linphone_content_set_subtype(content, [subtype UTF8String]); linphone_content_set_name(content, [name UTF8String]); linphone_content_set_size(content, _data.length); - _message = linphone_chat_room_create_file_transfer_message(chatRoom, content); + linphone_chat_message_add_text_content(_message, [_text UTF8String]); linphone_content_unref(content); linphone_chat_message_cbs_set_file_transfer_send(linphone_chat_message_get_callbacks(_message), diff --git a/Resources/images/delete_img.png b/Resources/images/delete_img.png new file mode 100644 index 0000000000000000000000000000000000000000..b9aaa86c98f4c6d8346996b29c35a20548739ba7 GIT binary patch literal 1184 zcmV;R1Yi4!P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1T{%SK~!i%?V3r* zPEi=gKl6|&Lm5`sNJ)l-1yLjmk&U4cvQegmSePx4X=Tce*kLC_VIf4xoGBC=Wz0PP ze>YFv-rjreIrrZ8zTe{g)o*c6cY3~ihUYx**VLdv^QX6*BlpV-@|FA~|H?n|t$ZL) z%8hcoY%7}u?c{uUN&c3+bfrJ5w3+YZK{-;^h1PPpd@1dvKFU*)_6f4DY$J_Ma*&)M zx6A92FJ;%Ck)vd3jFoq!y}(sDS9Xx043t~t8_B)+st05jSuL1(!^ zev#b!k*rB~V^AKGJmi<;q49206a(ZH$#V#5nk)>0I4!w3U0|H7C5*p4(*9_(Wo}x? zqmr94um{VsSS@+(cR7)UJTNA>*=sq3hLTw?c|Oy0e;TqdM)GN~*4@d4^@wYdZWjat z*{0ZaG}IB+pNEonVT}rc3%HOewidHfD2x%_=Y#A?gAdG7wqpCN6a2;U1lvurMx}8= z@=jZ#BM%lxE}&1QK@jidP1%cvKxWEsa$j`hW|$gZ5oE3eLfVlAUxFYw|CS61 zh9H<)xpo;19*~@J{;cT8K_5xJLju^51`l|7f(QvF9>gXZJXtKchHmDB{RAhH#fA_P zOx7TPJrCE>y;{+b1bvkgS42l)_ym(D2!qWT*AI=361XTiiQQWkT)`9(1X%Lw%)!{6 za#FwOC>zy+DJlrqEV&QqOCq+%&byb62lG@G6$C7m+=~o35l-NtML;l*CHKS~vIyo$ z?sqskO27&TMG)^!SUG00VpFhl5oML;Bv$M^#Dmx=E5}S8R!Z(g0V9$7l9TdmpsEe* z_?XEX%wEZToS85SAtzC?$%1QkiU)K1|8W*<+dVZpN`ML=CpkY`0zOkLFHaCzgCR?% zV#)QSQ;Dz>EmQ7 zfN6A>>?cBkFyg@+p&^C%hL62TDkCH(Q^QJ|hRHw=$t+1G5X^BoJvwr;Q1V_kq9aev zN-lEdGbPeqK9jst(2Pn|oK^m-q>Nc_EK#IjPSG$38!MM(Ju4}rC6fvDHM)H;UF8!= zFwRV~hA@VnOWwh|dQy$y^0VYdYiKAD{*X4Sos-V3tq+v;`K;WqtijLT+^RDKLr{f4 zQNk8IYO#5kaFEKHVEFL(g8*xaT53MjgV^ONvVsRvre%UDl^&8Q z(Yl*?Pm*@fO(+!bJNy#4O0p?pKVhl3$yX*#30~}qguYEJmTp!VluJ_aU9$8q-~;`d y3Lt^;%P$Kl|Ji`sB>U|tvhbOxL4%sm)btM!*jy$GkI