diff --git a/CHANGES.rst b/CHANGES.rst
index aa25f8af2..7e7b2e145 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,7 @@ Improvements:
* Privacy: Make clear that device names are publicly readable (#2662).
* Widgets: Whitelist [MSC1961](https://github.com/matrix-org/matrix-doc/pull/1961) widget urls.
* Settings: CALLS section: Always display the CallKit option but grey it out when not available (only on China).
+ * VoIP: Fallback to matrix.org STUN server with a confirmation dialog (#2646).
Changes in 0.9.2 (2019-08-08)
===============================================
diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m
index 9e6cf8f5b..e6dab7807 100644
--- a/Riot/AppDelegate.m
+++ b/Riot/AppDelegate.m
@@ -2605,6 +2605,11 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Let's call invite be valid for 1 minute
mxSession.callManager.inviteLifetime = 60000;
+ if (RiotSettings.shared.allowStunServerFallback)
+ {
+ mxSession.callManager.fallbackSTUNServer = RiotSettings.shared.stunServerFallback;
+ }
+
// Setup CallKit
if ([MXCallKitAdapter callKitAvailable])
{
@@ -3850,6 +3855,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
NSString *btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"active_call_details", @"Vector", nil), callViewController.callerNameLabel.text];
[self addCallStatusBar:btnTitle];
}
+
+ if ([callViewController isKindOfClass:[CallViewController class]]
+ && ((CallViewController*)callViewController).shouldPromptForStunServerFallback)
+ {
+ [self promptForStunServerFallback];
+ }
if (completion)
{
@@ -3883,6 +3894,52 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
}
}
+- (void)promptForStunServerFallback
+{
+ [_errorNotification dismissViewControllerAnimated:NO completion:nil];
+
+ NSString *stunFallbackHost = RiotSettings.shared.stunServerFallback;
+ // Remove "stun:"
+ stunFallbackHost = [stunFallbackHost componentsSeparatedByString:@":"].lastObject;
+
+ MXSession *mainSession = self.mxSessions.firstObject;
+ NSString *homeServerName = mainSession.matrixRestClient.credentials.homeServerName;
+
+ NSString *message = [NSString stringWithFormat:@"%@\n\n%@",
+ [NSString stringWithFormat:NSLocalizedStringFromTable(@"call_no_stun_server_error_message_1", @"Vector", nil), homeServerName],
+ [NSString stringWithFormat: NSLocalizedStringFromTable(@"call_no_stun_server_error_message_2", @"Vector", nil), stunFallbackHost]];
+
+ _errorNotification = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"call_no_stun_server_error_title", @"Vector", nil)
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+
+ [_errorNotification addAction:[UIAlertAction actionWithTitle:[NSString stringWithFormat: NSLocalizedStringFromTable(@"call_no_stun_server_error_use_fallback_button", @"Vector", nil), stunFallbackHost]
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction * action) {
+
+ RiotSettings.shared.allowStunServerFallback = YES;
+ mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.stunServerFallback;
+
+ [AppDelegate theDelegate].errorNotification = nil;
+ }]];
+
+ [_errorNotification addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"]
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction * action) {
+
+ RiotSettings.shared.allowStunServerFallback = NO;
+
+ [AppDelegate theDelegate].errorNotification = nil;
+ }]];
+
+ // Display the error notification
+ if (!isErrorNotificationSuspended)
+ {
+ [_errorNotification mxk_setAccessibilityIdentifier:@"AppDelegateErrorAlert"];
+ [self showNotificationAlert:_errorNotification];
+ }
+}
+
#pragma mark - Jitsi call
- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video
diff --git a/Riot/Assets/Riot-Defaults.plist b/Riot/Assets/Riot-Defaults.plist
index ebef1344a..5c360dd63 100644
--- a/Riot/Assets/Riot-Defaults.plist
+++ b/Riot/Assets/Riot-Defaults.plist
@@ -59,6 +59,8 @@
createConferenceCallsWithJitsi
+ stunServerFallback
+ stun:turn.matrix.org
enableRageShake
maxAllowedMediaCacheSize
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 0451b075f..872ed3340 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -405,6 +405,9 @@
"settings_enable_callkit" = "Integrated calling";
"settings_callkit_info" = "Receive incoming calls on your lock screen. See your Riot calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple.";
+"settings_calls_stun_server_fallback_button" = "Allow fallback call assist server";
+"settings_calls_stun_server_fallback_description" = "Allow fallback call assist server %@ when your homeserver does not offer one (your IP address would be shared during a call).";
+
"settings_ui_language" = "Language";
"settings_ui_theme" = "Theme";
"settings_ui_theme_auto" = "Auto";
@@ -631,6 +634,11 @@
"call_already_displayed" = "There is already a call in progress.";
"call_jitsi_error" = "Failed to join the conference call.";
+"call_no_stun_server_error_title" ="Call failed due to misconfigured server";
+"call_no_stun_server_error_message_1" ="Please ask the administrator of your homeserver %@ to configure a TURN server in order for calls to work reliably.";
+"call_no_stun_server_error_message_2" ="Alternatively, you can try to use the public server at %@, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings";
+"call_no_stun_server_error_use_fallback_button" = "Try using %@";
+
// No VoIP support
"no_voip_title" = "Incoming call";
"no_voip" = "%@ is calling you but %@ does not support calls yet.\nYou can ignore this notification and answer the call from another device or you can reject it.";
diff --git a/Riot/Generated/RiotDefaults.swift b/Riot/Generated/RiotDefaults.swift
index b760f5619..c82a21c44 100644
--- a/Riot/Generated/RiotDefaults.swift
+++ b/Riot/Generated/RiotDefaults.swift
@@ -39,6 +39,7 @@ internal enum RiotDefaults {
internal static let showRedactionsInRoomHistory: Bool = _document["showRedactionsInRoomHistory"]
internal static let showUnsupportedEventsInRoomHistory: Bool = _document["showUnsupportedEventsInRoomHistory"]
internal static let sortRoomMembersUsingLastSeenTime: Bool = _document["sortRoomMembersUsingLastSeenTime"]
+ internal static let stunServerFallback: String = _document["stunServerFallback"]
internal static let syncLocalContacts: Bool = _document["syncLocalContacts"]
internal static let webAppUrl: String = _document["webAppUrl"]
internal static let webAppUrlBeta: String = _document["webAppUrlBeta"]
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index c1660708c..72fa0f4df 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -370,6 +370,22 @@ internal enum VectorL10n {
internal static var callJitsiError: String {
return VectorL10n.tr("Vector", "call_jitsi_error")
}
+ /// Please ask the administrator of your homeserver %@ to configure a TURN server in order for calls to work reliably.
+ internal static func callNoStunServerErrorMessage1(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "call_no_stun_server_error_message_1", p1)
+ }
+ /// Alternatively, you can try to use the public server at %@, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings
+ internal static func callNoStunServerErrorMessage2(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "call_no_stun_server_error_message_2", p1)
+ }
+ /// Call failed due to misconfigured server
+ internal static var callNoStunServerErrorTitle: String {
+ return VectorL10n.tr("Vector", "call_no_stun_server_error_title")
+ }
+ /// Try using %@
+ internal static func callNoStunServerErrorUseFallbackButton(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "call_no_stun_server_error_use_fallback_button", p1)
+ }
/// Camera
internal static var camera: String {
return VectorL10n.tr("Vector", "camera")
@@ -2394,6 +2410,14 @@ internal enum VectorL10n {
internal static var settingsCallsSettings: String {
return VectorL10n.tr("Vector", "settings_calls_settings")
}
+ /// Allow fallback call assist server
+ internal static var settingsCallsStunServerFallbackButton: String {
+ return VectorL10n.tr("Vector", "settings_calls_stun_server_fallback_button")
+ }
+ /// Allow fallback call assist server %@ when your homeserver does not offer one (your IP address would be shared during a call).
+ internal static func settingsCallsStunServerFallbackDescription(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "settings_calls_stun_server_fallback_description", p1)
+ }
/// Change password
internal static var settingsChangePassword: String {
return VectorL10n.tr("Vector", "settings_change_password")
diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift
index 6db8ec7b3..59c45e6ab 100644
--- a/Riot/Managers/Settings/RiotSettings.swift
+++ b/Riot/Managers/Settings/RiotSettings.swift
@@ -30,6 +30,8 @@ final class RiotSettings: NSObject {
static let notificationsShowDecryptedContent = "showDecryptedContent"
static let pinRoomsWithMissedNotifications = "pinRoomsWithMissedNotif"
static let pinRoomsWithUnreadMessages = "pinRoomsWithUnread"
+ static let allowStunServerFallback = "allowStunServerFallback"
+ static let stunServerFallback = "stunServerFallback"
}
/// Riot Standard Room Member Power Level
@@ -119,4 +121,24 @@ final class RiotSettings: NSObject {
UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.createConferenceCallsWithJitsi)
}
}
+
+
+ // MARK: Calls
+
+ /// Indicate if `allowStunServerFallback` settings has been set once.
+ var isAllowStunServerFallbackHasBeenSetOnce: Bool {
+ return UserDefaults.standard.object(forKey: UserDefaultsKeys.allowStunServerFallback) != nil
+ }
+
+ var allowStunServerFallback: Bool {
+ get {
+ return UserDefaults.standard.bool(forKey: UserDefaultsKeys.allowStunServerFallback)
+ } set {
+ UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.allowStunServerFallback)
+ }
+ }
+
+ var stunServerFallback: String? {
+ return UserDefaults.standard.string(forKey: UserDefaultsKeys.stunServerFallback)
+ }
}
diff --git a/Riot/Modules/Call/CallViewController.h b/Riot/Modules/Call/CallViewController.h
index 3b46760aa..23fe8b82e 100644
--- a/Riot/Modules/Call/CallViewController.h
+++ b/Riot/Modules/Call/CallViewController.h
@@ -26,4 +26,7 @@
@property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *callerImageViewWidthConstraint;
+// At the end of call, this flag indicates if the prompt to use the fallback should be displayed
+@property (nonatomic) BOOL shouldPromptForStunServerFallback;
+
@end
diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m
index 7753347a8..c2a1c48ff 100644
--- a/Riot/Modules/Call/CallViewController.m
+++ b/Riot/Modules/Call/CallViewController.m
@@ -39,6 +39,9 @@
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
+
+ // Flag to compute self.shouldPromptForStunServerFallback
+ BOOL promptForStunServerFallback;
}
@end
@@ -52,6 +55,9 @@
// Setup `MXKViewControllerHandling` properties
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
+
+ promptForStunServerFallback = NO;
+ _shouldPromptForStunServerFallback = NO;
}
- (void)viewDidLoad
@@ -229,6 +235,13 @@
#pragma mark - MXCallDelegate
+- (void)call:(MXCall *)call stateDidChange:(MXCallState)state reason:(MXEvent *)event
+{
+ [super call:call stateDidChange:state reason:event];
+
+ [self checkStunServerFallbackWithCallState:state];
+}
+
- (void)call:(MXCall *)call didEncounterError:(NSError *)error
{
if ([error.domain isEqualToString:MXEncryptingErrorDomain]
@@ -333,6 +346,41 @@
}
}
+
+#pragma mark - Fallback STUN server
+
+- (void)checkStunServerFallbackWithCallState:(MXCallState)callState
+{
+ // Detect if we should display the prompt to fallback to the STUN server defined
+ // in the app plist if the homeserver does not provide STUN or TURN servers.
+ // We should if the call ends while we were in connecting state
+ if (!self.mainSession.callManager.turnServers
+ && !self.mainSession.callManager.fallbackSTUNServer
+ && !RiotSettings.shared.isAllowStunServerFallbackHasBeenSetOnce)
+ {
+ switch (callState)
+ {
+ case MXCallStateConnecting:
+ promptForStunServerFallback = YES;
+ break;
+
+ case MXCallStateConnected:
+ promptForStunServerFallback = NO;
+ break;
+
+ case MXCallStateEnded:
+ if (promptForStunServerFallback)
+ {
+ _shouldPromptForStunServerFallback = YES;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+
#pragma mark - Properties
- (UIImage*)picturePlaceholder
diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m
index 421fa8d03..4131df9cb 100644
--- a/Riot/Modules/Settings/SettingsViewController.m
+++ b/Riot/Modules/Settings/SettingsViewController.m
@@ -86,7 +86,9 @@ enum
enum
{
CALLS_ENABLE_CALLKIT_INDEX = 0,
- CALLS_DESCRIPTION_INDEX,
+ CALLS_CALLKIT_DESCRIPTION_INDEX,
+ CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX,
+ CALLS_STUN_SERVER_FALLBACK_DESCRIPTION_INDEX,
CALLS_COUNT
};
@@ -1266,6 +1268,11 @@ SingleImagePickerPresenterDelegate>
else if (section == SETTINGS_SECTION_CALLS_INDEX)
{
count = CALLS_COUNT;
+
+ if (!RiotSettings.shared.stunServerFallback)
+ {
+ count -= 2;
+ }
}
else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX)
{
@@ -1834,7 +1841,7 @@ SingleImagePickerPresenterDelegate>
cell = labelAndSwitchCell;
}
- else if (row == CALLS_DESCRIPTION_INDEX)
+ else if (row == CALLS_CALLKIT_DESCRIPTION_INDEX)
{
MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView];
globalInfoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_callkit_info", @"Vector", nil);
@@ -1846,6 +1853,30 @@ SingleImagePickerPresenterDelegate>
globalInfoCell.textLabel.enabled = NO;
}
+ cell = globalInfoCell;
+ }
+ else if (row == CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX)
+ {
+ MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
+ labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_calls_stun_server_fallback_button", @"Vector", nil);
+ labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.allowStunServerFallback;
+ labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
+ labelAndSwitchCell.mxkSwitch.enabled = YES;
+ [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleStunServerFallback:) forControlEvents:UIControlEventTouchUpInside];
+
+ cell = labelAndSwitchCell;
+ }
+ else if (row == CALLS_STUN_SERVER_FALLBACK_DESCRIPTION_INDEX)
+ {
+ NSString *stunFallbackHost = RiotSettings.shared.stunServerFallback;
+ // Remove "stun:"
+ stunFallbackHost = [stunFallbackHost componentsSeparatedByString:@":"].lastObject;
+
+ MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView];
+ globalInfoCell.textLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"settings_calls_stun_server_fallback_description", @"Vector", nil), stunFallbackHost];
+ globalInfoCell.textLabel.numberOfLines = 0;
+ globalInfoCell.selectionStyle = UITableViewCellSelectionStyleNone;
+
cell = globalInfoCell;
}
}
@@ -2945,6 +2976,14 @@ SingleImagePickerPresenterDelegate>
[MXKAppSettings standardAppSettings].enableCallKit = switchButton.isOn;
}
+- (void)toggleStunServerFallback:(id)sender
+{
+ UISwitch *switchButton = (UISwitch*)sender;
+ RiotSettings.shared.allowStunServerFallback = switchButton.isOn;
+
+ self.mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.allowStunServerFallback ? RiotSettings.shared.stunServerFallback : nil;
+}
+
- (void)toggleShowDecodedContent:(id)sender
{
UISwitch *switchButton = (UISwitch*)sender;