diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 2908c6779..10fe0e0ac 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 32E84FA11F6BD32700CA0B89 /* apps-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 32E84F9E1F6BD32700CA0B89 /* apps-icon.png */; }; 32E84FA21F6BD32700CA0B89 /* apps-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32E84F9F1F6BD32700CA0B89 /* apps-icon@2x.png */; }; 32E84FA31F6BD32700CA0B89 /* apps-icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32E84FA01F6BD32700CA0B89 /* apps-icon@3x.png */; }; + 32EF474920B6EE990031695C /* StickerPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32EF474820B6EE990031695C /* StickerPickerViewController.m */; }; 32F3AE1A1F6FF4E600F0F004 /* WidgetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32F3AE191F6FF4E600F0F004 /* WidgetViewController.m */; }; 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */; }; 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */; }; @@ -766,6 +767,8 @@ 32E84F9E1F6BD32700CA0B89 /* apps-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "apps-icon.png"; sourceTree = ""; }; 32E84F9F1F6BD32700CA0B89 /* apps-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "apps-icon@2x.png"; sourceTree = ""; }; 32E84FA01F6BD32700CA0B89 /* apps-icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "apps-icon@3x.png"; sourceTree = ""; }; + 32EF474720B6EE990031695C /* StickerPickerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StickerPickerViewController.h; sourceTree = ""; }; + 32EF474820B6EE990031695C /* StickerPickerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StickerPickerViewController.m; sourceTree = ""; }; 32F3AE181F6FF4E600F0F004 /* WidgetViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WidgetViewController.h; sourceTree = ""; }; 32F3AE191F6FF4E600F0F004 /* WidgetViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WidgetViewController.m; sourceTree = ""; }; 32FD0A3A1EB0CD9B0072B066 /* BugReportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugReportViewController.h; sourceTree = ""; }; @@ -1562,6 +1565,8 @@ 3233F7301F31F4BF006ACA81 /* JitsiViewController.xib */, 32C2356D1F7B871800E38FC5 /* WidgetPickerViewController.h */, 32C2356E1F7B871800E38FC5 /* WidgetPickerViewController.m */, + 32EF474720B6EE990031695C /* StickerPickerViewController.h */, + 32EF474820B6EE990031695C /* StickerPickerViewController.m */, ); path = Widgets; sourceTree = ""; @@ -3510,6 +3515,7 @@ F083BE661E7009ED00A9B29C /* RoomIncomingTextMsgWithPaginationTitleBubbleCell.m in Sources */, F083BE141E7009ED00A9B29C /* HomeViewController.m in Sources */, F083BDFB1E7009ED00A9B29C /* RoomSearchDataSource.m in Sources */, + 32EF474920B6EE990031695C /* StickerPickerViewController.m in Sources */, 3233F73C1F3306A7006ACA81 /* WidgetManager.m in Sources */, F0B8D0D31FDFECB200F34524 /* GroupTableViewCell.m in Sources */, F083BE281E7009ED00A9B29C /* StartChatViewController.m in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 949c079a6..178ed23d3 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -267,6 +267,8 @@ "room_event_action_view_encryption" = "Encryption Information"; "room_warning_about_encryption" = "End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption."; "room_event_failed_to_send" = "Failed to send"; +"room_action_send_photo_or_video" = "Send photo or video"; +"room_action_send_sticker" = "Send sticker"; // Unknown devices "unknown_devices_alert_title" = "Room contains unknown devices"; @@ -568,6 +570,8 @@ // Widget "widget_no_power_to_manage" = "You need permission to manage widgets in this room"; "widget_creation_failure" = "Widget creation has failed"; +"widget_sticker_picker_no_stickerpacks_alert" = "You don't currently have any stickerpacks enabled."; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Add some now?"; // Widget Integration Manager "widget_integration_need_to_be_able_to_invite" = "You need to be able to invite users to do that."; diff --git a/Riot/Assets/js/postMessageAPI.js b/Riot/Assets/js/postMessageAPI.js index 2b145debe..81b1fe9cb 100644 --- a/Riot/Assets/js/postMessageAPI.js +++ b/Riot/Assets/js/postMessageAPI.js @@ -31,15 +31,6 @@ window.riotIOS.events = {}; // Listen to messages posted by the widget window.riotIOS.onMessage = function(event) { - // Do not SPAM ObjC with event already managed - if (riotIOS.events[event.data._id]) { - return; - } - - if (!event.origin) { // stupid chrome - event.origin = event.originalEvent.origin; - } - // Use an internal "_id" field for matching onMessage events and requests // _id was originally used by the Modular API. Keep it if (!event.data._id) { @@ -52,6 +43,15 @@ window.riotIOS.onMessage = function(event) { if (!event.data._id) { event.data._id = Date.now() + "-" + Math.random().toString(36); } + + // Do not SPAM ObjC with event already managed + if (riotIOS.events[event.data._id]) { + return; + } + + if (!event.origin) { // stupid chrome + event.origin = event.originalEvent.origin; + } // Keep this event for future usage riotIOS.events[event.data._id] = event; diff --git a/Riot/Utils/Widgets/WidgetManager.h b/Riot/Utils/Widgets/WidgetManager.h index 655a88aba..315e363e8 100644 --- a/Riot/Utils/Widgets/WidgetManager.h +++ b/Riot/Utils/Widgets/WidgetManager.h @@ -35,6 +35,7 @@ FOUNDATION_EXPORT NSString *const kWidgetModularEventTypeString; Known types widgets. */ FOUNDATION_EXPORT NSString *const kWidgetTypeJitsi; +FOUNDATION_EXPORT NSString *const kWidgetTypeStickerPicker; /** Posted when a widget has been created, updated or disabled. @@ -101,6 +102,15 @@ WidgetManagerErrorCode; */ - (NSArray *)userWidgets:(MXSession*)mxSession; +/** + List all widgets of a given type of an account. + + @param mxSession the session of the user account. + @param widgetTypes the types of widget to search. Nil means all types. + @return a list of widgets. + */ +- (NSArray *)userWidgets:(MXSession*)mxSession ofTypes:(NSArray*)widgetTypes; + /** Add a modular widget to a room. diff --git a/Riot/Utils/Widgets/WidgetManager.m b/Riot/Utils/Widgets/WidgetManager.m index 8ea994634..cdbfb9835 100644 --- a/Riot/Utils/Widgets/WidgetManager.m +++ b/Riot/Utils/Widgets/WidgetManager.m @@ -23,6 +23,7 @@ NSString *const kWidgetMatrixEventTypeString = @"m.widget"; NSString *const kWidgetModularEventTypeString = @"im.vector.modular.widgets"; NSString *const kWidgetTypeJitsi = @"jitsi"; +NSString *const kWidgetTypeStickerPicker = @"m.stickerpicker"; NSString *const kWidgetManagerDidUpdateWidgetNotification = @"kWidgetManagerDidUpdateWidgetNotification"; @@ -174,14 +175,22 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; - (NSArray *)userWidgets:(MXSession*)mxSession { - // Disable user widgets (sticker picker) for now - return nil; + return [self userWidgets:mxSession ofTypes:nil]; +} +- (NSArray *)userWidgets:(MXSession*)mxSession ofTypes:(NSArray*)widgetTypes +{ // Get all widgets in the user account data NSMutableArray *userWidgets = [NSMutableArray array]; - for (NSDictionary *widgetEventContent in [mxSession.accountData accountDataForEventType:@"m.widgets"].allValues) + for (NSDictionary *widgetEventContent in [mxSession.accountData accountDataForEventType:kMXAccountDataTypeUserWidgets].allValues) { - // Patch: Modular uses a malformed key: "stateKey" instead of "state_key" + if (![widgetEventContent isKindOfClass:NSDictionary.class]) + { + NSLog(@"[WidgetManager] userWidgets: ERROR: invalid user widget format: %@", widgetEventContent); + continue; + } + + // Patch: Modular used a malformed key: "stateKey" instead of "state_key" // TODO: To remove once fixed server side NSDictionary *widgetEventContentFixed = widgetEventContent; if (!widgetEventContent[@"state_key"] && widgetEventContent[@"stateKey"]) @@ -192,7 +201,8 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; } MXEvent *widgetEvent = [MXEvent modelFromJSON:widgetEventContentFixed]; - if (widgetEvent) + if (widgetEvent + && (!widgetTypes || [widgetTypes containsObject:widgetEvent.content[@"type"]])) { Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent inMatrixSession:mxSession]; if (widget) diff --git a/Riot/ViewController/RoomViewController.m b/Riot/ViewController/RoomViewController.m index e8a3c03b7..093f89e17 100644 --- a/Riot/ViewController/RoomViewController.m +++ b/Riot/ViewController/RoomViewController.m @@ -115,6 +115,7 @@ #import "IntegrationManagerViewController.h" #import "WidgetPickerViewController.h" +#import "StickerPickerViewController.h" @interface RoomViewController () { @@ -2854,6 +2855,80 @@ self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; } +#pragma mark - RoomInputToolbarViewDelegate + +- (void)roomInputToolbarViewPresentStickerPicker:(MXKRoomInputToolbarView*)toolbarView +{ + // Search for the sticker picker widget in the user account + Widget *widget = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.mxSession ofTypes:@[kWidgetTypeStickerPicker]].firstObject; + + if (widget) + { + // Display the widget + [widget widgetUrl:^(NSString * _Nonnull widgetUrl) { + + StickerPickerViewController *stickerPickerVC = [[StickerPickerViewController alloc] initWithUrl:widgetUrl forWidget:widget]; + + stickerPickerVC.roomDataSource = self.roomDataSource; + + [self.navigationController pushViewController:stickerPickerVC animated:YES]; + } failure:^(NSError * _Nonnull error) { + + NSLog(@"[RoomVC] Cannot display widget %@", widget); + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + else + { + // The Sticker picker widget is not installed yet. Propose the user to install it + __weak typeof(self) weakSelf = self; + + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + + NSString *alertMessage = [NSString stringWithFormat:@"%@\n%@", + NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert", @"Vector", nil), + NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert_add_now", @"Vector", nil) + ]; + + currentAlert = [UIAlertController alertControllerWithTitle:nil message:alertMessage preferredStyle:UIAlertControllerStyleAlert]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) + { + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + } + + }]]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + + // Show the sticker picker settings screen + IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc] + initForMXSession:self.roomDataSource.mxSession + inRoom:self.roomDataSource.roomId + screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker] + widgetId:nil]; + + [self presentViewController:modularVC animated:NO completion:nil]; + } + }]]; + + [currentAlert mxk_setAccessibilityIdentifier:@"RoomVCStickerPickerAlert"]; + [self presentViewController:currentAlert animated:YES completion:nil]; + } +} + #pragma mark - MXKRoomInputToolbarViewDelegate - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView isTyping:(BOOL)typing @@ -3064,7 +3139,7 @@ // Matrix Apps button else if (self.navigationItem.rightBarButtonItems.count == 2 && sender == self.navigationItem.rightBarButtonItems[1]) { - if ([self widgetsCount:YES]) + if ([self widgetsCount:NO]) { WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession inRoom:self.roomDataSource.roomId]; diff --git a/Riot/ViewController/Widgets/IntegrationManagerViewController.h b/Riot/ViewController/Widgets/IntegrationManagerViewController.h index 3087c57f1..8edc670ce 100644 --- a/Riot/ViewController/Widgets/IntegrationManagerViewController.h +++ b/Riot/ViewController/Widgets/IntegrationManagerViewController.h @@ -37,4 +37,12 @@ FOUNDATION_EXPORT NSString *const kIntegrationManagerAddIntegrationScreen; */ - (instancetype)initForMXSession:(MXSession*)mxSession inRoom:(NSString*)roomId screen:(NSString*)screen widgetId:(NSString*)widgetId; +/** + Get the integration manager settings screen for a given widget type. + + @param widgetType the widget type. + @return the screen id for that widget type. + */ ++ (NSString*)screenForWidget:(NSString*)widgetType; + @end diff --git a/Riot/ViewController/Widgets/IntegrationManagerViewController.m b/Riot/ViewController/Widgets/IntegrationManagerViewController.m index 86e118ecb..e0568f5f0 100644 --- a/Riot/ViewController/Widgets/IntegrationManagerViewController.m +++ b/Riot/ViewController/Widgets/IntegrationManagerViewController.m @@ -50,6 +50,11 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; return self; } ++ (NSString*)screenForWidget:(NSString*)widgetType +{ + return [NSString stringWithFormat:@"type_%@", widgetType]; +} + - (void)destroy { [super destroy]; @@ -147,8 +152,25 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; if (!roomIdInEvent) { - [self sendLocalisedError:@"widget_integration_missing_room_id" toRequest:requestId]; - return; + // These APIs don't require roomId + // Get and set user widgets (not associated with a specific room) + // If roomId is specified, it must be validated, so room-based widgets agreed + // handled further down. + if ([@"set_widget" isEqualToString:action]) + { + [self setWidget:requestId data:requestData]; + return; + } + else if ([@"get_widgets" isEqualToString:action]) + { + [self getWidgets:requestId data:requestData]; + return; + } + else + { + [self sendLocalisedError:@"widget_integration_missing_room_id" toRequest:requestId]; + return; + } } if (![roomIdInEvent isEqualToString:roomId]) @@ -157,6 +179,17 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; return; } + // Get and set room-based widgets + if ([@"set_widget" isEqualToString:action]) + { + [self setWidget:requestId data:requestData]; + return; + } + else if ([@"get_widgets" isEqualToString:action]) + { + [self getWidgets:requestId data:requestData]; + return; + } // These APIs don't require userId if ([@"join_rules_state" isEqualToString:action]) @@ -174,14 +207,9 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; [self getMembershipCount:requestId data:requestData]; return; } - else if ([@"set_widget" isEqualToString:action]) + else if ([@"get_room_enc_state" isEqualToString:action]) { - [self setWidget:requestId data:requestData]; - return; - } - else if ([@"get_widgets" isEqualToString:action]) - { - [self getWidgets:requestId data:requestData]; + [self getRoomEncState:requestId data:requestData]; return; } else if ([@"can_send_event" isEqualToString:action]) @@ -281,75 +309,120 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; - (void)setWidget:(NSString*)requestId data:(NSDictionary*)requestData { - NSLog(@"[IntegrationManagerVC] Received request to set widget in room %@.", roomId); + NSLog(@"[IntegrationManagerVC] Received request to set widget"); - MXRoom *room = [self roomCheckForRequest:requestId data:requestData]; + NSString *widget_id, *widgetType, *widgetUrl; + NSString *widgetName; // optional + NSDictionary *widgetData ; // optional + BOOL userWidget = NO; - if (room) + MXJSONModelSetString(widget_id, requestData[@"widget_id"]); + MXJSONModelSetString(widgetType, requestData[@"type"]); + MXJSONModelSetString(widgetUrl, requestData[@"url"]); + MXJSONModelSetString(widgetName, requestData[@"name"]); + MXJSONModelSetDictionary(widgetData, requestData[@"data"]); + MXJSONModelSetBoolean(userWidget, requestData[@"userWidget"]); + + if (!widget_id) { - NSString *widget_id, *widgetType, *widgetUrl; - NSString *widgetName; // optional - NSDictionary *widgetData ; // optional + [self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; // new Error("Missing required widget fields.")); + return; + } - MXJSONModelSetString(widget_id, requestData[@"widget_id"]); - MXJSONModelSetString(widgetType, requestData[@"type"]); - MXJSONModelSetString(widgetUrl, requestData[@"url"]); - MXJSONModelSetString(widgetName, requestData[@"name"]); - MXJSONModelSetDictionary(widgetData, requestData[@"data"]); + if (!widgetType) + { + [self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; + return; + } - if (!widget_id) + NSMutableDictionary *widgetEventContent = [NSMutableDictionary dictionary]; + if (widgetUrl) + { + widgetEventContent[@"type"] = widgetType; + widgetEventContent[@"url"] = widgetUrl; + + if (widgetName) { - [self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; // new Error("Missing required widget fields.")); - return; + widgetEventContent[@"name"] = widgetName; } + if (widgetData) + { + widgetEventContent[@"data"] = widgetData; + } + } + // else this is a deletion - NSMutableDictionary *widgetEventContent = [NSMutableDictionary dictionary]; + __weak __typeof__(self) weakSelf = self; + + if (userWidget) + { + // Update the user account data + NSMutableDictionary *userWidgets = [NSMutableDictionary dictionaryWithDictionary:[mxSession.accountData accountDataForEventType:kMXAccountDataTypeUserWidgets]]; + + // Delete existing widget with ID + [userWidgets removeObjectForKey:widget_id]; + + // Add new widget / update if (widgetUrl) { - if (!widgetType) - { - [self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; - return; - } - - widgetEventContent[@"type"] = widgetType; - widgetEventContent[@"url"] = widgetUrl; - - if (widgetName) - { - widgetEventContent[@"name"] = widgetName; - } - if (widgetData) - { - widgetEventContent[@"data"] = widgetData; - } + userWidgets[widget_id] = @{ + @"content": widgetEventContent, + @"sender": mxSession.myUser.userId, + @"state_key": widget_id, + @"type": kWidgetMatrixEventTypeString, + @"id": widget_id, + }; } - __weak __typeof__(self) weakSelf = self; + [mxSession setAccountData:userWidgets forType:kMXAccountDataTypeUserWidgets success:^{ - // TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when? - [room sendStateEventOfType:kWidgetModularEventTypeString - content:widgetEventContent - stateKey:widget_id - success:^(NSString *eventId) { + typeof(self) self = weakSelf; + if (self) + { + [self sendNSObjectResponse:@{ + @"success": @(YES) + } + toRequest:requestId]; + } + } failure:^(NSError *error) { - typeof(self) self = weakSelf; - if (self) - { - [self sendNSObjectResponse:@{ - @"success": @(YES) - } - toRequest:requestId]; + typeof(self) self = weakSelf; + if (self) + { + [self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; + } + }]; + } + else + { + // Room widget + MXRoom *room = [self roomCheckForRequest:requestId data:requestData]; + if (room) + { + // TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when? + [room sendStateEventOfType:kWidgetModularEventTypeString + content:widgetEventContent + stateKey:widget_id + success:^(NSString *eventId) { + + typeof(self) self = weakSelf; + if (self) + { + [self sendNSObjectResponse:@{ + @"success": @(YES) + } + toRequest:requestId]; + } } - } - failure:^(NSError *error) { + failure:^(NSError *error) { - typeof(self) self = weakSelf; - if (self) - { - [self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId]; - } - }]; + typeof(self) self = weakSelf; + if (self) + { + [self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId]; + } + }]; + } } } @@ -376,6 +449,15 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; [self sendNSObjectResponse:widgetStateEvents toRequest:requestId]; } +- (void)getRoomEncState:(NSString*)requestId data:(NSDictionary*)requestData +{ + MXRoom *room = [self roomCheckForRequest:requestId data:requestData]; + if (room) + { + [self sendBoolResponse:room.state.isEncrypted toRequest:requestId]; + } +} + - (void)canSendEvent:(NSString*)requestId data:(NSDictionary*)requestData { NSString *eventType; diff --git a/Riot/ViewController/Widgets/StickerPickerViewController.h b/Riot/ViewController/Widgets/StickerPickerViewController.h new file mode 100644 index 000000000..036ac09d6 --- /dev/null +++ b/Riot/ViewController/Widgets/StickerPickerViewController.h @@ -0,0 +1,21 @@ +/* + Copyright 2018 New Vector 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 "WidgetViewController.h" + +@interface StickerPickerViewController : WidgetViewController + +@end diff --git a/Riot/ViewController/Widgets/StickerPickerViewController.m b/Riot/ViewController/Widgets/StickerPickerViewController.m new file mode 100644 index 000000000..c4270bddc --- /dev/null +++ b/Riot/ViewController/Widgets/StickerPickerViewController.m @@ -0,0 +1,60 @@ +/* + Copyright 2018 New Vector 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 "StickerPickerViewController.h" + +#import "IntegrationManagerViewController.h" + +@interface StickerPickerViewController () + +@end + +@implementation StickerPickerViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.navigationItem.title = NSLocalizedStringFromTable(@"room_action_send_sticker", @"Vector", nil); + + // Hide back button title + self.parentViewController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + + UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(onEditButtonPressed)]; + [self.navigationItem setRightBarButtonItem: editButton animated:YES]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Make sure the content is up-to-date when we come back from the sticker picker settings screen + [webView reload]; +} + +- (void)onEditButtonPressed +{ + // Show the sticker picker settings screen + IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc] + initForMXSession:self.roomDataSource.mxSession + inRoom:self.roomDataSource.roomId + screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker] + widgetId:self.widget.widgetId]; + + [self presentViewController:modularVC animated:NO completion:nil]; +} + +@end diff --git a/Riot/ViewController/Widgets/WidgetPickerViewController.m b/Riot/ViewController/Widgets/WidgetPickerViewController.m index 72b4665ee..fb663c1b0 100644 --- a/Riot/ViewController/Widgets/WidgetPickerViewController.m +++ b/Riot/ViewController/Widgets/WidgetPickerViewController.m @@ -53,15 +53,9 @@ MXRoom *room = [mxSession roomWithRoomId:roomId]; - NSArray *roomWidgets = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi] + NSArray *widgets = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi] inRoom:room]; - NSArray *userWidgets = [[WidgetManager sharedManager] userWidgets:room.mxSession]; - - NSMutableArray *widgets = [NSMutableArray array]; - [widgets addObjectsFromArray:roomWidgets]; - [widgets addObjectsFromArray:userWidgets]; - // List widgets for (Widget *widget in widgets) { diff --git a/Riot/ViewController/Widgets/WidgetViewController.h b/Riot/ViewController/Widgets/WidgetViewController.h index 0f9a70685..70c280c93 100644 --- a/Riot/ViewController/Widgets/WidgetViewController.h +++ b/Riot/ViewController/Widgets/WidgetViewController.h @@ -27,6 +27,11 @@ */ @interface WidgetViewController : WebViewViewController +/** + The displayed widget. + */ +@property (nonatomic, readonly) Widget *widget; + /** The room data source. Required if the widget needs to post messages. diff --git a/Riot/ViewController/Widgets/WidgetViewController.m b/Riot/ViewController/Widgets/WidgetViewController.m index aad876a52..8cc7cd6d6 100644 --- a/Riot/ViewController/Widgets/WidgetViewController.m +++ b/Riot/ViewController/Widgets/WidgetViewController.m @@ -17,17 +17,16 @@ #import "WidgetViewController.h" #import "AppDelegate.h" +#import "IntegrationManagerViewController.h" NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse('%@', %@);"; @interface WidgetViewController () -{ - Widget *widget; -} @end @implementation WidgetViewController +@synthesize widget; - (instancetype)initWithUrl:(NSString*)widgetUrl forWidget:(Widget*)theWidget { @@ -216,6 +215,37 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse // Consider we are done with the sticker picker widget [self withdrawViewControllerAnimated:YES completion:nil]; } + else if ([@"integration_manager_open" isEqualToString:action]) + { + NSDictionary *widgetData; + NSString *integType, *integId; + MXJSONModelSetDictionary(widgetData, requestData[@"widgetData"]); + if (widgetData) + { + MXJSONModelSetString(integType, widgetData[@"integType"]); + MXJSONModelSetString(integId, widgetData[@"integId"]); + } + + if (integType && integId) + { + // Open the integration manager requested page + IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc] + initForMXSession:self.roomDataSource.mxSession + inRoom:self.roomDataSource.roomId + screen:[IntegrationManagerViewController screenForWidget:integType] + widgetId:integId]; + + [self presentViewController:modularVC animated:NO completion:nil]; + } + else + { + NSLog(@"[WidgetVC] onPostMessageRequest: ERROR: Invalid content for integration_manager_open: %@", requestData); + } + } + else + { + NSLog(@"[WidgetVC] onPostMessageRequest: ERROR: Unsupported action: %@: %@", action, requestData); + } } - (void)sendBoolResponse:(BOOL)response toRequest:(NSString*)requestId diff --git a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.h b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.h index 0f5797583..ccf18e143 100644 --- a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.h +++ b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.h @@ -18,12 +18,28 @@ #import "MediaPickerViewController.h" +@protocol RoomInputToolbarViewDelegate + +/** + Tells the delegate that the user wants to display the sticker picker. + + @param toolbarView the room input toolbar view. + */ +- (void)roomInputToolbarViewPresentStickerPicker:(MXKRoomInputToolbarView*)toolbarView; + +@end + /** `RoomInputToolbarView` instance is a view used to handle all kinds of available inputs for a room (message composer, attachments selection...). */ @interface RoomInputToolbarView : MXKRoomInputToolbarViewWithHPGrowingText +/** + The delegate notified when inputs are ready. + */ +@property (nonatomic) id delegate; + @property (weak, nonatomic) IBOutlet UIView *mainToolbarView; @property (weak, nonatomic) IBOutlet UIView *separatorView; diff --git a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m index cd30b6802..47c243199 100644 --- a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m +++ b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m @@ -29,17 +29,21 @@ #import +#import "WidgetManager.h" +#import "IntegrationManagerViewController.h" + @interface RoomInputToolbarView() { MediaPickerViewController *mediaPicker; - // The call type selection (voice or video) - UIAlertController *callActionSheet; + // The intermediate action sheet + UIAlertController *actionSheet; } @end @implementation RoomInputToolbarView +@dynamic delegate; + (UINib *)nib { @@ -228,24 +232,53 @@ // Check whether media attachment is supported if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:presentViewController:)]) { - // MediaPickerViewController is based on the Photos framework. So it is available only for iOS 8 and later. - Class PHAsset_class = NSClassFromString(@"PHAsset"); - if (PHAsset_class) - { - mediaPicker = [MediaPickerViewController mediaPickerViewController]; - mediaPicker.mediaTypes = @[(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie]; - mediaPicker.delegate = self; - UINavigationController *navigationController = [UINavigationController new]; - [navigationController pushViewController:mediaPicker animated:NO]; - - [self.delegate roomInputToolbarView:self presentViewController:navigationController]; - } - else - { - // We use UIImagePickerController by default for iOS < 8 - self.leftInputToolbarButton = self.attachMediaButton; - [super onTouchUpInside:self.leftInputToolbarButton]; - } + // Ask the user the kind of the call: voice or video? + actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + + __weak typeof(self) weakSelf = self; + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_photo_or_video", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->actionSheet = nil; + + [self showMediaPicker]; + } + + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_sticker", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->actionSheet = nil; + + [self.delegate roomInputToolbarViewPresentStickerPicker:self]; + } + + }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->actionSheet = nil; + } + + }]]; + + [actionSheet popoverPresentationController].sourceView = self.voiceCallButton; + [actionSheet popoverPresentationController].sourceRect = self.voiceCallButton.bounds; + [self.window.rootViewController presentViewController:actionSheet animated:YES completion:nil]; } else { @@ -257,52 +290,52 @@ if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:placeCallWithVideo:)]) { // Ask the user the kind of the call: voice or video? - callActionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; __weak typeof(self) weakSelf = self; - [callActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"voice", @"Vector", nil) + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"voice", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; - self->callActionSheet = nil; + self->actionSheet = nil; [self.delegate roomInputToolbarView:self placeCallWithVideo:NO]; } }]]; - [callActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"video", @"Vector", nil) + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"video", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; - self->callActionSheet = nil; + self->actionSheet = nil; [self.delegate roomInputToolbarView:self placeCallWithVideo:YES]; } }]]; - [callActionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + [actionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; - self->callActionSheet = nil; + self->actionSheet = nil; } }]]; - [callActionSheet popoverPresentationController].sourceView = self.voiceCallButton; - [callActionSheet popoverPresentationController].sourceRect = self.voiceCallButton.bounds; - [self.window.rootViewController presentViewController:callActionSheet animated:YES completion:nil]; + [actionSheet popoverPresentationController].sourceView = self.voiceCallButton; + [actionSheet popoverPresentationController].sourceRect = self.voiceCallButton.bounds; + [self.window.rootViewController presentViewController:actionSheet animated:YES completion:nil]; } } else if (button == self.hangupCallButton) @@ -316,15 +349,36 @@ [super onTouchUpInside:button]; } +- (void)showMediaPicker +{ + // MediaPickerViewController is based on the Photos framework. So it is available only for iOS 8 and later. + Class PHAsset_class = NSClassFromString(@"PHAsset"); + if (PHAsset_class) + { + mediaPicker = [MediaPickerViewController mediaPickerViewController]; + mediaPicker.mediaTypes = @[(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie]; + mediaPicker.delegate = self; + UINavigationController *navigationController = [UINavigationController new]; + [navigationController pushViewController:mediaPicker animated:NO]; + + [self.delegate roomInputToolbarView:self presentViewController:navigationController]; + } + else + { + // We use UIImagePickerController by default for iOS < 8 + self.leftInputToolbarButton = self.attachMediaButton; + [super onTouchUpInside:self.leftInputToolbarButton]; + } +} - (void)destroy { [self dismissMediaPicker]; - if (callActionSheet) + if (actionSheet) { - [callActionSheet dismissViewControllerAnimated:NO completion:nil]; - callActionSheet = nil; + [actionSheet dismissViewControllerAnimated:NO completion:nil]; + actionSheet = nil; } [super destroy];