Merge remote-tracking branch 'origin/develop'

This commit is contained in:
manuroe 2014-12-09 14:24:47 +01:00
commit e4de36cfa1
17 changed files with 816 additions and 429 deletions

View file

@ -39,7 +39,10 @@
F0CEA5B119E6898800E47915 /* tab_home.ico in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5B019E6898800E47915 /* tab_home.ico */; };
F0D3C30C1A011EF10000D49E /* AppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D3C30B1A011EF10000D49E /* AppSettings.m */; };
F0D3C30F1A01330F0000D49E /* SettingsTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D3C30E1A01330F0000D49E /* SettingsTableViewCell.m */; };
F0D942F61A31F3A300826CC1 /* RecentRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D942F51A31F3A300826CC1 /* RecentRoom.m */; };
F0E84D401A1F9AEC005F2E42 /* RecentsTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */; };
F0F90C691A32596700455977 /* icon_search.png in Resources */ = {isa = PBXBuildFile; fileRef = F0F90C681A32596700455977 /* icon_search.png */; };
F0F90C6B1A325ABF00455977 /* icon_search@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0F90C6A1A325ABF00455977 /* icon_search@2x.png */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -108,8 +111,12 @@
F0D3C30B1A011EF10000D49E /* AppSettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSettings.m; sourceTree = "<group>"; };
F0D3C30D1A01330F0000D49E /* SettingsTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsTableViewCell.h; sourceTree = "<group>"; };
F0D3C30E1A01330F0000D49E /* SettingsTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsTableViewCell.m; sourceTree = "<group>"; };
F0D942F41A31F3A300826CC1 /* RecentRoom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentRoom.h; sourceTree = "<group>"; };
F0D942F51A31F3A300826CC1 /* RecentRoom.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RecentRoom.m; sourceTree = "<group>"; };
F0E84D3E1A1F9AEC005F2E42 /* RecentsTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentsTableViewCell.h; sourceTree = "<group>"; };
F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RecentsTableViewCell.m; sourceTree = "<group>"; };
F0F90C681A32596700455977 /* icon_search.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_search.png; sourceTree = "<group>"; };
F0F90C6A1A325ABF00455977 /* icon_search@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_search@2x.png"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -153,6 +160,8 @@
F01628B519E298710071C473 /* Assets */ = {
isa = PBXGroup;
children = (
F0F90C6A1A325ABF00455977 /* icon_search@2x.png */,
F0F90C681A32596700455977 /* icon_search.png */,
F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */,
F02BCE221A1A5A2B00543B47 /* play.png */,
F024098119E7D177006E741B /* tab_recents@2x.png */,
@ -204,6 +213,8 @@
F0465AF71A251F85003639F9 /* Model */ = {
isa = PBXGroup;
children = (
F0D942F41A31F3A300826CC1 /* RecentRoom.h */,
F0D942F51A31F3A300826CC1 /* RecentRoom.m */,
F0465AF81A251F85003639F9 /* RoomMessage.h */,
F0465AF91A251F85003639F9 /* RoomMessage.m */,
F01A0FF11A27314B009FAE2F /* RoomMessageComponent.h */,
@ -372,7 +383,9 @@
F0CEA5AF19E6895E00E47915 /* tab_recents.png in Resources */,
F01628C319E29C660071C473 /* logo.png in Resources */,
F01628C119E29C660071C473 /* default-profile.png in Resources */,
F0F90C6B1A325ABF00455977 /* icon_search@2x.png in Resources */,
F0CEA5AE19E6895E00E47915 /* logoHighRes.png in Resources */,
F0F90C691A32596700455977 /* icon_search.png in Resources */,
F0CEA5B119E6898800E47915 /* tab_home.ico in Resources */,
F07A80E619DD9DE700B621A1 /* Images.xcassets in Resources */,
F08B6FCC1A1DE7F80094A35B /* matrixConsole.jpg in Resources */,
@ -435,6 +448,7 @@
F03EF5FA19F171EB00A0EE52 /* RoomViewController.m in Sources */,
F03EF5F819F171EB00A0EE52 /* MasterTabBarController.m in Sources */,
F03EF5F619F171EB00A0EE52 /* HomeViewController.m in Sources */,
F0D942F61A31F3A300826CC1 /* RecentRoom.m in Sources */,
F03EF60219F19E7C00A0EE52 /* MediaManager.m in Sources */,
F03EF5F919F171EB00A0EE52 /* RecentsViewController.m in Sources */,
F0465AFA1A251F85003639F9 /* RoomMessage.m in Sources */,

View file

@ -70,10 +70,16 @@
[self.errorNotification dismiss:NO];
self.errorNotification = nil;
}
// Suspend Matrix handler
[[MatrixHandler sharedHandler] pause];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
// Resume Matrix handler
[[MatrixHandler sharedHandler] resume];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

View file

@ -1024,7 +1024,7 @@
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" id="4dn-O0-IJ0">
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" id="4dn-O0-IJ0">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>

View file

@ -27,6 +27,7 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
@property (strong, nonatomic) NSString *homeServer;
@property (strong, nonatomic) NSString *userLogin;
@property (strong, nonatomic) NSString *userId;
@property (strong, nonatomic, readonly) NSString *localPartFromUserId;
@property (strong, nonatomic) NSString *accessToken;
// The type of events to display
@ -38,9 +39,12 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
@property (nonatomic,readonly) BOOL isLogged;
@property (nonatomic,readonly) BOOL isInitialSyncDone;
@property (nonatomic,readonly) BOOL isResumeDone;
+ (MatrixHandler *)sharedHandler;
- (void)pause;
- (void)resume;
- (void)logout;
// Flush and restore Matrix data
@ -50,6 +54,10 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
- (BOOL)isSupportedAttachment:(MXEvent*)event;
- (BOOL)isEmote:(MXEvent*)event;
// Note: the room state expected by the 3 following methods is the room state right before handling the event
- (NSString*)senderDisplayNameForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState;
- (NSString*)senderAvatarUrlForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState;
- (NSString*)displayTextForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState inSubtitleMode:(BOOL)isSubtitle;
@end

View file

@ -28,12 +28,13 @@ static MatrixHandler *sharedHandler = nil;
BOOL notifyOpenSessionFailure;
// Handle user's settings change
id roomMembersListener;
id userUpdateListener;
// Handle events notification
id eventsListener;
}
@property (nonatomic,readwrite) BOOL isInitialSyncDone;
@property (nonatomic,readwrite) BOOL isResumeDone;
@property (strong, nonatomic) CustomAlert *mxNotification;
@end
@ -58,6 +59,7 @@ static MatrixHandler *sharedHandler = nil;
-(MatrixHandler *)init {
if (self = [super init]) {
_isInitialSyncDone = NO;
_isResumeDone = NO;
notifyOpenSessionFailure = YES;
// Read potential homeserver url in shared defaults object
@ -109,7 +111,7 @@ static MatrixHandler *sharedHandler = nil;
self.mxSession = [[MXSession alloc] initWithMatrixRestClient:self.mxRestClient andStore:store];
// Check here whether the app user wants to display all the events
if ([[AppSettings sharedSettings] displayAllEvents]) {
// Use a filter to retrieve all the events
// Use a filter to retrieve all the events (except kMXEventTypeStringPresence which are not related to a specific room)
self.eventsFilterForMessages = @[
kMXEventTypeStringRoomName,
kMXEventTypeStringRoomTopic,
@ -117,13 +119,9 @@ static MatrixHandler *sharedHandler = nil;
kMXEventTypeStringRoomCreate,
kMXEventTypeStringRoomJoinRules,
kMXEventTypeStringRoomPowerLevels,
kMXEventTypeStringRoomAddStateLevel,
kMXEventTypeStringRoomSendEventLevel,
kMXEventTypeStringRoomOpsLevel,
kMXEventTypeStringRoomAliases,
kMXEventTypeStringRoomMessage,
kMXEventTypeStringRoomMessageFeedback,
kMXEventTypeStringPresence
kMXEventTypeStringRoomMessageFeedback
];
}
else {
@ -139,21 +137,16 @@ static MatrixHandler *sharedHandler = nil;
// Launch mxSession
[self.mxSession start:^{
self.isInitialSyncDone = YES;
_isResumeDone = YES;
// Register listener to update user's information
roomMembersListener = [self.mxSession listenToEventsOfTypes:@[kMXEventTypeStringPresence] onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// Consider only live events
if (direction == MXEventDirectionForwards) {
// Consider only events from app user
if ([event.userId isEqualToString:self.userId]) {
// Update local storage
if (![self.userDisplayName isEqualToString:event.content[@"displayname"]]) {
self.userDisplayName = event.content[@"displayname"];
}
if (![self.userPictureURL isEqualToString:event.content[@"avatar_url"]]) {
self.userPictureURL = event.content[@"avatar_url"];
}
}
userUpdateListener = [self.mxSession.myUser listenToUserUpdate:^(MXEvent *event) {
// Update local storage
if (![self.userDisplayName isEqualToString:event.content[@"displayname"]]) {
self.userDisplayName = event.content[@"displayname"];
}
if (![self.userPictureURL isEqualToString:event.content[@"avatar_url"]]) {
self.userPictureURL = event.content[@"avatar_url"];
}
}];
@ -182,9 +175,9 @@ static MatrixHandler *sharedHandler = nil;
[self.mxSession removeListener:eventsListener];
eventsListener = nil;
}
if (roomMembersListener) {
[self.mxSession removeListener:roomMembersListener];
roomMembersListener = nil;
if (userUpdateListener) {
[self.mxSession.myUser removeListener:userUpdateListener];
userUpdateListener = nil;
}
[self.mxSession close];
self.mxSession = nil;
@ -197,6 +190,7 @@ static MatrixHandler *sharedHandler = nil;
}
self.isInitialSyncDone = NO;
_isResumeDone = NO;
notifyOpenSessionFailure = YES;
}
@ -226,9 +220,25 @@ static MatrixHandler *sharedHandler = nil;
return (self.accessToken != nil);
}
- (void)pause {
if (self.mxSession) {
[self.mxSession pause];
self.isResumeDone = NO;
}
}
- (void)resume {
if (self.mxSession) {
[self.mxSession resume:^{
self.isResumeDone = YES;
}];
}
}
- (void)logout {
// Reset access token (mxSession is closed by setter)
self.accessToken = nil;
self.userId = nil;
// Reset local storage of user's settings
self.userDisplayName = @"";
@ -247,8 +257,8 @@ static MatrixHandler *sharedHandler = nil;
if (isEnabled) {
// Register events listener
eventsListener = [self.mxSession listenToEventsOfTypes:self.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// Consider only live event (Ignore presence event)
if (direction == MXEventDirectionForwards && (event.eventType != MXEventTypePresence)) {
// Consider only live event
if (direction == MXEventDirectionForwards) {
MXRoomState* roomState = (MXRoomState*)customObject;
// If we are running on background, show a local notif
if (UIApplicationStateBackground == [UIApplication sharedApplication].applicationState)
@ -258,8 +268,10 @@ static MatrixHandler *sharedHandler = nil;
localNotification.hasAction = YES;
[localNotification setAlertBody:[self displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES]];
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
} else if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId] == NO) {
// The concerned room is not presently visible, we display a notification by removing existing one (if any)
} else if (![event.userId isEqualToString:self.userId]
&& ![[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId]) {
// The sender is not the user and the concerned room is not presently visible,
// we display a notification by removing existing one (if any)
if (self.mxNotification) {
[self.mxNotification dismiss:NO];
}
@ -347,12 +359,28 @@ static MatrixHandler *sharedHandler = nil;
- (void)setUserId:(NSString *)inUserId {
if (inUserId.length) {
[[NSUserDefaults standardUserDefaults] setObject:inUserId forKey:@"userid"];
// Deduce local userid
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"localuserid"];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"@(.*):\\w+" options:NSRegularExpressionCaseInsensitive error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:inUserId options:0 range:NSMakeRange(0, [inUserId length])];
if (match.numberOfRanges == 2) {
NSString* localId = [inUserId substringWithRange:[match rangeAtIndex:1]];
if (localId) {
[[NSUserDefaults standardUserDefaults] setObject:localId forKey:@"localuserid"];
}
}
} else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userid"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"localuserid"];
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)localPartFromUserId {
return [[NSUserDefaults standardUserDefaults] objectForKey:@"localuserid"];
}
- (NSString *)accessToken {
return [[NSUserDefaults standardUserDefaults] objectForKey:@"accesstoken"];
}
@ -438,10 +466,32 @@ static MatrixHandler *sharedHandler = nil;
}
- (NSString*)senderDisplayNameForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState {
// Consider first the current display name defined in provided room state (Note: this room state is supposed to not take the new event into account)
NSString *senderDisplayName = [roomState memberName:event.userId];
// Check whether this sender name is updated by the current event (This happens in case of new joined member)
if ([event.content[@"displayname"] length]) {
// Use the actual display name
senderDisplayName = event.content[@"displayname"];
}
return senderDisplayName;
}
- (NSString*)senderAvatarUrlForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState {
// Consider first the avatar url defined in provided room state (Note: this room state is supposed to not take the new event into account)
NSString *senderAvatarUrl = [roomState memberWithUserId:event.userId].avatarUrl;
// Check whether this avatar url is updated by the current event (This happens in case of new joined member)
if ([event.content[@"avatar_url"] length]) {
// Use the actual display name
senderAvatarUrl = event.content[@"avatar_url"];
}
return senderAvatarUrl;
}
- (NSString*)displayTextForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState inSubtitleMode:(BOOL)isSubtitle {
NSString *displayText = nil;
// Prepare display name for concerned users
NSString *memberDisplayName = [roomState memberName:event.userId];
NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState];
NSString *targetDisplayName = nil;
if (event.stateKey) {
targetDisplayName = [roomState memberName:event.stateKey];
@ -449,11 +499,11 @@ static MatrixHandler *sharedHandler = nil;
switch (event.eventType) {
case MXEventTypeRoomName: {
displayText = [NSString stringWithFormat:@"%@ changed the room name to: %@", memberDisplayName, event.content[@"name"]];
displayText = [NSString stringWithFormat:@"%@ changed the room name to: %@", senderDisplayName, event.content[@"name"]];
break;
}
case MXEventTypeRoomTopic: {
displayText = [NSString stringWithFormat:@"%@ changed the topic to: %@", memberDisplayName, event.content[@"topic"]];
displayText = [NSString stringWithFormat:@"%@ changed the topic to: %@", senderDisplayName, event.content[@"topic"]];
break;
}
case MXEventTypeRoomMember: {
@ -494,30 +544,30 @@ static MatrixHandler *sharedHandler = nil;
if (displayText) {
displayText = [NSString stringWithFormat:@"%@ (picture profile was changed too)", displayText];
} else {
displayText = [NSString stringWithFormat:@"%@ changed their picture profile", memberDisplayName];
displayText = [NSString stringWithFormat:@"%@ changed their picture profile", senderDisplayName];
}
}
} else {
// Consider here a membership change
if ([membership isEqualToString:@"invite"]) {
displayText = [NSString stringWithFormat:@"%@ invited %@", memberDisplayName, targetDisplayName];
displayText = [NSString stringWithFormat:@"%@ invited %@", senderDisplayName, targetDisplayName];
} else if ([membership isEqualToString:@"join"]) {
displayText = [NSString stringWithFormat:@"%@ joined", memberDisplayName];
displayText = [NSString stringWithFormat:@"%@ joined", senderDisplayName];
} else if ([membership isEqualToString:@"leave"]) {
if ([event.userId isEqualToString:event.stateKey]) {
displayText = [NSString stringWithFormat:@"%@ left", memberDisplayName];
displayText = [NSString stringWithFormat:@"%@ left", senderDisplayName];
} else if (prevMembership) {
if ([prevMembership isEqualToString:@"join"] || [prevMembership isEqualToString:@"invite"]) {
displayText = [NSString stringWithFormat:@"%@ kicked %@", memberDisplayName, targetDisplayName];
displayText = [NSString stringWithFormat:@"%@ kicked %@", senderDisplayName, targetDisplayName];
if (event.content[@"reason"]) {
displayText = [NSString stringWithFormat:@"%@: %@", displayText, event.content[@"reason"]];
}
} else if ([prevMembership isEqualToString:@"ban"]) {
displayText = [NSString stringWithFormat:@"%@ unbanned %@", memberDisplayName, targetDisplayName];
displayText = [NSString stringWithFormat:@"%@ unbanned %@", senderDisplayName, targetDisplayName];
}
}
} else if ([membership isEqualToString:@"ban"]) {
displayText = [NSString stringWithFormat:@"%@ banned %@", memberDisplayName, targetDisplayName];
displayText = [NSString stringWithFormat:@"%@ banned %@", senderDisplayName, targetDisplayName];
if (event.content[@"reason"]) {
displayText = [NSString stringWithFormat:@"%@: %@", displayText, event.content[@"reason"]];
}
@ -559,6 +609,9 @@ static MatrixHandler *sharedHandler = nil;
if (event.content[@"redact"]) {
displayText = [NSString stringWithFormat:@"%@\r\n\u2022 redact: %@", displayText, event.content[@"redact"]];
}
if (event.content[@"invite"]) {
displayText = [NSString stringWithFormat:@"%@\r\n\u2022 invite: %@", displayText, event.content[@"invite"]];
}
displayText = [NSString stringWithFormat:@"%@\r\nThe minimum power levels related to events are:", displayText];
NSDictionary *events = event.content[@"events"];
@ -573,27 +626,6 @@ static MatrixHandler *sharedHandler = nil;
}
break;
}
// case MXEventTypeRoomAddStateLevel: {
// NSString *minLevel = event.content[@"level"];
// if (minLevel) {
// displayText = [NSString stringWithFormat:@"The minimum power level a user needs to add state is: %@", minLevel];
// }
// break;
// }
// case MXEventTypeRoomSendEventLevel: {
// NSString *minLevel = event.content[@"level"];
// if (minLevel) {
// displayText = [NSString stringWithFormat:@"The minimum power level a user needs to send an event is: %@", minLevel];
// }
// break;
// }
// case MXEventTypeRoomOpsLevel: {
// displayText = @"The minimum power levels that a user must have before acting are:";
// for (NSString *key in event.content.allKeys) {
// displayText = [NSString stringWithFormat:@"%@\r\n%@:%@", displayText, key, [event.content objectForKey:key]];
// }
// break;
// }
case MXEventTypeRoomAliases: {
NSArray *aliases = event.content[@"aliases"];
if (aliases) {
@ -606,7 +638,7 @@ static MatrixHandler *sharedHandler = nil;
displayText = [event.content[@"body"] isKindOfClass:[NSString class]] ? event.content[@"body"] : nil;
if ([msgtype isEqualToString:kMXMessageTypeEmote]) {
displayText = [NSString stringWithFormat:@"* %@ %@", memberDisplayName, displayText];
displayText = [NSString stringWithFormat:@"* %@ %@", senderDisplayName, displayText];
} else if ([msgtype isEqualToString:kMXMessageTypeImage]) {
displayText = displayText? displayText : @"image attachment";
// Check attachment validity
@ -654,7 +686,7 @@ static MatrixHandler *sharedHandler = nil;
// Check whether the sender name has to be added
if (isSubtitle && [msgtype isEqualToString:kMXMessageTypeEmote] == NO) {
displayText = [NSString stringWithFormat:@"%@: %@", memberDisplayName, displayText];
displayText = [NSString stringWithFormat:@"%@: %@", senderDisplayName, displayText];
}
break;

View file

@ -0,0 +1,28 @@
/*
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 <MatrixSDK/MatrixSDK.h>
@interface RecentRoom : NSObject
@property (nonatomic, readonly) NSString *roomId;
@property (nonatomic, readonly) MXEvent *lastEvent;
@property (nonatomic, readonly) NSUInteger unreadCount;
- (id)initWithLastEvent:(MXEvent*)event andMarkAsUnread:(BOOL)isUnread;
- (void)updateWithLastEvent:(MXEvent*)event andMarkAsUnread:(BOOL)isUnread;
- (void)resetUnreadCount;
@end

View file

@ -0,0 +1,50 @@
/*
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 "RecentRoom.h"
@implementation RecentRoom
- (id)initWithLastEvent:(MXEvent*)event andMarkAsUnread:(BOOL)isUnread {
if (self = [super init]) {
_lastEvent = event;
_unreadCount = isUnread ? 1 : 0;
}
return self;
}
- (void)updateWithLastEvent:(MXEvent*)event andMarkAsUnread:(BOOL)isUnread {
_lastEvent = event;
if (isUnread) {
_unreadCount ++;
}
}
- (void)resetUnreadCount {
_unreadCount = 0;
}
- (void)dealloc {
_lastEvent = nil;
}
#pragma mark -
- (NSString*)roomId {
return _lastEvent.roomId;
}
@end

View file

@ -36,15 +36,16 @@ static NSAttributedString *messageSeparator = nil;
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState {
if (self = [super init]) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
_senderId = event.userId;
_senderName = [roomState memberName:event.userId];
_senderAvatarUrl = [roomState memberWithUserId:event.userId].avatarUrl;
_senderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
_senderAvatarUrl = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
_contentSize = CGSizeZero;
currentAttributedTextMsg = nil;
// Set message type (consider text by default), and check attachment if any
_messageType = RoomMessageTypeText;
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
if ([mxHandler isSupportedAttachment:event]) {
// Note: event.eventType is equal here to MXEventTypeRoomMessage
NSString *msgtype = event.content[@"msgtype"];
@ -101,12 +102,14 @@ static NSAttributedString *messageSeparator = nil;
}
// Check sender information
if ((_senderName || [roomState memberName:event.userId]) &&
([_senderName isEqualToString:[roomState memberName:event.userId]] == NO)) {
NSString *eventSenderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
NSString *eventSenderAvatar = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
if ((_senderName || eventSenderName) &&
([_senderName isEqualToString:eventSenderName] == NO)) {
return NO;
}
if ((_senderAvatarUrl || [roomState memberWithUserId:event.userId].avatarUrl) &&
([_senderAvatarUrl isEqualToString:[roomState memberWithUserId:event.userId].avatarUrl] == NO)) {
if ((_senderAvatarUrl || eventSenderAvatar) &&
([_senderAvatarUrl isEqualToString:eventSenderAvatar] == NO)) {
return NO;
}

View file

@ -21,7 +21,7 @@ extern NSString *const kFailedEventId;
typedef enum : NSUInteger {
RoomMessageComponentStyleDefault,
RoomMessageComponentStyleHighlighted,
RoomMessageComponentStyleBing,
RoomMessageComponentStyleInProgress,
RoomMessageComponentStyleFailed,
RoomMessageComponentStyleUnsupported

View file

@ -33,7 +33,7 @@ NSString *const kFailedEventId = @"failedEventId";
_eventId = event.eventId;
_height = 0;
NSString *senderName = [roomState memberName:event.userId];
NSString *senderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
_startsWithSenderName = ([textMessage hasPrefix:senderName] || [mxHandler isEmote:event]);
// Set date time text label
@ -43,21 +43,22 @@ NSString *const kFailedEventId = @"failedEventId";
_date = nil;
}
// Set state event flag
_isStateEvent = (event.eventType != MXEventTypeRoomMessage);
// Set style
BOOL isIncomingMsg = ([event.userId isEqualToString:mxHandler.userId] == NO);
if ([textMessage hasPrefix:kMatrixHandlerUnsupportedMessagePrefix]) {
_style = RoomMessageComponentStyleUnsupported;
} else if ([_eventId hasPrefix:kFailedEventId]) {
_style = RoomMessageComponentStyleFailed;
} else if (isIncomingMsg && ([textMessage rangeOfString:mxHandler.userDisplayName options:NSCaseInsensitiveSearch].location != NSNotFound || [textMessage rangeOfString:mxHandler.userId options:NSCaseInsensitiveSearch].location != NSNotFound)) {
_style = RoomMessageComponentStyleHighlighted;
} else if (isIncomingMsg && !_isStateEvent && [self containsBingWord]) {
_style = RoomMessageComponentStyleBing;
} else if (!isIncomingMsg && [_eventId hasPrefix:kLocalEchoEventIdPrefix]) {
_style = RoomMessageComponentStyleInProgress;
} else {
_style = RoomMessageComponentStyleDefault;
}
_isStateEvent = (event.eventType != MXEventTypeRoomMessage);
} else {
// Ignore this event
self = nil;
@ -66,6 +67,29 @@ NSString *const kFailedEventId = @"failedEventId";
return self;
}
- (BOOL)containsBingWord {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSString *pattern = nil;
if (mxHandler.userDisplayName.length) {
pattern = [NSString stringWithFormat:@"\\b%@\\b", mxHandler.userDisplayName];
}
if (mxHandler.localPartFromUserId.length) {
if (pattern) {
pattern = [NSString stringWithFormat:@"(%@|\\b%@\\b)", pattern, mxHandler.localPartFromUserId];
} else {
pattern = [NSString stringWithFormat:@"\\b%@\\b", mxHandler.localPartFromUserId];
}
}
if (pattern) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
if ([regex numberOfMatchesInString:_textMessage options:0 range:NSMakeRange(0, [_textMessage length])]) {
return YES;
}
}
return NO;
}
- (NSDictionary*)stringAttributes {
UIColor *textColor;
UIFont *font;
@ -74,7 +98,7 @@ NSString *const kFailedEventId = @"failedEventId";
case RoomMessageComponentStyleDefault:
textColor = [UIColor blackColor];
break;
case RoomMessageComponentStyleHighlighted:
case RoomMessageComponentStyleBing:
textColor = [UIColor blueColor];
break;
case RoomMessageComponentStyleInProgress:

View file

@ -51,38 +51,23 @@
self.backgroundColor = [UIColor whiteColor];
// Handle power level display
self.userPowerLevel.hidden = NO;
NSDictionary *powerLevels;
if (room.state.powerLevels[@"users"]){
// In Matrix 0.5, users power levels are listed under the `users` dictionnary
powerLevels = room.state.powerLevels[@"users"];
}
else {
// @TODO: Remove this backward compatibility
powerLevels = room.state.powerLevels;
}
if (powerLevels) {
int maxLevel = 0;
for (NSString *powerLevel in powerLevels.allValues) {
int level = [powerLevel intValue];
if (level > maxLevel) {
maxLevel = level;
}
self.userPowerLevel.hidden = NO;
MXRoomPowerLevels *roomPowerLevels = room.state.powerLevels;
int maxLevel = 0;
for (NSString *powerLevel in roomPowerLevels.users.allValues) {
int level = [powerLevel intValue];
if (level > maxLevel) {
maxLevel = level;
}
NSString *userPowerLevel = [powerLevels objectForKey:roomMember.userId]; // CAUTION: we invoke objectForKey here because user_id starts with an '@' character
if (userPowerLevel == nil) {
userPowerLevel = [powerLevels valueForKey:@"default"];
}
float userPowerLevelFloat = 0.0;
if (userPowerLevel) {
userPowerLevelFloat = [userPowerLevel floatValue];
}
self.userPowerLevel.progress = maxLevel ? userPowerLevelFloat / maxLevel : 1;
} else {
self.userPowerLevel.progress = 0;
}
NSUInteger userPowerLevel = [roomPowerLevels powerLevelOfUserWithUserID:roomMember.userId];
float userPowerLevelFloat = 0.0;
if (userPowerLevel) {
userPowerLevelFloat = userPowerLevel;
}
self.userPowerLevel.progress = maxLevel ? userPowerLevelFloat / maxLevel : 1;
if (roomMember.membership == MXMembershipInvite) {
self.lastActiveAgoLabel.backgroundColor = [UIColor lightGrayColor];
self.lastActiveAgoLabel.text = @"invited";

View file

@ -16,7 +16,7 @@
#import <UIKit/UIKit.h>
@interface HomeViewController : UITableViewController <UITextFieldDelegate>
@interface HomeViewController : UITableViewController <UITextFieldDelegate, UISearchBarDelegate>
@end

View file

@ -19,12 +19,17 @@
#import "MatrixHandler.h"
#import "AppDelegate.h"
@interface HomeViewController () {
NSArray *publicRooms;
// List of public room names to highlight in displayed list
NSArray* highlightedPublicRooms;
// Search in public room
UISearchBar *recentsSearchBar;
NSMutableArray *filteredPublicRooms;
BOOL searchBarShouldEndEditing;
UIView *savedTableHeaderView;
}
@property (weak, nonatomic) IBOutlet UITableView *publicRoomsTable;
@ -64,6 +69,10 @@
- (void)dealloc{
publicRooms = nil;
highlightedPublicRooms = nil;
recentsSearchBar = nil;
filteredPublicRooms = nil;
savedTableHeaderView = nil;
}
- (void)viewWillAppear:(BOOL)animated {
@ -84,6 +93,10 @@
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Leave potential search session
if (recentsSearchBar) {
[self searchBarCancelButtonClicked:recentsSearchBar];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
}
@ -110,6 +123,29 @@
}
- (void)search:(id)sender {
if (!recentsSearchBar) {
// Check whether there are data in which search
if (publicRooms.count) {
// Create search bar
recentsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
recentsSearchBar.showsCancelButton = YES;
recentsSearchBar.returnKeyType = UIReturnKeyDone;
recentsSearchBar.delegate = self;
[recentsSearchBar becomeFirstResponder];
// Hide table header during search session
savedTableHeaderView = self.tableView.tableHeaderView;
self.tableView.tableHeaderView = nil;
// Reload table in order to display search bar as section header
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
[self.tableView reloadData];
}
} else {
[self searchBarCancelButtonClicked: recentsSearchBar];
}
}
- (void)dismissKeyboard {
// Hide the keyboard
[_roomNameTextField resignFirstResponder];
@ -289,32 +325,64 @@
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (filteredPublicRooms) {
return filteredPublicRooms.count;
}
return publicRooms.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (recentsSearchBar) {
return (recentsSearchBar.frame.size.height + 40);
}
return 40;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UILabel *sectionHeader = [[UILabel alloc] initWithFrame:[tableView rectForHeaderInSection:section]];
sectionHeader.font = [UIFont boldSystemFontOfSize:16];
UIView *sectionHeader = [[UIView alloc] initWithFrame:[tableView rectForHeaderInSection:section]];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
UILabel *sectionLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sectionHeader.frame.size.width, 40)];
sectionLabel.font = [UIFont boldSystemFontOfSize:16];
sectionLabel.backgroundColor = [UIColor clearColor];
[sectionHeader addSubview:sectionLabel];
if (publicRooms) {
NSString *homeserver = [MatrixHandler sharedHandler].homeServerURL;
if (homeserver.length) {
sectionHeader.text = [NSString stringWithFormat:@" Public Rooms (at %@):", homeserver];
sectionLabel.text = [NSString stringWithFormat:@" Public Rooms (at %@):", homeserver];
} else {
sectionHeader.text = @" Public Rooms:";
sectionLabel.text = @" Public Rooms:";
}
UIButton *searchButton = [UIButton buttonWithType:UIButtonTypeCustom];
[searchButton setImage:[UIImage imageNamed:@"icon_search"] forState:UIControlStateNormal];
[searchButton setImage:[UIImage imageNamed:@"icon_search"] forState:UIControlStateHighlighted];
[searchButton addTarget:self action:@selector(search:) forControlEvents:UIControlEventTouchUpInside];
searchButton.frame = CGRectMake(sectionLabel.frame.size.width - 45, 0, 40, 40);
[sectionHeader addSubview:searchButton];
sectionHeader.userInteractionEnabled = YES;
if (recentsSearchBar) {
CGRect frame = recentsSearchBar.frame;
frame.origin.y = 40;
recentsSearchBar.frame = frame;
[sectionHeader addSubview:recentsSearchBar];
}
} else {
sectionHeader.text = @" No Public Rooms";
sectionLabel.text = @" No Public Rooms";
}
return sectionHeader;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Cell is larger for public room with topic
MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row];
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
if (publicRoom.topic) {
return 60;
}
@ -322,8 +390,13 @@
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row];
UITableViewCell *cell;
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
// Check whether this public room has topic
if (publicRoom.topic) {
@ -351,10 +424,15 @@
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
// Check whether the user has already joined the selected public room
MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
if ([mxHandler.mxSession roomWithRoomId:publicRoom.roomId]) {
// Open selected room
[[AppDelegate theDelegate].masterTabBarController showRoom:publicRoom.roomId];
@ -385,4 +463,55 @@
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - UISearchBarDelegate
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
searchBarShouldEndEditing = NO;
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar {
return searchBarShouldEndEditing;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
// Update filtered list
if (searchText.length) {
if (filteredPublicRooms) {
[filteredPublicRooms removeAllObjects];
} else {
filteredPublicRooms = [NSMutableArray arrayWithCapacity:publicRooms.count];
}
for (MXPublicRoom *publicRoom in publicRooms) {
if ([[publicRoom displayname] rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[filteredPublicRooms addObject:publicRoom];
}
}
} else {
filteredPublicRooms = nil;
}
// Refresh display
[self.tableView reloadData];
if (filteredPublicRooms.count) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// "Done" key has been pressed
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
// Leave search
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
recentsSearchBar = nil;
filteredPublicRooms = nil;
// Restore table header and refresh table display
self.tableView.tableHeaderView = savedTableHeaderView;
[self.tableView reloadData];
}
@end

View file

@ -17,12 +17,14 @@
#import "RecentsViewController.h"
#import "RoomViewController.h"
#import "RecentRoom.h"
#import "RecentsTableViewCell.h"
#import "AppDelegate.h"
#import "MatrixHandler.h"
@interface RecentsViewController () {
// Array of RecentRooms
NSMutableArray *recents;
id recentsListener;
@ -59,16 +61,19 @@
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(createNewRoom:)];
self.navigationItem.rightBarButtonItems = @[searchButton, addButton];
// Add activity indicator
[self.view addSubview:_activityIndicator];
_activityIndicator.center = CGPointMake(self.view.center.x, 100);
[self.view bringSubviewToFront:_activityIndicator];
// Add background to activity indicator
CGRect frame = _activityIndicator.frame;
frame.size.width += 30;
frame.size.height += 30;
_activityIndicator.bounds = frame;
_activityIndicator.backgroundColor = [UIColor darkGrayColor];
[_activityIndicator.layer setCornerRadius:5];
// Initialisation
recents = nil;
filteredRecents = nil;
NSString *dateFormat = @"MMM dd HH:mm";
NSString *dateFormat = @"MMM dd HH:mm";
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
@ -79,6 +84,7 @@
- (void)dealloc {
if (currentRoomViewController) {
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
if (recentsListener) {
[[MatrixHandler sharedHandler].mxSession removeListener:recentsListener];
@ -102,9 +108,10 @@
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Refresh recents table
// Refresh display
[self configureView];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isInitialSyncDone" options:0 context:nil];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isResumeDone" options:0 context:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
@ -116,14 +123,22 @@
if (recentsSearchBar) {
[self searchBarCancelButtonClicked:recentsSearchBar];
}
if (recentsListener) {
[[MatrixHandler sharedHandler].mxSession removeListener:recentsListener];
recentsListener = nil;
}
// Hide activity indicator
[self stopActivityIndicator];
_preSelectedRoomId = nil;
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isInitialSyncDone"];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isResumeDone"];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Release potential Room ViewController if none is visible (Note: check on room visibility is required to handle correctly splitViewController)
if ([AppDelegate theDelegate].masterTabBarController.visibleRoomId == nil && currentRoomViewController) {
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
}
#pragma mark -
@ -142,8 +157,8 @@
// Look for the room index in recents list
NSIndexPath *indexPath = nil;
for (NSUInteger index = 0; index < recents.count; index++) {
MXEvent *mxEvent = [recents objectAtIndex:index];
if ([roomId isEqualToString:mxEvent.roomId]) {
RecentRoom *recentRoom = [recents objectAtIndex:index];
if ([roomId isEqualToString:recentRoom.roomId]) {
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
@ -159,7 +174,7 @@
// Postpone room details display. We run activity indicator until recents are updated
_preSelectedRoomId = roomId;
// Start activity indicator
[_activityIndicator startAnimating];
[self startActivityIndicator];
}
}
}
@ -169,41 +184,74 @@
- (void)configureView {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// Remove potential listener
if (recentsListener && mxHandler.mxSession) {
[mxHandler.mxSession removeListener:recentsListener];
recentsListener = nil;
}
[_activityIndicator startAnimating];
[self startActivityIndicator];
if ([mxHandler isInitialSyncDone] || [mxHandler isLogged] == NO) {
// Update recents
// Create/Update recents
if (mxHandler.mxSession) {
recents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
// Register recent listener
recentsListener = [mxHandler.mxSession listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// consider only live event
if (direction == MXEventDirectionForwards) {
// Refresh the whole recents list
recents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
// Reload table
[self.tableView reloadData];
[_activityIndicator stopAnimating];
// Check whether a room is preselected
if (_preSelectedRoomId) {
self.preSelectedRoomId = _preSelectedRoomId;
if (!recents) {
NSArray *recentEvents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
recents = [NSMutableArray arrayWithCapacity:recentEvents.count];
for (MXEvent *mxEvent in recentEvents) {
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:mxEvent andMarkAsUnread:NO];
if (recentRoom) {
[recents addObject:recentRoom];
}
}
}];
// Register recent listener
recentsListener = [mxHandler.mxSession listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// Consider only live event
if (direction == MXEventDirectionForwards) {
// Check user's membership in live room state (We will remove left rooms from recents)
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:event.roomId];
BOOL isLeft = (mxRoom == nil || mxRoom.state.membership == MXMembershipLeave || mxRoom.state.membership == MXMembershipBan);
// Consider this new event as unread only if the sender is not the user and if the room is not visible
BOOL isUnread = (![event.userId isEqualToString:mxHandler.userId]
&& ![[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId]);
// Look for the room
BOOL isFound = NO;
for (NSUInteger index = 0; index < recents.count; index++) {
RecentRoom *recentRoom = [recents objectAtIndex:index];
if ([event.roomId isEqualToString:recentRoom.roomId]) {
isFound = YES;
if (isLeft) {
// Remove left room
[recents removeObjectAtIndex:index];
} else {
[recentRoom updateWithLastEvent:event andMarkAsUnread:isUnread];
// Move this room at first position
[recents removeObjectAtIndex:index];
[recents insertObject:recentRoom atIndex:0];
}
break;
}
}
if (!isFound && !isLeft) {
// Insert in first position this new room
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:event andMarkAsUnread:isUnread];
if (recentRoom) {
[recents insertObject:recentRoom atIndex:0];
}
}
// Reload table
[self.tableView reloadData];
}
}];
}
// else nothing to do
} else {
recents = nil;
}
// Reload table
[self.tableView reloadData];
[_activityIndicator stopAnimating];
if ([mxHandler isResumeDone]) {
[self stopActivityIndicator];
}
// Check whether a room is preselected
if (_preSelectedRoomId) {
@ -213,6 +261,14 @@
recents = nil;
[self.tableView reloadData];
}
if (!recents) {
// Remove potential listener
if (recentsListener && mxHandler.mxSession) {
[mxHandler.mxSession removeListener:recentsListener];
recentsListener = nil;
}
}
}
- (void)createNewRoom:(id)sender {
@ -238,6 +294,18 @@
}
}
- (void)startActivityIndicator {
// Add the spinner on main screen in order to ignore potential table scrolling
_activityIndicator.center = CGPointMake(self.view.center.x, self.view.center.x);
[[AppDelegate theDelegate].window addSubview:_activityIndicator];
[_activityIndicator startAnimating];
}
- (void)stopActivityIndicator {
[_activityIndicator stopAnimating];
[_activityIndicator removeFromSuperview];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
@ -245,6 +313,12 @@
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
} else if ([@"isResumeDone" isEqualToString:keyPath]) {
if ([[MatrixHandler sharedHandler] isResumeDone]) {
[self stopActivityIndicator];
} else {
[self startActivityIndicator];
}
}
}
@ -253,11 +327,11 @@
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
MXEvent *mxEvent;
RecentRoom *recentRoom;
if (filteredRecents) {
mxEvent = filteredRecents[indexPath.row];
recentRoom = filteredRecents[indexPath.row];
} else {
mxEvent = recents[indexPath.row];
recentRoom = recents[indexPath.row];
}
UIViewController *controller;
@ -268,18 +342,24 @@
}
if ([controller isKindOfClass:[RoomViewController class]]) {
// Release potential Room ViewController
if (currentRoomViewController) {
if ((currentRoomViewController != controller) || (![currentRoomViewController.roomId isEqualToString:mxEvent.roomId])) {
// Release the current one
currentRoomViewController.roomId = nil;
}
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
currentRoomViewController = (RoomViewController *)controller;
currentRoomViewController.roomId = mxEvent.roomId;
currentRoomViewController.roomId = recentRoom.roomId;
}
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
// Reset unread count for this room
[recentRoom resetUnreadCount];
if (self.splitViewController) {
// Refresh display (required in case of splitViewController)
[self.tableView reloadData];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
}
@ -314,18 +394,18 @@
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecentsTableViewCell *cell = (RecentsTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"RecentsCell" forIndexPath:indexPath];
MXEvent *mxEvent;
RecentRoom *recentRoom;
if (filteredRecents) {
mxEvent = filteredRecents[indexPath.row];
recentRoom = filteredRecents[indexPath.row];
} else {
mxEvent = recents[indexPath.row];
recentRoom = recents[indexPath.row];
}
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:mxEvent.roomId];
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:recentRoom.roomId];
cell.roomTitle.text = [mxRoom.state displayname];
cell.lastEventDescription.text = [mxHandler displayTextForEvent:mxEvent withRoomState:mxRoom.state inSubtitleMode:YES];
cell.lastEventDescription.text = [mxHandler displayTextForEvent:recentRoom.lastEvent withRoomState:mxRoom.state inSubtitleMode:YES];
// Set in bold public room name
if (mxRoom.state.isPublic) {
@ -334,12 +414,20 @@
cell.roomTitle.font = [UIFont systemFontOfSize:19];
}
if (mxEvent.originServerTs != kMXUndefinedTimestamp) {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:mxEvent.originServerTs/1000];
if (recentRoom.lastEvent.originServerTs != kMXUndefinedTimestamp) {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:recentRoom.lastEvent.originServerTs/1000];
cell.recentDate.text = [dateFormatter stringFromDate:date];
} else {
cell.recentDate.text = nil;
}
}
// set background color
if (recentRoom.unreadCount) {
cell.backgroundColor = [UIColor colorWithRed:1 green:0.9 blue:0.9 alpha:1.0];
cell.roomTitle.text = [NSString stringWithFormat:@"%@ (%lu)", cell.roomTitle.text, (unsigned long)recentRoom.unreadCount];
} else {
cell.backgroundColor = [UIColor clearColor];
}
return cell;
}
@ -351,13 +439,13 @@
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Leave the selected room
MXEvent *mxEvent;
RecentRoom *recentRoom;
if (filteredRecents) {
mxEvent = filteredRecents[indexPath.row];
recentRoom = filteredRecents[indexPath.row];
} else {
mxEvent = recents[indexPath.row];
recentRoom = recents[indexPath.row];
}
MXRoom *mxRoom = [[MatrixHandler sharedHandler].mxSession roomWithRoomId:mxEvent.roomId];
MXRoom *mxRoom = [[MatrixHandler sharedHandler].mxSession roomWithRoomId:recentRoom.roomId];
[mxRoom leave:^{
// Refresh table display
if (filteredRecents) {
@ -367,7 +455,7 @@
}
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} failure:^(NSError *error) {
NSLog(@"Failed to leave room (%@) failed: %@", mxEvent.roomId, error);
NSLog(@"Failed to leave room (%@) failed: %@", recentRoom.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
@ -376,6 +464,11 @@
#pragma mark - UISearchBarDelegate
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
searchBarShouldEndEditing = NO;
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar {
return searchBarShouldEndEditing;
}
@ -389,10 +482,10 @@
filteredRecents = [NSMutableArray arrayWithCapacity:recents.count];
}
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
for (MXEvent *mxEvent in recents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:mxEvent.roomId];
for (RecentRoom *recentRoom in recents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:recentRoom.roomId];
if ([[mxRoom.state displayname] rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[filteredRecents addObject:mxEvent];
[filteredRecents addObject:recentRoom];
}
}
} else {

View file

@ -28,7 +28,8 @@
#import "MediaManager.h"
#define UPLOAD_FILE_SIZE 5000000
#define ROOMVIEWCONTROLLER_UPLOAD_FILE_SIZE 5000000
#define ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE 20
#define ROOM_MESSAGE_CELL_DEFAULT_HEIGHT 50
#define ROOM_MESSAGE_CELL_DEFAULT_TEXTVIEW_TOP_CONST 10
@ -57,7 +58,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
// Back pagination
BOOL isBackPaginationInProgress;
NSUInteger backPaginationAddedItemsNb;
NSUInteger backPaginationAddedMsgNb;
// Members list
NSArray *members;
@ -96,6 +97,8 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
forceScrollToBottomOnViewDidAppear = YES;
// Hide messages table by default in order to hide initial scrolling to the bottom
self.messagesTableView.hidden = YES;
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
[button addTarget:self action:@selector(showHideRoomMembers:) forControlEvents:UIControlEventTouchUpInside];
@ -130,6 +133,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[mxRoom removeListener:messagesListener];
messagesListener = nil;
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedMessages"];
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"displayAllEvents"];
}
mxRoom = nil;
@ -165,9 +169,6 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[[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 {
@ -185,21 +186,29 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[[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];
// Set visible room id
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId;
if (forceScrollToBottomOnViewDidAppear) {
// Scroll to the bottom
[self scrollToBottomAnimated:animated];
forceScrollToBottomOnViewDidAppear = NO;
self.messagesTableView.hidden = NO;
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Reset visible room id
[AppDelegate theDelegate].masterTabBarController.visibleRoomId = nil;
}
#pragma mark - room ID
- (void)setRoomId:(NSString *)roomId {
@ -240,6 +249,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[mxRoom removeListener:messagesListener];
messagesListener = nil;
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"hideUnsupportedMessages"];
[[AppSettings sharedSettings] removeObserver:self forKeyPath:@"displayAllEvents"];
}
// The whole room history is flushed here to rebuild it from the current instant (live)
messages = nil;
@ -247,10 +257,12 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
self.roomNameTextField.enabled = NO;
// Update room data
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
mxRoom = nil;
if (self.roomId) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
mxRoom = [mxHandler.mxSession roomWithRoomId:self.roomId];
}
if (mxRoom) {
// Update room title
self.roomNameTextField.text = mxRoom.state.displayname;
@ -279,37 +291,38 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
messages = [NSMutableArray array];
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"hideUnsupportedMessages" options:0 context:nil];
[[AppSettings sharedSettings] addObserver:self forKeyPath:@"displayAllEvents" options:0 context:nil];
// Register a listener to handle messages
messagesListener = [mxRoom listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
BOOL shouldScrollToBottom = NO;
// Handle first live events
if (direction == MXEventDirectionForwards) {
// Check user's membership in live room state (Indeed we have to go back on recents when user leaves, or is kicked/banned)
if (mxRoom.state.membership == MXMembershipLeave || mxRoom.state.membership == MXMembershipBan) {
[self.navigationController popViewControllerAnimated:NO];
return;
}
// We will scroll to bottom after updating tableView only if the most recent message is entirely visible.
CGFloat maxPositionY = self.messagesTableView.contentOffset.y + (self.messagesTableView.frame.size.height - self.messagesTableView.contentInset.bottom);
shouldScrollToBottom = (maxPositionY >= self.messagesTableView.contentSize.height);
// Update Table
NSIndexPath *indexPathForInsertedRow = nil;
NSIndexPath *indexPathForDeletedRow = nil;
NSMutableArray *indexPathsForUpdatedRows = [NSMutableArray array];
BOOL isComplete = NO;
BOOL isHandled = NO;
// For outgoing message, remove the temporary event
if ([event.userId isEqualToString:[MatrixHandler sharedHandler].userId] && messages.count) {
// Consider first the last message
RoomMessage *message = [messages lastObject];
NSUInteger index = messages.count - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
if ([message containsEventId:event.eventId]) {
if (message.messageType == RoomMessageTypeText) {
// Removing temporary event (local echo)
[message removeEvent:event.eventId];
// Update message with the received event
isComplete = [message addEvent:event withRoomState:roomState];
if (message.attributedTextMessage.length) {
[indexPathsForUpdatedRows addObject:indexPath];
} else {
isHandled = [message addEvent:event withRoomState:roomState];
if (! message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
indexPathForDeletedRow = indexPath;
}
} else {
// Create a new message to handle attachment
@ -317,31 +330,24 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (!message) {
// Ignore unsupported/unexpected events
[messages removeObjectAtIndex:index];
indexPathForDeletedRow = indexPath;
} else {
[messages replaceObjectAtIndex:index withObject:message];
[indexPathsForUpdatedRows addObject:indexPath];
}
isComplete = YES;
isHandled = YES;
}
} else {
while (index--) {
message = [messages objectAtIndex:index];
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
if ([message containsEventId:event.eventId]) {
if (message.messageType == RoomMessageTypeText) {
// Removing temporary event (local echo)
[message removeEvent:event.eventId];
if (message.attributedTextMessage.length) {
[indexPathsForUpdatedRows addObject:indexPath];
} else {
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
indexPathForDeletedRow = indexPath;
}
} else {
// Remove the local event (a new one will be added to messages)
[messages removeObjectAtIndex:index];
indexPathForDeletedRow = indexPath;
}
break;
}
@ -349,51 +355,29 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
}
}
if (isComplete == NO) {
if (isHandled == NO) {
// Check whether the event may be grouped with last message
RoomMessage *lastMessage = [messages lastObject];
if (lastMessage && [lastMessage addEvent:event withRoomState:roomState]) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(messages.count - 1) inSection:0];
[indexPathsForUpdatedRows addObject:indexPath];
isHandled = YES;
} else {
// Create a new item
lastMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomState];
if (lastMessage) {
indexPathForInsertedRow = [NSIndexPath indexPathForRow:messages.count inSection:0];
[messages addObject:lastMessage];
isHandled = YES;
} // else ignore unsupported/unexpected events
}
}
// Refresh table display
BOOL isModified = NO;
[UIView setAnimationsEnabled:NO];
[self.messagesTableView beginUpdates];
if (indexPathForDeletedRow) {
if (indexPathForInsertedRow) {
[indexPathsForUpdatedRows removeAllObjects];
NSUInteger index = indexPathForDeletedRow.row;
for (; index < messages.count; index++) {
[indexPathsForUpdatedRows addObject:[NSIndexPath indexPathForRow:index inSection:0]];
// Refresh table display except if a back pagination is in progress
if (!isBackPaginationInProgress) {
[self.messagesTableView reloadData];
if (isHandled) {
if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:self.roomId] == NO) {
// Some new events are received for this room while it is not visible, scroll to bottom on viewDidAppear to focus on them
forceScrollToBottomOnViewDidAppear = YES;
}
} else {
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPathForDeletedRow] withRowAnimation:UITableViewRowAnimationNone];
isModified = YES;
}
} else if (indexPathForInsertedRow) {
[self.messagesTableView insertRowsAtIndexPaths:@[indexPathForInsertedRow] withRowAnimation:UITableViewRowAnimationNone];
isModified = YES;
}
if (indexPathsForUpdatedRows.count) {
[self.messagesTableView reloadRowsAtIndexPaths:indexPathsForUpdatedRows withRowAnimation:UITableViewRowAnimationNone];
isModified = YES;
}
[self.messagesTableView endUpdates];
[UIView setAnimationsEnabled:YES];
if (isModified) {
if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:self.roomId] == NO) {
// Some new events are received for this room while it is not visible, scroll to bottom on viewDidAppear to focus on them
forceScrollToBottomOnViewDidAppear = YES;
}
}
} else if (isBackPaginationInProgress && direction == MXEventDirectionBackwards) {
@ -403,7 +387,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
firstMessage = [[RoomMessage alloc] initWithEvent:event andRoomState:roomState];
if (firstMessage) {
[messages insertObject:firstMessage atIndex:0];
backPaginationAddedItemsNb++;
backPaginationAddedMsgNb++;
}
// Ignore unsupported/unexpected events
}
@ -419,7 +403,6 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
[mxRoom resetBackState];
[self triggerBackPagination];
} else {
mxRoom = nil;
// Update room title
self.roomNameTextField.text = nil;
}
@ -445,72 +428,81 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (mxRoom.canPaginate) {
[_activityIndicator startAnimating];
isBackPaginationInProgress = YES;
backPaginationAddedItemsNb = 0;
backPaginationAddedMsgNb = 0;
[mxRoom paginateBackMessages:20 complete:^{
// Sanity check: check whether the view controller has not been released while back pagination was running
if (self.roomId == nil) {
return;
}
if (backPaginationAddedItemsNb) {
// Prepare insertion of new rows at the top of the table (compute cumulative height of added cells)
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:backPaginationAddedItemsNb];
NSIndexPath *indexPath;
CGFloat verticalOffset = 0;
for (NSUInteger index = 0; index < backPaginationAddedItemsNb; index++) {
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPaths addObject:indexPath];
verticalOffset += [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath];
}
// Here indexPath corresponds to the first added message (We will reuse it at the end of table update to make it visible)
// Reset count to enable tableView update
backPaginationAddedItemsNb = 0;
// 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];
isBackPaginationInProgress = NO;
// Scroll tableView in order to make visible the first added message (dispatch this action in order to let table end its refresh)
dispatch_async(dispatch_get_main_queue(), ^{
if (indexPath.row == messages.count - 1) {
[self scrollToBottomAnimated:NO];
} else {
[self.messagesTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
});
} else {
// Here there was no event related to the listened types
[_activityIndicator stopAnimating];
isBackPaginationInProgress = NO;
// Trigger a new back pagination (if possible)
[self triggerBackPagination];
}
} failure:^(NSError *error) {
[_activityIndicator stopAnimating];
isBackPaginationInProgress = NO;
backPaginationAddedItemsNb = 0;
NSLog(@"Failed to paginate back: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
[self paginateBackMessages:ROOMVIEWCONTROLLER_BACK_PAGINATION_SIZE];
}
}
- (void)paginateBackMessages:(NSUInteger)requestedItemsNb {
[mxRoom paginateBackMessages:requestedItemsNb complete:^{
// Sanity check: check whether the view controller has not been released while back pagination was running
if (self.roomId == nil) {
return;
}
// Compute number of received items
NSUInteger itemsCount = 0;
for (NSUInteger index = 0; index < backPaginationAddedMsgNb; index++) {
RoomMessage *message = [messages objectAtIndex:index];
itemsCount += message.components.count;
}
// Check whether we got enough items
if (itemsCount < requestedItemsNb && mxRoom.canPaginate) {
// Ask more items
[self paginateBackMessages:(requestedItemsNb - itemsCount)];
} else {
[self onBackPaginationComplete];
}
} failure:^(NSError *error) {
[self onBackPaginationComplete];
NSLog(@"Failed to paginate back: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
- (void)onBackPaginationComplete {
if (backPaginationAddedMsgNb) {
// We scroll to bottom when table is loaded for the first time
BOOL shouldScrollToBottom = (self.messagesTableView.contentSize.height == 0);
CGFloat verticalOffset = 0;
if (shouldScrollToBottom == NO) {
// In this case, we will adjust the vertical offset in order to make visible only a few part of added messages (at the top of the table)
NSIndexPath *indexPath;
// Compute the cumulative height of the added messages
for (NSUInteger index = 0; index < backPaginationAddedMsgNb; index++) {
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
verticalOffset += [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath];
}
// Deduce the vertical offset from this height
verticalOffset -= 100;
}
// Reset count to enable tableView update
backPaginationAddedMsgNb = 0;
// Reload
[self.messagesTableView reloadData];
// Adjust vertical content offset
if (shouldScrollToBottom) {
[self scrollToBottomAnimated:NO];
} else if (verticalOffset > 0) {
// Adjust vertical offset in order to limit scrolling down
CGPoint contentOffset = self.messagesTableView.contentOffset;
contentOffset.y = verticalOffset - self.messagesTableView.contentInset.top;
[self.messagesTableView setContentOffset:contentOffset animated:NO];
}
}
[_activityIndicator stopAnimating];
isBackPaginationInProgress = NO;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"hideUnsupportedMessages" isEqualToString:keyPath]) {
if ([@"displayAllEvents" isEqualToString:keyPath]) {
// Back to recents (Room details are not available until the end of initial sync)
[self.navigationController popViewControllerAnimated:NO];
} else if ([@"hideUnsupportedMessages" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
@ -800,7 +792,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
return members.count;
}
if (backPaginationAddedItemsNb) {
if (backPaginationAddedMsgNb) {
// Here some old messages have been added to messages during back pagination.
// Stop table refreshing, the table will be refreshed at the end of pagination
return 0;
@ -1093,7 +1085,6 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
// 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;
@ -1120,92 +1111,117 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
}
}];
} else {
// Check user's power level before allowing an action (kick, ban, ...)
MXRoomPowerLevels *powerLevels = [mxRoom.state powerLevels];
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:mxHandler.userId];
NSUInteger memberPowerLevel = [powerLevels powerLevelOfUserWithUserID:roomMember.userId];
// Consider membership of the selected member
switch (roomMember.membership) {
case MXMembershipInvite:
case MXMembershipJoin: {
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];
}];
// Check conditions to be able to kick someone
if (userPowerLevel >= [powerLevels kick] && userPowerLevel >= memberPowerLevel) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Kick" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[[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];
}];
}
}];
}
// Check conditions to be able to ban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
}
}];
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[[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];
}];
}
}];
[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];
}];
// Check conditions to be able to invite someone
if (userPowerLevel >= [powerLevels invite]) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[[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];
}];
}
}];
}
// Check conditions to be able to ban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
}
}];
[self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[[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];
}];
}
}];
[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];
}];
}
}];
// Check conditions to be able to unban someone
if (userPowerLevel >= [powerLevels ban] && userPowerLevel >= memberPowerLevel) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet];
[self.actionMenu addActionWithTitle:@"Unban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
if (weakSelf) {
weakSelf.actionMenu = nil;
[[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: {
@ -1214,14 +1230,18 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
}
}
// Display the action sheet (if any)
// Notify user when his power is too weak
if (!self.actionMenu) {
self.actionMenu = [[CustomAlert alloc] initWithTitle:nil message:@"You are not authorized to change the status of this member" style:CustomAlertStyleAlert];
}
// Display the action sheet (or the alert)
if (self.actionMenu) {
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[self.actionMenu showInViewController:self];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} else if (tableView == self.messagesTableView) {
// Dismiss keyboard when user taps on messages table view content
@ -1235,7 +1255,9 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
// paginate ?
if (scrollView.contentOffset.y < -64)
{
[self triggerBackPagination];
dispatch_async(dispatch_get_main_queue(), ^{
[self triggerBackPagination];
});
}
}
}
@ -1258,8 +1280,26 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (textField == self.roomNameTextField) {
self.roomNameTextField.borderStyle = UITextBorderStyleRoundedRect;
self.roomNameTextField.backgroundColor = [UIColor whiteColor];
// Check whether the user has enough power to rename the room
MXRoomPowerLevels *powerLevels = [mxRoom.state powerLevels];
NSUInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:[MatrixHandler sharedHandler].userId];
if (userPowerLevel >= [powerLevels minimumPowerLevelForPostingEventAsStateEvent:kMXEventTypeStringRoomName]) {
self.roomNameTextField.borderStyle = UITextBorderStyleRoundedRect;
self.roomNameTextField.backgroundColor = [UIColor whiteColor];
return YES;
} else {
// Alert user
__weak typeof(self) weakSelf = self;
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
}
self.actionMenu = [[CustomAlert alloc] initWithTitle:nil message:@"You are not authorized to edit this room name" style:CustomAlertStyleAlert];
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
}];
[self.actionMenu showInViewController:self];
}
return NO;
}
return YES;
}
@ -1268,14 +1308,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (textField == self.roomNameTextField) {
self.roomNameTextField.borderStyle = UITextBorderStyleNone;
self.roomNameTextField.backgroundColor = [UIColor clearColor];
}
}
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
// "Done" key has been pressed
[textField resignFirstResponder];
if (textField == self.roomNameTextField) {
NSString *roomName = self.roomNameTextField.text;
if ([roomName isEqualToString:mxRoom.state.name] == NO) {
[self.activityIndicator startAnimating];
@ -1296,6 +1329,11 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
}];
}
}
}
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
// "Done" key has been pressed
[textField resignFirstResponder];
return YES;
}
@ -1409,17 +1447,12 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
while (index--) {
message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
localEvent.content = msgContent;
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
[message addEvent:localEvent withRoomState:mxRoom.state];
if (message.attributedTextMessage.length) {
// Refresh table display
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
} else {
// Create a new message
@ -1427,15 +1460,14 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}
break;
}
}
[self.messagesTableView reloadData];
} else {
// Create a temporary event to displayed outgoing message (local echo)
NSString* localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]];
@ -1449,19 +1481,16 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
localEvent.originServerTs = kMXUndefinedTimestamp;
// Check whether this new event may be grouped with last message
RoomMessage *lastMessage = [messages lastObject];
if (lastMessage && [lastMessage addEvent:localEvent withRoomState:mxRoom.state]) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(messages.count - 1) inSection:0];
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
if (lastMessage == nil || [lastMessage addEvent:localEvent withRoomState:mxRoom.state] == NO) {
// Create a new item
lastMessage = [[RoomMessage alloc] initWithEvent:localEvent andRoomState:mxRoom.state];
if (lastMessage) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0];
[messages addObject:lastMessage];
[self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"ERROR: Unable to add local event: %@", localEvent.description);
}
}
[self.messagesTableView reloadData];
[self scrollToBottomAnimated:NO];
}
@ -1482,7 +1511,6 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
while (index--) {
RoomMessage *message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
if (isEventAlreadyAddedToRoom == NO) {
@ -1490,12 +1518,8 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
localEvent.eventId = event_id;
[message addEvent:localEvent withRoomState:mxRoom.state];
}
if (message.attributedTextMessage.length) {
// Refresh table display
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
if (! message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
} else {
message = nil;
@ -1507,15 +1531,14 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}
break;
}
}
[self.messagesTableView reloadData];
} failure:^(NSError *error) {
[self handleError:error forLocalEvent:localEvent];
}];
@ -1564,13 +1587,11 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
// Update table sources
RoomMessage *message = [[RoomMessage alloc] initWithEvent:mxEvent andRoomState:mxRoom.state];
if (message) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0];
[messages addObject:message];
[self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"ERROR: Unable to add local event for attachment: %@", mxEvent.description);
}
[self.messagesTableView reloadData];
[self scrollToBottomAnimated:NO];
return mxEvent;
}
@ -1588,17 +1609,12 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
RoomMessage *message = [messages objectAtIndex:index];
if ([message containsEventId:localEvent.eventId]) {
NSLog(@"Posted event: %@", localEvent.description);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
if (message.messageType == RoomMessageTypeText) {
[message removeEvent:localEvent.eventId];
localEvent.eventId = kFailedEventId;
[message addEvent:localEvent withRoomState:mxRoom.state];
if (message.attributedTextMessage.length) {
// Refresh table display
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
if (!message.attributedTextMessage.length) {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
} else {
// Create a new message
@ -1607,15 +1623,14 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
if (message) {
// Refresh table display
[messages replaceObjectAtIndex:index withObject:message];
[self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
[messages removeObjectAtIndex:index];
[self.messagesTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}
break;
}
}
[self.messagesTableView reloadData];
}
- (BOOL)isIRCStyleCommand:(NSString*)text{
@ -1878,7 +1893,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop";
NSData *videoData = [NSData dataWithContentsOfURL:tmpVideoLocation];
[[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil];
if (videoData) {
if (videoData.length < UPLOAD_FILE_SIZE) {
if (videoData.length < ROOMVIEWCONTROLLER_UPLOAD_FILE_SIZE) {
[videoInfo setValue:[NSNumber numberWithUnsignedInteger:videoData.length] forKey:@"size"];
[mxHandler.mxRestClient uploadContent:videoData mimeType:videoInfo[@"mimetype"] timeout:30 success:^(NSString *url) {
[videoContent setValue:url forKey:@"url"];