From 6f49182c4cd931131105d5beffdb65baaf7398a5 Mon Sep 17 00:00:00 2001 From: Gautier Pelloux-Prayer Date: Thu, 10 Sep 2015 14:35:12 +0200 Subject: [PATCH] chat continuation --- Classes/ChatConversationTableView.h | 2 + Classes/ChatConversationTableView.m | 70 ++- Classes/ChatConversationView.m | 2 +- .../Base.lproj/UIChatBubbleCell.xib | 160 ------ Classes/LinphoneUI/UIChatBubbleCell.h | 61 --- Classes/LinphoneUI/UIChatBubbleCell.m | 517 ------------------ Classes/LinphoneUI/UIChatBubblePhotoCell.h | 46 +- Classes/LinphoneUI/UIChatBubblePhotoCell.m | 44 +- Classes/LinphoneUI/UIChatBubbleTextCell.h | 47 ++ Classes/LinphoneUI/UIChatBubbleTextCell.m | 235 ++++++++ Classes/LinphoneUI/UIChatBubbleTextCell.xib | 89 +++ .../ar.lproj/UIChatBubbleCell.strings | Bin 2546 -> 0 bytes linphone.xcodeproj/project.pbxproj | 30 +- 13 files changed, 472 insertions(+), 831 deletions(-) delete mode 100644 Classes/LinphoneUI/Base.lproj/UIChatBubbleCell.xib delete mode 100644 Classes/LinphoneUI/UIChatBubbleCell.h delete mode 100644 Classes/LinphoneUI/UIChatBubbleCell.m create mode 100644 Classes/LinphoneUI/UIChatBubbleTextCell.h create mode 100644 Classes/LinphoneUI/UIChatBubbleTextCell.m create mode 100644 Classes/LinphoneUI/UIChatBubbleTextCell.xib delete mode 100644 Classes/LinphoneUI/ar.lproj/UIChatBubbleCell.strings diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index a0f5b3f19..54c7a4731 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -41,4 +41,6 @@ - (void)updateChatEntry:(LinphoneChatMessage *)chat; - (void)setChatRoom:(LinphoneChatRoom *)room; ++ (CGSize)viewSize:(LinphoneChatMessage *)message width:(int)width; + @end diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index 46d059313..2844ec625 100644 --- a/Classes/ChatConversationTableView.m +++ b/Classes/ChatConversationTableView.m @@ -19,9 +19,19 @@ #import "LinphoneManager.h" #import "ChatConversationTableView.h" -#import "UIChatBubbleCell.h" +#import "UIChatBubbleTextCell.h" +#import "UIChatBubblePhotoCell.h" #import "PhoneMainView.h" +static const CGFloat CELL_MIN_HEIGHT = 50.0f; +static const CGFloat CELL_MIN_WIDTH = 150.0f; +static const CGFloat CELL_MESSAGE_X_MARGIN = 26.0f + 10.0f; +static const CGFloat CELL_MESSAGE_Y_MARGIN = 36.0f; +static const CGFloat CELL_FONT_SIZE = 17.0f; +static const CGFloat CELL_IMAGE_HEIGHT = 100.0f; +static const CGFloat CELL_IMAGE_WIDTH = 100.0f; +static UIFont *CELL_FONT = nil; + @implementation ChatConversationTableView @synthesize chatRoomDelegate; @@ -167,13 +177,18 @@ } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *kCellId = @"UIChatRoomCell"; - UIChatBubbleCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellId]; - if (cell == nil) { - cell = [[UIChatBubbleCell alloc] initWithIdentifier:kCellId]; - } - + NSString *kCellId = nil; LinphoneChatMessage *chat = ms_list_nth_data(self->messageList, (int)[indexPath row]); + if (linphone_chat_message_get_file_transfer_information(chat) || + linphone_chat_message_get_external_body_url(chat)) { + kCellId = NSStringFromClass(UIChatBubblePhotoCell.class); + } else { + kCellId = NSStringFromClass(UIChatBubbleTextCell.class); + } + UIChatBubbleTextCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellId]; + if (cell == nil) { + cell = [[NSClassFromString(kCellId) alloc] initWithIdentifier:kCellId]; + } [cell setChatMessage:chat]; [cell setChatRoomDelegate:chatRoomDelegate]; return cell; @@ -209,7 +224,46 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { LinphoneChatMessage *message = ms_list_nth_data(self->messageList, (int)[indexPath row]); - return [UIChatBubbleCell height:message width:[self.view frame].size.width]; + return [self.class viewSize:message width:[self.view frame].size.width].height; +} + +#pragma mark - Cell dimension + ++ (CGSize)viewSize:(LinphoneChatMessage *)message width:(int)width { + CGSize messageSize; + const char *url = linphone_chat_message_get_external_body_url(message); + if (url == nil && linphone_chat_message_get_file_transfer_information(message) == NULL) { + NSString *text = [UIChatBubbleTextCell TextMessageForChat:message]; + if (CELL_FONT == nil) { + CELL_FONT = [UIFont systemFontOfSize:CELL_FONT_SIZE]; + } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 + if (UIDevice.currentDevice.systemVersion.doubleValue >= 7) { + messageSize = + [text boundingRectWithSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, CGFLOAT_MAX) + options:(NSStringDrawingUsesLineFragmentOrigin | + NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading) + attributes:@{ + NSFontAttributeName : CELL_FONT + } context:nil] + .size; + } else +#endif + { + messageSize = [text sizeWithFont:CELL_FONT + constrainedToSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, 10000.0f) + lineBreakMode:NSLineBreakByTruncatingTail]; + } + } else { + messageSize = CGSizeMake(CELL_IMAGE_WIDTH, CELL_IMAGE_HEIGHT); + } + messageSize.height += CELL_MESSAGE_Y_MARGIN; + if (messageSize.height < CELL_MIN_HEIGHT) + messageSize.height = CELL_MIN_HEIGHT; + messageSize.width += CELL_MESSAGE_X_MARGIN; + if (messageSize.width < CELL_MIN_WIDTH) + messageSize.width = CELL_MIN_WIDTH; + return messageSize; } @end diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index a27ccf241..b20ac1f1e 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -21,7 +21,7 @@ #import "PhoneMainView.h" #import "Utils.h" #import "FileTransferDelegate.h" -#import "UIChatBubbleCell.h" +#import "UIChatBubbleTextCell.h" @implementation ChatConversationView diff --git a/Classes/LinphoneUI/Base.lproj/UIChatBubbleCell.xib b/Classes/LinphoneUI/Base.lproj/UIChatBubbleCell.xib deleted file mode 100644 index ddda4a94f..000000000 --- a/Classes/LinphoneUI/Base.lproj/UIChatBubbleCell.xib +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Classes/LinphoneUI/UIChatBubbleCell.h b/Classes/LinphoneUI/UIChatBubbleCell.h deleted file mode 100644 index 92f6fc128..000000000 --- a/Classes/LinphoneUI/UIChatBubbleCell.h +++ /dev/null @@ -1,61 +0,0 @@ -/* UIChatRoomCell.h - * - * Copyright (C) 2012 Belledonne Comunications, Grenoble, France - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * 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 Library General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#import - -#import "ChatConversationTableView.h" -#import "UILoadingImageView.h" -#import "UITransparentTVCell.h" -#import "UITextViewNoDefine.h" -#include "linphone/linphonecore.h" -#import "FileTransferDelegate.h" - -@interface UIChatBubbleCell : UITransparentTVCell { - LinphoneChatMessage *chat; -} - -@property(nonatomic, strong) IBOutlet UIView *innerView; -@property(nonatomic, strong) IBOutlet UIView *bubbleView; -@property(nonatomic, strong) IBOutlet UIImageView *backgroundImage; -@property(nonatomic, strong) IBOutlet UITextViewNoDefine *messageText; -@property(nonatomic, strong) IBOutlet UILoadingImageView *messageImageView; -@property(nonatomic, strong) IBOutlet UIButton *deleteButton; -@property(nonatomic, strong) IBOutlet UILabel *dateLabel; -@property(nonatomic, strong) IBOutlet UIImageView *statusImage; -@property(nonatomic, strong) IBOutlet UIButton *downloadButton; -@property(nonatomic, strong) IBOutlet UITapGestureRecognizer *imageTapGestureRecognizer; -@property(nonatomic, strong) IBOutlet UITapGestureRecognizer *resendTapGestureRecognizer; -@property(weak, nonatomic) IBOutlet UIProgressView *fileTransferProgress; -@property(weak, nonatomic) IBOutlet UIButton *cancelButton; - -- (id)initWithIdentifier:(NSString *)identifier; -+ (CGFloat)height:(LinphoneChatMessage *)chatMessage width:(int)width; - -@property(nonatomic, strong) id chatRoomDelegate; - -- (IBAction)onDeleteClick:(id)event; -- (IBAction)onDownloadClick:(id)event; -- (IBAction)onImageClick:(id)event; -- (IBAction)onCancelDownloadClick:(id)sender; - -- (void)setChatMessage:(LinphoneChatMessage *)message; - -- (void)connectToFileDelegate:(FileTransferDelegate *)ftd; - -@end diff --git a/Classes/LinphoneUI/UIChatBubbleCell.m b/Classes/LinphoneUI/UIChatBubbleCell.m deleted file mode 100644 index e166b7f25..000000000 --- a/Classes/LinphoneUI/UIChatBubbleCell.m +++ /dev/null @@ -1,517 +0,0 @@ -/* UIChatRoomCell.m - * - * Copyright (C) 2012 Belledonne Comunications, Grenoble, France - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * 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 Library General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#import "UIChatBubbleCell.h" -#import "Utils.h" -#import "LinphoneManager.h" -#import "PhoneMainView.h" - -#import -#import -#include "linphone/linphonecore.h" - -@implementation UIChatBubbleCell { - FileTransferDelegate *ftd; -} - -@synthesize innerView; -@synthesize bubbleView; -@synthesize backgroundImage; -@synthesize messageImageView; -@synthesize messageText; -@synthesize deleteButton; -@synthesize dateLabel; -@synthesize statusImage; -@synthesize downloadButton; -@synthesize chatRoomDelegate; -@synthesize imageTapGestureRecognizer; -@synthesize resendTapGestureRecognizer; - -static const CGFloat CELL_MIN_HEIGHT = 50.0f; -static const CGFloat CELL_MIN_WIDTH = 150.0f; -// static const CGFloat CELL_MAX_WIDTH = 320.0f; -static const CGFloat CELL_MESSAGE_X_MARGIN = 26.0f + 10.0f; -static const CGFloat CELL_MESSAGE_Y_MARGIN = 36.0f; -static const CGFloat CELL_FONT_SIZE = 17.0f; -static const CGFloat CELL_IMAGE_HEIGHT = 100.0f; -static const CGFloat CELL_IMAGE_WIDTH = 100.0f; -static UIFont *CELL_FONT = nil; - -#pragma mark - Lifecycle Functions - -- (id)initWithIdentifier:(NSString *)identifier { - if ((self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]) != nil) { - [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; - imageTapGestureRecognizer = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onImageClick:)]; - [messageImageView addGestureRecognizer:imageTapGestureRecognizer]; - - resendTapGestureRecognizer = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onResendClick:)]; - [innerView addGestureRecognizer:resendTapGestureRecognizer]; - - [self addSubview:innerView]; - [deleteButton setAlpha:0.0f]; - - // shift message box, otherwise it will collide with the bubble - CGRect messageCoords = [messageText frame]; - messageCoords.origin.x += 2; - messageCoords.origin.y += 2; - messageCoords.size.width -= 5; - [messageText setFrame:messageCoords]; - messageText.allowSelectAll = TRUE; - } - - return self; -} - -- (void)dealloc { - [self disconnectFromFileDelegate]; - if (self->chat) { - linphone_chat_message_unref(self->chat); - linphone_chat_message_set_user_data(self->chat, NULL); - linphone_chat_message_cbs_set_msg_state_changed(linphone_chat_message_get_callbacks(self->chat), NULL); - self->chat = NULL; - } -} - -#pragma mark - - -- (void)setChatMessage:(LinphoneChatMessage *)message { - if (message != self->chat) { - if (self->chat) { - linphone_chat_message_unref(self->chat); - linphone_chat_message_set_user_data(self->chat, NULL); - linphone_chat_message_cbs_set_msg_state_changed(linphone_chat_message_get_callbacks(self->chat), NULL); - } - self->chat = message; - messageImageView.image = nil; - [self disconnectFromFileDelegate]; - if (self->chat) { - linphone_chat_message_ref(self->chat); - linphone_chat_message_set_user_data(self->chat, (void *)CFBridgingRetain(self)); - linphone_chat_message_cbs_set_msg_state_changed(linphone_chat_message_get_callbacks(self->chat), - message_status); - - const LinphoneContent *c = linphone_chat_message_get_file_transfer_information(message); - if (c) { - const char *name = linphone_content_get_name(c); - for (FileTransferDelegate *aftd in [[LinphoneManager instance] fileTransferDelegates]) { - if (linphone_chat_message_get_file_transfer_information(aftd.message) && - strcmp(name, linphone_content_get_name( - linphone_chat_message_get_file_transfer_information(aftd.message))) == 0) { - LOGI(@"Chat message [%p] with file transfer delegate [%p], connecting to it!", message, aftd); - [self connectToFileDelegate:aftd]; - break; - } - } - } - } - [self update]; - } -} - -+ (NSString *)decodeTextMessage:(const char *)text { - NSString *decoded = [NSString stringWithUTF8String:text]; - if (decoded == nil) { - // couldn't decode the string as UTF8, do a lossy conversion - decoded = [NSString stringWithCString:text encoding:NSASCIIStringEncoding]; - if (decoded == nil) { - decoded = @"(invalid string)"; - } - } - return decoded; -} - -- (void)update { - if (chat == nil) { - LOGW(@"Cannot update chat room cell: null chat"); - return; - } - const char *url = linphone_chat_message_get_external_body_url(chat); - const char *text = linphone_chat_message_get_text(chat); - BOOL is_external = - (url && (strstr(url, "http") == url)) || linphone_chat_message_get_file_transfer_information(chat); - NSString *localImage = [LinphoneManager getMessageAppDataForKey:@"localimage" inMessage:chat]; - - // this is an image (either to download or already downloaded) - if (is_external || localImage) { - if (localImage) { - if (messageImageView.image == nil) { - NSURL *imageUrl = [NSURL URLWithString:localImage]; - messageText.hidden = YES; - [messageImageView startLoading]; - __block LinphoneChatMessage *achat = chat; - [LinphoneManager.instance.photoLibrary assetForURL:imageUrl - resultBlock:^(ALAsset *asset) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), - ^(void) { - if (achat == self->chat) { // Avoid glitch and scrolling - UIImage *image = [[UIImage alloc] initWithCGImage:[asset thumbnail]]; - dispatch_async(dispatch_get_main_queue(), ^{ - [messageImageView setImage:image]; - [messageImageView setFullImageUrl:asset]; - [messageImageView stopLoading]; - messageImageView.hidden = NO; - }); - } - }); - } - failureBlock:^(NSError *error) { - LOGE(@"Can't read image"); - }]; - } - if (ftd.message != nil) { - _cancelButton.hidden = NO; - _fileTransferProgress.hidden = NO; - downloadButton.hidden = YES; - } else { - _cancelButton.hidden = _fileTransferProgress.hidden = downloadButton.hidden = YES; - } - } else { - messageText.hidden = YES; - messageImageView.hidden = _cancelButton.hidden = _fileTransferProgress.hidden = (ftd.message == nil); - downloadButton.hidden = !_cancelButton.hidden; - } - // simple text message - } else { - [messageText setHidden:FALSE]; - if (text) { - NSString *nstext = [UIChatBubbleCell decodeTextMessage:text]; - - /* We need to use an attributed string here so that data detector don't mess - * with the text style. See http://stackoverflow.com/a/20669356 */ - - NSAttributedString *attr_text = - [[NSAttributedString alloc] initWithString:nstext - attributes:@{ - NSFontAttributeName : [UIFont systemFontOfSize:17.0], - NSForegroundColorAttributeName : [UIColor darkGrayColor] - }]; - messageText.attributedText = attr_text; - - } else { - messageText.text = @""; - } - messageImageView.hidden = YES; - _cancelButton.hidden = _fileTransferProgress.hidden = downloadButton.hidden = YES; - } - - // Date - dateLabel.text = - [LinphoneUtils timeToString:linphone_chat_message_get_time(chat) withStyle:NSDateFormatterMediumStyle]; - - LinphoneChatMessageState state = linphone_chat_message_get_state(chat); - BOOL outgoing = linphone_chat_message_is_outgoing(chat); - - if (!outgoing) { - [statusImage setAccessibilityValue:@"incoming"]; - statusImage.hidden = TRUE; // not useful for incoming chats.. - } else if (state == LinphoneChatMessageStateInProgress) { - [statusImage setImage:[UIImage imageNamed:@"chat_message_inprogress.png"]]; - [statusImage setAccessibilityValue:@"in progress"]; - statusImage.hidden = FALSE; - } else if (state == LinphoneChatMessageStateDelivered || state == LinphoneChatMessageStateFileTransferDone) { - [statusImage setImage:[UIImage imageNamed:@"chat_message_delivered.png"]]; - [statusImage setAccessibilityValue:@"delivered"]; - statusImage.hidden = FALSE; - } else { - [statusImage setImage:[UIImage imageNamed:@"chat_message_not_delivered.png"]]; - [statusImage setAccessibilityValue:@"not delivered"]; - statusImage.hidden = FALSE; - - NSAttributedString *resend_text = - [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Resend", @"Resend") - attributes:@{NSForegroundColorAttributeName : [UIColor redColor]}]; - [dateLabel setAttributedText:resend_text]; - } - - if (outgoing) { - [messageText setAccessibilityLabel:@"Outgoing message"]; - } else { - [messageText setAccessibilityLabel:@"Incoming message"]; - } -} - -- (void)setEditing:(BOOL)editing { - [self setEditing:editing animated:FALSE]; -} - -- (void)setEditing:(BOOL)editing animated:(BOOL)animated { - if (animated) { - [UIView beginAnimations:nil context:nil]; - [UIView setAnimationDuration:0.3]; - } - if (editing) { - [deleteButton setAlpha:1.0f]; - } else { - [deleteButton setAlpha:0.0f]; - } - if (animated) { - [UIView commitAnimations]; - } -} - -+ (CGSize)viewSize:(LinphoneChatMessage *)chat width:(int)width { - CGSize messageSize; - const char *url = linphone_chat_message_get_external_body_url(chat); - const char *text = linphone_chat_message_get_text(chat); - NSString *messageText = text ? [UIChatBubbleCell decodeTextMessage:text] : @""; - if (url == nil && linphone_chat_message_get_file_transfer_information(chat) == NULL) { - if (CELL_FONT == nil) { - CELL_FONT = [UIFont systemFontOfSize:CELL_FONT_SIZE]; - } - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 - - if ([[[UIDevice currentDevice] systemVersion] doubleValue] >= 7) { - messageSize = [messageText boundingRectWithSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, CGFLOAT_MAX) - options:(NSStringDrawingUsesLineFragmentOrigin | - NSStringDrawingTruncatesLastVisibleLine | - NSStringDrawingUsesFontLeading) - attributes:@{ - NSFontAttributeName : CELL_FONT - } context:nil] - .size; - } else -#endif - { - messageSize = [messageText sizeWithFont:CELL_FONT - constrainedToSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, 10000.0f) - lineBreakMode:NSLineBreakByTruncatingTail]; - } - } else { - messageSize = CGSizeMake(CELL_IMAGE_WIDTH, CELL_IMAGE_HEIGHT); - } - messageSize.height += CELL_MESSAGE_Y_MARGIN; - if (messageSize.height < CELL_MIN_HEIGHT) - messageSize.height = CELL_MIN_HEIGHT; - messageSize.width += CELL_MESSAGE_X_MARGIN; - if (messageSize.width < CELL_MIN_WIDTH) - messageSize.width = CELL_MIN_WIDTH; - return messageSize; -} - -+ (CGFloat)height:(LinphoneChatMessage *)chatMessage width:(int)width { - return [UIChatBubbleCell viewSize:chatMessage width:width].height; -} - -#pragma mark - View Functions - -- (void)layoutSubviews { - [super layoutSubviews]; - if (chat != nil) { - // Resize inner - CGRect innerFrame; - BOOL is_outgoing = linphone_chat_message_is_outgoing(chat); - innerFrame.size = [UIChatBubbleCell viewSize:chat width:[self frame].size.width]; - if (!is_outgoing) { // Inverted - innerFrame.origin.x = 0.0f; - innerFrame.origin.y = 0.0f; - } else { - innerFrame.origin.x = [self frame].size.width - innerFrame.size.width; - innerFrame.origin.y = 0.0f; - } - [innerView setFrame:innerFrame]; - - CGRect messageFrame = [bubbleView frame]; - messageFrame.origin.y = ([innerView frame].size.height - messageFrame.size.height) / 2; - if (!is_outgoing) { // Inverted - UIImage *image = [UIImage imageNamed:@"chat_bubble_incoming"]; - image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(26, 32, 34, 56)]; - [backgroundImage setImage:image]; - messageFrame.origin.y += 5; - } else { - UIImage *image = [UIImage imageNamed:@"chat_bubble_outgoing"]; - image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(14, 15, 25, 40)]; - [backgroundImage setImage:image]; - messageFrame.origin.y -= 5; - } - [bubbleView setFrame:messageFrame]; - } -} - -#pragma mark - Action Functions - -- (IBAction)onDeleteClick:(id)event { - if (chat != NULL) { - if (ftd.message != nil) { - [ftd cancel]; - } - UIView *view = [self superview]; - // Find TableViewCell - while (view != nil && ![view isKindOfClass:[UITableView class]]) - view = [view superview]; - if (view != nil) { - UITableView *tableView = (UITableView *)view; - NSIndexPath *indexPath = [tableView indexPathForCell:self]; - [[tableView dataSource] tableView:tableView - commitEditingStyle:UITableViewCellEditingStyleDelete - forRowAtIndexPath:indexPath]; - } - } -} - -- (IBAction)onDownloadClick:(id)event { - if (ftd.message == nil) { - ftd = [[FileTransferDelegate alloc] init]; - [self connectToFileDelegate:ftd]; - [ftd download:chat]; - _cancelButton.hidden = NO; - downloadButton.hidden = YES; - } -} - -- (IBAction)onCancelDownloadClick:(id)sender { - FileTransferDelegate *tmp = ftd; - [self disconnectFromFileDelegate]; - [tmp cancel]; - [self update]; -} - -- (IBAction)onImageClick:(id)event { - LinphoneChatMessageState state = linphone_chat_message_get_state(self->chat); - if (state == LinphoneChatMessageStateNotDelivered) { - [self onResendClick:nil]; - } else { - if (![messageImageView isLoading]) { - ImageView *view = VIEW(ImageView); - [PhoneMainView.instance changeCurrentView:view.compositeViewDescription push:TRUE]; - CGImageRef fullScreenRef = [[messageImageView.fullImageUrl defaultRepresentation] fullScreenImage]; - UIImage *fullScreen = [UIImage imageWithCGImage:fullScreenRef]; - [view setImage:fullScreen]; - } - } -} - -- (IBAction)onResendClick:(id)event { - if (chat == nil) - return; - - LinphoneChatMessageState state = linphone_chat_message_get_state(self->chat); - if (state == LinphoneChatMessageStateNotDelivered) { - if (linphone_chat_message_get_file_transfer_information(chat) != NULL) { - NSString *localImage = [LinphoneManager getMessageAppDataForKey:@"localimage" inMessage:chat]; - NSURL *imageUrl = [NSURL URLWithString:localImage]; - - [self onDeleteClick:nil]; - - [[LinphoneManager instance] - .photoLibrary assetForURL:imageUrl - resultBlock:^(ALAsset *asset) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), - ^(void) { - UIImage *image = [[UIImage alloc] initWithCGImage:[asset thumbnail]]; - [chatRoomDelegate startImageUpload:image url:imageUrl]; - }); - } - failureBlock:^(NSError *error) { - LOGE(@"Can't read image"); - }]; - } else { - const char *text = linphone_chat_message_get_text(self->chat); - NSString *message = text ? [NSString stringWithUTF8String:text] : nil; - - [self onDeleteClick:nil]; - - double delayInSeconds = 0.4; - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [chatRoomDelegate resendChat:message withExternalUrl:nil]; - }); - } - } -} -#pragma mark - State changed handling -static void message_status(LinphoneChatMessage *msg, LinphoneChatMessageState state) { - UIChatBubbleCell *thiz = (__bridge UIChatBubbleCell *)linphone_chat_message_get_user_data(msg); - LOGI(@"State for message [%p] changed to %s", msg, linphone_chat_message_state_to_string(state)); - if (linphone_chat_message_get_file_transfer_information(msg) != NULL) { - if (state == LinphoneChatMessageStateDelivered || state == LinphoneChatMessageStateNotDelivered) { - // we need to refresh the tableview because the filetransfer delegate unreffed - // the chat message before state was LinphoneChatMessageStateFileTransferDone - - // if we are coming back from another view between unreffing and change of state, - // the transient message will not be found and it will not appear in the list of - // message, so we must refresh the table when we change to this state to ensure that - // all transient messages apppear - // ChatRoomViewController *controller = DYNAMIC_CAST( - // [PhoneMainView.instance changeCurrentView:ChatRoomViewController.compositeViewDescription - // push:TRUE], - // ChatRoomViewController); - // [controller.tableController setChatRoom:linphone_chat_message_get_chat_room(msg)]; - // This is breaking interface too much, it must be fixed in file transfer cb.. meanwhile, disabling it. - - if (thiz->ftd) { - [thiz->ftd stopAndDestroy]; - thiz->ftd = nil; - } - } - } - [thiz update]; -} - -#pragma mark - LinphoneFileTransfer Notifications Handling - -- (void)connectToFileDelegate:(FileTransferDelegate *)aftd { - ftd = aftd; - _fileTransferProgress.progress = 0; - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onFileTransferSendUpdate:) - name:kLinphoneFileTransferSendUpdate - object:ftd]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onFileTransferRecvUpdate:) - name:kLinphoneFileTransferRecvUpdate - object:ftd]; -} - -- (void)disconnectFromFileDelegate { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - ftd = nil; -} - -- (void)onFileTransferSendUpdate:(NSNotification *)notif { - LinphoneChatMessageState state = [[[notif userInfo] objectForKey:@"state"] intValue]; - - if (state == LinphoneChatMessageStateInProgress) { - float progress = [[[notif userInfo] objectForKey:@"progress"] floatValue]; - // When uploading a file, the message file is first uploaded to the server, - // so we are in progress state. Then state goes to filetransfertdone. Then, - // the exact same message is sent to the other chat participant and we come - // back to in progress again. This second time is NOT an upload, so we must - // not update progress! - _fileTransferProgress.progress = MAX(_fileTransferProgress.progress, progress); - _fileTransferProgress.hidden = _cancelButton.hidden = (_fileTransferProgress.progress == 1.f); - } else { - [self update]; - } -} -- (void)onFileTransferRecvUpdate:(NSNotification *)notif { - LinphoneChatMessageState state = [[[notif userInfo] objectForKey:@"state"] intValue]; - if (state == LinphoneChatMessageStateInProgress) { - float progress = [[[notif userInfo] objectForKey:@"progress"] floatValue]; - _fileTransferProgress.progress = MAX(_fileTransferProgress.progress, progress); - _fileTransferProgress.hidden = _cancelButton.hidden = (_fileTransferProgress.progress == 1.f); - } else { - [self update]; - } -} - -@end diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.h b/Classes/LinphoneUI/UIChatBubblePhotoCell.h index 2fae8fe35..b6915c89f 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.h +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.h @@ -27,28 +27,28 @@ @interface UIChatBubblePhotoCell : UITransparentTVCell -@property(nonatomic, strong) IBOutlet UIView *innerView; -@property(nonatomic, strong) IBOutlet UIView *bubbleView; -@property(nonatomic, strong) IBOutlet UIImageView *backgroundImage; -@property(nonatomic, strong) IBOutlet UITextViewNoDefine *messageText; -@property(nonatomic, strong) IBOutlet UILoadingImageView *messageImageView; -@property(nonatomic, strong) IBOutlet UIButton *deleteButton; -@property(nonatomic, strong) IBOutlet UILabel *dateLabel; -@property(nonatomic, strong) IBOutlet UIImageView *statusImage; -@property(nonatomic, strong) IBOutlet UIButton *downloadButton; -@property(weak, nonatomic) IBOutlet UIProgressView *fileTransferProgress; -@property(weak, nonatomic) IBOutlet UIButton *cancelButton; -@property(nonatomic, strong) id chatRoomDelegate; - -+ (CGFloat)height:(LinphoneChatMessage *)chatMessage width:(int)width; - -- (void)setChatMessage:(LinphoneChatMessage *)message; -- (void)connectToFileDelegate:(FileTransferDelegate *)ftd; - -- (IBAction)onDeleteClick:(id)event; -- (IBAction)onDownloadClick:(id)event; -- (IBAction)onImageClick:(id)event; -- (IBAction)onCancelDownloadClick:(id)sender; -- (IBAction)onResendClick:(id)event; +//@property(nonatomic, strong) IBOutlet UIView *innerView; +//@property(nonatomic, strong) IBOutlet UIView *bubbleView; +//@property(nonatomic, strong) IBOutlet UIImageView *backgroundImage; +//@property(nonatomic, strong) IBOutlet UITextViewNoDefine *messageText; +//@property(nonatomic, strong) IBOutlet UILoadingImageView *messageImageView; +//@property(nonatomic, strong) IBOutlet UIButton *deleteButton; +//@property(nonatomic, strong) IBOutlet UILabel *dateLabel; +//@property(nonatomic, strong) IBOutlet UIImageView *statusImage; +//@property(nonatomic, strong) IBOutlet UIButton *downloadButton; +//@property(weak, nonatomic) IBOutlet UIProgressView *fileTransferProgress; +//@property(weak, nonatomic) IBOutlet UIButton *cancelButton; +//@property(nonatomic, strong) id chatRoomDelegate; +// +//+ (CGFloat)height:(LinphoneChatMessage *)chatMessage width:(int)width; +// +//- (void)setChatMessage:(LinphoneChatMessage *)message; +//- (void)connectToFileDelegate:(FileTransferDelegate *)ftd; +// +//- (IBAction)onDeleteClick:(id)event; +//- (IBAction)onDownloadClick:(id)event; +//- (IBAction)onImageClick:(id)event; +//- (IBAction)onCancelDownloadClick:(id)sender; +//- (IBAction)onResendClick:(id)event; @end diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.m b/Classes/LinphoneUI/UIChatBubblePhotoCell.m index 820a47b0c..cae51760a 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.m +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.m @@ -28,6 +28,8 @@ LinphoneChatMessage *message; FileTransferDelegate *ftd; } +#if 0 + static const CGFloat CELL_MIN_HEIGHT = 50.0f; static const CGFloat CELL_MIN_WIDTH = 150.0f; @@ -229,47 +231,6 @@ static UIFont *CELL_FONT = nil; } } -+ (CGSize)viewSize:(LinphoneChatMessage *)message width:(int)width { - CGSize messageSize; - const char *url = linphone_chat_message_get_external_body_url(message); - if (url == nil && linphone_chat_message_get_file_transfer_information(message) == NULL) { - NSString *text = [self.class TextMessageForChat:message]; - if (CELL_FONT == nil) { - CELL_FONT = [UIFont systemFontOfSize:CELL_FONT_SIZE]; - } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 - if (UIDevice.currentDevice.systemVersion.doubleValue >= 7) { - messageSize = - [text boundingRectWithSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, CGFLOAT_MAX) - options:(NSStringDrawingUsesLineFragmentOrigin | - NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading) - attributes:@{ - NSFontAttributeName : CELL_FONT - } context:nil] - .size; - } else -#endif - { - messageSize = [text sizeWithFont:CELL_FONT - constrainedToSize:CGSizeMake(width - CELL_MESSAGE_X_MARGIN, 10000.0f) - lineBreakMode:NSLineBreakByTruncatingTail]; - } - } else { - messageSize = CGSizeMake(CELL_IMAGE_WIDTH, CELL_IMAGE_HEIGHT); - } - messageSize.height += CELL_MESSAGE_Y_MARGIN; - if (messageSize.height < CELL_MIN_HEIGHT) - messageSize.height = CELL_MIN_HEIGHT; - messageSize.width += CELL_MESSAGE_X_MARGIN; - if (messageSize.width < CELL_MIN_WIDTH) - messageSize.width = CELL_MIN_WIDTH; - return messageSize; -} - -+ (CGFloat)height:(LinphoneChatMessage *)chatMessage width:(int)width { - return [self.class viewSize:chatMessage width:width].height; -} - #pragma mark - View Functions - (void)layoutSubviews { @@ -449,5 +410,6 @@ static void message_status(LinphoneChatMessage *msg, LinphoneChatMessageState st [self update]; } } +#endif @end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h new file mode 100644 index 000000000..d3ca14c16 --- /dev/null +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.h @@ -0,0 +1,47 @@ +/* UIChatRoomCell.h + * + * Copyright (C) 2012 Belledonne Comunications, Grenoble, France + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * 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 Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#import + +#import "UITransparentTVCell.h" +#import "UITextViewNoDefine.h" +#import "ChatConversationTableView.h" +#import "UIRoundedImageView.h" + +@interface UIChatBubbleTextCell : UITransparentTVCell + +@property(nonatomic, weak) IBOutlet UIView *innerView; +@property(nonatomic, weak) IBOutlet UIView *bubbleView; +@property(nonatomic, weak) IBOutlet UIImageView *backgroundColor; +@property(nonatomic, weak) IBOutlet UIRoundedImageView *avatarImage; +@property(nonatomic, weak) IBOutlet UILabel *contactDateLabel; +@property(nonatomic, weak) IBOutlet UIImageView *statusImage; +@property(nonatomic, weak) IBOutlet UITextViewNoDefine *messageText; +@property(nonatomic, weak) IBOutlet UIButton *deleteButton; +@property(weak, nonatomic) IBOutlet UIImageView *bottomBarColor; +@property(nonatomic, strong) id chatRoomDelegate; + +- (void)setChatMessage:(LinphoneChatMessage *)message; + +- (IBAction)onDeleteClick:(id)event; +- (IBAction)onResendClick:(id)event; + ++ (NSString *)TextMessageForChat:(LinphoneChatMessage *)message; + +@end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m new file mode 100644 index 000000000..17c00b952 --- /dev/null +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -0,0 +1,235 @@ +/* UIChatRoomCell.m + * + * Copyright (C) 2012 Belledonne Comunications, Grenoble, France + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * 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 Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#import "UIChatBubbleTextCell.h" +#import "LinphoneManager.h" +#import "PhoneMainView.h" + +#import +#import + +@implementation UIChatBubbleTextCell { + LinphoneChatMessage *message; +} + +#pragma mark - Lifecycle Functions + +- (id)initWithIdentifier:(NSString *)identifier { + if ((self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]) != nil) { + [[NSBundle mainBundle] loadNibNamed:@"UIChatBubbleTextCell" owner:self options:nil]; + +#if 0 + // shift message box, otherwise it will collide with the bubble + CGRect messageCoords = _messageText.frame; + messageCoords.origin.x += 2; + messageCoords.origin.y += 2; + messageCoords.size.width -= 5; + + _messageText.frame = messageCoords; + _messageText.allowSelectAll = TRUE; +#endif + } + + return self; +} + +- (void)dealloc { + [self setChatMessage:NULL]; +} + +#pragma mark - + +- (void)setChatMessage:(LinphoneChatMessage *)amessage { + if (amessage == message) { + return; + } + + if (message) { + linphone_chat_message_unref(message); + linphone_chat_message_set_user_data(message, NULL); + linphone_chat_message_cbs_set_msg_state_changed(linphone_chat_message_get_callbacks(message), NULL); + } + + message = amessage; + if (amessage) { + linphone_chat_message_ref(message); + linphone_chat_message_set_user_data(message, (void *)CFBridgingRetain(self)); + linphone_chat_message_cbs_set_msg_state_changed(linphone_chat_message_get_callbacks(message), message_status); + [self update]; + } +} + ++ (NSString *)TextMessageForChat:(LinphoneChatMessage *)message { + const char *text = linphone_chat_message_get_text(message); + return [NSString stringWithUTF8String:text] ?: [NSString stringWithCString:text encoding:NSASCIIStringEncoding] + ?: NSLocalizedString(@"(invalid string)", nil); +} + +- (NSString *)textMessage { + return [self.class TextMessageForChat:message]; +} + +- (void)update { + if (message == nil) { + LOGW(@"Cannot update message room cell: null message"); + return; + } + [_messageText setHidden:FALSE]; + /* We need to use an attributed string here so that data detector don't mess + * with the text style. See http://stackoverflow.com/a/20669356 */ + + NSAttributedString *attr_text = + [[NSAttributedString alloc] initWithString:self.textMessage + attributes:@{ + NSFontAttributeName : [UIFont systemFontOfSize:17.0], + NSForegroundColorAttributeName : [UIColor darkGrayColor] + }]; + _messageText.attributedText = attr_text; + + // Date + _contactDateLabel.text = + [LinphoneUtils timeToString:linphone_chat_message_get_time(message) withStyle:NSDateFormatterMediumStyle]; + + LinphoneChatMessageState state = linphone_chat_message_get_state(message); + BOOL outgoing = linphone_chat_message_is_outgoing(message); + + if (!outgoing) { + _statusImage.accessibilityValue = @"incoming"; + _statusImage.hidden = TRUE; // not useful for incoming chats.. + } else if (state == LinphoneChatMessageStateInProgress) { + _statusImage.image = [UIImage imageNamed:@"chat_message_inprogress.png"]; + _statusImage.accessibilityValue = @"in progress"; + _statusImage.hidden = FALSE; + } else if (state == LinphoneChatMessageStateDelivered) { + _statusImage.image = [UIImage imageNamed:@"chat_message_delivered.png"]; + _statusImage.accessibilityValue = @"delivered"; + _statusImage.hidden = FALSE; + } else { + _statusImage.image = [UIImage imageNamed:@"chat_message_not_delivered.png"]; + _statusImage.accessibilityValue = @"not delivered"; + _statusImage.hidden = FALSE; + + NSAttributedString *resend_text = + [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Resend", @"Resend") + attributes:@{NSForegroundColorAttributeName : [UIColor redColor]}]; + [_contactDateLabel setAttributedText:resend_text]; + } + + if (outgoing) { + [_messageText setAccessibilityLabel:@"Outgoing message"]; + } else { + [_messageText setAccessibilityLabel:@"Incoming message"]; + } +} + +- (void)setEditing:(BOOL)editing { + [self setEditing:editing animated:FALSE]; +} + +- (void)setEditing:(BOOL)editing animated:(BOOL)animated { + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.3]; + } + _deleteButton.hidden = !editing; + if (animated) { + [UIView commitAnimations]; + } +} + +#pragma mark - View Functions + +- (void)layoutSubviews { + [super layoutSubviews]; + if (message != nil) { + BOOL is_outgoing = linphone_chat_message_is_outgoing(message); + CGRect innerFrame; + innerFrame.size = [ChatConversationTableView viewSize:message width:self.frame.size.width]; + innerFrame.origin.y = 0.0f; + innerFrame.origin.x = is_outgoing ? self.frame.size.width - innerFrame.size.width : 0; + _innerView.frame = innerFrame; + + CGRect messageFrame = _bubbleView.frame; + messageFrame.origin.y = (_innerView.frame.size.height - messageFrame.size.height) / 2; + if (!is_outgoing) { + messageFrame.origin.y += 5; + } else { + messageFrame.origin.y -= 5; + } + _backgroundColor.image = + is_outgoing ? [UIImage imageNamed:@"chat_bubble_outgoing"] : [UIImage imageNamed:@"chat_bubble_incoming"]; + _bubbleView.frame = messageFrame; + } +} + +#pragma mark - Action Functions + +- (IBAction)onDeleteClick:(id)event { + if (message != NULL) { + UITableView *tableView = VIEW(ChatConversationView).tableController.tableView; + NSIndexPath *indexPath = [tableView indexPathForCell:self]; + [tableView.dataSource tableView:tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:indexPath]; + } +} + +- (IBAction)onResendClick:(id)event { + if (message == nil) + return; + + LinphoneChatMessageState state = linphone_chat_message_get_state(message); + if (state == LinphoneChatMessageStateNotDelivered) { + if (linphone_chat_message_get_file_transfer_information(message) != NULL) { + NSString *localImage = [LinphoneManager getMessageAppDataForKey:@"localimage" inMessage:message]; + NSURL *imageUrl = [NSURL URLWithString:localImage]; + + [self onDeleteClick:nil]; + + [[LinphoneManager instance] + .photoLibrary assetForURL:imageUrl + resultBlock:^(ALAsset *asset) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), + ^(void) { + UIImage *image = [[UIImage alloc] initWithCGImage:[asset thumbnail]]; + [_chatRoomDelegate startImageUpload:image url:imageUrl]; + }); + } + failureBlock:^(NSError *error) { + LOGE(@"Can't read image"); + }]; + } else { + [self onDeleteClick:nil]; + + double delayInSeconds = 0.4; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [_chatRoomDelegate resendChat:self.textMessage withExternalUrl:nil]; + }); + } + } +} +#pragma mark - State changed handling +static void message_status(LinphoneChatMessage *msg, LinphoneChatMessageState state) { + UIChatBubbleTextCell *thiz = (__bridge UIChatBubbleTextCell *)linphone_chat_message_get_user_data(msg); + LOGI(@"State for message [%p] changed to %s", msg, linphone_chat_message_state_to_string(state)); + [thiz update]; +} + +@end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.xib b/Classes/LinphoneUI/UIChatBubbleTextCell.xib new file mode 100644 index 000000000..ee8ddbd2c --- /dev/null +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.xib @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Classes/LinphoneUI/ar.lproj/UIChatBubbleCell.strings b/Classes/LinphoneUI/ar.lproj/UIChatBubbleCell.strings deleted file mode 100644 index 23bd6fc1835cdc5bae6c973bb838fd18d011a3c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2546 zcmbtW+iu!G5S^!a;44-^>V?LH*exop`cQB#&{kM!`U17|S{MF23dYz7FVSNqQ;%V=c z{a_Wv;SRrNLr>Y~WA<3S&sj1z@NC1KW3lTL1i!JyY;QW$!+iVe;wzTyjf@ccjs9-O z=#pE&D4w?NmgLHDlxOP!%`inL*m}TL%wC0a2v#f~U%_HXztR=eVfhp0iEeBh11r&; zSi-8>f$rK4boXG{^12VtkJa}aI~GInb0EI1Kan6rxIb(ArpER_2h zjrIJ_z+$HEVh)25nNGGdx(Q=>zFgs_?D8dlhN3NtulO7%cDJ)SPS5843LemnD%Q|L zEpNyAa*a>%)a5g;OBVSFJ2=Zn{NhpQi}h*@gdu9Qfp%`dy+Oqdpj?Am9MMnZen~&j zwr4p+UxJ=MtGQHosq72*B!=7v2Jb{qdZcjzcjv zXkWv=!@Ee=5W|++L{_^gZvdcE!2d#8b9MxeBF@ba=$4P~nKXVB(T5phRgu z)7!&=Do3zdsZN`Et9bB9?LE