Merge pull request #1864 from vector-im/riot_1860

Send Stickers: version 1
This commit is contained in:
manuroe
2018-05-18 11:35:00 +02:00
committed by GitHub
13 changed files with 535 additions and 340 deletions
+7 -7
View File
@@ -30,7 +30,7 @@
24EEE5A11F23A09A00B3C705 /* RiotDesignValues.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BC171E7009EC00A9B29C /* RiotDesignValues.m */; };
24EEE5A21F23A8B400B3C705 /* MXRoom+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BBE81E7009EC00A9B29C /* MXRoom+Riot.m */; };
24EEE5A31F23A8C300B3C705 /* AvatarGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BC111E7009EC00A9B29C /* AvatarGenerator.m */; };
24EEE5A41F24C06E00B3C705 /* BuildFile in Resources */ = {isa = PBXBuildFile; };
24EEE5A41F24C06E00B3C705 /* (null) in Resources */ = {isa = PBXBuildFile; };
24EEE5A81F25529600B3C705 /* cancel@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A121EDEE65000F5DC9A /* cancel@3x.png */; };
24EEE5A91F25529900B3C705 /* cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A111EDEE65000F5DC9A /* cancel@2x.png */; };
24EEE5AA1F25529C00B3C705 /* cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A101EDEE65000F5DC9A /* cancel.png */; };
@@ -70,11 +70,11 @@
327382C41F276AED00356143 /* Vector.strings in Resources */ = {isa = PBXBuildFile; fileRef = 327382C01F276AED00356143 /* Vector.strings */; };
32795C0B203DA4C4002420E2 /* DisabledRoomInputToolbarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 32795C09203DA4C3002420E2 /* DisabledRoomInputToolbarView.m */; };
32795C0C203DA4C4002420E2 /* DisabledRoomInputToolbarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32795C0A203DA4C4002420E2 /* DisabledRoomInputToolbarView.xib */; };
3284A35120A07C210044F922 /* postMessageAPI.js in Resources */ = {isa = PBXBuildFile; fileRef = 3284A35020A07C210044F922 /* postMessageAPI.js */; };
3284D7FF1FBB34B70090AA80 /* RoomKeyRequestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3284D7FE1FBB34B70090AA80 /* RoomKeyRequestViewController.m */; };
32918EA91F473BDB0076CA16 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 32918EA51F473BDB0076CA16 /* Localizable.strings */; };
32918EAA1F473BDB0076CA16 /* Vector.strings in Resources */ = {isa = PBXBuildFile; fileRef = 32918EA71F473BDB0076CA16 /* Vector.strings */; };
32935CB11F6056FD006888C8 /* IntegrationManagerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32935CB01F6056FD006888C8 /* IntegrationManagerViewController.m */; };
32935CB41F628BCE006888C8 /* IntegrationManager.js in Resources */ = {isa = PBXBuildFile; fileRef = 32935CB31F628BCE006888C8 /* IntegrationManager.js */; };
329DCE191F988F8100468420 /* riot_icon_callkit.png in Resources */ = {isa = PBXBuildFile; fileRef = 329DCE161F988F8100468420 /* riot_icon_callkit.png */; };
329DCE1A1F988F8100468420 /* riot_icon_callkit@2.png in Resources */ = {isa = PBXBuildFile; fileRef = 329DCE171F988F8100468420 /* riot_icon_callkit@2.png */; };
329DCE1B1F988F8100468420 /* riot_icon_callkit@3.png in Resources */ = {isa = PBXBuildFile; fileRef = 329DCE181F988F8100468420 /* riot_icon_callkit@3.png */; };
@@ -733,13 +733,13 @@
32795C08203DA4C3002420E2 /* DisabledRoomInputToolbarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DisabledRoomInputToolbarView.h; sourceTree = "<group>"; };
32795C09203DA4C3002420E2 /* DisabledRoomInputToolbarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DisabledRoomInputToolbarView.m; sourceTree = "<group>"; };
32795C0A203DA4C4002420E2 /* DisabledRoomInputToolbarView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DisabledRoomInputToolbarView.xib; sourceTree = "<group>"; };
3284A35020A07C210044F922 /* postMessageAPI.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = postMessageAPI.js; sourceTree = "<group>"; };
3284D7FD1FBB34B70090AA80 /* RoomKeyRequestViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomKeyRequestViewController.h; sourceTree = "<group>"; };
3284D7FE1FBB34B70090AA80 /* RoomKeyRequestViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomKeyRequestViewController.m; sourceTree = "<group>"; };
32918EA61F473BDB0076CA16 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = "<group>"; };
32918EA81F473BDB0076CA16 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Vector.strings; sourceTree = "<group>"; };
32935CAF1F6056FD006888C8 /* IntegrationManagerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IntegrationManagerViewController.h; sourceTree = "<group>"; };
32935CB01F6056FD006888C8 /* IntegrationManagerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationManagerViewController.m; sourceTree = "<group>"; };
32935CB31F628BCE006888C8 /* IntegrationManager.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = IntegrationManager.js; sourceTree = "<group>"; };
329DCE161F988F8100468420 /* riot_icon_callkit.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = riot_icon_callkit.png; sourceTree = "<group>"; };
329DCE171F988F8100468420 /* riot_icon_callkit@2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "riot_icon_callkit@2.png"; sourceTree = "<group>"; };
329DCE181F988F8100468420 /* riot_icon_callkit@3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "riot_icon_callkit@3.png"; sourceTree = "<group>"; };
@@ -1627,7 +1627,7 @@
32935CB21F628B98006888C8 /* js */ = {
isa = PBXGroup;
children = (
32935CB31F628BCE006888C8 /* IntegrationManager.js */,
3284A35020A07C210044F922 /* postMessageAPI.js */,
);
path = js;
sourceTree = "<group>";
@@ -2911,9 +2911,9 @@
F0BFBDBB1FF3BF6F00C88726 /* Vector.strings in Resources */,
F0BFBDBF1FF3BFD200C88726 /* Vector.strings in Resources */,
24D6B35E1F3CA03E00FC7A71 /* FallbackViewController.xib in Resources */,
24EEE5A41F24C06E00B3C705 /* BuildFile in Resources */,
24EEE5A41F24C06E00B3C705 /* (null) in Resources */,
2439DD641F6BBEA50090F42D /* RecentRoomTableViewCell.xib in Resources */,
24EEE5A41F24C06E00B3C705 /* BuildFile in Resources */,
24EEE5A41F24C06E00B3C705 /* (null) in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3270,7 +3270,6 @@
F083BD961E7009ED00A9B29C /* leave@2x.png in Resources */,
F0E05A461EA0F9EB004B83FB /* tab_rooms@2x.png in Resources */,
32AE61F41F0D2183007255F4 /* Vector.strings in Resources */,
32935CB41F628BCE006888C8 /* IntegrationManager.js in Resources */,
F083BD851E7009ED00A9B29C /* favouriteOff@3x.png in Resources */,
F083BD541E7009ED00A9B29C /* camera_switch@2x.png in Resources */,
F07CB3992029B20100C29C20 /* GroupTableViewCellWithSwitch.xib in Resources */,
@@ -3292,6 +3291,7 @@
327382C41F276AED00356143 /* Vector.strings in Resources */,
32BB89EF204D86DA002F3AEC /* Localizable.strings in Resources */,
F083BDE51E7009ED00A9B29C /* voice_call_icon@3x.png in Resources */,
3284A35120A07C210044F922 /* postMessageAPI.js in Resources */,
322806A11F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.xib in Resources */,
F083BE201E7009ED00A9B29C /* RoomParticipantsViewController.xib in Resources */,
F083BE471E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.xib in Resources */,
+4 -3
View File
@@ -2200,7 +2200,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
sdkOptions.backgroundModeHandler = [[MXUIKitBackgroundModeHandler alloc] init];
// Get modular widget events in rooms histories
[[MXKAppSettings standardAppSettings] addSupportedEventTypes:@[kWidgetEventTypeString]];
[[MXKAppSettings standardAppSettings] addSupportedEventTypes:@[kWidgetMatrixEventTypeString, kWidgetModularEventTypeString]];
// Disable long press on event in bubble cells
[MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:YES];
@@ -2259,9 +2259,10 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Each room member will be considered as a potential contact.
[MXKContactManager sharedManager].contactManagerMXRoomSource = MXKContactManagerMXRoomSourceAll;
// Send read receipts for modular widgets events too
// Send read receipts for widgets events too
NSMutableArray<MXEventTypeString> *acknowledgableEventTypes = [NSMutableArray arrayWithArray:mxSession.acknowledgableEventTypes];
[acknowledgableEventTypes addObject:kWidgetEventTypeString];
[acknowledgableEventTypes addObject:kWidgetMatrixEventTypeString];
[acknowledgableEventTypes addObject:kWidgetModularEventTypeString];
mxSession.acknowledgableEventTypes = acknowledgableEventTypes;
}
else if (mxSession.state == MXSessionStateStoreDataReady)
@@ -28,7 +28,7 @@ window.riotIOS.sendObjectMessageToObjC = function(parameters) {
window.riotIOS.events = {};
// Listen to messages posted by Modular
// Listen to messages posted by the widget
window.riotIOS.onMessage = function(event) {
// Do not SPAM ObjC with event already managed
@@ -40,6 +40,19 @@ window.riotIOS.onMessage = function(event) {
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) {
// The Matrix Widget API v2 spec says:
// "The requestId field should be unique and included in all requests"
event.data._id = event.data.requestId;
}
// Make sure to have one id
if (!event.data._id) {
event.data._id = Date.now() + "-" + Math.random().toString(36);
}
// Keep this event for future usage
riotIOS.events[event.data._id] = event;
@@ -50,7 +63,7 @@ window.riotIOS.onMessage = function(event) {
window.addEventListener('message', riotIOS.onMessage, false);
// ObjC -> Modular JS bridge
// ObjC -> Widget JS bridge
window.riotIOS.sendResponse = function(eventId, res) {
// Retrieve the correspong JS event
+8 -4
View File
@@ -39,10 +39,10 @@
- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState error:(MXKEventFormatterError *)error
{
// Build strings for modular widget events
// TODO: At the moment, we support only jitsi widgets
// Build strings for widget events
if (event.eventType == MXEventTypeCustom
&& [event.type isEqualToString:kWidgetEventTypeString])
&& ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|| [event.type isEqualToString:kWidgetModularEventTypeString]))
{
NSString *displayText;
@@ -71,7 +71,11 @@
// This is a closed widget
// Check if it corresponds to a jitsi widget by looking at other state events for
// this jitsi widget (widget id = event.stateKey).
for (MXEvent *widgetStateEvent in [roomState stateEventsWithType:kWidgetEventTypeString])
// Get all widgets state events in the room
NSMutableArray<MXEvent*> *widgetStateEvents = [NSMutableArray arrayWithArray:[roomState stateEventsWithType:kWidgetMatrixEventTypeString]];
[widgetStateEvents addObjectsFromArray:[roomState stateEventsWithType:kWidgetModularEventTypeString]];
for (MXEvent *widgetStateEvent in widgetStateEvents)
{
if ([widgetStateEvent.stateKey isEqualToString:widget.widgetId])
{
+13 -5
View File
@@ -22,9 +22,12 @@
- (instancetype)initWithWidgetEvent:(MXEvent *)widgetEvent inMatrixSession:(MXSession*)mxSession
{
if (![widgetEvent.type isEqualToString:kWidgetEventTypeString])
// TODO - Room widgets need to be moved to 'm.widget' state events
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
if (![widgetEvent.type isEqualToString:kWidgetMatrixEventTypeString]
&& ![widgetEvent.type isEqualToString:kWidgetModularEventTypeString])
{
// The Widget class works only with modular, aka "im.vector.modular.widgets", widgets
// The Widget class works only with modular, aka "m.widget" or "im.vector.modular.widgets", widgets
return nil;
}
@@ -67,9 +70,6 @@
widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_display_name" withString:displayName];
widgetUrl = [widgetUrl stringByReplacingOccurrencesOfString:@"$matrix_avatar_url" withString:avatarUrl];
// And add the user scalar token
widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"&scalar_token=%@", scalarToken]];
// Integrate widget data into widget url
for (NSString *key in _data)
{
@@ -97,6 +97,14 @@
}
}
// Add the user scalar token
widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"%@scalar_token=%@",
[widgetUrl containsString:@"?"] ? @"&" : @"?",
scalarToken]];
// Add the widget id
widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"&widgetId=%@", _widgetId]];
success(widgetUrl);
}
+16 -2
View File
@@ -21,9 +21,15 @@
#import "Widget.h"
/**
The type of matrix event used for modular widgets.
The type of matrix event used for matrix widgets.
*/
FOUNDATION_EXPORT NSString *const kWidgetEventTypeString;
FOUNDATION_EXPORT NSString *const kWidgetMatrixEventTypeString;
/**
The type of matrix event used for modular widgets.
TODO: It should be replaced by kWidgetMatrixEventTypeString.
*/
FOUNDATION_EXPORT NSString *const kWidgetModularEventTypeString;
/**
Known types widgets.
@@ -87,6 +93,14 @@ WidgetManagerErrorCode;
*/
- (NSArray<Widget*> *)widgetsNotOfTypes:(NSArray<NSString*>*)notWidgetTypes inRoom:(MXRoom*)room;
/**
List all widgets of an account.
@param mxSession the session of the user account.
@return a list of widgets.
*/
- (NSArray<Widget*> *)userWidgets:(MXSession*)mxSession;
/**
Add a modular widget to a room.
+42 -8
View File
@@ -20,7 +20,8 @@
#pragma mark - Contants
NSString *const kWidgetEventTypeString = @"im.vector.modular.widgets";
NSString *const kWidgetMatrixEventTypeString = @"m.widget";
NSString *const kWidgetModularEventTypeString = @"im.vector.modular.widgets";
NSString *const kWidgetTypeJitsi = @"jitsi";
NSString *const kWidgetManagerDidUpdateWidgetNotification = @"kWidgetManagerDidUpdateWidgetNotification";
@@ -102,10 +103,11 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
// Widget id -> widget
NSMutableDictionary <NSString*, Widget *> *widgets = [NSMutableDictionary dictionary];
// Get all im.vector.modular.widgets state events in the room
NSMutableArray<MXEvent*> *widgetEvents = [NSMutableArray arrayWithArray:[room.state stateEventsWithType:kWidgetEventTypeString]];
// Get all widgets state events in the room
NSMutableArray<MXEvent*> *widgetEvents = [NSMutableArray arrayWithArray:[room.state stateEventsWithType:kWidgetMatrixEventTypeString]];
[widgetEvents addObjectsFromArray:[room.state stateEventsWithType:kWidgetModularEventTypeString]];
// There can be several im.vector.modular.widgets state events for a same widget but
// There can be several widgets state events for a same widget but
// only the last one must be considered.
// Order widgetEvents with the last event first
@@ -124,7 +126,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
return result;
}];
// Create each widget from its lastest im.vector.modular.widgets state event
// Create each widget from its lastest widgets state event
for (MXEvent *widgetEvent in widgetEvents)
{
// Filter widget types if required
@@ -170,6 +172,36 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
return activeWidgets;
}
- (NSArray<Widget*> *)userWidgets:(MXSession*)mxSession
{
// Get all widgets in the user account data
NSMutableArray<Widget *> *userWidgets = [NSMutableArray array];
for (NSDictionary *widgetEventContent in [mxSession.accountData accountDataForEventType:@"m.widgets"].allValues)
{
// Patch: Modular uses a malformed key: "stateKey" instead of "state_key"
// TODO: To remove once fixed server side
NSDictionary *widgetEventContentFixed = widgetEventContent;
if (!widgetEventContent[@"state_key"] && widgetEventContent[@"stateKey"])
{
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:widgetEventContent];
dict[@"state_key"] = widgetEventContent[@"stateKey"];
widgetEventContentFixed = dict;
}
MXEvent *widgetEvent = [MXEvent modelFromJSON:widgetEventContentFixed];
if (widgetEvent)
{
Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent inMatrixSession:mxSession];
if (widget)
{
[userWidgets addObject:widget];
}
}
}
return userWidgets;
}
- (MXHTTPOperation *)createWidget:(NSString*)widgetId
withContent:(NSDictionary<NSString*, NSObject*>*)widgetContent
inRoom:(MXRoom*)room
@@ -192,7 +224,8 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
// Send a state event with the widget data
// TODO: This API will be shortly replaced by a pure modular API
return [room sendStateEventOfType:kWidgetEventTypeString
// TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when?
return [room sendStateEventOfType:kWidgetModularEventTypeString
content:widgetContent
stateKey:widgetId
success:nil failure:failure];
@@ -247,7 +280,8 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
// Send a state event with an empty content to disable the widget
// TODO: This API will be shortly replaced by a pure modular API
return [room sendStateEventOfType:kWidgetEventTypeString
// TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when?
return [room sendStateEventOfType:kWidgetModularEventTypeString
content:@{}
stateKey:widgetId
success:^(NSString *eventId)
@@ -292,7 +326,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
NSString *hash = [NSString stringWithFormat:@"%p", mxSession];
id listener = [mxSession listenToEventsOfTypes:@[kWidgetEventTypeString] onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) {
id listener = [mxSession listenToEventsOfTypes:@[kWidgetMatrixEventTypeString, kWidgetModularEventTypeString] onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) {
typeof(self) self = weakSelf;
+12 -5
View File
@@ -1257,9 +1257,10 @@
// If the setting is disabled, do not show the icon
self.navigationItem.rightBarButtonItems = @[self.navigationItem.rightBarButtonItem];
}
else if (self.widgetsCount)
else if ([self widgetsCount:NO])
{
// Show there are widgets by changing the "apps" icon color
// Show it in red only for room widgets, not user's widgets
// TODO: Design must be reviewed
UIImage *icon = self.navigationItem.rightBarButtonItems[1].image;
icon = [MXKTools paintImage:icon withColor:kRiotColorPinkRed];
@@ -3063,7 +3064,7 @@
// Matrix Apps button
else if (self.navigationItem.rightBarButtonItems.count == 2 && sender == self.navigationItem.rightBarButtonItems[1])
{
if (self.widgetsCount)
if ([self widgetsCount:YES])
{
WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession
inRoom:self.roomDataSource.roomId];
@@ -3643,10 +3644,16 @@
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
- (NSUInteger)widgetsCount
- (NSUInteger)widgetsCount:(BOOL)includeUserWidgets
{
return [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi]
inRoom:self.roomDataSource.room].count;
NSUInteger widgetsCount = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi]
inRoom:self.roomDataSource.room].count;
if (includeUserWidgets)
{
widgetsCount = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.room.mxSession].count;
}
return widgetsCount;
}
#pragma mark - Unreachable Network Handling
@@ -14,9 +14,7 @@
limitations under the License.
*/
#import "WebViewViewController.h"
#import <MatrixSDK/MatrixSDK.h>
#import "WidgetViewController.h"
FOUNDATION_EXPORT NSString *const kIntegrationManagerMainScreen;
FOUNDATION_EXPORT NSString *const kIntegrationManagerAddIntegrationScreen;
@@ -24,8 +22,10 @@ FOUNDATION_EXPORT NSString *const kIntegrationManagerAddIntegrationScreen;
/**
`IntegrationManagerViewController` displays the Modular integration manager webapp
into a webview.
It reuses the postMessage API pipe defined in `WidgetViewController`.
*/
@interface IntegrationManagerViewController : WebViewViewController <UIWebViewDelegate>
@interface IntegrationManagerViewController : WidgetViewController
/**
Initialise with params for the Modular interface webapp.
@@ -17,15 +17,10 @@
#import "IntegrationManagerViewController.h"
#import "WidgetManager.h"
#import "AppDelegate.h"
#import <JavaScriptCore/JavaScriptCore.h>
NSString *const kIntegrationManagerMainScreen = nil;
NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ";
NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@', %@);";
@interface IntegrationManagerViewController ()
{
@@ -63,19 +58,6 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
operation = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
webView.scalesPageToFit = NO;
webView.scrollView.bounces = NO;
// Disable opacity so that the webview background uses the current interface theme
webView.opaque = NO;
webView.delegate = self;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
@@ -86,7 +68,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self startActivityIndicator];
// Make sure we a scalar token
// Make sure we have a scalar token
operation = [[WidgetManager sharedManager] getScalarTokenForMXSession:mxSession success:^(NSString *theScalarToken) {
typeof(self) self = weakSelf;
@@ -147,143 +129,15 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
return url;
}
- (void)enableDebug
#pragma mark - Modular postMessage API implementation
- (void)onPostMessageRequest:(NSString*)requestId data:(NSDictionary*)requestData
{
// Setup console.log() -> NSLog() route
JSContext *ctx = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
ctx[@"console"][@"log"] = ^(JSValue * msg) {
NSLog(@"-- JavaScript: %@", msg);
};
// Redirect all console.* logging methods to console.log
[webView stringByEvaluatingJavaScriptFromString:@"console.debug = console.log; console.info = console.log; console.warn = console.log; console.error = console.log;"];
}
- (void)showErrorAsAlert:(NSError*)error
{
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
if (!title)
{
if (msg)
{
title = msg;
msg = nil;
}
else
{
title = [NSBundle mxk_localizedStringForKey:@"error"];
}
}
__weak __typeof__(self) weakSelf = self;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
if (self)
{
// Leave this Intergrations Manager VC
[self withdrawViewControllerAnimated:YES completion:nil];
}
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - UIWebViewDelegate
-(void)webViewDidFinishLoad:(UIWebView *)theWebView
{
[self enableDebug];
// Setup js code
NSString *path = [[NSBundle mainBundle] pathForResource:@"IntegrationManager" ofType:@"js"];
NSString *js = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[webView stringByEvaluatingJavaScriptFromString:js];
[self stopActivityIndicator];
// Check connectivity
if ([AppDelegate theDelegate].isOffline)
{
// The web page may be in the cache, so its loading will be successful
// but we cannot go further, it often leads to a blank screen.
// So, display an error so that the user can escape.
NSError *error = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorNotConnectedToInternet
userInfo:@{
NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"network_offline_prompt", @"Vector", nil)
}];
[self showErrorAsAlert:error];
}
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *urlString = [[request URL] absoluteString];
if ([urlString hasPrefix:@"js:"])
{
// Listen only to scheme of the JS-UIWebView bridge
NSString *jsonString = [[[urlString componentsSeparatedByString:@"js:"] lastObject] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *parameters = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers
error:&error];
if (!error)
{
[self onMessage:parameters];
}
return NO;
}
if (navigationType == UIWebViewNavigationTypeLinkClicked )
{
// Open links outside the app
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
return YES;
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
// Filter out the users's scalar token
NSString *errorDescription = error.description;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"scalar_token=\\w*"
options:NSRegularExpressionCaseInsensitive error:nil];
errorDescription = [regex stringByReplacingMatchesInString:errorDescription
options:0
range:NSMakeRange(0, errorDescription.length)
withTemplate:@"scalar_token=..."];
NSLog(@"[IntegrationManagerVC] didFailLoadWithError: %@", errorDescription);
[self stopActivityIndicator];
[self showErrorAsAlert:error];
}
#pragma mark - Modular postMessage API
- (void)onMessage:(NSDictionary*)JSData
{
NSDictionary *eventData;
MXJSONModelSetDictionary(eventData, JSData[@"event.data"]);
NSString *roomIdInEvent, *userId, *action;
MXJSONModelSetString(roomIdInEvent, eventData[@"room_id"]);
MXJSONModelSetString(userId, eventData[@"user_id"]);
MXJSONModelSetString(action, eventData[@"action"]);
MXJSONModelSetString(roomIdInEvent, requestData[@"room_id"]);
MXJSONModelSetString(userId, requestData[@"user_id"]);
MXJSONModelSetString(action, requestData[@"action"]);
if ([action isEqualToString:@"close_scalar"])
{
@@ -293,13 +147,13 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
if (!roomIdInEvent)
{
[self sendLocalisedError:@"widget_integration_missing_room_id" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_missing_room_id" toRequest:requestId];
return;
}
if (![roomIdInEvent isEqualToString:roomId])
{
[self sendError:[NSString stringWithFormat:NSLocalizedStringFromTable(@"widget_integration_room_not_visible", @"Vector", nil), roomIdInEvent] toEvent:eventData];
[self sendError:[NSString stringWithFormat:NSLocalizedStringFromTable(@"widget_integration_room_not_visible", @"Vector", nil), roomIdInEvent] toRequest:requestId];
return;
}
@@ -307,149 +161,86 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
// These APIs don't require userId
if ([@"join_rules_state" isEqualToString:action])
{
[self getJoinRules:eventData];
[self getJoinRules:requestId data:requestData];
return;
}
else if ([@"set_plumbing_state" isEqualToString:action])
{
[self setPlumbingState:eventData];
[self setPlumbingState:requestId data:requestData];
return;
}
else if ([@"get_membership_count" isEqualToString:action])
{
[self getMembershipCount:eventData];
[self getMembershipCount:requestId data:requestData];
return;
}
else if ([@"set_widget" isEqualToString:action])
{
[self setWidget:eventData];
[self setWidget:requestId data:requestData];
return;
}
else if ([@"get_widgets" isEqualToString:action])
{
[self getWidgets:eventData];
[self getWidgets:requestId data:requestData];
return;
}
else if ([@"can_send_event" isEqualToString:action])
{
[self canSendEvent:eventData];
[self canSendEvent:requestId data:requestData];
return;
}
if (!userId)
{
[self sendLocalisedError:@"widget_integration_missing_user_id" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_missing_user_id" toRequest:requestId];
return;
}
if ([@"membership_state" isEqualToString:action])
{
[self getMembershipState:userId eventData:eventData];
[self getMembershipState:userId request:requestId data:requestData];
}
else if ([@"invite" isEqualToString:action])
{
[self inviteUser:userId eventData:eventData];
[self inviteUser:userId request:requestId data:requestData];
}
else if ([@"bot_options" isEqualToString:action])
{
[self getBotOptions:userId eventData:eventData];
[self getBotOptions:userId request:requestId data:requestData];
}
else if ([@"set_bot_options" isEqualToString:action])
{
[self setBotOptions:userId eventData:eventData];
[self setBotOptions:userId request:requestId data:requestData];
}
else if ([@"set_bot_power" isEqualToString:action])
{
[self setBotPower:userId eventData:eventData];
[self setBotPower:userId request:requestId data:requestData];
}
else
{
NSLog(@"[IntegrationManagerViewControllerVC] Unhandled postMessage event with action %@: %@", action, JSData);
NSLog(@"[IntegrationManagerViewControllerVC] Unhandled postMessage event with action %@: %@", action, requestData);
}
}
- (void)sendBoolResponse:(BOOL)response toEvent:(NSDictionary*)eventData
{
// Convert BOOL to "true" or "false"
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToModular,
eventData[@"_id"],
response ? @"true" : @"false"];
#pragma mark - Private methods
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendIntegerResponse:(NSUInteger)response toEvent:(NSDictionary*)eventData
{
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToModular,
eventData[@"_id"],
@(response)];
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendNSObjectResponse:(NSObject*)response toEvent:(NSDictionary*)eventData
{
NSString *jsString;
if (response)
{
// Convert response into a JS object through a JSON string
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:response
options:0
error:0];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsString = [NSString stringWithFormat:@"JSON.parse('%@')", jsonString];
}
else
{
jsString = @"null";
}
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToModular,
eventData[@"_id"],
jsString];
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendError:(NSString*)message toEvent:(NSDictionary*)eventData
{
NSLog(@"[IntegrationManagerVC] sendError: Action %@ failed with message: %@", eventData[@"action"], message);
// TODO: JS has an additional optional parameter: nestedError
[self sendNSObjectResponse:@{
@"error": @{
@"message": message
}
}
toEvent:eventData];
}
- (void)sendLocalisedError:(NSString*)errorKey toEvent:(NSDictionary*)eventData
{
[self sendError:NSLocalizedStringFromTable(errorKey, @"Vector", nil) toEvent:eventData];
}
#pragma mark - Modular postMessage Implementation
- (MXRoom *)roomCheckWithEvent:(NSDictionary*)eventData
- (MXRoom *)roomCheckForRequest:(NSString*)requestId data:(NSDictionary*)requestData
{
MXRoom *room = [mxSession roomWithRoomId:roomId];
if (!room)
{
[self sendLocalisedError:@"widget_integration_room_not_recognised" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_room_not_recognised" toRequest:requestId];
}
return room;
}
- (void)inviteUser:(NSString*)userId eventData:(NSDictionary*)eventData
- (void)inviteUser:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to invite %@ into room %@.", userId, roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
@@ -459,7 +250,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
else
{
@@ -473,7 +264,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
} failure:^(NSError *error) {
@@ -481,18 +272,18 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
typeof(self) self = weakSelf;
if (self)
{
[self sendLocalisedError:@"widget_integration_need_to_be_able_to_invite" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_need_to_be_able_to_invite" toRequest:requestId];
}
}];
}
}
}
- (void)setWidget:(NSDictionary*)eventData
- (void)setWidget:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to set widget in room %@.", roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
@@ -500,15 +291,15 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
NSString *widgetName; // optional
NSDictionary *widgetData ; // optional
MXJSONModelSetString(widget_id, eventData[@"widget_id"]);
MXJSONModelSetString(widgetType, eventData[@"type"]);
MXJSONModelSetString(widgetUrl, eventData[@"url"]);
MXJSONModelSetString(widgetName, eventData[@"name"]);
MXJSONModelSetDictionary(widgetData, eventData[@"data"]);
MXJSONModelSetString(widget_id, requestData[@"widget_id"]);
MXJSONModelSetString(widgetType, requestData[@"type"]);
MXJSONModelSetString(widgetUrl, requestData[@"url"]);
MXJSONModelSetString(widgetName, requestData[@"name"]);
MXJSONModelSetDictionary(widgetData, requestData[@"data"]);
if (!widget_id)
{
[self sendLocalisedError:@"widget_integration_unable_to_create" toEvent:eventData]; // new Error("Missing required widget fields."));
[self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId]; // new Error("Missing required widget fields."));
return;
}
@@ -517,7 +308,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
{
if (!widgetType)
{
[self sendLocalisedError:@"widget_integration_unable_to_create" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_unable_to_create" toRequest:requestId];
return;
}
@@ -536,7 +327,8 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
__weak __typeof__(self) weakSelf = self;
[room sendStateEventOfType:kWidgetEventTypeString
// TODO: Move to kWidgetMatrixEventTypeString ("m.widget") type but when?
[room sendStateEventOfType:kWidgetModularEventTypeString
content:widgetEventContent
stateKey:widget_id
success:^(NSString *eventId) {
@@ -547,7 +339,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
}
failure:^(NSError *error) {
@@ -555,48 +347,52 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
typeof(self) self = weakSelf;
if (self)
{
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId];
}
}];
}
}
- (void)getWidgets:(NSDictionary*)eventData
- (void)getWidgets:(NSString*)requestId data:(NSDictionary*)requestData
{
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
NSMutableArray<NSDictionary*> *widgetStateEvents = [NSMutableArray array];
if (room)
{
NSArray<Widget*> *widgets = [[WidgetManager sharedManager] widgetsInRoom:room];
NSMutableArray<NSDictionary*> *widgetStateEvents = [NSMutableArray arrayWithCapacity:widgets.count];
for (Widget *widget in widgets)
{
[widgetStateEvents addObject:widget.widgetEvent.JSONDictionary];
}
[self sendNSObjectResponse:widgetStateEvents toEvent:eventData];
}
// Add user widgets (not linked to a specific room)
for (Widget *widget in [[WidgetManager sharedManager] userWidgets:mxSession])
{
[widgetStateEvents addObject:widget.widgetEvent.JSONDictionary];
}
[self sendNSObjectResponse:widgetStateEvents toRequest:requestId];
}
- (void)canSendEvent:(NSDictionary*)eventData
- (void)canSendEvent:(NSString*)requestId data:(NSDictionary*)requestData
{
NSString *eventType;
BOOL isState = NO;
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
if (room.state.membership != MXMembershipJoin)
{
[self sendLocalisedError:@"widget_integration_must_be_in_room" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_must_be_in_room" toRequest:requestId];
return;
}
MXJSONModelSetString(eventType, eventData[@"event_type"]);
MXJSONModelSetBoolean(isState, eventData[@"is_state"]);
MXJSONModelSetString(eventType, requestData[@"event_type"]);
MXJSONModelSetBoolean(isState, requestData[@"is_state"]);
MXRoomPowerLevels *powerLevels = room.state.powerLevels;
NSInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:mxSession.myUser.userId];
@@ -614,48 +410,48 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
if (canSend)
{
[self sendBoolResponse:YES toEvent:eventData];
[self sendBoolResponse:YES toRequest:requestId];
}
else
{
[self sendLocalisedError:@"widget_integration_no_permission_in_room" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_no_permission_in_room" toRequest:requestId];
}
}
}
- (void)getMembershipState:(NSString*)userId eventData:(NSDictionary*)eventData
- (void)getMembershipState:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] membership_state of %@ in room %@ requested.", userId, roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
MXRoomMember *member = [room.state memberWithUserId:userId];
[self sendNSObjectResponse:member.originalEvent.content toEvent:eventData];
[self sendNSObjectResponse:member.originalEvent.content toRequest:requestId];
}
}
- (void)getJoinRules:(NSDictionary*)eventData
- (void)getJoinRules:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] join_rules of %@ requested.", roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
MXEvent *event = [room.state stateEventsWithType:kMXEventTypeStringRoomJoinRules].lastObject;
[self sendNSObjectResponse:event.JSONDictionary toEvent:eventData];
[self sendNSObjectResponse:event.JSONDictionary toRequest:requestId];
}
}
- (void)setPlumbingState:(NSDictionary*)eventData
- (void)setPlumbingState:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to set plumbing state to status %@ in room %@.", eventData[@"status"], roomId);
NSLog(@"[IntegrationManagerVC] Received request to set plumbing state to status %@ in room %@.", requestData[@"status"], roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
NSString *status;
MXJSONModelSetString(status, eventData[@"status"]);
MXJSONModelSetString(status, requestData[@"status"]);
if (status)
{
@@ -674,7 +470,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
}
failure:^(NSError *error) {
@@ -682,7 +478,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
typeof(self) self = weakSelf;
if (self)
{
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId];
}
}];
}
@@ -693,11 +489,11 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
}
}
- (void)getBotOptions:(NSString*)userId eventData:(NSDictionary*)eventData
- (void)getBotOptions:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to get options for bot %@ in room %@", userId, roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
NSString *stateKey = [NSString stringWithFormat:@"_%@", userId];
@@ -717,19 +513,19 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
}
}
[self sendNSObjectResponse:botOptionsEvent.JSONDictionary toEvent:eventData];
[self sendNSObjectResponse:botOptionsEvent.JSONDictionary toRequest:requestId];
}
}
- (void)setBotOptions:(NSString*)userId eventData:(NSDictionary*)eventData
- (void)setBotOptions:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to set options for bot %@ in room %@", userId, roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
NSDictionary *content;
MXJSONModelSetDictionary(content, eventData[@"content"]);
MXJSONModelSetDictionary(content, requestData[@"content"]);
if (content)
{
@@ -748,7 +544,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
}
failure:^(NSError *error) {
@@ -756,7 +552,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
typeof(self) self = weakSelf;
if (self)
{
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId];
}
}];
}
@@ -767,15 +563,15 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
}
}
- (void)setBotPower:(NSString*)userId eventData:(NSDictionary*)eventData
- (void)setBotPower:(NSString*)userId request:(NSString*)requestId data:(NSDictionary*)requestData
{
NSLog(@"[IntegrationManagerVC] Received request to set power level to %@ for bot %@ in room %@.", eventData[@"level"], userId, roomId);
NSLog(@"[IntegrationManagerVC] Received request to set power level to %@ for bot %@ in room %@.", requestData[@"level"], userId, roomId);
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
NSInteger level = -1;
MXJSONModelSetInteger(level, eventData[@"level"]);
MXJSONModelSetInteger(level, requestData[@"level"]);
if (level >= 0)
{
@@ -789,7 +585,7 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
[self sendNSObjectResponse:@{
@"success": @(YES)
}
toEvent:eventData];
toRequest:requestId];
}
} failure:^(NSError *error) {
@@ -797,25 +593,25 @@ NSString *const kJavascriptSendResponseToModular = @"riotIOS.sendResponse('%@',
typeof(self) self = weakSelf;
if (self)
{
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_failed_to_send_request" toRequest:requestId];
}
}];
}
else
{
NSLog(@"[IntegrationManagerVC] setBotPower. Power level must be positive integer.");
[self sendLocalisedError:@"widget_integration_positive_power_level" toEvent:eventData];
[self sendLocalisedError:@"widget_integration_positive_power_level" toRequest:requestId];
}
}
}
- (void)getMembershipCount:(NSDictionary*)eventData
- (void)getMembershipCount:(NSString*)requestId data:(NSDictionary*)requestData
{
MXRoom *room = [self roomCheckWithEvent:eventData];
MXRoom *room = [self roomCheckForRequest:requestId data:requestData];
if (room)
{
NSUInteger membershipCount = room.state.joinedMembers.count;
[self sendIntegerResponse:membershipCount toEvent:eventData];
[self sendIntegerResponse:membershipCount toRequest:requestId];
}
}
@@ -53,12 +53,19 @@
MXRoom *room = [mxSession roomWithRoomId:roomId];
NSArray<Widget*> *widgets = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi]
NSArray<Widget*> *roomWidgets = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsi]
inRoom:room];
NSArray<Widget*> *userWidgets = [[WidgetManager sharedManager] userWidgets:room.mxSession];
NSMutableArray<Widget*> *widgets = [NSMutableArray array];
[widgets addObjectsFromArray:roomWidgets];
[widgets addObjectsFromArray:userWidgets];
// List widgets
for (Widget *widget in widgets)
{
alertAction = [UIAlertAction actionWithTitle:widget.name
alertAction = [UIAlertAction actionWithTitle:widget.name ? widget.name : widget.type
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action)
{
@@ -68,8 +75,12 @@
// Display the widget
[widget widgetUrl:^(NSString * _Nonnull widgetUrl) {
WidgetViewController *widgetVC = [[WidgetViewController alloc] initWithUrl:widgetUrl forWidget:widget];
[mxkViewController.navigationController pushViewController:widgetVC animated:YES];
WidgetViewController *widgetVC = [[WidgetViewController alloc] initWithUrl:widgetUrl forWidget:widget];
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession];
widgetVC.roomDataSource = [roomDataSourceManager roomDataSourceForRoom:roomId create:NO];
[mxkViewController.navigationController pushViewController:widgetVC animated:YES];
} failure:^(NSError * _Nonnull error) {
@@ -17,12 +17,22 @@
#import "WebViewViewController.h"
#import "WidgetManager.h"
#import "MatrixKit/MatrixKit.h"
/**
`WidgetViewController` displays widget within a webview.
It also exposes a generic pipe, the postMessage API, to communicate with the
content within the webview, ie the widget (matrix app).
*/
@interface WidgetViewController : WebViewViewController
/**
The room data source.
Required if the widget needs to post messages.
*/
@property (nonatomic) MXKRoomDataSource *roomDataSource;
/**
Init 'WidgetViewController' instance with a widget.
@@ -31,4 +41,65 @@
*/
- (instancetype)initWithUrl:(NSString*)widgetUrl forWidget:(Widget*)widget;
/**
Display an alert over this controller.
@param error the error to display.
*/
- (void)showErrorAsAlert:(NSError*)error;
#pragma mark - postMessage API
/**
Callback called when the widget make a postMessage API request.
This method can be overidden to implement a specific API between the matrix client
and widget.
@param requestId the id of the widget request.
@param requestData the request data.
*/
- (void)onPostMessageRequest:(NSString*)requestId data:(NSDictionary*)requestData;
/**
Send a boolean response to a request from the widget.
@param response the response to send.
@param requestId the id of the widget request.
*/
- (void)sendBoolResponse:(BOOL)response toRequest:(NSString*)requestId;
/**
Send an integer response to a request from the widget.
@param response the response to send.
@param requestId the id of the widget request.
*/
- (void)sendIntegerResponse:(NSUInteger)response toRequest:(NSString*)requestId;
/**
Send a serialiable object response to a request the widget.
@param response the response to send.
@param requestId the id of the widget request.
*/
- (void)sendNSObjectResponse:(NSObject*)response toRequest:(NSString*)requestId;
/**
Send a message error to a request from the widget.
@param message the error message.
@param requestId the id of the widget request.
*/
- (void)sendError:(NSString*)message toRequest:(NSString*)requestId;
/**
Send a localised message error to a request from the widget.
@param errorKey the string id of the message error.
@param requestId the id of the widget request.
*/
- (void)sendLocalisedError:(NSString*)errorKey toRequest:(NSString*)requestId;
@end
@@ -16,6 +16,10 @@
#import "WidgetViewController.h"
#import "AppDelegate.h"
NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse('%@', %@);";
@interface WidgetViewController ()
{
Widget *widget;
@@ -42,7 +46,239 @@
webView.scalesPageToFit = NO;
webView.scrollView.bounces = NO;
self.navigationItem.title = widget.name;
// Disable opacity so that the webview background uses the current interface theme
webView.opaque = NO;
if (widget)
{
self.navigationItem.title = widget.name ? widget.name : widget.type;
}
}
- (void)showErrorAsAlert:(NSError*)error
{
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
if (!title)
{
if (msg)
{
title = msg;
msg = nil;
}
else
{
title = [NSBundle mxk_localizedStringForKey:@"error"];
}
}
__weak __typeof__(self) weakSelf = self;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
if (self)
{
// Leave this widget VC
[self withdrawViewControllerAnimated:YES completion:nil];
}
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - UIWebViewDelegate
-(void)webViewDidFinishLoad:(UIWebView *)theWebView
{
[self enableDebug];
// Setup js code
NSString *path = [[NSBundle mainBundle] pathForResource:@"postMessageAPI" ofType:@"js"];
NSString *js = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[webView stringByEvaluatingJavaScriptFromString:js];
[self stopActivityIndicator];
// Check connectivity
if ([AppDelegate theDelegate].isOffline)
{
// The web page may be in the cache, so its loading will be successful
// but we cannot go further, it often leads to a blank screen.
// So, display an error so that the user can escape.
NSError *error = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorNotConnectedToInternet
userInfo:@{
NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"network_offline_prompt", @"Vector", nil)
}];
[self showErrorAsAlert:error];
}
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *urlString = [[request URL] absoluteString];
if ([urlString hasPrefix:@"js:"])
{
// Listen only to scheme of the JS-UIWebView bridge
NSString *jsonString = [[[urlString componentsSeparatedByString:@"js:"] lastObject] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *parameters = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers
error:&error];
if (!error)
{
// Retrieve the js event payload data
NSDictionary *eventData;
MXJSONModelSetDictionary(eventData, parameters[@"event.data"]);
NSString *requestId;
MXJSONModelSetString(requestId, eventData[@"_id"]);
if (requestId)
{
[self onPostMessageRequest:requestId data:eventData];
}
else
{
NSLog(@"[WidgetVC] shouldStartLoadWithRequest: ERROR: Missing request id in postMessage API %@", parameters);
}
}
return NO;
}
if (navigationType == UIWebViewNavigationTypeLinkClicked )
{
// Open links outside the app
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
return YES;
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
// Filter out the users's scalar token
NSString *errorDescription = error.description;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"scalar_token=\\w*"
options:NSRegularExpressionCaseInsensitive error:nil];
errorDescription = [regex stringByReplacingMatchesInString:errorDescription
options:0
range:NSMakeRange(0, errorDescription.length)
withTemplate:@"scalar_token=..."];
NSLog(@"[WidgetVC] didFailLoadWithError: %@", errorDescription);
[self stopActivityIndicator];
[self showErrorAsAlert:error];
}
#pragma mark - postMessage API
- (void)onPostMessageRequest:(NSString*)requestId data:(NSDictionary*)requestData
{
NSString *action;
MXJSONModelSetString(action, requestData[@"action"]);
if ([@"m.sticker" isEqualToString:action])
{
// Extract the sticker event content and send it as is
// The key should be "data" according to https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
// TODO: Fix it once spec is finalised
NSDictionary *widgetData;
NSDictionary *stickerContent;
MXJSONModelSetDictionary(widgetData, requestData[@"widgetData"]);
if (widgetData)
{
MXJSONModelSetDictionary(stickerContent, widgetData[@"content"]);
}
if (stickerContent)
{
// Let the data source manage the sending cycle
[_roomDataSource sendEventOfType:kMXEventTypeStringSticker content:stickerContent success:nil failure:nil];
}
else
{
NSLog(@"[WidgetVC] onPostMessageRequest: ERROR: Invalid content for m.sticker: %@", requestData);
}
// Consider we are done with the sticker picker widget
[self withdrawViewControllerAnimated:YES completion:nil];
}
}
- (void)sendBoolResponse:(BOOL)response toRequest:(NSString*)requestId
{
// Convert BOOL to "true" or "false"
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToPostMessageAPI,
requestId,
response ? @"true" : @"false"];
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendIntegerResponse:(NSUInteger)response toRequest:(NSString*)requestId
{
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToPostMessageAPI,
requestId,
@(response)];
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendNSObjectResponse:(NSObject*)response toRequest:(NSString*)requestId
{
NSString *jsString;
if (response)
{
// Convert response into a JS object through a JSON string
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:response
options:0
error:0];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsString = [NSString stringWithFormat:@"JSON.parse('%@')", jsonString];
}
else
{
jsString = @"null";
}
NSString *js = [NSString stringWithFormat:kJavascriptSendResponseToPostMessageAPI,
requestId,
jsString];
[webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)sendError:(NSString*)message toRequest:(NSString*)requestId
{
NSLog(@"[WidgetVC] sendError: Action %@ failed with message: %@", requestId, message);
// TODO: JS has an additional optional parameter: nestedError
[self sendNSObjectResponse:@{
@"error": @{
@"message": message
}
}
toRequest:requestId];
}
- (void)sendLocalisedError:(NSString*)errorKey toRequest:(NSString*)requestId
{
[self sendError:NSLocalizedStringFromTable(errorKey, @"Vector", nil) toRequest:requestId];
}
@end