2014-11-24 09:38:23 +00:00
|
|
|
/*
|
|
|
|
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"
|
2014-12-23 17:51:49 +00:00
|
|
|
#import "MemberViewController.h"
|
2014-11-27 09:50:09 +00:00
|
|
|
#import "RoomMessage.h"
|
2014-11-24 09:38:23 +00:00
|
|
|
#import "RoomMessageTableCell.h"
|
|
|
|
#import "RoomMemberTableCell.h"
|
2014-12-15 15:54:31 +00:00
|
|
|
#import "RoomTitleView.h"
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-23 14:36:05 +00:00
|
|
|
#import "MatrixSDKHandler.h"
|
2014-11-24 09:38:23 +00:00
|
|
|
#import "AppDelegate.h"
|
|
|
|
#import "AppSettings.h"
|
|
|
|
|
|
|
|
#import "MediaManager.h"
|
2015-01-23 12:46:27 +00:00
|
|
|
#import "MXCTools.h"
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
#import "MXCGrowingTextView.h"
|
2015-01-15 15:18:12 +00:00
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
#import "EventDetailsView.h"
|
|
|
|
|
2015-01-13 15:07:34 +00:00
|
|
|
#define ROOMVIEWCONTROLLER_TYPING_TIMEOUT_SEC 10
|
2015-01-07 10:19:59 +00:00
|
|
|
|
2014-12-04 15:33:31 +00:00
|
|
|
#define ROOMVIEWCONTROLLER_UPLOAD_FILE_SIZE 5000000
|
2015-01-15 12:17:00 +00:00
|
|
|
|
2014-12-04 15:33:31 +00:00
|
|
|
#define ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE 20
|
2015-01-15 12:17:00 +00:00
|
|
|
#define ROOMVIEWCONTROLLER_BACK_PAGINATION_MAX_SCROLLING_OFFSET 100
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-11-28 18:23:51 +00:00
|
|
|
#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
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-12-10 17:49:04 +00:00
|
|
|
#define ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW 120 // (51 + 69)
|
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
#define ROOM_MESSAGE_CELL_TEXTVIEW_TAG 1
|
|
|
|
#define ROOM_MESSAGE_CELL_ATTACHMENTVIEW_TAG 2
|
|
|
|
#define ROOM_MESSAGE_CELL_PROGRESSVIEW_TAG 3
|
|
|
|
#define ROOM_MESSAGE_CELL_HIDDEN_UNSENT_MSG_LABEL_TAG 4
|
2015-01-12 16:13:40 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
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;
|
2015-02-17 09:01:55 +00:00
|
|
|
BOOL isScrollingToBottom;
|
2015-01-13 15:07:34 +00:00
|
|
|
|
|
|
|
// Typing notification
|
|
|
|
NSDate *lastTypingDate;
|
|
|
|
NSTimer* typingTimer;
|
2015-01-13 17:32:52 +00:00
|
|
|
id typingNotifListener;
|
2015-01-16 15:39:58 +00:00
|
|
|
NSArray *currentTypingUsers;
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Back pagination
|
|
|
|
BOOL isBackPaginationInProgress;
|
2014-12-16 20:33:45 +00:00
|
|
|
BOOL isFirstPagination;
|
2014-12-04 15:33:31 +00:00
|
|
|
NSUInteger backPaginationAddedMsgNb;
|
2014-12-16 20:33:45 +00:00
|
|
|
NSUInteger backPaginationHandledEventsNb;
|
2015-02-17 09:54:09 +00:00
|
|
|
MXHTTPOperation *backPaginationOperation;
|
2015-01-15 12:17:00 +00:00
|
|
|
CGFloat backPaginationSavedFirstMsgHeight;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Members list
|
|
|
|
NSArray *members;
|
|
|
|
id membersListener;
|
|
|
|
|
|
|
|
// Attachment handling
|
2015-01-23 12:46:27 +00:00
|
|
|
MXCImageView *highResImageView;
|
2014-11-24 09:38:23 +00:00
|
|
|
NSString *AVAudioSessionCategory;
|
|
|
|
MPMoviePlayerController *videoPlayer;
|
2014-12-11 15:41:21 +00:00
|
|
|
MPMoviePlayerController *tmpVideoPlayer;
|
2015-01-12 10:00:53 +00:00
|
|
|
NSString *selectedVideoURL;
|
|
|
|
NSString *selectedVideoCachePath;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-12-16 14:01:31 +00:00
|
|
|
// used to trap the slide to close the keyboard
|
|
|
|
UIView* inputAccessoryView;
|
|
|
|
BOOL isKeyboardObserver;
|
2015-01-14 14:17:20 +00:00
|
|
|
BOOL isKeyboardDisplayed;
|
2015-01-22 16:31:44 +00:00
|
|
|
CGFloat keyboardHeight;
|
2014-12-16 14:01:31 +00:00
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
// default contraint values
|
|
|
|
CGFloat defaultMessagesTableViewBottomConstraint;
|
|
|
|
CGFloat defaultControlViewBottomConstraint;
|
|
|
|
|
|
|
|
// save the last edited text
|
|
|
|
// do not send unexpected typing events
|
|
|
|
// HPGrowingTextView triggers growingTextViewDidChange event when it recomposes itself
|
|
|
|
NSString* lastEditedText;
|
|
|
|
|
2014-11-28 09:51:22 +00:00
|
|
|
// Date formatter (nil if dateTime info is hidden)
|
2014-11-24 09:38:23 +00:00
|
|
|
NSDateFormatter *dateFormatter;
|
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
// Local echo
|
|
|
|
NSMutableArray *pendingOutgoingEvents;
|
2014-11-24 09:38:23 +00:00
|
|
|
NSMutableArray *tmpCachedAttachments;
|
2015-01-28 10:30:15 +00:00
|
|
|
|
|
|
|
// the user taps on a member thumbnail
|
|
|
|
MXRoomMember *selectedRoomMember;
|
2015-02-18 17:29:32 +00:00
|
|
|
|
|
|
|
// Reachability observer
|
|
|
|
id reachabilityObserver;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@property (weak, nonatomic) IBOutlet UINavigationItem *roomNavItem;
|
2015-02-23 14:46:57 +00:00
|
|
|
@property (strong, nonatomic) IBOutlet UIBarButtonItem *showRoomMembersButtonItem;
|
2014-12-15 15:54:31 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet RoomTitleView *roomTitleView;
|
2015-02-23 14:46:57 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet UITableView *messagesTableView;
|
|
|
|
@property (weak, nonatomic) IBOutlet UIView *controlView;
|
|
|
|
@property (weak, nonatomic) IBOutlet UIButton *optionBtn;
|
2015-01-23 12:46:27 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet MXCGrowingTextView *messageTextView;
|
2015-02-16 09:37:17 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageTextViewTopConstraint;
|
|
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageTextViewBottomConstraint;
|
2014-11-24 09:38:23 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet UIButton *sendBtn;
|
2015-01-14 16:51:53 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messagesTableViewBottomConstraint;
|
2014-11-24 09:38:23 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *controlViewBottomConstraint;
|
2015-01-14 16:51:53 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *controlViewHeightConstraint;
|
2014-11-24 09:38:23 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
|
|
|
|
@property (weak, nonatomic) IBOutlet UIView *membersView;
|
|
|
|
@property (weak, nonatomic) IBOutlet UITableView *membersTableView;
|
2015-02-03 14:34:32 +00:00
|
|
|
@property (weak, nonatomic) IBOutlet EventDetailsView *eventDetailsView;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-12-11 17:10:15 +00:00
|
|
|
@property (strong, nonatomic) MXRoom *mxRoom;
|
2015-01-23 12:46:27 +00:00
|
|
|
@property (strong, nonatomic) MXCAlert *actionMenu;
|
|
|
|
@property (strong, nonatomic) MXCImageView* imageValidationView;
|
2015-01-13 16:53:31 +00:00
|
|
|
|
|
|
|
// Messages
|
|
|
|
@property (strong, nonatomic)NSMutableArray *messages;
|
|
|
|
@property (strong, nonatomic)id messagesListener;
|
2015-01-30 17:47:43 +00:00
|
|
|
@property (strong, nonatomic)id redactionListener;
|
2014-11-24 09:38:23 +00:00
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation RoomViewController
|
2015-01-30 17:47:43 +00:00
|
|
|
@synthesize messages;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
[super viewDidLoad];
|
2015-02-23 14:46:57 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Do any additional setup after loading the view, typically from a nib.
|
|
|
|
forceScrollToBottomOnViewDidAppear = YES;
|
2014-12-04 10:43:43 +00:00
|
|
|
// Hide messages table by default in order to hide initial scrolling to the bottom
|
|
|
|
self.messagesTableView.hidden = YES;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Add tap detection on members view in order to hide members when the user taps outside members list
|
2015-02-23 14:46:57 +00:00
|
|
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideRoomMembers:)];
|
2014-11-24 09:38:23 +00:00
|
|
|
[tap setNumberOfTouchesRequired:1];
|
|
|
|
[tap setNumberOfTapsRequired:1];
|
|
|
|
[tap setDelegate:self];
|
|
|
|
[self.membersView addGestureRecognizer:tap];
|
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
// Add shadow on event details view
|
|
|
|
_eventDetailsView.layer.cornerRadius = 5;
|
|
|
|
_eventDetailsView.layer.shadowOffset = CGSizeMake(0, 1);
|
|
|
|
_eventDetailsView.layer.shadowOpacity = 0.5f;
|
|
|
|
|
2014-12-16 14:01:31 +00:00
|
|
|
isKeyboardObserver = NO;
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
_sendBtn.enabled = NO;
|
|
|
|
_sendBtn.alpha = 0.5;
|
2014-12-16 14:01:31 +00:00
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
pendingOutgoingEvents = [NSMutableArray array];
|
2014-12-16 14:01:31 +00:00
|
|
|
|
|
|
|
// add an input to check if the keyboard is hiding with sliding it
|
|
|
|
inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.internalTextView.inputAccessoryView = inputAccessoryView;
|
2014-12-18 13:36:55 +00:00
|
|
|
|
|
|
|
// ensure that the titleView will be scaled when it will be required
|
|
|
|
// during a screen rotation for example.
|
|
|
|
self.roomTitleView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
2015-01-14 14:17:20 +00:00
|
|
|
|
2015-02-12 16:07:24 +00:00
|
|
|
// set text input font
|
|
|
|
self.messageTextView.font = [UIFont systemFontOfSize:14];
|
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
// default contraint values
|
|
|
|
defaultMessagesTableViewBottomConstraint = _messagesTableViewBottomConstraint.constant;
|
|
|
|
defaultControlViewBottomConstraint = _controlViewBottomConstraint.constant;
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
// draw a rounded border around the textView
|
|
|
|
self.messageTextView.layer.cornerRadius = 5;
|
|
|
|
self.messageTextView.layer.borderWidth = 1;
|
|
|
|
self.messageTextView.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
|
|
|
self.messageTextView.clipsToBounds = YES;
|
2015-01-15 15:18:12 +00:00
|
|
|
self.messageTextView.backgroundColor = [UIColor whiteColor];
|
2015-01-22 17:09:33 +00:00
|
|
|
// on IOS 8, the growing textview animation could trigger weird UI animations
|
|
|
|
// indeed, the messages tableView can be refreshed while its height is updated (e.g. when setting a message)
|
|
|
|
self.messageTextView.animateHeightChange = NO;
|
2015-01-14 16:51:53 +00:00
|
|
|
lastEditedText = self.messageTextView.text;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dealloc {
|
2015-01-13 15:07:34 +00:00
|
|
|
lastTypingDate = nil;
|
|
|
|
[typingTimer invalidate];
|
|
|
|
typingTimer = nil;
|
2015-01-13 17:32:52 +00:00
|
|
|
if (typingNotifListener) {
|
|
|
|
[self.mxRoom removeListener:typingNotifListener];
|
|
|
|
typingNotifListener = nil;
|
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
currentTypingUsers = nil;
|
2015-01-07 10:19:59 +00:00
|
|
|
|
2015-02-18 17:29:32 +00:00
|
|
|
if (reachabilityObserver) {
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
|
|
|
|
reachabilityObserver = nil;
|
|
|
|
}
|
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
// Release local echo resources
|
|
|
|
pendingOutgoingEvents = nil;
|
2014-11-24 09:38:23 +00:00
|
|
|
NSUInteger index = tmpCachedAttachments.count;
|
|
|
|
NSError *error = nil;
|
|
|
|
while (index--) {
|
|
|
|
if (![[NSFileManager defaultManager] removeItemAtPath:[tmpCachedAttachments objectAtIndex:index] error:&error]) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Failed to delete cached media: %@", error);
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
tmpCachedAttachments = nil;
|
|
|
|
|
|
|
|
[self hideAttachmentView];
|
|
|
|
|
|
|
|
messages = nil;
|
2015-01-30 17:47:43 +00:00
|
|
|
if (_messagesListener) {
|
|
|
|
[self.mxRoom removeListener:_messagesListener];
|
|
|
|
_messagesListener = nil;
|
|
|
|
[self.mxRoom removeListener:_redactionListener];
|
|
|
|
_redactionListener = nil;
|
2015-02-20 13:43:11 +00:00
|
|
|
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideRedactions"];
|
2015-01-28 17:39:23 +00:00
|
|
|
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedEvents"];
|
2015-02-12 10:16:28 +00:00
|
|
|
[[MatrixSDKHandler sharedHandler] removeObserver:self forKeyPath:@"isActivityInProgress"];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-12-11 17:10:15 +00:00
|
|
|
self.mxRoom = nil;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-12-22 10:13:23 +00:00
|
|
|
if (backPaginationOperation) {
|
|
|
|
[backPaginationOperation cancel];
|
|
|
|
backPaginationOperation = nil;
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
members = nil;
|
|
|
|
if (membersListener) {
|
|
|
|
membersListener = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self.actionMenu) {
|
|
|
|
[self.actionMenu dismiss:NO];
|
|
|
|
self.actionMenu = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dateFormatter) {
|
|
|
|
dateFormatter = nil;
|
|
|
|
}
|
2014-12-16 14:01:31 +00:00
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
if (_messageTextView) {
|
|
|
|
|
|
|
|
_messageTextView.internalTextView.inputAccessoryView = inputAccessoryView = nil;
|
|
|
|
_messageTextView.delegate = nil;
|
|
|
|
_messageTextView = nil;
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (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
|
2014-12-09 16:09:51 +00:00
|
|
|
[self startActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2014-12-24 15:32:24 +00:00
|
|
|
// Register a listener for events that concern room members
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2014-12-24 15:32:24 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-01-28 17:39:23 +00:00
|
|
|
// Check whether no text field is editing before refreshing title view
|
|
|
|
if (!self.roomTitleView.isEditing) {
|
|
|
|
[self.roomTitleView refreshDisplay];
|
|
|
|
}
|
2014-12-24 15:32:24 +00:00
|
|
|
|
|
|
|
// refresh the
|
|
|
|
if (members.count > 0) {
|
|
|
|
// Hide potential action sheet
|
|
|
|
if (self.actionMenu) {
|
|
|
|
[self.actionMenu dismiss:NO];
|
|
|
|
self.actionMenu = nil;
|
|
|
|
}
|
|
|
|
// Refresh members list
|
|
|
|
[self updateRoomMembers];
|
|
|
|
[self.membersTableView reloadData];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.delegate = self;
|
2015-01-16 09:50:15 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
2014-12-22 10:52:29 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
|
|
[super viewWillDisappear:animated];
|
|
|
|
|
2015-02-16 09:37:17 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// hide action
|
|
|
|
if (self.actionMenu) {
|
|
|
|
[self.actionMenu dismiss:NO];
|
|
|
|
self.actionMenu = nil;
|
|
|
|
}
|
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
// Hide event details by default
|
|
|
|
[self hideEventDetails];
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Hide members by default
|
2015-02-23 14:46:57 +00:00
|
|
|
[self hideRoomMembers:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.delegate = nil;
|
2015-02-16 09:37:17 +00:00
|
|
|
// Store the potential message partially typed in text input
|
|
|
|
[mxHandler storePartialTextMessage:self.messageTextView.text forRoomId:self.roomId];
|
2015-01-14 14:17:20 +00:00
|
|
|
|
2014-12-16 14:01:31 +00:00
|
|
|
// slide to hide keyboard management
|
|
|
|
if (isKeyboardObserver) {
|
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"frame"];
|
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"center"];
|
|
|
|
isKeyboardObserver = NO;
|
|
|
|
}
|
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
[self dismissAttachmentImageViews];
|
2014-12-22 10:52:29 +00:00
|
|
|
|
2014-12-24 15:32:24 +00:00
|
|
|
if (membersListener) {
|
|
|
|
[mxHandler.mxSession removeListener:membersListener];
|
|
|
|
membersListener = nil;
|
|
|
|
}
|
|
|
|
|
2015-01-27 14:42:04 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
2014-12-22 10:52:29 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)viewDidAppear:(BOOL)animated {
|
|
|
|
[super viewDidAppear:animated];
|
|
|
|
|
2014-12-09 09:39:56 +00:00
|
|
|
// Set visible room id
|
|
|
|
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId;
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
if (forceScrollToBottomOnViewDidAppear) {
|
2014-12-16 09:05:37 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2014-12-22 10:55:28 +00:00
|
|
|
// Scroll to the bottom
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:animated];
|
2014-12-16 09:05:37 +00:00
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
forceScrollToBottomOnViewDidAppear = NO;
|
2014-12-04 10:43:43 +00:00
|
|
|
self.messagesTableView.hidden = NO;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-12-24 10:10:41 +00:00
|
|
|
|
2015-01-22 10:09:14 +00:00
|
|
|
[self updateUI];
|
2015-02-16 15:20:33 +00:00
|
|
|
|
|
|
|
// Retrieve the potential message partially typed during last room display.
|
|
|
|
// Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
|
|
|
|
self.messageTextView.text = [[MatrixSDKHandler sharedHandler] partialTextMessageForRoomId:self.roomId];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2014-12-09 09:39:56 +00:00
|
|
|
- (void)viewDidDisappear:(BOOL)animated {
|
|
|
|
[super viewDidDisappear:animated];
|
|
|
|
|
|
|
|
// Reset visible room id
|
|
|
|
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = nil;
|
|
|
|
}
|
|
|
|
|
2015-01-20 11:41:27 +00:00
|
|
|
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
|
|
|
|
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
|
|
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(coordinator.transitionDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
if (!isKeyboardDisplayed) {
|
|
|
|
[self updateMessageTextViewFrame];
|
|
|
|
}
|
|
|
|
// Cell width will be updated, force table refresh to take into account changes of message components
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// The 2 following methods are deprecated since iOS 8
|
|
|
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
|
|
|
|
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
|
|
|
|
|
|
|
|
// Cell width will be updated, force table refresh to take into account changes of message components
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
});
|
|
|
|
}
|
2015-01-16 09:50:15 +00:00
|
|
|
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
|
|
|
|
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
|
|
|
|
|
|
|
|
if (!isKeyboardDisplayed) {
|
|
|
|
[self updateMessageTextViewFrame];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-22 10:52:29 +00:00
|
|
|
- (void)onAppDidEnterBackground {
|
2015-01-23 12:46:27 +00:00
|
|
|
[self dismissAttachmentImageViews];
|
2014-12-22 10:52:29 +00:00
|
|
|
}
|
|
|
|
|
2015-01-22 10:09:14 +00:00
|
|
|
- (void)updateUI {
|
|
|
|
// Check whether a room is selected to show/hide UI items
|
|
|
|
if (self.mxRoom) {
|
|
|
|
self.controlView.hidden = NO;
|
|
|
|
// Check room members to enable/disable members button in nav bar
|
2015-02-23 14:46:57 +00:00
|
|
|
self.showRoomMembersButtonItem.enabled = ([self.mxRoom.state members].count != 0);
|
2015-01-22 10:09:14 +00:00
|
|
|
} else {
|
|
|
|
self.controlView.hidden = YES;
|
2015-02-23 14:46:57 +00:00
|
|
|
self.showRoomMembersButtonItem.enabled = NO;
|
2015-01-22 16:26:51 +00:00
|
|
|
_activityIndicator.hidden = YES;
|
2015-01-22 10:09:14 +00:00
|
|
|
}
|
|
|
|
[self.roomTitleView refreshDisplay];
|
|
|
|
}
|
|
|
|
|
2015-01-28 10:30:15 +00:00
|
|
|
- (void)addPictureViewTapGesture:(RoomMessageTableCell*)cell {
|
|
|
|
if (!cell.pictureView.hidden) {
|
|
|
|
UITapGestureRecognizer* tapGesture = nil;
|
|
|
|
|
|
|
|
// check if it is already defined
|
|
|
|
// gesture in storyboard does not seem to work properly
|
|
|
|
// it always triggers a tap event on the first cell
|
|
|
|
for (UIGestureRecognizer* gesture in cell.pictureView.gestureRecognizers) {
|
|
|
|
|
|
|
|
if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) {
|
|
|
|
tapGesture = (UITapGestureRecognizer*)gesture;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add it if it is not yet defined
|
|
|
|
if (!tapGesture) {
|
|
|
|
tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onContactThumbnailTap:)];
|
|
|
|
[tapGesture setNumberOfTouchesRequired:1];
|
|
|
|
[tapGesture setNumberOfTapsRequired:1];
|
|
|
|
[tapGesture setDelegate:self];
|
|
|
|
[cell.pictureView addGestureRecognizer:tapGesture];
|
|
|
|
cell.pictureView.userInteractionEnabled = YES;
|
|
|
|
|
|
|
|
// ensure that nothing will hide this view
|
|
|
|
[cell.pictureView.superview bringSubviewToFront:cell.pictureView];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-30 17:47:43 +00:00
|
|
|
#pragma mark -
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
- (void)setRoomId:(NSString *)roomId {
|
|
|
|
if ([self.roomId isEqualToString:roomId] == NO) {
|
|
|
|
_roomId = roomId;
|
2015-01-30 17:47:43 +00:00
|
|
|
[self forceRefresh];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-30 17:47:43 +00:00
|
|
|
- (void)forceRefresh {
|
|
|
|
// Reload room data here
|
|
|
|
[self configureView];
|
|
|
|
// Update UI
|
|
|
|
[self updateUI];
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
#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;
|
|
|
|
}
|
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
- (IBAction)onLongPressGesture:(UILongPressGestureRecognizer*)longPressGestureRecognizer {
|
|
|
|
if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {
|
|
|
|
// Find out the related RoomMessageTableCell
|
|
|
|
RoomMessageTableCell *messageTableCell = nil;
|
|
|
|
UIView* view = longPressGestureRecognizer.view;
|
|
|
|
while (view) {
|
|
|
|
if ([view isKindOfClass:[RoomMessageTableCell class]]) {
|
|
|
|
messageTableCell = (RoomMessageTableCell*)view;
|
|
|
|
break;
|
|
|
|
}
|
2015-01-08 14:40:05 +00:00
|
|
|
view = view.superview;
|
|
|
|
}
|
|
|
|
|
2015-02-03 14:34:32 +00:00
|
|
|
if (!messageTableCell) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
view = longPressGestureRecognizer.view;
|
|
|
|
|
|
|
|
// Check the view on which long press has been detected
|
|
|
|
if (view.tag == ROOM_MESSAGE_CELL_PROGRESSVIEW_TAG) {
|
|
|
|
NSString* url = messageTableCell.message.attachmentURL;
|
2015-01-19 13:33:57 +00:00
|
|
|
MediaLoader *loader = [MediaManager existingDownloaderForURL:url inFolder:self.roomId];
|
2015-01-08 14:40:05 +00:00
|
|
|
|
2015-01-09 10:33:32 +00:00
|
|
|
// offer to cancel a download only if there is a pending one
|
|
|
|
if (loader) {
|
2015-02-03 14:34:32 +00:00
|
|
|
__weak typeof(self) weakSelf = self;
|
2015-01-23 12:46:27 +00:00
|
|
|
self.actionMenu = [[MXCAlert alloc] initWithTitle:nil message:@"Cancel the download ?" style:MXCAlertStyleAlert];
|
|
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2015-01-09 10:33:32 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
2015-01-23 12:46:27 +00:00
|
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2015-01-09 10:33:32 +00:00
|
|
|
|
|
|
|
// get again the loader, the cell could have been reused.
|
2015-01-19 13:33:57 +00:00
|
|
|
MediaLoader *loader = [MediaManager existingDownloaderForURL:url inFolder:weakSelf.roomId];
|
2015-01-09 10:33:32 +00:00
|
|
|
if (loader) {
|
|
|
|
[loader cancel];
|
|
|
|
}
|
2015-02-03 14:34:32 +00:00
|
|
|
|
2015-01-09 10:33:32 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
|
|
|
|
|
|
|
[self.actionMenu showInViewController:self];
|
|
|
|
}
|
2015-02-03 14:34:32 +00:00
|
|
|
} else if (view.tag == ROOM_MESSAGE_CELL_TEXTVIEW_TAG || view.tag == ROOM_MESSAGE_CELL_ATTACHMENTVIEW_TAG) {
|
|
|
|
RoomMessage *message = messageTableCell.message;
|
|
|
|
MXEvent *selectedEvent = nil;
|
|
|
|
if (message.components.count == 1) {
|
|
|
|
RoomMessageComponent *component = [message.components objectAtIndex:0];
|
|
|
|
selectedEvent = component.event;
|
|
|
|
} else if (message.components.count) {
|
|
|
|
// Here the selected view is a textView (attachment has no more than one component)
|
|
|
|
|
|
|
|
// Look for the selected component
|
|
|
|
CGPoint longPressPoint = [longPressGestureRecognizer locationInView:view];
|
|
|
|
CGFloat yPosition = ROOM_MESSAGE_TEXTVIEW_MARGIN;
|
|
|
|
RoomMessageComponent *component = [message.components objectAtIndex:0];
|
|
|
|
selectedEvent = component.event;
|
2015-02-03 14:54:19 +00:00
|
|
|
[message checkComponentsHeight];
|
2015-02-03 14:34:32 +00:00
|
|
|
for (component in message.components) {
|
|
|
|
if (longPressPoint.y < yPosition) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
yPosition += component.height;
|
|
|
|
selectedEvent = component.event;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selectedEvent) {
|
2015-02-19 12:08:43 +00:00
|
|
|
// Check status of the selected event
|
|
|
|
if ([selectedEvent.eventId hasPrefix:kFailedEventIdPrefix]) {
|
|
|
|
// The user may want to resend it
|
|
|
|
[self promptUserToResendEvent:selectedEvent.eventId];
|
|
|
|
} else if (! [selectedEvent.eventId hasPrefix:kLocalEchoEventIdPrefix]) {
|
|
|
|
// Display event details
|
|
|
|
[self showEventDetails:selectedEvent];
|
|
|
|
}
|
2015-02-03 14:34:32 +00:00
|
|
|
}
|
2015-01-08 14:40:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-28 10:30:15 +00:00
|
|
|
- (IBAction)onContactThumbnailTap:(UITapGestureRecognizer*)sender {
|
|
|
|
UIView* view = sender.view;
|
|
|
|
|
|
|
|
while (view && ![view isKindOfClass:[RoomMessageTableCell class]]) {
|
|
|
|
view = view.superview;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([view isKindOfClass:[RoomMessageTableCell class]]) {
|
|
|
|
NSIndexPath *indexPath = [self.messagesTableView indexPathForCell:(RoomMessageTableCell*)view];
|
|
|
|
RoomMessage* message = nil;
|
|
|
|
|
|
|
|
@synchronized(self) {
|
|
|
|
if (indexPath.row < messages.count) {
|
|
|
|
message = [messages objectAtIndex:indexPath.row];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message) {
|
|
|
|
selectedRoomMember = [self.mxRoom.state memberWithUserId:message.senderId];
|
|
|
|
|
|
|
|
if (selectedRoomMember) {
|
|
|
|
[self performSegueWithIdentifier:@"showMemberSheet" sender:self];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-08 14:40:05 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
#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
|
2014-12-09 16:09:51 +00:00
|
|
|
[self startActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-13 17:32:52 +00:00
|
|
|
if (self.mxRoom) {
|
|
|
|
// Remove potential listener
|
2015-01-30 17:47:43 +00:00
|
|
|
if (_messagesListener){
|
|
|
|
[self.mxRoom removeListener:_messagesListener];
|
|
|
|
_messagesListener = nil;
|
|
|
|
[self.mxRoom removeListener:_redactionListener];
|
|
|
|
_redactionListener = nil;
|
2015-02-20 13:43:11 +00:00
|
|
|
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideRedactions"];
|
2015-01-28 17:39:23 +00:00
|
|
|
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedEvents"];
|
2015-02-12 10:16:28 +00:00
|
|
|
[[MatrixSDKHandler sharedHandler] removeObserver:self forKeyPath:@"isActivityInProgress"];
|
2015-01-13 17:32:52 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
currentTypingUsers = nil;
|
2015-01-13 17:32:52 +00:00
|
|
|
if (typingNotifListener) {
|
|
|
|
[self.mxRoom removeListener:typingNotifListener];
|
2015-02-18 17:29:32 +00:00
|
|
|
typingNotifListener = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reachabilityObserver) {
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
|
|
|
|
reachabilityObserver = nil;
|
2015-01-13 17:32:52 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
// The whole room history is flushed here to rebuild it from the current instant (live)
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
|
|
|
messages = nil;
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Disable room title edition
|
2014-12-15 15:54:31 +00:00
|
|
|
self.roomTitleView.editable = NO;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Update room data
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2014-12-11 17:10:15 +00:00
|
|
|
self.mxRoom = nil;
|
2014-11-24 09:38:23 +00:00
|
|
|
if (self.roomId) {
|
2014-12-11 17:10:15 +00:00
|
|
|
self.mxRoom = [mxHandler.mxSession roomWithRoomId:self.roomId];
|
2014-12-08 10:27:36 +00:00
|
|
|
}
|
2014-12-11 17:10:15 +00:00
|
|
|
if (self.mxRoom) {
|
2014-11-24 09:38:23 +00:00
|
|
|
// Check first whether we have to join the room
|
2014-12-11 17:10:15 +00:00
|
|
|
if (self.mxRoom.state.membership == MXMembershipInvite) {
|
2014-11-24 09:38:23 +00:00
|
|
|
isJoinRequestInProgress = YES;
|
2014-12-09 16:09:51 +00:00
|
|
|
[self startActivityIndicator];
|
2014-12-11 17:10:15 +00:00
|
|
|
[self.mxRoom join:^{
|
2014-12-09 16:09:51 +00:00
|
|
|
[self stopActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
isJoinRequestInProgress = NO;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self configureView];
|
|
|
|
});
|
|
|
|
} failure:^(NSError *error) {
|
2014-12-09 16:09:51 +00:00
|
|
|
[self stopActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
isJoinRequestInProgress = NO;
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Failed to join room (%@): %@", self.mxRoom.state.displayname, error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enable room title edition
|
2014-12-15 15:54:31 +00:00
|
|
|
self.roomTitleView.editable = YES;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
|
|
|
messages = [NSMutableArray array];
|
|
|
|
}
|
|
|
|
|
2015-02-20 13:43:11 +00:00
|
|
|
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"hideRedactions" options:0 context:nil];
|
2015-01-28 17:39:23 +00:00
|
|
|
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"hideUnsupportedEvents" options:0 context:nil];
|
2015-02-12 10:16:28 +00:00
|
|
|
[mxHandler addObserver:self forKeyPath:@"isActivityInProgress" options:0 context:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
// Register a listener to handle messages
|
2015-01-30 17:47:43 +00:00
|
|
|
_messagesListener = [self.mxRoom listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
|
2014-11-24 09:38:23 +00:00
|
|
|
// Handle first live events
|
|
|
|
if (direction == MXEventDirectionForwards) {
|
2014-12-08 14:22:26 +00:00
|
|
|
// Check user's membership in live room state (Indeed we have to go back on recents when user leaves, or is kicked/banned)
|
2014-12-11 17:10:15 +00:00
|
|
|
if (self.mxRoom.state.membership == MXMembershipLeave || self.mxRoom.state.membership == MXMembershipBan) {
|
2014-12-10 09:11:45 +00:00
|
|
|
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:NO];
|
2014-12-08 14:22:26 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
// Update Table on processing queue
|
|
|
|
MXRoomState *roomStateCpy = [roomState copy];
|
|
|
|
dispatch_async(mxHandler.processingQueue, ^{
|
|
|
|
BOOL isHandled = NO;
|
|
|
|
|
|
|
|
// For outgoing message, we update here local echo data
|
|
|
|
if ([event.userId isEqualToString:mxHandler.userId] && messages.count) {
|
|
|
|
RoomMessage *message = nil;
|
|
|
|
// Consider first the last message
|
|
|
|
@synchronized(self) {
|
|
|
|
message = [messages lastObject];
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([message containsEventId:event.eventId]) {
|
|
|
|
// The handling of this outgoing message is complete. We remove here its local echo
|
|
|
|
if (message.messageType == RoomMessageTypeText) {
|
|
|
|
[message removeEvent:event.eventId];
|
|
|
|
// Update message with the actual outgoing event
|
|
|
|
isHandled = [message addEvent:event withRoomState:roomStateCpy];
|
|
|
|
if (!message.components.count) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:message];
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Create a new message to handle attachment
|
2015-01-16 15:39:58 +00:00
|
|
|
RoomMessage *aNewMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomStateCpy];
|
|
|
|
if (aNewMessage) {
|
|
|
|
[self replaceMessage:message withMessage:aNewMessage];
|
2015-01-16 09:42:36 +00:00
|
|
|
} else {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Ignore unsupported/unexpected events
|
|
|
|
[self removeMessage:message];
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
|
|
|
isHandled = YES;
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-01-16 09:42:36 +00:00
|
|
|
// Look for the event id in other messages
|
2015-01-16 15:39:58 +00:00
|
|
|
message = [self messageWithEventId:event.eventId];
|
|
|
|
if (message) {
|
2014-12-19 13:16:22 +00:00
|
|
|
// The handling of this outgoing message is complete. We remove here its local echo
|
2014-11-27 09:50:09 +00:00
|
|
|
if (message.messageType == RoomMessageTypeText) {
|
|
|
|
[message removeEvent:event.eventId];
|
2014-12-16 09:23:14 +00:00
|
|
|
if (!message.components.count) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:message];
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-02-18 16:06:53 +00:00
|
|
|
// Workaround: in case of attachment, we keep our own timestamp and ignore server timestamp to prevent messages jump
|
|
|
|
RoomMessageComponent *component = [message componentWithEventId:event.eventId];
|
|
|
|
event.originServerTs = component.event.originServerTs;
|
|
|
|
RoomMessage *aNewMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomStateCpy];
|
|
|
|
if (aNewMessage) {
|
|
|
|
[self replaceMessage:message withMessage:aNewMessage];
|
|
|
|
} else {
|
|
|
|
// Ignore unsupported/unexpected events
|
|
|
|
[self removeMessage:message];
|
|
|
|
}
|
|
|
|
isHandled = YES;
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
} else {
|
|
|
|
// Here the received event id has not been found in current messages list.
|
|
|
|
// This may happen in 2 cases:
|
|
|
|
// - the message has been posted from another device.
|
|
|
|
// - the message is received from events stream whereas the app is waiting for our PUT to return (see pendingOutgoingEvents).
|
|
|
|
// In this second case, the pending event is replaced here (No additional action is required when PUT will return).
|
|
|
|
MXEvent *pendingEvent = [self pendingEventRelatedToEvent:event];
|
|
|
|
if (pendingEvent) {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Remove this event from the pending list
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
|
|
|
[pendingOutgoingEvents removeObject:pendingEvent];
|
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
// Remove this local event from messages
|
|
|
|
message = [self messageWithEventId:pendingEvent.eventId];
|
2015-01-16 09:42:36 +00:00
|
|
|
if (message) {
|
2015-01-14 13:05:45 +00:00
|
|
|
if (message.messageType == RoomMessageTypeText) {
|
|
|
|
[message removeEvent:pendingEvent.eventId];
|
|
|
|
if (!message.components.count) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:message];
|
2015-01-14 13:05:45 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-02-18 16:06:53 +00:00
|
|
|
// Workaround: in case of attachment, we keep our own timestamp and ignore server timestamp to prevent messages jump
|
|
|
|
RoomMessageComponent *component = [message componentWithEventId:pendingEvent.eventId];
|
|
|
|
event.originServerTs = component.event.originServerTs;
|
|
|
|
RoomMessage *aNewMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomStateCpy];
|
|
|
|
if (aNewMessage) {
|
|
|
|
[self replaceMessage:message withMessage:aNewMessage];
|
|
|
|
} else {
|
|
|
|
// Ignore unsupported/unexpected events
|
|
|
|
[self removeMessage:message];
|
|
|
|
}
|
|
|
|
isHandled = YES;
|
2015-01-14 13:05:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
|
|
|
|
if (isHandled == NO) {
|
|
|
|
// Check whether the event may be grouped with last message
|
|
|
|
RoomMessage *lastMessage = nil;
|
|
|
|
@synchronized(self) {
|
|
|
|
lastMessage = [messages lastObject];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastMessage && [lastMessage addEvent:event withRoomState:roomStateCpy]) {
|
2014-12-04 10:43:43 +00:00
|
|
|
isHandled = YES;
|
2015-01-16 09:42:36 +00:00
|
|
|
} else {
|
|
|
|
// Create a new item
|
|
|
|
lastMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomStateCpy];
|
|
|
|
if (lastMessage) {
|
|
|
|
@synchronized(self) {
|
|
|
|
[messages addObject:lastMessage];
|
|
|
|
}
|
|
|
|
isHandled = YES;
|
|
|
|
} // else ignore unsupported/unexpected events
|
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
2014-12-16 10:09:07 +00:00
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
// Refresh table display except if a back pagination is in progress
|
|
|
|
if (!isBackPaginationInProgress) {
|
|
|
|
// Refresh tableView
|
2014-12-16 10:09:07 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2015-02-17 09:01:55 +00:00
|
|
|
// We will scroll to bottom after updating tableView if the current table position is already at the bottom.
|
|
|
|
BOOL shouldScrollToBottom = [self isMessagesTableScrollViewAtTheBottom];
|
2015-01-16 09:42:36 +00:00
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
if (shouldScrollToBottom) {
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:YES];
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
2014-12-16 10:09:07 +00:00
|
|
|
});
|
2015-01-16 09:42:36 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
} else if (isBackPaginationInProgress && direction == MXEventDirectionBackwards) {
|
2015-01-16 09:42:36 +00:00
|
|
|
// Back pagination is in progress, we add an old event at the beginning of messages (on processing queue)
|
|
|
|
MXRoomState *roomStateCpy = [roomState copy];
|
|
|
|
dispatch_async(mxHandler.processingQueue, ^{
|
|
|
|
RoomMessage *firstMessage;
|
|
|
|
@synchronized(self) {
|
|
|
|
firstMessage = [messages firstObject];
|
|
|
|
}
|
|
|
|
if (!firstMessage || [firstMessage addEvent:event withRoomState:roomStateCpy] == NO) {
|
|
|
|
firstMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomStateCpy];
|
|
|
|
if (firstMessage) {
|
|
|
|
@synchronized(self) {
|
|
|
|
[messages insertObject:firstMessage atIndex:0];
|
|
|
|
}
|
|
|
|
backPaginationAddedMsgNb ++;
|
|
|
|
backPaginationHandledEventsNb ++;
|
|
|
|
}
|
|
|
|
// Ignore unsupported/unexpected events
|
|
|
|
} else {
|
2014-12-16 20:33:45 +00:00
|
|
|
backPaginationHandledEventsNb ++;
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
|
|
|
|
// Display is refreshed at the end of back pagination (see onComplete block)
|
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
2015-01-30 17:47:43 +00:00
|
|
|
// Register a listener to handle redaction in live stream
|
|
|
|
_redactionListener = [self.mxRoom listenToEventsOfTypes:@[kMXEventTypeStringRoomRedaction] onEvent:^(MXEvent *redactionEvent, MXEventDirection direction, MXRoomState *roomState) {
|
|
|
|
// Consider only live redaction events
|
|
|
|
if (direction == MXEventDirectionForwards) {
|
|
|
|
// Update Table on processing queue
|
|
|
|
dispatch_async(mxHandler.processingQueue, ^{
|
|
|
|
// Check whether a message contains the redacted event
|
|
|
|
RoomMessage *message = [self messageWithEventId:redactionEvent.redacts];
|
|
|
|
if (message) {
|
|
|
|
// Retrieve the original to redact it
|
|
|
|
MXEvent *originalEvent = [message componentWithEventId:redactionEvent.redacts].event;
|
|
|
|
MXEvent *redactedEvent = [originalEvent prune];
|
|
|
|
redactedEvent.redactedBecause = redactionEvent.originalDictionary;
|
|
|
|
|
|
|
|
if (redactedEvent.isState) {
|
|
|
|
// FIXME: The room state must be refreshed here since this redacted event.
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Warning: A state event has been redacted, room state may not be up to date");
|
2015-01-30 17:47:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We replace the event with the redacted one
|
|
|
|
[message updateRedactedEvent:redactedEvent];
|
|
|
|
if (!message.components.count) {
|
|
|
|
[self removeMessage:message];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh table display except if a back pagination is in progress
|
|
|
|
if (!isBackPaginationInProgress) {
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
2015-01-13 17:32:52 +00:00
|
|
|
// Add typing notification listener
|
|
|
|
typingNotifListener = [self.mxRoom listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
|
|
|
|
// Handle only live events
|
|
|
|
if (direction == MXEventDirectionForwards) {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Switch on the processing queue in order to serialize this operation with messages handling
|
|
|
|
dispatch_async(mxHandler.processingQueue, ^{
|
|
|
|
// Retrieve typing users list
|
|
|
|
NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.mxRoom.typingUsers];
|
|
|
|
// Remove typing info for the current user
|
|
|
|
NSUInteger index = [typingUsers indexOfObject:mxHandler.userId];
|
|
|
|
if (index != NSNotFound) {
|
|
|
|
[typingUsers removeObjectAtIndex:index];
|
|
|
|
}
|
|
|
|
// Ignore this notification if both arrays are empty
|
|
|
|
if (currentTypingUsers.count || typingUsers.count) {
|
|
|
|
currentTypingUsers = typingUsers;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
if (members) {
|
|
|
|
[self.membersTableView reloadData];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2015-01-13 17:32:52 +00:00
|
|
|
}
|
|
|
|
}];
|
2015-01-16 15:39:58 +00:00
|
|
|
currentTypingUsers = self.mxRoom.typingUsers;
|
2015-01-13 17:32:52 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Trigger a back pagination by reseting first backState to get room history from live
|
2014-12-11 17:10:15 +00:00
|
|
|
[self.mxRoom resetBackState];
|
2014-11-24 09:38:23 +00:00
|
|
|
[self triggerBackPagination];
|
|
|
|
}
|
|
|
|
|
2014-12-15 15:54:31 +00:00
|
|
|
self.roomTitleView.mxRoom = self.mxRoom;
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
}
|
|
|
|
|
2015-02-17 09:01:55 +00:00
|
|
|
- (BOOL)isMessagesTableScrollViewAtTheBottom {
|
|
|
|
// Check whether the most recent message is visible.
|
|
|
|
// Compute the max vertical position visible according to contentOffset
|
|
|
|
CGFloat maxPositionY = self.messagesTableView.contentOffset.y + (self.messagesTableView.frame.size.height - self.messagesTableView.contentInset.bottom);
|
|
|
|
// Be a bit less retrictive, consider the table view at the bottom even if the most recent message is partially hidden
|
|
|
|
maxPositionY += 30;
|
|
|
|
BOOL isScrolledToBottom = (maxPositionY >= self.messagesTableView.contentSize.height);
|
|
|
|
|
|
|
|
// Consider the table view at the bottom if a scrolling to bottom is in progress too
|
|
|
|
return (isScrolledToBottom || isScrollingToBottom);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)scrollMessagesTableViewToBottomAnimated:(BOOL)animated {
|
|
|
|
if (self.messagesTableView.contentSize.height) {
|
|
|
|
CGFloat visibleHeight = self.messagesTableView.frame.size.height - self.messagesTableView.contentInset.bottom;
|
|
|
|
if (visibleHeight < self.messagesTableView.contentSize.height) {
|
|
|
|
CGFloat wantedOffsetY = self.messagesTableView.contentSize.height - visibleHeight;
|
|
|
|
CGFloat currentOffsetY = self.messagesTableView.contentOffset.y;
|
|
|
|
if (wantedOffsetY != currentOffsetY) {
|
|
|
|
isScrollingToBottom = YES;
|
|
|
|
[self.messagesTableView setContentOffset:CGPointMake(0, wantedOffsetY) animated:animated];
|
|
|
|
}
|
|
|
|
}
|
2014-12-16 09:05:37 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
- (void)startActivityIndicator {
|
|
|
|
[_activityIndicator startAnimating];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)stopActivityIndicator {
|
|
|
|
// Check whether all conditions are satisfied before stopping loading wheel
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2015-02-12 10:16:28 +00:00
|
|
|
if (!mxHandler.isActivityInProgress && !isBackPaginationInProgress && !isJoinRequestInProgress) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[_activityIndicator stopAnimating];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)updateMessageTextViewFrame {
|
|
|
|
if (!isKeyboardDisplayed) {
|
|
|
|
// compute the visible area (tableview + text input)
|
|
|
|
// the tableview must use at least 50 pixels to let the user hides the keybaord
|
|
|
|
CGFloat maxTextHeight = (self.view.frame.size.height - self.navigationController.navigationBar.frame.size.height - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height - MIN([UIApplication sharedApplication].statusBarFrame.size.height, [UIApplication sharedApplication].statusBarFrame.size.width)) - 50;
|
|
|
|
|
|
|
|
_messageTextView.maxHeight = maxTextHeight;
|
|
|
|
[_messageTextView refreshHeight];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - KVO
|
|
|
|
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
2015-02-12 10:16:28 +00:00
|
|
|
if ([@"isActivityInProgress" isEqualToString:keyPath]) {
|
|
|
|
if ([MatrixSDKHandler sharedHandler].isActivityInProgress) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self startActivityIndicator];
|
|
|
|
} else {
|
2015-02-12 10:16:28 +00:00
|
|
|
[self stopActivityIndicator];
|
2015-01-16 15:39:58 +00:00
|
|
|
}
|
|
|
|
} else if ((object == inputAccessoryView.superview) && ([@"frame" isEqualToString:keyPath] || [@"center" isEqualToString:keyPath])) {
|
|
|
|
|
|
|
|
// if the keyboard is displayed, check if the keyboard is hiding with a slide animation
|
|
|
|
if (inputAccessoryView && inputAccessoryView.superview) {
|
|
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
2015-01-22 16:31:44 +00:00
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
|
|
|
|
|
2015-01-22 16:31:44 +00:00
|
|
|
// on IOS 8, the screen size is oriented
|
|
|
|
if ((NSFoundationVersionNumber <= NSFoundationVersionNumber_iOS_7_1) && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
|
|
|
|
screenSize = CGSizeMake(screenSize.height, screenSize.width);
|
2015-01-16 15:39:58 +00:00
|
|
|
}
|
|
|
|
|
2015-01-22 16:31:44 +00:00
|
|
|
keyboardHeight = screenSize.height - inputAccessoryView.superview.frame.origin.y;
|
2015-01-16 15:39:58 +00:00
|
|
|
|
2015-01-22 16:31:44 +00:00
|
|
|
insets.bottom = keyboardHeight + self.controlView.frame.size.height - defaultMessagesTableViewBottomConstraint;
|
2015-01-16 15:39:58 +00:00
|
|
|
|
|
|
|
// Move the control view
|
|
|
|
// Don't forget the offset related to tabBar
|
2015-01-22 16:31:44 +00:00
|
|
|
CGFloat newConstant = keyboardHeight - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height;
|
2015-01-16 15:39:58 +00:00
|
|
|
|
|
|
|
// draw over the bound
|
|
|
|
if ((_controlViewBottomConstraint.constant < 0) || (insets.bottom < self.controlView.frame.size.height)) {
|
|
|
|
newConstant = 0;
|
|
|
|
insets.bottom = self.controlView.frame.size.height;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// IOS 8 / landscape issue
|
|
|
|
// when the top of the keyboard reaches the top of the tabbar, it triggers UIKeyboardWillShowNotification events in loop
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
}
|
|
|
|
// update the table the tableview height
|
|
|
|
self.messagesTableView.contentInset = insets;
|
|
|
|
_controlViewBottomConstraint.constant = newConstant;
|
|
|
|
}
|
2015-02-20 13:43:11 +00:00
|
|
|
} else if ([@"hideUnsupportedEvents" isEqualToString:keyPath] || [@"hideRedactions" isEqualToString:keyPath]) {
|
2015-01-28 17:39:23 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self configureView];
|
|
|
|
});
|
2015-01-16 15:39:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Message handling
|
|
|
|
|
|
|
|
// Return the message (if any) which contains the provided event id in its components
|
|
|
|
- (RoomMessage*)messageWithEventId:(NSString *)eventId {
|
|
|
|
RoomMessage *message = nil;
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
2015-01-16 15:39:58 +00:00
|
|
|
NSUInteger index = messages.count;
|
|
|
|
while (index--) {
|
|
|
|
message = [messages objectAtIndex:index];
|
|
|
|
if ([message containsEventId:eventId]) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
message = nil;
|
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)removeMessage:(RoomMessage*)message {
|
2015-01-16 09:42:36 +00:00
|
|
|
RoomMessage *previousMessage = nil;
|
|
|
|
RoomMessage *nextMessage = nil;
|
|
|
|
@synchronized(self) {
|
2015-01-16 15:39:58 +00:00
|
|
|
NSUInteger index = [messages indexOfObject:message];
|
|
|
|
if (index != NSNotFound) {
|
|
|
|
[messages removeObjectAtIndex:index];
|
|
|
|
|
|
|
|
// Retrieve adjoined messages if the message was neither the first nor the last one
|
|
|
|
if (index && index < messages.count) {
|
|
|
|
previousMessage = [messages objectAtIndex:index - 1];
|
|
|
|
nextMessage = [messages objectAtIndex:index];
|
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
if (previousMessage && nextMessage) {
|
2014-12-16 13:00:37 +00:00
|
|
|
// Check whether both messages can merge
|
|
|
|
if ([previousMessage mergeWithRoomMessage:nextMessage]) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:nextMessage];
|
2014-12-16 13:00:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
- (void)replaceMessage:(RoomMessage*)message withMessage:(RoomMessage*)aNewMessage {
|
|
|
|
@synchronized(self) {
|
|
|
|
NSUInteger index = [messages indexOfObject:message];
|
|
|
|
if (index != NSNotFound) {
|
|
|
|
[messages replaceObjectAtIndex:index withObject:aNewMessage];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-19 12:08:43 +00:00
|
|
|
# pragma mark - Event handling
|
2015-02-03 14:34:32 +00:00
|
|
|
|
|
|
|
- (void)showEventDetails:(MXEvent *)event {
|
|
|
|
[self dismissKeyboard];
|
|
|
|
|
|
|
|
_eventDetailsView.event = event;
|
|
|
|
_eventDetailsView.hidden = NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)hideEventDetails {
|
|
|
|
_eventDetailsView.event = nil;
|
|
|
|
_eventDetailsView.hidden = YES;
|
|
|
|
}
|
|
|
|
|
2015-02-19 12:08:43 +00:00
|
|
|
- (void)promptUserToResendEvent:(NSString *)eventId {
|
|
|
|
RoomMessage* roomMessage = [self messageWithEventId:eventId];
|
|
|
|
RoomMessageComponent* component = [roomMessage componentWithEventId:eventId];
|
|
|
|
|
|
|
|
if (component) {
|
|
|
|
NSString* textMessage = component.textMessage;
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
self.actionMenu = [[MXCAlert alloc] initWithTitle:@"Resend the message"
|
|
|
|
message:(roomMessage.messageType == RoomMessageTypeText) ? textMessage : nil
|
|
|
|
style:MXCAlertStyleAlert];
|
|
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel"
|
|
|
|
style:MXCAlertActionStyleDefault
|
|
|
|
handler:^(MXCAlert *alert) {
|
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
|
|
|
[self.actionMenu addActionWithTitle:@"OK"
|
|
|
|
style:MXCAlertActionStyleDefault
|
|
|
|
handler:^(MXCAlert *alert) {
|
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
|
|
|
|
if (roomMessage.messageType == RoomMessageTypeText) {
|
|
|
|
// remove the message
|
|
|
|
[roomMessage removeEvent:eventId];
|
|
|
|
if (!roomMessage.components.count) {
|
|
|
|
[weakSelf removeMessage:roomMessage];
|
|
|
|
}
|
|
|
|
[weakSelf sendTextMessage:textMessage];
|
|
|
|
} else if (roomMessage.messageType == RoomMessageTypeImage) {
|
|
|
|
[weakSelf removeMessage:roomMessage];
|
|
|
|
UIImage* image = [MediaManager loadCachePictureForURL:roomMessage.attachmentURL inFolder:weakSelf.roomId];
|
|
|
|
|
|
|
|
// if the URL is still a local one
|
|
|
|
if (image) {
|
|
|
|
// it should mean that the media upload fails
|
|
|
|
[weakSelf sendImage:image];
|
|
|
|
} else if (roomMessage.attachmentURL.length > 0) {
|
|
|
|
// build the image dict
|
|
|
|
NSMutableDictionary* imageMessage = [[NSMutableDictionary alloc] init];
|
|
|
|
[imageMessage setObject:@"Image" forKey:@"body"];
|
|
|
|
[imageMessage setObject:roomMessage.attachmentInfo forKey:@"info"];
|
|
|
|
[imageMessage setObject:kMXMessageTypeImage forKey:@"msgtype"];
|
|
|
|
[imageMessage setObject:roomMessage.attachmentURL forKey:@"url"];
|
|
|
|
|
|
|
|
if (roomMessage.previewURL) {
|
|
|
|
[imageMessage setObject:roomMessage.previewURL forKey:kRoomMessageLocalPreviewKey];
|
|
|
|
}
|
|
|
|
|
|
|
|
// send it again
|
|
|
|
[weakSelf sendMessage:imageMessage withLocalEvent:nil];
|
|
|
|
}
|
|
|
|
} else if (roomMessage.messageType == RoomMessageTypeVideo) {
|
|
|
|
[weakSelf removeMessage:roomMessage];
|
|
|
|
|
|
|
|
// if the URL is still a local one
|
|
|
|
if (![NSURL URLWithString:roomMessage.thumbnailURL].scheme) {
|
|
|
|
UIImage* image = [MediaManager loadCachePictureForURL:roomMessage.thumbnailURL inFolder:weakSelf.roomId];
|
|
|
|
// it should mean that the thumbnail upload fails
|
|
|
|
[weakSelf sendVideo:[NSURL fileURLWithPath:roomMessage.attachmentURL] withThumbnail:image];
|
|
|
|
} else {
|
|
|
|
NSMutableDictionary* videoMessage = [[NSMutableDictionary alloc] init];
|
|
|
|
[videoMessage setObject:@"Video" forKey:@"body"];
|
|
|
|
[videoMessage setObject:roomMessage.attachmentInfo forKey:@"info"];
|
|
|
|
[videoMessage setObject:kMXMessageTypeVideo forKey:@"msgtype"];
|
|
|
|
[videoMessage setObject:roomMessage.attachmentURL forKey:@"url"];
|
|
|
|
|
|
|
|
if (roomMessage.previewURL) {
|
|
|
|
[videoMessage setObject:roomMessage.previewURL forKey:kRoomMessageLocalPreviewKey];
|
|
|
|
}
|
|
|
|
|
|
|
|
// the attachment is still a local path
|
|
|
|
if (![NSURL URLWithString:roomMessage.attachmentURL].scheme) {
|
|
|
|
// Add a new local event
|
|
|
|
MXEvent* localEvent = [weakSelf createLocalEchoEventWithoutContent];
|
|
|
|
localEvent.content = videoMessage;
|
|
|
|
[weakSelf addLocalEchoEvent:localEvent];
|
|
|
|
[weakSelf sendVideoContent:videoMessage localEvent:localEvent];
|
|
|
|
} else {
|
|
|
|
// set localEvent to nil to avoid useless search
|
|
|
|
[weakSelf sendMessage:videoMessage withLocalEvent:nil];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
|
|
|
[self.actionMenu showInViewController:[[AppDelegate theDelegate].masterTabBarController selectedViewController]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
#pragma mark - Back pagination
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
- (void)triggerBackPagination {
|
|
|
|
// Check whether a back pagination is already in progress
|
|
|
|
if (isBackPaginationInProgress) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-12-11 17:10:15 +00:00
|
|
|
if (self.mxRoom.canPaginate) {
|
2014-12-16 20:33:45 +00:00
|
|
|
NSUInteger requestedItemsNb = ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE;
|
|
|
|
// In case of first pagination, we will request only messages from the store to speed up the room display
|
|
|
|
if (!messages.count) {
|
|
|
|
isFirstPagination = YES;
|
|
|
|
requestedItemsNb = self.mxRoom.remainingMessagesForPaginationInStore;
|
|
|
|
if (!requestedItemsNb || ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE < requestedItemsNb) {
|
|
|
|
requestedItemsNb = ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-09 16:09:51 +00:00
|
|
|
[self startActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
isBackPaginationInProgress = YES;
|
2014-12-04 15:33:31 +00:00
|
|
|
backPaginationAddedMsgNb = 0;
|
2015-01-15 12:17:00 +00:00
|
|
|
// Store the current height of the first message (if any)
|
|
|
|
backPaginationSavedFirstMsgHeight = 0;
|
|
|
|
if (messages.count) {
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
|
|
|
|
backPaginationSavedFirstMsgHeight = [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2014-12-22 15:50:45 +00:00
|
|
|
[self paginateBackMessages:requestedItemsNb];
|
|
|
|
});
|
2014-12-04 15:33:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)paginateBackMessages:(NSUInteger)requestedItemsNb {
|
2014-12-16 20:33:45 +00:00
|
|
|
backPaginationHandledEventsNb = 0;
|
2014-12-22 10:13:23 +00:00
|
|
|
backPaginationOperation = [self.mxRoom paginateBackMessages:requestedItemsNb complete:^{
|
2014-12-04 15:33:31 +00:00
|
|
|
// Sanity check: check whether the view controller has not been released while back pagination was running
|
|
|
|
if (self.roomId == nil) {
|
|
|
|
return;
|
|
|
|
}
|
2014-12-16 20:33:45 +00:00
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
// Check whether we received less items than expected, and check condition to be able to ask more.
|
|
|
|
// This operation must be done on processing queue to be sync with the events reception
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2015-01-16 09:42:36 +00:00
|
|
|
BOOL shouldLoop = ((backPaginationHandledEventsNb < requestedItemsNb) && self.mxRoom.canPaginate);
|
|
|
|
if (shouldLoop) {
|
|
|
|
NSUInteger missingItemsNb = requestedItemsNb - backPaginationHandledEventsNb;
|
|
|
|
// About first pagination, we will loop only if the store has more items (except if none item has been handled, in this case loop is required)
|
|
|
|
if (isFirstPagination && backPaginationHandledEventsNb) {
|
|
|
|
if (self.mxRoom.remainingMessagesForPaginationInStore < missingItemsNb) {
|
|
|
|
missingItemsNb = self.mxRoom.remainingMessagesForPaginationInStore;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (missingItemsNb) {
|
|
|
|
// Ask more items
|
|
|
|
[self paginateBackMessages:missingItemsNb];
|
|
|
|
return;
|
2014-12-16 20:33:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
// Here we are done
|
|
|
|
[self onBackPaginationComplete];
|
|
|
|
});
|
2014-12-04 15:33:31 +00:00
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Failed to paginate back: %@", error);
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2015-01-16 09:42:36 +00:00
|
|
|
[self onBackPaginationComplete];
|
|
|
|
});
|
2014-12-04 15:33:31 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)onBackPaginationComplete {
|
2015-01-16 09:42:36 +00:00
|
|
|
// Reset
|
|
|
|
isFirstPagination = NO;
|
|
|
|
backPaginationOperation = nil;
|
|
|
|
|
2015-01-20 16:54:57 +00:00
|
|
|
// We scroll to bottom when table is loaded for the first time
|
|
|
|
BOOL shouldScrollToBottom = (self.messagesTableView.contentSize.height == 0);
|
|
|
|
if (!shouldScrollToBottom) {
|
|
|
|
// We will scroll to bottom if the displayed content does not reach the bottom (after adding back pagination)
|
|
|
|
CGFloat maxPositionY = self.messagesTableView.contentOffset.y + (self.messagesTableView.frame.size.height - self.messagesTableView.contentInset.bottom);
|
|
|
|
// Compute the height of the blank part at the bottom
|
|
|
|
if (maxPositionY > self.messagesTableView.contentSize.height) {
|
|
|
|
CGFloat blankAreaHeight = maxPositionY - self.messagesTableView.contentSize.height;
|
|
|
|
// Scroll to bottom if this blank area is greater than max scrolling offet
|
|
|
|
shouldScrollToBottom = (blankAreaHeight >= ROOMVIEWCONTROLLER_BACK_PAGINATION_MAX_SCROLLING_OFFSET);
|
2015-01-15 12:17:00 +00:00
|
|
|
}
|
2015-01-20 16:54:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
2014-12-04 15:33:31 +00:00
|
|
|
}
|
2015-01-20 16:54:57 +00:00
|
|
|
// Add delta of the height of the first existing message
|
|
|
|
if (messages.count > backPaginationAddedMsgNb) {
|
|
|
|
indexPath = [NSIndexPath indexPathForRow:backPaginationAddedMsgNb inSection:0];
|
|
|
|
verticalOffset += ([self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath] - backPaginationSavedFirstMsgHeight);
|
|
|
|
}
|
|
|
|
// Deduce the vertical offset from this height
|
|
|
|
verticalOffset -= ROOMVIEWCONTROLLER_BACK_PAGINATION_MAX_SCROLLING_OFFSET;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2015-01-20 16:54:57 +00:00
|
|
|
// Reset count to enable tableView update
|
|
|
|
backPaginationAddedMsgNb = 0;
|
|
|
|
|
|
|
|
// Return on main thread to end back pagination
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
// Reload table
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
|
|
|
|
// Adjust vertical content offset
|
|
|
|
if (shouldScrollToBottom) {
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:NO];
|
2015-01-20 16:54:57 +00:00
|
|
|
} 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];
|
|
|
|
});
|
2014-12-09 16:09:51 +00:00
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
# pragma mark - Room members
|
|
|
|
|
|
|
|
- (void)updateRoomMembers {
|
2014-12-17 13:40:39 +00:00
|
|
|
NSArray* membersList = [self.mxRoom.state members];
|
|
|
|
|
|
|
|
if (![[AppSettings sharedSettings] displayLeftUsers]) {
|
|
|
|
NSMutableArray* filteredMembers = [[NSMutableArray alloc] init];
|
|
|
|
|
|
|
|
for (MXRoomMember* member in membersList) {
|
|
|
|
if (member.membership != MXMembershipLeave) {
|
|
|
|
[filteredMembers addObject:member];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
membersList = filteredMembers;
|
|
|
|
}
|
|
|
|
|
2014-12-22 10:55:28 +00:00
|
|
|
members = [membersList 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
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2014-12-22 10:55:28 +00:00
|
|
|
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;
|
|
|
|
}
|
2015-02-24 14:25:23 +00:00
|
|
|
return [[self.mxRoom.state memberSortedName:member1.userId] compare:[self.mxRoom.state memberSortedName:member2.userId] options:NSCaseInsensitiveSearch];
|
2014-12-22 10:55:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Consider user's lastActive ago value
|
|
|
|
if (user1.lastActiveAgo < user2.lastActiveAgo) {
|
|
|
|
return NSOrderedAscending;
|
|
|
|
} else if (user1.lastActiveAgo == user2.lastActiveAgo) {
|
2015-02-24 14:25:23 +00:00
|
|
|
return [[self.mxRoom.state memberSortedName:member1.userId] compare:[self.mxRoom.state memberSortedName:member2.userId] options:NSCaseInsensitiveSearch];
|
2014-12-22 10:55:28 +00:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-02-24 14:25:23 +00:00
|
|
|
return [[self.mxRoom.state memberSortedName:member1.userId] compare:[self.mxRoom.state memberSortedName:member2.userId] options:NSCaseInsensitiveSearch];
|
2014-12-22 10:55:28 +00:00
|
|
|
}
|
|
|
|
}];
|
2014-12-24 10:06:54 +00:00
|
|
|
|
2015-02-23 14:46:57 +00:00
|
|
|
self.showRoomMembersButtonItem.enabled = members.count != 0;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-02-23 14:46:57 +00:00
|
|
|
- (IBAction)showRoomMembers:(id)sender {
|
2014-11-24 09:38:23 +00:00
|
|
|
// Dismiss keyboard
|
|
|
|
[self dismissKeyboard];
|
2015-02-23 14:46:57 +00:00
|
|
|
// Hide other sub-views
|
|
|
|
[self hideAttachmentView];
|
2015-02-03 14:34:32 +00:00
|
|
|
[self hideEventDetails];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
[self updateRoomMembers];
|
2014-12-24 07:59:25 +00:00
|
|
|
|
|
|
|
// check if there is some members to display
|
|
|
|
// else it makes no sense to display the list
|
|
|
|
if (0 == members.count) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
self.membersView.hidden = NO;
|
|
|
|
[self.membersTableView reloadData];
|
2015-02-23 14:46:57 +00:00
|
|
|
|
|
|
|
// Update navigation bar items
|
|
|
|
self.navigationItem.hidesBackButton = YES;
|
|
|
|
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(hideRoomMembers:)];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-02-23 14:46:57 +00:00
|
|
|
- (IBAction)hideRoomMembers:(id)sender {
|
2014-11-24 09:38:23 +00:00
|
|
|
self.membersView.hidden = YES;
|
|
|
|
members = nil;
|
2015-02-23 14:46:57 +00:00
|
|
|
|
|
|
|
// Update navigation bar items
|
|
|
|
self.navigationItem.hidesBackButton = NO;
|
|
|
|
self.navigationItem.rightBarButtonItem = _showRoomMembersButtonItem;
|
|
|
|
|
2015-01-21 10:18:03 +00:00
|
|
|
// Force a reload to release all table cells (and then stop running timer)
|
|
|
|
[self.membersTableView reloadData];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# pragma mark - Attachment handling
|
|
|
|
|
|
|
|
- (void)showAttachmentView:(UIGestureRecognizer *)gestureRecognizer {
|
2015-01-23 12:46:27 +00:00
|
|
|
MXCImageView *attachment = (MXCImageView*)gestureRecognizer.view;
|
2014-11-24 09:38:23 +00:00
|
|
|
[self dismissKeyboard];
|
|
|
|
|
|
|
|
// Retrieve attachment information
|
|
|
|
NSDictionary *content = attachment.mediaInfo;
|
2014-11-27 09:50:09 +00:00
|
|
|
NSUInteger msgtype = ((NSNumber*)content[@"msgtype"]).unsignedIntValue;
|
|
|
|
if (msgtype == RoomMessageTypeImage) {
|
|
|
|
NSString *url = content[@"url"];
|
2014-11-24 09:38:23 +00:00
|
|
|
if (url.length) {
|
2015-01-23 12:46:27 +00:00
|
|
|
highResImageView = [[MXCImageView alloc] initWithFrame:self.membersView.frame];
|
2015-01-06 14:44:34 +00:00
|
|
|
highResImageView.stretchable = YES;
|
|
|
|
highResImageView.fullScreen = YES;
|
2015-01-19 13:33:57 +00:00
|
|
|
highResImageView.mediaFolder = self.roomId;
|
2015-01-06 10:08:29 +00:00
|
|
|
[highResImageView setImageURL:url withPreviewImage:attachment.image];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Add tap recognizer to hide attachment
|
|
|
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideAttachmentView)];
|
|
|
|
[tap setNumberOfTouchesRequired:1];
|
|
|
|
[tap setNumberOfTapsRequired:1];
|
2015-01-06 10:08:29 +00:00
|
|
|
[highResImageView addGestureRecognizer:tap];
|
|
|
|
highResImageView.userInteractionEnabled = YES;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
} else if (msgtype == RoomMessageTypeVideo) {
|
2014-11-24 09:38:23 +00:00
|
|
|
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];
|
2015-01-12 10:00:53 +00:00
|
|
|
selectedVideoURL = url;
|
2015-01-13 16:53:31 +00:00
|
|
|
|
|
|
|
// check if the file is a local one
|
|
|
|
// could happen because a media upload has failed
|
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:selectedVideoURL]) {
|
|
|
|
selectedVideoCachePath = selectedVideoURL;
|
|
|
|
} else {
|
2015-01-19 13:33:57 +00:00
|
|
|
selectedVideoCachePath = [MediaManager cachePathForMediaURL:selectedVideoURL andType:mimetype inFolder:self.roomId];
|
2015-01-13 16:53:31 +00:00
|
|
|
}
|
|
|
|
|
2015-01-12 10:00:53 +00:00
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:selectedVideoCachePath]) {
|
|
|
|
videoPlayer.contentURL = [NSURL fileURLWithPath:selectedVideoCachePath];
|
2014-11-24 09:38:23 +00:00
|
|
|
[videoPlayer play];
|
2015-01-12 10:00:53 +00:00
|
|
|
} else {
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
|
2015-01-19 13:33:57 +00:00
|
|
|
[MediaManager downloadMediaFromURL:selectedVideoURL withType:mimetype inFolder:self.roomId];
|
2015-01-12 10:00:53 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
} else if (msgtype == RoomMessageTypeAudio) {
|
|
|
|
} else if (msgtype == RoomMessageTypeLocation) {
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-12 10:00:53 +00:00
|
|
|
- (void)onMediaDownloadEnd:(NSNotification *)notif {
|
|
|
|
if ([notif.object isKindOfClass:[NSString class]]) {
|
|
|
|
NSString* url = notif.object;
|
|
|
|
if ([url isEqualToString:selectedVideoURL]) {
|
|
|
|
// remove the observers
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil];
|
|
|
|
|
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:selectedVideoCachePath]) {
|
|
|
|
videoPlayer.contentURL = [NSURL fileURLWithPath:selectedVideoCachePath];
|
|
|
|
[videoPlayer play];
|
|
|
|
} else {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Video Download failed"); // TODO we should notify user
|
2015-01-12 10:00:53 +00:00
|
|
|
[self hideAttachmentView];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
- (void)hideAttachmentView {
|
2015-01-12 10:00:53 +00:00
|
|
|
selectedVideoURL = nil;
|
|
|
|
selectedVideoCachePath = nil;
|
2014-11-24 09:38:23 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerPlaybackDidFinishNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerWillExitFullscreenNotification object:nil];
|
2015-01-12 10:00:53 +00:00
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
[self dismissAttachmentImageViews];
|
2014-12-23 15:38:04 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// 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) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Playback failed with error description: %@", [mediaPlayerError localizedDescription]);
|
2014-11-24 09:38:23 +00:00
|
|
|
[self hideAttachmentView];
|
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:mediaPlayerError];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-11 15:41:21 +00:00
|
|
|
- (void)moviePlayerThumbnailImageRequestDidFinishNotification:(NSNotification *)notification {
|
|
|
|
// Finalize video attachment
|
|
|
|
UIImage* videoThumbnail = [[notification userInfo] objectForKey:MPMoviePlayerThumbnailImageKey];
|
|
|
|
NSURL* selectedVideo = [tmpVideoPlayer contentURL];
|
|
|
|
[tmpVideoPlayer stop];
|
|
|
|
tmpVideoPlayer = nil;
|
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
[self sendVideo:selectedVideo withThumbnail:videoThumbnail];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)sendVideoContent:(NSMutableDictionary*)videoContent localEvent:(MXEvent*)localEvent {
|
|
|
|
NSData* videoData = [NSData dataWithContentsOfFile:[videoContent valueForKey:@"url"]];
|
|
|
|
|
|
|
|
// sanity check
|
|
|
|
if (videoData) {
|
|
|
|
NSMutableDictionary* videoInfo = [videoContent valueForKey:@"info"];
|
|
|
|
|
2015-01-19 13:33:57 +00:00
|
|
|
MediaLoader *videoUploader = [MediaManager prepareUploaderWithId:localEvent.eventId initialRange:0.1 andRange:0.9 inFolder:self.roomId];
|
2015-01-13 16:53:31 +00:00
|
|
|
[videoUploader uploadData:videoData mimeType:videoInfo[@"mimetype"] success:^(NSString *url) {
|
|
|
|
|
|
|
|
// remove the tmp file
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:[videoInfo valueForKey:@"url"] error:nil];
|
|
|
|
// remove the related uploadLoader
|
2015-01-19 13:33:57 +00:00
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
2015-01-13 16:53:31 +00:00
|
|
|
// store the video file in the cache
|
|
|
|
// there is no reason to download an oneself uploaded media
|
2015-01-19 13:33:57 +00:00
|
|
|
[MediaManager cacheMediaData:videoData forURL:url andType:videoInfo[@"mimetype"] inFolder:self.roomId];
|
2015-01-13 16:53:31 +00:00
|
|
|
|
|
|
|
[videoContent setValue:url forKey:@"url"];
|
|
|
|
[self sendMessage:videoContent withLocalEvent:localEvent];
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Video upload failed");
|
2015-01-19 14:30:45 +00:00
|
|
|
// check if the upload is still defined
|
|
|
|
// it could have been cancelled with an external events
|
|
|
|
if ([MediaManager existingUploaderWithId:localEvent.eventId inFolder:self.roomId]) {
|
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
|
|
|
[self handleError:error forLocalEvent:localEvent];
|
|
|
|
}
|
2015-01-13 16:53:31 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
else {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Attach video failed: no data");
|
2015-01-13 16:53:31 +00:00
|
|
|
[self handleError:nil forLocalEvent:localEvent];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
- (void)sendVideo:(NSURL*)videoURL withThumbnail:(UIImage*)videoThumbnail {
|
2015-01-13 16:53:31 +00:00
|
|
|
if (videoThumbnail && videoURL) {
|
2014-12-11 15:41:21 +00:00
|
|
|
// Prepare video thumbnail description
|
|
|
|
NSUInteger thumbnailSize = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
|
2015-01-23 12:46:27 +00:00
|
|
|
UIImage *thumbnail = [MXCTools resize:videoThumbnail toFitInSize:CGSizeMake(thumbnailSize, thumbnailSize)];
|
2014-12-11 15:41:21 +00:00
|
|
|
|
|
|
|
// Create the local event displayed during uploading
|
2015-01-13 16:53:31 +00:00
|
|
|
MXEvent *localEvent = [self addLocalEchoEventForAttachedVideo:thumbnail videoPath:videoURL.path];
|
2015-01-07 09:45:35 +00:00
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
NSMutableDictionary *infoDict = [localEvent.content valueForKey:@"info"];
|
|
|
|
NSMutableDictionary *thumbnailInfo = [infoDict valueForKey:@"thumbnail_info"];
|
2015-01-19 13:33:57 +00:00
|
|
|
NSData *thumbnailData = [NSData dataWithContentsOfFile:[MediaManager cachePathForMediaURL:[infoDict valueForKey:@"thumbnail_url"] andType:[thumbnailInfo objectForKey:@"mimetype"] inFolder:self.roomId]];
|
2015-01-13 11:56:48 +00:00
|
|
|
|
2014-12-11 15:41:21 +00:00
|
|
|
// Upload thumbnail
|
2015-01-19 13:33:57 +00:00
|
|
|
MediaLoader *uploader = [MediaManager prepareUploaderWithId:localEvent.eventId initialRange:0 andRange:0.1 inFolder:self.roomId];
|
2015-01-13 11:56:48 +00:00
|
|
|
[uploader uploadData:thumbnailData mimeType:[thumbnailInfo valueForKey:@"mimetype"] success:^(NSString *url) {
|
2015-01-19 13:33:57 +00:00
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
2014-12-11 15:41:21 +00:00
|
|
|
// Prepare content of attached video
|
|
|
|
NSMutableDictionary *videoContent = [[NSMutableDictionary alloc] init];
|
|
|
|
NSMutableDictionary *videoInfo = [[NSMutableDictionary alloc] init];
|
2015-01-13 11:56:48 +00:00
|
|
|
[videoContent setValue:kMXMessageTypeVideo forKey:@"msgtype"];
|
2014-12-11 15:41:21 +00:00
|
|
|
[videoInfo setValue:url forKey:@"thumbnail_url"];
|
|
|
|
[videoInfo setValue:thumbnailInfo forKey:@"thumbnail_info"];
|
|
|
|
|
|
|
|
// Convert video container to mp4
|
2015-01-13 16:53:31 +00:00
|
|
|
AVURLAsset* videoAsset = [AVURLAsset URLAssetWithURL:videoURL options:nil];
|
2014-12-11 15:41:21 +00:00
|
|
|
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 {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Warning: MPEG-4 file format is not supported");
|
2014-12-11 15:41:21 +00:00
|
|
|
// 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
|
2015-01-13 16:53:31 +00:00
|
|
|
[videoContent setValue:videoInfo forKey:@"info"];
|
|
|
|
[videoContent setValue:@"Video" forKey:@"body"];
|
|
|
|
[videoContent setValue:tmpVideoLocation.path forKey:@"url"];
|
|
|
|
localEvent.content = videoContent;
|
|
|
|
[self sendVideoContent:videoContent localEvent:localEvent];
|
2014-12-11 15:41:21 +00:00
|
|
|
}
|
|
|
|
else {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Video export failed: %d", (int)[exportSession status]);
|
2014-12-11 15:41:21 +00:00
|
|
|
// remove tmp file (if any)
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil];
|
|
|
|
[self handleError:nil forLocalEvent:localEvent];
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
} failure:^(NSError *error) {
|
2015-01-19 14:30:45 +00:00
|
|
|
// check if the upload is still defined
|
|
|
|
// it could have been cancelled with an external events
|
|
|
|
if ([MediaManager existingUploaderWithId:localEvent.eventId inFolder:self.roomId]) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Video thumbnail upload failed");
|
2015-01-19 14:30:45 +00:00
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
|
|
|
[self handleError:error forLocalEvent:localEvent];
|
|
|
|
}
|
2014-12-11 15:41:21 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
[self dismissMediaPicker];
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
#pragma mark - Keyboard handling
|
2014-12-16 13:00:37 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
- (void)onKeyboardWillShow:(NSNotification *)notif {
|
2014-12-16 07:25:27 +00:00
|
|
|
// get the keyboard size
|
2014-11-24 09:38:23 +00:00
|
|
|
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
|
|
|
|
CGRect endRect = rectVal.CGRectValue;
|
|
|
|
|
2014-12-16 16:22:47 +00:00
|
|
|
// IOS 8 triggers some unexpected keyboard events
|
2014-12-16 14:01:31 +00:00
|
|
|
if ((endRect.size.height == 0) || (endRect.size.width == 0)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-02-19 15:18:09 +00:00
|
|
|
// Check screen orientation
|
2015-01-22 16:31:44 +00:00
|
|
|
keyboardHeight = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
// bottom view offset
|
2014-12-16 09:05:37 +00:00
|
|
|
// Don't forget the offset related to tabBar
|
2015-01-21 15:52:36 +00:00
|
|
|
CGFloat nextBottomViewContanst = keyboardHeight - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height;
|
|
|
|
|
|
|
|
// the tableview bottom inset must also be updated
|
|
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
|
|
|
// insets.bottom is the bottom part of the tableview content size which is not displayed
|
|
|
|
// The bottom margin is equal to the keyboard height + controlview part which is greather than the tableview bottom margin.
|
|
|
|
// The tableview bottom margin has the same value as the defauft bottom view height;
|
|
|
|
insets.bottom = keyboardHeight + self.controlView.frame.size.height - defaultMessagesTableViewBottomConstraint;
|
|
|
|
|
2015-01-22 16:31:44 +00:00
|
|
|
// compute the visible area (tableview + text input)
|
|
|
|
CGFloat maxTextHeight = (self.view.frame.size.height - defaultMessagesTableViewBottomConstraint - keyboardHeight - self.navigationController.navigationBar.frame.size.height - MIN([UIApplication sharedApplication].statusBarFrame.size.height, [UIApplication sharedApplication].statusBarFrame.size.width));
|
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
// get the animation info
|
|
|
|
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
|
|
|
|
UIViewAnimationCurve animationCurve = curveValue.intValue;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
// the duration is ignored but it is better to define it
|
|
|
|
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-12 12:31:54 +00:00
|
|
|
// remove pending observers to avoid duplicates
|
|
|
|
// onKeyboardWillShow could be called several times bebore onKeyboardWillHide
|
|
|
|
// because the keyboard height is updated (swicth to a chinese keyboard for example)
|
|
|
|
// fixes https://github.com/matrix-org/matrix-ios-sdk/issues/4
|
|
|
|
if (isKeyboardObserver) {
|
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"frame"];
|
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"center"];
|
|
|
|
isKeyboardObserver = NO;
|
|
|
|
}
|
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
isKeyboardDisplayed = YES;
|
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
[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;
|
|
|
|
|
2014-12-16 09:05:37 +00:00
|
|
|
// scroll the tableview content
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:NO];
|
2014-12-16 07:25:27 +00:00
|
|
|
|
|
|
|
// force to redraw the layout (else _controlViewBottomConstraint.constant will not be animated)
|
|
|
|
[self.view layoutIfNeeded];
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
// update the text input frame
|
|
|
|
_messageTextView.maxHeight = maxTextHeight;
|
2015-01-14 16:51:53 +00:00
|
|
|
[_messageTextView refreshHeight];
|
2015-01-14 14:17:20 +00:00
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
} completion:^(BOOL finished) {
|
2015-02-19 15:18:09 +00:00
|
|
|
// Check whether the keyboard is still visible at the end of animation
|
|
|
|
if (inputAccessoryView.superview) {
|
|
|
|
// be warned when the keyboard frame is updated
|
|
|
|
// used to trap the slide to close the keyboard
|
|
|
|
[inputAccessoryView.superview addObserver:self forKeyPath:@"frame" options:0 context:nil];
|
|
|
|
[inputAccessoryView.superview addObserver:self forKeyPath:@"center" options:0 context:nil];
|
|
|
|
isKeyboardObserver = YES;
|
|
|
|
}
|
2014-12-16 07:25:27 +00:00
|
|
|
}];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)onKeyboardWillHide:(NSNotification *)notif {
|
2014-12-16 14:01:31 +00:00
|
|
|
// onKeyboardWillHide seems being called several times by IOS
|
|
|
|
if (isKeyboardObserver) {
|
2014-12-16 16:22:47 +00:00
|
|
|
// IOS 8 / landscape issue
|
|
|
|
// when the keyboard reaches the tabbar, it triggers UIKeyboardWillShowNotification events in loop
|
|
|
|
// ensure that there is only one evene registration
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
|
2014-12-16 14:01:31 +00:00
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"frame"];
|
|
|
|
[inputAccessoryView.superview removeObserver:self forKeyPath:@"center"];
|
|
|
|
isKeyboardObserver = NO;
|
|
|
|
}
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-16 16:22:47 +00:00
|
|
|
// get the keyboard size
|
|
|
|
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
|
|
|
|
CGRect endRect = rectVal.CGRectValue;
|
|
|
|
|
|
|
|
rectVal = notif.userInfo[UIKeyboardFrameBeginUserInfoKey];
|
|
|
|
CGRect beginRect = rectVal.CGRectValue;
|
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
// IOS 7/8 triggers some unexpected keyboard events
|
|
|
|
if (!isKeyboardDisplayed) {
|
2014-12-16 16:22:47 +00:00
|
|
|
return;
|
|
|
|
}
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-16 15:11:37 +00:00
|
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
2015-01-21 15:52:36 +00:00
|
|
|
// insets.bottom is the bottom part of the tableview content size which is not displayed
|
|
|
|
// The bottom margin is equal to the tabbar height + controlview part which is greather than the tableview bottom margin.
|
|
|
|
// The tableview bottom margin has the same value as the defauft bottom view height.
|
|
|
|
insets.bottom = [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height + (self.controlView.frame.size.height - defaultMessagesTableViewBottomConstraint);
|
2014-12-16 15:11:37 +00:00
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
isKeyboardDisplayed = NO;
|
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
// do not animate if the both rect are the same
|
|
|
|
// but ensure that the fields are properly resetted
|
|
|
|
// e.g. when the user swipes to hide the keyboard
|
|
|
|
// this method is called with invalid rects
|
|
|
|
// animationDuration is ignored because of the animation curve
|
|
|
|
// use it to be sure that it will be broken with any new IOS update
|
|
|
|
if (CGRectEqualToRect(endRect, beginRect)) {
|
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
self.messagesTableView.contentInset = insets;
|
2015-01-14 16:51:53 +00:00
|
|
|
_controlViewBottomConstraint.constant = defaultControlViewBottomConstraint;
|
|
|
|
_messagesTableViewBottomConstraint.constant = defaultMessagesTableViewBottomConstraint;
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-16 07:25:27 +00:00
|
|
|
[self.view layoutIfNeeded];
|
2015-01-14 16:51:53 +00:00
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 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];
|
|
|
|
|
|
|
|
// animate the keyboard closing
|
|
|
|
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
|
|
|
|
|
|
|
|
_controlViewBottomConstraint.constant = defaultControlViewBottomConstraint;
|
|
|
|
_messagesTableViewBottomConstraint.constant = defaultMessagesTableViewBottomConstraint;
|
|
|
|
|
|
|
|
self.messagesTableView.contentInset = insets;
|
|
|
|
[self.view layoutIfNeeded];
|
|
|
|
|
|
|
|
} completion:^(BOOL finished) {
|
|
|
|
}];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dismissKeyboard {
|
|
|
|
// Hide the keyboard
|
2015-01-14 14:17:20 +00:00
|
|
|
[_messageTextView resignFirstResponder];
|
2014-12-15 15:54:31 +00:00
|
|
|
[_roomTitleView dismissKeyboard];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - UITableView data source
|
|
|
|
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
|
|
// Check table view members vs messages
|
2014-12-02 14:27:37 +00:00
|
|
|
if (tableView == self.membersTableView) {
|
2014-11-24 09:38:23 +00:00
|
|
|
return members.count;
|
|
|
|
}
|
2014-12-02 17:26:50 +00:00
|
|
|
|
2014-12-04 15:33:31 +00:00
|
|
|
if (backPaginationAddedMsgNb) {
|
2014-12-02 17:26:50 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
return messages.count;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
// Check table view members vs messages
|
2014-11-27 09:50:09 +00:00
|
|
|
if (tableView == self.membersTableView) {
|
2014-11-28 18:23:51 +00:00
|
|
|
// Use the same default height than message cell
|
|
|
|
return ROOM_MESSAGE_CELL_DEFAULT_HEIGHT;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2014-12-10 17:49:04 +00:00
|
|
|
// Compute here height of message cell
|
2014-11-24 09:38:23 +00:00
|
|
|
CGFloat rowHeight;
|
2015-01-16 09:42:36 +00:00
|
|
|
RoomMessage* message = nil;
|
|
|
|
@synchronized(self) {
|
|
|
|
if (indexPath.row < messages.count) {
|
|
|
|
message = [messages objectAtIndex:indexPath.row];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-21 10:18:03 +00:00
|
|
|
// Sanity check
|
2015-01-20 11:41:27 +00:00
|
|
|
if (!message) {
|
2014-12-16 09:23:14 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
// Else compute height of message content (The maximum width available for the textview must be updated dynamically)
|
2014-12-10 17:49:04 +00:00
|
|
|
message.maxTextViewWidth = self.messagesTableView.frame.size.width - ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW;
|
2014-11-27 09:50:09 +00:00
|
|
|
rowHeight = message.contentSize.height;
|
2014-12-10 17:49:04 +00:00
|
|
|
|
2014-11-28 17:36:57 +00:00
|
|
|
// Add top margin
|
|
|
|
if (message.messageType == RoomMessageTypeText) {
|
2014-11-28 18:23:51 +00:00
|
|
|
rowHeight += ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST;
|
2014-11-28 17:36:57 +00:00
|
|
|
} else {
|
2014-11-28 18:23:51 +00:00
|
|
|
rowHeight += ROOM_MESSAGE_CELL_DEFAULT_ATTACHMENTVIEW_TOP_CONST;
|
2014-11-28 17:36:57 +00:00
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
|
2014-11-28 18:23:51 +00:00
|
|
|
// 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) {
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
|
|
|
RoomMessage *previousMessage = [messages objectAtIndex:indexPath.row - 1];
|
|
|
|
shouldHideSenderInfo = [message hasSameSenderAsRoomMessage:previousMessage];
|
|
|
|
}
|
2014-11-28 18:23:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
return rowHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Check table view members vs messages
|
|
|
|
if (tableView == self.membersTableView) {
|
|
|
|
RoomMemberTableCell *memberCell = [tableView dequeueReusableCellWithIdentifier:@"RoomMemberCell" forIndexPath:indexPath];
|
|
|
|
if (indexPath.row < members.count) {
|
2015-01-14 08:42:01 +00:00
|
|
|
MXRoomMember *roomMember = [members objectAtIndex:indexPath.row];
|
|
|
|
[memberCell setRoomMember:roomMember withRoom:self.mxRoom];
|
2015-01-14 14:01:10 +00:00
|
|
|
if ([roomMember.userId isEqualToString:mxHandler.userId]) {
|
|
|
|
memberCell.typingBadge.hidden = YES; //hide typing badge for the current user
|
|
|
|
} else {
|
2015-01-16 15:39:58 +00:00
|
|
|
memberCell.typingBadge.hidden = ([currentTypingUsers indexOfObject:roomMember.userId] == NSNotFound);
|
2015-01-30 18:14:59 +00:00
|
|
|
if (!memberCell.typingBadge.hidden) {
|
|
|
|
[memberCell.typingBadge.superview bringSubviewToFront:memberCell.typingBadge];
|
|
|
|
}
|
2015-01-14 14:01:10 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
return memberCell;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle here room message cells
|
2015-01-16 09:42:36 +00:00
|
|
|
RoomMessage* message = nil;
|
|
|
|
@synchronized(self) {
|
|
|
|
if (indexPath.row < messages.count) {
|
|
|
|
message = [messages objectAtIndex:indexPath.row];
|
|
|
|
}
|
|
|
|
}
|
2015-01-21 10:18:03 +00:00
|
|
|
// Sanity check
|
2015-01-20 11:41:27 +00:00
|
|
|
if (!message) {
|
2014-12-16 13:00:37 +00:00
|
|
|
return [[UITableViewCell alloc] initWithFrame:CGRectZero];
|
|
|
|
}
|
|
|
|
// Else prepare the message cell
|
|
|
|
RoomMessageTableCell *cell;
|
2014-11-24 09:38:23 +00:00
|
|
|
BOOL isIncomingMsg = NO;
|
2014-11-27 09:50:09 +00:00
|
|
|
if ([message.senderId isEqualToString:mxHandler.userId]) {
|
2014-11-24 09:38:23 +00:00
|
|
|
cell = [tableView dequeueReusableCellWithIdentifier:@"OutgoingMessageCell" forIndexPath:indexPath];
|
|
|
|
} else {
|
|
|
|
cell = [tableView dequeueReusableCellWithIdentifier:@"IncomingMessageCell" forIndexPath:indexPath];
|
|
|
|
isIncomingMsg = YES;
|
|
|
|
}
|
|
|
|
|
2015-01-07 13:11:33 +00:00
|
|
|
// Keep reference on message
|
2014-12-18 09:50:31 +00:00
|
|
|
cell.message = message;
|
2015-01-07 13:11:33 +00:00
|
|
|
|
2015-01-19 13:33:57 +00:00
|
|
|
// set the media folders
|
|
|
|
cell.pictureView.mediaFolder = kMediaManagerThumbnailFolder;
|
|
|
|
cell.attachmentView.mediaFolder = self.roomId;
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Check whether the previous message has been sent by the same user.
|
2014-11-27 09:50:09 +00:00
|
|
|
// The user's picture and name are displayed only for the first message.
|
|
|
|
BOOL shouldHideSenderInfo = NO;
|
2014-11-24 09:38:23 +00:00
|
|
|
if (indexPath.row) {
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
|
|
|
RoomMessage *previousMessage = [messages objectAtIndex:indexPath.row - 1];
|
|
|
|
shouldHideSenderInfo = [message hasSameSenderAsRoomMessage:previousMessage];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-12-10 11:05:11 +00:00
|
|
|
// Handle sender's picture and adjust view's constraints
|
2014-11-28 18:23:51 +00:00
|
|
|
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
|
2015-01-14 17:30:00 +00:00
|
|
|
NSString *avatarThumbURL = nil;
|
|
|
|
if (message.senderAvatarUrl) {
|
|
|
|
// Suppose this url is a matrix content uri, we use SDK to get the well adapted thumbnail from server
|
|
|
|
avatarThumbURL = [mxHandler thumbnailURLForContent:message.senderAvatarUrl inViewSize:cell.pictureView.frame.size withMethod:MXThumbnailingMethodCrop];
|
|
|
|
}
|
|
|
|
[cell.pictureView setImageURL:avatarThumbURL withPreviewImage:[UIImage imageNamed:@"default-profile"]];
|
2014-11-24 09:38:23 +00:00
|
|
|
[cell.pictureView.layer setCornerRadius:cell.pictureView.frame.size.width / 2];
|
|
|
|
cell.pictureView.clipsToBounds = YES;
|
2015-01-07 14:49:29 +00:00
|
|
|
cell.pictureView.backgroundColor = [UIColor redColor];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-01-28 10:30:15 +00:00
|
|
|
[self addPictureViewTapGesture:cell];
|
|
|
|
|
2014-12-10 11:05:11 +00:00
|
|
|
// 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;
|
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
BOOL displayMsgTimestamp = (nil != dateFormatter);
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// Update incoming/outgoing message layout
|
|
|
|
if (isIncomingMsg) {
|
|
|
|
IncomingMessageTableCell* incomingMsgCell = (IncomingMessageTableCell*)cell;
|
2014-11-27 09:50:09 +00:00
|
|
|
// 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;
|
2015-01-13 17:32:52 +00:00
|
|
|
// Set typing badge visibility
|
2015-01-16 15:39:58 +00:00
|
|
|
incomingMsgCell.typingBadge.hidden = (cell.pictureView.hidden || ([currentTypingUsers indexOfObject:message.senderId] == NSNotFound));
|
2015-01-30 18:14:59 +00:00
|
|
|
if (!incomingMsgCell.typingBadge.hidden) {
|
|
|
|
[incomingMsgCell.typingBadge.superview bringSubviewToFront:incomingMsgCell.typingBadge];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
2014-11-28 09:51:22 +00:00
|
|
|
// Add unsent label for failed components
|
2014-12-10 11:05:11 +00:00
|
|
|
CGFloat yPosition = (message.messageType == RoomMessageTypeText) ? ROOM_MESSAGE_TEXTVIEW_MARGIN : -ROOM_MESSAGE_TEXTVIEW_MARGIN;
|
2015-01-20 11:41:27 +00:00
|
|
|
[message checkComponentsHeight];
|
2014-11-28 09:51:22 +00:00
|
|
|
for (RoomMessageComponent *component in message.components) {
|
2014-12-02 15:48:26 +00:00
|
|
|
if (component.style == RoomMessageComponentStyleFailed) {
|
2015-01-12 16:13:40 +00:00
|
|
|
UIButton *unsentButton = [[UIButton alloc] initWithFrame:CGRectMake(0, yPosition, 58 , 20)];
|
|
|
|
|
|
|
|
[unsentButton setTitle:@"Unsent" forState:UIControlStateNormal];
|
|
|
|
[unsentButton setTitle:@"Unsent" forState:UIControlStateSelected];
|
|
|
|
[unsentButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
|
|
|
|
[unsentButton setTitleColor:[UIColor redColor] forState:UIControlStateSelected];
|
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
unsentButton.backgroundColor = [UIColor whiteColor];
|
2015-01-12 16:13:40 +00:00
|
|
|
unsentButton.titleLabel.font = [UIFont systemFontOfSize:14];
|
|
|
|
|
|
|
|
// add a dummy label to store the event ID
|
|
|
|
// so the message will be easily found when the button will be tapped
|
|
|
|
UILabel* hiddenLabel = [[UILabel alloc] init];
|
2015-02-03 14:34:32 +00:00
|
|
|
hiddenLabel.tag = ROOM_MESSAGE_CELL_HIDDEN_UNSENT_MSG_LABEL_TAG;
|
2015-01-12 16:13:40 +00:00
|
|
|
hiddenLabel.text = component.eventId;
|
|
|
|
hiddenLabel.hidden = YES;
|
|
|
|
hiddenLabel.frame = CGRectZero;
|
|
|
|
hiddenLabel.userInteractionEnabled = YES;
|
|
|
|
[unsentButton addSubview:hiddenLabel];
|
|
|
|
|
2015-02-19 12:08:43 +00:00
|
|
|
[unsentButton addTarget:self action:@selector(onResendToggle:) forControlEvents:UIControlEventTouchUpInside];
|
2015-01-12 16:13:40 +00:00
|
|
|
|
|
|
|
[cell.dateTimeLabelContainer addSubview:unsentButton];
|
2014-12-10 11:05:11 +00:00
|
|
|
cell.dateTimeLabelContainer.hidden = NO;
|
2015-01-12 16:13:40 +00:00
|
|
|
cell.dateTimeLabelContainer.userInteractionEnabled = YES;
|
2015-01-12 16:35:38 +00:00
|
|
|
|
|
|
|
// ensure that dateTimeLabelContainer is at front to catch the the tap event
|
|
|
|
[cell.dateTimeLabelContainer.superview bringSubviewToFront:cell.dateTimeLabelContainer];
|
2014-11-28 09:51:22 +00:00
|
|
|
}
|
|
|
|
yPosition += component.height;
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-12-10 17:49:04 +00:00
|
|
|
|
|
|
|
// Set message content
|
|
|
|
message.maxTextViewWidth = self.messagesTableView.frame.size.width - ROOM_MESSAGE_CELL_TEXTVIEW_LEADING_AND_TRAILING_CONSTRAINT_TO_SUPERVIEW;
|
2014-11-27 09:50:09 +00:00
|
|
|
CGSize contentSize = message.contentSize;
|
|
|
|
if (message.messageType != RoomMessageTypeText) {
|
2014-11-28 17:36:57 +00:00
|
|
|
cell.messageTextView.hidden = YES;
|
2014-11-27 09:50:09 +00:00
|
|
|
cell.attachmentView.hidden = NO;
|
2015-02-03 14:34:32 +00:00
|
|
|
|
2014-11-28 16:02:46 +00:00
|
|
|
// 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;
|
2015-01-08 17:25:43 +00:00
|
|
|
|
2014-11-27 09:50:09 +00:00
|
|
|
NSString *url = message.thumbnailURL;
|
|
|
|
if (message.messageType == RoomMessageTypeVideo) {
|
|
|
|
cell.playIconView.hidden = NO;
|
2015-01-07 13:11:33 +00:00
|
|
|
} else {
|
|
|
|
cell.playIconView.hidden = YES;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2015-01-09 13:13:03 +00:00
|
|
|
UIImage *preview = nil;
|
|
|
|
if (message.previewURL) {
|
2015-01-19 13:33:57 +00:00
|
|
|
preview = [MediaManager loadCachePictureForURL:message.previewURL inFolder:self.roomId];
|
2015-01-09 13:13:03 +00:00
|
|
|
}
|
|
|
|
[cell.attachmentView setImageURL:url withPreviewImage:preview];
|
2015-01-06 10:08:29 +00:00
|
|
|
|
2014-11-27 09:50:09 +00:00
|
|
|
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};
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
|
2015-01-08 10:31:25 +00:00
|
|
|
[cell startProgressUI];
|
|
|
|
|
2015-01-08 17:25:43 +00:00
|
|
|
// wait after upload info
|
|
|
|
if (message.isUploadInProgress) {
|
|
|
|
[((OutgoingMessageTableCell*)cell) startUploadAnimating];
|
|
|
|
cell.attachmentView.hideActivityIndicator = YES;
|
|
|
|
} else {
|
|
|
|
cell.attachmentView.hideActivityIndicator = NO;
|
|
|
|
}
|
|
|
|
|
2014-11-28 17:36:57 +00:00
|
|
|
// Adjust Attachment width constant
|
|
|
|
cell.attachViewWidthConstraint.constant = contentSize.width;
|
2015-02-03 14:34:32 +00:00
|
|
|
|
|
|
|
// Add a long gesture recognizer on attachment view in order to display event details
|
|
|
|
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPressGesture:)];
|
|
|
|
cell.attachmentView.tag = ROOM_MESSAGE_CELL_ATTACHMENTVIEW_TAG;
|
|
|
|
[cell.attachmentView addGestureRecognizer:longPress];
|
|
|
|
// Add another long gesture recognizer on progressView to cancel the current operation (Note: only the download can be cancelled).
|
|
|
|
// Note2: It is not possible to manage this gesture recognizer from the storyboard -> The gesture view is always the same i.e. the latest composed one.
|
|
|
|
longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPressGesture:)];
|
|
|
|
cell.progressView.tag = ROOM_MESSAGE_CELL_PROGRESSVIEW_TAG;
|
|
|
|
[cell.progressView addGestureRecognizer:longPress];
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
2015-01-07 13:11:33 +00:00
|
|
|
cell.attachmentView.hidden = YES;
|
|
|
|
cell.playIconView.hidden = YES;
|
2014-11-28 17:36:57 +00:00
|
|
|
cell.messageTextView.hidden = NO;
|
2014-12-10 17:49:04 +00:00
|
|
|
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);
|
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
cell.messageTextView.attributedText = message.attributedTextMessage;
|
2015-02-03 14:34:32 +00:00
|
|
|
|
|
|
|
// Add a long gesture recognizer on text view in order to display event details
|
|
|
|
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPressGesture:)];
|
|
|
|
cell.messageTextView.tag = ROOM_MESSAGE_CELL_TEXTVIEW_TAG;
|
|
|
|
[cell.messageTextView addGestureRecognizer:longPress];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-11-27 09:50:09 +00:00
|
|
|
|
2014-11-28 09:51:22 +00:00
|
|
|
// Handle timestamp display
|
2015-01-13 16:53:31 +00:00
|
|
|
if (displayMsgTimestamp) {
|
2014-11-28 09:51:22 +00:00
|
|
|
// Add datetime label for each component
|
2014-12-10 11:05:11 +00:00
|
|
|
cell.dateTimeLabelContainer.hidden = NO;
|
2015-01-20 11:41:27 +00:00
|
|
|
[message checkComponentsHeight];
|
2014-12-10 11:05:11 +00:00
|
|
|
CGFloat yPosition = (message.messageType == RoomMessageTypeText) ? ROOM_MESSAGE_TEXTVIEW_MARGIN : -ROOM_MESSAGE_TEXTVIEW_MARGIN;
|
2014-11-28 09:51:22 +00:00
|
|
|
for (RoomMessageComponent *component in message.components) {
|
2015-01-20 15:29:07 +00:00
|
|
|
if (component.date && (component.style != RoomMessageComponentStyleFailed)) {
|
2014-11-28 18:33:40 +00:00
|
|
|
UILabel *dateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, yPosition, cell.dateTimeLabelContainer.frame.size.width , 20)];
|
2014-11-28 09:51:22 +00:00
|
|
|
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];
|
2014-11-28 18:33:40 +00:00
|
|
|
[cell.dateTimeLabelContainer addSubview:dateTimeLabel];
|
2014-11-28 09:51:22 +00:00
|
|
|
// Force dateTimeLabel in full width (to handle auto-layout in case of screen rotation)
|
|
|
|
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
|
|
|
|
attribute:NSLayoutAttributeLeading
|
|
|
|
relatedBy:NSLayoutRelationEqual
|
2014-11-28 18:33:40 +00:00
|
|
|
toItem:cell.dateTimeLabelContainer
|
2014-11-28 09:51:22 +00:00
|
|
|
attribute:NSLayoutAttributeLeading
|
|
|
|
multiplier:1.0
|
|
|
|
constant:0];
|
|
|
|
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
|
|
|
|
attribute:NSLayoutAttributeTrailing
|
|
|
|
relatedBy:NSLayoutRelationEqual
|
2014-11-28 18:33:40 +00:00
|
|
|
toItem:cell.dateTimeLabelContainer
|
2014-11-28 09:51:22 +00:00
|
|
|
attribute:NSLayoutAttributeTrailing
|
|
|
|
multiplier:1.0
|
|
|
|
constant:0];
|
2014-11-28 16:53:27 +00:00
|
|
|
// Vertical constraints are required for iOS > 8
|
|
|
|
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel
|
|
|
|
attribute:NSLayoutAttributeTop
|
|
|
|
relatedBy:NSLayoutRelationEqual
|
2014-11-28 18:33:40 +00:00
|
|
|
toItem:cell.dateTimeLabelContainer
|
2014-11-28 16:53:27 +00:00
|
|
|
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];
|
2014-11-28 09:51:22 +00:00
|
|
|
if ([NSLayoutConstraint respondsToSelector:@selector(activateConstraints:)]) {
|
2014-11-28 16:53:27 +00:00
|
|
|
[NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
|
2014-11-28 09:51:22 +00:00
|
|
|
} else {
|
2014-11-28 18:33:40 +00:00
|
|
|
[cell.dateTimeLabelContainer addConstraint:leftConstraint];
|
|
|
|
[cell.dateTimeLabelContainer addConstraint:rightConstraint];
|
|
|
|
[cell.dateTimeLabelContainer addConstraint:topConstraint];
|
2014-11-28 16:53:27 +00:00
|
|
|
[dateTimeLabel addConstraint:heightConstraint];
|
2014-11-28 09:51:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
yPosition += component.height;
|
|
|
|
}
|
|
|
|
}
|
2015-01-14 16:51:53 +00:00
|
|
|
|
2014-11-27 09:50:09 +00:00
|
|
|
return cell;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - UITableView delegate
|
|
|
|
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
// Check table view members vs messages
|
|
|
|
if (tableView == self.membersTableView) {
|
|
|
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
|
|
|
} else if (tableView == self.messagesTableView) {
|
|
|
|
// Dismiss keyboard when user taps on messages table view content
|
|
|
|
[self dismissKeyboard];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-21 10:18:03 +00:00
|
|
|
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath {
|
|
|
|
// Release here resources, and restore reusable cells
|
|
|
|
|
|
|
|
// Check table view members vs messages
|
2015-02-18 16:06:53 +00:00
|
|
|
if ([cell isKindOfClass:[RoomMemberTableCell class]]) {
|
2015-01-21 10:18:03 +00:00
|
|
|
RoomMemberTableCell *memberCell = (RoomMemberTableCell*)cell;
|
|
|
|
// Stop potential timer used to refresh member's presence
|
|
|
|
[memberCell setRoomMember:nil withRoom:nil];
|
2015-02-18 16:06:53 +00:00
|
|
|
} else if ([cell isKindOfClass:[RoomMessageTableCell class]]) {
|
2015-01-21 10:18:03 +00:00
|
|
|
RoomMessageTableCell *msgCell = (RoomMessageTableCell*)cell;
|
|
|
|
if ([cell isKindOfClass:[OutgoingMessageTableCell class]]) {
|
|
|
|
OutgoingMessageTableCell *outgoingMsgCell = (OutgoingMessageTableCell*)cell;
|
|
|
|
// Hide potential loading wheel
|
|
|
|
[outgoingMsgCell stopAnimating];
|
|
|
|
}
|
|
|
|
msgCell.message = nil;
|
|
|
|
|
|
|
|
// Remove all gesture recognizer
|
|
|
|
while (msgCell.attachmentView.gestureRecognizers.count) {
|
|
|
|
[msgCell.attachmentView removeGestureRecognizer:msgCell.attachmentView.gestureRecognizers[0]];
|
|
|
|
}
|
|
|
|
// Remove potential dateTime (or unsent) label(s)
|
|
|
|
if (msgCell.dateTimeLabelContainer.subviews.count > 0) {
|
|
|
|
if ([NSLayoutConstraint respondsToSelector:@selector(deactivateConstraints:)]) {
|
|
|
|
[NSLayoutConstraint deactivateConstraints:msgCell.dateTimeLabelContainer.constraints];
|
|
|
|
} else {
|
|
|
|
[msgCell.dateTimeLabelContainer removeConstraints:msgCell.dateTimeLabelContainer.constraints];
|
|
|
|
}
|
|
|
|
for (UIView *view in msgCell.dateTimeLabelContainer.subviews) {
|
|
|
|
[view removeFromSuperview];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[msgCell stopProgressUI];
|
|
|
|
|
|
|
|
// Remove long tap gesture on the progressView
|
|
|
|
while (msgCell.progressView.gestureRecognizers.count) {
|
|
|
|
[msgCell.progressView removeGestureRecognizer:msgCell.progressView.gestureRecognizers[0]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
// 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 ?
|
2014-12-16 10:09:07 +00:00
|
|
|
if (scrollView.contentOffset.y < -64) {
|
2014-12-04 15:33:31 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self triggerBackPagination];
|
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-17 09:01:55 +00:00
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
|
|
// Consider this callback to reset scrolling to bottom flag
|
|
|
|
isScrollingToBottom = NO;
|
|
|
|
}
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
#pragma mark - HPGrowingTextView delegate
|
2014-11-24 09:38:23 +00:00
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
- (void)growingTextViewDidEndEditing:(HPGrowingTextView *)growingTextView {
|
|
|
|
if (growingTextView == _messageTextView) {
|
|
|
|
[self handleTypingNotification:NO];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)growingTextViewDidChange:(HPGrowingTextView *)growingTextView {
|
|
|
|
if (growingTextView == _messageTextView) {
|
|
|
|
NSString *msg = _messageTextView.text;
|
|
|
|
|
2015-01-14 16:51:53 +00:00
|
|
|
// save the last edited text
|
|
|
|
// to do not send unexpected typing events
|
|
|
|
// HPGrowingTextView triggers growingTextViewDidChange event when it recomposes itself
|
|
|
|
if (![lastEditedText isEqualToString:msg]) {
|
|
|
|
lastEditedText = msg;
|
|
|
|
if (msg.length) {
|
|
|
|
[self handleTypingNotification:YES];
|
|
|
|
_sendBtn.enabled = YES;
|
|
|
|
_sendBtn.alpha = 1;
|
|
|
|
// Reset potential placeholder (used in case of wrong command usage)
|
|
|
|
_messageTextView.placeholder = nil;
|
|
|
|
} else {
|
|
|
|
[self handleTypingNotification:NO];
|
|
|
|
_sendBtn.enabled = NO;
|
|
|
|
_sendBtn.alpha = 0.5;
|
|
|
|
}
|
2014-12-15 15:54:31 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
|
2015-02-16 15:20:33 +00:00
|
|
|
// Update _messageTextView's superview (controlView)
|
2015-02-16 09:37:17 +00:00
|
|
|
CGFloat controlViewUpdatedHeight = height + _messageTextViewTopConstraint.constant + _messageTextViewBottomConstraint.constant;
|
|
|
|
_controlViewHeightConstraint.constant = controlViewUpdatedHeight;
|
2015-01-14 14:17:20 +00:00
|
|
|
|
2015-02-17 09:01:55 +00:00
|
|
|
// We will scroll to bottom after updating tableView if the current table position is already at the bottom.
|
|
|
|
BOOL shouldScrollToBottom = [self isMessagesTableScrollViewAtTheBottom];
|
2015-01-14 17:15:12 +00:00
|
|
|
|
2015-02-16 15:20:33 +00:00
|
|
|
// Update messages table view inset (only if change is observed)
|
|
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
|
|
|
CGFloat insetBottom;
|
|
|
|
if (isKeyboardDisplayed) {
|
|
|
|
insetBottom = keyboardHeight + controlViewUpdatedHeight - defaultMessagesTableViewBottomConstraint;
|
2015-01-22 16:31:44 +00:00
|
|
|
} else {
|
2015-02-16 15:20:33 +00:00
|
|
|
insetBottom = [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height + controlViewUpdatedHeight - defaultMessagesTableViewBottomConstraint;
|
2015-01-22 16:31:44 +00:00
|
|
|
}
|
2015-02-16 15:20:33 +00:00
|
|
|
if (insets.bottom != insetBottom) {
|
|
|
|
insets.bottom = insetBottom;
|
|
|
|
self.messagesTableView.contentInset = insets;
|
|
|
|
// Force to render the view
|
|
|
|
[self.view layoutIfNeeded];
|
|
|
|
|
|
|
|
// Adjust scroll view
|
|
|
|
if (shouldScrollToBottom) {
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:NO];
|
2015-01-22 16:31:44 +00:00
|
|
|
}
|
2015-01-14 14:17:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - UITextField delegate
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
|
2014-12-15 15:54:31 +00:00
|
|
|
NSString *alertMsg = nil;
|
|
|
|
|
|
|
|
if (textField == _roomTitleView.displayNameTextField) {
|
2014-12-08 14:48:35 +00:00
|
|
|
// Check whether the user has enough power to rename the room
|
2014-12-11 17:10:15 +00:00
|
|
|
MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels];
|
2015-01-23 14:36:05 +00:00
|
|
|
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:[MatrixSDKHandler sharedHandler].userId];
|
2014-12-22 09:17:08 +00:00
|
|
|
if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomName]) {
|
2014-12-12 13:58:53 +00:00
|
|
|
// Only the room name is edited here, update the text field with the room name
|
2014-12-15 15:54:31 +00:00
|
|
|
textField.text = self.mxRoom.state.name;
|
|
|
|
textField.backgroundColor = [UIColor whiteColor];
|
2014-12-08 14:48:35 +00:00
|
|
|
} else {
|
2014-12-15 15:54:31 +00:00
|
|
|
alertMsg = @"You are not authorized to edit this room name";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether the user is allowed to change room topic
|
2014-12-22 09:17:08 +00:00
|
|
|
if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomTopic]) {
|
2014-12-15 15:54:31 +00:00
|
|
|
// 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;
|
2014-12-08 14:48:35 +00:00
|
|
|
}
|
|
|
|
}
|
2014-12-15 15:54:31 +00:00
|
|
|
} else if (textField == _roomTitleView.topicTextField) {
|
|
|
|
// Check whether the user has enough power to edit room topic
|
|
|
|
MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels];
|
2015-01-23 14:36:05 +00:00
|
|
|
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:[MatrixSDKHandler sharedHandler].userId];
|
2014-12-22 09:17:08 +00:00
|
|
|
if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomTopic]) {
|
2014-12-15 15:54:31 +00:00
|
|
|
textField.backgroundColor = [UIColor whiteColor];
|
2014-12-19 07:03:39 +00:00
|
|
|
[self.roomTitleView stopTopicAnimation];
|
2014-12-15 15:54:31 +00:00
|
|
|
} 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];
|
|
|
|
}
|
2015-01-23 12:46:27 +00:00
|
|
|
self.actionMenu = [[MXCAlert alloc] initWithTitle:nil message:alertMsg style:MXCAlertStyleAlert];
|
|
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-12-15 15:54:31 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
|
|
|
[self.actionMenu showInViewController:self];
|
2014-12-08 14:48:35 +00:00
|
|
|
return NO;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
2014-12-15 15:54:31 +00:00
|
|
|
if (textField == _roomTitleView.displayNameTextField) {
|
|
|
|
textField.backgroundColor = [UIColor clearColor];
|
2014-12-08 14:48:35 +00:00
|
|
|
|
2014-12-15 15:54:31 +00:00
|
|
|
NSString *roomName = textField.text;
|
|
|
|
if ((roomName.length || self.mxRoom.state.name.length) && [roomName isEqualToString:self.mxRoom.state.name] == NO) {
|
2014-12-09 16:09:51 +00:00
|
|
|
[self startActivityIndicator];
|
2014-12-11 17:37:25 +00:00
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
[self.mxRoom setName:roomName success:^{
|
|
|
|
[weakSelf stopActivityIndicator];
|
2014-12-15 15:54:31 +00:00
|
|
|
// Refresh title display
|
|
|
|
textField.text = weakSelf.mxRoom.state.displayname;
|
2014-11-24 09:38:23 +00:00
|
|
|
} failure:^(NSError *error) {
|
2014-12-11 17:37:25 +00:00
|
|
|
[weakSelf stopActivityIndicator];
|
2014-11-24 09:38:23 +00:00
|
|
|
// Revert change
|
2014-12-15 15:54:31 +00:00
|
|
|
textField.text = weakSelf.mxRoom.state.displayname;
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Rename room failed: %@", error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
2014-12-12 13:58:53 +00:00
|
|
|
} else {
|
|
|
|
// No change on room name, restore title with room displayName
|
2014-12-15 15:54:31 +00:00
|
|
|
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;
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Topic room change failed: %@", error);
|
2014-12-15 15:54:31 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Hide topic field if empty
|
|
|
|
_roomTitleView.hiddenTopic = !topic.length;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
2014-12-08 14:48:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
|
2014-12-15 15:54:31 +00:00
|
|
|
if (textField == _roomTitleView.displayNameTextField) {
|
|
|
|
// "Next" key has been pressed
|
|
|
|
[_roomTitleView.topicTextField becomeFirstResponder];
|
|
|
|
} else {
|
|
|
|
// "Done" key has been pressed
|
|
|
|
[textField resignFirstResponder];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
|
|
- (IBAction)onButtonPressed:(id)sender {
|
|
|
|
if (sender == _sendBtn) {
|
2015-01-14 14:17:20 +00:00
|
|
|
NSString *msgTxt = self.messageTextView.text;
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
// Handle potential commands in room chat
|
|
|
|
if ([self isIRCStyleCommand:msgTxt] == NO) {
|
2014-12-22 09:17:08 +00:00
|
|
|
[self sendTextMessage:msgTxt];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.text = nil;
|
2015-01-13 15:07:34 +00:00
|
|
|
[self handleTypingNotification:NO];
|
2014-11-24 09:38:23 +00:00
|
|
|
// disable send button
|
2015-01-07 10:19:59 +00:00
|
|
|
_sendBtn.enabled = NO;
|
|
|
|
_sendBtn.alpha = 0.5;
|
2014-11-24 09:38:23 +00:00
|
|
|
} else if (sender == _optionBtn) {
|
|
|
|
[self dismissKeyboard];
|
|
|
|
|
|
|
|
// Display action menu: Add attachments, Invite user...
|
|
|
|
__weak typeof(self) weakSelf = self;
|
2015-01-23 12:46:27 +00:00
|
|
|
self.actionMenu = [[MXCAlert alloc] initWithTitle:@"Select an action:" message:nil style:MXCAlertStyleActionSheet];
|
2014-11-24 09:38:23 +00:00
|
|
|
// Attachments
|
2015-01-23 12:46:27 +00:00
|
|
|
[self.actionMenu addActionWithTitle:@"Attach" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
if (weakSelf) {
|
|
|
|
// Ask for attachment type
|
2015-01-23 12:46:27 +00:00
|
|
|
weakSelf.actionMenu = [[MXCAlert alloc] initWithTitle:@"Select an attachment type:" message:nil style:MXCAlertStyleActionSheet];
|
|
|
|
[weakSelf.actionMenu addActionWithTitle:@"Media" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
if (weakSelf) {
|
|
|
|
weakSelf.actionMenu = nil;
|
2014-12-22 10:52:29 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2015-01-23 12:46:27 +00:00
|
|
|
weakSelf.actionMenu = [[MXCAlert alloc] initWithTitle:@"Media:" message:nil style:MXCAlertStyleActionSheet];
|
2014-12-22 10:52:29 +00:00
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
[weakSelf.actionMenu addActionWithTitle:@"Photo Library" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-12-22 14:45:43 +00:00
|
|
|
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];
|
|
|
|
}
|
2014-12-22 10:52:29 +00:00
|
|
|
}];
|
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
[weakSelf.actionMenu addActionWithTitle:@"Take Photo or Video" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-12-22 14:45:43 +00:00
|
|
|
if (weakSelf) {
|
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
|
|
|
|
// Open media gallery
|
|
|
|
UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init];
|
|
|
|
mediaPicker.delegate = weakSelf;
|
|
|
|
mediaPicker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
|
|
|
mediaPicker.allowsEditing = NO;
|
|
|
|
mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
|
|
|
|
[[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker];
|
|
|
|
}
|
2014-12-22 10:52:29 +00:00
|
|
|
}];
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-12-22 10:55:28 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
2014-12-22 10:52:29 +00:00
|
|
|
[weakSelf.actionMenu showInViewController:weakSelf];
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-22 10:52:29 +00:00
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2014-12-22 10:52:29 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
}];
|
2015-01-23 12:46:27 +00:00
|
|
|
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
|
|
|
[weakSelf.actionMenu showInViewController:weakSelf];
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
// Invitation
|
2015-01-23 12:46:27 +00:00
|
|
|
[self.actionMenu addActionWithTitle:@"Invite" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
if (weakSelf) {
|
|
|
|
// Ask for userId to invite
|
2015-01-23 12:46:27 +00:00
|
|
|
weakSelf.actionMenu = [[MXCAlert alloc] initWithTitle:@"User ID:" message:nil style:MXCAlertStyleAlert];
|
|
|
|
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
}];
|
|
|
|
[weakSelf.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
|
|
textField.secureTextEntry = NO;
|
|
|
|
textField.placeholder = @"ex: @bob:homeserver";
|
|
|
|
}];
|
2015-01-23 12:46:27 +00:00
|
|
|
[weakSelf.actionMenu addActionWithTitle:@"Invite" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-11-24 09:38:23 +00:00
|
|
|
UITextField *textField = [alert textFieldAtIndex:0];
|
|
|
|
NSString *userId = textField.text;
|
|
|
|
weakSelf.actionMenu = nil;
|
|
|
|
if (userId.length) {
|
2014-12-11 17:10:15 +00:00
|
|
|
[weakSelf.mxRoom inviteUser:userId success:^{
|
2014-11-24 09:38:23 +00:00
|
|
|
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Invite %@ failed: %@", userId, error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
[weakSelf.actionMenu showInViewController:weakSelf];
|
|
|
|
}
|
|
|
|
}];
|
2015-01-23 12:46:27 +00:00
|
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
2014-12-22 10:55:28 +00:00
|
|
|
weakSelf.actionMenu = nil;
|
2014-11-24 09:38:23 +00:00
|
|
|
}];
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
2015-01-16 10:40:38 +00:00
|
|
|
- (IBAction)onResendToggle:(id)sender {
|
|
|
|
// sanity check
|
|
|
|
if ([sender isKindOfClass:[UIButton class]]) {
|
2015-02-03 14:34:32 +00:00
|
|
|
id hiddenLabel = [(UIButton*)sender viewWithTag:ROOM_MESSAGE_CELL_HIDDEN_UNSENT_MSG_LABEL_TAG];
|
2015-01-16 10:40:38 +00:00
|
|
|
|
|
|
|
// get the hidden label where the event ID is store
|
|
|
|
if ([hiddenLabel isKindOfClass:[UILabel class]]) {
|
|
|
|
NSString* eventID =((UILabel*)hiddenLabel).text;
|
2015-02-19 12:08:43 +00:00
|
|
|
if (eventID.length) {
|
|
|
|
[self promptUserToResendEvent:eventID];
|
2015-01-16 10:40:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
#pragma mark - Post messages
|
|
|
|
|
2014-12-22 09:17:08 +00:00
|
|
|
- (void)sendMessage:(NSDictionary*)msgContent withLocalEvent:(MXEvent*)localEvent {
|
2014-11-24 09:38:23 +00:00
|
|
|
MXMessageType msgType = msgContent[@"msgtype"];
|
|
|
|
if (msgType) {
|
|
|
|
// Check whether a temporary event has already been added for local echo (this happens on attachments)
|
2014-11-27 09:50:09 +00:00
|
|
|
if (localEvent) {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Look for this local event in messages
|
|
|
|
RoomMessage *message = [self messageWithEventId:localEvent.eventId];
|
2015-01-16 09:42:36 +00:00
|
|
|
if (message) {
|
|
|
|
// Update the local event with the actual msg content
|
|
|
|
localEvent.content = msgContent;
|
|
|
|
if (message.thumbnailURL) {
|
|
|
|
// Reuse the current thumbnailURL as preview
|
|
|
|
[localEvent.content setValue:message.thumbnailURL forKey:kRoomMessageLocalPreviewKey];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.messageType == RoomMessageTypeText) {
|
|
|
|
[message removeEvent:localEvent.eventId];
|
|
|
|
[message addEvent:localEvent withRoomState:self.mxRoom.state];
|
|
|
|
if (!message.components.count) {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:message];
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Create a new message
|
2015-01-16 15:39:58 +00:00
|
|
|
RoomMessage *aNewMessage = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:self.mxRoom.state];
|
|
|
|
if (aNewMessage) {
|
|
|
|
[self replaceMessage:message withMessage:aNewMessage];
|
2015-01-16 09:42:36 +00:00
|
|
|
} else {
|
2015-01-16 15:39:58 +00:00
|
|
|
[self removeMessage:message];
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
|
2014-12-04 10:43:43 +00:00
|
|
|
[self.messagesTableView reloadData];
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
2014-12-19 13:16:22 +00:00
|
|
|
// Add a new local event
|
|
|
|
localEvent = [self createLocalEchoEventWithoutContent];
|
2014-11-27 09:50:09 +00:00
|
|
|
localEvent.content = msgContent;
|
2014-12-19 13:16:22 +00:00
|
|
|
[self addLocalEchoEvent:localEvent];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send message to the room
|
2015-01-09 13:13:03 +00:00
|
|
|
[self.mxRoom sendMessageOfType:msgType content:msgContent success:^(NSString *eventId) {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Switch on processing queue
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2015-01-16 15:39:58 +00:00
|
|
|
// Check whether the event is still pending (It may be received from event stream)
|
|
|
|
NSUInteger index;
|
|
|
|
MXEvent *pendingEvent = nil;
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized(self) {
|
2015-01-16 15:39:58 +00:00
|
|
|
for (index = 0; index < pendingOutgoingEvents.count; index++) {
|
|
|
|
pendingEvent = [pendingOutgoingEvents objectAtIndex:index];
|
|
|
|
if ([pendingEvent.eventId isEqualToString:localEvent.eventId]) {
|
|
|
|
// This event is not pending anymore
|
|
|
|
[pendingOutgoingEvents removeObjectAtIndex:index];
|
2015-01-16 09:42:36 +00:00
|
|
|
break;
|
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
pendingEvent = nil;
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-16 15:39:58 +00:00
|
|
|
if (pendingEvent) {
|
2015-01-16 17:23:09 +00:00
|
|
|
// Replace the local event id in the related message
|
2015-01-16 15:39:58 +00:00
|
|
|
RoomMessage *message = [self messageWithEventId:localEvent.eventId];
|
|
|
|
if (message) {
|
2015-01-16 17:23:09 +00:00
|
|
|
[message replaceLocalEventId:localEvent.eventId withEventId:eventId];
|
2014-11-27 09:50:09 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
|
2015-01-16 17:23:09 +00:00
|
|
|
// Note: Messages table will be refreshed for this outgoing event on event stream notification (see messagesListener)
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
});
|
2015-01-14 13:05:45 +00:00
|
|
|
} failure:^(NSError *error) {
|
2015-01-16 15:39:58 +00:00
|
|
|
// Switch on processing queue to serialize this operation with the message handling
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2015-01-16 15:39:58 +00:00
|
|
|
// Check whether the event is still pending (It may be received from event stream)
|
|
|
|
NSUInteger index;
|
|
|
|
MXEvent *pendingEvent = nil;
|
|
|
|
@synchronized (self) {
|
|
|
|
for (index = 0; index < pendingOutgoingEvents.count; index++) {
|
|
|
|
pendingEvent = [pendingOutgoingEvents objectAtIndex:index];
|
|
|
|
if ([pendingEvent.eventId isEqualToString:localEvent.eventId]) {
|
|
|
|
// This event is not pending anymore
|
|
|
|
[pendingOutgoingEvents removeObjectAtIndex:index];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
pendingEvent = nil;
|
2015-01-16 09:42:36 +00:00
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
|
|
|
|
if (pendingEvent) {
|
|
|
|
// Handle error
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self handleError:error forLocalEvent:localEvent];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-22 09:17:08 +00:00
|
|
|
- (void)sendTextMessage:(NSString*)msgTxt {
|
2014-11-24 09:38:23 +00:00
|
|
|
MXMessageType msgType = kMXMessageTypeText;
|
|
|
|
// Check whether the message is an emote
|
|
|
|
if ([msgTxt hasPrefix:@"/me "]) {
|
|
|
|
msgType = kMXMessageTypeEmote;
|
|
|
|
// Remove "/me " string
|
|
|
|
msgTxt = [msgTxt substringFromIndex:4];
|
|
|
|
}
|
|
|
|
|
2014-12-22 09:17:08 +00:00
|
|
|
[self sendMessage:@{@"msgtype":msgType, @"body":msgTxt} withLocalEvent:nil];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
- (MXEvent*)createLocalEchoEventWithoutContent {
|
2014-11-24 09:38:23 +00:00
|
|
|
// Create a temporary event to displayed outgoing message (local echo)
|
|
|
|
NSString *localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
|
2014-12-19 13:16:22 +00:00
|
|
|
MXEvent *localEvent = [[MXEvent alloc] init];
|
|
|
|
localEvent.roomId = self.roomId;
|
|
|
|
localEvent.eventId = localEventId;
|
|
|
|
localEvent.type = kMXEventTypeStringRoomMessage;
|
2015-01-12 16:35:38 +00:00
|
|
|
localEvent.originServerTs = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000);
|
2014-12-19 13:16:22 +00:00
|
|
|
|
2015-01-23 14:36:05 +00:00
|
|
|
localEvent.userId = [MatrixSDKHandler sharedHandler].userId;
|
2014-12-19 13:16:22 +00:00
|
|
|
return localEvent;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)addLocalEchoEvent:(MXEvent*)mxEvent {
|
|
|
|
// Check whether this new event may be grouped with last message
|
2015-01-16 09:42:36 +00:00
|
|
|
RoomMessage *lastMessage;
|
|
|
|
@synchronized (self) {
|
|
|
|
lastMessage = [messages lastObject];
|
|
|
|
}
|
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
if (lastMessage == nil || [lastMessage addEvent:mxEvent withRoomState:self.mxRoom.state] == NO) {
|
|
|
|
// Create a new RoomMessage
|
|
|
|
lastMessage = [[RoomMessage alloc] initWithEvent:mxEvent andRoomState:self.mxRoom.state];
|
|
|
|
if (lastMessage) {
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized (self) {
|
|
|
|
[messages addObject:lastMessage];
|
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
} else {
|
|
|
|
lastMessage = nil;
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Unable to add local event: %@", mxEvent.description);
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastMessage) {
|
2015-01-16 09:42:36 +00:00
|
|
|
@synchronized (self) {
|
|
|
|
// Report this event as pending one
|
|
|
|
[pendingOutgoingEvents addObject:mxEvent];
|
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
|
|
|
|
// Refresh table display
|
|
|
|
[self.messagesTableView reloadData];
|
2015-02-17 09:01:55 +00:00
|
|
|
[self scrollMessagesTableViewToBottomAnimated:NO];
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
- (NSData*)cachedImageData:(UIImage*)image withURL:(NSString*)url {
|
2015-01-13 11:56:48 +00:00
|
|
|
NSData *imageData = UIImageJPEGRepresentation(image, 0.8);
|
2015-01-19 13:33:57 +00:00
|
|
|
NSString *cacheFilePath = [MediaManager cacheMediaData:imageData forURL:url andType:@"image/jpeg" inFolder:self.roomId];
|
2014-11-24 09:38:23 +00:00
|
|
|
if (cacheFilePath) {
|
|
|
|
if (tmpCachedAttachments == nil) {
|
|
|
|
tmpCachedAttachments = [NSMutableArray array];
|
|
|
|
}
|
|
|
|
[tmpCachedAttachments addObject:cacheFilePath];
|
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
return imageData;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (MXEvent*)addLocalEchoEventForAttachedImage:(UIImage*)image {
|
|
|
|
// Create new item
|
|
|
|
MXEvent *localEvent = [self createLocalEchoEventWithoutContent];
|
|
|
|
// We store temporarily the image in cache, use the localId to build temporary url
|
|
|
|
NSString *dummyURL = [NSString stringWithFormat:@"%@%@", kMediaManagerPrefixForDummyURL, localEvent.eventId];
|
|
|
|
NSData* imageData = [self cachedImageData:image withURL:dummyURL];
|
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
// Prepare event content
|
2014-12-19 16:16:24 +00:00
|
|
|
NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
|
|
|
|
[info setValue:@"image/jpeg" forKey:@"mimetype"];
|
|
|
|
[info setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.width] forKey:@"w"];
|
|
|
|
[info setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.height] forKey:@"h"];
|
|
|
|
[info setValue:[NSNumber numberWithUnsignedInteger:imageData.length] forKey:@"size"];
|
2015-01-13 11:56:48 +00:00
|
|
|
localEvent.content = @{@"msgtype":kMXMessageTypeImage, @"url":dummyURL, @"info":info, kRoomMessageUploadIdKey:localEvent.eventId};
|
2015-01-12 17:28:43 +00:00
|
|
|
// Note: we have defined here an upload id with the local event id
|
2014-12-16 09:05:37 +00:00
|
|
|
|
2014-12-19 13:16:22 +00:00
|
|
|
// Add this new event
|
|
|
|
[self addLocalEchoEvent:localEvent];
|
|
|
|
return localEvent;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
- (MXEvent*)addLocalEchoEventForAttachedVideo:(UIImage*)thumbnail videoPath:(NSString*)videoPath {
|
|
|
|
// Create new item
|
|
|
|
MXEvent *localEvent = [self createLocalEchoEventWithoutContent];
|
|
|
|
NSString *dummyURL = [NSString stringWithFormat:@"%@%@", kMediaManagerPrefixForDummyURL, localEvent.eventId];
|
|
|
|
NSData* imageData = [self cachedImageData:thumbnail withURL:dummyURL];
|
|
|
|
|
|
|
|
NSMutableDictionary* content = [[NSMutableDictionary alloc] init];
|
|
|
|
|
|
|
|
[content setObject:kMXMessageTypeVideo forKey:@"msgtype"];
|
|
|
|
[content setObject:videoPath forKey:@"url"];
|
|
|
|
|
|
|
|
// thumbnail
|
|
|
|
NSMutableDictionary *thumbnail_info = [[NSMutableDictionary alloc] init];
|
|
|
|
[thumbnail_info setValue:@"image/jpeg" forKey:@"mimetype"];
|
|
|
|
[thumbnail_info setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.width] forKey:@"w"];
|
|
|
|
[thumbnail_info setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.height] forKey:@"h"];
|
|
|
|
[thumbnail_info setValue:[NSNumber numberWithUnsignedInteger:imageData.length] forKey:@"size"];
|
|
|
|
|
|
|
|
NSMutableDictionary* attachmentInfo = [[NSMutableDictionary alloc] init];
|
|
|
|
[attachmentInfo setValue:dummyURL forKey:@"thumbnail_url"];
|
|
|
|
[attachmentInfo setValue:thumbnail_info forKey:@"thumbnail_info"];
|
|
|
|
|
|
|
|
[content setObject:attachmentInfo forKey:@"info"];
|
|
|
|
[content setObject:localEvent.eventId forKey:kRoomMessageUploadIdKey];
|
|
|
|
|
|
|
|
localEvent.content = content;
|
|
|
|
|
|
|
|
// Add this new event
|
|
|
|
[self addLocalEchoEvent:localEvent];
|
|
|
|
return localEvent;
|
|
|
|
}
|
|
|
|
|
2014-11-27 09:50:09 +00:00
|
|
|
- (void)handleError:(NSError *)error forLocalEvent:(MXEvent *)localEvent {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Post message failed: %@", error);
|
2014-11-24 09:38:23 +00:00
|
|
|
if (error) {
|
|
|
|
// Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}
|
|
|
|
|
2015-01-23 14:36:05 +00:00
|
|
|
dispatch_async([MatrixSDKHandler sharedHandler].processingQueue, ^{
|
2015-01-16 15:39:58 +00:00
|
|
|
// Update the temporary event with this local event id
|
|
|
|
RoomMessage *message = [self messageWithEventId:localEvent.eventId];
|
|
|
|
if (message) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Posted event: %@", localEvent.description);
|
2015-01-20 11:41:27 +00:00
|
|
|
NSString *failedEventId = [NSString stringWithFormat:@"%@%lld", kFailedEventIdPrefix, (long long)(CFAbsoluteTimeGetCurrent() * 1000)];
|
|
|
|
[message replaceLocalEventId:localEvent.eventId withEventId:failedEventId];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
2015-01-16 15:39:58 +00:00
|
|
|
|
|
|
|
// Reload on the right thread
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
[self.messagesTableView reloadData];
|
|
|
|
});
|
2015-01-13 16:53:31 +00:00
|
|
|
});
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
|
2015-01-14 13:05:45 +00:00
|
|
|
- (MXEvent*)pendingEventRelatedToEvent:(MXEvent*)mxEvent {
|
2014-12-19 13:16:22 +00:00
|
|
|
// Note: mxEvent is supposed here to be an outgoing event received from event stream.
|
2015-01-14 13:05:45 +00:00
|
|
|
// This method returns a pending event (if any) whose content matches with received event content.
|
2014-12-19 13:16:22 +00:00
|
|
|
NSString *msgtype = mxEvent.content[@"msgtype"];
|
|
|
|
|
2015-01-16 09:42:36 +00:00
|
|
|
MXEvent *pendingEvent = nil;
|
|
|
|
@synchronized(self) {
|
|
|
|
for (NSInteger index = 0; index < pendingOutgoingEvents.count; index++) {
|
|
|
|
pendingEvent = [pendingOutgoingEvents objectAtIndex:index];
|
|
|
|
NSString *pendingEventType = pendingEvent.content[@"msgtype"];
|
|
|
|
|
|
|
|
if ([msgtype isEqualToString:pendingEventType]) {
|
|
|
|
if ([msgtype isEqualToString:kMXMessageTypeText] || [msgtype isEqualToString:kMXMessageTypeEmote]) {
|
|
|
|
// Compare content body
|
|
|
|
if ([mxEvent.content[@"body"] isEqualToString:pendingEvent.content[@"body"]]) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else if ([msgtype isEqualToString:kMXMessageTypeLocation]) {
|
|
|
|
// Compare geo uri
|
|
|
|
if ([mxEvent.content[@"geo_uri"] isEqualToString:pendingEvent.content[@"geo_uri"]]) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Here the type is kMXMessageTypeImage, kMXMessageTypeAudio or kMXMessageTypeVideo
|
|
|
|
if ([mxEvent.content[@"url"] isEqualToString:pendingEvent.content[@"url"]]) {
|
|
|
|
break;
|
|
|
|
}
|
2015-01-14 13:05:45 +00:00
|
|
|
}
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
pendingEvent = nil;
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
|
|
|
}
|
2015-01-16 09:42:36 +00:00
|
|
|
|
|
|
|
return pendingEvent;
|
2014-12-19 13:16:22 +00:00
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
- (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]) {
|
2014-12-22 09:17:08 +00:00
|
|
|
// send message as an emote
|
|
|
|
[self sendTextMessage:text];
|
2014-11-24 09:38:23 +00:00
|
|
|
} 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) {
|
2015-01-23 14:36:05 +00:00
|
|
|
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
2014-11-24 09:38:23 +00:00
|
|
|
[mxHandler.mxRestClient setDisplayName:displayName success:^{
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Set displayName failed: %@", error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /nick <display_name>";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} 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) {
|
2015-01-23 14:36:05 +00:00
|
|
|
[[MatrixSDKHandler sharedHandler].mxSession joinRoom:roomAlias success:^(MXRoom *room) {
|
2014-12-17 09:55:33 +00:00
|
|
|
// Show the room
|
|
|
|
[[AppDelegate theDelegate].masterTabBarController showRoom:room.state.roomId];
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Join roomAlias (%@) failed: %@", roomAlias, error);
|
2014-12-17 09:55:33 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /join <room_alias>";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} 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
|
2014-12-11 17:10:15 +00:00
|
|
|
[self.mxRoom kickUser:userId reason:reason success:^{
|
2014-11-24 09:38:23 +00:00
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Kick user (%@) failed: %@", userId, error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /kick <userId> [<reason>]";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} 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
|
2014-12-11 17:10:15 +00:00
|
|
|
[self.mxRoom banUser:userId reason:reason success:^{
|
2014-11-24 09:38:23 +00:00
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Ban user (%@) failed: %@", userId, error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /ban <userId> [<reason>]";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} else if ([cmd isEqualToString:kCmdUnbanUser]) {
|
|
|
|
if (userId) {
|
|
|
|
// Unban the user
|
2014-12-11 17:10:15 +00:00
|
|
|
[self.mxRoom unbanUser:userId success:^{
|
2014-11-24 09:38:23 +00:00
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Unban user (%@) failed: %@", userId, error);
|
2014-11-24 09:38:23 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /unban <userId>";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} 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) {
|
2014-12-18 10:06:42 +00:00
|
|
|
// Set user power level
|
|
|
|
[self.mxRoom setPowerLevelOfUserWithUserID:userId powerLevel:[powerLevel integerValue] success:^{
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Set user power (%@) failed: %@", userId, error);
|
2014-12-18 10:06:42 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /op <userId> <power level>";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} else if ([cmd isEqualToString:kCmdResetUserPowerLevel]) {
|
|
|
|
if (userId) {
|
|
|
|
// Reset user power level
|
2014-12-18 10:06:42 +00:00
|
|
|
[self.mxRoom setPowerLevelOfUserWithUserID:userId powerLevel:0 success:^{
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Reset user power (%@) failed: %@", userId, error);
|
2014-12-18 10:06:42 +00:00
|
|
|
//Alert user
|
|
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
|
|
}];
|
2014-11-24 09:38:23 +00:00
|
|
|
} else {
|
|
|
|
// Display cmd usage in text input as placeholder
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = @"Usage: /deop <userId>";
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Unrecognised IRC-style command: %@", text);
|
2015-01-14 14:17:20 +00:00
|
|
|
self.messageTextView.placeholder = [NSString stringWithFormat:@"Unrecognised IRC-style command: %@", cmd];
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2015-01-07 10:19:59 +00:00
|
|
|
# pragma mark - Typing notification
|
|
|
|
|
2015-01-13 15:07:34 +00:00
|
|
|
- (void)handleTypingNotification:(BOOL)typing {
|
2015-02-18 17:29:32 +00:00
|
|
|
// Remove potential reachability observer
|
|
|
|
if (reachabilityObserver) {
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
|
|
|
|
reachabilityObserver = nil;
|
|
|
|
}
|
|
|
|
|
2015-01-13 15:07:34 +00:00
|
|
|
NSUInteger notificationTimeoutMS = -1;
|
2015-01-07 10:19:59 +00:00
|
|
|
if (typing) {
|
2015-01-13 15:07:34 +00:00
|
|
|
// Check whether a typing event has been already reported to server (We wait for the end of the local timout before considering this new event)
|
|
|
|
if (typingTimer) {
|
|
|
|
// Refresh date of the last observed typing
|
|
|
|
lastTypingDate = [[NSDate alloc] init];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Launch a timer to prevent sending multiple typing notifications
|
|
|
|
NSTimeInterval timerTimeout = ROOMVIEWCONTROLLER_TYPING_TIMEOUT_SEC;
|
|
|
|
if (lastTypingDate) {
|
|
|
|
NSTimeInterval lastTypingAge = -[lastTypingDate timeIntervalSinceNow];
|
|
|
|
if (lastTypingAge < timerTimeout) {
|
|
|
|
// Subtract the time interval since last typing from the timer timeout
|
|
|
|
timerTimeout -= lastTypingAge;
|
|
|
|
} else {
|
|
|
|
timerTimeout = 0;
|
2015-01-07 10:19:59 +00:00
|
|
|
}
|
2015-01-13 15:07:34 +00:00
|
|
|
} else {
|
|
|
|
// Keep date of this typing event
|
|
|
|
lastTypingDate = [[NSDate alloc] init];
|
2015-01-07 10:19:59 +00:00
|
|
|
}
|
2015-01-13 15:07:34 +00:00
|
|
|
|
|
|
|
if (timerTimeout) {
|
|
|
|
typingTimer = [NSTimer scheduledTimerWithTimeInterval:timerTimeout target:self selector:@selector(typingTimeout:) userInfo:self repeats:NO];
|
|
|
|
// Compute the notification timeout in ms (consider the double of the local typing timeout)
|
|
|
|
notificationTimeoutMS = 2000 * ROOMVIEWCONTROLLER_TYPING_TIMEOUT_SEC;
|
|
|
|
} else {
|
|
|
|
// This typing event is too old, we will ignore it
|
|
|
|
typing = NO;
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Ignore typing event (too old)");
|
2015-01-13 15:07:34 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Cancel any typing timer
|
|
|
|
[typingTimer invalidate];
|
|
|
|
typingTimer = nil;
|
|
|
|
// Reset last typing date
|
|
|
|
lastTypingDate = nil;
|
2015-01-07 10:19:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send typing notification to server
|
2015-01-13 15:07:34 +00:00
|
|
|
[self.mxRoom sendTypingNotification:typing
|
|
|
|
timeout:notificationTimeoutMS
|
|
|
|
success:^{
|
|
|
|
// Reset last typing date
|
|
|
|
lastTypingDate = nil;
|
|
|
|
} failure:^(NSError *error) {
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Failed to send typing notification (%d) failed: %@", typing, error);
|
2015-01-13 15:07:34 +00:00
|
|
|
// Cancel timer (if any)
|
|
|
|
[typingTimer invalidate];
|
|
|
|
typingTimer = nil;
|
2015-02-18 17:29:32 +00:00
|
|
|
|
|
|
|
// Check network reachability
|
|
|
|
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorNotConnectedToInternet) {
|
|
|
|
// Add observer to launch a new attempt according to reachability.
|
2015-02-19 09:46:00 +00:00
|
|
|
__weak typeof(self) weakSelf = self;
|
2015-02-18 17:29:32 +00:00
|
|
|
reachabilityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingReachabilityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
|
|
|
NSNumber *statusItem = note.userInfo[AFNetworkingReachabilityNotificationStatusItem];
|
|
|
|
if (statusItem) {
|
|
|
|
AFNetworkReachabilityStatus reachabilityStatus = statusItem.integerValue;
|
|
|
|
if (reachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi || reachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN) {
|
|
|
|
// New attempt
|
2015-02-19 09:46:00 +00:00
|
|
|
[weakSelf handleTypingNotification:typing];
|
2015-02-18 17:29:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
// Send again
|
|
|
|
[self handleTypingNotification:typing];
|
|
|
|
}
|
2015-01-13 15:07:34 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)typingTimeout:(id)sender {
|
|
|
|
[typingTimer invalidate];
|
|
|
|
typingTimer = nil;
|
|
|
|
|
|
|
|
// Check whether a new typing event has been observed
|
|
|
|
BOOL typing = (lastTypingDate != nil);
|
|
|
|
// Post a new typing notification
|
|
|
|
[self handleTypingNotification:typing];
|
2015-01-07 10:19:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
# pragma mark - UIImagePickerControllerDelegate
|
|
|
|
|
2014-12-22 15:50:45 +00:00
|
|
|
- (void)sendImage:(UIImage*)image {
|
2015-02-19 15:58:17 +00:00
|
|
|
// Make sure the uploaded image orientation is up
|
|
|
|
image = [MXCTools forceImageOrientationUp:image];
|
|
|
|
|
2014-12-22 10:55:28 +00:00
|
|
|
// Add a temporary event while the image is attached (local echo)
|
2015-01-13 16:53:31 +00:00
|
|
|
MXEvent *localEvent = [self addLocalEchoEventForAttachedImage:image];
|
2015-01-13 11:56:48 +00:00
|
|
|
|
2015-01-13 16:53:31 +00:00
|
|
|
// use the generated info dict to retrieve useful data
|
2015-01-13 11:56:48 +00:00
|
|
|
NSMutableDictionary *infoDict = [localEvent.content valueForKey:@"info"];
|
2015-01-19 13:33:57 +00:00
|
|
|
NSData *imageData = [NSData dataWithContentsOfFile:[MediaManager cachePathForMediaURL:[localEvent.content valueForKey:@"url"] andType:[infoDict objectForKey:@"mimetype"] inFolder:self.roomId]];
|
2015-01-07 09:45:35 +00:00
|
|
|
|
2015-01-12 17:07:39 +00:00
|
|
|
// Upload data
|
2015-01-19 13:33:57 +00:00
|
|
|
MediaLoader *mediaLoader = [MediaManager prepareUploaderWithId:localEvent.eventId initialRange:0 andRange:1.0 inFolder:self.roomId];
|
2015-01-13 11:56:48 +00:00
|
|
|
[mediaLoader uploadData:imageData mimeType:[infoDict objectForKey:@"mimetype"] success:^(NSString *url) {
|
2015-01-19 13:33:57 +00:00
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
2015-01-12 17:07:39 +00:00
|
|
|
|
2014-12-22 10:55:28 +00:00
|
|
|
NSMutableDictionary *imageMessage = [[NSMutableDictionary alloc] init];
|
2015-01-13 11:56:48 +00:00
|
|
|
[imageMessage setValue:kMXMessageTypeImage forKey:@"msgtype"];
|
2014-12-22 10:55:28 +00:00
|
|
|
[imageMessage setValue:url forKey:@"url"];
|
2015-01-13 11:56:48 +00:00
|
|
|
[imageMessage setValue:infoDict forKey:@"info"];
|
2014-12-22 10:55:28 +00:00
|
|
|
[imageMessage setValue:@"Image" forKey:@"body"];
|
|
|
|
// Send message for this attachment
|
|
|
|
[self sendMessage:imageMessage withLocalEvent:localEvent];
|
|
|
|
} failure:^(NSError *error) {
|
2015-01-19 14:30:45 +00:00
|
|
|
// check if the upload is still defined
|
|
|
|
// it could have been cancelled with an external events
|
|
|
|
if ([MediaManager existingUploaderWithId:localEvent.eventId inFolder:self.roomId])
|
|
|
|
{
|
|
|
|
[MediaManager removeUploaderWithId:localEvent.eventId inFolder:self.roomId];
|
2015-02-24 12:49:05 +00:00
|
|
|
NSLog(@"[RoomVC] Failed to upload image: %@", error);
|
2015-01-19 14:30:45 +00:00
|
|
|
[self handleError:error forLocalEvent:localEvent];
|
|
|
|
}
|
2014-12-22 10:55:28 +00:00
|
|
|
}];
|
2014-12-22 10:52:29 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 12:46:27 +00:00
|
|
|
- (void)dismissAttachmentImageViews {
|
2014-12-22 17:07:21 +00:00
|
|
|
if (self.imageValidationView) {
|
|
|
|
[self.imageValidationView dismissSelection];
|
|
|
|
[self.imageValidationView removeFromSuperview];
|
|
|
|
self.imageValidationView = nil;
|
|
|
|
}
|
|
|
|
|
2015-01-06 10:08:29 +00:00
|
|
|
if (highResImageView) {
|
|
|
|
[highResImageView removeFromSuperview];
|
|
|
|
highResImageView = nil;
|
2014-12-22 17:07:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-22 10:52:29 +00:00
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
|
|
|
|
|
2015-01-06 08:57:21 +00:00
|
|
|
// Set visible room id
|
|
|
|
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId;
|
|
|
|
|
2014-12-22 10:52:29 +00:00
|
|
|
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
|
|
|
|
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
|
|
|
|
UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
|
|
|
|
if (selectedImage) {
|
|
|
|
__weak typeof(self) weakSelf = self;
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-12-22 10:52:29 +00:00
|
|
|
// media picker does not offer a preview
|
|
|
|
// so add a preview to let the user validates his selection
|
|
|
|
if (picker.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) {
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2015-01-06 08:41:44 +00:00
|
|
|
// wait that the media picker is dismissed to have the valid membersView frame
|
|
|
|
// else it would include a status bar height offset
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2015-01-23 12:46:27 +00:00
|
|
|
self.imageValidationView = [[MXCImageView alloc] initWithFrame:self.membersView.frame];
|
2015-01-06 14:44:34 +00:00
|
|
|
self.imageValidationView.stretchable = YES;
|
|
|
|
self.imageValidationView.fullScreen = YES;
|
2015-01-19 13:33:57 +00:00
|
|
|
self.imageValidationView.mediaFolder = self.roomId;
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2015-01-06 08:41:44 +00:00
|
|
|
// the user validates the image
|
2015-01-23 12:46:27 +00:00
|
|
|
[self.imageValidationView setRightButtonTitle:@"OK" handler:^(MXCImageView* imageView, NSString* buttonTitle) {
|
2015-01-06 08:41:44 +00:00
|
|
|
// dismiss the image view
|
2015-01-23 12:46:27 +00:00
|
|
|
[weakSelf dismissAttachmentImageViews];
|
2015-01-06 08:41:44 +00:00
|
|
|
|
|
|
|
[weakSelf sendImage:selectedImage];
|
|
|
|
}];
|
2014-12-22 10:52:29 +00:00
|
|
|
|
2015-01-06 08:41:44 +00:00
|
|
|
// the user wants to use an other image
|
2015-01-23 12:46:27 +00:00
|
|
|
[self.imageValidationView setLeftButtonTitle:@"Cancel" handler:^(MXCImageView* imageView, NSString* buttonTitle) {
|
2015-01-06 08:41:44 +00:00
|
|
|
|
|
|
|
// dismiss the image view
|
2015-01-23 12:46:27 +00:00
|
|
|
[weakSelf dismissAttachmentImageViews];
|
2015-01-06 08:41:44 +00:00
|
|
|
|
|
|
|
// Open again media gallery
|
|
|
|
UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init];
|
|
|
|
mediaPicker.delegate = weakSelf;
|
|
|
|
mediaPicker.sourceType = picker.sourceType;
|
|
|
|
mediaPicker.allowsEditing = NO;
|
|
|
|
mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
|
|
|
|
[[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker];
|
|
|
|
}];
|
2014-12-22 10:52:29 +00:00
|
|
|
|
2015-01-06 08:41:44 +00:00
|
|
|
self.imageValidationView.image = selectedImage;
|
2014-12-22 10:52:29 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
[weakSelf sendImage:selectedImage];
|
|
|
|
}
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
|
|
|
|
NSURL* selectedVideo = [info objectForKey:UIImagePickerControllerMediaURL];
|
2014-12-11 15:41:21 +00:00
|
|
|
// Check the selected video, and ignore multiple calls (observed when user pressed several time Choose button)
|
|
|
|
if (selectedVideo && !tmpVideoPlayer) {
|
2014-11-24 09:38:23 +00:00
|
|
|
// Create video thumbnail
|
2014-12-11 15:41:21 +00:00
|
|
|
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;
|
2014-11-24 09:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-12-22 10:55:28 +00:00
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
[self dismissMediaPicker];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
|
|
|
[self dismissMediaPicker];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dismissMediaPicker {
|
|
|
|
[[AppDelegate theDelegate].masterTabBarController dismissMediaPicker];
|
|
|
|
}
|
2014-12-23 17:51:49 +00:00
|
|
|
|
|
|
|
#pragma mark - Segues
|
|
|
|
|
|
|
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
|
|
|
if ([[segue identifier] isEqualToString:@"showMemberSheet"]) {
|
|
|
|
MemberViewController* controller = [segue destinationViewController];
|
2015-01-28 10:30:15 +00:00
|
|
|
|
|
|
|
if (selectedRoomMember) {
|
|
|
|
controller.mxRoomMember = selectedRoomMember;
|
|
|
|
selectedRoomMember = nil;
|
|
|
|
} else {
|
|
|
|
NSIndexPath *indexPath = [self.membersTableView indexPathForSelectedRow];
|
|
|
|
controller.mxRoomMember = [members objectAtIndex:indexPath.row];
|
|
|
|
}
|
2014-12-24 07:59:25 +00:00
|
|
|
controller.mxRoom = self.mxRoom;
|
2014-12-23 17:51:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-24 09:38:23 +00:00
|
|
|
@end
|
2014-12-22 10:55:28 +00:00
|
|
|
|
|
|
|
|