mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
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:
commit
38a4181ff9
3 changed files with 339 additions and 18 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in a new issue