From 10fb36f4be0488f5393a451b68e15bdac9ca7c3e Mon Sep 17 00:00:00 2001 From: gmbnt Date: Mon, 23 Mar 2020 17:09:59 +1100 Subject: [PATCH 1/4] Notify user of unexpected device link requests --- Signal/src/Loki/View Controllers/BaseVC.swift | 14 ++++++++++++++ .../ConversationViewController.m | 15 +++++++++++++++ .../Multi Device/DeviceLinkingUtilities.swift | 17 +++++++++++++++-- .../src/Loki/Utilities/Notification+Loki.swift | 4 ++++ .../src/Messages/OWSMessageManager.m | 3 +++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Signal/src/Loki/View Controllers/BaseVC.swift b/Signal/src/Loki/View Controllers/BaseVC.swift index 4beca6679..19803f660 100644 --- a/Signal/src/Loki/View Controllers/BaseVC.swift +++ b/Signal/src/Loki/View Controllers/BaseVC.swift @@ -5,5 +5,19 @@ class BaseVC : UIViewController { override func viewDidLoad() { setNeedsStatusBarAppearanceUpdate() + NotificationCenter.default.addObserver(self, selector: #selector(handleUnexpectedDeviceLinkRequestReceivedNotification), name: .unexpectedDeviceLinkRequestReceived, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func handleUnexpectedDeviceLinkRequestReceivedNotification() { + guard DeviceLinkingUtilities.shouldShowUnexpectedDeviceLinkRequestReceivedAlert else { return } + DispatchQueue.main.async { + let alert = UIAlertController(title: "Device Link Request Received", message: "Open the device link screen by going to \"Settings\"> \"Devices\" > \"Link a Device\" to link your devices.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 6a8af49d0..bdb42f243 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -444,6 +444,11 @@ typedef enum : NSUInteger { selector:@selector(handleMessageFailedNotification:) name:NSNotification.messageFailed object:nil]; + // Device linking + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleUnexpectedDeviceLinkRequestReceivedNotification) + name:NSNotification.unexpectedDeviceLinkRequestReceived + object:nil]; } - (BOOL)isGroupConversation @@ -5479,6 +5484,16 @@ typedef enum : NSUInteger { }); } +- (void)handleUnexpectedDeviceLinkRequestReceivedNotification +{ + if (!LKDeviceLinkingUtilities.shouldShowUnexpectedDeviceLinkRequestReceivedAlert) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Device Link Request Received" message:@"Open the device link screen by going to \"Settings\"> \"Devices\" > \"Link a Device\" to link your devices." preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; + }); +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/API/Multi Device/DeviceLinkingUtilities.swift b/SignalServiceKit/src/Loki/API/Multi Device/DeviceLinkingUtilities.swift index a36a75e60..8af5145c2 100644 --- a/SignalServiceKit/src/Loki/API/Multi Device/DeviceLinkingUtilities.swift +++ b/SignalServiceKit/src/Loki/API/Multi Device/DeviceLinkingUtilities.swift @@ -1,6 +1,19 @@ -public enum DeviceLinkingUtilities { - +@objc(LKDeviceLinkingUtilities) +public final class DeviceLinkingUtilities : NSObject { + private static var lastUnexpectedDeviceLinkRequestDate: Date? = nil + + private override init() { } + + @objc public static var shouldShowUnexpectedDeviceLinkRequestReceivedAlert: Bool { + let now = Date() + if let lastUnexpectedDeviceLinkRequestDate = lastUnexpectedDeviceLinkRequestDate { + if now.timeIntervalSince(lastUnexpectedDeviceLinkRequestDate) < 30 { return false } + } + lastUnexpectedDeviceLinkRequestDate = now + return true + } + // When requesting a device link, the slave device signs the master device's public key. When authorizing // a device link, the master device signs the slave device's public key. diff --git a/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift index f4ab9bef8..c2adfb2bf 100644 --- a/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift +++ b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift @@ -17,6 +17,8 @@ public extension Notification.Name { public static let seedViewed = Notification.Name("seedViewed") // Interaction public static let dataNukeRequested = Notification.Name("dataNukeRequested") + // Device linking + public static let unexpectedDeviceLinkRequestReceived = Notification.Name("unexpectedDeviceLinkRequestReceived") } @objc public extension NSNotification { @@ -37,4 +39,6 @@ public extension Notification.Name { @objc public static let seedViewed = Notification.Name.seedViewed.rawValue as NSString // Interaction @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString + // Device linking + @objc public static let unexpectedDeviceLinkRequestReceived = Notification.Name.unexpectedDeviceLinkRequestReceived.rawValue as NSString } diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 16ea7fd0b..bc6ce2031 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -494,6 +494,9 @@ NS_ASSUME_NONNULL_BEGIN } } else if (slaveSignature != nil) { // Request OWSLogInfo(@"[Loki] Received a device linking request from: %@", envelope.source); // Not slaveHexEncodedPublicKey + if (LKDeviceLinkingSession.current == nil) { + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.unexpectedDeviceLinkRequestReceived object:nil]; + } [LKDeviceLinkingSession.current processLinkingRequestFrom:slaveHexEncodedPublicKey to:masterHexEncodedPublicKey with:slaveSignature]; } } else if (contentProto.syncMessage) { From 5ad32af0d30f5f6130d0abd4a1609bcf3d190969 Mon Sep 17 00:00:00 2001 From: gmbnt Date: Wed, 25 Mar 2020 10:27:43 +1100 Subject: [PATCH 2/4] Ditch long polling --- Signal/src/AppDelegate.h | 4 +- Signal/src/AppDelegate.m | 28 +++++----- .../src/Loki/View Controllers/LandingVC.swift | 4 +- .../HomeView/HomeViewController.m | 2 +- ...{LokiLongPoller.swift => LokiPoller.swift} | 51 ++++++++----------- 5 files changed, 41 insertions(+), 48 deletions(-) rename SignalServiceKit/src/Loki/API/{LokiLongPoller.swift => LokiPoller.swift} (53%) diff --git a/Signal/src/AppDelegate.h b/Signal/src/AppDelegate.h index e36cdadca..fe1346722 100644 --- a/Signal/src/AppDelegate.h +++ b/Signal/src/AppDelegate.h @@ -8,8 +8,8 @@ extern NSString *const AppDelegateStoryboardMain; @interface AppDelegate : UIResponder -- (void)startLongPollerIfNeeded; -- (void)stopLongPollerIfNeeded; +- (void)startPollerIfNeeded; +- (void)stopPollerIfNeeded; - (void)setUpDefaultPublicChatsIfNeeded; - (void)startOpenGroupPollersIfNeeded; - (void)stopOpenGroupPollersIfNeeded; diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 19db0cfee..40a825c66 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -69,7 +69,7 @@ static BOOL isInternalTestVersion = NO; // Loki @property (nonatomic) LKP2PServer *lokiP2PServer; -@property (nonatomic) LKLongPoller *lokiLongPoller; +@property (nonatomic) LKPoller *lokiPoller; @property (nonatomic) LKRSSFeedPoller *lokiNewsFeedPoller; @property (nonatomic) LKRSSFeedPoller *lokiMessengerUpdatesFeedPoller; @@ -181,7 +181,7 @@ static BOOL isInternalTestVersion = NO; [DDLog flushLog]; // Loki: Stop pollers - [self stopLongPollerIfNeeded]; + [self stopPollerIfNeeded]; [self stopOpenGroupPollersIfNeeded]; } @@ -202,7 +202,7 @@ static BOOL isInternalTestVersion = NO; [DDLog flushLog]; // Loki: Stop pollers - [self stopLongPollerIfNeeded]; + [self stopPollerIfNeeded]; [self stopOpenGroupPollersIfNeeded]; if (self.lokiP2PServer) { [self.lokiP2PServer stop]; } @@ -784,7 +784,7 @@ static BOOL isInternalTestVersion = NO; [LKP2PAPI broadcastOnlineStatus]; // Loki: Start pollers - [self startLongPollerIfNeeded]; + [self startPollerIfNeeded]; [self startOpenGroupPollersIfNeeded]; // Loki: Get device links @@ -1477,7 +1477,7 @@ static BOOL isInternalTestVersion = NO; [self.lokiFriendRequestExpirationJob startIfNecessary]; // Loki: Start pollers - [self startLongPollerIfNeeded]; + [self startPollerIfNeeded]; [self startOpenGroupPollersIfNeeded]; // Loki: Get device links @@ -1591,12 +1591,12 @@ static BOOL isInternalTestVersion = NO; #pragma mark - Loki -- (void)setUpLongPollerIfNeeded +- (void)setUpPollerIfNeeded { - if (self.lokiLongPoller != nil) { return; } + if (self.lokiPoller != nil) { return; } NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; if (userHexEncodedPublicKey == nil) { return; } - self.lokiLongPoller = [[LKLongPoller alloc] initOnMessagesReceived:^(NSArray *messages) { + self.lokiPoller = [[LKPoller alloc] initOnMessagesReceived:^(NSArray *messages) { for (SSKProtoEnvelope *message in messages) { NSData *data = [message serializedDataAndReturnError:nil]; if (data != nil) { @@ -1608,15 +1608,15 @@ static BOOL isInternalTestVersion = NO; }]; } -- (void)startLongPollerIfNeeded +- (void)startPollerIfNeeded { - [self setUpLongPollerIfNeeded]; - [self.lokiLongPoller startIfNeeded]; + [self setUpPollerIfNeeded]; + [self.lokiPoller startIfNeeded]; } -- (void)stopLongPollerIfNeeded +- (void)stopPollerIfNeeded { - [self.lokiLongPoller stopIfNeeded]; + [self.lokiPoller stopIfNeeded]; } - (void)setUpDefaultPublicChatsIfNeeded @@ -1713,7 +1713,7 @@ static BOOL isInternalTestVersion = NO; [SSKEnvironment.shared.messageSenderJobQueue clearAllJobs]; [SSKEnvironment.shared.identityManager clearIdentityKey]; [LKAPI clearRandomSnodePool]; - [self stopLongPollerIfNeeded]; + [self stopPollerIfNeeded]; [self stopOpenGroupPollersIfNeeded]; [self.lokiNewsFeedPoller stop]; [self.lokiMessengerUpdatesFeedPoller stop]; diff --git a/Signal/src/Loki/View Controllers/LandingVC.swift b/Signal/src/Loki/View Controllers/LandingVC.swift index 1607ced83..3b8eda399 100644 --- a/Signal/src/Loki/View Controllers/LandingVC.swift +++ b/Signal/src/Loki/View Controllers/LandingVC.swift @@ -155,7 +155,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey TSAccountManager.sharedInstance().didRegister() let appDelegate = UIApplication.shared.delegate as! AppDelegate - appDelegate.startLongPollerIfNeeded() + appDelegate.startPollerIfNeeded() let deviceLinkingModal = DeviceLinkingModal(mode: .slave, delegate: self) deviceLinkingModal.modalPresentationStyle = .overFullScreen deviceLinkingModal.modalTransitionStyle = .crossDissolve @@ -176,7 +176,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate func handleDeviceLinkingModalDismissed() { let appDelegate = UIApplication.shared.delegate as! AppDelegate - appDelegate.stopLongPollerIfNeeded() + appDelegate.stopPollerIfNeeded() TSAccountManager.sharedInstance().resetForReregistration() } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 6e65a8e0f..80fbf20b2 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -681,7 +681,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) { [SSKEnvironment.shared.identityManager clearIdentityKey]; [LKAPI clearRandomSnodePool]; AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; - [appDelegate stopLongPollerIfNeeded]; + [appDelegate stopPollerIfNeeded]; [appDelegate stopOpenGroupPollersIfNeeded]; [SSKEnvironment.shared.tsAccountManager resetForReregistration]; UIViewController *rootViewController = [[OnboardingController new] initialViewController]; diff --git a/SignalServiceKit/src/Loki/API/LokiLongPoller.swift b/SignalServiceKit/src/Loki/API/LokiPoller.swift similarity index 53% rename from SignalServiceKit/src/Loki/API/LokiLongPoller.swift rename to SignalServiceKit/src/Loki/API/LokiPoller.swift index f2ee4248e..4b18f1945 100644 --- a/SignalServiceKit/src/Loki/API/LokiLongPoller.swift +++ b/SignalServiceKit/src/Loki/API/LokiPoller.swift @@ -1,20 +1,16 @@ import PromiseKit -@objc(LKLongPoller) -public final class LokiLongPoller : NSObject { +@objc(LKPoller) +public final class LokiPoller : NSObject { private let onMessagesReceived: ([SSKProtoEnvelope]) -> Void private let storage = OWSPrimaryStorage.shared() private var hasStarted = false private var hasStopped = false - private var connections = Set>() private var usedSnodes = Set() // MARK: Settings - private let connectionCount = 3 - private let retryInterval: TimeInterval = 4 - - // MARK: Convenience - private var userHexEncodedPublicKey: String { return getUserHexEncodedPublicKey() } + private static let pollInterval: TimeInterval = 1 + private static let retryInterval: TimeInterval = 4 // MARK: Initialization @objc public init(onMessagesReceived: @escaping ([SSKProtoEnvelope]) -> Void) { @@ -25,7 +21,7 @@ public final class LokiLongPoller : NSObject { // MARK: Public API @objc public func startIfNeeded() { guard !hasStarted else { return } - print("[Loki] Started long polling.") + print("[Loki] Started polling.") hasStarted = true hasStopped = false openConnections() @@ -33,7 +29,7 @@ public final class LokiLongPoller : NSObject { @objc public func stopIfNeeded() { guard !hasStopped else { return } - print("[Loki] Stopped long polling.") + print("[Loki] Stopped polling.") hasStarted = false hasStopped = true usedSnodes.removeAll() @@ -42,49 +38,46 @@ public final class LokiLongPoller : NSObject { // MARK: Private API private func openConnections() { guard !hasStopped else { return } - LokiAPI.getSwarm(for: userHexEncodedPublicKey).then { [weak self] _ -> Guarantee<[Result]> in - guard let strongSelf = self else { return Guarantee.value([Result]()) } + LokiAPI.getSwarm(for: getUserHexEncodedPublicKey()).then { [weak self] _ -> Promise in + guard let strongSelf = self else { return Promise { $0.fulfill(()) } } strongSelf.usedSnodes.removeAll() - let connections: [Promise] = (0...pending() - strongSelf.openConnectionToNextSnode(seal: seal) - return promise - } - strongSelf.connections = Set(connections) - return when(resolved: connections) + let (promise, seal) = Promise.pending() + strongSelf.pollNextSnode(seal: seal) + return promise }.ensure { [weak self] in guard let strongSelf = self else { return } - Timer.scheduledTimer(withTimeInterval: strongSelf.retryInterval, repeats: false) { _ in + Timer.scheduledTimer(withTimeInterval: LokiPoller.retryInterval, repeats: false) { _ in guard let strongSelf = self else { return } strongSelf.openConnections() } } } - private func openConnectionToNextSnode(seal: Resolver) { + private func pollNextSnode(seal: Resolver) { + let userHexEncodedPublicKey = getUserHexEncodedPublicKey() let swarm = LokiAPI.swarmCache[userHexEncodedPublicKey] ?? [] - let userHexEncodedPublicKey = self.userHexEncodedPublicKey let unusedSnodes = Set(swarm).subtracting(usedSnodes) if !unusedSnodes.isEmpty { + // randomElement() uses the system's default random generator, which is cryptographically secure let nextSnode = unusedSnodes.randomElement()! usedSnodes.insert(nextSnode) - print("[Loki] Opening long polling connection to \(nextSnode).") - longPoll(nextSnode, seal: seal).catch(on: LokiAPI.errorHandlingQueue) { [weak self] error in - print("[Loki] Long polling connection to \(nextSnode) failed; dropping it and switching to next snode.") + print("[Loki] Polling \(nextSnode).") + poll(nextSnode, seal: seal).catch(on: LokiAPI.errorHandlingQueue) { [weak self] error in + print("[Loki] Polling \(nextSnode) failed; dropping it and switching to next snode.") LokiAPI.dropIfNeeded(nextSnode, hexEncodedPublicKey: userHexEncodedPublicKey) - self?.openConnectionToNextSnode(seal: seal) + self?.pollNextSnode(seal: seal) } } else { seal.fulfill(()) } } - private func longPoll(_ target: LokiAPITarget, seal: Resolver) -> Promise { - return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then(on: DispatchQueue.global()) { [weak self] rawResponse -> Promise in + private func poll(_ target: LokiAPITarget, seal: Resolver) -> Promise { + return LokiAPI.getRawMessages(from: target, usingLongPolling: false).then(on: DispatchQueue.global()) { [weak self] rawResponse -> Promise in guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) } let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target) strongSelf.onMessagesReceived(messages) - return strongSelf.longPoll(target, seal: seal) + return strongSelf.poll(target, seal: seal) } } } From 5b60f925776aca36d0208c6cc0cc058c2ce60c70 Mon Sep 17 00:00:00 2001 From: gmbnt Date: Wed, 25 Mar 2020 10:32:57 +1100 Subject: [PATCH 3/4] Update Pods --- Pods | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pods b/Pods index f636079cb..7d2822b86 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit f636079cbdf8e7a0b959f25917b1bcc0b64b0f0c +Subproject commit 7d2822b86d1ff3bf09eb6cab2f01a03e7e214081 From 4052bcf43a093ea523b78716b40c6ab35ae4623d Mon Sep 17 00:00:00 2001 From: gmbnt Date: Wed, 25 Mar 2020 11:21:23 +1100 Subject: [PATCH 4/4] Make multi device instructions clearer --- Signal/src/Loki/View Controllers/DeviceLinkingModal.swift | 4 ++-- Signal/src/Loki/View Controllers/LinkDeviceVC.swift | 4 ++-- Signal/translations/en.lproj/Localizable.strings | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Signal/src/Loki/View Controllers/DeviceLinkingModal.swift b/Signal/src/Loki/View Controllers/DeviceLinkingModal.swift index 79bdfa64f..4865ac877 100644 --- a/Signal/src/Loki/View Controllers/DeviceLinkingModal.swift +++ b/Signal/src/Loki/View Controllers/DeviceLinkingModal.swift @@ -42,7 +42,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate { private lazy var subtitleLabel: UILabel = { let result = UILabel() - result.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + result.textColor = Colors.text result.font = .systemFont(ofSize: Values.smallFontSize) result.numberOfLines = 0 result.lineBreakMode = .byWordWrapping @@ -131,7 +131,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate { }() subtitleLabel.text = { switch mode { - case .master: return NSLocalizedString("Open Session on your secondary device and tap \"Link to an existing account\"", comment: "") + case .master: return NSLocalizedString("Download Session on your other device and tap \"Link to an existing account\" at the bottom of the landing screen. If you have an existing account on your other device already you will have to delete that account first.", comment: "") case .slave: return NSLocalizedString("Please check that the words below match those shown on your other device", comment: "") } }() diff --git a/Signal/src/Loki/View Controllers/LinkDeviceVC.swift b/Signal/src/Loki/View Controllers/LinkDeviceVC.swift index 730a8f33c..5033cfe33 100644 --- a/Signal/src/Loki/View Controllers/LinkDeviceVC.swift +++ b/Signal/src/Loki/View Controllers/LinkDeviceVC.swift @@ -33,7 +33,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon }() private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = { - let message = NSLocalizedString("Link to your existing account by going into your in-app settings and clicking \"Devices\".", comment: "") + let message = NSLocalizedString("Navigate to \"Settings\" > \"Devices\" > \"Link a Device\" on your other device and then scan the QR code that comes up to start the linking process.", comment: "") let result = ScanQRCodeWrapperVC(message: message) result.delegate = self return result @@ -167,7 +167,7 @@ private final class EnterPublicKeyVC : UIViewController { let explanationLabel = UILabel() explanationLabel.textColor = Colors.text explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) - explanationLabel.text = "Enter your Session ID to start the linking process." + explanationLabel.text = "Navigate to \"Settings\" > \"Devices\" > \"Link a Device\" on your other device and then enter your Session ID here to start the linking process." explanationLabel.numberOfLines = 0 explanationLabel.lineBreakMode = .byWordWrapping // Link button diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index e8284a5c9..0b1ed26ec 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2768,7 +2768,7 @@ "Link to an existing account" = "Link to an existing account"; "Enter your public key" = "Enter your public key"; "Link to your existing account by going into your in-app settings and clicking \"Devices\"." = "Link to your existing account by going into your in-app settings and clicking \"Devices\"."; -"Open Session on your secondary device and tap \"Link to an existing account\"" = "Open Session on your secondary device and tap \"Link to an existing account\""; +"Download Session on your other device and tap \"Link to an existing account\" at the bottom of the landing screen. If you have an existing account on your other device already you will have to delete that account first." = "Download Session on your other device and tap \"Link to an existing account\" at the bottom of the landing screen. If you have an existing account on your other device already you will have to delete that account first."; "Group Settings" = "Group Settings"; "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design." = "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design."; "Enter the recovery phrase that was given to you when you signed up to restore your account." = "Enter the recovery phrase that was given to you when you signed up to restore your account.";