diff --git a/Riot/ViewController/Communities/GroupHomeViewController.h b/Riot/ViewController/Communities/GroupHomeViewController.h index 41c9d08cd..d07b59471 100644 --- a/Riot/ViewController/Communities/GroupHomeViewController.h +++ b/Riot/ViewController/Communities/GroupHomeViewController.h @@ -16,7 +16,7 @@ #import -@interface GroupHomeViewController : MXKViewController +@interface GroupHomeViewController : MXKViewController @property (weak, nonatomic) IBOutlet UIView *mainHeaderContainer; @property (weak, nonatomic) IBOutlet MXKImageView *groupAvatar; diff --git a/Riot/ViewController/Communities/GroupHomeViewController.m b/Riot/ViewController/Communities/GroupHomeViewController.m index 5a6def131..983710c56 100644 --- a/Riot/ViewController/Communities/GroupHomeViewController.m +++ b/Riot/ViewController/Communities/GroupHomeViewController.m @@ -19,9 +19,12 @@ #import "AppDelegate.h" #import "RiotDesignValues.h" +#import "Tools.h" #import "MXGroup+Riot.h" +#import "DTCoreText.h" + @interface GroupHomeViewController () { MXHTTPOperation *currentRequest; @@ -33,6 +36,13 @@ // Observe kRiotDesignValuesDidChangeThemeNotification to handle user interface theme change. id kRiotDesignValuesDidChangeThemeNotificationObserver; + + // The options used to load long description html content. + NSDictionary *options; + NSString *sanitisedGroupLongDescription; + + // The current pushed view controller + UIViewController *pushedViewController; } @end @@ -118,9 +128,6 @@ self.separatorView.backgroundColor = kRiotSecondaryBgColor; - _groupLongDescription.textColor = kRiotSecondaryTextColor; - _groupLongDescription.tintColor = kRiotColorBlue; - [self.leftButton.layer setCornerRadius:5]; self.leftButton.clipsToBounds = YES; self.leftButton.backgroundColor = kRiotColorBlue; @@ -128,6 +135,35 @@ [self.rightButton.layer setCornerRadius:5]; self.rightButton.clipsToBounds = YES; self.rightButton.backgroundColor = kRiotColorBlue; + + if (_groupLongDescription) + { + _groupLongDescription.textColor = kRiotSecondaryTextColor; + _groupLongDescription.tintColor = kRiotColorBlue; + + // Update HTML loading options + NSUInteger bgColor = [MXKTools rgbValueWithColor:kRiotSecondaryBgColor]; + NSString *defaultCSS = [NSString stringWithFormat:@" \ + pre,code { \ + background-color: #%06lX; \ + display: inline; \ + font-family: monospace; \ + white-space: pre; \ + -coretext-fontname: Menlo-Regular; \ + font-size: small; \ + }", (unsigned long)bgColor]; + + // Apply the css style + options = @{ + DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView + DTDefaultFontFamily: _groupLongDescription.font.familyName, + DTDefaultFontName: _groupLongDescription.font.fontName, + DTDefaultFontSize: @(_groupLongDescription.font.pointSize), + DTDefaultTextColor: _groupLongDescription.textColor, + DTDefaultLinkDecoration: @(NO), + DTDefaultStyleSheet: [[DTCSSStylesheet alloc] initWithStyleBlock:defaultCSS] + }; + } } - (UIStatusBarStyle)preferredStatusBarStyle @@ -148,6 +184,9 @@ // Screen tracking [[AppDelegate theDelegate] trackScreen:@"GroupDetailsHome"]; + // Release the potential pushed view controller + [self releasePushedViewController]; + if (_group) { // Restore the listeners on the group update. @@ -196,8 +235,19 @@ [self cancelRegistrationOnGroupChangeNotifications]; } +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + // Scroll to the top the long group description. + _groupLongDescription.contentOffset = CGPointZero; +} + - (void)destroy { + // Release the potential pushed view controller + [self releasePushedViewController]; + // Note: all observers are removed during super call. [super destroy]; @@ -231,6 +281,52 @@ #pragma mark - +- (void)pushViewController:(UIViewController*)viewController +{ + // Keep ref on pushed view controller + pushedViewController = viewController; + + // Check whether the view controller is displayed inside a segmented one. + if (self.parentViewController.navigationController) + { + // Hide back button title + self.parentViewController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + + [self.parentViewController.navigationController pushViewController:viewController animated:YES]; + } + else + { + // Hide back button title + self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + + [self.navigationController pushViewController:viewController animated:YES]; + } +} + +- (void)releasePushedViewController +{ + if (pushedViewController) + { + if ([pushedViewController isKindOfClass:[UINavigationController class]]) + { + UINavigationController *navigationController = (UINavigationController*)pushedViewController; + for (id subViewController in navigationController.viewControllers) + { + if ([subViewController respondsToSelector:@selector(destroy)]) + { + [subViewController destroy]; + } + } + } + else if ([pushedViewController respondsToSelector:@selector(destroy)]) + { + [(id)pushedViewController destroy]; + } + + pushedViewController = nil; + } +} + - (void)registerOnGroupChangeNotifications { if (_mxSession) @@ -263,6 +359,12 @@ { _group = group; + // Check whether the view controller has been loaded + if (!self.isViewLoaded) + { + return; + } + if (_group) { [_group setGroupAvatarImageIn:_groupAvatar matrixSession:self.mxSession]; @@ -339,7 +441,7 @@ if (_separatorViewTopConstraint.constant != self.inviteContainer.frame.size.height) { _separatorViewTopConstraint.constant = self.inviteContainer.frame.size.height; - [self.view layoutIfNeeded]; + [self.view setNeedsLayout]; } } else @@ -348,21 +450,11 @@ if (_separatorViewTopConstraint.constant != 0) { _separatorViewTopConstraint.constant = 0; - [self.view layoutIfNeeded]; + [self.view setNeedsLayout]; } } - if (_group.summary.profile.longDescription.length) - { - //@TODO: implement a specific html renderer to support h1/h2 and handle the Matrix media content URI (in the form of "mxc://..."). - MXKEventFormatter *eventFormatter = [[MXKEventFormatter alloc] initWithMatrixSession:self.mxSession]; - _groupLongDescription.attributedText = [eventFormatter renderHTMLString:_group.summary.profile.longDescription forEvent:nil]; - _groupLongDescription.contentOffset = CGPointZero; - } - else - { - _groupLongDescription.text = nil; - } + [self refreshGroupLongDescription]; } else { @@ -376,7 +468,6 @@ self.inviteContainer.hidden = YES; - _groupLongDescription.text = nil; _separatorViewTopConstraint.constant = 0; _membersCountLabel.text = nil; @@ -391,6 +482,127 @@ _groupAvatar.defaultBackgroundColor = kRiotSecondaryBgColor; } +- (void)refreshGroupLongDescription +{ + if (_group.summary.profile.longDescription.length) + { + // Render this html content in a text view. + NSArray * allowedHTMLTags = @[ + @"font", // custom to matrix for IRC-style font coloring + @"del", // for markdown + @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", @"blockquote", @"p", @"a", @"ul", @"ol", + @"nl", @"li", @"b", @"i", @"u", @"strong", @"em", @"strike", @"code", @"hr", @"br", @"div", + @"table", @"thead", @"caption", @"tbody", @"tr", @"th", @"td", @"pre", + @"img" + ]; + + // Do some sanitisation by handling the potential image + sanitisedGroupLongDescription = [MXKTools sanitiseHTML:_group.summary.profile.longDescription withAllowedHTMLTags:allowedHTMLTags imageHandler:^NSString *(NSString *sourceURL, CGFloat width, CGFloat height) { + + NSString *imageURL; + + if (width != -1 && height != -1) + { + CGSize size = CGSizeMake(width, height); + imageURL = [self.mxSession.matrixRestClient urlOfContentThumbnail:sourceURL toFitViewSize:size withMethod:MXThumbnailingMethodScale]; + } + else + { + imageURL = [self.mxSession.matrixRestClient urlOfContent:sourceURL]; + } + + NSString *mimeType = nil; + // Check if the extension could not be deduced from url + if (![imageURL pathExtension].length) + { + // Set default mime type if no information is available + mimeType = @"image/jpeg"; + } + + NSString *cacheFilePath = [MXMediaManager cachePathForMediaWithURL:imageURL andType:mimeType inFolder:kMXMediaManagerDefaultCacheFolder]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cacheFilePath]) + { + [MXMediaManager downloadMediaFromURL:imageURL andSaveAtFilePath:cacheFilePath success:^{ + + [self renderGroupLongDescription]; + + } failure:nil]; + } + + return [NSString stringWithFormat:@"file://%@", cacheFilePath]; + }]; + } + else + { + sanitisedGroupLongDescription = nil; + } + + [self renderGroupLongDescription]; +} + +- (void)renderGroupLongDescription +{ + if (sanitisedGroupLongDescription) + { + // Using DTCoreText, which renders static string, helps to avoid code injection attacks + // that could happen with the default HTML renderer of NSAttributedString which is a + // webview. + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTMLData:[sanitisedGroupLongDescription dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; + + // Apply additional treatments + NSInteger mxIdsBitMask = (MXKTOOLS_USER_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_ALIAS_BITWISE | MXKTOOLS_EVENT_IDENTIFIER_BITWISE | MXKTOOLS_GROUP_IDENTIFIER_BITWISE); + attributedString = [MXKTools createLinksInAttributedString:attributedString forEnabledMatrixIds:mxIdsBitMask]; + + // Finalize the attributed string by removing DTCoreText artifacts (Trim trailing newlines, replace DTImageTextAttachments...) + _groupLongDescription.attributedText = [MXKTools removeDTCoreTextArtifacts:attributedString]; + _groupLongDescription.contentOffset = CGPointZero; + } + else + { + _groupLongDescription.text = nil; + } +} + +- (void)didSelectRoomId:(NSString*)roomId +{ + // Check first if the user already joined this room. + if ([self.mxSession roomWithRoomId:roomId]) + { + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mxSession]; + MXKRoomDataSource *roomDataSource = [roomDataSourceManager roomDataSourceForRoom:roomId create:YES]; + + // Open this room + RoomViewController *roomViewController = [RoomViewController roomViewController]; + roomViewController.showMissedDiscussionsBadge = NO; + [roomViewController displayRoom:roomDataSource]; + [self pushViewController:roomViewController]; + } + else + { + // Prepare a preview + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomId andSession:self.mxSession]; + __weak typeof(self) weakSelf = self; + [self startActivityIndicator]; + + // Try to get more information about the room before opening its preview + [roomPreviewData peekInRoom:^(BOOL succeeded) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self stopActivityIndicator]; + + // Display the room preview + RoomViewController *roomViewController = [RoomViewController roomViewController]; + roomViewController.showMissedDiscussionsBadge = NO; + [roomViewController displayRoomPreview:roomPreviewData]; + [self pushViewController:roomViewController]; + } + + }]; + } +} + #pragma mark - Action - (IBAction)onButtonPressed:(id)sender @@ -514,4 +726,110 @@ } } +#pragma mark - UITextView delegate + +- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange +{ + BOOL shouldInteractWithURL = YES; + // Try to catch universal link supported by the app + + // When a link refers to a room alias/id, a user id or an event id, the non-ASCII characters (like '#' in room alias) has been escaped + // to be able to convert it into a legal URL string. + NSString *absoluteURLString = [URL.absoluteString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + // If the link can be open it by the app, let it do + if ([Tools isUniversalLink:URL]) + { + shouldInteractWithURL = NO; + + // iOS Patch: fix vector.im urls before using it + NSURL *fixedURL = [Tools fixURLWithSeveralHashKeys:URL]; + + [[AppDelegate theDelegate] handleUniversalLinkFragment:fixedURL.fragment]; + } + // Open a detail screen about the clicked user + else if ([MXTools isMatrixUserIdentifier:absoluteURLString]) + { + shouldInteractWithURL = NO; + + NSString *userId = absoluteURLString; + MXKContact *contact; + // Use the contact detail VC for other users + MXUser *user = [self.mxSession userWithUserId:userId]; + if (user) + { + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId]; + } + else + { + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId]; + } + + ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController contactDetailsViewController]; + contactDetailsViewController.enableVoipCall = NO; + contactDetailsViewController.contact = contact; + + [self pushViewController:contactDetailsViewController]; + } + // Open the clicked room + else if ([MXTools isMatrixRoomIdentifier:absoluteURLString] || [MXTools isMatrixRoomAlias:absoluteURLString]) + { + shouldInteractWithURL = NO; + + NSString *roomIdOrAlias = absoluteURLString; + NSString *roomId; + + if ([roomIdOrAlias hasPrefix:@"#"]) + { + // Check whether the room alias can be translated locally into the room id. + MXRoom *room = [self.mxSession roomWithAlias:roomIdOrAlias]; + if (room) + { + roomId = room.roomId; + } + } + else + { + roomId = roomIdOrAlias; + } + + if (roomId) + { + [self didSelectRoomId:roomId]; + } + else + { + // The alias may be not part of user's rooms states + // Ask the HS to resolve the room alias into a room id and then retry + __weak typeof(self) weakSelf = self; + [self startActivityIndicator]; + + [self.mxSession.matrixRestClient roomIDForRoomAlias:roomIdOrAlias success:^(NSString *roomId) { + + if (roomId && weakSelf) + { + typeof(self) self = weakSelf; + + [self stopActivityIndicator]; + [self didSelectRoomId:roomId]; + } + + } failure:^(NSError *error) { + NSLog(@"[GroupHomeViewController] Error: The home server failed to resolve the room alias (%@)", roomIdOrAlias); + }]; + } + } + // Preview the clicked group + else if ([MXTools isMatrixGroupIdentifier:absoluteURLString]) + { + shouldInteractWithURL = NO; + + // Open the group or preview it + NSString *fragment = [NSString stringWithFormat:@"/group/%@", [absoluteURLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + [[AppDelegate theDelegate] handleUniversalLinkFragment:fragment]; + } + + return shouldInteractWithURL; +} + @end diff --git a/Riot/ViewController/Communities/GroupHomeViewController.xib b/Riot/ViewController/Communities/GroupHomeViewController.xib index ccceda926..95ff92412 100644 --- a/Riot/ViewController/Communities/GroupHomeViewController.xib +++ b/Riot/ViewController/Communities/GroupHomeViewController.xib @@ -253,6 +253,9 @@ + + +