diff --git a/matrixConsole/matrixConsole.xcodeproj/project.pbxproj b/matrixConsole/matrixConsole.xcodeproj/project.pbxproj index a70c03724..99f5a267f 100644 --- a/matrixConsole/matrixConsole.xcodeproj/project.pbxproj +++ b/matrixConsole/matrixConsole.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 71193D381A6E49F000E59A9E /* MXCPhoneNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 71193D351A6E49F000E59A9E /* MXCPhoneNumber.m */; }; 71193D3B1A6E50EC00E59A9E /* ContactManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 71193D3A1A6E50EC00E59A9E /* ContactManager.m */; }; 71193D3E1A6E61AD00E59A9E /* SectionedContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 71193D3D1A6E61AD00E59A9E /* SectionedContacts.m */; }; + 7176294C1A77F5D000927125 /* ContactDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7176294B1A77F5D000927125 /* ContactDetailsViewController.m */; }; + 7176294F1A77FED800927125 /* ContactDetailsTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 7176294E1A77FED800927125 /* ContactDetailsTableCell.m */; }; 71D2E4EC1A49814B000DE015 /* RoomMemberActionsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D2E4EB1A49814B000DE015 /* RoomMemberActionsCell.m */; }; 71DB9DC11A495B6400504A09 /* MemberViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 71DB9DC01A495B6400504A09 /* MemberViewController.m */; }; 71E94A771A5C4020009F52E5 /* PieChartView.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E94A761A5C4020009F52E5 /* PieChartView.m */; }; @@ -60,6 +62,7 @@ F07A80F519DD9DE700B621A1 /* matrixConsoleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F07A80F419DD9DE700B621A1 /* matrixConsoleTests.m */; }; F08B6FCC1A1DE7F80094A35B /* matrixConsole.jpg in Resources */ = {isa = PBXBuildFile; fileRef = F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */; }; F08DCBDB1A093BFA008C65B6 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F08DCBDA1A093BFA008C65B6 /* MobileCoreServices.framework */; }; + F08E67961A77965A00AABD4C /* MXC3PID.m in Sources */ = {isa = PBXBuildFile; fileRef = F08E67931A77965A00AABD4C /* MXC3PID.m */; }; F0CEA5AE19E6895E00E47915 /* logoHighRes.png in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5AC19E6895E00E47915 /* logoHighRes.png */; }; F0CEA5AF19E6895E00E47915 /* tab_recents.png in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5AD19E6895E00E47915 /* tab_recents.png */; }; F0CEA5B119E6898800E47915 /* tab_home.ico in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5B019E6898800E47915 /* tab_home.ico */; }; @@ -106,6 +109,10 @@ 71193D3A1A6E50EC00E59A9E /* ContactManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactManager.m; sourceTree = ""; }; 71193D3C1A6E61AD00E59A9E /* SectionedContacts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SectionedContacts.h; sourceTree = ""; }; 71193D3D1A6E61AD00E59A9E /* SectionedContacts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SectionedContacts.m; sourceTree = ""; }; + 7176294A1A77F5D000927125 /* ContactDetailsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactDetailsViewController.h; sourceTree = ""; }; + 7176294B1A77F5D000927125 /* ContactDetailsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactDetailsViewController.m; sourceTree = ""; }; + 7176294D1A77FED800927125 /* ContactDetailsTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactDetailsTableCell.h; sourceTree = ""; }; + 7176294E1A77FED800927125 /* ContactDetailsTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactDetailsTableCell.m; sourceTree = ""; }; 71D2E4EA1A49814B000DE015 /* RoomMemberActionsCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMemberActionsCell.h; sourceTree = ""; }; 71D2E4EB1A49814B000DE015 /* RoomMemberActionsCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMemberActionsCell.m; sourceTree = ""; }; 71DB9DBF1A495B6400504A09 /* MemberViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MemberViewController.h; sourceTree = ""; }; @@ -174,6 +181,8 @@ F07A80F419DD9DE700B621A1 /* matrixConsoleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = matrixConsoleTests.m; sourceTree = ""; }; F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = matrixConsole.jpg; sourceTree = ""; }; F08DCBDA1A093BFA008C65B6 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + F08E67921A77965A00AABD4C /* MXC3PID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MXC3PID.h; sourceTree = ""; }; + F08E67931A77965A00AABD4C /* MXC3PID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MXC3PID.m; sourceTree = ""; }; F0CEA5AC19E6895E00E47915 /* logoHighRes.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logoHighRes.png; sourceTree = ""; }; F0CEA5AD19E6895E00E47915 /* tab_recents.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_recents.png; sourceTree = ""; }; F0CEA5B019E6898800E47915 /* tab_home.ico */ = {isa = PBXFileReference; lastKnownFileType = image.ico; path = tab_home.ico; sourceTree = ""; }; @@ -274,6 +283,8 @@ F03EF5E919F171EB00A0EE52 /* ViewController */ = { isa = PBXGroup; children = ( + 7176294A1A77F5D000927125 /* ContactDetailsViewController.h */, + 7176294B1A77F5D000927125 /* ContactDetailsViewController.m */, 71193D271A6E3DC000E59A9E /* ContactsViewController.h */, 71193D281A6E3DC000E59A9E /* ContactsViewController.m */, F03EF5EA19F171EB00A0EE52 /* HomeViewController.h */, @@ -297,6 +308,8 @@ F03EF5FC19F1762000A0EE52 /* View */ = { isa = PBXGroup; children = ( + 7176294D1A77FED800927125 /* ContactDetailsTableCell.h */, + 7176294E1A77FED800927125 /* ContactDetailsTableCell.m */, 71193D2A1A6E433900E59A9E /* ContactTableCell.h */, 71193D2B1A6E433900E59A9E /* ContactTableCell.m */, F05237811A728A0A0079F3E0 /* MXCGrowingTextView.h */, @@ -324,6 +337,8 @@ F0465AF71A251F85003639F9 /* Model */ = { isa = PBXGroup; children = ( + F08E67921A77965A00AABD4C /* MXC3PID.h */, + F08E67931A77965A00AABD4C /* MXC3PID.m */, F052377E1A728A010079F3E0 /* MXCAlert.h */, F052377F1A728A010079F3E0 /* MXCAlert.m */, 71193D301A6E49F000E59A9E /* MXCContact.h */, @@ -565,6 +580,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7176294F1A77FED800927125 /* ContactDetailsTableCell.m in Sources */, F04A8AD81A3B3DF4008AC915 /* RoomTitleView.m in Sources */, F07A80DB19DD9DE700B621A1 /* AppDelegate.m in Sources */, F052377C1A7289F00079F3E0 /* APNSHandler.m in Sources */, @@ -585,6 +601,7 @@ 71DB9DC11A495B6400504A09 /* MemberViewController.m in Sources */, F0D942F61A31F3A300826CC1 /* RecentRoom.m in Sources */, F03EF5F919F171EB00A0EE52 /* RecentsViewController.m in Sources */, + F08E67961A77965A00AABD4C /* MXC3PID.m in Sources */, 71E94A771A5C4020009F52E5 /* PieChartView.m in Sources */, F0465AFA1A251F85003639F9 /* RoomMessage.m in Sources */, F021FBF21A5F1F8E00EA3AE6 /* MediaManager.m in Sources */, @@ -598,6 +615,7 @@ F00B5DB91A1B9BCE00EA1C8D /* MXCImageView.m in Sources */, F0D3C30C1A011EF10000D49E /* AppSettings.m in Sources */, F05237831A728A0A0079F3E0 /* MXCGrowingTextView.m in Sources */, + 7176294C1A77F5D000927125 /* ContactDetailsViewController.m in Sources */, F03EF5F719F171EB00A0EE52 /* LoginViewController.m in Sources */, F0D3C30F1A01330F0000D49E /* SettingsTableViewCell.m in Sources */, 71D2E4EC1A49814B000DE015 /* RoomMemberActionsCell.m in Sources */, diff --git a/matrixConsole/matrixConsole/API/ContactManager.m b/matrixConsole/matrixConsole/API/ContactManager.m index 46e068945..4ef351e4b 100644 --- a/matrixConsole/matrixConsole/API/ContactManager.m +++ b/matrixConsole/matrixConsole/API/ContactManager.m @@ -104,6 +104,9 @@ static ContactManager* sharedContactManager = nil; } [self saveMatrixIDsDict]; + + // warn of the contacts list update + [[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil]; } - (void)fullRefresh { diff --git a/matrixConsole/matrixConsole/API/MatrixSDKHandler.h b/matrixConsole/matrixConsole/API/MatrixSDKHandler.h index 2b9ee62e0..c043a623f 100644 --- a/matrixConsole/matrixConsole/API/MatrixSDKHandler.h +++ b/matrixConsole/matrixConsole/API/MatrixSDKHandler.h @@ -75,8 +75,16 @@ typedef enum : NSUInteger { // return a MatrixIDs list of 1:1 room members - (NSArray*)oneToOneRoomMemberMatrixIDs; +// search if a private room has been started with this user +// returns the room ID +// nil if not found +- (NSString*) privateRoomIdWith:(NSString*)otherMatrixID; + +// check first if there no room between the both users +// if there is one, open it +// else create a new one // create a private one to one chat room -- (void)createPrivateOneToOneRoomWith:(NSString*)otherMatrixID; +- (void)startPrivateOneToOneRoomWith:(NSString*)otherMatrixID; // Return the suitable url to display the content thumbnail into the provided view size // Note: the provided view size is supposed in points, this method will convert this size in pixels by considering screen scale @@ -87,9 +95,6 @@ typedef enum : NSUInteger { - (NSString*)senderAvatarUrlForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState; - (NSString*)displayTextForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState inSubtitleMode:(BOOL)isSubtitle; -// search if a 1:1 conversation has been started with this member -- (NSString*)getRoomStartedWithMember:(MXRoomMember*)roomMember; - // user power level in a dedicated room - (CGFloat)getPowerLevel:(MXRoomMember *)roomMember inRoom:(MXRoom *)room; diff --git a/matrixConsole/matrixConsole/API/MatrixSDKHandler.m b/matrixConsole/matrixConsole/API/MatrixSDKHandler.m index a423f899b..6bfbf228b 100644 --- a/matrixConsole/matrixConsole/API/MatrixSDKHandler.m +++ b/matrixConsole/matrixConsole/API/MatrixSDKHandler.m @@ -542,35 +542,77 @@ static MatrixSDKHandler *sharedHandler = nil; return matrixIDs; } +// search if a private room has been started with this user +// returns the room ID +// nil if not found +- (NSString*) privateRoomIdWith:(NSString*)otherMatrixID { + // + if (self.mxSession) { + // list the last messages of each room to get the rooms list + NSArray *recentEvents = [NSMutableArray arrayWithArray:[self.mxSession recentsWithTypeIn:self.eventsFilterForMessages]]; + + // loops + for (MXEvent *mxEvent in recentEvents) { + // get the dedicated mxRooms + MXRoom *mxRoom = [self.mxSession roomWithRoomId:mxEvent.roomId]; + + // accept only room with 2 users + if (mxRoom.state.members.count == 2) { + NSArray* roomMembers = mxRoom.state.members; + + MXRoomMember* member1 = [roomMembers objectAtIndex:0]; + MXRoomMember* member2 = [roomMembers objectAtIndex:1]; + + // check if they are the dedicated users + if ( + ([member1.userId isEqualToString:self.mxSession.myUser.userId] || [member1.userId isEqualToString:otherMatrixID]) && + ([member2.userId isEqualToString:self.mxSession.myUser.userId] || [member2.userId isEqualToString:otherMatrixID])) { + return mxRoom.state.roomId; + } + } + } + } + + return nil; +} + // create a private one to one chat room -- (void)createPrivateOneToOneRoomWith:(NSString*)otherMatrixID { - // sanity check +- (void)startPrivateOneToOneRoomWith:(NSString*)otherMatrixID { if (self.mxRestClient) { - [self.mxRestClient createRoom:nil - visibility:kMXRoomVisibilityPrivate - roomAlias:nil - topic:nil - success:^(MXCreateRoomResponse *response) { - - // invite the other user only if it is defined and not onself - if (otherMatrixID && ![self.userId isEqualToString:otherMatrixID]) { - // add the user - [self.mxRestClient inviteUser:otherMatrixID toRoom:response.roomId success:^{ - } failure:^(NSError *error) { - NSLog(@"%@ invitation failed (roomId: %@): %@", otherMatrixID, response.roomId, error); - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; - } - - // Open created room - [[AppDelegate theDelegate].masterTabBarController showRoom:response.roomId]; - - } failure:^(NSError *error) { - NSLog(@"Create room failed: %@", error); - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; + NSString* roomId = [self privateRoomIdWith:otherMatrixID]; + + // if the room exists + if (roomId) { + // open it + [[AppDelegate theDelegate].masterTabBarController showRoom:roomId]; + } else { + // create a new room + [self.mxRestClient createRoom:nil + visibility:kMXRoomVisibilityPrivate + roomAlias:nil + topic:nil + success:^(MXCreateRoomResponse *response) { + + // invite the other user only if it is defined and not onself + if (otherMatrixID && ![self.userId isEqualToString:otherMatrixID]) { + // add the user + [self.mxRestClient inviteUser:otherMatrixID toRoom:response.roomId success:^{ + } failure:^(NSError *error) { + NSLog(@"%@ invitation failed (roomId: %@): %@", otherMatrixID, response.roomId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + + // Open created room + [[AppDelegate theDelegate].masterTabBarController showRoom:response.roomId]; + + } failure:^(NSError *error) { + NSLog(@"Create room failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } } } @@ -845,39 +887,6 @@ static MatrixSDKHandler *sharedHandler = nil; return displayText; } - -// search if a conversation has been started with this user -- (NSString*) getRoomStartedWithMember:(MXRoomMember*)roomMember { - // - if (self.mxSession) { - // list the last messages of each room to get the rooms list - NSArray *recentEvents = [NSMutableArray arrayWithArray:[self.mxSession recentsWithTypeIn:self.eventsFilterForMessages]]; - - // loops - for (MXEvent *mxEvent in recentEvents) { - // get the dedicated mxRooms - MXRoom *mxRoom = [self.mxSession roomWithRoomId:mxEvent.roomId]; - - // accept only room with 2 users - if (mxRoom.state.members.count == 2) { - NSArray* roomMembers = mxRoom.state.members; - - MXRoomMember* member1 = [roomMembers objectAtIndex:0]; - MXRoomMember* member2 = [roomMembers objectAtIndex:1]; - - // check if they are the dedicated users - if ( - ([member1.userId isEqualToString:self.mxSession.myUser.userId] || [member1.userId isEqualToString:roomMember.userId]) && - ([member2.userId isEqualToString:self.mxSession.myUser.userId] || [member2.userId isEqualToString:roomMember.userId])) { - return mxRoom.state.roomId; - } - } - } - } - - return nil; -} - - (NSUInteger) MXCacheSize { if (self.mxFileStore) { diff --git a/matrixConsole/matrixConsole/AppDelegate.m b/matrixConsole/matrixConsole/AppDelegate.m index e020f40b3..1fbd85216 100644 --- a/matrixConsole/matrixConsole/AppDelegate.m +++ b/matrixConsole/matrixConsole/AppDelegate.m @@ -176,7 +176,6 @@ - (void)logout { [[UIApplication sharedApplication] unregisterForRemoteNotifications]; [[APNSHandler sharedHandler] reset]; - [[ContactManager sharedManager] reset]; isAPNSRegistered = NO; // Clear cache [MediaManager clearCache]; @@ -185,6 +184,8 @@ [self.masterTabBarController showLoginScreen]; // Reset App settings [[AppSettings sharedSettings] reset]; + // reset the contact manager + [[ContactManager sharedManager] reset]; // By default the "Home" tab is focussed [self.masterTabBarController setSelectedIndex:TABBAR_HOME_INDEX]; } diff --git a/matrixConsole/matrixConsole/Base.lproj/Main.storyboard b/matrixConsole/matrixConsole/Base.lproj/Main.storyboard index 83aff7b2c..5641ad062 100644 --- a/matrixConsole/matrixConsole/Base.lproj/Main.storyboard +++ b/matrixConsole/matrixConsole/Base.lproj/Main.storyboard @@ -532,7 +532,101 @@ - + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -636,7 +730,7 @@ - + @@ -1112,7 +1206,7 @@ - + @@ -1154,7 +1248,7 @@ - + @@ -1224,7 +1318,7 @@ - + @@ -1233,8 +1327,8 @@ - + + + + + + + + @@ -1526,15 +1656,6 @@ - - - - - - - - - @@ -1553,6 +1674,7 @@ + diff --git a/matrixConsole/matrixConsole/Model/MXC3PID.h b/matrixConsole/matrixConsole/Model/MXC3PID.h new file mode 100644 index 000000000..fb125ce70 --- /dev/null +++ b/matrixConsole/matrixConsole/Model/MXC3PID.h @@ -0,0 +1,69 @@ +/* + 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 +#import "MX3PID.h" + +typedef enum : NSUInteger { + MXC3PIDAuthStateUnknown, + MXC3PIDAuthStateTokenRequested, + MXC3PIDAuthStateTokenReceived, + MXC3PIDAuthStateTokenSubmitted, + MXC3PIDAuthStateAuthenticated +} MXC3PIDAuthState; + + +@interface MXC3PID : MX3PID + +@property (nonatomic, readonly) MXC3PIDAuthState validationState; + +/** + Start the validation process + The identity server will send a validation token to the user's address. + This validation token must be then send back to the identity server with [MXC3PID validateWithToken] + in order to complete the 3PID authentication. + + @param success A block object called when the operation succeeds. + @param failure A block object called when the operation fails. + */ +- (void)requestValidationToken:(void (^)())success + failure:(void (^)(NSError *error))failure; + +/** + Complete the 3rd party id validation by sending the validation token the user received. + + @param validationToken the validation token the user received. + @param success A block object called when the operation succeeds. It indicates if the + validation has succeeded. + @param failure A block object called when the operation fails. + */ +- (void)validateWithToken:(NSString*)validationToken + success:(void (^)(BOOL success))success + failure:(void (^)(NSError *error))failure; + +/** + Link an authenticated 3rd party id to a Matrix user id. + + @param userId the Matrix user id to link the 3PID with. + @param success A block object called when the operation succeeds. It provides the raw + server response. + @param failure A block object called when the operation fails. + */ +- (void)bindWithUserId:(NSString*)userId + success:(void (^)())success + failure:(void (^)(NSError *error))failure; + +@end \ No newline at end of file diff --git a/matrixConsole/matrixConsole/Model/MXC3PID.m b/matrixConsole/matrixConsole/Model/MXC3PID.m new file mode 100644 index 000000000..433078d14 --- /dev/null +++ b/matrixConsole/matrixConsole/Model/MXC3PID.m @@ -0,0 +1,158 @@ +/* + 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 "MXC3PID.h" +#import "MatrixSDKHandler.h" +#import "MXTools.h" + +@interface MXC3PID () +@property (nonatomic) NSString *clientSecret; +@property (nonatomic) NSUInteger sendAttempt; +@property (nonatomic) NSString *sid; +@end + +@implementation MXC3PID + +- (void)resetValidationParameters { + _validationState = MXC3PIDAuthStateUnknown; + self.clientSecret = nil; + self.sendAttempt = 1; + self.sid = nil; + // Removed potential linked userId + self.userId = nil; +} + +- (void)requestValidationToken:(void (^)())success + failure:(void (^)(NSError *error))failure { + // Sanity Check + if (_validationState != MXC3PIDAuthStateTokenRequested) { + MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; + // Reset if the current state is different than "Unknown" + if (_validationState != MXC3PIDAuthStateUnknown) { + [self resetValidationParameters]; + } + + if ([self.medium isEqualToString:kMX3PIDMediumEmail]) { + self.clientSecret = [MXTools generateSecret]; + _validationState = MXC3PIDAuthStateTokenRequested; + [mxHandler.mxRestClient requestEmailValidation:self.address clientSecret:self.clientSecret sendAttempt:self.sendAttempt success:^(NSString *sid) { + _validationState = MXC3PIDAuthStateTokenReceived; + self.sid = sid; + if (success) { + success(); + } + } failure:^(NSError *error) { + // Return in unknown state + _validationState = MXC3PIDAuthStateUnknown; + // Increment attempt counter + self.sendAttempt++; + if (failure) { + failure (error); + } + }]; + + return; + } else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) { + // FIXME: support msisdn as soon as identity server supports it + NSLog(@"MXC3PID requestValidationToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } else { + NSLog(@"MXC3PID requestValidationToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } + } else { + NSLog(@"MXC3PID requestValidationToken: Wrong validation flow for this 3PID: %@ (%@), state: %d", self.address, self.medium, _validationState); + } + +} + +- (void)validateWithToken:(NSString*)validationToken + success:(void (^)(BOOL success))success + failure:(void (^)(NSError *error))failure { + // Sanity check + if (_validationState == MXC3PIDAuthStateTokenReceived) { + MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; + + if ([self.medium isEqualToString:kMX3PIDMediumEmail]) { + _validationState = MXC3PIDAuthStateTokenSubmitted; + [mxHandler.mxRestClient validateEmail:self.sid validationToken:validationToken clientSecret:self.clientSecret success:^(BOOL successFlag) { + if (successFlag) { + // Validation is complete + _validationState = MXC3PIDAuthStateAuthenticated; + } else { + // Return in previous step + _validationState = MXC3PIDAuthStateTokenReceived; + } + if (success) { + success(successFlag); + } + } failure:^(NSError *error) { + // Return in previous step + _validationState = MXC3PIDAuthStateTokenReceived; + if (failure) { + failure (error); + } + }]; + + return; + } else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) { + // FIXME: support msisdn as soon as identity server supports it + NSLog(@"MXC3PID validateWithToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } else { + NSLog(@"MXC3PID validateWithToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } + } else { + NSLog(@"MXC3PID validateWithToken: Wrong validation flow for this 3PID: %@ (%@), state: %d", self.address, self.medium, _validationState); + } + + // Here the validation process failed + if (failure) { + failure (nil); + } +} + +- (void)bindWithUserId:(NSString*)userId + success:(void (^)())success + failure:(void (^)(NSError *error))failure { + // Sanity check + if (_validationState == MXC3PIDAuthStateAuthenticated) { + MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; + + if ([self.medium isEqualToString:kMX3PIDMediumEmail]) { + [mxHandler.mxRestClient bind3PID:userId sid:self.sid clientSecret:self.clientSecret success:^(NSDictionary *JSONResponse) { + // Update linked userId in 3PID + self.userId = userId; + if (success) { + success(); + } + } failure:failure]; + + return; + } else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) { + // FIXME: support msisdn as soon as identity server supports it + NSLog(@"MXC3PID bindWithUserId: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } else { + NSLog(@"MXC3PID bindWithUserId: is not supported for this 3PID: %@ (%@)", self.address, self.medium); + } + } else { + NSLog(@"MXC3PID bindWithUserId: Wrong validation flow for this 3PID: %@ (%@), state: %d", self.address, self.medium, _validationState); + } + + // Here the validation process failed + if (failure) { + failure (nil); + } +} + +@end diff --git a/matrixConsole/matrixConsole/Model/MXCContact.h b/matrixConsole/matrixConsole/Model/MXCContact.h index 2284b0b30..c4251db5e 100644 --- a/matrixConsole/matrixConsole/Model/MXCContact.h +++ b/matrixConsole/matrixConsole/Model/MXCContact.h @@ -54,4 +54,7 @@ extern NSString *const kMXCContactThumbnailUpdateNotification; // self.thumbnail triggered a request with a 256 X 256 pixels - (UIImage*)thumbnailWithPreferedSize:(CGSize)size; +// check if the patterns can match with this contact +- (BOOL) matchedWithPatterns:(NSArray*)patterns; + @end \ No newline at end of file diff --git a/matrixConsole/matrixConsole/Model/MXCContact.m b/matrixConsole/matrixConsole/Model/MXCContact.m index 34c8bf45b..07981cab3 100644 --- a/matrixConsole/matrixConsole/Model/MXCContact.m +++ b/matrixConsole/matrixConsole/Model/MXCContact.m @@ -196,6 +196,48 @@ NSString *const kMXCContactThumbnailUpdateNotification = @"kMXCContactThumbnailU return self; } +// check if the patterns can match with this contact +- (BOOL) matchedWithPatterns:(NSArray*)patterns { + + BOOL matched = NO; + + if (patterns.count > 0) { + + matched = YES; + + // test first display name + for(NSString* pattern in patterns) { + if ([_displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound) { + matched = NO; + break; + } + } + + if (!matched && _phoneNumbers.count > 0) { + for(MXCPhoneNumber* phonenumber in _phoneNumbers) { + if ([phonenumber matchedWithPatterns:patterns]) { + matched = YES; + break; + } + } + } + + if (!matched && _emailAddresses.count > 0) { + for(MXCEmail* email in _emailAddresses) { + if ([email matchedWithPatterns:patterns]) { + matched = YES; + break; + } + } + } + } else { + // if there is no pattern to search, it should always matched + matched = YES; + } + + return matched; +} + - (BOOL) isMatrixContact { return (nil != dummyField); } diff --git a/matrixConsole/matrixConsole/Model/MXCEmail.h b/matrixConsole/matrixConsole/Model/MXCEmail.h index f02e4948f..f026ca510 100644 --- a/matrixConsole/matrixConsole/Model/MXCEmail.h +++ b/matrixConsole/matrixConsole/Model/MXCEmail.h @@ -23,7 +23,8 @@ @property (nonatomic, readonly) NSString *type; @property (nonatomic, readonly) NSString *emailAddress; - - (id)initWithEmailAddress:(NSString*)anEmailAddress type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID; +- (BOOL)matchedWithPatterns:(NSArray*)patterns; + @end diff --git a/matrixConsole/matrixConsole/Model/MXCEmail.m b/matrixConsole/matrixConsole/Model/MXCEmail.m index 98a008884..3ab90158f 100644 --- a/matrixConsole/matrixConsole/Model/MXCEmail.m +++ b/matrixConsole/matrixConsole/Model/MXCEmail.m @@ -54,4 +54,20 @@ return self; } +- (BOOL)matchedWithPatterns:(NSArray*)patterns { + // no number -> cannot match + if (_emailAddress.length == 0) { + return NO; + } + if (patterns.count > 0) { + for(NSString *pattern in patterns) { + if ([_emailAddress rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound) { + return NO; + } + } + } + + return YES; +} + @end diff --git a/matrixConsole/matrixConsole/Model/MXCPhoneNumber.h b/matrixConsole/matrixConsole/Model/MXCPhoneNumber.h index 24a3d0980..b3bf0d6fc 100644 --- a/matrixConsole/matrixConsole/Model/MXCPhoneNumber.h +++ b/matrixConsole/matrixConsole/Model/MXCPhoneNumber.h @@ -25,4 +25,6 @@ - (id)initWithTextNumber:(NSString*)textNumber type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID; +- (BOOL)matchedWithPatterns:(NSArray*)patterns; + @end \ No newline at end of file diff --git a/matrixConsole/matrixConsole/Model/MXCPhoneNumber.m b/matrixConsole/matrixConsole/Model/MXCPhoneNumber.m index 3735f0226..d0bb00138 100644 --- a/matrixConsole/matrixConsole/Model/MXCPhoneNumber.m +++ b/matrixConsole/matrixConsole/Model/MXCPhoneNumber.m @@ -16,6 +16,12 @@ #import "MXCPhoneNumber.h" +@interface MXCPhoneNumber() { + // for search purpose + NSString* cleanedPhonenumber; +} +@end + @implementation MXCPhoneNumber - (id)initWithTextNumber:(NSString*)aTextNumber type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID { @@ -24,9 +30,57 @@ if (self) { _type = aType; _textNumber = aTextNumber; + cleanedPhonenumber = nil; } return self; } +// remove the unuseful characters in a phonenumber ++ (NSString*) cleanPhonenumber:(NSString*)phoneNumber { + + // sanity check + if (nil == phoneNumber) + { + return nil; + } + + // empty string + if (0 == [phoneNumber length]) + { + return @""; + } + + static NSCharacterSet *invertedPhoneCharSet = nil; + + if (!invertedPhoneCharSet) + { + invertedPhoneCharSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789+*#,"] invertedSet]; + } + + return [[phoneNumber componentsSeparatedByCharactersInSet:invertedPhoneCharSet] componentsJoinedByString:@""]; +} + + +- (BOOL)matchedWithPatterns:(NSArray*)patterns { + // no number -> cannot match + if (_textNumber.length == 0) { + return NO; + } + + if (!cleanedPhonenumber) { + cleanedPhonenumber = [MXCPhoneNumber cleanPhonenumber:_textNumber]; + } + + if (patterns.count > 0) { + for(NSString *pattern in patterns) { + if (([_textNumber rangeOfString:pattern].location == NSNotFound) && ([cleanedPhonenumber rangeOfString:pattern].location == NSNotFound)) { + return NO; + } + } + } + + return YES; +} + @end \ No newline at end of file diff --git a/matrixConsole/matrixConsole/View/ContactDetailsTableCell.h b/matrixConsole/matrixConsole/View/ContactDetailsTableCell.h new file mode 100644 index 000000000..8c7daf486 --- /dev/null +++ b/matrixConsole/matrixConsole/View/ContactDetailsTableCell.h @@ -0,0 +1,25 @@ +/* + 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 + +@interface ContactDetailsTableCell : UITableViewCell + +@property (strong, nonatomic) IBOutlet UILabel *matrixUserIDLabel; +@property (strong, nonatomic) IBOutlet UIButton *startChatButton; + +@end + diff --git a/matrixConsole/matrixConsole/View/ContactDetailsTableCell.m b/matrixConsole/matrixConsole/View/ContactDetailsTableCell.m new file mode 100644 index 000000000..d4915fc34 --- /dev/null +++ b/matrixConsole/matrixConsole/View/ContactDetailsTableCell.m @@ -0,0 +1,20 @@ +/* + 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 "ContactDetailsTableCell.h" + +@implementation ContactDetailsTableCell +@end \ No newline at end of file diff --git a/matrixConsole/matrixConsole/View/ContactTableCell.h b/matrixConsole/matrixConsole/View/ContactTableCell.h index 755693cd2..f7fc099e3 100644 --- a/matrixConsole/matrixConsole/View/ContactTableCell.h +++ b/matrixConsole/matrixConsole/View/ContactTableCell.h @@ -22,6 +22,8 @@ @property (strong, nonatomic) IBOutlet MXCImageView *thumbnailView; @property (strong, nonatomic) IBOutlet UILabel *contactDisplayNameLabel; +@property (strong, nonatomic) IBOutlet UILabel *matrixDisplayNameLabel; +@property (strong, nonatomic) IBOutlet UILabel *matrixIDLabel; @property (strong, nonatomic) IBOutlet UIImageView *matrixUserIconView; // reference to the linked message diff --git a/matrixConsole/matrixConsole/View/ContactTableCell.m b/matrixConsole/matrixConsole/View/ContactTableCell.m index c811f4ec1..f31812221 100644 --- a/matrixConsole/matrixConsole/View/ContactTableCell.m +++ b/matrixConsole/matrixConsole/View/ContactTableCell.m @@ -82,7 +82,25 @@ // init the contact info [[ContactManager sharedManager] refreshContactMatrixIDs:_contact]; - self.contactDisplayNameLabel.text = _contact.displayName; + + NSArray* matrixIDs = _contact.matrixIdentifiers; + + if (matrixIDs.count == 1) { + self.contactDisplayNameLabel.hidden = YES; + + self.matrixDisplayNameLabel.hidden = NO; + self.matrixDisplayNameLabel.text = _contact.displayName; + self.matrixIDLabel.hidden = NO; + self.matrixIDLabel.text = [ _contact.matrixIdentifiers objectAtIndex:0]; + + } else { + self.contactDisplayNameLabel.hidden = NO; + self.contactDisplayNameLabel.text = _contact.displayName; + + self.matrixDisplayNameLabel.hidden = YES; + self.matrixIDLabel.hidden = YES; + } + [self refreshContactThumbnail]; [self manageMatrixIcon]; } diff --git a/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.h b/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.h new file mode 100644 index 000000000..3238bde27 --- /dev/null +++ b/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.h @@ -0,0 +1,25 @@ +/* + 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 + +#import "MXCContact.h" + +@interface ContactDetailsViewController : UITableViewController + +@property (strong, nonatomic) MXCContact* contact; +@end + diff --git a/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.m b/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.m new file mode 100644 index 000000000..af3c15d77 --- /dev/null +++ b/matrixConsole/matrixConsole/ViewController/ContactDetailsViewController.m @@ -0,0 +1,130 @@ +/* + 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 "ContactDetailsViewController.h" + +#import "ContactDetailsTableCell.h" + +#import "MatrixSDKHandler.h" + +@interface ContactDetailsViewController () { + NSArray* matrixIDs; +} + +@property (weak, nonatomic) IBOutlet UIButton *memberThumbnailButton; +@property (weak, nonatomic) IBOutlet UITextView *roomMemberMID; + +@end + +@implementation ContactDetailsViewController + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + self.memberThumbnailButton = nil; + self.roomMemberMID = nil; + matrixIDs = nil; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // remove the line separator color + self.tableView.separatorColor = [UIColor clearColor]; + self.tableView.rowHeight = 44; + self.tableView.allowsSelection = NO; +} + +- (void)viewWillAppear:(BOOL)animated { + + [super viewWillAppear:animated]; + self.roomMemberMID.text = _contact.displayName; + + // set the thumbnail info + [self.memberThumbnailButton.imageView setContentMode: UIViewContentModeScaleAspectFill]; + [self.memberThumbnailButton.imageView setClipsToBounds:YES]; + + if (_contact.thumbnail) { + self.memberThumbnailButton.imageView.image = _contact.thumbnail; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThumbnailUpdate:) name:kMXCContactThumbnailUpdateNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + matrixIDs = _contact.matrixIdentifiers; + return matrixIDs.count; +} + +- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + NSInteger row = indexPath.row; + ContactDetailsTableCell* contactDetailsTableCell = (ContactDetailsTableCell*)[aTableView dequeueReusableCellWithIdentifier:@"ContactDetailsTableCell" forIndexPath:indexPath]; + + if (row < matrixIDs.count) { + contactDetailsTableCell.matrixUserIDLabel.text = [matrixIDs objectAtIndex:row]; + } else { + // should never happen + contactDetailsTableCell.matrixUserIDLabel.text = @""; + } + + contactDetailsTableCell.startChatButton.layer.cornerRadius = 5; + contactDetailsTableCell.startChatButton.layer.borderColor = [UIColor blackColor].CGColor; + contactDetailsTableCell.startChatButton.layer.borderWidth = 2; + contactDetailsTableCell.startChatButton.clipsToBounds = YES; + [contactDetailsTableCell.startChatButton addTarget:self action:@selector(startChat:) forControlEvents:UIControlEventTouchUpInside]; + + return contactDetailsTableCell; +} + +- (void)startChat:(UIButton*)sender { + UIView* view = sender; + + // search the parentce cell + while (view && ![view isKindOfClass:[ContactDetailsTableCell class]]) { + view = view.superview; + } + + if ([view isKindOfClass:[ContactDetailsTableCell class]]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[MatrixSDKHandler sharedHandler] startPrivateOneToOneRoomWith:((ContactDetailsTableCell*)view).matrixUserIDLabel.text]; + }); + } +} + +- (void)onThumbnailUpdate:(NSNotification *)notif { + // sanity check + if ([notif.object isKindOfClass:[NSString class]]) { + NSString* matrixID = notif.object; + + if ([matrixID isEqualToString:self.contact.contactID]) { + if (_contact.thumbnail) { + self.memberThumbnailButton.imageView.image = _contact.thumbnail; + } + } + } +} + +@end diff --git a/matrixConsole/matrixConsole/ViewController/ContactsViewController.h b/matrixConsole/matrixConsole/ViewController/ContactsViewController.h index a47918301..b0e759a8c 100644 --- a/matrixConsole/matrixConsole/ViewController/ContactsViewController.h +++ b/matrixConsole/matrixConsole/ViewController/ContactsViewController.h @@ -22,7 +22,7 @@ #import "SectionedContacts.h" -@interface ContactsViewController : UIViewController { +@interface ContactsViewController : UIViewController { NSArray* collationTitles; } diff --git a/matrixConsole/matrixConsole/ViewController/ContactsViewController.m b/matrixConsole/matrixConsole/ViewController/ContactsViewController.m index ab7df921b..b1eb22018 100644 --- a/matrixConsole/matrixConsole/ViewController/ContactsViewController.m +++ b/matrixConsole/matrixConsole/ViewController/ContactsViewController.m @@ -37,6 +37,9 @@ // settings #import "AppSettings.h" +// +#import "ContactDetailsViewController.h" + NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Please, visit the website http://matrix.org to have more information."; @interface ContactsViewController () { @@ -52,7 +55,17 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl NSMutableDictionary* matrixUserByMatrixID; SectionedContacts* sectionedMatrixContacts; + // tap on thumbnail to display contact info + MXCContact* selectedContact; + + // Search + UISearchBar *contactsSearchBar; + NSMutableArray *filteredContacts; + SectionedContacts* sectionedFilteredContacts; + BOOL searchBarShouldEndEditing; + NSString* latestSearchedPattern; } + @property (strong, nonatomic) MXCAlert *startChatMenu; @property (strong, nonatomic) MXCAlert *allowContactSyncAlert; @property (weak, nonatomic) IBOutlet UITableView* tableView; @@ -74,10 +87,35 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl // event listener [[MatrixSDKHandler sharedHandler] addObserver:self forKeyPath:@"status" options:0 context:nil]; + + // add the search icon on the right + // need to add more buttons ? + UIBarButtonItem *searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)]; + self.navigationItem.rightBarButtonItems = @[searchButton]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactsRefresh:) name:kContactManagerContactsListRefreshNotification object:nil]; } +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // required to reduce the tableview height while searching + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + // Leave potential search session + if (contactsSearchBar) { + [self searchBarCancelButtonClicked:contactsSearchBar]; + } + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; +} + - (void)startActivityIndicator { [_activityIndicator.layer setCornerRadius:5]; _activityIndicator.hidden = NO; @@ -89,84 +127,191 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl _activityIndicator.hidden = YES; } +- (void)scrollToTop { + // stop any scrolling effect + [UIView setAnimationsEnabled:NO]; + // before scrolling to the tableview top + self.tableView.contentOffset = CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top); + [UIView setAnimationsEnabled:YES]; +} + +// should be called when resetting the application +// the contact manager warn there is a contacts list update +// but the Matrix SDK handler has no more userID -> so assume there is a reset +- (void)reset { + // Leave potential search session + if (contactsSearchBar) { + [self searchBarCancelButtonClicked:contactsSearchBar]; + } + + localContacts = nil; + sectionedLocalContacts = nil; + + matrixUserByMatrixID = [[NSMutableDictionary alloc] init];; + sectionedMatrixContacts = nil; + + [self.contactsControls setSelectedSegmentIndex:0]; + [self.tableView reloadData]; +} + +#pragma mark - Keyboard handling + +- (void)onKeyboardWillShow:(NSNotification *)notif { + // get the keyboard size + NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey]; + CGRect endRect = rectVal.CGRectValue; + + // IOS 8 triggers some unexpected keyboard events + if ((endRect.size.height == 0) || (endRect.size.width == 0)) { + return; + } + + CGFloat keyboardHeight = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height; + + // the tableview bottom inset must also be updated + UIEdgeInsets insets = self.tableView.contentInset; + insets.bottom = keyboardHeight; + + // get the animation info + NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey]; + UIViewAnimationCurve animationCurve = curveValue.intValue; + + // the duration is ignored but it is better to define it + double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + [UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{ + // reduce the tableview height + self.tableView.contentInset = insets; + [self.view layoutIfNeeded]; + + } completion:^(BOOL finished) { + }]; +} + +- (void)onKeyboardWillHide:(NSNotification *)notif { + // get the keyboard size + NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey]; + CGRect endRect = rectVal.CGRectValue; + + rectVal = notif.userInfo[UIKeyboardFrameBeginUserInfoKey]; + CGRect beginRect = rectVal.CGRectValue; + + UIEdgeInsets insets = self.tableView.contentInset; + insets.bottom = 0; + + // do not animate if the both rect are the same + // but ensure that the fields are properly resetted + // e.g. when the user swipes to hide the keyboard + // this method is called with invalid rects + // animationDuration is ignored because of the animation curve + // use it to be sure that it will be broken with any new IOS update + if (CGRectEqualToRect(endRect, beginRect)) { + + self.tableView.contentInset = insets; + [self.view layoutIfNeeded]; + + } else { + // get the animation info + NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey]; + UIViewAnimationCurve animationCurve = curveValue.intValue; + + // the duration is ignored but it is better to define it + double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + // animate the keyboard closing + [UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{ + self.tableView.contentInset = insets; + [self.view layoutIfNeeded]; + + } completion:^(BOOL finished) { + }]; + } +} #pragma mark - UITableView delegate -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - if (displayMatrixUsers) { - // check if the user is already known - MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; - - if ((mxHandler.status != MatrixSDKHandlerStatusServerSyncDone) && (mxHandler.status != MatrixSDKHandlerStatusStoreDataReady)) { - [self startActivityIndicator]; - return 0; - } else { - [self stopActivityIndicator]; +- (void)updateSectionedLocalContacts { + [self stopActivityIndicator]; + + ContactManager* sharedManager = [ContactManager sharedManager]; + + if (!localContacts) { + localContacts = sharedManager.contacts; + } + + if (!sectionedLocalContacts) { + sectionedLocalContacts = [sharedManager getSectionedContacts:sharedManager.contacts]; + } +} - //NSArray* users = [mxHandler.mxSession users]; - NSArray* usersIDs = [mxHandler oneToOneRoomMemberMatrixIDs]; - // return a MatrixIDs list of 1:1 room members - - NSMutableArray* knownUserIDs = [[matrixUserByMatrixID allKeys] mutableCopy]; - - // list the contacts IDs - // avoid delete and create the same ones - // it could save thumbnail downloads - for(NSString* userID in usersIDs) { - // - MXUser* user = [mxHandler.mxSession userWithUserId:userID]; - - // sanity check - if (user) { - // managed UserID - [knownUserIDs removeObject:userID]; - - MXCContact* contact = [matrixUserByMatrixID objectForKey:userID]; - - // already defined - if (contact) { - contact.displayName = (user.displayname.length > 0) ? user.displayname : user.userId; - } else { - contact = [[MXCContact alloc] initWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) matrixID:user.userId]; - [matrixUserByMatrixID setValue:contact forKey:userID]; - } - } - } - - // some userIDs don't exist anymore - for (NSString* userID in knownUserIDs) { - [matrixUserByMatrixID removeObjectForKey:userID]; - } - - sectionedMatrixContacts = [[ContactManager sharedManager] getSectionedContacts:[matrixUserByMatrixID allValues]]; - - return sectionedMatrixContacts.sectionedContacts.count; - } - +- (void)updateSectionedMatrixContacts { + // check if the user is already known + MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; + + if ((mxHandler.status != MatrixSDKHandlerStatusServerSyncDone) && (mxHandler.status != MatrixSDKHandlerStatusStoreDataReady)) { + [self startActivityIndicator]; + sectionedMatrixContacts = nil; } else { [self stopActivityIndicator]; - ContactManager* sharedManager = [ContactManager sharedManager]; + //NSArray* users = [mxHandler.mxSession users]; + NSArray* usersIDs = [mxHandler oneToOneRoomMemberMatrixIDs]; + // return a MatrixIDs list of 1:1 room members - if (!localContacts) { - localContacts = sharedManager.contacts; + NSMutableArray* knownUserIDs = [[matrixUserByMatrixID allKeys] mutableCopy]; + + // list the contacts IDs + // avoid delete and create the same ones + // it could save thumbnail downloads + for(NSString* userID in usersIDs) { + // + MXUser* user = [mxHandler.mxSession userWithUserId:userID]; + + // sanity check + if (user) { + // managed UserID + [knownUserIDs removeObject:userID]; + + MXCContact* contact = [matrixUserByMatrixID objectForKey:userID]; + + // already defined + if (contact) { + contact.displayName = (user.displayname.length > 0) ? user.displayname : user.userId; + } else { + contact = [[MXCContact alloc] initWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) matrixID:user.userId]; + [matrixUserByMatrixID setValue:contact forKey:userID]; + } + } } - if (!sectionedLocalContacts) { - sectionedLocalContacts = [sharedManager getSectionedContacts:sharedManager.contacts]; + // some userIDs don't exist anymore + for (NSString* userID in knownUserIDs) { + [matrixUserByMatrixID removeObjectForKey:userID]; } + sectionedMatrixContacts = [[ContactManager sharedManager] getSectionedContacts:[matrixUserByMatrixID allValues]]; + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + // search in progress + if (contactsSearchBar) { + return sectionedFilteredContacts.sectionedContacts.count; + } + else if (displayMatrixUsers) { + [self updateSectionedMatrixContacts]; + return sectionedMatrixContacts.sectionedContacts.count; + + } else { + [self updateSectionedLocalContacts]; return sectionedLocalContacts.sectionedContacts.count; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); - if (displayMatrixUsers) { - return [[sectionedMatrixContacts.sectionedContacts objectAtIndex:section] count]; - } else { - return [[sectionedLocalContacts.sectionedContacts objectAtIndex:section] count]; - } + return [[sectionedContacts.sectionedContacts objectAtIndex:section] count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -174,7 +319,7 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl } - (NSString *)tableView:(UITableView *)aTableView titleForHeaderInSection:(NSInteger)section { - SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; + SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); if (sectionedContacts.sectionTitles.count <= section) { return nil; @@ -185,14 +330,19 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl } - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)aTableView { - [self.tableView setSectionIndexColor:[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor]; - [self.tableView setSectionIndexBackgroundColor:[UIColor clearColor]]; - - return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; + // do not display the collation during a search + if (contactsSearchBar) { + return nil; + } else { + [self.tableView setSectionIndexColor:[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor]; + [self.tableView setSectionIndexBackgroundColor:[UIColor clearColor]]; + + return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; + } } - (NSInteger)tableView:(UITableView *)aTableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { - SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; + SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); NSUInteger section = [sectionedContacts.sectionTitles indexOfObject:title]; // undefined title -> jump to the first valid non empty section @@ -216,7 +366,7 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { ContactTableCell* cell = [tableView dequeueReusableCellWithIdentifier:@"ContactCell" forIndexPath:indexPath]; - SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; + SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); MXCContact* contact = nil; @@ -228,6 +378,29 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl } } + // tap on matrix user thumbnail -> open a detailled sheet + UITapGestureRecognizer* tapGesture = nil; + + // check if it is already defined + // gesture in storyboard does not seem to work properly + // it always triggers a tap event on the first cell + for (UIGestureRecognizer* gesture in cell.thumbnailView.gestureRecognizers) { + + if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) { + tapGesture = (UITapGestureRecognizer*)gesture; + break; + } + } + + // add it if it is not yet defined + if (!tapGesture) { + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onContactThumbnailTap:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [tap setDelegate:self]; + [cell.thumbnailView addGestureRecognizer:tap]; + } + cell.contact = contact; return cell; @@ -236,7 +409,7 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; - SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; + SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); MXCContact* contact = nil; if (indexPath.section < sectionedContacts.sectionedContacts.count) { @@ -261,7 +434,7 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl if (matrixIDs.count == 1) { NSString* matrixID = [matrixIDs objectAtIndex:0]; - self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Start chat with %@", matrixID] message:nil style:MXCAlertStyleAlert]; + self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with %@", matrixID] message:nil style:MXCAlertStyleAlert]; [self.startChatMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) { weakSelf.startChatMenu = nil; @@ -270,16 +443,16 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl [self.startChatMenu addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) { weakSelf.startChatMenu = nil; - [mxHandler createPrivateOneToOneRoomWith:matrixID]; + [mxHandler startPrivateOneToOneRoomWith:matrixID]; }]; } else { - self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Start chat with "] message:nil style:MXCAlertStyleActionSheet]; + self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with "] message:nil style:MXCAlertStyleActionSheet]; for(NSString* matrixID in matrixIDs) { [self.startChatMenu addActionWithTitle:matrixID style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) { weakSelf.startChatMenu = nil; - [mxHandler createPrivateOneToOneRoomWith:matrixID]; + [mxHandler startPrivateOneToOneRoomWith:matrixID]; }]; } @@ -353,7 +526,13 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl if ([@"status" isEqualToString:keyPath]) { dispatch_async(dispatch_get_main_queue(), ^{ if (displayMatrixUsers) { - [self.tableView reloadData]; + if (contactsSearchBar) { + [self updateSectionedMatrixContacts]; + latestSearchedPattern = nil; + [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; + } else { + [self.tableView reloadData]; + } } }); } @@ -364,14 +543,43 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl - (void)onContactsRefresh:(NSNotification *)notif { localContacts = nil; sectionedLocalContacts = nil; - [self.tableView reloadData]; + + // there is an user id + if ([[MatrixSDKHandler sharedHandler] userId]) { + [self updateSectionedLocalContacts]; + // + if (!displayMatrixUsers) { + if (contactsSearchBar) { + latestSearchedPattern = nil; + [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; + } else { + [self.tableView reloadData]; + } + } + } else { + // the client could have been logged out + [self reset]; + } } - (IBAction)onSegmentValueChange:(id)sender { if (sender == self.contactsControls) { + displayMatrixUsers = (0 == self.contactsControls.selectedSegmentIndex); - [self.tableView reloadData]; + if (contactsSearchBar) { + if (displayMatrixUsers) { + [self updateSectionedMatrixContacts]; + } else { + [self updateSectionedLocalContacts]; + } + + latestSearchedPattern = nil; + [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; + } else { + [self.tableView reloadData]; + } + if (!displayMatrixUsers) { AppSettings* appSettings = [AppSettings sharedSettings]; @@ -397,10 +605,160 @@ NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Pl } } } + +- (IBAction)onContactThumbnailTap:(id)sender { + + if ([sender isKindOfClass:[UITapGestureRecognizer class]]) { + UIView* tappedView = ((UITapGestureRecognizer*)sender).view; + + // search the parentce cell + while (tappedView && ![tappedView isKindOfClass:[ContactTableCell class]]) { + tappedView = tappedView.superview; + } + + // find it ? + if ([tappedView isKindOfClass:[ContactTableCell class]]) { + MXCContact* contact = ((ContactTableCell*)tappedView).contact; + + // open detailled sheet if there + if (contact.matrixIdentifiers.count > 0) { + selectedContact = ((ContactTableCell*)tappedView).contact; + [self performSegueWithIdentifier:@"showContactDetails" sender:self]; + } + } + + } +} + +#pragma mark - Segues + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + if ([segue.identifier isEqualToString:@"showContactDetails"]) { + ContactDetailsViewController *contactDetailsViewController = segue.destinationViewController; + contactDetailsViewController.contact = selectedContact; + selectedContact = nil; + } +} + #pragma mark MFMessageComposeViewControllerDelegate - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [self dismissViewControllerAnimated:YES completion:nil]; } +#pragma mark Search management + +- (void)search:(id)sender { + if (!contactsSearchBar) { + SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; + + // Check whether there are data in which search + if (sectionedContacts.sectionedContacts.count > 0) { + // Create search bar + contactsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)]; + contactsSearchBar.showsCancelButton = YES; + contactsSearchBar.returnKeyType = UIReturnKeyDone; + contactsSearchBar.delegate = self; + contactsSearchBar.tintColor = [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor; + searchBarShouldEndEditing = NO; + + // init the table content + latestSearchedPattern = @""; + filteredContacts = [(displayMatrixUsers ? [matrixUserByMatrixID allValues] : localContacts) mutableCopy]; + sectionedFilteredContacts = [[ContactManager sharedManager] getSectionedContacts:filteredContacts]; + + self.tableView.tableHeaderView = contactsSearchBar; + [self.tableView reloadData]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [contactsSearchBar becomeFirstResponder]; + }); + } + } else { + [self searchBarCancelButtonClicked:contactsSearchBar]; + } +} + +#pragma mark - UISearchBarDelegate + +- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar { + searchBarShouldEndEditing = NO; + return YES; +} + +- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar { + return searchBarShouldEndEditing; +} + +- (NSArray*)patternsFromText:(NSString*)text { + NSArray* items = [text componentsSeparatedByString:@" "]; + + if (items.count <= 1) { + return items; + } + + NSMutableArray* patterns = [[NSMutableArray alloc] init]; + + for (NSString* item in items) { + if (item.length > 0) { + [patterns addObject:item]; + } + } + + return patterns; +} + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { + if ((contactsSearchBar == searchBar) && (![latestSearchedPattern isEqualToString:searchText])) { + latestSearchedPattern = searchText; + + // contacts + NSArray* contacts = displayMatrixUsers ? [matrixUserByMatrixID allValues] : localContacts; + + // Update filtered list + if (searchText.length && contacts.count) { + + filteredContacts = [[NSMutableArray alloc] init]; + + NSArray* patterns = [self patternsFromText:searchText]; + for(MXCContact* contact in contacts) { + if ([contact matchedWithPatterns:patterns]) { + [filteredContacts addObject:contact]; + } + } + } else { + filteredContacts = [contacts mutableCopy]; + } + + sectionedFilteredContacts = [[ContactManager sharedManager] getSectionedContacts:filteredContacts]; + + // Refresh display + [self.tableView reloadData]; + [self scrollToTop]; + } +} + +- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { + if (contactsSearchBar == searchBar) { + // "Done" key has been pressed + searchBarShouldEndEditing = YES; + [contactsSearchBar resignFirstResponder]; + } +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { + if (contactsSearchBar == searchBar) { + // Leave search + searchBarShouldEndEditing = YES; + [contactsSearchBar resignFirstResponder]; + contactsSearchBar = nil; + filteredContacts = nil; + sectionedFilteredContacts = nil; + latestSearchedPattern = nil; + self.tableView.tableHeaderView = nil; + [self.tableView reloadData]; + [self scrollToTop]; + } +} + @end diff --git a/matrixConsole/matrixConsole/ViewController/MemberViewController.m b/matrixConsole/matrixConsole/ViewController/MemberViewController.m index 0a28ca29d..0b86bedfd 100644 --- a/matrixConsole/matrixConsole/ViewController/MemberViewController.m +++ b/matrixConsole/matrixConsole/ViewController/MemberViewController.m @@ -301,9 +301,9 @@ // offer to start a new chat only if the room is not a 1:1 room with this user // it does not make sense : it would open the same room - NSString* roomId = [mxHandler getRoomStartedWithMember:_mxRoomMember]; + NSString* roomId = [mxHandler privateRoomIdWith:_mxRoomMember.userId]; if (![roomId isEqualToString:mxRoom.state.roomId]) { - [buttonsTitles addObject:@"Start chat"]; + [buttonsTitles addObject:@"Chat"]; } } @@ -497,20 +497,9 @@ [[AppDelegate theDelegate] showErrorAsAlert:error]; }]; - } else if ([text isEqualToString:@"Start chat"]) { + } else if ([text isEqualToString:@"Chat"]) { [self addPendingActionMask]; - - MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; - NSString* roomId = [mxHandler getRoomStartedWithMember:_mxRoomMember]; - - // if the room has already been started - if (roomId) { - // open it - [[AppDelegate theDelegate].masterTabBarController showRoom:roomId]; - } - else { - [mxHandler createPrivateOneToOneRoomWith:_mxRoomMember.userId]; - } + [[MatrixSDKHandler sharedHandler] startPrivateOneToOneRoomWith:_mxRoomMember.userId]; } } } diff --git a/matrixConsole/matrixConsole/ViewController/RoomViewController.m b/matrixConsole/matrixConsole/ViewController/RoomViewController.m index b5e15949d..e5107c9ae 100644 --- a/matrixConsole/matrixConsole/ViewController/RoomViewController.m +++ b/matrixConsole/matrixConsole/ViewController/RoomViewController.m @@ -110,6 +110,9 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; // Local echo NSMutableArray *pendingOutgoingEvents; NSMutableArray *tmpCachedAttachments; + + // the user taps on a member thumbnail + MXRoomMember *selectedRoomMember; } @property (weak, nonatomic) IBOutlet UINavigationItem *roomNavItem; @@ -325,6 +328,7 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; membersListener = nil; } + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; } @@ -401,6 +405,36 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; [self.roomTitleView refreshDisplay]; } +- (void)addPictureViewTapGesture:(RoomMessageTableCell*)cell { + if (!cell.pictureView.hidden) { + UITapGestureRecognizer* tapGesture = nil; + + // check if it is already defined + // gesture in storyboard does not seem to work properly + // it always triggers a tap event on the first cell + for (UIGestureRecognizer* gesture in cell.pictureView.gestureRecognizers) { + + if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) { + tapGesture = (UITapGestureRecognizer*)gesture; + break; + } + } + + // add it if it is not yet defined + if (!tapGesture) { + tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onContactThumbnailTap:)]; + [tapGesture setNumberOfTouchesRequired:1]; + [tapGesture setNumberOfTapsRequired:1]; + [tapGesture setDelegate:self]; + [cell.pictureView addGestureRecognizer:tapGesture]; + cell.pictureView.userInteractionEnabled = YES; + + // ensure that nothing will hide this view + [cell.pictureView.superview bringSubviewToFront:cell.pictureView]; + } + } +} + #pragma mark - room ID - (void)setRoomId:(NSString *)roomId { @@ -466,6 +500,32 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; } } } +- (IBAction)onContactThumbnailTap:(UITapGestureRecognizer*)sender { + UIView* view = sender.view; + + while (view && ![view isKindOfClass:[RoomMessageTableCell class]]) { + view = view.superview; + } + + if ([view isKindOfClass:[RoomMessageTableCell class]]) { + NSIndexPath *indexPath = [self.messagesTableView indexPathForCell:(RoomMessageTableCell*)view]; + RoomMessage* message = nil; + + @synchronized(self) { + if (indexPath.row < messages.count) { + message = [messages objectAtIndex:indexPath.row]; + } + } + + if (message) { + selectedRoomMember = [self.mxRoom.state memberWithUserId:message.senderId]; + + if (selectedRoomMember) { + [self performSegueWithIdentifier:@"showMemberSheet" sender:self]; + } + } + } +} #pragma mark - Internal methods @@ -1689,6 +1749,8 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; cell.pictureView.backgroundColor = [UIColor redColor]; } + [self addPictureViewTapGesture:cell]; + // Adjust top constraint constant for dateTime labels container, and hide it by default if (message.messageType == RoomMessageTypeText) { cell.dateTimeLabelContainerTopConstraint.constant = cell.msgTextViewTopConstraint.constant; @@ -3063,10 +3125,15 @@ NSString *const kCmdResetUserPowerLevel = @"/deop"; - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"showMemberSheet"]) { - NSIndexPath *indexPath = [self.membersTableView indexPathForSelectedRow]; - MemberViewController* controller = [segue destinationViewController]; - controller.mxRoomMember = [members objectAtIndex:indexPath.row]; + + if (selectedRoomMember) { + controller.mxRoomMember = selectedRoomMember; + selectedRoomMember = nil; + } else { + NSIndexPath *indexPath = [self.membersTableView indexPathForSelectedRow]; + controller.mxRoomMember = [members objectAtIndex:indexPath.row]; + } controller.mxRoom = self.mxRoom; } } diff --git a/matrixConsole/matrixConsole/ViewController/SettingsViewController.m b/matrixConsole/matrixConsole/ViewController/SettingsViewController.m index dd307ae13..9d52ccbf7 100644 --- a/matrixConsole/matrixConsole/ViewController/SettingsViewController.m +++ b/matrixConsole/matrixConsole/ViewController/SettingsViewController.m @@ -21,6 +21,7 @@ #import "APNSHandler.h" #import "MatrixSDKHandler.h" #import "MediaManager.h" +#import "MXC3PID.h" #import "SettingsTableViewCell.h" @@ -63,7 +64,8 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl // Linked emails NSMutableArray *linkedEmails; - SettingsCellWithTextFieldAndButton* linkedEmailCell; + MXC3PID *submittedEmail; + SettingsCellWithTextFieldAndButton* submittedEmailCell; SettingsCellWithLabelTextFieldAndButton* emailTokenCell; // Dynamic rows in the Linked emails section int submittedEmailRowIndex; @@ -87,6 +89,12 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl UISwitch *sortMembersSwitch; UISwitch *displayLeftMembersSwitch; SettingsCellWithLabelAndSlider* maxCacheSizeCell; + + // Configuration + SettingsCellWithTextView *configurationCell; + + // Commands + SettingsCellWithTextView *commandsCell; } @property (strong, nonatomic) IBOutlet UITableView *tableView; @property (weak, nonatomic) IBOutlet UIView *tableHeader; @@ -141,22 +149,9 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl - (void)dealloc { [self reset]; - alertsArray = nil; - logoutBtn = nil; - apnsNotificationsSwitch = nil; - inAppNotificationsSwitch = nil; - allEventsSwitch = nil; - unsupportedMsgSwitch = nil; - sortMembersSwitch = nil; - displayLeftMembersSwitch = nil; - contactsSyncSwitch = nil; - inAppNotificationsRulesCell = nil; - linkedEmailCell = nil; - emailTokenCell = nil; - maxCacheSizeCell = nil; [[MatrixSDKHandler sharedHandler] removeObserver:self forKeyPath:@"status"]; } @@ -250,6 +245,24 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl _saveUserInfoButton.enabled = NO; linkedEmails = nil; + submittedEmail = nil; + submittedEmailCell = nil; + emailTokenCell = nil; + + contactsSyncSwitch = nil; + + apnsNotificationsSwitch = nil; + inAppNotificationsSwitch = nil; + inAppNotificationsRulesCell = nil; + + allEventsSwitch = nil; + unsupportedMsgSwitch = nil; + sortMembersSwitch = nil; + displayLeftMembersSwitch = nil; + maxCacheSizeCell = nil; + + configurationCell = nil; + commandsCell = nil; } - (void)startActivityIndicator { @@ -615,13 +628,65 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl [[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker]; } else if (sender == logoutBtn) { [[AppDelegate theDelegate] logout]; - } else if (sender == linkedEmailCell.settingButton) { - // FIXME - NSLog(@"link email is not supported yet (%@)", linkedEmailCell.settingTextField.text); - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"link is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; - [alert show]; - linkedEmailCell.settingTextField.text = nil; - linkedEmailCell.settingButton.enabled = NO; + } else if (sender == submittedEmailCell.settingButton) { + if (!submittedEmail || ![submittedEmail.address isEqualToString:submittedEmailCell.settingTextField.text]) { + submittedEmail = [[MXC3PID alloc] initWithMedium:kMX3PIDMediumEmail andAddress:submittedEmailCell.settingTextField.text]; + } + + submittedEmailCell.settingButton.enabled = NO; + [submittedEmail requestValidationToken:^{ + // Reset email field + submittedEmailCell.settingTextField.text = nil; + [self.tableView reloadData]; + } failure:^(NSError *error) { + NSLog(@"Request email token failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + submittedEmailCell.settingButton.enabled = YES; + }]; + } else if (sender == emailTokenCell.settingButton) { + emailTokenCell.settingButton.enabled = NO; + [submittedEmail validateWithToken:emailTokenCell.settingTextField.text success:^(BOOL success) { + if (success) { + // The email has been "Authenticated" + // Link the email with user's account + MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler]; + [submittedEmail bindWithUserId:mxHandler.userId success:^{ + // Add new linked email + if (!linkedEmails) { + linkedEmails = [NSMutableArray array]; + } + [linkedEmails addObject:submittedEmail.address]; + + // Release pending email and refresh table to remove related cell + submittedEmail = nil; + [self.tableView reloadData]; + } failure:^(NSError *error) { + NSLog(@"Link email failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + // Release the pending email (even if it is Authenticated) + submittedEmail = nil; + [self.tableView reloadData]; + }]; + } else { + NSLog(@"Failed to link email"); + MXCAlert *alert = [[MXCAlert alloc] initWithTitle:nil message:@"Failed to link email" style:MXCAlertStyleAlert]; + [alertsArray addObject:alert]; + alert.cancelButtonIndex = [alert addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) { + [alertsArray removeObject:alert]; + }]; + [alert showInViewController:self]; + // Reset wrong token + emailTokenCell.settingTextField.text = nil; + } + } failure:^(NSError *error) { + NSLog(@"Submitted email token failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + emailTokenCell.settingButton.enabled = YES; + }]; } else if (sender == apnsNotificationsSwitch) { [APNSHandler sharedHandler].isActive = apnsNotificationsSwitch.on; } else if (sender == inAppNotificationsSwitch) { @@ -672,8 +737,10 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl } else if (inAppNotificationsRulesCell && [inAppNotificationsRulesCell.settingTextField isFirstResponder]) { [self manageWordsList]; [inAppNotificationsRulesCell.settingTextField resignFirstResponder]; - } else if ([linkedEmailCell.settingTextField isFirstResponder]) { - [linkedEmailCell.settingTextField resignFirstResponder]; + } else if ([submittedEmailCell.settingTextField isFirstResponder]) { + [submittedEmailCell.settingTextField resignFirstResponder]; + } else if ([emailTokenCell.settingTextField isFirstResponder]) { + [emailTokenCell.settingTextField resignFirstResponder]; } } @@ -688,8 +755,10 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl - (IBAction)textFieldDidChange:(id)sender { if (sender == _userDisplayName) { [self updateSaveUserInfoButtonStatus]; - } else if (sender == linkedEmailCell.settingTextField) { - linkedEmailCell.settingButton.enabled = (linkedEmailCell.settingTextField.text.length != 0); + } else if (sender == submittedEmailCell.settingTextField) { + submittedEmailCell.settingButton.enabled = (submittedEmailCell.settingTextField.text.length != 0); + } else if (sender == emailTokenCell.settingTextField) { + emailTokenCell.settingButton.enabled = (emailTokenCell.settingTextField.text.length != 0); } } @@ -700,38 +769,45 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSInteger count = 0; if (section == SETTINGS_SECTION_LINKED_EMAILS_INDEX) { - return linkedEmails.count + 1; - } else if (section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { + submittedEmailRowIndex = emailTokenRowIndex = -1; + count = linkedEmails.count; + submittedEmailRowIndex = count++; + if (submittedEmail && submittedEmail.validationState >= MXC3PIDAuthStateTokenReceived) { + emailTokenRowIndex = count++; + } else { + emailTokenCell = nil; + } + } else if (section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { enableInAppNotifRowIndex = inAppNotifRulesRowIndex = enablePushNotifRowIndex = -1; - int count = 0; if ([APNSHandler sharedHandler].isAvailable) { enablePushNotifRowIndex = count++; } - enableInAppNotifRowIndex = count++; if ([[AppSettings sharedSettings] enableInAppNotifications]) { inAppNotifRulesRowIndex = count++; } - - return count; } else if (section == SETTINGS_SECTION_CONTACTS_INDEX) { - return 1; + count = 1; } else if (section == SETTINGS_SECTION_ROOMS_INDEX) { - return SETTINGS_SECTION_ROOMS_INDEX_COUNT; + count = SETTINGS_SECTION_ROOMS_INDEX_COUNT; } else if (section == SETTINGS_SECTION_CONFIGURATION_INDEX) { - return 1; + count = 1; } else if (section == SETTINGS_SECTION_COMMANDS_INDEX) { - return 1; + count = 1; } - return 0; + return count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == SETTINGS_SECTION_LINKED_EMAILS_INDEX) { + if (indexPath.row == emailTokenRowIndex) { + return 70; + } return 44; } else if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { if (indexPath.row == inAppNotifRulesRowIndex) { @@ -799,18 +875,48 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl UITableViewCell *cell = nil; if (indexPath.section == SETTINGS_SECTION_LINKED_EMAILS_INDEX) { - // Report the current email value (if any) - NSString *currentEmail = nil; - if (linkedEmailCell) { - currentEmail = linkedEmailCell.settingTextField.text; + if (indexPath.row < linkedEmails.count) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"LinkedEmailCell"]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"LinkedEmailCell"]; + } + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.textLabel.text = [linkedEmails objectAtIndex:indexPath.row]; + } else if (indexPath.row == submittedEmailRowIndex) { + // Report the current email value (if any) + NSString *currentEmail = nil; + if (submittedEmailCell) { + currentEmail = submittedEmailCell.settingTextField.text; + } + submittedEmailCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextFieldAndButton" forIndexPath:indexPath]; + submittedEmailCell.settingTextField.text = currentEmail; + submittedEmailCell.settingButton.enabled = (currentEmail.length != 0); + [submittedEmailCell.settingButton setTitle:@"Link Email" forState:UIControlStateNormal]; + [submittedEmailCell.settingButton setTitle:@"Link Email" forState:UIControlStateHighlighted]; + if (emailTokenRowIndex != -1) { + // Hide the separator + CGSize screenSize = [[UIScreen mainScreen] bounds].size; + CGFloat rightInset = (screenSize.width < screenSize.height) ? screenSize.height : screenSize.width; + submittedEmailCell.separatorInset = UIEdgeInsetsMake(0.f, 0.f, 0.f, rightInset); + } + cell = submittedEmailCell; + } else if (indexPath.row == emailTokenRowIndex) { + // Report the current token value (if any) + NSString *currentToken = nil; + if (emailTokenCell) { + currentToken = emailTokenCell.settingTextField.text; + } + emailTokenCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithLabelTextFieldAndButton" forIndexPath:indexPath]; + emailTokenCell.settingLabel.text = [NSString stringWithFormat:@"Enter validation token for %@:", submittedEmail.address]; + emailTokenCell.settingTextField.text = currentToken; + emailTokenCell.settingButton.enabled = (currentToken.length != 0); + [emailTokenCell.settingButton setTitle:@"Submit code" forState:UIControlStateNormal]; + [emailTokenCell.settingButton setTitle:@"Submit code" forState:UIControlStateHighlighted]; + cell = emailTokenCell; } - linkedEmailCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextFieldAndButton" forIndexPath:indexPath]; - linkedEmailCell.settingTextField.text = currentEmail; - linkedEmailCell.settingButton.enabled = (currentEmail.length != 0); - cell = linkedEmailCell; } else if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { if (indexPath.row == inAppNotifRulesRowIndex) { - // Report the current email value (if any) + // Report the current value (if any) NSString *currentRules = nil; BOOL isFirstResponder = NO; if (inAppNotificationsRulesCell) { @@ -854,21 +960,17 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ClearCacheCell"]; } - cell.textLabel.text = [NSString stringWithFormat:@"Clear cache (%@)", [NSByteCountFormatter stringFromByteCount:mxHandler.cachesSize countStyle:NSByteCountFormatterCountStyleFile]]; cell.textLabel.textAlignment = NSTextAlignmentCenter; cell.textLabel.textColor = [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor; } else if (indexPath.row == SETTINGS_SECTION_ROOMS_SET_CACHE_SIZE_INDEX) { - cell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithLabelAndSilder" forIndexPath:indexPath]; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - maxCacheSizeCell = (SettingsCellWithLabelAndSlider*)cell; - + maxCacheSizeCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithLabelAndSilder" forIndexPath:indexPath]; maxCacheSizeCell.settingSlider.minimumValue = 0; maxCacheSizeCell.settingSlider.maximumValue = mxHandler.maxAllowedCachesSize; maxCacheSizeCell.settingSlider.value = mxHandler.currentMaxCachesSize; [self onSliderValueChange:maxCacheSizeCell.settingSlider]; - + cell = maxCacheSizeCell; } else { SettingsCellWithSwitch *roomsSettingCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath]; @@ -893,13 +995,17 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl cell = roomsSettingCell; } } else if (indexPath.section == SETTINGS_SECTION_CONFIGURATION_INDEX) { - SettingsCellWithTextView *configCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; - NSString* appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - configCell.settingTextView.text = [NSString stringWithFormat:kConfigurationFormatText, appVersion, MatrixSDKVersion, mxHandler.homeServerURL, nil, mxHandler.userId, mxHandler.accessToken]; - cell = configCell; + if (!configurationCell) { + configurationCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; + NSString* appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + configurationCell.settingTextView.text = [NSString stringWithFormat:kConfigurationFormatText, appVersion, MatrixSDKVersion, mxHandler.homeServerURL, nil, mxHandler.userId, mxHandler.accessToken]; + } + cell = configurationCell; } else if (indexPath.section == SETTINGS_SECTION_COMMANDS_INDEX) { - SettingsCellWithTextView *commandsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; - commandsCell.settingTextView.text = kCommandsDescriptionText; + if (!commandsCell) { + commandsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; + commandsCell.settingTextView.text = kCommandsDescriptionText; + } cell = commandsCell; }