Merge pull request #1735 from vector-im/render_html

Group Home screen: render the potential images in the long description.
This commit is contained in:
giomfo 2018-01-22 15:31:48 +01:00 committed by GitHub
commit 38a4181ff9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 339 additions and 18 deletions

View file

@ -16,7 +16,7 @@
#import <MatrixKit/MatrixKit.h>
@interface GroupHomeViewController : MXKViewController <UIGestureRecognizerDelegate>
@interface GroupHomeViewController : MXKViewController <UIGestureRecognizerDelegate, UITextViewDelegate>
@property (weak, nonatomic) IBOutlet UIView *mainHeaderContainer;
@property (weak, nonatomic) IBOutlet MXKImageView *groupAvatar;

View file

@ -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 <NSString*>* 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

View file

@ -253,6 +253,9 @@
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<dataDetectorType key="dataDetectorTypes" link="YES"/>
<connections>
<outlet property="delegate" destination="-1" id="QYj-x9-kpV"/>
</connections>
</textView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>