From 416b271460b8634edce0b776239c9eeea030505d Mon Sep 17 00:00:00 2001 From: Danmei Chen Date: Tue, 30 Oct 2018 15:46:43 +0100 Subject: [PATCH] 4.1 : new ChatConversationView --- Classes/ChatConversationTableView.m | 75 ++++++++--- .../Base.lproj/UIChatBubbleTextCell.xib | 84 ++++-------- Classes/LinphoneUI/UIChatBubbleTextCell.h | 13 +- Classes/LinphoneUI/UIChatBubbleTextCell.m | 123 +++++++++++++----- Resources/images/chat_delivered.png | Bin 799 -> 925 bytes Resources/images/chat_error.png | Bin 494 -> 540 bytes Resources/images/chat_read.png | Bin 867 -> 963 bytes 7 files changed, 181 insertions(+), 114 deletions(-) diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index 20842d192..40e6edde9 100644 --- a/Classes/ChatConversationTableView.m +++ b/Classes/ChatConversationTableView.m @@ -93,6 +93,7 @@ NSIndexPath *indexPath = [NSIndexPath indexPathForRow:pos inSection:0]; [self.tableView beginUpdates]; [self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade]; + [self.tableView reloadData]; [self.tableView endUpdates]; } @@ -165,6 +166,51 @@ [self reloadData]; } +- (BOOL)isFirstIndexInTableView:(NSIndexPath *)indexPath chat:(LinphoneChatMessage *)chat { + if (indexPath.row == 0) + return TRUE; + + LinphoneEventLog *previousEvent = nil; + NSInteger indexOfPreviousEvent = indexPath.row - 1; + while (!previousEvent && indexOfPreviousEvent > -1) { + LinphoneEventLog *tmp = [[eventList objectAtIndex:indexOfPreviousEvent] pointerValue]; + if (linphone_event_log_get_type(tmp) == LinphoneEventLogTypeConferenceChatMessage) { + previousEvent = tmp; + } + --indexOfPreviousEvent; + } + if (previousEvent) { + LinphoneChatMessage *previousChat = linphone_event_log_get_chat_message(previousEvent); + if (!linphone_address_equal(linphone_chat_message_get_from_address(previousChat), linphone_chat_message_get_from_address(chat))) { + return TRUE; + } + } + + return FALSE; +} + +- (BOOL)isLastIndexInTableView:(NSIndexPath *)indexPath chat:(LinphoneChatMessage *)chat { + LinphoneEventLog *nextEvent = nil; + NSInteger indexOfNextEvent = indexPath.row + 1; + while (!nextEvent && indexOfNextEvent < [eventList count]) { + LinphoneEventLog *tmp = [[eventList objectAtIndex:indexOfNextEvent] pointerValue]; + if (linphone_event_log_get_type(tmp) == LinphoneEventLogTypeConferenceChatMessage) { + nextEvent = tmp; + } + ++indexOfNextEvent; + } + + if (!nextEvent) + return TRUE; + + LinphoneChatMessage *nextChat = linphone_event_log_get_chat_message(nextEvent); + if (!linphone_address_equal(linphone_chat_message_get_from_address(nextChat), linphone_chat_message_get_from_address(chat))) { + return TRUE; + } + + return FALSE; +} + #pragma mark - UITableViewDataSource Functions - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { @@ -190,8 +236,11 @@ cell = [[NSClassFromString(kCellId) alloc] initWithIdentifier:kCellId]; } [cell setEvent:event]; - if (chat) - [cell update]; + if (chat) { + cell.isFirst = [self isFirstIndexInTableView:indexPath chat:chat]; + cell.isLast = [self isLastIndexInTableView:indexPath chat:chat]; + [cell update]; + } [cell setChatRoomDelegate:_chatRoomDelegate]; [super accessoryForCell:cell atPath:indexPath]; @@ -216,7 +265,7 @@ [_chatRoomDelegate tableViewIsScrolling]; } -static const CGFloat MESSAGE_SPACING_PERCENTAGE = 0.f; +static const CGFloat MESSAGE_SPACING_PERCENTAGE = 1.f; - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { LinphoneEventLog *event = [[eventList objectAtIndex:indexPath.row] pointerValue]; @@ -225,21 +274,11 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 0.f; //If the message is followed by another one that is not from the same address, we add a little space under it CGFloat height = 0; - LinphoneEventLog *nextEvent = nil; - NSInteger indexOfNextEvent = indexPath.row + 1; - while (!nextEvent && indexOfNextEvent < [eventList count]) { - LinphoneEventLog *tmp = [[eventList objectAtIndex:indexOfNextEvent] pointerValue]; - if (linphone_event_log_get_type(tmp) == LinphoneEventLogTypeConferenceChatMessage) { - nextEvent = tmp; - } - ++indexOfNextEvent; - } - if (nextEvent) { - LinphoneChatMessage *nextChat = linphone_event_log_get_chat_message(nextEvent); - if (!linphone_address_equal(linphone_chat_message_get_from_address(nextChat), linphone_chat_message_get_from_address(chat))) { - height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100; - } - } + if ([self isLastIndexInTableView:indexPath chat:chat]) + height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100; + if (![self isFirstIndexInTableView:indexPath chat:chat]) + height -= 20; + return [UIChatBubbleTextCell ViewHeightForMessage:chat withWidth:self.view.frame.size.width].height + height; } return [UIChatNotifiedEventCell height]; diff --git a/Classes/LinphoneUI/Base.lproj/UIChatBubbleTextCell.xib b/Classes/LinphoneUI/Base.lproj/UIChatBubbleTextCell.xib index 3540175ea..71473db07 100644 --- a/Classes/LinphoneUI/Base.lproj/UIChatBubbleTextCell.xib +++ b/Classes/LinphoneUI/Base.lproj/UIChatBubbleTextCell.xib @@ -11,85 +11,54 @@ - - + - - - - + + + - - + + + - + - - - - - - - - - - - - - - - - - - - - + + + - - + + - - - - - - - - - @@ -97,8 +66,7 @@ - - + diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h index f5a4a25e4..382398711 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.h +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.h @@ -30,17 +30,20 @@ @property(nonatomic, weak) IBOutlet UIImageView *backgroundColorImage; @property(nonatomic, weak) IBOutlet UIRoundedImageView *avatarImage; @property(nonatomic, weak) IBOutlet UILabel *contactDateLabel; -@property(weak, nonatomic) IBOutlet UIActivityIndicatorView *statusInProgressSpinner; +//@property(weak, nonatomic) IBOutlet UIActivityIndicatorView *statusInProgressSpinner; @property(nonatomic, weak) IBOutlet UITextViewNoDefine *messageText; -@property(weak, nonatomic) IBOutlet UIImageView *bottomBarColor; +//@property(weak, nonatomic) IBOutlet UIImageView *bottomBarColor; @property(nonatomic, strong) id chatRoomDelegate; @property(strong, nonatomic) IBOutlet UIView *bubbleView; @property(strong, nonatomic) IBOutlet UITapGestureRecognizer *resendRecognizer; -@property(weak, nonatomic) IBOutlet UIImageView *LIMEKO; +//@property(weak, nonatomic) IBOutlet UIImageView *LIMEKO; @property(weak, nonatomic) IBOutlet UIImageView *imdmIcon; -@property(weak, nonatomic) IBOutlet UILabel *imdmLabel; - +//@property(weak, nonatomic) IBOutlet UILabel *imdmLabel; @property (nonatomic, strong) UIDocumentPickerViewController *documentPicker; +@property (weak, nonatomic) IBOutlet UIView *innerView; + +@property(nonatomic) Boolean isFirst; +@property(nonatomic) Boolean isLast; + (CGSize)ViewSizeForMessage:(LinphoneChatMessage *)chat withWidth:(int)width; + (CGSize)ViewHeightForMessageText:(LinphoneChatMessage *)chat withWidth:(int)width textForImdn:(NSString *)imdnText; diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m index 75937c0a4..6e24788f1 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.m +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -44,8 +44,8 @@ UITapGestureRecognizer *limeRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onLime)]; limeRecognizer.numberOfTapsRequired = 1; - [_LIMEKO addGestureRecognizer:limeRecognizer]; - _LIMEKO.userInteractionEnabled = YES; + //[_LIMEKO addGestureRecognizer:limeRecognizer]; + //_LIMEKO.userInteractionEnabled = YES; UITapGestureRecognizer *resendRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onResend)]; resendRecognizer.numberOfTapsRequired = 1; @@ -54,8 +54,8 @@ UITapGestureRecognizer *resendRecognizer2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onResend)]; resendRecognizer2.numberOfTapsRequired = 1; - [_imdmLabel addGestureRecognizer:resendRecognizer2]; - _imdmLabel.userInteractionEnabled = YES; + //[_imdmLabel addGestureRecognizer:resendRecognizer2]; + //_imdmLabel.userInteractionEnabled = YES; return self; } @@ -126,7 +126,7 @@ return; } - _statusInProgressSpinner.accessibilityLabel = @"Delivery in progress"; + //_statusInProgressSpinner.accessibilityLabel = @"Delivery in progress"; if (_messageText && ![LinphoneManager getMessageAppDataForKey:@"localvideo" inMessage:_message]) { LOGD(_messageText.text); @@ -145,27 +145,83 @@ LinphoneChatMessageState state = linphone_chat_message_get_state(_message); BOOL outgoing = linphone_chat_message_is_outgoing(_message); + + + _contactDateLabel.hidden = !_isFirst; + if (outgoing) { + _contactDateLabel.text = [LinphoneUtils timeToString:linphone_chat_message_get_time(_message) + withFormat:LinphoneDateChatBubble]; + _contactDateLabel.textAlignment = NSTextAlignmentRight; + _avatarImage.hidden = TRUE; + + } else { + [_avatarImage setImage:[FastAddressBook imageForAddress:linphone_chat_message_get_from_address(_message)] + bordered:NO + withRoundedRadius:YES]; + _contactDateLabel.text = [self.class ContactDateForChat:_message]; + _contactDateLabel.textAlignment = NSTextAlignmentLeft; + _avatarImage.hidden = !_isFirst; + } + - if (outgoing) { - _avatarImage.image = [LinphoneUtils selfAvatar]; - } else { - [_avatarImage setImage:[FastAddressBook imageForAddress:linphone_chat_message_get_from_address(_message)] - bordered:NO - withRoundedRadius:YES]; - } - _contactDateLabel.text = [self.class ContactDateForChat:_message]; - - _backgroundColorImage.image = _bottomBarColor.image = + _backgroundColorImage.image = [UIImage imageNamed:(outgoing ? @"color_A.png" : @"color_D.png")]; - _contactDateLabel.textColor = [UIColor colorWithPatternImage:_backgroundColorImage.image]; + + // set maskedCorners + if (@available(iOS 11.0, *)) { + _backgroundColorImage.layer.cornerRadius = 10; + if (outgoing) { + _backgroundColorImage.layer.maskedCorners = kCALayerMinXMaxYCorner | kCALayerMinXMinYCorner; + if (_isFirst) + _backgroundColorImage.layer.maskedCorners = _backgroundColorImage.layer.maskedCorners | kCALayerMaxXMinYCorner; + if (_isLast) + _backgroundColorImage.layer.maskedCorners = _backgroundColorImage.layer.maskedCorners | kCALayerMaxXMaxYCorner; + } else { + _backgroundColorImage.layer.maskedCorners = kCALayerMaxXMinYCorner | kCALayerMaxXMaxYCorner; + if (_isFirst) + _backgroundColorImage.layer.maskedCorners = _backgroundColorImage.layer.maskedCorners | kCALayerMinXMinYCorner; + if (_isLast) + _backgroundColorImage.layer.maskedCorners = _backgroundColorImage.layer.maskedCorners | kCALayerMinXMaxYCorner; + } + _backgroundColorImage.layer.masksToBounds = YES; + } else { + // TODO it doesn't work for ios < 11.0 + UIRectCorner corner; + if (outgoing) { + corner = UIRectCornerTopLeft | UIRectCornerBottomLeft; + if (_isFirst) + corner = corner | UIRectCornerTopRight; + if (_isLast) + corner = corner | UIRectCornerBottomRight; + } else { + corner = UIRectCornerTopRight | UIRectCornerBottomRight; + if (_isFirst) + corner = corner | UIRectCornerTopLeft; + if (_isLast) + corner = corner | UIRectCornerBottomLeft; + } + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_backgroundColorImage.frame byRoundingCorners:corner cornerRadii:CGSizeMake(10,10)]; + CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; + maskLayer.frame = _backgroundColorImage.frame; + maskLayer.path = maskPath.CGPath; + _backgroundColorImage.layer.mask = maskLayer; + } + + // need space for dateLabel + CGRect frame = _innerView.frame; + frame.origin.y = _isFirst ? 20 : 0; + _innerView.frame = frame; + + + //_contactDateLabel.textColor = [UIColor colorWithPatternImage:_backgroundColorImage.image]; - if (outgoing && state == LinphoneChatMessageStateInProgress) { + /*if (outgoing && state == LinphoneChatMessageStateInProgress) { [_statusInProgressSpinner startAnimating]; } else if (!outgoing && state == LinphoneChatMessageStateFileTransferError) { [_statusInProgressSpinner stopAnimating]; } else { [_statusInProgressSpinner stopAnimating]; - } + }*/ [_messageText setAccessibilityLabel:outgoing ? @"Outgoing message" : @"Incoming message"]; if (outgoing && @@ -175,12 +231,12 @@ } else [self displayImdmStatus:LinphoneChatMessageStateInProgress]; - if (!outgoing && !linphone_chat_message_is_secured(_message) && + /*if (!outgoing && !linphone_chat_message_is_secured(_message) && linphone_core_lime_enabled(LC) == LinphoneLimeMandatory) { _LIMEKO.hidden = FALSE; } else { _LIMEKO.hidden = TRUE; - } + }*/ } - (void)setEditing:(BOOL)editing { @@ -250,8 +306,8 @@ } - (void)onLime { - if (!_LIMEKO.hidden) - [self displayLIMEWarning]; + /*if (!_LIMEKO.hidden) + [self displayLIMEWarning];*/ } - (void)onResend { @@ -349,25 +405,25 @@ static void message_status(LinphoneChatMessage *msg, LinphoneChatMessageState st - (void)displayImdmStatus:(LinphoneChatMessageState)state { if (state == LinphoneChatMessageStateDeliveredToUser) { [_imdmIcon setImage:[UIImage imageNamed:@"chat_delivered"]]; - [_imdmLabel setText:NSLocalizedString(@"Delivered", nil)]; - [_imdmLabel setTextColor:[UIColor grayColor]]; + //[_imdmLabel setText:NSLocalizedString(@"Delivered", nil)]; + //[_imdmLabel setTextColor:[UIColor grayColor]]; [_imdmIcon setHidden:FALSE]; - [_imdmLabel setHidden:FALSE]; + //[_imdmLabel setHidden:FALSE]; } else if (state == LinphoneChatMessageStateDisplayed) { [_imdmIcon setImage:[UIImage imageNamed:@"chat_read"]]; - [_imdmLabel setText:NSLocalizedString(@"Read", nil)]; - [_imdmLabel setTextColor:([UIColor colorWithRed:(24 / 255.0) green:(167 / 255.0) blue:(175 / 255.0) alpha:1.0])]; + //[_imdmLabel setText:NSLocalizedString(@"Read", nil)]; + //[_imdmLabel setTextColor:([UIColor colorWithRed:(24 / 255.0) green:(167 / 255.0) blue:(175 / 255.0) alpha:1.0])]; [_imdmIcon setHidden:FALSE]; - [_imdmLabel setHidden:FALSE]; + //[_imdmLabel setHidden:FALSE]; } else if (state == LinphoneChatMessageStateNotDelivered || state == LinphoneChatMessageStateFileTransferError) { [_imdmIcon setImage:[UIImage imageNamed:@"chat_error"]]; - [_imdmLabel setText:NSLocalizedString(@"Resend", nil)]; - [_imdmLabel setTextColor:[UIColor redColor]]; + //[_imdmLabel setText:NSLocalizedString(@"Resend", nil)]; + //[_imdmLabel setTextColor:[UIColor redColor]]; [_imdmIcon setHidden:FALSE]; - [_imdmLabel setHidden:FALSE]; + //[_imdmLabel setHidden:FALSE]; } else { [_imdmIcon setHidden:TRUE]; - [_imdmLabel setHidden:TRUE]; + //[_imdmLabel setHidden:TRUE]; } } @@ -389,7 +445,7 @@ static void message_status(LinphoneChatMessage *msg, LinphoneChatMessageState st static const CGFloat CELL_MIN_HEIGHT = 60.0f; static const CGFloat CELL_MIN_WIDTH = 190.0f; static const CGFloat CELL_MESSAGE_X_MARGIN = 78 + 10.0f; -static const CGFloat CELL_MESSAGE_Y_MARGIN = 52; // 44; +static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; // 44; + (CGSize)ViewHeightForMessage:(LinphoneChatMessage *)chat withWidth:(int)width { return [self ViewHeightForMessageText:chat withWidth:width textForImdn:nil]; @@ -480,6 +536,7 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 52; // 44; CGSize messageSize = [self ViewHeightForMessage:chat withWidth:width]; CGSize dateSize = [self computeBoundingBox:[self ContactDateForChat:chat] size:dateViewSize font:dateFont]; messageSize.width = MAX(MAX(messageSize.width, MIN(dateSize.width + CELL_MESSAGE_X_MARGIN, width)), CELL_MIN_WIDTH); + messageSize.width = MAX(MAX(messageSize.width, MIN(CELL_MESSAGE_X_MARGIN, width)), CELL_MIN_WIDTH); return messageSize; } diff --git a/Resources/images/chat_delivered.png b/Resources/images/chat_delivered.png index 9d741055de31cb1e09060595830f1406db1d798b..356b8ecd187f6c8703e62a9f0ca2809c88637cc9 100644 GIT binary patch delta 899 zcmV-}1AP3S2Au~XiBL{Q4GJ0x0000DNk~Le0000P0000P2nGNE0L1BqW04^#e*gz` zNliru;tClL0yO9+!IuC411d>GK~zY`otD2#BWW1Nzwi6bOlHO(kQKC4QEaslj_qQs zL=Y{q>^8^k9}wKDx+$fUrfu8rgCKZAL>xVYF{T(}pFGcdBc=3uLNb}mO8}phQWl;mjIo+w7)i%* z@~!&NUv6)2f2WlCG6(woU-?0nnRlT0S_QYrPN9Xy~YP1BY(e>XS(XhNiv zrfHi04C+Bd=v?3TUq~rE4gjWU{xT>)(=_P1-Ys;NZQJhwfMv7U(Q38&Z$DMkE{ltc zNF)*ffU~nR6bglQZ-Swrp%>hB-5&=5OifK8nM^_m0U-nuiA0A-L?k@VOLHQ6^<02u zSy)|N1!Jt|-5>~FYmc2Af0EhRSu8Iv0{~7>PjPj16*^y8S;5H2NJv*G6wqiix)s1A zC!$!ZQV4;ywKc@!am3?sY;0^`dV0EfzOb->xw*NJp_`i4nG zw{nj4^>s{4Okiwmtm)xbX*3!*I50PibXf@TG3w&o-5vJ!_E4=>0RR^l7m?-K z#fuM8%1Uzz$6~R2BC?nKZX(-}gs_ z5I-}NS8#A~K0cJimzge*(Wr zL_t(Y$Gw)%OH@%5$3N%32g8^Sh{l)}n7PP>E5SizKoBb0gb2gT1OJBzYSFTw=wAq0 zgfb3>RHQ9x<3sW{IW9X0wc&-d~@_ndovT;T}x_xE40 zEUQOEZUL8o^SjhfRsCX&d2Nh&e;JF#ew5OXV#cANp=QtX9s#|8;b`Q6Vau}a$6~R? zV;v-u$zBn84xHm;_^qmaj^n&KXe_v0DwVn;A}>xM4p1o~!^vdwcF_n@sZ?B5lcmyj z0;sCS9mg5kcQ7z8aM2j^K}4$fH~5vy<*xSi^{p5H%d#GyMZBV-g38Lue}krwZQG9l zWN>h>5%^d}wG>5lbv51H-G~UIqoZtYZthVYaM=Jorxmv>i;j*CY}-aeP}Kq&=rO8# z{j{lRYilDE3IUK#r`g)t@*lZjMC6L!8V-jEhr>sTH#9WR)YJsP>gp<)Or}^lHQ7L2 z!Rv%VbaizhB1}z9vAB4se>n*TgS5A|1CY&TnV6XHr)LlK#$Wf@Y?hs!o&6Nl*Vh-e zz6g;>gg_twfa|)&Qd7{$-~A58#>U9ya)=0>ot@Oy*6#ByEiKg5{S7*k$*{bDd2n!1f%+1a1KmJ}x&d$!VwzhWM z?LAc;F^NQC33yiO)XdBbOG`@yk= zz#6a!%mZ&g2WSD8z?B0Z{Kf=xe}aObBTx!|790!a(gHleeahrvZwYcqfOWysn1F$I z1^KuNCIt-qMX+q!gV7c_j> zYW2GPc~nc`l>P?U18gg{S-6#>ev9BSc6yRP@c;?h2tE?$KB`5HMw&D8PNF_dUdaQ3 z=9Ks32Eo~sr|Sg4j#VngIVW}r+WLf*}(0Vw)r3{`YDCZHKvfdw=pIOtDDn&j>N z@L2y=oCH_~*Z}r{RbUBN0A7LikN>V~2R@_nd;tI-)#g*foJ0Ts002ovPDHLkV1k0m B-E05= delta 465 zcmV;?0WSWW1nvVNiBL{Q4GJ0x0000DNk~Le0000O0000O2nGNE0N{5$_>mzge*un3 zL_t(Y$Gw)%N&-<7#(!h8v{nZF6Qt}>`m-wuBBB=vT11afi+TZ*=@Ht5OMM01=w_54 zB`ktgX)2O>TFfm)rnz?#{NTWWd*=J@z30q1*CGG{%Rma)1LDAO1)@L~f`#sb+a42NL^Y6zT7(R*}uia^4sEk~mw znT$v}?fH80zIZbP1n#sSIL@dSIgW|I?RYP`1X_W&X^j<|KG99!YO4f>0W000000NkvXX Hu0mjfV}Qw) diff --git a/Resources/images/chat_read.png b/Resources/images/chat_read.png index b7ae0d29a6f45214229c55d6e709253c232de594..8f8f467c1e84ecd7088333160b9221f07621884f 100644 GIT binary patch delta 937 zcmV;a16KUw2Ezv-iBL{Q4GJ0x0000DNk~Le0000P0000P2nGNE0L1BqW04^#e*gz` zNliru;tClL0UvL+YZL$g15imsK~zY`t(H%0R8OOZbwMN!F{wMoMx{s@D_|oat)ap& z1EtK+N;~sr-gO}@^E!R)wEFGde>vx#@1A?UbMF(DLG!?Xf97uPRjqV~Ko|%C8-Y6r z39X5X5DnD$PhD+m%X;OJC9ScT8A_!OYfbNi#4F*p7CuVV*B&Dh$*)Kf>Oaxw7%D27 zt*jouD8uYbSnU&@B*Fe~T2XupJjU9QLY(&G!LH#Fl2HGNMy=G@|41;8e@X2)yHed< z;{a;D2achl{}TbgCQ<5ZVli`pB$!I~tg#1F8U|H{v6O3DD3v}8Ak6~<{+Uen&oWiy zLeJApe6XbjfTO?u#urz{7l$MQ>-|runau3owGg~m|0um%pT(yXKBf3@OAC*fB^5RN zot}#*jnT1Mf;zvC&vx%Xe~Ehnv@VU+qB@kI!;2kz8-kqPzmK8)`*`i~V9CNycWxtC zRq3+zT^VP#P*`G1I;=okN99wB&tC}hWT2YOfol5pwDNNBq38}ZZRYhS+^2hCD$U0i zfA=c9L4n@jQj2L&ZJG<;6-v>!rBO=Me-GaiAYrvnAWr*oM5}oj z6K@>&tEl-!jXu~ltPHai7+F(#A+C^}#ZPYu$$Ul0%jd(0yalC$>ZKwP-cHun?k=ui z`Cp<3XgPk|Hmzge**(a zL_t(Y$Gw(MXjE4e#((F&hh{WMHD;WdL@~w*RKyldZL7ibkEK;NItIfe5V~_ESlXrw zcPb{h5DQ(1RS~hI(RLga+d>r)MHGS<|HWd|NHvpaiV0S0{>*#Fg-MzjXEHICe!F+R z`@VDU$2sR-VHD&ahy-lQ3aRKSe}S2RcaZ8r`4huY#|%WfDl0qFX-KM|_t4Rx`{rLYe-#}DAa0rZ z*AXwA7eAb)P1G3y8%t`)nsn1Kf z&pWF4^1=XB#j^l7^Pr1;f30n)>Z#ZO{*kd@ZVrtfF5%!uWz+j0oSpw~)Mv69D+C>C#KqB(8^8VuF6E zPM_y#JdQ2G?y^NJ%Fi3-*Uv5C1OIdY_O;&OxBDIGeH3Xos@zGAe_!qHVcoeV;;Lj> z7W