mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-20 16:42:44 +02:00
1282 lines
58 KiB
Objective-C
1282 lines
58 KiB
Objective-C
/*
|
|
Copyright 2014 OpenMarket Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
#import <MobileCoreServices/MobileCoreServices.h>
|
|
|
|
#import "RoomViewController.h"
|
|
#import "RoomMessageTableCell.h"
|
|
#import "RoomMemberTableCell.h"
|
|
|
|
#import "MatrixHandler.h"
|
|
#import "AppDelegate.h"
|
|
#import "AppSettings.h"
|
|
|
|
#import "MediaManager.h"
|
|
|
|
#define ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH 200
|
|
#define ROOM_MESSAGE_CELL_TOP_MARGIN 5
|
|
#define ROOM_MESSAGE_CELL_BOTTOM_MARGIN 5
|
|
#define INCOMING_MESSAGE_CELL_USER_LABEL_HEIGHT 20
|
|
#define ROOM_MESSAGE_CELL_IMAGE_MARGIN 5
|
|
|
|
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";
|
|
|
|
NSString *const kLocalEchoEventIdPrefix = @"localEcho-";
|
|
NSString *const kFailedEventId = @"failedEventId";
|
|
|
|
|
|
@interface RoomViewController ()
|
|
{
|
|
BOOL isFirstDisplay;
|
|
BOOL isJoinRequestInProgress;
|
|
|
|
MXRoom *mxRoom;
|
|
|
|
NSMutableArray *messages;
|
|
id messagesListener;
|
|
|
|
// Members list
|
|
NSArray *members;
|
|
id membersListener;
|
|
|
|
// Cache
|
|
NSMutableArray *tmpCachedAttachments;
|
|
}
|
|
|
|
@property (weak, nonatomic) IBOutlet UINavigationItem *roomNavItem;
|
|
@property (weak, nonatomic) IBOutlet UITableView *messagesTableView;
|
|
@property (weak, nonatomic) IBOutlet UIView *controlView;
|
|
@property (weak, nonatomic) IBOutlet UIButton *optionBtn;
|
|
@property (weak, nonatomic) IBOutlet UITextField *messageTextField;
|
|
@property (weak, nonatomic) IBOutlet UIButton *sendBtn;
|
|
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *controlViewBottomConstraint;
|
|
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
|
|
@property (weak, nonatomic) IBOutlet UIView *membersView;
|
|
@property (weak, nonatomic) IBOutlet UITableView *membersTableView;
|
|
|
|
@property (strong, nonatomic) CustomAlert *actionMenu;
|
|
@end
|
|
|
|
@implementation RoomViewController
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
// Do any additional setup after loading the view, typically from a nib.
|
|
isFirstDisplay = YES;
|
|
|
|
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
|
|
[button addTarget:self action:@selector(showHideRoomMembers:) forControlEvents:UIControlEventTouchUpInside];
|
|
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
|
|
|
|
_sendBtn.enabled = NO;
|
|
_sendBtn.alpha = 0.5;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
#ifdef TEMPORARY_PATCH_INITIAL_SYNC
|
|
// FIXME: these lines should be removed when SDK will fix the initial sync issue
|
|
if (isJoinRequestInProgress) {
|
|
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isInitialSyncDone"];
|
|
}
|
|
#endif
|
|
// Clear temporary cached attachments (used for local echo)
|
|
NSUInteger index = tmpCachedAttachments.count;
|
|
while (index--) {
|
|
[MediaManager clearCacheForURL:[tmpCachedAttachments objectAtIndex:index]];
|
|
}
|
|
tmpCachedAttachments = nil;
|
|
|
|
messages = nil;
|
|
if (messagesListener) {
|
|
[mxRoom unregisterListener:messagesListener];
|
|
messagesListener = nil;
|
|
}
|
|
mxRoom = nil;
|
|
|
|
members = nil;
|
|
if (membersListener) {
|
|
membersListener = nil;
|
|
}
|
|
|
|
if (_actionMenu) {
|
|
[_actionMenu dismiss:NO];
|
|
_actionMenu = nil;
|
|
}
|
|
}
|
|
|
|
- (void)didReceiveMemoryWarning {
|
|
[super didReceiveMemoryWarning];
|
|
// Dispose of any resources that can be recreated.
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
[super viewWillAppear:animated];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil];
|
|
|
|
// Set visible room id
|
|
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
[super viewWillDisappear:animated];
|
|
|
|
// hide action
|
|
if (_actionMenu) {
|
|
[_actionMenu dismiss:NO];
|
|
_actionMenu = nil;
|
|
}
|
|
|
|
// Hide members by default
|
|
[self hideRoomMembers];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
|
|
|
|
// Reset visible room id
|
|
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = nil;
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated {
|
|
[super viewDidAppear:animated];
|
|
|
|
if (isFirstDisplay) {
|
|
// Scroll to the bottom
|
|
[self scrollToBottomAnimated:animated];
|
|
isFirstDisplay = NO;
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)setRoomId:(NSString *)roomId {
|
|
_roomId = roomId;
|
|
|
|
// Load room data
|
|
[self configureView];
|
|
|
|
// Trigger a back pagination if messages number is low
|
|
if (messages && messages.count < 10) {
|
|
[self triggerBackPagination];
|
|
}
|
|
}
|
|
|
|
#ifdef TEMPORARY_PATCH_INITIAL_SYNC
|
|
// FIXME: this method should be removed when SDK will fix the initial sync issue
|
|
#pragma mark - KVO
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
if ([@"isInitialSyncDone" isEqualToString:keyPath])
|
|
{
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
if ([mxHandler isInitialSyncDone]) {
|
|
[_activityIndicator stopAnimating];
|
|
isJoinRequestInProgress = NO;
|
|
[mxHandler removeObserver:self forKeyPath:@"isInitialSyncDone"];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self configureView];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#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
|
|
[_activityIndicator startAnimating];
|
|
return;
|
|
}
|
|
|
|
// Flush messages
|
|
messages = nil;
|
|
|
|
// Remove potential roomData listener
|
|
if (messagesListener && mxRoom) {
|
|
[mxRoom unregisterListener:messagesListener];
|
|
messagesListener = nil;
|
|
}
|
|
|
|
// Update room data
|
|
if (self.roomId) {
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
mxRoom = [mxHandler.mxSession room:self.roomId];
|
|
|
|
// Update room title
|
|
self.roomNavItem.title = mxRoom.displayname;
|
|
|
|
// Join the room if the user is not already listed in room's members
|
|
if ([mxRoom getMember:mxHandler.userId] == nil) {
|
|
isJoinRequestInProgress = YES;
|
|
[_activityIndicator startAnimating];
|
|
[mxHandler.mxRestClient joinRoom:self.roomId success:^{
|
|
#ifdef TEMPORARY_PATCH_INITIAL_SYNC
|
|
// Presently the SDK is not able to handle correctly the context for the room recently joined
|
|
// PATCH: we force new initial sync
|
|
// FIXME: this new initial sync should be removed when SDK will fix the issue
|
|
[mxHandler addObserver:self forKeyPath:@"isInitialSyncDone" options:0 context:nil];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[mxHandler forceInitialSync];
|
|
});
|
|
#else
|
|
[_activityIndicator stopAnimating];
|
|
isJoinRequestInProgress = NO;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self configureView];
|
|
});
|
|
#endif
|
|
} failure:^(NSError *error) {
|
|
[_activityIndicator stopAnimating];
|
|
isJoinRequestInProgress = NO;
|
|
NSLog(@"Failed to join room (%@): %@", mxRoom.displayname, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
return;
|
|
}
|
|
|
|
messages = [NSMutableArray arrayWithArray:mxRoom.messages];
|
|
// Register a listener for events that modify the `messages` property
|
|
messagesListener = [mxRoom registerEventListenerForTypes:mxHandler.mxSession.eventsFilterForMessages block:^(MXRoom *room, MXEvent *event, BOOL isLive) {
|
|
// consider only live event
|
|
if (isLive) {
|
|
// For outgoing message, remove the temporary event
|
|
if ([event.userId isEqualToString:[MatrixHandler sharedHandler].userId]) {
|
|
NSUInteger index = messages.count;
|
|
while (index--) {
|
|
MXEvent *mxEvent = [messages objectAtIndex:index];
|
|
if ([mxEvent.eventId isEqualToString:event.eventId]) {
|
|
[messages replaceObjectAtIndex:index withObject:event];
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Here a new event is added
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0];
|
|
[messages addObject:event];
|
|
[self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
|
|
[self scrollToBottomAnimated:YES];
|
|
}
|
|
}];
|
|
} else {
|
|
mxRoom = nil;
|
|
// Update room title
|
|
self.roomNavItem.title = nil;
|
|
}
|
|
|
|
[self.messagesTableView reloadData];
|
|
}
|
|
|
|
- (void)scrollToBottomAnimated:(BOOL)animated {
|
|
// Scroll table view to the bottom
|
|
NSInteger rowNb = messages.count;
|
|
if (rowNb) {
|
|
[self.messagesTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(rowNb - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
|
|
}
|
|
}
|
|
|
|
- (void)triggerBackPagination {
|
|
if (mxRoom.canPaginate)
|
|
{
|
|
[_activityIndicator startAnimating];
|
|
|
|
[mxRoom paginateBackMessages:20 success:^(NSArray *oldMessages) {
|
|
if (oldMessages.count)
|
|
{
|
|
// Update table sources
|
|
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldMessages.count)];
|
|
[messages insertObjects:oldMessages atIndexes:indexSet];
|
|
|
|
// Prepare insertion of new rows at the top of the table (compute cumulative height of added cells)
|
|
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:oldMessages.count];
|
|
CGFloat verticalOffset = 0;
|
|
for (NSUInteger index = 0; index < oldMessages.count; index++) {
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[indexPaths addObject:indexPath];
|
|
verticalOffset += [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
// Disable animation during cells insertion to prevent flickering
|
|
[UIView setAnimationsEnabled:NO];
|
|
// Store the current content offset
|
|
CGPoint contentOffset = self.messagesTableView.contentOffset;
|
|
[self.messagesTableView beginUpdates];
|
|
[self.messagesTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
|
[self.messagesTableView endUpdates];
|
|
// Enable animation again
|
|
[UIView setAnimationsEnabled:YES];
|
|
// Fix vertical offset in order to prevent scrolling down
|
|
contentOffset.y += verticalOffset;
|
|
[self.messagesTableView setContentOffset:contentOffset animated:NO];
|
|
|
|
[_activityIndicator stopAnimating];
|
|
|
|
// Move the current message at the middle of the visible area (dispatch this action in order to let table end its refresh)
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self.messagesTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(oldMessages.count - 1) inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
|
|
});
|
|
} else {
|
|
// Here there was no event related to the `messages` property
|
|
[_activityIndicator stopAnimating];
|
|
// Trigger a new back pagination (if possible)
|
|
[self triggerBackPagination];
|
|
}
|
|
} failure:^(NSError *error) {
|
|
[_activityIndicator stopAnimating];
|
|
NSLog(@"Failed to paginate back: %@", error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)showHideRoomMembers:(id)sender {
|
|
// Check whether the members list is displayed
|
|
if (members) {
|
|
[self hideRoomMembers];
|
|
} else {
|
|
[self showRoomMembers];
|
|
}
|
|
}
|
|
|
|
- (void)updateRoomMembers {
|
|
members = [[mxRoom members] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
|
|
MXRoomMember *member1 = (MXRoomMember*)obj1;
|
|
MXRoomMember *member2 = (MXRoomMember*)obj2;
|
|
|
|
// Move banned and left members at the end of the list
|
|
if (member1.membership == MXMembershipLeave || member1.membership == MXMembershipBan) {
|
|
if (member2.membership != MXMembershipLeave && member2.membership != MXMembershipBan) {
|
|
return NSOrderedDescending;
|
|
}
|
|
} else if (member2.membership == MXMembershipLeave || member2.membership == MXMembershipBan) {
|
|
return NSOrderedAscending;
|
|
}
|
|
|
|
// Move invited members just before left and banned members
|
|
if (member1.membership == MXMembershipInvite) {
|
|
if (member2.membership != MXMembershipInvite) {
|
|
return NSOrderedDescending;
|
|
}
|
|
} else if (member2.membership == MXMembershipInvite) {
|
|
return NSOrderedAscending;
|
|
}
|
|
|
|
if ([[AppSettings sharedSettings] sortMembersUsingLastSeenTime]) {
|
|
|
|
// Get the users that correspond to these members
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
MXUser *user1 = [mxHandler.mxSession user:member1.userId];
|
|
MXUser *user2 = [mxHandler.mxSession user:member2.userId];
|
|
|
|
if (user1.lastActiveAgo < user2.lastActiveAgo) {
|
|
return NSOrderedAscending;
|
|
} else if (user1.lastActiveAgo == user2.lastActiveAgo) {
|
|
return [[mxRoom memberName:member1.userId] compare:[mxRoom memberName:member2.userId] options:NSCaseInsensitiveSearch];
|
|
}
|
|
return NSOrderedDescending;
|
|
} else {
|
|
// Move user without display name at the end (before invited users)
|
|
if (member1.displayname) {
|
|
if (!member2.displayname) {
|
|
return NSOrderedAscending;
|
|
}
|
|
} else if (member2.displayname) {
|
|
return NSOrderedDescending;
|
|
}
|
|
|
|
return [[mxRoom memberName:member1.userId] compare:[mxRoom memberName:member2.userId] options:NSCaseInsensitiveSearch];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)showRoomMembers {
|
|
// Dismiss keyboard
|
|
[self dismissKeyboard];
|
|
|
|
[self updateRoomMembers];
|
|
// Register a listener for events that concern room members
|
|
NSArray *mxMembersEvents = @[
|
|
kMXEventTypeStringRoomMember,
|
|
kMXEventTypeStringRoomPowerLevels,
|
|
kMXEventTypeStringPresence
|
|
];
|
|
membersListener = [mxRoom registerEventListenerForTypes:mxMembersEvents block:^(MXRoom *room, MXEvent *event, BOOL isLive) {
|
|
// consider only live event
|
|
if (isLive) {
|
|
// Refresh members list
|
|
[self updateRoomMembers];
|
|
[self.membersTableView reloadData];
|
|
}
|
|
}];
|
|
|
|
self.membersView.hidden = NO;
|
|
[self.membersTableView reloadData];
|
|
}
|
|
|
|
- (void)hideRoomMembers {
|
|
if (membersListener) {
|
|
[mxRoom unregisterListener:membersListener];
|
|
membersListener = nil;
|
|
}
|
|
self.membersView.hidden = YES;
|
|
members = nil;
|
|
}
|
|
|
|
#pragma mark - keyboard handling
|
|
|
|
- (void)onKeyboardWillShow:(NSNotification *)notif {
|
|
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
|
|
CGRect endRect = rectVal.CGRectValue;
|
|
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
|
// Handle portrait/landscape mode
|
|
insets.bottom = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
|
|
self.messagesTableView.contentInset = insets;
|
|
|
|
[self scrollToBottomAnimated:YES];
|
|
|
|
// Move up control view
|
|
// Don't forget the offset related to tabBar
|
|
_controlViewBottomConstraint.constant = insets.bottom - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height;
|
|
}
|
|
|
|
- (void)onKeyboardWillHide:(NSNotification *)notif {
|
|
UIEdgeInsets insets = self.messagesTableView.contentInset;
|
|
insets.bottom = self.controlView.frame.size.height;
|
|
self.messagesTableView.contentInset = insets;
|
|
|
|
_controlViewBottomConstraint.constant = 0;
|
|
}
|
|
|
|
- (void)dismissKeyboard {
|
|
// Hide the keyboard
|
|
[_messageTextField resignFirstResponder];
|
|
}
|
|
|
|
#pragma mark - Table view data source
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
|
return 1;
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
// Check table view members vs messages
|
|
if (tableView == self.membersTableView)
|
|
{
|
|
return members.count;
|
|
}
|
|
|
|
return messages.count;
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
// Check table view members vs messages
|
|
if (tableView == self.membersTableView)
|
|
{
|
|
return 50;
|
|
}
|
|
|
|
// Compute here height of message cells
|
|
CGFloat rowHeight;
|
|
// Get event related to this row
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
MXEvent *mxEvent = [messages objectAtIndex:indexPath.row];
|
|
BOOL isIncomingMsg = ([mxEvent.userId isEqualToString:mxHandler.userId] == NO);
|
|
CGSize contentSize;
|
|
|
|
if ([mxHandler isAttachment:mxEvent]) {
|
|
contentSize = [self attachmentContentSize:mxEvent];
|
|
} else {
|
|
// Use a TextView template to compute cell height
|
|
UITextView *dummyTextView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH, MAXFLOAT)];
|
|
dummyTextView.font = [UIFont systemFontOfSize:14];
|
|
dummyTextView.text = [mxHandler displayTextFor:mxEvent inSubtitleMode:NO];
|
|
contentSize = [dummyTextView sizeThatFits:dummyTextView.frame.size];
|
|
}
|
|
|
|
// Handle incoming / outgoing layout
|
|
if (isIncomingMsg) {
|
|
// By default the user name is displayed above the message or attachment
|
|
rowHeight = contentSize.height + ROOM_MESSAGE_CELL_TOP_MARGIN + INCOMING_MESSAGE_CELL_USER_LABEL_HEIGHT + ROOM_MESSAGE_CELL_BOTTOM_MARGIN;
|
|
|
|
if (indexPath.row) {
|
|
// This user name is hide if the previous message is from the same user
|
|
MXEvent *previousMxEvent = [messages objectAtIndex:indexPath.row - 1];
|
|
if ([previousMxEvent.userId isEqualToString:mxEvent.userId]) {
|
|
rowHeight -= INCOMING_MESSAGE_CELL_USER_LABEL_HEIGHT;
|
|
}
|
|
}
|
|
} else {
|
|
rowHeight = contentSize.height + ROOM_MESSAGE_CELL_TOP_MARGIN + ROOM_MESSAGE_CELL_BOTTOM_MARGIN;
|
|
}
|
|
|
|
// Force minimum height: 50
|
|
if (rowHeight < 50) {
|
|
rowHeight = 50;
|
|
}
|
|
return rowHeight;
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
|
|
// Check table view members vs messages
|
|
if (tableView == self.membersTableView) {
|
|
RoomMemberTableCell *memberCell = [tableView dequeueReusableCellWithIdentifier:@"RoomMemberCell" forIndexPath:indexPath];
|
|
if (indexPath.row < members.count) {
|
|
[memberCell setRoomMember:[members objectAtIndex:indexPath.row] withRoom:mxRoom];
|
|
}
|
|
|
|
return memberCell;
|
|
}
|
|
|
|
// Handle here room message cells
|
|
RoomMessageTableCell *cell;
|
|
MXEvent *mxEvent = [messages objectAtIndex:indexPath.row];
|
|
BOOL isIncomingMsg = NO;
|
|
|
|
if ([mxEvent.userId isEqualToString:mxHandler.userId]) {
|
|
cell = [tableView dequeueReusableCellWithIdentifier:@"OutgoingMessageCell" forIndexPath:indexPath];
|
|
cell.messageTextView.backgroundColor = [UIColor groupTableViewBackgroundColor];
|
|
[((OutgoingMessageTableCell*)cell).activityIndicator stopAnimating];
|
|
} else {
|
|
cell = [tableView dequeueReusableCellWithIdentifier:@"IncomingMessageCell" forIndexPath:indexPath];
|
|
cell.messageTextView.backgroundColor = [UIColor lightGrayColor];
|
|
isIncomingMsg = YES;
|
|
}
|
|
|
|
// Clear background for notifications (We consider as notification mxEvent which is not a text message or an attachment)
|
|
if ([mxHandler isNotification:mxEvent]) {
|
|
cell.messageTextView.backgroundColor = [UIColor clearColor];
|
|
}
|
|
|
|
// Hide user picture if the previous message is from the same user
|
|
cell.pictureView.hidden = NO;
|
|
if (indexPath.row) {
|
|
MXEvent *previousMxEvent = [messages objectAtIndex:indexPath.row - 1];
|
|
if ([previousMxEvent.userId isEqualToString:mxEvent.userId]) {
|
|
cell.pictureView.hidden = YES;
|
|
}
|
|
}
|
|
// Set url for visible picture
|
|
if (!cell.pictureView.hidden) {
|
|
cell.placeholder = @"default-profile";
|
|
cell.pictureURL = [mxRoom getMember:mxEvent.userId].avatarUrl;
|
|
}
|
|
|
|
// Update incoming/outgoing message layout
|
|
if (isIncomingMsg) {
|
|
// Hide userName in incoming message if the previous message is from the same user
|
|
IncomingMessageTableCell* incomingMsgCell = (IncomingMessageTableCell*)cell;
|
|
CGRect frame = incomingMsgCell.userNameLabel.frame;
|
|
if (cell.pictureView.hidden) {
|
|
incomingMsgCell.userNameLabel.text = nil;
|
|
frame.size.height = 0;
|
|
incomingMsgCell.userNameLabel.hidden = YES;
|
|
} else {
|
|
frame.size.height = INCOMING_MESSAGE_CELL_USER_LABEL_HEIGHT;
|
|
incomingMsgCell.userNameLabel.hidden = NO;
|
|
NSString *userName = [mxRoom memberName:mxEvent.userId];
|
|
incomingMsgCell.userNameLabel.text = [NSString stringWithFormat:@"- %@", userName];
|
|
}
|
|
incomingMsgCell.userNameLabel.frame = frame;
|
|
|
|
// Reset text color
|
|
cell.messageTextView.textColor = [UIColor blackColor];
|
|
} else {
|
|
// Hide unsent label by default
|
|
UILabel *unsentLabel = ((OutgoingMessageTableCell*)cell).unsentLabel;
|
|
unsentLabel.hidden = YES;
|
|
|
|
// Set the right text color for outgoing messages
|
|
if ([mxEvent.eventId hasPrefix:kLocalEchoEventIdPrefix]) {
|
|
cell.messageTextView.textColor = [UIColor lightGrayColor];
|
|
} else if ([mxEvent.eventId hasPrefix:kFailedEventId]) {
|
|
cell.messageTextView.textColor = [UIColor redColor];
|
|
unsentLabel.hidden = NO;
|
|
} else {
|
|
cell.messageTextView.textColor = [UIColor blackColor];
|
|
}
|
|
}
|
|
|
|
if ([mxHandler isAttachment:mxEvent]) {
|
|
cell.attachmentView.hidden = NO;
|
|
cell.messageTextView.text = nil; // Note: Text view is used as attachment background view
|
|
CGSize contentSize = [self attachmentContentSize:mxEvent];
|
|
cell.msgTextViewWidthConstraint.constant = contentSize.width;
|
|
|
|
// Fade attachments during upload
|
|
if (isIncomingMsg == NO && [mxEvent.eventId hasPrefix:kLocalEchoEventIdPrefix]) {
|
|
cell.attachmentView.alpha = 0.5;
|
|
[((OutgoingMessageTableCell*)cell).activityIndicator startAnimating];
|
|
} else {
|
|
cell.attachmentView.alpha = 1;
|
|
}
|
|
|
|
NSString *msgtype = mxEvent.content[@"msgtype"];
|
|
if ([msgtype isEqualToString:kMXMessageTypeImage]) {
|
|
NSString *url = mxEvent.content[@"thumbnail_url"];
|
|
if (url == nil) {
|
|
url = mxEvent.content[@"url"];
|
|
}
|
|
cell.attachedImageURL = url;
|
|
} else {
|
|
cell.attachedImageURL = nil;
|
|
}
|
|
} else {
|
|
// Text message will be displayed in textView with max width
|
|
cell.msgTextViewWidthConstraint.constant = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH;
|
|
// Cancel potential attachment loading
|
|
cell.attachedImageURL = nil;
|
|
cell.attachmentView.hidden = YES;
|
|
|
|
NSString *displayText = [mxHandler displayTextFor:mxEvent inSubtitleMode:NO];
|
|
// Update text color according to text content
|
|
if ([displayText hasPrefix:kMatrixHandlerUnsupportedMessagePrefix]) {
|
|
cell.messageTextView.textColor = [UIColor redColor];
|
|
} else if (isIncomingMsg && ([displayText rangeOfString:mxHandler.userDisplayName options:NSCaseInsensitiveSearch].location != NSNotFound || [displayText rangeOfString:mxHandler.userId options:NSCaseInsensitiveSearch].location != NSNotFound)) {
|
|
cell.messageTextView.textColor = [UIColor blueColor];
|
|
}
|
|
cell.messageTextView.text = displayText;
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (CGSize)attachmentContentSize:(MXEvent*)mxEvent;
|
|
{
|
|
CGSize contentSize;
|
|
NSString *msgtype = mxEvent.content[@"msgtype"];
|
|
if ([msgtype isEqualToString:kMXMessageTypeImage]) {
|
|
CGFloat width, height;
|
|
width = height = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH;
|
|
NSDictionary *thumbInfo = mxEvent.content[@"thumbnail_info"];
|
|
if (thumbInfo) {
|
|
width = [thumbInfo[@"w"] integerValue] + 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN;
|
|
height = [thumbInfo[@"h"] integerValue] + 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN;
|
|
if (width > ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH || height > ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) {
|
|
if (width > height) {
|
|
height = (height * ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) / width;
|
|
height = floorf(height / 2) * 2;
|
|
width = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH;
|
|
} else {
|
|
width = (width * ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) / height;
|
|
width = floorf(width / 2) * 2;
|
|
height = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH;
|
|
}
|
|
}
|
|
}
|
|
contentSize = CGSizeMake(width, height);
|
|
} else {
|
|
contentSize = CGSizeMake(40, 40);
|
|
}
|
|
return contentSize;
|
|
}
|
|
|
|
#pragma mark - Table view delegate
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
// Check table view members vs messages
|
|
if (tableView == self.membersTableView) {
|
|
// List action(s) available on this member
|
|
// TODO: Check user's power level before allowing an action (kick, ban, ...)
|
|
MXRoomMember *roomMember = [members objectAtIndex:indexPath.row];
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
__weak typeof(self) weakSelf = self;
|
|
if (_actionMenu) {
|
|
[_actionMenu dismiss:NO];
|
|
_actionMenu = nil;
|
|
}
|
|
|
|
// Consider the case of the user himself
|
|
if ([roomMember.userId isEqualToString:mxHandler.userId]) {
|
|
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
|
|
[self.actionMenu addActionWithTitle:@"Leave" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient leaveRoom:weakSelf.roomId
|
|
success:^{
|
|
// Back to recents
|
|
[weakSelf.navigationController popViewControllerAnimated:YES];
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Leave room %@ failed: %@", weakSelf.roomId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
} else {
|
|
// Consider membership of the selected member
|
|
switch (roomMember.membership) {
|
|
case MXMembershipInvite:
|
|
case MXMembershipJoin: {
|
|
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
|
|
[self.actionMenu addActionWithTitle:@"Kick" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient kickUser:roomMember.userId
|
|
fromRoom:weakSelf.roomId
|
|
reason:nil
|
|
success:^{
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Kick %@ failed: %@", roomMember.userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient banUser:roomMember.userId
|
|
inRoom:weakSelf.roomId
|
|
reason:nil
|
|
success:^{
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Ban %@ failed: %@", roomMember.userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
break;
|
|
}
|
|
case MXMembershipLeave: {
|
|
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
|
|
[self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient inviteUser:roomMember.userId
|
|
toRoom:weakSelf.roomId
|
|
success:^{
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Invite %@ failed: %@", roomMember.userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient banUser:roomMember.userId
|
|
inRoom:weakSelf.roomId
|
|
reason:nil
|
|
success:^{
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Ban %@ failed: %@", roomMember.userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
break;
|
|
}
|
|
case MXMembershipBan: {
|
|
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
|
|
[self.actionMenu addActionWithTitle:@"Unban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
weakSelf.actionMenu = nil;
|
|
[[MatrixHandler sharedHandler].mxRestClient unbanUser:roomMember.userId
|
|
inRoom:weakSelf.roomId
|
|
success:^{
|
|
}
|
|
failure:^(NSError *error) {
|
|
NSLog(@"Unban %@ failed: %@", roomMember.userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display the action sheet (if any)
|
|
if (self.actionMenu) {
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
weakSelf.actionMenu = nil;
|
|
}];
|
|
[self.actionMenu showInViewController:self];
|
|
}
|
|
|
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
|
} else if (tableView == self.messagesTableView) {
|
|
// Dismiss keyboard when user taps on messages table view content
|
|
[self dismissKeyboard];
|
|
}
|
|
}
|
|
|
|
// Detect vertical bounce at the top of the tableview to trigger pagination
|
|
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
|
|
if (scrollView == self.messagesTableView) {
|
|
// paginate ?
|
|
if ((scrollView.contentOffset.y < -64) && (_activityIndicator.isAnimating == NO))
|
|
{
|
|
[self triggerBackPagination];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - UITextField delegate
|
|
|
|
- (void)onTextFieldChange:(NSNotification *)notif {
|
|
NSString *msg = _messageTextField.text;
|
|
|
|
if (msg.length) {
|
|
_sendBtn.enabled = YES;
|
|
_sendBtn.alpha = 1;
|
|
// Reset potential placeholder (used in case of wrong command usage)
|
|
_messageTextField.placeholder = nil;
|
|
} else {
|
|
_sendBtn.enabled = NO;
|
|
_sendBtn.alpha = 0.5;
|
|
}
|
|
}
|
|
|
|
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
|
|
// "Done" key has been pressed
|
|
[textField resignFirstResponder];
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (IBAction)onButtonPressed:(id)sender {
|
|
if (sender == _sendBtn) {
|
|
NSString *msgTxt = self.messageTextField.text;
|
|
|
|
// Handle potential commands in room chat
|
|
if ([self isIRCStyleCommand:msgTxt] == NO) {
|
|
[self postTextMessage:msgTxt];
|
|
}
|
|
|
|
self.messageTextField.text = nil;
|
|
// disable send button
|
|
[self onTextFieldChange:nil];
|
|
} else if (sender == _optionBtn) {
|
|
[self dismissKeyboard];
|
|
|
|
// Display action menu: Add attachments, Invite user...
|
|
__weak typeof(self) weakSelf = self;
|
|
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
|
|
// Attachments
|
|
[self.actionMenu addActionWithTitle:@"Attach" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
// Ask for attachment type
|
|
weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an attachment type:" message:nil style:CustomAlertStyleActionSheet];
|
|
[weakSelf.actionMenu addActionWithTitle:@"Media" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
// Open picture gallery
|
|
UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init];
|
|
mediaPicker.delegate = weakSelf;
|
|
mediaPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
|
mediaPicker.allowsEditing = NO;
|
|
mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
|
|
[[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker];
|
|
}
|
|
}];
|
|
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
weakSelf.actionMenu = nil;
|
|
}];
|
|
[weakSelf.actionMenu showInViewController:weakSelf];
|
|
}
|
|
}];
|
|
// Invitation
|
|
[self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
if (weakSelf) {
|
|
// Ask for userId to invite
|
|
weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"User ID:" message:nil style:CustomAlertStyleAlert];
|
|
weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
weakSelf.actionMenu = nil;
|
|
}];
|
|
[weakSelf.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.secureTextEntry = NO;
|
|
textField.placeholder = @"ex: @bob:homeserver";
|
|
}];
|
|
[weakSelf.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
UITextField *textField = [alert textFieldAtIndex:0];
|
|
NSString *userId = textField.text;
|
|
weakSelf.actionMenu = nil;
|
|
if (userId.length) {
|
|
[[MatrixHandler sharedHandler].mxRestClient inviteUser:userId toRoom:weakSelf.roomId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Invite %@ failed: %@", userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
}
|
|
}];
|
|
[weakSelf.actionMenu showInViewController:weakSelf];
|
|
}
|
|
}];
|
|
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
|
|
weakSelf.actionMenu = nil;
|
|
}];
|
|
weakSelf.actionMenu.sourceView = weakSelf.optionBtn;
|
|
[self.actionMenu showInViewController:self];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Post messages
|
|
|
|
- (void)postMessage:(NSDictionary*)msgContent withLocalEventId:(NSString*)localEventId {
|
|
MXMessageType msgType = msgContent[@"msgtype"];
|
|
if (msgType) {
|
|
// Check whether a temporary event has already been added for local echo (this happens on attachments)
|
|
MXEvent *mxEvent = nil;
|
|
if (localEventId) {
|
|
// Update the temporary event with the actual msg content
|
|
NSUInteger index = messages.count;
|
|
while (index--) {
|
|
mxEvent = [messages objectAtIndex:index];
|
|
if ([mxEvent.eventId isEqualToString:localEventId]) {
|
|
mxEvent.content = msgContent;
|
|
// Refresh table display
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Create a temporary event to displayed outgoing message (local echo)
|
|
localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
|
|
mxEvent = [[MXEvent alloc] init];
|
|
mxEvent.roomId = self.roomId;
|
|
mxEvent.eventId = localEventId;
|
|
mxEvent.eventType = MXEventTypeRoomMessage;
|
|
mxEvent.type = kMXEventTypeStringRoomMessage;
|
|
mxEvent.content = msgContent;
|
|
mxEvent.userId = [MatrixHandler sharedHandler].userId;
|
|
// Update table sources
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0];
|
|
[messages addObject:mxEvent];
|
|
// Refresh table display
|
|
[self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
|
|
[self scrollToBottomAnimated:YES];
|
|
}
|
|
|
|
// Send message to the room
|
|
[[[MatrixHandler sharedHandler] mxRestClient] postMessage:self.roomId msgType:msgType content:mxEvent.content success:^(NSString *event_id) {
|
|
// Update the temporary event with the actual event id
|
|
NSUInteger index = messages.count;
|
|
while (index--) {
|
|
MXEvent *mxEvent = [messages objectAtIndex:index];
|
|
if ([mxEvent.eventId isEqualToString:localEventId]) {
|
|
mxEvent.eventId = event_id;
|
|
// Refresh table display
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
|
break;
|
|
}
|
|
}
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Failed to send message (%@): %@", mxEvent.description, error);
|
|
// Update the temporary event with the failed event id
|
|
NSUInteger index = messages.count;
|
|
while (index--) {
|
|
MXEvent *mxEvent = [messages objectAtIndex:index];
|
|
if ([mxEvent.eventId isEqualToString:localEventId]) {
|
|
mxEvent.eventId = kFailedEventId;
|
|
// Refresh table display
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
|
[self scrollToBottomAnimated:YES];
|
|
break;
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)postTextMessage:(NSString*)msgTxt {
|
|
MXMessageType msgType = kMXMessageTypeText;
|
|
// Check whether the message is an emote
|
|
if ([msgTxt hasPrefix:@"/me "]) {
|
|
msgType = kMXMessageTypeEmote;
|
|
// Remove "/me " string
|
|
msgTxt = [msgTxt substringFromIndex:4];
|
|
}
|
|
|
|
[self postMessage:@{@"msgtype":msgType, @"body":msgTxt} withLocalEventId:nil];
|
|
}
|
|
|
|
- (BOOL)isIRCStyleCommand:(NSString*)text{
|
|
// Check whether the provided text may be an IRC-style command
|
|
if ([text hasPrefix:@"/"] == NO || [text hasPrefix:@"//"] == YES) {
|
|
return NO;
|
|
}
|
|
|
|
// Parse command line
|
|
NSArray *components = [text componentsSeparatedByString:@" "];
|
|
NSString *cmd = [components objectAtIndex:0];
|
|
NSUInteger index = 1;
|
|
|
|
if ([cmd isEqualToString:kCmdEmote]) {
|
|
// post message as an emote
|
|
[self postTextMessage:text];
|
|
} else if ([text hasPrefix:kCmdChangeDisplayName]) {
|
|
// Change display name
|
|
NSString *displayName = [text substringFromIndex:kCmdChangeDisplayName.length + 1];
|
|
// Remove white space from both ends
|
|
displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
|
|
if (displayName.length) {
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
[mxHandler.mxRestClient setDisplayName:displayName success:^{
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Set displayName failed: %@", error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /nick <display_name>";
|
|
}
|
|
} else if ([text hasPrefix:kCmdJoinRoom]) {
|
|
// Join a room
|
|
NSString *roomAlias = [text substringFromIndex:kCmdJoinRoom.length + 1];
|
|
// Remove white space from both ends
|
|
roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
|
|
// Check
|
|
if (roomAlias.length) {
|
|
// FIXME
|
|
NSLog(@"Join Alias is not supported yet (%@)", text);
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/join is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
|
|
[alert show];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /join <room_alias>";
|
|
}
|
|
} else {
|
|
// Retrieve userId
|
|
NSString *userId = nil;
|
|
while (index < components.count) {
|
|
userId = [components objectAtIndex:index++];
|
|
if (userId.length) {
|
|
// done
|
|
break;
|
|
}
|
|
// reset
|
|
userId = nil;
|
|
}
|
|
|
|
if ([cmd isEqualToString:kCmdKickUser]) {
|
|
if (userId) {
|
|
// Retrieve potential reason
|
|
NSString *reason = nil;
|
|
while (index < components.count) {
|
|
if (reason) {
|
|
reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
|
|
} else {
|
|
reason = [components objectAtIndex:index++];
|
|
}
|
|
}
|
|
// Kick the user
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
[mxHandler.mxRestClient kickUser:userId fromRoom:self.roomId reason:reason success:^{
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Kick user (%@) failed: %@", userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /kick <userId> [<reason>]";
|
|
}
|
|
} else if ([cmd isEqualToString:kCmdBanUser]) {
|
|
if (userId) {
|
|
// Retrieve potential reason
|
|
NSString *reason = nil;
|
|
while (index < components.count) {
|
|
if (reason) {
|
|
reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
|
|
} else {
|
|
reason = [components objectAtIndex:index++];
|
|
}
|
|
}
|
|
// Ban the user
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
[mxHandler.mxRestClient banUser:userId inRoom:self.roomId reason:reason success:^{
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Ban user (%@) failed: %@", userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /ban <userId> [<reason>]";
|
|
}
|
|
} else if ([cmd isEqualToString:kCmdUnbanUser]) {
|
|
if (userId) {
|
|
// Unban the user
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
[mxHandler.mxRestClient unbanUser:userId inRoom:self.roomId success:^{
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Unban user (%@) failed: %@", userId, error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /unban <userId>";
|
|
}
|
|
} else if ([cmd isEqualToString:kCmdSetUserPowerLevel]) {
|
|
// Retrieve power level
|
|
NSString *powerLevel = nil;
|
|
while (index < components.count) {
|
|
powerLevel = [components objectAtIndex:index++];
|
|
if (powerLevel.length) {
|
|
// done
|
|
break;
|
|
}
|
|
// reset
|
|
powerLevel = nil;
|
|
}
|
|
// Set power level
|
|
if (userId && powerLevel) {
|
|
// FIXME
|
|
NSLog(@"Set user power level (/op) is not supported yet (%@)", userId);
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/op is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
|
|
[alert show];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /op <userId> <power level>";
|
|
}
|
|
} else if ([cmd isEqualToString:kCmdResetUserPowerLevel]) {
|
|
if (userId) {
|
|
// Reset user power level
|
|
// FIXME
|
|
NSLog(@"Reset user power level (/deop) is not supported yet (%@)", userId);
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/deop is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
|
|
[alert show];
|
|
} else {
|
|
// Display cmd usage in text input as placeholder
|
|
self.messageTextField.placeholder = @"Usage: /deop <userId>";
|
|
}
|
|
} else {
|
|
NSLog(@"Unrecognised IRC-style command: %@", text);
|
|
self.messageTextField.placeholder = [NSString stringWithFormat:@"Unrecognised IRC-style command: %@", cmd];
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
# pragma mark - UIImagePickerControllerDelegate
|
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
|
|
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
|
|
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
|
|
UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
|
|
if (selectedImage) {
|
|
// Create a temporary event to displayed outgoing message (local echo)
|
|
NSString *localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
|
|
MXEvent *mxEvent = [[MXEvent alloc] init];
|
|
mxEvent.roomId = self.roomId;
|
|
mxEvent.eventId = localEventId;
|
|
mxEvent.eventType = MXEventTypeRoomMessage;
|
|
mxEvent.type = kMXEventTypeStringRoomMessage;
|
|
// We store temporarily the selected image in cache, use the localId to build temporary url
|
|
NSString *dummyURL = [NSString stringWithFormat:@"%@%@", kMediaManagerPrefixForDummyURL, localEventId];
|
|
NSData *selectedImageData = UIImageJPEGRepresentation(selectedImage, 0.5);
|
|
[MediaManager cachePictureWithData:selectedImageData forURL:dummyURL];
|
|
if (tmpCachedAttachments == nil) {
|
|
tmpCachedAttachments = [NSMutableArray array];
|
|
}
|
|
[tmpCachedAttachments addObject:dummyURL];
|
|
NSMutableDictionary *thumbnailInfo = [[NSMutableDictionary alloc] init];
|
|
[thumbnailInfo setValue:@"image/jpeg" forKey:@"mimetype"];
|
|
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)selectedImage.size.width] forKey:@"w"];
|
|
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)selectedImage.size.height] forKey:@"h"];
|
|
[thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:selectedImageData.length] forKey:@"size"];
|
|
mxEvent.content = @{@"msgtype":@"m.image", @"thumbnail_info":thumbnailInfo, @"thumbnail_url":dummyURL};
|
|
mxEvent.userId = [MatrixHandler sharedHandler].userId;
|
|
|
|
// Update table sources
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0];
|
|
[messages addObject:mxEvent];
|
|
// Refresh table display
|
|
[self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
|
|
[self scrollToBottomAnimated:YES];
|
|
|
|
// Upload image and its thumbnail
|
|
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
|
|
NSUInteger thumbnailSize = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH - 5 * ROOM_MESSAGE_CELL_IMAGE_MARGIN;
|
|
[mxHandler.mxRestClient uploadImage:selectedImage thumbnailSize:thumbnailSize timeout:30 success:^(NSDictionary *imageMessage) {
|
|
// Send image
|
|
[self postMessage:imageMessage withLocalEventId:localEventId];
|
|
} failure:^(NSError *error) {
|
|
NSLog(@"Failed to upload image: %@", error);
|
|
//Alert user
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
// Update the temporary event with the failed event id
|
|
NSUInteger index = messages.count;
|
|
while (index--) {
|
|
MXEvent *mxEvent = [messages objectAtIndex:index];
|
|
if ([mxEvent.eventId isEqualToString:localEventId]) {
|
|
mxEvent.eventId = kFailedEventId;
|
|
// Refresh table display
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
|
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
|
[self scrollToBottomAnimated:YES];
|
|
break;
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
|
|
//TODO
|
|
}
|
|
|
|
[self dismissMediaPicker];
|
|
}
|
|
|
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
|
[self dismissMediaPicker];
|
|
}
|
|
|
|
- (void)dismissMediaPicker {
|
|
[[AppDelegate theDelegate].masterTabBarController dismissMediaPicker];
|
|
}
|
|
@end
|