element-ios/matrixConsole/ViewController/RoomViewController.m

2064 lines
98 KiB
Objective-C

/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MobileCoreServices/MobileCoreServices.h>
#import <MediaPlayer/MediaPlayer.h>
#import "RoomViewController.h"
#import "RoomMessage.h"
#import "RoomMessageTableCell.h"
#import "RoomMemberTableCell.h"
#import "RoomTitleView.h"
#import "MatrixHandler.h"
#import "AppDelegate.h"
#import "AppSettings.h"
#import "MediaManager.h"
#define ROOMVIEWCONTROLLER_UPLOAD_FILE_SIZE 5000000
#define ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE 20
#define ROOM_MESSAGE_CELL_DEFAULT_HEIGHT 50
#define ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST 10
#define ROOM_MESSAGE_CELL_DEFAULT_ATTACHMENTVIEW_TOP_CONST 18
#define ROOM_MESSAGE_CELL_HEIGHT_REDUCTION_WHEN_SENDER_INFO_IS_HIDDEN -10
#define ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW 120 // (51 + 69)
NSString *const kCmdChangeDisplayName = @"/nick";
NSString *const kCmdEmote = @"/me";
NSString *const kCmdJoinRoom = @"/join";
NSString *const kCmdKickUser = @"/kick";
NSString *const kCmdBanUser = @"/ban";
NSString *const kCmdUnbanUser = @"/unban";
NSString *const kCmdSetUserPowerLevel = @"/op";
NSString *const kCmdResetUserPowerLevel = @"/deop";
@interface RoomViewController () {
BOOL forceScrollToBottomOnViewDidAppear;
BOOL isJoinRequestInProgress;
// Messages
NSMutableArray *messages;
id messagesListener;
// Back pagination
BOOL isBackPaginationInProgress;
NSUInteger backPaginationAddedMsgNb;
// Members list
NSArray *members;
id membersListener;
// Attachment handling
CustomImageView *highResImage;
NSString *AVAudioSessionCategory;
MPMoviePlayerController *videoPlayer;
MPMoviePlayerController *tmpVideoPlayer;
// Date formatter (nil if dateTime info is hidden)
NSDateFormatter *dateFormatter;
// Cache
NSMutableArray *tmpCachedAttachments;
}
@property (weak, nonatomic) IBOutlet UINavigationItem *roomNavItem;
@property (weak, nonatomic) IBOutlet RoomTitleView *roomTitleView;
@property (weak, nonatomic) IBOutlet UITableView *messagesTableView;
@property (weak, nonatomic) IBOutlet UIView *controlView;
@property (weak, nonatomic) IBOutlet UIButton *optionBtn;
@property (weak, nonatomic) IBOutlet UITextField *messageTextField;
@property (weak, nonatomic) IBOutlet UIButton *sendBtn;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *controlViewBottomConstraint;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@property (weak, nonatomic) IBOutlet UIView *membersView;
@property (weak, nonatomic) IBOutlet UITableView *membersTableView;
@property (strong, nonatomic) MXRoom *mxRoom;
@property (strong, nonatomic) CustomAlert *actionMenu;
@end
@implementation RoomViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
forceScrollToBottomOnViewDidAppear = YES;
// Hide messages table by default in order to hide initial scrolling to the bottom
self.messagesTableView.hidden = YES;
UIButton *membersButton = [UIButton buttonWithType:UIButtonTypeSystem];
[membersButton setImage:[UIImage imageNamed:@"icon_users"] forState:UIControlStateNormal];
[membersButton setImage:[UIImage imageNamed:@"icon_users"] forState:UIControlStateHighlighted];
membersButton.frame = CGRectMake(0, 0, 44, 44);
[membersButton addTarget:self action:@selector(showHideRoomMembers:) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:membersButton];
// Add tap detection on members view in order to hide members when the user taps outside members list
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideRoomMembers)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[self.membersView addGestureRecognizer:tap];
_sendBtn.enabled = NO;
_sendBtn.alpha = 0.5;
}
- (void)dealloc {
// Clear temporary cached attachments (used for local echo)
NSUInteger index = tmpCachedAttachments.count;
NSError *error = nil;
while (index--) {
if (![[NSFileManager defaultManager] removeItemAtPath:[tmpCachedAttachments objectAtIndex:index] error:&error]) {
NSLog(@"Fail to delete cached media: %@", error);
}
}
tmpCachedAttachments = nil;
[self hideAttachmentView];
messages = nil;
if (messagesListener) {
[self.mxRoom removeListener:messagesListener];
messagesListener = nil;
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedMessages"];
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"displayAllEvents"];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isResumeDone"];
}
self.mxRoom = nil;
members = nil;
if (membersListener) {
membersListener = nil;
}
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
if (dateFormatter) {
dateFormatter = nil;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (isBackPaginationInProgress || isJoinRequestInProgress) {
// Busy - be sure that activity indicator is running
[self startActivityIndicator];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// hide action
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
// Hide members by default
[self hideRoomMembers];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Set visible room id
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId;
if (forceScrollToBottomOnViewDidAppear) {
// Scroll to the bottom
[self scrollToBottomAnimated:animated];
forceScrollToBottomOnViewDidAppear = NO;
self.messagesTableView.hidden = NO;
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Reset visible room id
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = nil;
}
#pragma mark - room ID
- (void)setRoomId:(NSString *)roomId {
if ([self.roomId isEqualToString:roomId] == NO) {
_roomId = roomId;
// Reload room data here
[self configureView];
}
}
#pragma mark - UIGestureRecognizer delegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.view == self.membersView) {
// Compute actual frame of the displayed members list
CGRect frame = self.membersTableView.frame;
if (self.membersTableView.tableFooterView.frame.origin.y < frame.size.height) {
frame.size.height = self.membersTableView.tableFooterView.frame.origin.y;
}
// gestureRecognizer should begin only if tap is outside members list
return !CGRectContainsPoint(frame, [gestureRecognizer locationInView:self.membersView]);
}
return YES;
}
#pragma mark - Internal methods
- (void)configureView {
// Check whether a request is in progress to join the room
if (isJoinRequestInProgress) {
// Busy - be sure that activity indicator is running
[self startActivityIndicator];
return;
}
// Remove potential listener
if (messagesListener && self.mxRoom) {
[self.mxRoom removeListener:messagesListener];
messagesListener = nil;
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedMessages"];
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"displayAllEvents"];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isResumeDone"];
}
// The whole room history is flushed here to rebuild it from the current instant (live)
messages = nil;
// Disable room title edition
self.roomTitleView.editable = NO;
// Update room data
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
self.mxRoom = nil;
if (self.roomId) {
self.mxRoom = [mxHandler.mxSession roomWithRoomId:self.roomId];
}
if (self.mxRoom) {
// Check first whether we have to join the room
if (self.mxRoom.state.membership == MXMembershipInvite) {
isJoinRequestInProgress = YES;
[self startActivityIndicator];
[self.mxRoom join:^{
[self stopActivityIndicator];
isJoinRequestInProgress = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
} failure:^(NSError *error) {
[self stopActivityIndicator];
isJoinRequestInProgress = NO;
NSLog(@"Failed to join room (%@): %@", self.mxRoom.state.displayname, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
return;
}
// Enable room title edition
self.roomTitleView.editable = YES;
messages = [NSMutableArray array];
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"hideUnsupportedMessages" options:0 context:nil];
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"displayAllEvents" options:0 context:nil];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isResumeDone" options:0 context:nil];
// Register a listener to handle messages
messagesListener = [self.mxRoom listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
BOOL shouldScrollToBottom = NO;
// Handle first live events
if (direction == MXEventDirectionForwards) {
// Check user's membership in live room state (Indeed we have to go back on recents when user leaves, or is kicked/banned)
if (self.mxRoom.state.membership == MXMembershipLeave || self.mxRoom.state.membership == MXMembershipBan) {
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:NO];
return;
}
// We will scroll to bottom after updating tableView only if the most recent message is entirely visible.
CGFloat maxPositionY = self.messagesTableView.contentOffset.y + (self.messagesTableView.frame.size.height - self.messagesTableView.contentInset.bottom);
// Be a bit less retrictive, scroll even if the most recent message is partially hidden
maxPositionY += 30;
shouldScrollToBottom = (maxPositionY >= self.messagesTableView.contentSize.height);
// Update Table
BOOL isHandled = NO;
// For outgoing message, remove the temporary event
if ([event.userId isEqualToString:[MatrixHandler sharedHandler].userId] && messages.count) {
// Consider first the last message
RoomMessage *message = [messages lastObject];
NSUInteger index = messages.count - 1;
if ([message containsEventId:event.eventId]) {
if (message.messageType == RoomMessageTypeText) {
// Removing temporary event (local echo)
[message removeEvent:event.eventId];
// Update message with the received event
isHandled = [message addEvent:event withRoomState:roomState];
if (! message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
}
} else {
// Create a new message to handle attachment
message = [[RoomMessage alloc] initWithEvent:event andRoomState:roomState];
if (!message) {
// Ignore unsupported/unexpected events
[messages removeObjectAtIndex:index];
} else {
[messages replaceObjectAtIndex:index withObject:message];
}
isHandled = YES;
}
} else {
while (index--) {
message = [messages objectAtIndex:index];
if ([message containsEventId:event.eventId]) {
if (message.messageType == RoomMessageTypeText) {
// Removing temporary event (local echo)
[message removeEvent:event.eventId];
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
}
} else {
// Remove the local event (a new one will be added to messages)
[messages removeObjectAtIndex:index];
}
break;
}
}
}
}
if (isHandled == NO) {
// Check whether the event may be grouped with last message
RoomMessage *lastMessage = [messages lastObject];
if (lastMessage && [lastMessage addEvent:event withRoomState:roomState]) {
isHandled = YES;
} else {
// Create a new item
lastMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomState];
if (lastMessage) {
[messages addObject:lastMessage];
isHandled = YES;
} // else ignore unsupported/unexpected events
}
}
// Refresh table display except if a back pagination is in progress
if (!isBackPaginationInProgress) {
[self.messagesTableView reloadData];
if (isHandled) {
if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:self.roomId] == NO) {
// Some new events are received for this room while it is not visible, scroll to bottom on viewDidAppear to focus on them
forceScrollToBottomOnViewDidAppear = YES;
}
}
}
} else if (isBackPaginationInProgress && direction == MXEventDirectionBackwards) {
// Back pagination is in progress, we add an old event at the beginning of messages
RoomMessage *firstMessage = [messages firstObject];
if (!firstMessage || [firstMessage addEvent:event withRoomState:roomState] == NO) {
firstMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomState];
if (firstMessage) {
[messages insertObject:firstMessage atIndex:0];
backPaginationAddedMsgNb++;
}
// Ignore unsupported/unexpected events
}
// Display is refreshed at the end of back pagination (see onComplete block)
}
if (shouldScrollToBottom) {
[self scrollToBottomAnimated:YES];
}
}];
// Trigger a back pagination by reseting first backState to get room history from live
[self.mxRoom resetBackState];
[self triggerBackPagination];
}
self.roomTitleView.mxRoom = self.mxRoom;
[self.messagesTableView reloadData];
}
- (void)scrollToBottomAnimated:(BOOL)animated {
// Delay the scrolling to handle correctly bunch of added events
dispatch_async(dispatch_get_main_queue(), ^{
// Scroll table view to the bottom
NSInteger rowNb = messages.count;
// Check whether there is some data and whether the table has already been loaded
if (rowNb && self.messagesTableView.contentSize.height) {
[self.messagesTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(rowNb - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
}
});
}
- (void)triggerBackPagination {
// Check whether a back pagination is already in progress
if (isBackPaginationInProgress) {
return;
}
if (self.mxRoom.canPaginate) {
[self startActivityIndicator];
isBackPaginationInProgress = YES;
backPaginationAddedMsgNb = 0;
[self paginateBackMessages:ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE];
}
}
- (void)paginateBackMessages:(NSUInteger)requestedItemsNb {
[self.mxRoom paginateBackMessages:requestedItemsNb complete:^{
// Sanity check: check whether the view controller has not been released while back pagination was running
if (self.roomId == nil) {
return;
}
// Compute number of received items
NSUInteger itemsCount = 0;
for (NSUInteger index = 0; index < backPaginationAddedMsgNb; index++) {
RoomMessage *message = [messages objectAtIndex:index];
itemsCount += message.components.count;
}
// Check whether we got enough items
if (itemsCount < requestedItemsNb && self.mxRoom.canPaginate) {
// Ask more items
[self paginateBackMessages:(requestedItemsNb - itemsCount)];
} else {
[self onBackPaginationComplete];
}
} failure:^(NSError *error) {
[self onBackPaginationComplete];
NSLog(@"Failed to paginate back: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
- (void)onBackPaginationComplete {
if (backPaginationAddedMsgNb) {
// We scroll to bottom when table is loaded for the first time
BOOL shouldScrollToBottom = (self.messagesTableView.contentSize.height == 0);
CGFloat verticalOffset = 0;
if (shouldScrollToBottom == NO) {
// In this case, we will adjust the vertical offset in order to make visible only a few part of added messages (at the top of the table)
NSIndexPath *indexPath;
// Compute the cumulative height of the added messages
for (NSUInteger index = 0; index < backPaginationAddedMsgNb; index++) {
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
verticalOffset += [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath];
}
// Deduce the vertical offset from this height
verticalOffset -= 100;
}
// Reset count to enable tableView update
backPaginationAddedMsgNb = 0;
// Reload
[self.messagesTableView reloadData];
// Adjust vertical content offset
if (shouldScrollToBottom) {
[self scrollToBottomAnimated:NO];
} else if (verticalOffset > 0) {
// Adjust vertical offset in order to limit scrolling down
CGPoint contentOffset = self.messagesTableView.contentOffset;
contentOffset.y = verticalOffset - self.messagesTableView.contentInset.top;
[self.messagesTableView setContentOffset:contentOffset animated:NO];
}
}
isBackPaginationInProgress = NO;
[self stopActivityIndicator];
}
- (void)startActivityIndicator {
[_activityIndicator startAnimating];
}
- (void)stopActivityIndicator {
// Check whether all conditions are satisfied before stopping loading wheel
if ([[MatrixHandler sharedHandler] isResumeDone] && !isBackPaginationInProgress && !isJoinRequestInProgress) {
[_activityIndicator stopAnimating];
}
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"displayAllEvents" isEqualToString:keyPath]) {
// Back to recents (Room details are not available until the end of initial sync)
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:NO];
} else if ([@"hideUnsupportedMessages" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
} else if ([@"isResumeDone" isEqualToString:keyPath]) {
if ([[MatrixHandler sharedHandler] isResumeDone]) {
[self stopActivityIndicator];
} else {
[self startActivityIndicator];
}
}
}
# pragma mark - Room members
- (void)showHideRoomMembers:(id)sender {
// Check whether the members list is displayed
if (members) {
[self hideRoomMembers];
} else {
[self hideAttachmentView];
[self showRoomMembers];
}
}
- (void)updateRoomMembers {
members = [[self.mxRoom.state members] sortedArrayUsingComparator:^NSComparisonResult(MXRoomMember *member1, MXRoomMember *member2) {
// Move banned and left members at the end of the list
if (member1.membership == MXMembershipLeave || member1.membership == MXMembershipBan) {
if (member2.membership != MXMembershipLeave && member2.membership != MXMembershipBan) {
return NSOrderedDescending;
}
} else if (member2.membership == MXMembershipLeave || member2.membership == MXMembershipBan) {
return NSOrderedAscending;
}
// Move invited members just before left and banned members
if (member1.membership == MXMembershipInvite) {
if (member2.membership != MXMembershipInvite) {
return NSOrderedDescending;
}
} else if (member2.membership == MXMembershipInvite) {
return NSOrderedAscending;
}
if ([[AppSettings sharedSettings] sortMembersUsingLastSeenTime]) {
// Get the users that correspond to these members
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
MXUser *user1 = [mxHandler.mxSession userWithUserId:member1.userId];
MXUser *user2 = [mxHandler.mxSession userWithUserId:member2.userId];
// Move users who are not online or unavailable at the end (before invited users)
if ((user1.presence == MXPresenceOnline) || (user1.presence == MXPresenceUnavailable)) {
if ((user2.presence != MXPresenceOnline) && (user2.presence != MXPresenceUnavailable)) {
return NSOrderedAscending;
}
} else if ((user2.presence == MXPresenceOnline) || (user2.presence == MXPresenceUnavailable)) {
return NSOrderedDescending;
} else {
// Here both users are neither online nor unavailable (the lastActive ago is useless)
// We will sort them according to their display, by keeping in front the offline users
if (user1.presence == MXPresenceOffline) {
if (user2.presence != MXPresenceOffline) {
return NSOrderedAscending;
}
} else if (user2.presence == MXPresenceOffline) {
return NSOrderedDescending;
}
return [[self.mxRoom.state memberName:member1.userId] compare:[self.mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch];
}
// Consider user's lastActive ago value
if (user1.lastActiveAgo < user2.lastActiveAgo) {
return NSOrderedAscending;
} else if (user1.lastActiveAgo == user2.lastActiveAgo) {
return [[self.mxRoom.state memberName:member1.userId] compare:[self.mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch];
}
return NSOrderedDescending;
} else {
// Move user without display name at the end (before invited users)
if (member1.displayname.length) {
if (!member2.displayname.length) {
return NSOrderedAscending;
}
} else if (member2.displayname.length) {
return NSOrderedDescending;
}
return [[self.mxRoom.state memberName:member1.userId] compare:[self.mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch];
}
}];
}
- (void)showRoomMembers {
// Dismiss keyboard
[self dismissKeyboard];
[self updateRoomMembers];
// Register a listener for events that concern room members
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSArray *mxMembersEvents = @[
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomPowerLevels,
kMXEventTypeStringPresence
];
membersListener = [mxHandler.mxSession listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// consider only live event
if (direction == MXEventDirectionForwards) {
// Check the room Id (if any)
if (event.roomId && [event.roomId isEqualToString:self.roomId] == NO) {
// This event does not concern the current room members
return;
}
// Hide potential action sheet
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
// Refresh members list
[self updateRoomMembers];
[self.membersTableView reloadData];
}
}];
self.membersView.hidden = NO;
[self.membersTableView reloadData];
}
- (void)hideRoomMembers {
if (membersListener) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxSession removeListener:membersListener];
membersListener = nil;
}
self.membersView.hidden = YES;
members = nil;
}
# pragma mark - Attachment handling
- (void)showAttachmentView:(UIGestureRecognizer *)gestureRecognizer {
CustomImageView *attachment = (CustomImageView*)gestureRecognizer.view;
[self dismissKeyboard];
// Retrieve attachment information
NSDictionary *content = attachment.mediaInfo;
NSUInteger msgtype = ((NSNumber*)content[@"msgtype"]).unsignedIntValue;
if (msgtype == RoomMessageTypeImage) {
NSString *url = content[@"url"];
if (url.length) {
highResImage = [[CustomImageView alloc] initWithFrame:self.membersView.frame];
highResImage.contentMode = UIViewContentModeScaleAspectFit;
highResImage.backgroundColor = [UIColor blackColor];
highResImage.imageURL = url;
[self.view addSubview:highResImage];
// Add tap recognizer to hide attachment
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideAttachmentView)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[highResImage addGestureRecognizer:tap];
highResImage.userInteractionEnabled = YES;
}
} else if (msgtype == RoomMessageTypeVideo) {
NSString *url =content[@"url"];
if (url.length) {
NSString *mimetype = nil;
if (content[@"info"]) {
mimetype = content[@"info"][@"mimetype"];
}
AVAudioSessionCategory = [[AVAudioSession sharedInstance] category];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
videoPlayer = [[MPMoviePlayerController alloc] init];
if (videoPlayer != nil) {
videoPlayer.scalingMode = MPMovieScalingModeAspectFit;
[self.view addSubview:videoPlayer.view];
[videoPlayer setFullscreen:YES animated:NO];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerPlaybackDidFinishNotification:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerWillExitFullscreen:)
name:MPMoviePlayerWillExitFullscreenNotification
object:videoPlayer];
[MediaManager prepareMedia:url mimeType:mimetype success:^(NSString *cacheFilePath) {
if (cacheFilePath) {
if (tmpCachedAttachments == nil) {
tmpCachedAttachments = [NSMutableArray array];
}
if ([tmpCachedAttachments indexOfObject:cacheFilePath]) {
[tmpCachedAttachments addObject:cacheFilePath];
}
}
videoPlayer.contentURL = [NSURL fileURLWithPath:cacheFilePath];
[videoPlayer play];
} failure:^(NSError *error) {
[self hideAttachmentView];
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
} else if (msgtype == RoomMessageTypeAudio) {
} else if (msgtype == RoomMessageTypeLocation) {
}
}
- (void)hideAttachmentView {
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerPlaybackDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerWillExitFullscreenNotification object:nil];
if (highResImage) {
[highResImage removeFromSuperview];
highResImage = nil;
}
// Restore audio category
if (AVAudioSessionCategory) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategory error:nil];
}
if (videoPlayer) {
[videoPlayer stop];
[videoPlayer setFullscreen:NO];
[videoPlayer.view removeFromSuperview];
videoPlayer = nil;
}
}
- (void)moviePlayerWillExitFullscreen:(NSNotification*)notification {
if (notification.object == videoPlayer) {
[self hideAttachmentView];
}
}
- (void)moviePlayerPlaybackDidFinishNotification:(NSNotification *)notification {
NSDictionary *notificationUserInfo = [notification userInfo];
NSNumber *resultValue = [notificationUserInfo objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey];
MPMovieFinishReason reason = [resultValue intValue];
// error cases
if (reason == MPMovieFinishReasonPlaybackError) {
NSError *mediaPlayerError = [notificationUserInfo objectForKey:@"error"];
if (mediaPlayerError) {
NSLog(@"Playback failed with error description: %@", [mediaPlayerError localizedDescription]);
[self hideAttachmentView];
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:mediaPlayerError];
}
}
}
- (void)moviePlayerThumbnailImageRequestDidFinishNotification:(NSNotification *)notification {
// Finalize video attachment
UIImage* videoThumbnail = [[notification userInfo] objectForKey:MPMoviePlayerThumbnailImageKey];
NSURL* selectedVideo = [tmpVideoPlayer contentURL];
[tmpVideoPlayer stop];
tmpVideoPlayer = nil;
if (videoThumbnail && selectedVideo) {
// Prepare video thumbnail description
NSUInteger thumbnailSize = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
UIImage *thumbnail = [MediaManager resize:videoThumbnail toFitInSize:CGSizeMake(thumbnailSize, thumbnailSize)];
NSMutableDictionary *thumbnailInfo = [[NSMutableDictionary alloc] init];
[thumbnailInfo setValue:@"image/jpeg" forKey:@"mimetype"];
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.width] forKey:@"w"];
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.height] forKey:@"h"];
NSData *thumbnailData = UIImageJPEGRepresentation(thumbnail, 0.9);
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:thumbnailData.length] forKey:@"size"];
// Create the local event displayed during uploading
MXEvent *localEvent = [self addLocalEventForAttachedImage:thumbnail];
// Upload thumbnail
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxRestClient uploadContent:thumbnailData mimeType:@"image/jpeg" timeout:30 success:^(NSString *url) {
// Prepare content of attached video
NSMutableDictionary *videoContent = [[NSMutableDictionary alloc] init];
NSMutableDictionary *videoInfo = [[NSMutableDictionary alloc] init];
[videoContent setValue:@"m.video" forKey:@"msgtype"];
[videoInfo setValue:url forKey:@"thumbnail_url"];
[videoInfo setValue:thumbnailInfo forKey:@"thumbnail_info"];
// Convert video container to mp4
AVURLAsset* videoAsset = [AVURLAsset URLAssetWithURL:selectedVideo options:nil];
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:videoAsset presetName:AVAssetExportPresetMediumQuality];
// Set output URL
NSString * outputFileName = [NSString stringWithFormat:@"%.0f.mp4",[[NSDate date] timeIntervalSince1970]];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheRoot = [paths objectAtIndex:0];
NSURL *tmpVideoLocation = [NSURL fileURLWithPath:[cacheRoot stringByAppendingPathComponent:outputFileName]];
exportSession.outputURL = tmpVideoLocation;
// Check supported output file type
NSArray *supportedFileTypes = exportSession.supportedFileTypes;
if ([supportedFileTypes containsObject:AVFileTypeMPEG4]) {
exportSession.outputFileType = AVFileTypeMPEG4;
[videoInfo setValue:@"video/mp4" forKey:@"mimetype"];
} else {
NSLog(@"Unexpected case: MPEG-4 file format is not supported");
// we send QuickTime movie file by default
exportSession.outputFileType = AVFileTypeQuickTimeMovie;
[videoInfo setValue:@"video/quicktime" forKey:@"mimetype"];
}
// Export video file and send it
[exportSession exportAsynchronouslyWithCompletionHandler:^{
// Check status
if ([exportSession status] == AVAssetExportSessionStatusCompleted) {
AVURLAsset* asset = [AVURLAsset URLAssetWithURL:tmpVideoLocation
options:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],
AVURLAssetPreferPreciseDurationAndTimingKey,
nil]
];
[videoInfo setValue:[NSNumber numberWithDouble:(1000 * CMTimeGetSeconds(asset.duration))] forKey:@"duration"];
NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if (videoTracks.count > 0) {
AVAssetTrack *videoTrack = [videoTracks objectAtIndex:0];
CGSize videoSize = videoTrack.naturalSize;
[videoInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)videoSize.width] forKey:@"w"];
[videoInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)videoSize.height] forKey:@"h"];
}
// Upload the video
NSData *videoData = [NSData dataWithContentsOfURL:tmpVideoLocation];
[[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil];
if (videoData) {
if (videoData.length < ROOMVIEWCONTROLLER_UPLOAD_FILE_SIZE) {
[videoInfo setValue:[NSNumber numberWithUnsignedInteger:videoData.length] forKey:@"size"];
[mxHandler.mxRestClient uploadContent:videoData mimeType:videoInfo[@"mimetype"] timeout:30 success:^(NSString *url) {
[videoContent setValue:url forKey:@"url"];
[videoContent setValue:videoInfo forKey:@"info"];
[videoContent setValue:@"Video" forKey:@"body"];
[self postMessage:videoContent withLocalEvent:localEvent];
} failure:^(NSError *error) {
[self handleError:error forLocalEvent:localEvent];
}];
} else {
NSLog(@"Video is too large");
[self handleError:nil forLocalEvent:localEvent];
}
} else {
NSLog(@"Attach video failed: no data");
[self handleError:nil forLocalEvent:localEvent];
}
}
else {
NSLog(@"Video export failed: %d", (int)[exportSession status]);
// remove tmp file (if any)
[[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil];
[self handleError:nil forLocalEvent:localEvent];
}
}];
} failure:^(NSError *error) {
NSLog(@"Video thumbnail upload failed");
[self handleError:error forLocalEvent:localEvent];
}];
}
[self dismissMediaPicker];
}
#pragma mark - Keyboard handling
- (void)onKeyboardWillShow:(NSNotification *)notif {
// get the keyboard size
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
CGRect endRect = rectVal.CGRectValue;
UIEdgeInsets insets = self.messagesTableView.contentInset;
// Handle portrait/landscape mode
insets.bottom = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
// bottom view offset
CGFloat nextBottomViewContanst = insets.bottom - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height;
// get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// the duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
NSInteger rowNb = messages.count;
BOOL scrollToBottom = (rowNb && self.messagesTableView.contentSize.height);
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
// Move up control view
// Don't forget the offset related to tabBar
_controlViewBottomConstraint.constant = nextBottomViewContanst;
// reduce the tableview height
self.messagesTableView.contentInset = insets;
if (scrollToBottom)
{
[self.messagesTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(rowNb - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:NO];
}
// force to redraw the layout (else _controlViewBottomConstraint.constant will not be animated)
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
}
- (void)onKeyboardWillHide:(NSNotification *)notif {
// get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// the duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// ani
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
UIEdgeInsets insets = self.messagesTableView.contentInset;
insets.bottom = self.controlView.frame.size.height;
self.messagesTableView.contentInset = insets;
_controlViewBottomConstraint.constant = 0;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
}
- (void)dismissKeyboard {
// Hide the keyboard
[_messageTextField resignFirstResponder];
[_roomTitleView dismissKeyboard];
}
#pragma mark - UITableView data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Check table view members vs messages
if (tableView == self.membersTableView) {
return members.count;
}
if (backPaginationAddedMsgNb) {
// Here some old messages have been added to messages during back pagination.
// Stop table refreshing, the table will be refreshed at the end of pagination
return 0;
}
return messages.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Check table view members vs messages
if (tableView == self.membersTableView) {
// Use the same default height than message cell
return ROOM_MESSAGE_CELL_DEFAULT_HEIGHT;
}
// Compute here height of message cell
CGFloat rowHeight;
// Compute first height of message content (The maximum width available for the textview must be updated dynamically)
RoomMessage* message = [messages objectAtIndex:indexPath.row];
message.maxTextViewWidth = self.messagesTableView.frame.size.width - ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW;
rowHeight = message.contentSize.height;
// Add top margin
if (message.messageType == RoomMessageTypeText) {
rowHeight += ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST;
} else {
rowHeight += ROOM_MESSAGE_CELL_DEFAULT_ATTACHMENTVIEW_TOP_CONST;
}
// Check whether the previous message has been sent by the same user.
// The user's picture and name are displayed only for the first message.
BOOL shouldHideSenderInfo = NO;
if (indexPath.row) {
RoomMessage *previousMessage = [messages objectAtIndex:indexPath.row - 1];
if ([previousMessage.senderId isEqualToString:message.senderId]
&& [previousMessage.senderName isEqualToString:message.senderName]
&& [previousMessage.senderAvatarUrl isEqualToString:message.senderAvatarUrl]) {
shouldHideSenderInfo = YES;
}
}
if (shouldHideSenderInfo) {
// Reduce top margin -> row height reduction
rowHeight += ROOM_MESSAGE_CELL_HEIGHT_REDUCTION_WHEN_SENDER_INFO_IS_HIDDEN;
} else {
// We consider a minimun cell height in order to display correctly user's picture
if (rowHeight < ROOM_MESSAGE_CELL_DEFAULT_HEIGHT) {
rowHeight = ROOM_MESSAGE_CELL_DEFAULT_HEIGHT;
}
}
return rowHeight;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// Check table view members vs messages
if (tableView == self.membersTableView) {
RoomMemberTableCell *memberCell = [tableView dequeueReusableCellWithIdentifier:@"RoomMemberCell" forIndexPath:indexPath];
if (indexPath.row < members.count) {
[memberCell setRoomMember:[members objectAtIndex:indexPath.row] withRoom:self.mxRoom];
}
return memberCell;
}
// Handle here room message cells
RoomMessageTableCell *cell;
RoomMessage *message = [messages objectAtIndex:indexPath.row];
BOOL isIncomingMsg = NO;
if ([message.senderId isEqualToString:mxHandler.userId]) {
cell = [tableView dequeueReusableCellWithIdentifier:@"OutgoingMessageCell" forIndexPath:indexPath];
OutgoingMessageTableCell* outgoingMsgCell = (OutgoingMessageTableCell*)cell;
// Hide potential loading wheel
[outgoingMsgCell.activityIndicator stopAnimating];
} else {
cell = [tableView dequeueReusableCellWithIdentifier:@"IncomingMessageCell" forIndexPath:indexPath];
isIncomingMsg = YES;
}
// Restore initial settings
cell.attachmentView.imageURL = nil; // Cancel potential attachment loading
cell.attachmentView.hidden = YES;
cell.playIconView.hidden = YES;
// Remove all gesture recognizer
while (cell.attachmentView.gestureRecognizers.count) {
[cell.attachmentView removeGestureRecognizer:cell.attachmentView.gestureRecognizers[0]];
}
// Remove potential dateTime (or unsent) label(s)
if (cell.dateTimeLabelContainer.constraints.count) {
if ([NSLayoutConstraint respondsToSelector:@selector(deactivateConstraints:)]) {
[NSLayoutConstraint deactivateConstraints:cell.dateTimeLabelContainer.constraints];
} else {
[cell.dateTimeLabelContainer removeConstraints:cell.dateTimeLabelContainer.constraints];
}
for (UIView *view in cell.dateTimeLabelContainer.subviews) {
[view removeFromSuperview];
}
}
// Check whether the previous message has been sent by the same user.
// The user's picture and name are displayed only for the first message.
BOOL shouldHideSenderInfo = NO;
if (indexPath.row) {
RoomMessage *previousMessage = [messages objectAtIndex:indexPath.row - 1];
if ([previousMessage.senderId isEqualToString:message.senderId]
&& [previousMessage.senderName isEqualToString:message.senderName]
&& [previousMessage.senderAvatarUrl isEqualToString:message.senderAvatarUrl]) {
shouldHideSenderInfo = YES;
}
}
// Handle sender's picture and adjust view's constraints
if (shouldHideSenderInfo) {
cell.pictureView.hidden = YES;
cell.msgTextViewTopConstraint.constant = ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST + ROOM_MESSAGE_CELL_HEIGHT_REDUCTION_WHEN_SENDER_INFO_IS_HIDDEN;
cell.attachViewTopConstraint.constant = ROOM_MESSAGE_CELL_DEFAULT_ATTACHMENTVIEW_TOP_CONST + ROOM_MESSAGE_CELL_HEIGHT_REDUCTION_WHEN_SENDER_INFO_IS_HIDDEN;
} else {
cell.pictureView.hidden = NO;
cell.msgTextViewTopConstraint.constant = ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST;
cell.attachViewTopConstraint.constant = ROOM_MESSAGE_CELL_DEFAULT_ATTACHMENTVIEW_TOP_CONST;
// Handle user's picture
cell.pictureView.placeholder = @"default-profile";
cell.pictureView.imageURL = message.senderAvatarUrl;
[cell.pictureView.layer setCornerRadius:cell.pictureView.frame.size.width / 2];
cell.pictureView.clipsToBounds = YES;
}
// Adjust top constraint constant for dateTime labels container, and hide it by default
if (message.messageType == RoomMessageTypeText) {
cell.dateTimeLabelContainerTopConstraint.constant = cell.msgTextViewTopConstraint.constant;
} else {
cell.dateTimeLabelContainerTopConstraint.constant = cell.attachViewTopConstraint.constant;
}
cell.dateTimeLabelContainer.hidden = YES;
// Update incoming/outgoing message layout
if (isIncomingMsg) {
IncomingMessageTableCell* incomingMsgCell = (IncomingMessageTableCell*)cell;
// Display user's display name except if the name appears in the displayed text (see emote and membership event)
incomingMsgCell.userNameLabel.hidden = (shouldHideSenderInfo || message.startsWithSenderName);
incomingMsgCell.userNameLabel.text = message.senderName;
} else {
// Add unsent label for failed components
CGFloat yPosition = (message.messageType == RoomMessageTypeText) ? ROOM_MESSAGE_TEXTVIEW_MARGIN : -ROOM_MESSAGE_TEXTVIEW_MARGIN;
for (RoomMessageComponent *component in message.components) {
if (component.style == RoomMessageComponentStyleFailed) {
UILabel *unsentLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, yPosition, 58 , 20)];
unsentLabel.text = @"Unsent";
unsentLabel.textAlignment = NSTextAlignmentCenter;
unsentLabel.textColor = [UIColor redColor];
unsentLabel.font = [UIFont systemFontOfSize:14];
[cell.dateTimeLabelContainer addSubview:unsentLabel];
cell.dateTimeLabelContainer.hidden = NO;
}
yPosition += component.height;
}
}
// Set message content
message.maxTextViewWidth = self.messagesTableView.frame.size.width - ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW;
CGSize contentSize = message.contentSize;
if (message.messageType != RoomMessageTypeText) {
cell.messageTextView.hidden = YES;
cell.attachmentView.hidden = NO;
// Update image view frame in order to center loading wheel (if any)
CGRect frame = cell.attachmentView.frame;
frame.size.width = contentSize.width;
frame.size.height = contentSize.height;
cell.attachmentView.frame = frame;
// Fade attachments during upload
if (message.isUploadInProgress) {
cell.attachmentView.alpha = 0.5;
[((OutgoingMessageTableCell*)cell).activityIndicator startAnimating];
cell.attachmentView.hideActivityIndicator = YES;
} else {
cell.attachmentView.alpha = 1;
cell.attachmentView.hideActivityIndicator = NO;
}
NSString *url = message.thumbnailURL;
if (!url && message.messageType == RoomMessageTypeImage) {
url = message.attachmentURL;
}
if (message.messageType == RoomMessageTypeVideo) {
cell.playIconView.hidden = NO;
}
cell.attachmentView.imageURL = url;
if (url && message.attachmentURL && message.attachmentInfo) {
// Add tap recognizer to open attachment
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showAttachmentView:)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[cell.attachmentView addGestureRecognizer:tap];
// Store attachment content description used in showAttachmentView:
cell.attachmentView.mediaInfo = @{@"msgtype" : [NSNumber numberWithUnsignedInt:message.messageType],
@"url" : message.attachmentURL,
@"info" : message.attachmentInfo};
}
// Adjust Attachment width constant
cell.attachViewWidthConstraint.constant = contentSize.width;
} else {
cell.messageTextView.hidden = NO;
if (!isIncomingMsg) {
// Adjust horizontal position for outgoing messages (text is left aligned, but the textView should be right aligned)
CGFloat leftInset = message.maxTextViewWidth - contentSize.width;
cell.messageTextView.contentInset = UIEdgeInsetsMake(0, leftInset, 0, -leftInset);
}
cell.messageTextView.attributedText = message.attributedTextMessage;
}
// Handle timestamp display
if (dateFormatter) {
// Add datetime label for each component
cell.dateTimeLabelContainer.hidden = NO;
CGFloat yPosition = (message.messageType == RoomMessageTypeText) ? ROOM_MESSAGE_TEXTVIEW_MARGIN : -ROOM_MESSAGE_TEXTVIEW_MARGIN;
for (RoomMessageComponent *component in message.components) {
if (component.date) {
UILabel *dateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, yPosition, cell.dateTimeLabelContainer.frame.size.width , 20)];
dateTimeLabel.text = [dateFormatter stringFromDate:component.date];
if (isIncomingMsg) {
dateTimeLabel.textAlignment = NSTextAlignmentRight;
} else {
dateTimeLabel.textAlignment = NSTextAlignmentLeft;
}
dateTimeLabel.textColor = [UIColor lightGrayColor];
dateTimeLabel.font = [UIFont systemFontOfSize:12];
dateTimeLabel.adjustsFontSizeToFitWidth = YES;
dateTimeLabel.minimumScaleFactor = 0.6;
[dateTimeLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[cell.dateTimeLabelContainer addSubview:dateTimeLabel];
// Force dateTimeLabel in full width (to handle auto-layout in case of screen rotation)
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:cell.dateTimeLabelContainer
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:cell.dateTimeLabelContainer
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
// Vertical constraints are required for iOS > 8
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:cell.dateTimeLabelContainer
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:yPosition];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:20];
if ([NSLayoutConstraint respondsToSelector:@selector(activateConstraints:)]) {
[NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
} else {
[cell.dateTimeLabelContainer addConstraint:leftConstraint];
[cell.dateTimeLabelContainer addConstraint:rightConstraint];
[cell.dateTimeLabelContainer addConstraint:topConstraint];
[dateTimeLabel addConstraint:heightConstraint];
}
}
yPosition += component.height;
}
}
return cell;
}
#pragma mark - UITableView delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Check table view members vs messages
if (tableView == self.membersTableView) {
// List action(s) available on this member
MXRoomMember *roomMember = [members objectAtIndex:indexPath.row];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
__weak typeof(self) weakSelf = self;
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
// Consider the case of the user himself
if ([roomMember.userId isEqualToString:mxHandler.userId]) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Leave" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
MXRoom *currentRoom = [[MatrixHandler sharedHandler].mxSession roomWithRoomId:weakSelf.roomId];
[currentRoom leave:^{
// Back to recents
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:YES];
} failure:^(NSError *error) {
NSLog(@"Leave room %@ failed: %@", weakSelf.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
} else {
// Check user's power level before allowing an action (kick, ban, ...)
MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels];
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:mxHandler.userId];
NSUInteger memberPowerLevel = [powerLevels powerLevelOfUserWithUserID:roomMember.userId];
// Consider membership of the selected member
switch (roomMember.membership) {
case MXMembershipInvite:
case MXMembershipJoin: {
// Check conditions to be able to kick someone
if (userPowerLevel >= [powerLevels kick] && userPowerLevel >= memberPowerLevel) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Kick" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[weakSelf.mxRoom kickUser:roomMember.userId
reason:nil
success:^{
}
failure:^(NSError *error) {
NSLog(@"Kick %@ failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
// Check conditions to be able to ban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
}
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[weakSelf.mxRoom banUser:roomMember.userId
reason:nil
success:^{
}
failure:^(NSError *error) {
NSLog(@"Ban %@ failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
break;
}
case MXMembershipLeave: {
// Check conditions to be able to invite someone
if (userPowerLevel >= [powerLevels invite]) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[weakSelf.mxRoom inviteUser:roomMember.userId
success:^{
}
failure:^(NSError *error) {
NSLog(@"Invite %@ failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
// Check conditions to be able to ban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
}
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[weakSelf.mxRoom banUser:roomMember.userId
reason:nil
success:^{
}
failure:^(NSError *error) {
NSLog(@"Ban %@ failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
break;
}
case MXMembershipBan: {
// Check conditions to be able to unban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Unban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[weakSelf.mxRoom unbanUser:roomMember.userId
success:^{
}
failure:^(NSError *error) {
NSLog(@"Unban %@ failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
}
break;
}
default: {
break;
}
}
}
// Notify user when his power is too weak
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:nil message:@"You are not authorized to change the status of this member" style:CustomAlertStyleAlert];
}
// Display the action sheet (or the alert)
if (self.actionMenu) {
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[self.actionMenu showInViewController:self];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} else if (tableView == self.messagesTableView) {
// Dismiss keyboard when user taps on messages table view content
[self dismissKeyboard];
}
}
// Detect vertical bounce at the top of the tableview to trigger pagination
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if (scrollView == self.messagesTableView) {
// paginate ?
if (scrollView.contentOffset.y < -64)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self triggerBackPagination];
});
}
}
}
#pragma mark - UITextField delegate
- (void)onTextFieldChange:(NSNotification *)notification {
if (notification.object == _messageTextField) {
NSString *msg = _messageTextField.text;
if (msg.length) {
_sendBtn.enabled = YES;
_sendBtn.alpha = 1;
// Reset potential placeholder (used in case of wrong command usage)
_messageTextField.placeholder = nil;
} else {
_sendBtn.enabled = NO;
_sendBtn.alpha = 0.5;
}
}
}
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
NSString *alertMsg = nil;
if (textField == _roomTitleView.displayNameTextField) {
// Check whether the user has enough power to rename the room
MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels];
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:[MatrixHandler sharedHandler].userId];
if (userPowerLevel >= [powerLevels minimumPowerLevelForPostingEventAsStateEvent:kMXEventTypeStringRoomName]) {
// Only the room name is edited here, update the text field with the room name
textField.text = self.mxRoom.state.name;
textField.backgroundColor = [UIColor whiteColor];
} else {
alertMsg = @"You are not authorized to edit this room name";
}
// Check whether the user is allowed to change room topic
if (userPowerLevel >= [powerLevels minimumPowerLevelForPostingEventAsStateEvent:kMXEventTypeStringRoomTopic]) {
// Show topic text field even if the current value is nil
_roomTitleView.hiddenTopic = NO;
if (alertMsg) {
// Here the user can only update the room topic, switch on room topic field (without displaying alert)
alertMsg = nil;
[_roomTitleView.topicTextField becomeFirstResponder];
return NO;
}
}
} else if (textField == _roomTitleView.topicTextField) {
// Check whether the user has enough power to edit room topic
MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels];
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:[MatrixHandler sharedHandler].userId];
if (userPowerLevel >= [powerLevels minimumPowerLevelForPostingEventAsStateEvent:kMXEventTypeStringRoomTopic]) {
textField.backgroundColor = [UIColor whiteColor];
} else {
alertMsg = @"You are not authorized to edit this room topic";
}
}
if (alertMsg) {
// Alert user
__weak typeof(self) weakSelf = self;
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
}
self.actionMenu = [[CustomAlert alloc] initWithTitle:nil message:alertMsg style:CustomAlertStyleAlert];
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[self.actionMenu showInViewController:self];
return NO;
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField == _roomTitleView.displayNameTextField) {
textField.backgroundColor = [UIColor clearColor];
NSString *roomName = textField.text;
if ((roomName.length || self.mxRoom.state.name.length) && [roomName isEqualToString:self.mxRoom.state.name] == NO) {
[self startActivityIndicator];
__weak typeof(self) weakSelf = self;
[self.mxRoom setName:roomName success:^{
[weakSelf stopActivityIndicator];
// Refresh title display
textField.text = weakSelf.mxRoom.state.displayname;
} failure:^(NSError *error) {
[weakSelf stopActivityIndicator];
// Revert change
textField.text = weakSelf.mxRoom.state.displayname;
NSLog(@"Rename room failed: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// No change on room name, restore title with room displayName
textField.text = self.mxRoom.state.displayname;
}
} else if (textField == _roomTitleView.topicTextField) {
textField.backgroundColor = [UIColor clearColor];
NSString *topic = textField.text;
if ((topic.length || self.mxRoom.state.topic.length) && [topic isEqualToString:self.mxRoom.state.topic] == NO) {
[self startActivityIndicator];
__weak typeof(self) weakSelf = self;
[self.mxRoom setTopic:topic success:^{
[weakSelf stopActivityIndicator];
// Hide topic field if empty
weakSelf.roomTitleView.hiddenTopic = !textField.text.length;
} failure:^(NSError *error) {
[weakSelf stopActivityIndicator];
// Revert change
textField.text = weakSelf.mxRoom.state.topic;
// Hide topic field if empty
weakSelf.roomTitleView.hiddenTopic = !textField.text.length;
NSLog(@"Topic room change failed: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Hide topic field if empty
_roomTitleView.hiddenTopic = !topic.length;
}
}
}
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
if (textField == _roomTitleView.displayNameTextField) {
// "Next" key has been pressed
[_roomTitleView.topicTextField becomeFirstResponder];
} else {
// "Done" key has been pressed
[textField resignFirstResponder];
}
return YES;
}
#pragma mark - Actions
- (IBAction)onButtonPressed:(id)sender {
if (sender == _sendBtn) {
NSString *msgTxt = self.messageTextField.text;
// Handle potential commands in room chat
if ([self isIRCStyleCommand:msgTxt] == NO) {
[self postTextMessage:msgTxt];
}
self.messageTextField.text = nil;
// disable send button
[self onTextFieldChange:nil];
} else if (sender == _optionBtn) {
[self dismissKeyboard];
// Display action menu: Add attachments, Invite user...
__weak typeof(self) weakSelf = self;
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
// Attachments
[self.actionMenu addActionWithTitle:@"Attach" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
// Ask for attachment type
weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an attachment type:" message:nil style:CustomAlertStyleActionSheet];
[weakSelf.actionMenu addActionWithTitle:@"Media" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
// Open media gallery
UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init];
mediaPicker.delegate = weakSelf;
mediaPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
mediaPicker.allowsEditing = NO;
mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
[[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker];
}
}];
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[weakSelf.actionMenu showInViewController:weakSelf];
}
}];
// Invitation
[self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
// Ask for userId to invite
weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"User ID:" message:nil style:CustomAlertStyleAlert];
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[weakSelf.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.placeholder = @"ex: @bob:homeserver";
}];
[weakSelf.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
UITextField *textField = [alert textFieldAtIndex:0];
NSString *userId = textField.text;
weakSelf.actionMenu = nil;
if (userId.length) {
[weakSelf.mxRoom inviteUser:userId success:^{
} failure:^(NSError *error) {
NSLog(@"Invite %@ failed: %@", userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}];
[weakSelf.actionMenu showInViewController:weakSelf];
}
}];
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
weakSelf.actionMenu.sourceView = weakSelf.optionBtn;
[self.actionMenu showInViewController:self];
}
}
- (IBAction)showHideDateTime:(id)sender {
if (dateFormatter) {
// dateTime will be hidden
dateFormatter = nil;
} else {
// dateTime will be visible
NSString *dateFormat = @"MMM dd HH:mm";
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setDateFormat:dateFormat];
}
[self.messagesTableView reloadData];
}
#pragma mark - Post messages
- (void)postMessage:(NSDictionary*)msgContent withLocalEvent:(MXEvent*)localEvent {
MXMessageType msgType = msgContent[@"msgtype"];
if (msgType) {
// Check whether a temporary event has already been added for local echo (this happens on attachments)
RoomMessage *message = nil;
if (localEvent) {
// Update the temporary event with the actual msg content
NSUInteger index = messages.count;
while (index--) {
message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
localEvent.content = msgContent;
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
[message addEvent:localEvent withRoomState:self.mxRoom.state];
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
}
} else {
// Create a new message
message = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:self.mxRoom.state];
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
} else {
[messages removeObjectAtIndex:index];
}
}
break;
}
}
[self.messagesTableView reloadData];
} else {
// Create a temporary event to displayed outgoing message (local echo)
NSString* localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
localEvent = [[MXEvent alloc] init];
localEvent.roomId = self.roomId;
localEvent.eventId = localEventId;
localEvent.eventType = MXEventTypeRoomMessage;
localEvent.type = kMXEventTypeStringRoomMessage;
localEvent.content = msgContent;
localEvent.userId = [MatrixHandler sharedHandler].userId;
localEvent.originServerTs = kMXUndefinedTimestamp;
// Check whether this new event may be grouped with last message
RoomMessage *lastMessage = [messages lastObject];
if (lastMessage == nil || [lastMessage addEvent:localEvent withRoomState:self.mxRoom.state] == NO) {
// Create a new item
lastMessage = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:self.mxRoom.state];
if (lastMessage) {
[messages addObject:lastMessage];
} else {
NSLog(@"ERROR: Unable to add local event: %@", localEvent.description);
}
}
[self.messagesTableView reloadData];
[self scrollToBottomAnimated:NO];
}
// Send message to the room
[self.mxRoom postMessageOfType:msgType content:localEvent.content success:^(NSString *eventId) {
// Check whether this event has already been received from events listener
BOOL isEventAlreadyAddedToRoom = NO;
NSUInteger index = messages.count;
while (index--) {
RoomMessage *message = [messages objectAtIndex:index];
if ([message containsEventId:eventId]) {
isEventAlreadyAddedToRoom = YES;
break;
}
}
// Remove or update the temporary event
index = messages.count;
while (index--) {
RoomMessage *message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
if (isEventAlreadyAddedToRoom == NO) {
// Update the temporary event with the actual event id
localEvent.eventId = eventId;
[message addEvent:localEvent withRoomState:self.mxRoom.state];
}
if (! message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
}
} else {
message = nil;
if (isEventAlreadyAddedToRoom == NO) {
// Create a new message
localEvent.eventId = eventId;
message = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:self.mxRoom.state];
}
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
} else {
[messages removeObjectAtIndex:index];
}
}
break;
}
}
[self.messagesTableView reloadData];
} failure:^(NSError *error) {
[self handleError:error forLocalEvent:localEvent];
}];
}
}
- (void)postTextMessage:(NSString*)msgTxt {
MXMessageType msgType = kMXMessageTypeText;
// Check whether the message is an emote
if ([msgTxt hasPrefix:@"/me "]) {
msgType = kMXMessageTypeEmote;
// Remove "/me " string
msgTxt = [msgTxt substringFromIndex:4];
}
[self postMessage:@{@"msgtype":msgType, @"body":msgTxt} withLocalEvent:nil];
}
- (MXEvent*)addLocalEventForAttachedImage:(UIImage*)image {
// Create a temporary event to displayed outgoing message (local echo)
NSString *localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
MXEvent *mxEvent = [[MXEvent alloc] init];
mxEvent.roomId = self.roomId;
mxEvent.eventId = localEventId;
mxEvent.eventType = MXEventTypeRoomMessage;
mxEvent.type = kMXEventTypeStringRoomMessage;
mxEvent.originServerTs = kMXUndefinedTimestamp;
// We store temporarily the image in cache, use the localId to build temporary url
NSString *dummyURL = [NSString stringWithFormat:@"%@%@", kMediaManagerPrefixForDummyURL, localEventId];
NSData *imageData = UIImageJPEGRepresentation(image, 0.5);
NSString *cacheFilePath = [MediaManager cacheMediaData:imageData forURL:dummyURL mimeType:@"image/jpeg"];
if (cacheFilePath) {
if (tmpCachedAttachments == nil) {
tmpCachedAttachments = [NSMutableArray array];
}
[tmpCachedAttachments addObject:cacheFilePath];
}
NSMutableDictionary *thumbnailInfo = [[NSMutableDictionary alloc] init];
[thumbnailInfo setValue:@"image/jpeg" forKey:@"mimetype"];
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.width] forKey:@"w"];
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.height] forKey:@"h"];
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:imageData.length] forKey:@"size"];
mxEvent.content = @{@"msgtype":@"m.image", @"thumbnail_info":thumbnailInfo, @"thumbnail_url":dummyURL, @"url":dummyURL, @"info":thumbnailInfo};
mxEvent.userId = [MatrixHandler sharedHandler].userId;
// Update table sources
RoomMessage *message = [[RoomMessage alloc] initWithEvent:mxEvent andRoomState:self.mxRoom.state];
if (message) {
[messages addObject:message];
} else {
NSLog(@"ERROR: Unable to add local event for attachment: %@", mxEvent.description);
}
[self.messagesTableView reloadData];
[self scrollToBottomAnimated:NO];
return mxEvent;
}
- (void)handleError:(NSError *)error forLocalEvent:(MXEvent *)localEvent {
NSLog(@"Post message failed: %@", error);
if (error) {
// Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
// Update the temporary event with this local event id
NSUInteger index = messages.count;
while (index--) {
RoomMessage *message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
NSLog(@"Posted event: %@", localEvent.description);
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
localEvent.eventId = kFailedEventId;
[message addEvent:localEvent withRoomState:self.mxRoom.state];
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
}
} else {
// Create a new message
localEvent.eventId = kFailedEventId;
message = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:self.mxRoom.state];
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
} else {
[messages removeObjectAtIndex:index];
}
}
break;
}
}
[self.messagesTableView reloadData];
}
- (BOOL)isIRCStyleCommand:(NSString*)text{
// Check whether the provided text may be an IRC-style command
if ([text hasPrefix:@"/"] == NO || [text hasPrefix:@"//"] == YES) {
return NO;
}
// Parse command line
NSArray *components = [text componentsSeparatedByString:@" "];
NSString *cmd = [components objectAtIndex:0];
NSUInteger index = 1;
if ([cmd isEqualToString:kCmdEmote]) {
// post message as an emote
[self postTextMessage:text];
} else if ([text hasPrefix:kCmdChangeDisplayName]) {
// Change display name
NSString *displayName = [text substringFromIndex:kCmdChangeDisplayName.length + 1];
// Remove white space from both ends
displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (displayName.length) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxRestClient setDisplayName:displayName success:^{
} failure:^(NSError *error) {
NSLog(@"Set displayName failed: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /nick <display_name>";
}
} else if ([text hasPrefix:kCmdJoinRoom]) {
// Join a room
NSString *roomAlias = [text substringFromIndex:kCmdJoinRoom.length + 1];
// Remove white space from both ends
roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// Check
if (roomAlias.length) {
// FIXME
NSLog(@"Join Alias is not supported yet (%@)", text);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/join is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /join <room_alias>";
}
} else {
// Retrieve userId
NSString *userId = nil;
while (index < components.count) {
userId = [components objectAtIndex:index++];
if (userId.length) {
// done
break;
}
// reset
userId = nil;
}
if ([cmd isEqualToString:kCmdKickUser]) {
if (userId) {
// Retrieve potential reason
NSString *reason = nil;
while (index < components.count) {
if (reason) {
reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
} else {
reason = [components objectAtIndex:index++];
}
}
// Kick the user
[self.mxRoom kickUser:userId reason:reason success:^{
} failure:^(NSError *error) {
NSLog(@"Kick user (%@) failed: %@", userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /kick <userId> [<reason>]";
}
} else if ([cmd isEqualToString:kCmdBanUser]) {
if (userId) {
// Retrieve potential reason
NSString *reason = nil;
while (index < components.count) {
if (reason) {
reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
} else {
reason = [components objectAtIndex:index++];
}
}
// Ban the user
[self.mxRoom banUser:userId reason:reason success:^{
} failure:^(NSError *error) {
NSLog(@"Ban user (%@) failed: %@", userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /ban <userId> [<reason>]";
}
} else if ([cmd isEqualToString:kCmdUnbanUser]) {
if (userId) {
// Unban the user
[self.mxRoom unbanUser:userId success:^{
} failure:^(NSError *error) {
NSLog(@"Unban user (%@) failed: %@", userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /unban <userId>";
}
} else if ([cmd isEqualToString:kCmdSetUserPowerLevel]) {
// Retrieve power level
NSString *powerLevel = nil;
while (index < components.count) {
powerLevel = [components objectAtIndex:index++];
if (powerLevel.length) {
// done
break;
}
// reset
powerLevel = nil;
}
// Set power level
if (userId && powerLevel) {
// FIXME
NSLog(@"Set user power level (/op) is not supported yet (%@)", userId);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/op is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /op <userId> <power level>";
}
} else if ([cmd isEqualToString:kCmdResetUserPowerLevel]) {
if (userId) {
// Reset user power level
// FIXME
NSLog(@"Reset user power level (/deop) is not supported yet (%@)", userId);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/deop is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
} else {
// Display cmd usage in text input as placeholder
self.messageTextField.placeholder = @"Usage: /deop <userId>";
}
} else {
NSLog(@"Unrecognised IRC-style command: %@", text);
self.messageTextField.placeholder = [NSString stringWithFormat:@"Unrecognised IRC-style command: %@", cmd];
}
}
return YES;
}
# pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
if (selectedImage) {
MXEvent *localEvent = [self addLocalEventForAttachedImage:selectedImage];
// Upload image and its thumbnail
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSUInteger thumbnailSize = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
[mxHandler.mxRestClient uploadImage:selectedImage thumbnailSize:thumbnailSize timeout:30 success:^(NSDictionary *imageMessage) {
// Send image
[self postMessage:imageMessage withLocalEvent:localEvent];
} failure:^(NSError *error) {
[self handleError:error forLocalEvent:localEvent];
}];
}
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
NSURL* selectedVideo = [info objectForKey:UIImagePickerControllerMediaURL];
// Check the selected video, and ignore multiple calls (observed when user pressed several time Choose button)
if (selectedVideo && !tmpVideoPlayer) {
// Create video thumbnail
tmpVideoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:selectedVideo];
if (tmpVideoPlayer) {
[tmpVideoPlayer setShouldAutoplay:NO];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerThumbnailImageRequestDidFinishNotification:)
name:MPMoviePlayerThumbnailImageRequestDidFinishNotification
object:nil];
[tmpVideoPlayer requestThumbnailImagesAtTimes:@[@1.0f] timeOption:MPMovieTimeOptionNearestKeyFrame];
// We will finalize video attachment when thumbnail will be available (see movie player callback)
return;
}
}
}
[self dismissMediaPicker];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[self dismissMediaPicker];
}
- (void)dismissMediaPicker {
[[AppDelegate theDelegate].masterTabBarController dismissMediaPicker];
}
@end