Merge branch 'dev' into notifications

This commit is contained in:
nielsandriesse 2020-07-29 11:51:24 +10:00
commit 72858dbaf6
64 changed files with 2403 additions and 1413 deletions

12
Podfile
View File

@ -105,9 +105,21 @@ target 'SignalMessaging' do
end
post_install do |installer|
enable_whole_module_optimization_for_cryptoswift(installer)
enable_extension_support_for_purelayout(installer)
end
def enable_whole_module_optimization_for_cryptoswift(installer)
installer.pods_project.targets.each do |target|
if target.name.end_with? "CryptoSwift"
target.build_configurations.each do |config|
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 'fast'
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-O'
end
end
end
end
# PureLayout by default makes use of UIApplication, and must be configured to be built for an extension.
def enable_extension_support_for_purelayout(installer)
installer.pods_project.targets.each do |target|

View File

@ -325,6 +325,6 @@ SPEC CHECKSUMS:
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
PODFILE CHECKSUM: 5963da3d09b1f6e9aee15d9e684901b350dce6ec
PODFILE CHECKSUM: b4f88816a817cc27f499940c644c0449ef5d7cc7
COCOAPODS: 1.9.3

2
Pods

@ -1 +1 @@
Subproject commit 8f3d6d46718795227074d6545776206cf18c0244
Subproject commit d1322a0a5e38a04b7c9fb57c847ea0f2eb7c13c3

View File

@ -519,7 +519,6 @@
B80C6B572384A56D00FDBC8B /* DeviceLinksVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */; };
B80C6B592384C4E700FDBC8B /* DeviceNameModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */; };
B80C6B5B2384C7F900FDBC8B /* DeviceNameModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */; };
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */; };
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40872399EB0E00A248E7 /* LandingVC.swift */; };
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B40892399EC0600A248E7 /* FakeChatView.swift */; };
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; };
@ -1340,7 +1339,6 @@
B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinksVC.swift; sourceTree = "<group>"; };
B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModal.swift; sourceTree = "<group>"; };
B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceNameModalDelegate.swift; sourceTree = "<group>"; };
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiRSSFeedPoller.swift; sourceTree = "<group>"; };
B82B40872399EB0E00A248E7 /* LandingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingVC.swift; sourceTree = "<group>"; };
B82B40892399EC0600A248E7 /* FakeChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeChatView.swift; sourceTree = "<group>"; };
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
@ -1410,6 +1408,7 @@
C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeoLite2-Country-Blocks-IPv4.csv"; sourceTree = "<group>"; };
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP2Country.swift; sourceTree = "<group>"; };
C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LK002RemoveFriendRequests.swift; sourceTree = "<group>"; };
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = "<group>"; };
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = "<group>"; };
C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = "<group>"; };
C3DFFAC723E970080058DAF8 /* OpenGroupSuggestionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSuggestionSheet.swift; sourceTree = "<group>"; };
@ -2607,6 +2606,7 @@
isa = PBXGroup;
children = (
B6F509951AA53F760068F56A /* Localizable.strings */,
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */,
);
name = Translations;
sourceTree = "<group>";
@ -2616,7 +2616,6 @@
children = (
B8CCF63B239757C10091D419 /* Components */,
C32B405424A961E1001117B5 /* Dependencies */,
B8BFFF392355426100102A27 /* Shelved */,
B8CCF63C239757DB0091D419 /* Utilities */,
B8CCF63D2397580E0091D419 /* View Controllers */,
);
@ -2664,14 +2663,6 @@
path = Loki;
sourceTree = "<group>";
};
B8BFFF392355426100102A27 /* Shelved */ = {
isa = PBXGroup;
children = (
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */,
);
path = Shelved;
sourceTree = "<group>";
};
B8C9689223FA1B05005F64E0 /* Redesign */ = {
isa = PBXGroup;
children = (
@ -3895,7 +3886,6 @@
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */,
C353F8F7244808E90011121A /* PNModeSheet.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,

View File

@ -239,8 +239,7 @@
debugDocumentVersioning = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
showNonLocalizedStrings = "YES">
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View File

@ -0,0 +1,16 @@
* Replace <string name=
* Replace “> → “ = “
* Replace </string> → “;
* Replace activity_ → vc_
* Replace fragment_ → vc_
* Replace dialog_ → modal_
* Replace Firebase Cloud Messaging → Apple Push Notification Service
* Replace fcm_ → apns_
* Replace FCM → APNs
* Replace &gt; → >
* Remove any \
* Check for any unescaped “
* Replace %1$s → %@
* Replace %s → %@
* Replace edit_text → text_field
* Replace Google → Apple

View File

@ -212,7 +212,7 @@ final class ConversationCell : UITableViewCell {
}
} else {
if threadViewModel.threadRecord.isNoteToSelf() {
return NSLocalizedString("Note to Self", comment: "")
return NSLocalizedString("NOTE_TO_SELF", comment: "")
} else {
let hexEncodedPublicKey = threadViewModel.contactIdentifier!
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey

View File

@ -8,11 +8,11 @@ final class FakeChatView : UIView {
}
private lazy var chatBubbles = [
getChatBubble(withText: NSLocalizedString("What's Session?", comment: ""), wasSentByCurrentUser: true),
getChatBubble(withText: NSLocalizedString("It's a decentralized, encrypted messaging app.", comment: ""), wasSentByCurrentUser: false),
getChatBubble(withText: NSLocalizedString("So it doesn't collect my personal information or my conversation metadata? How does it work?", comment: ""), wasSentByCurrentUser: true),
getChatBubble(withText: NSLocalizedString("Using a combination of advanced anonymous routing and end-to-end encryption technologies.", comment: ""), wasSentByCurrentUser: false),
getChatBubble(withText: NSLocalizedString("Friends don't let friends use compromised messengers. You're welcome.", comment: ""), wasSentByCurrentUser: false)
getChatBubble(withText: NSLocalizedString("view_fake_chat_bubble_1", comment: ""), wasSentByCurrentUser: true),
getChatBubble(withText: NSLocalizedString("view_fake_chat_bubble_2", comment: ""), wasSentByCurrentUser: false),
getChatBubble(withText: NSLocalizedString("view_fake_chat_bubble_3", comment: ""), wasSentByCurrentUser: true),
getChatBubble(withText: NSLocalizedString("view_fake_chat_bubble_4", comment: ""), wasSentByCurrentUser: false),
getChatBubble(withText: NSLocalizedString("view_fake_chat_bubble_5", comment: ""), wasSentByCurrentUser: false)
]
private lazy var scrollView: UIScrollView = {

View File

@ -64,7 +64,7 @@ final class OptionView : UIView {
let recommendedLabel = UILabel()
recommendedLabel.textColor = Colors.accent
recommendedLabel.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
recommendedLabel.text = NSLocalizedString("Recommended", comment: "")
recommendedLabel.text = NSLocalizedString("vc_pn_mode_recommended_option_tag", comment: "")
stackView.addArrangedSubview(recommendedLabel)
}
// Set up tap gesture recognizer

View File

@ -56,7 +56,7 @@ final class SeedReminderView : UIView {
labelStackView.spacing = 4
// Set up button
let button = Button(style: .prominentOutline, size: .small)
button.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
button.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
button.set(.width, to: 80)
button.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
// Set up content stack view
@ -68,7 +68,8 @@ final class SeedReminderView : UIView {
contentStackView.axis = .horizontal
contentStackView.spacing = 4
contentStackView.alignment = .center
contentStackView.layoutMargins = UIEdgeInsets(top: 0, leading: Values.mediumSpacing + Values.accentLineThickness, bottom: 0, trailing: Values.mediumSpacing)
let horizontalSpacing = isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing
contentStackView.layoutMargins = UIEdgeInsets(top: 0, leading: horizontalSpacing + Values.accentLineThickness, bottom: 0, trailing: horizontalSpacing)
contentStackView.isLayoutMarginsRelativeArrangement = true
// Set up separator
let separator = UIView()
@ -77,7 +78,7 @@ final class SeedReminderView : UIView {
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ progressIndicatorView, contentStackView, separator ])
stackView.axis = .vertical
stackView.spacing = Values.mediumSpacing
stackView.spacing = isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing
addSubview(stackView)
stackView.pin(to: self)
}

View File

@ -29,7 +29,7 @@ final class SessionRestorationView : UIView {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Session Out of Sync", comment: "")
titleLabel.text = "Session Out of Sync"
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
@ -37,7 +37,7 @@ final class SessionRestorationView : UIView {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Would you like to restore your session? This can help resolve issues. Your messages will be preserved.", comment: "")
explanationLabel.text = "Would you like to restore your session? This can help resolve issues. Your messages will be preserved."
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -48,7 +48,7 @@ final class SessionRestorationView : UIView {
restoreButton.backgroundColor = Colors.accent
restoreButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
restoreButton.setTitleColor(Colors.text, for: UIControl.State.normal)
restoreButton.setTitle(NSLocalizedString("Restore", comment: ""), for: UIControl.State.normal)
restoreButton.setTitle(NSLocalizedString("session_reset_banner_restore_button_title", comment: ""), for: UIControl.State.normal)
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
// Set up dismiss button
let dismissButton = UIButton()
@ -57,7 +57,7 @@ final class SessionRestorationView : UIView {
dismissButton.backgroundColor = Colors.buttonBackground
dismissButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
dismissButton.setTitleColor(Colors.text, for: UIControl.State.normal)
dismissButton.setTitle(NSLocalizedString("Dismiss", comment: ""), for: UIControl.State.normal)
dismissButton.setTitle(NSLocalizedString("session_reset_banner_dismiss_button_title", comment: ""), for: UIControl.State.normal)
dismissButton.addTarget(self, action: #selector(dismiss), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ dismissButton, restoreButton ])
@ -73,7 +73,7 @@ final class SessionRestorationView : UIView {
// Update explanation label if possible
if let contactID = thread.contactIdentifier() {
let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: contactID) ?? contactID
explanationLabel.text = String(format: NSLocalizedString("Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved.", comment: ""), displayName)
explanationLabel.text = String(format: "Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved.", displayName)
}
}

View File

@ -1,73 +0,0 @@
import FeedKit
@objc(LKRSSFeedPoller)
public final class LokiRSSFeedPoller : NSObject {
private let feed: LokiRSSFeed
private var timer: Timer? = nil
private var hasStarted = false
private let interval: TimeInterval = 8 * 60
@objc(initForFeed:)
public init(for feed: LokiRSSFeed) {
self.feed = feed
super.init()
}
@objc public func startIfNeeded() {
if hasStarted { return }
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in self?.poll() }
poll() // Perform initial update
hasStarted = true
}
@objc public func stop() {
timer?.invalidate()
hasStarted = false
}
private func poll() {
let feed = self.feed
let url = feed.server
let _ = LokiRSSFeedProxy.fetchContent(for: url).done { xml in
guard let data = xml.data(using: String.Encoding.utf8) else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") }
FeedParser(data: data).parseAsync { wrapper in
guard case .rss(let x) = wrapper, let items = x.items else { return print("[Loki] Failed to parse RSS feed for: \(feed.server).") }
items.reversed().forEach { item in
guard let title = item.title, let description = item.description, let date = item.pubDate else { return }
let timestamp = UInt64(date.timeIntervalSince1970 * 1000)
let urlRegex = try! NSRegularExpression(pattern: "<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>")
var bodyAsHTML = "\(title)<br><br>\(description)".replacingOccurrences(of: "</p>", with: "</p><br>")
while true {
guard let match = urlRegex.firstMatch(in: bodyAsHTML, options: [], range: NSRange(location: 0, length: bodyAsHTML.utf16.count)) else { break }
let matchRange = match.range(at: 0)
let urlRange = match.range(at: 1)
let descriptionRange = match.range(at: 2)
let url = (bodyAsHTML as NSString).substring(with: urlRange)
let description = (bodyAsHTML as NSString).substring(with: descriptionRange)
bodyAsHTML = (bodyAsHTML as NSString).replacingCharacters(in: matchRange, with: "\(description) (\(url))") as String
}
guard let bodyAsData = bodyAsHTML.data(using: String.Encoding.unicode) else { return }
let options = [ NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html ]
guard let body = try? NSAttributedString(data: bodyAsData, options: options, documentAttributes: nil).string else { return }
let id = LKGroupUtilities.getEncodedRSSFeedIDAsData(feed.id)
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
groupContext.setName(feed.displayName)
let dataMessage = SSKProtoDataMessage.builder()
dataMessage.setTimestamp(timestamp)
dataMessage.setGroup(try! groupContext.build())
dataMessage.setBody(body)
let content = SSKProtoContent.builder()
content.setDataMessage(try! dataMessage.build())
let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: timestamp)
envelope.setSource(NSLocalizedString("Loki", comment: ""))
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
envelope.setContent(try! content.build().serializedData())
try! Storage.writeSync { transaction in
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: 0)
}
}
}
}
}
}

View File

@ -72,7 +72,7 @@ class BaseVC : UIViewController {
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))
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}

View File

@ -76,7 +76,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
result.backgroundColor = Colors.accent
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
result.setTitleColor(Colors.text, for: UIControl.State.normal)
result.setTitle(NSLocalizedString("Authorize", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("modal_link_device_master_mode_authorize_button_title", comment: ""), for: UIControl.State.normal)
return result
}()
@ -130,14 +130,14 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
}
titleLabel.text = {
switch mode {
case .master: return NSLocalizedString("Waiting for Device", comment: "")
case .slave: return NSLocalizedString("Waiting for Authorization", comment: "")
case .master: return NSLocalizedString("modal_link_device_master_mode_title_1", comment: "")
case .slave: return NSLocalizedString("modal_link_device_slave_mode_title_1", comment: "")
}
}()
subtitleLabel.text = {
switch mode {
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: "")
case .master: return NSLocalizedString("modal_link_device_master_mode_explanation_1", comment: "")
case .slave: return NSLocalizedString("modal_link_device_slave_mode_explanation_1", comment: "")
}
}()
mnemonicLabel.isHidden = (mode == .master)
@ -157,8 +157,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
func requestUserAuthorization(for deviceLink: DeviceLink) {
self.deviceLink = deviceLink
qrCodeImageViewContainer.isHidden = true
titleLabel.text = NSLocalizedString("Linking Request Received", comment: "")
subtitleLabel.text = NSLocalizedString("Please check that the words below match those shown on your other device", comment: "")
titleLabel.text = NSLocalizedString("modal_link_device_master_mode_title_2", comment: "")
subtitleLabel.text = NSLocalizedString("modal_link_device_master_mode_explanation_2", comment: "")
let hexEncodedPublicKey = deviceLink.slave.publicKey.removing05PrefixIfNeeded()
mnemonicLabel.text = Mnemonic.hash(hexEncodedString: hexEncodedPublicKey)
mnemonicLabel.isHidden = false
@ -172,8 +172,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
mainStackView.insertArrangedSubview(spinner, at: 0)
spinner.set(.height, to: 64)
spinner.startAnimating()
titleLabel.text = NSLocalizedString("Authorizing Device Link", comment: "")
subtitleLabel.text = NSLocalizedString("Please wait while the device link is created. This can take up to a minute.", comment: "")
titleLabel.text = NSLocalizedString("modal_link_device_master_mode_title_3", comment: "")
subtitleLabel.text = NSLocalizedString("modal_link_device_master_mode_explanation_3", comment: "")
mnemonicLabel.isHidden = true
buttonStackView.isHidden = true
let deviceLink = self.deviceLink!
@ -204,7 +204,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
let _ = FileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
DispatchQueue.main.async {
self?.close()
let alert = UIAlertController(title: NSLocalizedString("Device Linking Failed", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: "Device Linking Failed", message: "Please check your internet connection and try again", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.presentingViewController?.present(alert, animated: true, completion: nil)
}
@ -213,7 +213,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
print("[Loki] Failed to add device link due to error: \(error).")
DispatchQueue.main.async {
self?.close() // TODO: Show a message to the user
let alert = UIAlertController(title: NSLocalizedString("Device Linking Failed", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: "Device Linking Failed", message: "Please check your internet connection and try again", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.presentingViewController?.present(alert, animated: true, completion: nil)
}
@ -225,8 +225,8 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
session.stopListeningForLinkingAuthorization()
spinner.stopAnimating()
spinner.isHidden = true
titleLabel.text = NSLocalizedString("Device Link Authorized", comment: "")
subtitleLabel.text = NSLocalizedString("Your device has been linked successfully", comment: "")
titleLabel.text = NSLocalizedString("modal_link_device_slave_mode_title_2", comment: "")
subtitleLabel.text = NSLocalizedString("modal_link_device_slave_mode_explanation_2", comment: "")
mnemonicLabel.isHidden = true
buttonStackView.isHidden = true
FileServerAPI.addDeviceLink(deviceLink).catch { error in

View File

@ -23,11 +23,11 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
explanationLabel.text = NSLocalizedString("You haven't linked any devices yet", comment: "")
explanationLabel.text = NSLocalizedString("vc_linked_devices_empty_state_message", comment: "")
let linkNewDeviceButton = Button(style: .prominentOutline, size: .large)
linkNewDeviceButton.setTitle(NSLocalizedString("Link a Device (Beta)", comment: ""), for: UIControl.State.normal)
linkNewDeviceButton.setTitle(NSLocalizedString("vc_linked_devices_empty_state_button_title", comment: ""), for: UIControl.State.normal)
linkNewDeviceButton.addTarget(self, action: #selector(linkNewDevice), for: UIControl.Event.touchUpInside)
linkNewDeviceButton.set(.width, to: 180)
linkNewDeviceButton.set(.width, to: 196)
let result = UIStackView(arrangedSubviews: [ explanationLabel, linkNewDeviceButton ])
result.axis = .vertical
result.spacing = Values.mediumSpacing
@ -40,7 +40,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Devices", comment: ""))
setNavBarTitle(NSLocalizedString("vc_linked_devices_title", comment: ""))
// Set up link new device button
let linkNewDeviceButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(linkNewDevice))
linkNewDeviceButton.tintColor = Colors.text
@ -108,7 +108,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
deviceLinkingModal.modalTransitionStyle = .crossDissolve
present(deviceLinkingModal, animated: true, completion: nil)
} else {
let alert = UIAlertController(title: NSLocalizedString("Multi Device Limit Reached", comment: ""), message: NSLocalizedString("It's currently not allowed to link more than one device.", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: NSLocalizedString("vc_linked_devices_multi_device_limit_reached_modal_title", comment: ""), message: NSLocalizedString("It's currently not allowed to link more than one device.", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
@ -118,7 +118,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
defer { tableView.deselectRow(at: indexPath, animated: true) }
let deviceLink = deviceLinks[indexPath.row]
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
sheet.addAction(UIAlertAction(title: NSLocalizedString("Change Name", comment: ""), style: .default) { [weak self] _ in
sheet.addAction(UIAlertAction(title: NSLocalizedString("vc_device_list_bottom_sheet_change_name_button_title", comment: ""), style: .default) { [weak self] _ in
guard let self = self else { return }
let deviceNameModal = DeviceNameModal()
deviceNameModal.device = deviceLink.other
@ -127,10 +127,10 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
deviceNameModal.modalTransitionStyle = .crossDissolve
self.present(deviceNameModal, animated: true, completion: nil)
})
sheet.addAction(UIAlertAction(title: NSLocalizedString("Unlink", comment: ""), style: .destructive) { [weak self] _ in
sheet.addAction(UIAlertAction(title: NSLocalizedString("vc_device_list_bottom_sheet_unlink_device_button_title", comment: ""), style: .destructive) { [weak self] _ in
self?.removeDeviceLink(deviceLink)
})
sheet.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in })
sheet.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in })
present(sheet, animated: true, completion: nil)
}
@ -166,7 +166,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
})
self?.updateDeviceLinks()
}.catch { [weak self] _ in
let alert = UIAlertController(title: NSLocalizedString("Couldn't Unlink Device", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: "Couldn't Unlink Device", message: "Please check your internet connection and try again", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}

View File

@ -10,7 +10,7 @@ final class DeviceNameModal : Modal {
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.textAlignment = .center
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a Name", comment: ""))
let placeholder = NSMutableAttributedString(string: NSLocalizedString("modal_edit_device_name_text_field_hint", comment: ""))
placeholder.addAttribute(.foregroundColor, value: Colors.text.withAlphaComponent(Values.unimportantElementOpacity), range: NSRange(location: 0, length: placeholder.length))
result.attributedPlaceholder = placeholder
result.tintColor = Colors.accent
@ -31,7 +31,7 @@ final class DeviceNameModal : Modal {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Change Device Name", comment: "")
titleLabel.text = "Change Device Name"
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
@ -39,7 +39,7 @@ final class DeviceNameModal : Modal {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Enter the new display name for your device below", comment: "")
explanationLabel.text = "Enter the new display name for your device below"
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -95,7 +95,7 @@ final class DeviceNameModal : Modal {
UserDefaults.standard[.slaveDeviceName(device.publicKey)] = name
delegate?.handleDeviceNameChanged(to: name, for: device)
} else {
let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), message: NSLocalizedString("Please pick a name", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: "Error", message: "Please pick a name", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}

View File

@ -7,7 +7,7 @@ final class DisplayNameVC : BaseVC {
// MARK: Components
private lazy var displayNameTextField: TextField = {
let result = TextField(placeholder: NSLocalizedString("Enter a display name", comment: ""))
let result = TextField(placeholder: NSLocalizedString("vc_display_name_text_field_hint", comment: ""))
result.layer.borderColor = Colors.text.cgColor
return result
}()
@ -22,14 +22,14 @@ final class DisplayNameVC : BaseVC {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Pick your display name", comment: "")
titleLabel.text = NSLocalizedString("vc_display_name_title_2", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = "This will be your name when you use Session. It can be your real name, an alias, or anything else you like."
explanationLabel.text = NSLocalizedString("vc_display_name_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Set up spacers
@ -43,7 +43,7 @@ final class DisplayNameVC : BaseVC {
registerButtonBottomOffsetConstraint = registerButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
// Set up register button
let registerButton = Button(style: .prominentFilled, size: .large)
registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
registerButton.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
// Set up register button container
@ -128,15 +128,15 @@ final class DisplayNameVC : BaseVC {
}
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard !displayName.isEmpty else {
return showError(title: NSLocalizedString("Please pick a display name", comment: ""))
return showError(title: NSLocalizedString("vc_display_name_display_name_missing_error", comment: ""))
}
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ")
let hasInvalidCharacters = !displayName.allSatisfy { $0.unicodeScalars.allSatisfy { allowedCharacters.contains($0) } }
guard !hasInvalidCharacters else {
return showError(title: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
return showError(title: NSLocalizedString("vc_display_name_display_name_invalid_error", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
return showError(title: NSLocalizedString("vc_display_name_display_name_too_long_error", comment: ""))
}
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { _ in }, requiresSync: false) // Try to save the user name but ignore the result
let pnModeVC = PNModeVC()

View File

@ -35,12 +35,12 @@ final class GroupMembersVC : BaseVC, UITableViewDataSource {
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Group Members", comment: ""))
setNavBarTitle("Group Members")
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("The ability to add members to a closed group is coming soon.", comment: "")
explanationLabel.text = "The ability to add members to a closed group is coming soon."
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping

View File

@ -30,7 +30,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
let attributedTitle = NSMutableAttributedString(string: title)
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "80%"))
result.title = attributedTitle
result.subtitle = NSLocalizedString("Secure your account by saving your recovery phrase", comment: "")
result.subtitle = NSLocalizedString("view_seed_reminder_subtitle_1", comment: "")
result.setProgress(0.8, animated: false)
result.delegate = self
return result
@ -70,11 +70,11 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
explanationLabel.text = NSLocalizedString("You don't have any contacts yet", comment: "")
explanationLabel.text = NSLocalizedString("vc_home_empty_state_message", comment: "")
let createNewPrivateChatButton = Button(style: .prominentOutline, size: .large)
createNewPrivateChatButton.setTitle(NSLocalizedString("Start a Session", comment: ""), for: UIControl.State.normal)
createNewPrivateChatButton.setTitle(NSLocalizedString("vc_home_empty_state_button_title", comment: ""), for: UIControl.State.normal)
createNewPrivateChatButton.addTarget(self, action: #selector(createNewPrivateChat), for: UIControl.Event.touchUpInside)
createNewPrivateChatButton.set(.width, to: 180)
createNewPrivateChatButton.set(.width, to: 196)
let result = UIStackView(arrangedSubviews: [ explanationLabel, createNewPrivateChatButton ])
result.axis = .vertical
result.spacing = Values.mediumSpacing
@ -91,7 +91,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
setUpNavBarStyle()
}
updateNavigationBarButtons()
setNavBarTitle(NSLocalizedString("Messages", comment: ""))
setNavBarTitle("Messages")
// Set up seed reminder view if needed
let userDefaults = UserDefaults.standard
let hasViewedSeed = userDefaults[.hasViewedSeed]
@ -392,7 +392,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
publicChat = LokiDatabaseUtilities.getPublicChat(for: thread.uniqueId!, in: transaction)
}
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { [weak self] _, _ in
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
try! Storage.writeSync { transaction in

View File

@ -8,11 +8,11 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("Open Group URL", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_join_public_chat_enter_group_url_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
},
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_join_public_chat_scan_qr_code_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
}
@ -33,7 +33,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = NSLocalizedString("Scan the QR code of the open group you'd like to join", comment: "")
let message = NSLocalizedString("vc_join_public_chat_scan_qr_code_explanation", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
@ -44,7 +44,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Join Open Group", comment: ""))
setNavBarTitle(NSLocalizedString("vc_join_public_chat_title", comment: ""))
let navigationBar = navigationController!.navigationBar
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
@ -128,7 +128,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
fileprivate func joinPublicChatIfPossible(with chatURL: String) {
guard !isJoining else { return }
guard let url = URL(string: chatURL), let scheme = url.scheme, scheme == "https", url.host != nil else {
return showError(title: NSLocalizedString("Invalid URL", comment: ""), message: NSLocalizedString("Please check the URL you entered and try again", comment: ""))
return showError(title: NSLocalizedString("invalid_url", comment: ""), message: "Please check the URL you entered and try again")
}
isJoining = true
let channelID: UInt64 = 1
@ -142,24 +142,27 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
transaction.removeObject(forKey: "\(urlAsString).\(channelID)", inCollection: PublicChatAPI.lastMessageServerIDCollection)
transaction.removeObject(forKey: "\(urlAsString).\(channelID)", inCollection: PublicChatAPI.lastDeletionServerIDCollection)
}
PublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
.done(on: .main) { [weak self] _ in
let _ = PublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
let _ = PublicChatAPI.setProfilePictureURL(to: profilePictureURL, using: profileKey, on: urlAsString)
let _ = PublicChatAPI.join(channelID, on: urlAsString)
let syncManager = SSKEnvironment.shared.syncManager
let _ = syncManager.syncAllOpenGroups()
self?.presentingViewController!.dismiss(animated: true, completion: nil)
}
.catch(on: .main) { [weak self] error in
var title = NSLocalizedString("Couldn't Join", comment: "")
var message = ""
if case LokiHTTPClient.HTTPError.networkError(let statusCode, _, _) = error, (statusCode == 401 || statusCode == 403) {
title = NSLocalizedString("Unauthorized", comment: "")
message = NSLocalizedString("Please ask the open group operator to add you to the group.", comment: "")
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
PublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
.done(on: DispatchQueue.main) { [weak self] _ in
let _ = PublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
let _ = PublicChatAPI.setProfilePictureURL(to: profilePictureURL, using: profileKey, on: urlAsString)
let _ = PublicChatAPI.join(channelID, on: urlAsString)
let syncManager = SSKEnvironment.shared.syncManager
let _ = syncManager.syncAllOpenGroups()
self?.presentingViewController!.dismiss(animated: true, completion: nil)
}
.catch(on: DispatchQueue.main) { [weak self] error in
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
var title = "Couldn't Join"
var message = ""
if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
title = "Unauthorized"
message = "Please ask the open group operator to add you to the group."
}
self?.isJoining = false
self?.showError(title: title, message: message)
}
self?.isJoining = false
self?.showError(title: title, message: message)
}
}
@ -177,7 +180,7 @@ private final class EnterChatURLVC : UIViewController {
// MARK: Components
private lazy var chatURLTextField: TextField = {
let result = TextField(placeholder: "Enter an open group URL")
let result = TextField(placeholder: NSLocalizedString("vc_enter_chat_url_text_field_hint", comment: ""))
result.keyboardType = .URL
result.autocapitalizationType = .none
return result
@ -187,17 +190,9 @@ private final class EnterChatURLVC : UIViewController {
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
explanationLabel.text = NSLocalizedString("Open groups can be joined by anyone and do not provide full privacy protection", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Next button
let nextButton = Button(style: .prominentOutline, size: .large)
nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal)
nextButton.setTitle(NSLocalizedString("next", comment: ""), for: UIControl.State.normal)
nextButton.addTarget(self, action: #selector(joinPublicChatIfPossible), for: UIControl.Event.touchUpInside)
let nextButtonContainer = UIView()
nextButtonContainer.addSubview(nextButton)
@ -206,11 +201,10 @@ private final class EnterChatURLVC : UIViewController {
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ chatURLTextField, UIView.vStretchingSpacer(), nextButtonContainer, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel ])
let stackView = UIStackView(arrangedSubviews: [ chatURLTextField, UIView.vStretchingSpacer(), nextButtonContainer ])
stackView.axis = .vertical
stackView.alignment = .fill
let bottomSpacing = isIPhone5OrSmaller ? Values.smallSpacing : Values.largeSpacing
stackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: bottomSpacing, right: Values.largeSpacing)
stackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: view)
@ -277,7 +271,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Session needs camera access to scan QR codes", comment: "")
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -285,7 +279,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])

View File

@ -11,7 +11,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
private lazy var registerButton: Button = {
let result = Button(style: .prominentFilled, size: .large)
result.setTitle(NSLocalizedString("Create Session ID", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("vc_landing_register_button_title", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
return result
@ -19,7 +19,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
private lazy var restoreButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Continue your Session", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("vc_landing_restore_button_title", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
return result
@ -27,7 +27,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
private lazy var linkButton: Button = {
let result = Button(style: .regularBorderless, size: .small)
result.setTitle(NSLocalizedString("Link to an existing account", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("vc_landing_link_button_title", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
result.addTarget(self, action: #selector(linkDevice), for: UIControl.Event.touchUpInside)
return result
@ -43,7 +43,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Your Session begins here...", comment: "")
titleLabel.text = NSLocalizedString("vc_landing_title_2", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up title label container
@ -72,9 +72,9 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
// Set up button stack view container
let buttonStackViewContainer = UIView()
buttonStackViewContainer.addSubview(buttonStackView)
buttonStackView.pin(.leading, to: .leading, of: buttonStackViewContainer, withInset: Values.massiveSpacing)
buttonStackView.pin(.leading, to: .leading, of: buttonStackViewContainer, withInset: isIPhone5OrSmaller ? CGFloat(52) : Values.massiveSpacing)
buttonStackView.pin(.top, to: .top, of: buttonStackViewContainer)
buttonStackViewContainer.pin(.trailing, to: .trailing, of: buttonStackView, withInset: Values.massiveSpacing)
buttonStackViewContainer.pin(.trailing, to: .trailing, of: buttonStackView, withInset: isIPhone5OrSmaller ? CGFloat(52) : Values.massiveSpacing)
buttonStackViewContainer.pin(.bottom, to: .bottom, of: buttonStackView)
// Set up main stack view
let mainStackView = UIStackView(arrangedSubviews: [ topSpacer, titleLabelContainer, UIView.spacer(withHeight: isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing), fakeChatView, bottomSpacer, buttonStackViewContainer, linkButtonContainer ])
@ -85,7 +85,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
// Show device unlinked alert if needed
if UserDefaults.standard[.wasUnlinked] {
let alert = UIAlertController(title: NSLocalizedString("Device Unlinked", comment: ""), message: NSLocalizedString("Your device was unlinked successfully", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: "Device Unlinked", message: NSLocalizedString("vc_landing_device_unlinked_dialog_title", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
present(alert, animated: true, completion: nil)
UserDefaults.removeAll()
@ -128,7 +128,7 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
// MARK: Device Linking
func requestDeviceLink(with hexEncodedPublicKey: String) {
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
let alert = UIAlertController(title: NSLocalizedString("Invalid Session ID", comment: ""), message: NSLocalizedString("Please make sure the Session ID you entered is correct and try again.", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: "Please make sure the Session ID you entered is correct and try again.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
return present(alert, animated: true, completion: nil)
}

View File

@ -8,11 +8,11 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("Enter Session ID", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_link_device_enter_session_id_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
},
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_link_device_scan_qr_code_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
}
@ -33,7 +33,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
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 message = NSLocalizedString("vc_link_device_scan_qr_code_explanation", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
@ -44,7 +44,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Link Device", comment: ""))
setNavBarTitle(NSLocalizedString("vc_link_device_title", comment: ""))
let navigationBar = navigationController!.navigationBar
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
@ -143,9 +143,9 @@ private final class EnterPublicKeyVC : UIViewController {
// MARK: Components
private lazy var publicKeyTextField: TextField = {
if isIPhone6OrSmaller {
return TextField(placeholder: NSLocalizedString("Enter your Session ID", comment: ""), customHeight: 56, customVerticalInset: 12)
return TextField(placeholder: NSLocalizedString("vc_enter_session_id_text_field_hint", comment: ""), customHeight: 56, customVerticalInset: 12)
} else {
return TextField(placeholder: NSLocalizedString("Enter your Session ID", comment: ""))
return TextField(placeholder: NSLocalizedString("vc_enter_session_id_text_field_hint", comment: ""))
}
}()
@ -157,19 +157,19 @@ private final class EnterPublicKeyVC : UIViewController {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone6OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Link your device", comment: "")
titleLabel.text = NSLocalizedString("vc_enter_session_id_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
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.text = NSLocalizedString("vc_enter_session_id_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Link button
let linkButton = Button(style: .prominentOutline, size: .large)
linkButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
linkButton.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
linkButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
linkButton.addTarget(self, action: #selector(requestDeviceLink), for: UIControl.Event.touchUpInside)
let linkButtonContainer = UIView()
@ -270,7 +270,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Session needs camera access to scan QR codes", comment: "")
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -278,7 +278,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])

View File

@ -24,7 +24,7 @@ class Modal : BaseVC {
result.backgroundColor = Colors.buttonBackground
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
result.setTitleColor(Colors.text, for: UIControl.State.normal)
result.setTitle(NSLocalizedString("Cancel", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("cancel", comment: ""), for: UIControl.State.normal)
return result
}()

View File

@ -30,7 +30,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
}()
// MARK: Components
private lazy var nameTextField = TextField(placeholder: NSLocalizedString("Enter a group name", comment: ""))
private lazy var nameTextField = TextField(placeholder: NSLocalizedString("vc_create_closed_group_text_field_hint", comment: ""))
private lazy var tableView: UITableView = {
let result = UITableView()
@ -49,7 +49,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
setUpGradientBackground()
setUpNavBarStyle()
let customTitleFontSize = Values.largeFontSize
setNavBarTitle(NSLocalizedString("New Closed Group", comment: ""), customFontSize: customTitleFontSize)
setNavBarTitle(NSLocalizedString("vc_create_closed_group_title", comment: ""), customFontSize: customTitleFontSize)
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
@ -89,11 +89,11 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
explanationLabel.text = NSLocalizedString("You don't have any contacts yet", comment: "")
explanationLabel.text = NSLocalizedString("vc_create_closed_group_empty_state_message", comment: "")
let createNewPrivateChatButton = Button(style: .prominentOutline, size: .large)
createNewPrivateChatButton.setTitle(NSLocalizedString("Start a Session", comment: ""), for: UIControl.State.normal)
createNewPrivateChatButton.setTitle(NSLocalizedString("vc_create_closed_group_empty_state_button_title", comment: ""), for: UIControl.State.normal)
createNewPrivateChatButton.addTarget(self, action: #selector(createNewPrivateChat), for: UIControl.Event.touchUpInside)
createNewPrivateChatButton.set(.width, to: 180)
createNewPrivateChatButton.set(.width, to: 196)
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, createNewPrivateChatButton ])
stackView.axis = .vertical
stackView.spacing = Values.mediumSpacing
@ -120,7 +120,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
// MARK: Interaction
func textFieldDidEndEditing(_ textField: UITextField) {
crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("New Closed Group", comment: "") : textField.text!
crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("vc_create_closed_group_title", comment: "") : textField.text!
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -166,16 +166,16 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
presentAlert(alert)
}
guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else {
return showError(title: NSLocalizedString("Please enter a group name", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: ""))
}
guard name.count < 64 else {
return showError(title: NSLocalizedString("Please enter a shorter group name", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: ""))
}
guard selectedContacts.count >= 2 else {
return showError(title: NSLocalizedString("Please pick at least 2 group members", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_not_enough_group_members_error", comment: ""))
}
guard selectedContacts.count < 50 else { // Minus one because we're going to include self later
return showError(title: NSLocalizedString("A closed group cannot have more than 50 members", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
}
let selectedContacts = self.selectedContacts
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
@ -189,9 +189,9 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}.catch(on: DispatchQueue.main) { _ in
self?.dismiss(animated: true, completion: nil) // Dismiss the modal
let title = NSLocalizedString("Couldn't Create Group", comment: "")
let message = NSLocalizedString("Please check your internet connection and try again.", comment: "")
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
let title = "Couldn't Create Group"
let message = "Please check your internet connection and try again."
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
@ -206,16 +206,16 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
presentAlert(alert)
}
guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else {
return showError(title: NSLocalizedString("Please enter a group name", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: ""))
}
guard name.count < 64 else {
return showError(title: NSLocalizedString("Please enter a shorter group name", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: ""))
}
guard selectedContacts.count >= 2 else {
return showError(title: NSLocalizedString("Please pick at least 2 group members", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_not_enough_group_members_error", comment: ""))
}
guard selectedContacts.count < 10 else { // Minus one because we're going to include self later
return showError(title: NSLocalizedString("A closed group cannot have more than 10 members", comment: ""))
return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
}
let userPublicKey = getUserHexEncodedPublicKey()
let storage = OWSPrimaryStorage.shared()

View File

@ -7,11 +7,11 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("Enter Session ID", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_create_private_chat_enter_session_id_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
},
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_create_private_chat_scan_qr_code_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
}
@ -32,7 +32,7 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = NSLocalizedString("Scan a users QR code to start a session. QR codes can be found by tapping the QR code icon in account settings.", comment: "")
let message = NSLocalizedString("vc_create_private_chat_scan_qr_code_explanation", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
@ -43,7 +43,7 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("New Session", comment: ""))
setNavBarTitle(NSLocalizedString("vc_create_private_chat_title", comment: ""))
let navigationBar = navigationController!.navigationBar
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
@ -126,7 +126,7 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let alert = UIAlertController(title: NSLocalizedString("Invalid Session ID", comment: ""), message: NSLocalizedString("Please check the Session ID and try again", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: NSLocalizedString("Please check the Session ID and try again", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
} else {
@ -149,11 +149,11 @@ private final class EnterPublicKeyVC : UIViewController {
}()
// MARK: Components
private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("Enter a Session ID", comment: ""))
private lazy var publicKeyTextField = TextField(placeholder: NSLocalizedString("vc_enter_public_key_text_field_hint", comment: ""))
private lazy var copyButton: Button = {
let result = Button(style: .unimportant, size: .medium)
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
return result
}()
@ -165,13 +165,13 @@ private final class EnterPublicKeyVC : UIViewController {
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Users can share their Session ID from their account settings, or by sharing their QR code.", comment: "")
explanationLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
explanationLabel.text = NSLocalizedString("vc_enter_public_key_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up separator
let separator = Separator(title: NSLocalizedString("Your Session ID", comment: ""))
let separator = Separator(title: NSLocalizedString("your_session_id", comment: ""))
// Set up user public key label
let userPublicKeyLabel = UILabel()
userPublicKeyLabel.textColor = Colors.text
@ -182,7 +182,7 @@ private final class EnterPublicKeyVC : UIViewController {
userPublicKeyLabel.text = userHexEncodedPublicKey
// Set up share button
let shareButton = Button(style: .unimportant, size: .medium)
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
// Set up button container
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
@ -191,7 +191,7 @@ private final class EnterPublicKeyVC : UIViewController {
buttonContainer.distribution = .fillEqually
// Next button
let nextButton = Button(style: .prominentOutline, size: .large)
nextButton.setTitle(NSLocalizedString("Next", comment: ""), for: UIControl.State.normal)
nextButton.setTitle(NSLocalizedString("next", comment: ""), for: UIControl.State.normal)
nextButton.addTarget(self, action: #selector(startNewPrivateChatIfPossible), for: UIControl.Event.touchUpInside)
let nextButtonContainer = UIView()
nextButtonContainer.addSubview(nextButton)
@ -200,7 +200,7 @@ private final class EnterPublicKeyVC : UIViewController {
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ publicKeyTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.spacer(withHeight: Values.largeSpacing), separator, UIView.spacer(withHeight: Values.veryLargeSpacing), userPublicKeyLabel, UIView.spacer(withHeight: Values.veryLargeSpacing), buttonContainer, UIView.vStretchingSpacer(), nextButtonContainer ])
let stackView = UIStackView(arrangedSubviews: [ publicKeyTextField, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, UIView.spacer(withHeight: Values.largeSpacing), separator, UIView.spacer(withHeight: Values.largeSpacing), userPublicKeyLabel, UIView.spacer(withHeight: Values.largeSpacing), buttonContainer, UIView.vStretchingSpacer(), nextButtonContainer ])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing)
@ -226,7 +226,7 @@ private final class EnterPublicKeyVC : UIViewController {
@objc private func enableCopyButton() {
copyButton.isUserInteractionEnabled = true
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
}, completion: nil)
}
@ -235,7 +235,7 @@ private final class EnterPublicKeyVC : UIViewController {
UIPasteboard.general.string = userHexEncodedPublicKey
copyButton.isUserInteractionEnabled = false
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}
@ -261,7 +261,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Session needs camera access to scan QR codes", comment: "")
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -269,7 +269,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])

View File

@ -8,7 +8,7 @@ final class NukeDataModal : Modal {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Clear All Data", comment: "")
titleLabel.text = NSLocalizedString("modal_clear_all_data_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
@ -16,7 +16,7 @@ final class NukeDataModal : Modal {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("This will permanently delete your messages, sessions, and contacts.", comment: "")
explanationLabel.text = NSLocalizedString("modal_clear_all_data_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -29,7 +29,7 @@ final class NukeDataModal : Modal {
}
nukeDataButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
nukeDataButton.setTitleColor(isLightMode ? Colors.destructive : Colors.text, for: UIControl.State.normal)
nukeDataButton.setTitle(NSLocalizedString("Delete", comment: ""), for: UIControl.State.normal)
nukeDataButton.setTitle(NSLocalizedString("TXT_DELETE_TITLE", comment: ""), for: UIControl.State.normal)
nukeDataButton.addTarget(self, action: #selector(nuke), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, nukeDataButton ])

View File

@ -11,8 +11,8 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
}
// MARK: Components
private lazy var apnsOptionView = OptionView(title: NSLocalizedString("Apple Push Notification Service", comment: ""), explanation: NSLocalizedString("Session will use the Apple Push Notification service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.", comment: ""), delegate: self, isRecommended: true)
private lazy var backgroundPollingOptionView = OptionView(title: NSLocalizedString("Background Polling", comment: ""), explanation: NSLocalizedString("Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.", comment: ""), delegate: self)
private lazy var apnsOptionView = OptionView(title: NSLocalizedString("vc_pn_mode_apns_option_title", comment: ""), explanation: NSLocalizedString("vc_pn_mode_apns_option_explanation", comment: ""), delegate: self, isRecommended: true)
private lazy var backgroundPollingOptionView = OptionView(title: NSLocalizedString("vc_pn_mode_background_polling_option_title", comment: ""), explanation: NSLocalizedString("vc_pn_mode_background_polling_option_explanation", comment: ""), delegate: self)
// MARK: Lifecycle
override func viewDidLoad() {
@ -24,24 +24,24 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Push Notifications", comment: "")
titleLabel.text = NSLocalizedString("vc_pn_mode_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose.", comment: "")
explanationLabel.text = NSLocalizedString("vc_pn_mode_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
let registerButtonBottomOffsetSpacer = UIView()
registerButtonBottomOffsetSpacer.set(.height, to: isIPhone5OrSmaller ? CGFloat(16) : Values.onboardingButtonBottomOffset)
registerButtonBottomOffsetSpacer.set(.height, to: Values.mediumSpacing)
// Set up register button
let registerButton = Button(style: .prominentFilled, size: .large)
registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
registerButton.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
// Set up register button container
@ -75,7 +75,7 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
@objc private func register() {
guard selectedOptionView != nil else {
let title = NSLocalizedString("Please Pick an Option", comment: "")
let title = NSLocalizedString("vc_pn_mode_no_option_picked_dialog_title", comment: "")
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
return present(alert, animated: true, completion: nil)

View File

@ -19,7 +19,7 @@ final class PathVC : BaseVC {
private lazy var learnMoreButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Learn More", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("vc_path_learn_more_button_title", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(learnMore), for: UIControl.Event.touchUpInside)
return result
}()
@ -35,7 +35,7 @@ final class PathVC : BaseVC {
private func setUpNavBar() {
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Path", comment: ""))
setNavBarTitle(NSLocalizedString("vc_path_title", comment: ""))
// Set up close button
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
@ -47,7 +47,7 @@ final class PathVC : BaseVC {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Session hides your IP by bouncing your messages through several Service Nodes in Sessions decentralized network. These are the countries your connection is currently being bounced through:", comment: "")
explanationLabel.text = NSLocalizedString("vc_path_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -113,8 +113,8 @@ final class PathVC : BaseVC {
let isGuardSnode = (snode == pathToDisplay.first!)
return getPathRow(snode: snode, location: .middle, dotAnimationStartDelay: Double(index) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval, isGuardSnode: isGuardSnode)
}
let youRow = getPathRow(title: NSLocalizedString("You", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let destinationRow = getPathRow(title: NSLocalizedString("Destination", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let youRow = getPathRow(title: NSLocalizedString("vc_path_device_row_title", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let destinationRow = getPathRow(title: NSLocalizedString("vc_path_destination_row_title", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let rows = [ youRow ] + snodeRows + [ destinationRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
@ -158,7 +158,7 @@ final class PathVC : BaseVC {
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
let title = isGuardSnode ? NSLocalizedString("Entry Node", comment: "") : NSLocalizedString("Service Node", comment: "")
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "")
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
}

View File

@ -8,11 +8,11 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
// MARK: Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: NSLocalizedString("View My QR Code", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_qr_code_view_my_qr_code_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
},
TabBar.Tab(title: NSLocalizedString("Scan QR Code", comment: "")) { [weak self] in
TabBar.Tab(title: NSLocalizedString("vc_qr_code_view_scan_qr_code_tab_title", comment: "")) { [weak self] in
guard let self = self else { return }
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
}
@ -33,7 +33,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = NSLocalizedString("Scan someone's QR code to start a conversation with them", comment: "")
let message = NSLocalizedString("vc_qr_code_view_scan_qr_code_explanation", comment: "")
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
@ -44,7 +44,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("QR Code", comment: ""))
setNavBarTitle(NSLocalizedString("vc_qr_code_title", comment: ""))
let navigationBar = navigationController!.navigationBar
// Set up page VC
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
@ -122,7 +122,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let alert = UIAlertController(title: NSLocalizedString("Invalid Session ID", comment: ""), message: NSLocalizedString("Please check the Session ID and try again.", comment: ""), preferredStyle: .alert)
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: NSLocalizedString("Please check the Session ID and try again.", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
} else {
@ -153,10 +153,11 @@ private final class ViewMyQRCodeVC : UIViewController {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? CGFloat(40) : Values.massiveFontSize)
titleLabel.text = NSLocalizedString("Scan Me", comment: "")
titleLabel.numberOfLines = 0
titleLabel.text = "Scan Me"
titleLabel.numberOfLines = 1
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.set(.height, to: isIPhone5OrSmaller ? CGFloat(40) : Values.massiveFontSize)
// Set up QR code image view
let qrCodeImageView = UIImageView()
let qrCode = QRCode.generate(for: userHexEncodedPublicKey, hasBackground: true)
@ -178,13 +179,13 @@ private final class ViewMyQRCodeVC : UIViewController {
// let attributedText = NSMutableAttributedString(string: text)
// attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: Values.mediumFontSize), range: (text as NSString).range(of: "your unique public QR code"))
// explanationLabel.attributedText = attributedText
explanationLabel.text = NSLocalizedString("This is your QR code. Other users can scan it to start a session with you.", comment: "")
explanationLabel.text = NSLocalizedString("vc_view_my_qr_code_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up share button
let shareButton = Button(style: .regular, size: .large)
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(shareQRCode), for: UIControl.Event.touchUpInside)
// Set up share button container
let shareButtonContainer = UIView()
@ -192,13 +193,14 @@ private final class ViewMyQRCodeVC : UIViewController {
shareButton.pin(.leading, to: .leading, of: shareButtonContainer, withInset: 80)
shareButton.pin(.top, to: .top, of: shareButtonContainer)
shareButtonContainer.pin(.trailing, to: .trailing, of: shareButton, withInset: 80)
shareButtonContainer.pin(.bottom, to: .bottom, of: shareButton)
shareButtonContainer.pin(.bottom, to: .bottom, of: shareButton, withInset: isIPhone6OrSmaller ? Values.largeSpacing : Values.onboardingButtonBottomOffset)
let spacing = isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ titleLabel, qrCodeImageViewContainer, explanationLabel, shareButtonContainer, UIView.vStretchingSpacer() ])
let stackView = UIStackView(arrangedSubviews: [ titleLabel, UIView.spacer(withHeight: spacing), qrCodeImageViewContainer, UIView.spacer(withHeight: spacing),
explanationLabel, UIView.spacer(withHeight: spacing), shareButtonContainer ])
stackView.axis = .vertical
stackView.spacing = isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing)
stackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: view)
@ -232,7 +234,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Session needs camera access to scan QR codes", comment: "")
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
@ -240,7 +242,7 @@ private final class ScanQRCodePlaceholderVC : UIViewController {
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("Enable Camera Access", comment: ""), for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])

View File

@ -15,7 +15,7 @@ final class RegisterVC : BaseVC {
private lazy var copyPublicKeyButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
result.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
return result
@ -47,14 +47,14 @@ final class RegisterVC : BaseVC {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Say hello to your Session ID", comment: "")
titleLabel.text = NSLocalizedString("vc_register_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("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.", comment: "")
explanationLabel.text = NSLocalizedString("vc_register_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Set up public key label container
@ -69,7 +69,7 @@ final class RegisterVC : BaseVC {
let bottomSpacer = UIView.vStretchingSpacer()
// Set up register button
let registerButton = Button(style: .prominentFilled, size: .large)
registerButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
registerButton.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
registerButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
// Set up button stack view
@ -123,7 +123,7 @@ final class RegisterVC : BaseVC {
@objc private func enableCopyButton() {
copyPublicKeyButton.isUserInteractionEnabled = true
UIView.transition(with: copyPublicKeyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyPublicKeyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
self.copyPublicKeyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
}, completion: nil)
}
@ -180,7 +180,7 @@ final class RegisterVC : BaseVC {
UIPasteboard.general.string = keyPair.hexEncodedPublicKey
copyPublicKeyButton.isUserInteractionEnabled = false
UIView.transition(with: copyPublicKeyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyPublicKeyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
self.copyPublicKeyButton.setTitle("Copied", for: UIControl.State.normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}

View File

@ -8,7 +8,7 @@ final class RestoreVC : BaseVC {
// MARK: Components
private lazy var mnemonicTextField: TextField = {
let result = TextField(placeholder: NSLocalizedString("Enter your recovery phrase", comment: ""))
let result = TextField(placeholder: NSLocalizedString("vc_restore_seed_text_field_hint", comment: ""))
result.layer.borderColor = Colors.text.cgColor
return result
}()
@ -39,14 +39,14 @@ final class RestoreVC : BaseVC {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Restore your account", comment: "")
titleLabel.text = NSLocalizedString("vc_restore_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = "Enter the recovery phrase that was given to you when you signed up to restore your account."
explanationLabel.text = NSLocalizedString("vc_restore_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Set up legal label
@ -66,7 +66,7 @@ final class RestoreVC : BaseVC {
restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
// Set up restore button
let restoreButton = Button(style: .prominentFilled, size: .large)
restoreButton.setTitle(NSLocalizedString("Continue", comment: ""), for: UIControl.State.normal)
restoreButton.setTitle(NSLocalizedString("continue_2", comment: ""), for: UIControl.State.normal)
restoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
// Set up restore button container
@ -124,10 +124,10 @@ final class RestoreVC : BaseVC {
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
bottomConstraint.constant = -newHeight // Negative due to how the constraint is set up
restoreButtonBottomOffsetConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.largeSpacing
spacer1HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing
spacer2HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing
spacer3HeightConstraint.constant = isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing
restoreButtonBottomOffsetConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.largeSpacing
spacer1HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing
spacer2HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing
spacer3HeightConstraint.constant = isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}

View File

@ -18,7 +18,7 @@ final class SeedModal : Modal {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Your Recovery Phrase", comment: "")
titleLabel.text = NSLocalizedString("modal_seed_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
@ -34,7 +34,7 @@ final class SeedModal : Modal {
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.", comment: "")
explanationLabel.text = NSLocalizedString("modal_seed_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
@ -45,7 +45,7 @@ final class SeedModal : Modal {
copyButton.backgroundColor = Colors.buttonBackground
copyButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
copyButton.setTitleColor(Colors.text, for: UIControl.State.normal)
copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
copyButton.addTarget(self, action: #selector(copySeed), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, copyButton ])

View File

@ -30,7 +30,7 @@ final class SeedVC : BaseVC {
let attributedTitle = NSMutableAttributedString(string: title)
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "90%"))
result.title = attributedTitle
result.subtitle = NSLocalizedString("Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID.", comment: "")
result.subtitle = NSLocalizedString("view_seed_reminder_subtitle_2", comment: "")
result.setProgress(0.9, animated: false)
return result
}()
@ -47,7 +47,7 @@ final class SeedVC : BaseVC {
private lazy var copyButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(copyMnemonic), for: UIControl.Event.touchUpInside)
return result
}()
@ -57,7 +57,7 @@ final class SeedVC : BaseVC {
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Your Recovery Phrase", comment: ""))
setNavBarTitle(NSLocalizedString("vc_seed_title", comment: ""))
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
@ -66,14 +66,14 @@ final class SeedVC : BaseVC {
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
titleLabel.text = NSLocalizedString("Meet your recovery phrase", comment: "")
titleLabel.text = NSLocalizedString("vc_seed_title_2", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone.", comment: "")
explanationLabel.text = NSLocalizedString("vc_seed_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Set up mnemonic label
@ -85,7 +85,7 @@ final class SeedVC : BaseVC {
// Set up mnemonic label container
let mnemonicLabelContainer = UIView()
mnemonicLabelContainer.addSubview(mnemonicLabel)
mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: isIPhone5OrSmaller ? Values.smallSpacing : Values.mediumSpacing)
mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: isIPhone6OrSmaller ? Values.smallSpacing : Values.mediumSpacing)
mnemonicLabelContainer.layer.cornerRadius = Values.textFieldCornerRadius
mnemonicLabelContainer.layer.borderWidth = Values.borderThickness
mnemonicLabelContainer.layer.borderColor = Colors.text.cgColor
@ -93,7 +93,7 @@ final class SeedVC : BaseVC {
let callToActionLabel = UILabel()
callToActionLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
callToActionLabel.font = .systemFont(ofSize: isIPhone5OrSmaller ? Values.smallFontSize : Values.mediumFontSize)
callToActionLabel.text = NSLocalizedString("Hold to reveal", comment: "")
callToActionLabel.text = NSLocalizedString("vc_seed_reveal_button_title", comment: "")
callToActionLabel.textAlignment = .center
let callToActionLabelGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(revealMnemonic))
callToActionLabel.addGestureRecognizer(callToActionLabelGestureRecognizer)
@ -110,9 +110,12 @@ final class SeedVC : BaseVC {
copyButtonContainer.pin(.trailing, to: .trailing, of: copyButton, withInset: Values.massiveSpacing)
copyButtonContainer.pin(.bottom, to: .bottom, of: copyButton)
// Set up top stack view
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, mnemonicLabelContainer, callToActionLabel ])
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, mnemonicLabelContainer ])
if !isIPhone5OrSmaller {
topStackView.addArrangedSubview(callToActionLabel) // Not that important and it really gets in the way on small screens
}
topStackView.axis = .vertical
topStackView.spacing = isIPhone5OrSmaller ? Values.smallSpacing : Values.largeSpacing
topStackView.spacing = isIPhone6OrSmaller ? Values.smallSpacing : Values.largeSpacing
topStackView.alignment = .fill
// Set up top stack view container
let topStackViewContainer = UIView()
@ -144,7 +147,7 @@ final class SeedVC : BaseVC {
@objc private func enableCopyButton() {
copyButton.isUserInteractionEnabled = true
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
}, completion: nil)
}
@ -164,7 +167,7 @@ final class SeedVC : BaseVC {
self.seedReminderView.title = attributedTitle
}, completion: nil)
UIView.transition(with: seedReminderView.subtitleLabel, duration: 1, options: .transitionCrossDissolve, animations: {
self.seedReminderView.subtitle = NSLocalizedString("Make sure to store your recovery phrase in a safe place", comment: "")
self.seedReminderView.subtitle = NSLocalizedString("view_seed_reminder_subtitle_3", comment: "")
}, completion: nil)
seedReminderView.setProgress(1, animated: true)
UserDefaults.standard[.hasViewedSeed] = true
@ -176,7 +179,7 @@ final class SeedVC : BaseVC {
UIPasteboard.general.string = mnemonic
copyButton.isUserInteractionEnabled = false
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}

View File

@ -38,14 +38,14 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
}()
private lazy var displayNameTextField: TextField = {
let result = TextField(placeholder: NSLocalizedString("Enter a display name", comment: ""), usesDefaultHeight: false)
let result = TextField(placeholder: NSLocalizedString("vc_settings_display_name_text_field_hint", comment: ""), usesDefaultHeight: false)
result.textAlignment = .center
return result
}()
private lazy var copyButton: Button = {
let result = Button(style: .prominentOutline, size: .medium)
result.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
return result
}()
@ -55,9 +55,9 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("Settings", comment: ""))
setNavBarTitle(NSLocalizedString("vc_settings_title", comment: ""))
// Set up navigation bar buttons
let backButton = UIBarButtonItem(title: NSLocalizedString("Back", comment: ""), style: .plain, target: nil, action: nil)
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton
updateNavigationBarButtons()
@ -84,7 +84,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
headerStackView.spacing = Values.smallSpacing
headerStackView.alignment = .center
// Set up separator
let separator = Separator(title: NSLocalizedString("Your Session ID", comment: ""))
let separator = Separator(title: NSLocalizedString("your_session_id", comment: ""))
// Set up public key label
let publicKeyLabel = UILabel()
publicKeyLabel.textColor = Colors.text
@ -95,7 +95,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
publicKeyLabel.text = userHexEncodedPublicKey
// Set up share button
let shareButton = Button(style: .regular, size: .medium)
shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: UIControl.State.normal)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
// Set up button container
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
@ -161,19 +161,19 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
}
var result = [
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("Privacy", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("Notifications", comment: ""), color: Colors.text, action: #selector(showNotificationSettings))
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings))
]
let isMasterDevice = UserDefaults.standard.isMasterDevice
if isMasterDevice {
result.append(getSeparator())
result.append(getSettingButton(withTitle: NSLocalizedString("Devices", comment: ""), color: Colors.text, action: #selector(showLinkedDevices)))
result.append(getSettingButton(withTitle: NSLocalizedString("vc_settings_devices_button_title", comment: ""), color: Colors.text, action: #selector(showLinkedDevices)))
result.append(getSeparator())
result.append(getSettingButton(withTitle: NSLocalizedString("Recovery Phrase", comment: ""), color: Colors.text, action: #selector(showSeed)))
result.append(getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)))
}
result.append(getSeparator())
result.append(getSettingButton(withTitle: NSLocalizedString("Clear All Data", comment: ""), color: Colors.destructive, action: #selector(clearAllData)))
result.append(getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)))
result.append(getSeparator())
return result
}
@ -182,12 +182,12 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
@objc private func enableCopyButton() {
copyButton.isUserInteractionEnabled = true
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
}, completion: nil)
}
func avatarActionSheetTitle() -> String? {
return NSLocalizedString("Update Profile Picture", comment: "")
return "Update Profile Picture"
}
func fromViewController() -> UIViewController {
@ -199,7 +199,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
}
func clearAvatarActionLabel() -> String {
return NSLocalizedString("Clear", comment: "")
return "Clear"
}
// MARK: Updating
@ -266,8 +266,8 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
if let error = error as? DotNetAPI.DotNetAPIError {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : NSLocalizedString("Couldn't Update Profile", comment: "")
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : NSLocalizedString("Please check your internet connection and try again", comment: "")
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
@ -299,15 +299,15 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
}
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard !displayName.isEmpty else {
return showError(title: NSLocalizedString("Please pick a display name", comment: ""))
return showError(title: NSLocalizedString("vc_settings_display_name_missing_error", comment: ""))
}
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ ")
let hasInvalidCharacters = !displayName.allSatisfy { $0.unicodeScalars.allSatisfy { allowedCharacters.contains($0) } }
guard !hasInvalidCharacters else {
return showError(title: NSLocalizedString("Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", comment: ""))
return showError(title: NSLocalizedString("vc_settings_invalid_display_name_error", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
return showError(title: NSLocalizedString("Please pick a shorter display name", comment: ""))
return showError(title: NSLocalizedString("vc_settings_display_name_too_long_error", comment: ""))
}
isEditingDisplayName = false
displayNameToBeUploaded = displayName
@ -326,7 +326,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
UIPasteboard.general.string = userHexEncodedPublicKey
copyButton.isUserInteractionEnabled = false
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("Copied", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}

View File

@ -36,13 +36,13 @@
// Loki: Customize title
UILabel *titleLabel = [UILabel new];
titleLabel.text = NSLocalizedString(@"Notifications", @"");
titleLabel.text = NSLocalizedString(@"vc_notification_settings_title", @"");
titleLabel.textColor = LKColors.text;
titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize];
self.navigationItem.titleView = titleLabel;
// Loki: Set up back button
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Back", "") style:UIBarButtonItemStylePlain target:nil action:nil];
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStylePlain target:nil action:nil];
backButton.tintColor = LKColors.text;
self.navigationItem.backBarButtonItem = backButton;
}
@ -65,8 +65,8 @@
OWSPreferences *prefs = Environment.shared.preferences;
OWSTableSection *strategySection = [OWSTableSection new];
strategySection.headerTitle = NSLocalizedString(@"Notification Strategy", @"");
[strategySection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"Use APNs", @"")
strategySection.headerTitle = NSLocalizedString(@"preferences_notifications_strategy_category_title", @"");
[strategySection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"preferences_notifications_use_apns_option_title", @"")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"push_notification_strategy")
isOnBlock:^{
return [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"];
@ -76,7 +76,7 @@
}
target:weakSelf
selector:@selector(didToggleAPNsSwitch:)]];
strategySection.footerTitle = NSLocalizedString(@"Session will use the Apple Push Notification service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.", @"");
strategySection.footerTitle = NSLocalizedString(@"preferences_notifications_use_apns_option_explanation", @"");
[contents addSection:strategySection];
// Sounds section.

View File

@ -43,7 +43,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
// Loki: Customize title
UILabel *titleLabel = [UILabel new];
titleLabel.text = NSLocalizedString(@"Privacy", @"");
titleLabel.text = NSLocalizedString(@"vc_privacy_settings_title", @"");
titleLabel.textColor = LKColors.text;
titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize];
self.navigationItem.titleView = titleLabel;
@ -538,11 +538,11 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
{
BOOL isOn = sender.isOn;
if (isOn) {
NSString *title = NSLocalizedString(@"Enable Link Previews?", @"");
NSString *message = NSLocalizedString(@"You will not have full metadata protection when sending or receiving link previews.", @"");
NSString *title = @"Enable Link Previews?";
NSString *message = @"You will not have full metadata protection when sending or receiving link previews.";
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[sender setOn:NO animated:YES];
SSKPreferences.areLinkPreviewsEnabled = NO;
}]];

View File

@ -286,7 +286,7 @@ public class ConversationMediaView: UIView {
Logger.warn("Ignoring invalid attachment.")
return nil
}
return attachmentStream.thumbnailImageMedium(success: { (image) in
return attachmentStream.thumbnailImageLarge(success: { (image) in
AssertIsOnMainThread()
stillImageView.image = image

View File

@ -395,7 +395,7 @@ extension PhotoCapture: CaptureOutputDelegate {
let dataSource = DataSourceValue.dataSource(with: photoData, utiType: kUTTypeJPEG as String)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String, imageQuality: .medium)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String, imageQuality: .original)
delegate?.photoCapture(self, didFinishProcessingAttachment: attachment)
}

View File

@ -206,7 +206,7 @@ class PhotoCollectionContents {
switch asset.mediaType {
case .image:
return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
}
case .video:
return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Du hast die Zeit für verschwindende Nachrichten auf %@ festgelegt.";
// MARK: - Session
"continue_2" = "Fortsetzen";
"copy" = "Kopieren";
"invalid_url" = "Ungültige URL";
"copied_to_clipboard" = "In die Zwischenablage kopiert.";
"device_linking_failed" = "Das Gerät konnte nicht verbunden werden.";
"next" = "Weiter";
"share" = "Teilen";
"invalid_session_id" = "Ungültige Session ID";
"cancel" = "Abbrechen";
"your_session_id" = "Ihre Session ID";
"vc_landing_title_2" = "Ihre Session beginnt hier...";
"vc_landing_register_button_title" = "Session ID erstellen";
"vc_landing_restore_button_title" = "Ihre Session fortsetzen";
"vc_landing_link_button_title" = "Mit einem bestehenden Konto verlinken";
"vc_landing_device_unlinked_modal_title" = "Ihr Gerät wurde erfolgreich getrennt.";
"view_fake_chat_bubble_1" = "Was ist Session?";
"view_fake_chat_bubble_2" = "Es ist eine dezentrale, verschlüsselte Messaging-App.";
"view_fake_chat_bubble_3" = "Es werden also weder meine persönlichen Daten noch die Metadaten meiner Konversation erfasst? Wie funktioniert das?";
"view_fake_chat_bubble_4" = "Mit einer Kombination fortschrittlicher anonyme Routing- und End-to-End-Verschlüsselungstechnologien.";
"view_fake_chat_bubble_5" = "Freunde lassen Freunde keine kompromittierten Messenger verwenden. Herzlich Willkommen.";
"vc_register_title" = "Das ist Ihre Session ID.";
"vc_register_explanation" = "Ihre Session ID ist die eindeutige Adresse, unter der Personen Sie über Session kontaktieren können. Ihre Session ID ist nicht mit Ihrer realen Identität verbunden, völlig anonym und von Natur aus privat.";
"vc_register_public_key_copied_message" = "In die Zwischenablage kopiert.";
"vc_restore_title" = "Ihr Konto wiederherstellen";
"vc_restore_explanation" = "Geben Sie den Wiederherstellungssatz ein, den Sie bei der Anmeldung zur Wiederherstellung Ihres Kontos erhalten haben.";
"vc_restore_seed_text_field_hint" = "Ihr Wiederherstellungssatz";
"vc_link_device_title" = "Gerät verbinden";
"vc_link_device_enter_session_id_tab_title" = "Session ID eingeben";
"vc_link_device_scan_qr_code_tab_title" = "QR-Code scannen";
"vc_link_device_scan_qr_code_explanation" = "Navigieren Sie zu \"Einstellungen\" > \"Geräte\" > \"Ihr Gerät verknüpfen\" und scannen Sie den angezeigten QR-Code, um den Verknüpfungsprozess zu starten.";
"vc_enter_session_id_title" = "Ihr Gerät verknüpfen";
"vc_enter_session_id_explanation" = "Navigieren Sie zu \"Einstellungen\" > \"Geräte\" > \"Ihr Gerät verknüpfen\" und geben Sie Ihre Session ID hier ein, um den Verknüpfungsprozess zu starten.";
"vc_enter_session_id_text_field_hint" = "Geben Sie Ihre Session ID ein.";
"vc_display_name_title_2" = "Wählen Sie Ihren Anzeigenamen";
"vc_display_name_explanation" = "Dies ist Ihr Name, wenn Sie Session verwenden. Es kann Ihr richtiger Name, ein Alias oder etwas andere sein.";
"vc_display_name_text_field_hint" = "Geben Sie einen Anzeigenamen ein";
"vc_display_name_display_name_missing_error" = "Bitte wählen Sie einen Anzeigenamen";
"vc_display_name_display_name_invalid_error" = "Bitte wählen Sie einen Anzeigenamen, der nur aus den Zeichen a - z, A - Z, 0 - 9 und _ besteht.";
"vc_display_name_display_name_too_long_error" = "Bitte wählen Sie einen kürzeren Anzeigenamen";
"vc_pn_mode_title" = "Benachrichtigungen";
"vc_pn_mode_explanation" = "Session verfügt über zwei Methoden zur Verarbeitung von Benachrichtigungen. Lesen Sie die Beschreibungen sorgfältig, bevor Sie sich entscheiden.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "Session verwendet den Apple Push Notification Service-Dienst, um Push-Benachrichtigungen zu empfangen. Sie werden zuverlässig und sofort über neue Nachrichten informiert. Die Verwendung von APNs bedeutet, dass Ihre IP-Adresse und Ihr Geräte-Token für Apple verfügbar sind. Wenn Sie Push-Benachrichtigungen anderer Apps verwenden, ist dies auch der Fall. Auch Loki erfährt Ihre IP-Adresse und Ihren Geräte-Token, jedoch werden Ihre Nachrichten weiterhin per Onion geroutet und durchgängig verschlüsselt, sodass der Inhalt Ihrer Nachrichten vollständig privat bleibt.";
"vc_pn_mode_background_polling_option_title" = "Hintergrundabfrage";
"vc_pn_mode_background_polling_option_explanation" = "Session sucht von Zeit zu Zeit im Hintergrund nach neuen Nachrichten. Das garantiert einen vollständigen Schutz der Metadaten, aber Benachrichtigungen können sich jedoch erheblich verzögern.";
"vc_pn_mode_recommended_option_tag" = "Empfohlen";
"vc_pn_mode_no_option_picked_modal_title" = "Bitte wählen Sie eine Option aus.";
"vc_home_empty_state_message" = "Sie haben noch keine Kontakte.";
"vc_home_empty_state_button_title" = "Session starten";
"vc_home_leave_group_modal_message" = "Sind Sie sich sicher, dass Sie diese Gruppe verlassen möchten?";
"vc_home_leaving_group_failed_message" = "Gruppe konnte nicht verlassen werden.";
"vc_home_delete_conversation_modal_message" = "Möchten Sie diese Unterhaltung wirklich löschen?";
"vc_home_conversation_deleted_message" = "Die Unterhaltung wurde gelöscht.";
"sheet_pn_mode_title" = "Benachrichtigungen";
"sheet_pn_mode_explanation" = "Session verfügt über zwei Methoden zur Verarbeitung von Benachrichtigungen. Lesen Sie die Beschreibungen sorgfältig, bevor Sie sich entscheiden.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "Session verwendet den Apple Push Notification Service-Dienst, um Push-Benachrichtigungen zu empfangen. Sie werden zuverlässig und sofort über neue Nachrichten informiert. Die Verwendung von APNs bedeutet, dass Ihre IP-Adresse und Ihr Geräte-Token für Apple verfügbar sind. Wenn Sie Push-Benachrichtigungen anderer Apps verwenden, ist dies auch der Fall. Auch Loki erfährt Ihre IP-Adresse und Ihren Geräte-Token, jedoch werden Ihre Nachrichten weiterhin per Onion geroutet und durchgängig verschlüsselt, sodass der Inhalt Ihrer Nachrichten vollständig privat bleibt.";
"sheet_pn_mode_background_polling_option_title" = "Hintergrundabfrage";
"sheet_pn_mode_background_polling_option_explanation" = "Session sucht von Zeit zu Zeit im Hintergrund nach neuen Nachrichten. Das garantiert einen vollständigen Schutz der Metadaten, aber Benachrichtigungen können sich jedoch erheblich verzögern.";
"sheet_pn_mode_recommended_option_tag" = "Empfohlen";
"sheet_pn_mode_no_option_picked_modal_title" = "Bitte wählen Sie eine Option aus.";
"sheet_pn_mode_confirm_button_title" = "Bestätigen";
"sheet_pn_mode_skip_button_title" = "Überspringen";
"vc_seed_title" = "Ihr Wiederherstellungssatz";
"vc_seed_title_2" = "Das ist Ihr Wiederherstellungssatz.";
"vc_seed_explanation" = "Ihr Wiederherstellungssatz ist der Hauptschlüssel für Ihre Session ID. Mit diesem Satz können Sie Ihre Session ID wiederherstellen, wenn Sie den Zugriff auf Ihr Gerät verlieren. Bewahren Sie Ihren Wiederherstellungssatz an einem sicheren Ort auf und geben Sie ihn an niemandem weiter.";
"vc_seed_reveal_button_title" = "Zur Anzeige gedrückt halten";
"view_seed_reminder_subtitle_1" = "Sichern Sie Ihr Konto, indem Sie Ihren Wiederherstellungssatz speichern";
"view_seed_reminder_subtitle_2" = "Tippen und halten Sie die verborgenen Wörter, um Ihren Wiederherstellungssatz anzuzeigen, und speichern Sie ihn dann sicher, um Ihre Session ID zu sichern.";
"view_seed_reminder_subtitle_3" = "Bewahren Sie Ihren Wiederherstellungssatz an einem sicheren Ort auf.";
"vc_path_title" = "Pfad";
"vc_path_explanation" = "Session verbirgt Ihre IP-Adresse, indem Ihre Nachrichten über mehrere Dienstknoten im dezentralen Session-Netzwerk weitergeleitet werden. Dies sind die Länder, durch die Ihre Verbindung derzeit weitergeleitet wird:";
"vc_path_device_row_title" = "Sie";
"vc_path_guard_node_row_title" = "Eingangsknoten";
"vc_path_service_node_row_title" = "Dienstknoten";
"vc_path_destination_row_title" = "Ziel";
"vc_path_learn_more_button_title" = "Mehr erfahren";
"vc_create_private_chat_title" = "Neue Session";
"vc_create_private_chat_enter_session_id_tab_title" = "Session ID eingeben";
"vc_create_private_chat_scan_qr_code_tab_title" = "QR-Code scannen";
"vc_create_private_chat_scan_qr_code_explanation" = "Scannen Sie den QR-Code eines Benutzers, um eine Session zu starten. QR-Codes finden Sie, indem Sie in den Einstellungen auf das QR-Code-Symbol tippen.";
"vc_enter_public_key_text_field_hint" = "Geben Sie eine Session ID ein.";
"vc_enter_public_key_explanation" = "Benutzer können ihre Session ID freigeben, indem sie in ihren Einstellungen auf \"Session ID freigeben\" tippen oder ihren QR-Code freigeben.";
"vc_scan_qr_code_camera_access_explanation" = "Session benötigt Kamerazugriff, um die QR-Codes scannen zu können.";
"vc_scan_qr_code_grant_camera_access_button_title" = "Kamerazugriff gewähren";
"vc_create_closed_group_title" = "Neue geschlossene Gruppe";
"vc_create_closed_group_text_field_hint" = "Geben Sie einen Gruppennamen ein.";
"vc_create_closed_group_explanation" = "Geschlossene Gruppen unterstützen bis zu 10 Mitglieder und bieten den gleichen Schutz der Privatsphäre wie Einzelgespräche.";
"vc_create_closed_group_empty_state_message" = "Sie haben noch keine Kontakte.";
"vc_create_closed_group_empty_state_button_title" = "Session starten";
"vc_create_closed_group_group_name_missing_error" = "Bitte geben Sie einen Gruppennamen ein.";
"vc_create_closed_group_group_name_too_long_error" = "Bitte geben Sie einen kürzeren Gruppennamen ein.";
"vc_create_closed_group_not_enough_group_members_error" = "Bitte wählen Sie mindestens zwei Gruppenmitglieder aus.";
"vc_create_closed_group_too_many_group_members_error" = "Eine geschlossene Gruppe kann maximal zehn Mitglieder haben.";
"vc_create_closed_group_invalid_session_id_error" = "Ein Mitglied Ihrer Gruppe hat eine ungültige Session ID.";
"vc_join_public_chat_title" = "Offener Gruppe beitreten";
"vc_join_public_chat_error" = "Konnte der Gruppe nicht beitreten.";
"vc_join_public_chat_enter_group_url_tab_title" = "Gruppen-URL öffnen";
"vc_join_public_chat_scan_qr_code_tab_title" = "QR-Code scannen";
"vc_join_public_chat_scan_qr_code_explanation" = "Scannen Sie den QR-Code der offenen Gruppe, der Sie beitreten möchten.";
"vc_enter_chat_url_text_field_hint" = "Geben Sie eine offene Gruppen-URL ein.";
"vc_enter_chat_url_privacy_warning" = "Offenen Gruppen kann jeder beitreten und sie bieten keinen vollständigen Schutz der Privatsphäre.";
"vc_settings_title" = "Einstellungen";
"vc_settings_display_name_text_field_hint" = "Geben Sie einen Anzeigenamen ein.";
"vc_settings_display_name_missing_error" = "Bitte wählen Sie einen Anzeigenamen.";
"vc_settings_invalid_display_name_error" = "Bitte wählen Sie einen Anzeigenamen, der nur aus den Zeichen a - z, A - Z, 0 - 9 und _ besteht.";
"vc_settings_display_name_too_long_error" = "Bitte wählen Sie einen kürzeren Anzeigenamen.";
"vc_settings_privacy_button_title" = "Datenschutz";
"vc_settings_notifications_button_title" = "Benachrichtigungen";
"vc_settings_chats_button_title" = "Chats";
"vc_settings_devices_button_title" = "Geräte";
"vc_settings_recovery_phrase_button_title" = "Wiederherstellungssatz";
"vc_settings_clear_all_data_button_title" = "Daten löschen";
"vc_notification_settings_title" = "Benachrichtigungen";
"vc_notification_settings_style_section_title" = "Stil der Benachrichtigungen";
"vc_notification_settings_content_section_title" = "Inhalt der Benachrichtigungen";
"vc_privacy_settings_title" = "Datenschutz";
"vc_chat_settings_title" = "Chats";
"vc_linked_devices_title" = "Geräte";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Gerätelimit erreicht";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "Es ist derzeit nicht erlaubt, mehr als ein Gerät zu verbinden.";
"vc_linked_devices_unlinking_failed_message" = "Gerät konnte nicht getrennt werden.";
"vc_linked_devices_unlinking_successful_message" = "Ihr Gerät wurde erfolgreich getrennt.";
"vc_linked_devices_linking_failed_message" = "Das Gerät konnte nicht verbunden werden.";
"vc_linked_devices_empty_state_message" = "Sie haben noch keine Geräte verlinkt.";
"vc_linked_devices_empty_state_button_title" = "Gerät (Beta) verknüpfen";
"preferences_notifications_strategy_category_title" = "Benachrichtigungsstrategie";
"preferences_notifications_use_apns_option_title" = "APNs verwenden";
"preferences_notifications_use_apns_option_explanation" = "Die Verwendung von Apple Push Notification Service ermöglicht zuverlässigere Push-Benachrichtigungen, Ihre IP- und Geräte-Token werden Apple und Loki jedoch bekannt.";
"modal_link_device_slave_mode_title_1" = "Warten auf Autorisierung";
"modal_link_device_slave_mode_title_2" = "Geräteverbindung autorisiert";
"modal_link_device_slave_mode_explanation_1" = "Bitte überprüfen Sie, ob die folgenden Wörter mit denen auf Ihrem anderen Gerät übereinstimmen.";
"modal_link_device_slave_mode_explanation_2" = "Ihr Gerät wurde erfolgreich verknüpft.";
"modal_link_device_master_mode_title_1" = "Warten auf Gerät";
"modal_link_device_master_mode_title_2" = "Verknüpfungsanfrage empfangen";
"modal_link_device_master_mode_title_3" = "Gerätelink wird autorisiert";
"modal_link_device_master_mode_explanation_1" = "Laden Sie Session auf Ihr anderes Gerät herunter und tippen Sie unten auf dem Startbildschirm auf \"Mit einem bestehenden Konto verlinken\". Wenn Sie bereits ein Konto auf Ihrem anderen Gerät haben, müssen Sie dieses Konto zuerst löschen.";
"modal_link_device_master_mode_explanation_2" = "Bitte überprüfen Sie, ob die folgenden Wörter mit denen auf Ihrem anderen Gerät übereinstimmen.";
"modal_link_device_master_mode_explanation_3" = "Bitte warten Sie, während der Gerätelink erstellt wird. Dies kann bis zu einer Minute dauern.";
"modal_link_device_master_mode_authorize_button_title" = "Autorisieren";
"vc_device_list_bottom_sheet_change_name_button_title" = "Namen ändern";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Gerät trennen";
"modal_edit_device_name_text_field_hint" = "Geben Sie einen Namen ein.";
"modal_seed_title" = "Ihr Wiederherstellungssatz";
"modal_seed_explanation" = "Das ist Ihr Wiederherstellungssatz. Damit können Sie Ihre Session ID wiederherstellen oder auf ein neues Gerät migrieren.";
"modal_clear_all_data_title" = "Alle Daten löschen";
"modal_clear_all_data_explanation" = "Dadurch werden Ihre Nachrichten, Sessions und Kontakte dauerhaft gelöscht.";
"vc_qr_code_title" = "QR-Code";
"vc_qr_code_view_my_qr_code_tab_title" = "Meinen QR-Code anzeigen";
"vc_qr_code_view_scan_qr_code_tab_title" = "QR-Code scannen";
"vc_qr_code_view_scan_qr_code_explanation" = "Scannen Sie den QR-Code einer Person, um ein Gespräch mit ihr zu beginnen.";
"vc_view_my_qr_code_explanation" = "Das ist Ihr QR-Code. Andere Benutzer können ihn scannen, um eine Session mit Ihnen zu starten.";
"vc_view_my_qr_code_share_title" = "QR-Code freigeben";
"view_friend_request_accept_button_title" = "Akzeptieren";
"view_friend_request_reject_button_title" = "Ablehnen";
"view_friend_request_incoming_pending_message" = "%@ hat Ihnen eine Session-Anfrage gesendet.";
"view_friend_request_incoming_accepted_message" = "Sie haben die Session-Anfrage von %@ akzeptiert.";
"view_friend_request_incoming_declined_message" = "Sie haben die Session-Anfrage von %@ abgelehnt.";
"view_friend_request_incoming_expired_message" = "Die Session-Anfragte von %@ ist abgelaufen.";
"view_friend_request_outgoing_pending_message" = "Sie haben %@ eine Session-Anfrage gesendet.";
"view_friend_request_outgoing_accepted_message" = "%@ hat Ihre Session-Anfrage akzeptiert.";
"view_friend_request_outgoing_expired_message" = "Ihre Session-Anfrage an %@ ist abgelaufen.";
"session_reset_banner_message" = "Möchten Sie Ihre Session mit %@ wiederherstellen?";
"session_reset_banner_dismiss_button_title" = "Verwerfen";
"session_reset_banner_restore_button_title" = "Wiederherstellen";
"vc_contact_selection_contacts_title" = "Kontakte";
"vc_contact_selection_closed_groups_title" = "Geschlossene Gruppen";
"vc_contact_selection_open_groups_title" = "Gruppen öffnen";

View File

@ -2554,296 +2554,208 @@
// MARK: - Loki Messenger:
"Session can let you know when you get a message (and who it is from)" = "Session can let you know when you get a message (and who it is from)";
"Create Your Session Account" = "Create Your Session Account";
"Enter a name to be shown to your contacts" = "Enter a name to be shown to your contacts";
"Display Name" = "Display Name";
"Type an optional password for added security" = "Type an optional password for added security";
"Password (Optional)" = "Password (Optional)";
"Next" = "Next";
"Add" = "Add";
"Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device." = "Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device.";
"Restore your account by entering your seed below." = "Restore your account by entering your seed below.";
"Copy" = "Copy";
"Copied ✓" = "Copied ✓";
"Restore Using Seed" = "Restore Using Seed";
"Register" = "Register";
"Enter Your Seed" = "Enter Your Seed";
"Register a New Account" = "Register a New Account";
"Restore" = "Restore";
"Something went wrong. Please check your seed and try again." = "Something went wrong. Please check your seed and try again.";
"Looks like you didn't enter enough words. Please check your seed and try again." = "Looks like you didn't enter enough words. Please check your seed and try again.";
"You seem to be missing the last word of your seed. Please check what you entered and try again." = "You seem to be missing the last word of your seed. Please check what you entered and try again.";
"There appears to be an invalid word in your seed. Please check what you entered and try again." = "There appears to be an invalid word in your seed. Please check what you entered and try again.";
"Your seed couldn't be verified. Please check what you entered and try again." = "Your seed couldn't be verified. Please check what you entered and try again.";
"Search by public key" = "Search by public key";
"Start a Conversation" = "Start a Conversation";
"Invalid Session ID" = "Invalid Session ID";
"No search results" = "No search results";
"Calculating proof of work" = "Calculating proof of work";
"Failed to calculate proof of work." = "Failed to calculate proof of work.";
"Share Public Key" = "Share Public Key";
"%@ sent you a session request" = "%@ sent you a session request";
"Accept" = "Accept";
"Decline" = "Decline";
"Pending session request" = "Pending session request";
"New Message" = "New Message";
"Secure session reset in progress" = "Secure session reset in progress";
"Secure session reset done" = "Secure session reset done";
"Session" = "Session";
"You've sent %@ a session request" = "You've sent %@ a session request";
"You've declined %@'s session request" = "You've declined %@'s session request";
"You've accepted %@'s session request" = "You've accepted %@'s session request";
"%@ accepted your session request" = "%@ accepted your session request";
"%@'s session request has expired" = "%@'s session request has expired";
"Your session request to %@ has expired" = "Your session request to %@ has expired";
"Show Seed" = "Show Seed";
"Your Seed" = "Your Seed";
"Require Touch ID, Face ID or your device passcode to unlock Sessions screen. You can still receive notifications when Screen Lock is enabled. Use Sessions notification settings to customise the information displayed in notifications." = "Require Touch ID, Face ID or your device passcode to unlock Sessions screen. You can still receive notifications when Screen Lock is enabled. Use Sessions notification settings to customise the information displayed in notifications.";
"Prevent Session previews from appearing in the app switcher." = "Prevent Session previews from appearing in the app switcher.";
"Session" = "Session";
"Privacy Policy" = "Privacy Policy";
"New Session" = "New Session";
"Add Public Chat Server" = "Add Public Chat Server";
"Enter a Public Key" = "Enter a Public Key";
"Enter a Server URL" = "Enter a Server URL";
"For example: 059abcf223aa8c10e3dc2d623688b75dd25896794717e4a9c486772664fc95e41e." = "For example: 059abcf223aa8c10e3dc2d623688b75dd25896794717e4a9c486772664fc95e41e.";
"Invalid Session ID" = "Invalid Session ID";
"Please check the Session ID and try again" = "Please check the Session ID and try again";
"Looks like you don't have any conversations yet. Get started by messaging a friend." = "Looks like you don't have any conversations yet. Get started by messaging a friend.";
"Enter the public key of the person you'd like to securely message. They can share their public key with you by going into Session's in-app settings and clicking \"Share Public Key\"." = "Enter the public key of the person you'd like to securely message. They can share their public key with you by going into Session's in-app settings and clicking \"Share Public Key\".";
"Unlock Session" = "Unlock Session";
"Clear All Data" = "Clear All Data";
"Are you sure you want to clear all your data? This will delete your entire account, including all conversations and your personal key pair." = "Are you sure you want to clear all your data? This will delete your entire account, including all conversations and your personal key pair.";
"Cancel" = "Cancel";
"Update Required" = "Update Required";
"This version of Session is no longer supported. Please press OK to reset your account and migrate to the latest version." = "This version of Session is no longer supported. Please press OK to reset your account and migrate to the latest version.";
"Loki Public Chat" = "Loki Public Chat";
"Loki News" = "Loki News";
"Session Updates" = "Session Updates";
"Show QR Code" = "Show QR Code";
"This is your QR code. Other people can scan it to start a secure conversation with you." = "This is your QR code. Other people can scan it to start a secure conversation with you.";
"Scan a QR Code Instead" = "Scan a QR Code Instead";
"Session needs camera access to scan QR codes." = "Session needs camera access to scan QR codes.";
"You can enable camera access in your device settings." = "You can enable camera access in your device settings.";
"Scan QR Code" = "Scan QR Code";
"Loki" = "Loki";
"Can't Start Conversation" = "Can't Start Conversation";
"Please enter the public key of the person you'd like to message." = "Please enter the public key of the person you'd like to message.";
"Session is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't yet provide full privacy and shouldn't be used to transmit sensitive information." = "Session is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't yet provide full privacy and shouldn't be used to transmit sensitive information.";
"Copy Public Key" = "Copy Public Key";
"Link Device" = "Link Device";
"Waiting for Device" = "Waiting for Device";
"Waiting for Authorization" = "Waiting for Authorization";
"Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Session Account\" step to start the linking process" = "Create a new account on your other device and click \"Link Device\" when you're at the \"Create Your Session Account\" step to start the linking process";
"Linking Request Received" = "Linking Request Received";
"Invalid Session ID" = "Invalid Session ID";
"Please check that the words below match those shown on your other device" = "Please check that the words below match those shown on your other device";
"Link to an existing device by going into its in-app settings and clicking \"Link Device\"." = "Link to an existing device by going into its in-app settings and clicking \"Link Device\".";
"Authorize" = "Authorize";
"Enter the Other Device's Public Key" = "Enter the Other Device's Public Key";
"This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device." = "This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device.";
"Device Link Authorized" = "Device Link Authorized";
"Your device has been linked successfully" = "Your device has been linked successfully";
"Link" = "Link";
"Anonymous" = "Anonymous";
"Invalid URL" = "Invalid URL";
"Please check the URL you entered and try again." = "Please check the URL you entered and try again.";
"Please pick a shorter display name" = "Please pick a shorter display name";
"Please pick a display name" = "Please pick a display name";
"Add Public Chat" = "Add Public Chat";
"Enter a URL" = "Enter a URL";
"Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org." = "Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org.";
"Connecting..." = "Connecting...";
"Couldn't Connect" = "Couldn't Connect";
"Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters" = "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters";
"Multi Device Limit Reached" = "Multi Device Limit Reached";
"It's currently not allowed to link more than one device." = "It's currently not allowed to link more than one device.";
"Profile Picture" = "Profile Picture";
"Set Profile Picture" = "Set Profile Picture";
"Clear Profile Picture" = "Clear Profile Picture";
"Invalid QR Code" = "Invalid QR Code";
"Please make sure the QR code you scanned is correct and try again." = "Please make sure the QR code you scanned is correct and try again.";
"Devices" = "Devices";
"You haven't linked any devices yet" = "You haven't linked any devices yet";
"Link a Device (Beta)" = "Link a Device (Beta)";
"Unlink" = "Unlink";
"Change Name" = "Change Name";
"Change Device Name" = "Change Device Name";
"Enter the new display name for your device below" = "Enter the new display name for your device below";
"Enter a Name" = "Enter a Name";
"Error" = "Error";
"Please pick a name" = "Please pick a name";
"Couldn't Link Device" = "Couldn't Link Device";
"Couldn't Unlink Device" = "Couldn't Unlink Device";
"Please check your internet connection and try again" = "Please check your internet connection and try again";
"Device Unlinked" = "Device Unlinked";
"Your device was unlinked successfully" = "Your device was unlinked successfully";
"Unnamed Device" = "Unnamed Device";
"Linked device (%@)" = "Linked device (%@)";
"Restore session" = "Restore session";
"Would you like to start a new session with %@?" = "Would you like to start a new session with %@?";
// MARK: - Session
"Messages" = "Messages";
"Note to Self" = "Note to Self";
"New Group" = "New Group";
"Delete" = "Delete";
"Search" = "Search";
"New Session" = "New Session";
"Enter a Session ID" = "Enter a Session ID";
"Users can share their Session ID from their account settings, or by sharing their QR code." = "Users can share their Session ID from their account settings, or by sharing their QR code.";
"Scan a users QR code to start a session. QR codes can be found by tapping the QR code icon in account settings." = "Scan a users QR code to start a session. QR codes can be found by tapping the QR code icon in account settings.";
"Your Session ID" = "Your Session ID";
"Copy" = "Copy";
"Copied" = "Copied";
"Share" = "Share";
"Next" = "Next";
"Session needs camera access to scan QR codes" = "Session needs camera access to scan QR codes";
"Enable Camera Access" = "Enable Camera Access";
"Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Session's in-app settings and tapping \"Show QR Code\"." = "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Session's in-app settings and tapping \"Show QR Code\".";
"Enter Session ID" = "Enter Session ID";
"Open Group URL" = "Open Group URL";
"Scan QR Code" = "Scan QR Code";
"Scan the QR code of the open group you'd like to join" = "Scan the QR code of the open group you'd like to join";
"Join Open Group" = "Join Open Group";
"Enter an open group URL" = "Enter an open group URL";
"Invalid URL" = "Invalid URL";
"Please check the URL you entered and try again" = "Please check the URL you entered and try again";
"Couldn't Join" = "Couldn't Join";
"Settings" = "Settings";
"Privacy" = "Privacy";
"Notifications" = "Notifications";
"Devices" = "Devices";
"Recovery Phrase" = "Recovery Phrase";
"Clear All Data" = "Clear All Data";
"This will permanently delete your messages, sessions, and contacts." = "This will permanently delete your messages, sessions, and contacts.";
"Delete" = "Delete";
"This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device." = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.";
"The information shown in notifications when your phone is locked." = "The information shown in notifications when your phone is locked.";
"Notifications" = "Notifications";
"Back" = "Back";
"View My QR Code" = "View My QR Code";
"Scan someone's QR code to start a conversation with them" = "Scan someone's QR code to start a conversation with them";
"QR Code" = "QR Code";
"Scan Me" = "Scan Me";
"This is your QR code. Other users can scan it to start a session with you." = "This is your QR code. Other users can scan it to start a session with you.";
"Privacy" = "Privacy";
"Unlock Session's screen using Touch ID, Face ID, or your iOS device passcode. You can still receive message notifications while Screen Lock is enabled. Session's notification settings allow you to customize the information that is displayed." = "Unlock Session's screen using Touch ID, Face ID, or your iOS device passcode. You can still receive message notifications while Screen Lock is enabled. Session's notification settings allow you to customize the information that is displayed.";
"Sound" = "Sound";
"Content" = "Content";
"Update Profile Picture" = "Update Profile Picture";
"Couldn't Update Profile Picture" = "Couldn't Update Profile Picture";
"Clear" = "Clear";
"Enter a display name" = "Enter a display name";
"Your Session begins here..." = "Your Session begins here...";
"What's Session?" = "What's Session?";
"It's a decentralized, encrypted messaging app." = "It's a decentralized, encrypted messaging app.";
"So it doesn't collect my personal information or my conversation metadata? How does it work?" = "So it doesn't collect my personal information or my conversation metadata? How does it work?";
"Using a combination of advanced anonymous routing and end-to-end encryption technologies." = "Using a combination of advanced anonymous routing and end-to-end encryption technologies.";
"Friends don't let friends use compromised messengers. You're welcome." = "Friends don't let friends use compromised messengers. You're welcome.";
"Create Session ID" = "Create Session ID";
"Continue your Session" = "Continue your Session";
"Say hello to your Session ID" = "Say hello to your Session ID";
"Continue" = "Continue";
"Copy Session ID" = "Copy Session ID";
"Pick your display name" = "Pick your display name";
"Enter a display name" = "Enter a display name";
"Restore your account" = "Restore your account";
"Enter your recovery phrase" = "Enter your recovery phrase";
"Message" = "Message";
"You" = "You";
"Encrypting message" = "Encrypting message";
"Tracing a path" = "Tracing a path";
"Sending message" = "Sending message";
"Message sent securely" = "Message sent securely";
"Message failed to send" = "Message failed to send";
"Secure your account by saving your recovery phrase" = "Secure your account by saving your recovery phrase";
"Continue" = "Continue";
"Your Recovery Phrase" = "Your Recovery Phrase";
"Meet your recovery phrase" = "Meet your recovery phrase";
"Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone." = "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone.";
"Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID." = "Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID.";
"Hold to reveal" = "Hold to reveal";
"Make sure to store your recovery phrase in a safe place" = "Make sure to store your recovery phrase in a safe place";
"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\".";
"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.";
"Enter Session ID" = "Enter Session ID";
"Link your device" = "Link your device";
"Enter your Session ID to start the linking process." = "Enter your Session ID to start the linking process.";
"Enter your Session ID" = "Enter your Session ID";
"Recent Chats" = "Recent Chats";
"Other Chats" = "Other Chats";
"See and share when messages are being typed (applies to all sessions)." = "See and share when messages are being typed (applies to all sessions).";
"Disable Preview in App Switcher" = "Disable Preview in App Switcher";
"Are you sure? This cannot be undone." = "Are you sure? This cannot be undone.";
"When enabled, messages between you and %@ will disappear after they have been seen." = "When enabled, messages between you and %@ will disappear after they have been seen.";
"This will be your name when you use Session. It can be your real name, an alias, or anything else you like." = "This will be your name when you use Session. It can be your real name, an alias, or anything else you like.";
"Session Out of Sync" = "Session Out of Sync";
"Would you like to restore your session? This can help resolve issues. Your messages will be preserved." = "Would you like to restore your session? This can help resolve issues. Your messages will be preserved.";
"Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved." = "Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved.";
"Restore" = "Restore";
"Dismiss" = "Dismiss";
"New Closed Group" = "New Closed Group";
"Group Members" = "Group Members";
"You don't have any contacts yet" = "You don't have any contacts yet";
"Start a Session" = "Start a Session";
"Enter a group name" = "Enter a group name";
"Please enter a group name" = "Please enter a group name";
"Please enter a shorter group name" = "Please enter a shorter group name";
"Please pick at least 2 group members" = "Please pick at least 2 group members";
"Enable Link Previews?" = "Enable Link Previews?";
"You will not have full metadata protection when sending or receiving link previews." = "You will not have full metadata protection when sending or receiving link previews.";
"Open groups can be joined by anyone and do not provide full privacy protection" = "Open groups can be joined by anyone and do not provide full privacy protection";
"Search GIFs?" = "Search GIFs?";
"You will not have full metadata protection when sending GIFs." = "You will not have full metadata protection when sending GIFs.";
"The ability to add members to a closed group is coming soon." = "The ability to add members to a closed group is coming soon.";
"A closed group cannot have more than 10 members" = "A closed group cannot have more than 10 members";
"A closed group cannot have more than 50 members" = "A closed group cannot have more than 50 members";
"Closed groups support up to 10 members" = "Closed groups support up to 10 members";
"Closed groups support up to 50 members" = "Closed groups support up to 50 members";
"No messages yet" = "No messages yet";
"Would you like to join the Session Public Chat?" = "Would you like to join the Session Public Chat?";
"Join Public Chat" = "Join Public Chat";
"No, thank you" = "No, thank you";
"Report" = "Report";
"Please Pick an Option" = "Please Pick an Option";
"There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose." = "There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose.";
"Apple Push Notification Service" = "Apple Push Notification Service";
"Session will use the Apple Push Notification service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private." = "Session will use the Apple Push Notification service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.";
"Background Polling" = "Background Polling";
"Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed." = "Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.";
"Use APNs" = "Use APNs";
"Recommended" = "Recommended";
"Notification Strategy" = "Notification Strategy";
"Session now features two ways to handle push notifications. Make sure to read the descriptions carefully before you choose." = "Session now features two ways to handle push notifications. Make sure to read the descriptions carefully before you choose.";
"Push Notifications" = "Push Notifications";
"Confirm" = "Confirm";
"Skip" = "Skip";
"Link Previews" = "Link Previews";
"Invalid Session ID" = "Invalid Session ID";
"Please make sure the Session ID you entered is correct and try again." = "Please make sure the Session ID you entered is correct and try again.";
"Device Linking Failed" = "Device Linking Failed";
"Please check your internet connection and try again" = "Please check your internet connection and try again";
"Authorizing Device Link" = "Authorizing Device Link";
"Please wait while the device link is created. This can take up to a minute." = "Please wait while the device link is created. This can take up to a minute.";
"Path" = "Path";
"Session hides your IP by bouncing your messages through several Service Nodes in Sessions decentralized network. These are the countries your connection is currently being bounced through:" = "Session hides your IP by bouncing your messages through several Service Nodes in Sessions decentralized network. These are the countries your connection is currently being bounced through:";
"Entry Node" = "Entry Node";
"Service Node" = "Service Node";
"You" = "You";
"Destination" = "Destination";
"Learn More" = "Learn More";
"Please ask the open group operator to add you to the group." = "Please ask the open group operator to add you to the group.";
"Unauthorized" = "Unauthorized";
"Closed group created" = "Closed group created";
"Couldn't Create Group" = "Couldn't Create Group";
"Please check your internet connection and try again." = "Please check your internet connection and try again.";
"continue_2" = "Continue";
"copy" = "Copy";
"invalid_url" = "Invalid URL";
"copied_to_clipboard" = "Copied to clipboard";
"device_linking_failed" = "Couldn't link device.";
"next" = "Next";
"share" = "Share";
"invalid_session_id" = "Invalid Session ID";
"cancel" = "Cancel";
"your_session_id" = "Your Session ID";
"vc_landing_title_2" = "Your Session begins here...";
"vc_landing_register_button_title" = "Create Session ID";
"vc_landing_restore_button_title" = "Continue Your Session";
"vc_landing_link_button_title" = "Link to an existing account";
"vc_landing_device_unlinked_modal_title" = "Your device was unlinked successfully";
"view_fake_chat_bubble_1" = "What's Session?";
"view_fake_chat_bubble_2" = "It's a decentralized, encrypted messaging app";
"view_fake_chat_bubble_3" = "So it doesn't collect my personal information or my conversation metadata? How does it work?";
"view_fake_chat_bubble_4" = "Using a combination of advanced anonymous routing and end-to-end encryption technologies.";
"view_fake_chat_bubble_5" = "Friends don't let friends use compromised messengers. You're welcome.";
"vc_register_title" = "Say hello to your Session ID";
"vc_register_explanation" = "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.";
"vc_register_public_key_copied_message" = "Copied to clipboard";
"vc_restore_title" = "Restore your account";
"vc_restore_explanation" = "Enter the recovery phrase that was given to you when you signed up to restore your account.";
"vc_restore_seed_text_field_hint" = "Enter your recovery phrase";
"vc_link_device_title" = "Link Device";
"vc_link_device_enter_session_id_tab_title" = "Enter Session ID";
"vc_link_device_scan_qr_code_tab_title" = "Scan QR Code";
"vc_link_device_scan_qr_code_explanation" = "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.";
"vc_enter_session_id_title" = "Link your device";
"vc_enter_session_id_explanation" = "Navigate to \"Settings\" > \"Devices\" > \"Link a Device\" on your other device and then enter your Session ID here to start the linking process.";
"vc_enter_session_id_text_field_hint" = "Enter your Session ID";
"vc_display_name_title_2" = "Pick your display name";
"vc_display_name_explanation" = "This will be your name when you use Session. It can be your real name, an alias, or anything else you like.";
"vc_display_name_text_field_hint" = "Enter a display name";
"vc_display_name_display_name_missing_error" = "Please pick a display name";
"vc_display_name_display_name_invalid_error" = "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters";
"vc_display_name_display_name_too_long_error" = "Please pick a shorter display name";
"vc_pn_mode_title" = "Push Notifications";
"vc_pn_mode_explanation" = "There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "Session will use the Apple Push Notification Service service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.";
"vc_pn_mode_background_polling_option_title" = "Background Polling";
"vc_pn_mode_background_polling_option_explanation" = "Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.";
"vc_pn_mode_recommended_option_tag" = "Recommended";
"vc_pn_mode_no_option_picked_modal_title" = "Please Pick an Option";
"vc_home_empty_state_message" = "You don't have any contacts yet";
"vc_home_empty_state_button_title" = "Start a Session";
"vc_home_leave_group_modal_message" = "Are you sure you want to leave this group?";
"vc_home_leaving_group_failed_message" = "Couldn't leave group";
"vc_home_delete_conversation_modal_message" = "Are you sure you want to delete this conversation?";
"vc_home_conversation_deleted_message" = "Conversation deleted";
"sheet_pn_mode_title" = "Push Notifications";
"sheet_pn_mode_explanation" = "Session now features two ways to handle push notifications. Make sure to read the descriptions carefully before you choose.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "Session will use the Apple Push Notification Service service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private.";
"sheet_pn_mode_background_polling_option_title" = "Background Polling";
"sheet_pn_mode_background_polling_option_explanation" = "Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed.";
"sheet_pn_mode_recommended_option_tag" = "Recommended";
"sheet_pn_mode_no_option_picked_modal_title" = "Please Pick an Option";
"sheet_pn_mode_confirm_button_title" = "Confirm";
"sheet_pn_mode_skip_button_title" = "Skip";
"vc_seed_title" = "Your Recovery Phrase";
"vc_seed_title_2" = "Meet your recovery phrase";
"vc_seed_explanation" = "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone.";
"vc_seed_reveal_button_title" = "Hold to reveal";
"view_seed_reminder_subtitle_1" = "Secure your account by saving your recovery phrase";
"view_seed_reminder_subtitle_2" = "Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID.";
"view_seed_reminder_subtitle_3" = "Make sure to store your recovery phrase in a safe place";
"vc_path_title" = "Path";
"vc_path_explanation" = "Session hides your IP by bouncing your messages through several Service Nodes in Session's decentralized network. These are the countries your connection is currently being bounced through:";
"vc_path_device_row_title" = "You";
"vc_path_guard_node_row_title" = "Entry Node";
"vc_path_service_node_row_title" = "Service Node";
"vc_path_destination_row_title" = "Destination";
"vc_path_learn_more_button_title" = "Learn More";
"vc_create_private_chat_title" = "New Session";
"vc_create_private_chat_enter_session_id_tab_title" = "Enter Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Scan QR Code";
"vc_create_private_chat_scan_qr_code_explanation" = "Scan a users QR code to start a session. QR codes can be found by tapping the QR code icon in account settings.";
"vc_enter_public_key_text_field_hint" = "Enter Session ID of recipient";
"vc_enter_public_key_explanation" = "Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code.";
"vc_scan_qr_code_camera_access_explanation" = "Session needs camera access to scan QR codes";
"vc_scan_qr_code_grant_camera_access_button_title" = "Grant Camera Access";
"vc_create_closed_group_title" = "New Closed Group";
"vc_create_closed_group_text_field_hint" = "Enter a group name";
"vc_create_closed_group_explanation" = "Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions.";
"vc_create_closed_group_empty_state_message" = "You don't have any contacts yet";
"vc_create_closed_group_empty_state_button_title" = "Start a Session";
"vc_create_closed_group_group_name_missing_error" = "Please enter a group name";
"vc_create_closed_group_group_name_too_long_error" = "Please enter a shorter group name";
"vc_create_closed_group_not_enough_group_members_error" = "Please pick at least 2 group members";
"vc_create_closed_group_too_many_group_members_error" = "A closed group cannot have more than 10 members";
"vc_create_closed_group_invalid_session_id_error" = "One of the members of your group has an invalid Session ID";
"vc_join_public_chat_title" = "Join Open Group";
"vc_join_public_chat_error" = "Couldn't join group";
"vc_join_public_chat_enter_group_url_tab_title" = "Open Group URL";
"vc_join_public_chat_scan_qr_code_tab_title" = "Scan QR Code";
"vc_join_public_chat_scan_qr_code_explanation" = "Scan the QR code of the open group you'd like to join";
"vc_enter_chat_url_text_field_hint" = "Enter an open group URL";
"vc_enter_chat_url_privacy_warning" = "Open groups can be joined by anyone and do not provide full privacy protection";
"vc_settings_title" = "Settings";
"vc_settings_display_name_text_field_hint" = "Enter a display name";
"vc_settings_display_name_missing_error" = "Please pick a display name";
"vc_settings_invalid_display_name_error" = "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters";
"vc_settings_display_name_too_long_error" = "Please pick a shorter display name";
"vc_settings_privacy_button_title" = "Privacy";
"vc_settings_notifications_button_title" = "Notifications";
"vc_settings_chats_button_title" = "Chats";
"vc_settings_devices_button_title" = "Devices";
"vc_settings_recovery_phrase_button_title" = "Recovery Phrase";
"vc_settings_clear_all_data_button_title" = "Clear Data";
"vc_notification_settings_title" = "Notifications";
"vc_notification_settings_style_section_title" = "Notification Style";
"vc_notification_settings_content_section_title" = "Notification Content";
"vc_privacy_settings_title" = "Privacy";
"vc_chat_settings_title" = "Chats";
"vc_linked_devices_title" = "Devices";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Device Limit Reached";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "It's currently not allowed to link more than one device.";
"vc_linked_devices_unlinking_failed_message" = "Couldn't unlink device.";
"vc_linked_devices_unlinking_successful_message" = "Your device was unlinked successfully";
"vc_linked_devices_linking_failed_message" = "Couldn't link device.";
"vc_linked_devices_empty_state_message" = "You haven't linked any devices yet";
"vc_linked_devices_empty_state_button_title" = "Link a Device (Beta)";
"preferences_notifications_strategy_category_title" = "Notification Strategy";
"preferences_notifications_use_apns_option_title" = "Use APNs";
"preferences_notifications_use_apns_option_explanation" = "Using Apple Push Notification Service allows for more reliable push notifications, but exposes your IP and device token to Apple and Loki.";
"modal_link_device_slave_mode_title_1" = "Waiting for Authorization";
"modal_link_device_slave_mode_title_2" = "Device Link Authorized";
"modal_link_device_slave_mode_explanation_1" = "Please check that the words below match those shown on your other device.";
"modal_link_device_slave_mode_explanation_2" = "Your device has been linked successfully";
"modal_link_device_master_mode_title_1" = "Waiting for Device";
"modal_link_device_master_mode_title_2" = "Linking Request Received";
"modal_link_device_master_mode_title_3" = "Authorizing Device Link";
"modal_link_device_master_mode_explanation_1" = "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.";
"modal_link_device_master_mode_explanation_2" = "Please check that the words below match those shown on your other device.";
"modal_link_device_master_mode_explanation_3" = "Please wait while the device link is created. This can take up to a minute.";
"modal_link_device_master_mode_authorize_button_title" = "Authorize";
"vc_device_list_bottom_sheet_change_name_button_title" = "Change name";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Unlink device";
"modal_edit_device_name_text_field_hint" = "Enter a name";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.";
"modal_clear_all_data_title" = "Clear All Data";
"modal_clear_all_data_explanation" = "This will permanently delete your messages, sessions, and contacts.";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code";
"vc_qr_code_view_scan_qr_code_explanation" = "Scan someone's QR code to start a conversation with them";
"vc_view_my_qr_code_explanation" = "This is your QR code. Other users can scan it to start a session with you.";
"vc_view_my_qr_code_share_title" = "Share QR Code";
"view_friend_request_accept_button_title" = "Accept";
"view_friend_request_reject_button_title" = "Decline";
"view_friend_request_incoming_pending_message" = "%@ sent you a session request";
"view_friend_request_incoming_accepted_message" = "You've accepted %@'s session request";
"view_friend_request_incoming_declined_message" = "You've declined %@'s session request";
"view_friend_request_incoming_expired_message" = "%@'s session request has expired";
"view_friend_request_outgoing_pending_message" = "You've sent %@ a session request";
"view_friend_request_outgoing_accepted_message" = "%@ accepted your session request";
"view_friend_request_outgoing_expired_message" = "Your session request to %@ has expired";
"session_reset_banner_message" = "Would you like to restore your session with %@?";
"session_reset_banner_dismiss_button_title" = "Dismiss";
"session_reset_banner_restore_button_title" = "Restore";
"vc_contact_selection_contacts_title" = "Contacts";
"vc_contact_selection_closed_groups_title" = "Closed Groups";
"vc_contact_selection_open_groups_title" = "Open Groups";

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Has fijado la desaparición de mensajes en %@.";
// MARK: - Session
"continue_2" = "Continuar";
"copy" = "Copiar";
"invalid_url" = "URL no válida";
"copied_to_clipboard" = "Copiado en el portapapeles";
"device_linking_failed" = "No se pudo vincular el dispositivo.";
"next" = "Siguiente";
"share" = "Compartir";
"invalid_session_id" = "ID de Session no válida";
"cancel" = "Cancelar";
"your_session_id" = "Tu ID de Session";
"vc_landing_title_2" = "Tu Session comienza aquí...";
"vc_landing_register_button_title" = "Crear ID de Session";
"vc_landing_restore_button_title" = "Continúa tu Session";
"vc_landing_link_button_title" = "Vincular a una cuenta existente";
"vc_landing_device_unlinked_modal_title" = "Tu dispositivo se ha desvinculado correctamente";
"view_fake_chat_bubble_1" = "¿Qué es Session?";
"view_fake_chat_bubble_2" = "Es una aplicación de mensajería descentralizada y cifrada";
"view_fake_chat_bubble_3" = "¿Entonces no recopila mi información personal ni los metadatos de mi conversación? ¿Cómo funciona?";
"view_fake_chat_bubble_4" = "Utiliza una combinación de enrutamiento anónimo avanzado y tecnologías de cifrado de extremo a extremo.";
"view_fake_chat_bubble_5" = "Los amigos no dejan que sus amigos usen messengers riesgosos. De nada.";
"vc_register_title" = "Saluda a tu ID de Session";
"vc_register_explanation" = "Tu ID de Session es la dirección única que las personas pueden usar para contactarte en Session. Por diseño, tu ID de Session es totalmente anónima y privada, sin vínculo con tu identidad real.";
"vc_register_public_key_copied_message" = "Copiado en el portapapeles";
"vc_restore_title" = "Restaura tu cuenta";
"vc_restore_explanation" = "Ingresa la frase de recuperación que se te dio cuando te registraste para restaurar tu cuenta.";
"vc_restore_seed_text_field_hint" = "Ingresa tu frase de recuperación";
"vc_link_device_title" = "Vincular dispositivo";
"vc_link_device_enter_session_id_tab_title" = "Session ID";
"vc_link_device_scan_qr_code_tab_title" = "Escanear código QR";
"vc_link_device_scan_qr_code_explanation" = "Ve a Ajustes > Dispositivos > Vincular un dispositivo en tu otro dispositivo y después escanea el código QR que aparece para iniciar el proceso de vinculación.";
"vc_enter_session_id_title" = "Vincula tu dispositivo";
"vc_enter_session_id_explanation" = "Ve a Ajustes > Dispositivos > Vincular un dispositivo en tu otro dispositivo y después ingresa tu ID de Session aquí para iniciar el proceso de vinculación.";
"vc_enter_session_id_text_field_hint" = "Ingresa tu ID de Session";
"vc_display_name_title_2" = "Elige tu nombre";
"vc_display_name_explanation" = "Este será tu nombre al usar Session. Puede ser tu nombre real, un alias, o lo que prefieras.";
"vc_display_name_text_field_hint" = "Ingresa un nombre para mostrar";
"vc_display_name_display_name_missing_error" = "Por favor, elige un nombre para mostrar";
"vc_display_name_display_name_invalid_error" = "Por favor, elige un nombre para mostrar que contenga solo caracteres a-z, A-Z, 0-9 y _";
"vc_display_name_display_name_too_long_error" = "Por favor, elige un nombre para mostrar más corto";
"vc_pn_mode_title" = "Notificaciones Push";
"vc_pn_mode_explanation" = "Session tiene dos tipos de notificaciones push. Asegúrate de leer cuidadosamente las descripciones antes de elegir.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "Session usará el servicio Apple Push Notification Service para recibir notificaciones push. Recibirás notificaciones de nuevos mensajes de manera segura e inmediata. Usar APNs significa que tu dirección IP y device token serán compartidos con Apple. Este sería ya el caso si recibes notificaciones push con otras aplicaciones. Tu dirección IP y device token serán compartidos con Loki, pero tus mensajes seguirán teniendo enrutamiento cebolla y encriptación de extremo a extremo, por lo que el contenido de tus mensajes seguirá siendo completamente privado.";
"vc_pn_mode_background_polling_option_title" = "Sondeo en segundo plano";
"vc_pn_mode_background_polling_option_explanation" = "Session revisará si hay nuevos mensajes en segundo plano y de manera ocasional. Esto garantiza una protección total de la privacidad, pero las notificaciones de mensajes pueden retrasarse significativamente.";
"vc_pn_mode_recommended_option_tag" = "Recomendado";
"vc_pn_mode_no_option_picked_modal_title" = "Por favor, elige una opción";
"vc_home_empty_state_message" = "Aún no tienes contactos";
"vc_home_empty_state_button_title" = "Empieza una Session";
"vc_home_leave_group_modal_message" = "¿Seguro que quieres salir de este grupo?";
"vc_home_leaving_group_failed_message" = "No pudiste salir del grupo";
"vc_home_delete_conversation_modal_message" = "¿Seguro que quieres eliminar esta conversación?";
"vc_home_conversation_deleted_message" = "Conversación eliminada";
"sheet_pn_mode_title" = "Notificaciones Push";
"sheet_pn_mode_explanation" = "Session ahora tiene dos formas de manejar las notificaciones push. Asegúrate de leer las descripciones cuidadosamente antes de elegir.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "Session usará el servicio Apple Push Notification Service para recibir las notificaciones push. Recibirás notificaciones de nuevos mensajes de manera confiable e inmediata. Usar APNs significa que este dispositivo se comunicará directamente con los servidores de Apple para recuperar las notificaciones push, lo que expondrá tu dirección IP a Apple. A tus mensajes se les seguirá realizando enrutamiento cebolla y cifrado de extremo a extremo, por lo que el contenido de tus mensajes permanecerá completamente privado.";
"sheet_pn_mode_background_polling_option_title" = "Sondeo en segundo plano";
"sheet_pn_mode_background_polling_option_explanation" = "Session revisará si hay nuevos mensajes en segundo plano y de manera ocasional. Esto garantiza una protección total de los metadatos, pero las notificaciónes de nuevos mensajes pueden retrasarse significativamente.";
"sheet_pn_mode_recommended_option_tag" = "Recomendado";
"sheet_pn_mode_no_option_picked_modal_title" = "Por favor, elige una opción";
"sheet_pn_mode_confirm_button_title" = "Confirmar";
"sheet_pn_mode_skip_button_title" = "Omitir";
"vc_seed_title" = "Tu frase de recuperación";
"vc_seed_title_2" = "Guarda tu frase de recuperación";
"vc_seed_explanation" = "Tu frase de recuperación es la llave maestra de tu ID de Session, puedes usarla para recuperar tu ID de Session en caso de pérdida de acceso a tu dispositivo. Guarda tu frase de recuperación en un lugar seguro y no se la digas a nadie.";
"vc_seed_reveal_button_title" = "Mantén pulsado para revelar";
"view_seed_reminder_subtitle_1" = "Protege tu cuenta guardando tu frase de recuperación";
"view_seed_reminder_subtitle_2" = "Toca y mantén presionadas las palabras redactadas para revelar tu frase de recuperación, después guárdala de manera segura para proteger tu ID de Session.";
"view_seed_reminder_subtitle_3" = "Asegúrate de guardar tu frase de recuperación en un lugar seguro";
"vc_path_title" = "Ruta";
"vc_path_explanation" = "Session oculta tu dirección IP haciendo rebotar tus mensajes a través de los Nodos de servicio de la red descentralizada de Session. Estos son los países por los que tu conexión está siendo rebotada actualmente.";
"vc_path_device_row_title" = "Tú";
"vc_path_guard_node_row_title" = "Entry Node";
"vc_path_service_node_row_title" = "Service Node";
"vc_path_destination_row_title" = "Destino";
"vc_path_learn_more_button_title" = "Saber Más";
"vc_create_private_chat_title" = "Nueva Session";
"vc_create_private_chat_enter_session_id_tab_title" = "Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Escanear código QR";
"vc_create_private_chat_scan_qr_code_explanation" = "Escanea el código QR de un usuario para empezar una Session. Los códigos QR se pueden encontrar tocando el icono del código QR en los ajustes de la cuenta.";
"vc_enter_public_key_text_field_hint" = "Ingresa la ID de Session del destinatario";
"vc_enter_public_key_explanation" = "Los usuarios pueden compartir su ID de Session yendo a los ajustes de su cuenta y pulsando en Compartir ID de Session o compartiendo su código QR";
"vc_scan_qr_code_camera_access_explanation" = "Session necesita acceso a la cámara para escanear códigos QR";
"vc_scan_qr_code_grant_camera_access_button_title" = "Permitir acceso a cámara";
"vc_create_closed_group_title" = "Nuevo grupo cerrado";
"vc_create_closed_group_text_field_hint" = "Ingresa un nombre de grupo";
"vc_create_closed_group_explanation" = "Los grupos cerrados admiten hasta 10 miembros y brindan las mismas protecciones de privacidad que las sesiones individuales.";
"vc_create_closed_group_empty_state_message" = "Aún no tienes contactos";
"vc_create_closed_group_empty_state_button_title" = "Empezar una Session";
"vc_create_closed_group_group_name_missing_error" = "Por favor, ingresa un nombre de grupo";
"vc_create_closed_group_group_name_too_long_error" = "Por favor, ingresa un nombre de grupo más corto";
"vc_create_closed_group_not_enough_group_members_error" = "Por favor, elige al menos 2 miembros del grupo";
"vc_create_closed_group_too_many_group_members_error" = "Un grupo cerrado no puede tener más de 10 miembros";
"vc_create_closed_group_invalid_session_id_error" = "Uno de los miembros de tu grupo tiene un ID de Session no válido";
"vc_join_public_chat_title" = "Únete al grupo abierto";
"vc_join_public_chat_error" = "No te pudiste unir al grupo";
"vc_join_public_chat_enter_group_url_tab_title" = "URL de grupo abierto";
"vc_join_public_chat_scan_qr_code_tab_title" = "Escanear código QR";
"vc_join_public_chat_scan_qr_code_explanation" = "Escanea el código QR del grupo abierto al que quieras unirte";
"vc_enter_chat_url_text_field_hint" = "Ingresa una URL de grupo abierto";
"vc_enter_chat_url_privacy_warning" = "Cualquiera puede unirse a los grupos abiertos. Esto no brinda una protección completa de privacidad";
"vc_settings_title" = "Ajustes";
"vc_settings_display_name_text_field_hint" = "Ingresa un nombre para mostrar";
"vc_settings_display_name_missing_error" = "Por favor, elige un nombre para mostrar";
"vc_settings_invalid_display_name_error" = "Por favor, elige un nombre para mostrar que contenga solo caracteres a-z, A-Z, 0-9 y _";
"vc_settings_display_name_too_long_error" = "Por favor, elige un nombre para mostrar más corto";
"vc_settings_privacy_button_title" = "Privacidad";
"vc_settings_notifications_button_title" = "Notificaciones";
"vc_settings_chats_button_title" = "Chats";
"vc_settings_devices_button_title" = "Dispositivos";
"vc_settings_recovery_phrase_button_title" = "Frase de recuperación";
"vc_settings_clear_all_data_button_title" = "Borrar datos";
"vc_notification_settings_title" = "Notificaciones";
"vc_notification_settings_style_section_title" = "Estilo de Notificación";
"vc_notification_settings_content_section_title" = "Contenido de Notificación";
"vc_privacy_settings_title" = "Privacidad";
"vc_chat_settings_title" = "Chats";
"vc_linked_devices_title" = "Dispositivos";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Límite de dispositivos alcanzado";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "Actualmente no está permitido vincular más de un dispositivo.";
"vc_linked_devices_unlinking_failed_message" = "No se pudo desvincular el dispositivo.";
"vc_linked_devices_unlinking_successful_message" = "Tu dispositivo se ha desvinculado correctamente";
"vc_linked_devices_linking_failed_message" = "No se pudo vincular el dispositivo.";
"vc_linked_devices_empty_state_message" = "Aún no has enlazado ningún dispositivo";
"vc_linked_devices_empty_state_button_title" = "Enlazar un dispositivo";
"preferences_notifications_strategy_category_title" = "Estrategia de notificación";
"preferences_notifications_use_apns_option_title" = "Utilizar APNs";
"preferences_notifications_use_apns_option_explanation" = "El uso de Apple Push Notification Service permite notificaciones push más seguras, pero expone tu IP a Apple.";
"modal_link_device_slave_mode_title_1" = "Esperando la autorización";
"modal_link_device_slave_mode_title_2" = "Vinculación de dispositivo autorizada";
"modal_link_device_slave_mode_explanation_1" = "Por favor, revisa que las siguientes palabras coincidan con las que aparecen en tu otro dispositivo.";
"modal_link_device_slave_mode_explanation_2" = "Tu dispositivo se ha vinculado correctamente";
"modal_link_device_master_mode_title_1" = "Esperando el dispositivo";
"modal_link_device_master_mode_title_2" = "Solicitud de vinculación recibida";
"modal_link_device_master_mode_title_3" = "Autorizando la vinculación de dispositivo";
"modal_link_device_master_mode_explanation_1" = "Descarga Session en tu otro dispositivo y toca Vincular a una cuenta existente en la parte inferior de la pantalla de inicio. Si ya tienes una cuenta en tu otro dispositivo, primero deberás eliminar esa cuenta.";
"modal_link_device_master_mode_explanation_2" = "Por favor, revisa que las siguientes palabras coincidan con las que aparecen en tu otro dispositivo.";
"modal_link_device_master_mode_explanation_3" = "Espera mientras se crea la vinculación del dispositivo. Esto puede tomar hasta un minuto.";
"modal_link_device_master_mode_authorize_button_title" = "Autorizar";
"vc_device_list_bottom_sheet_change_name_button_title" = "Cambiar nombre";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Desvincular dispositivo";
"modal_edit_device_name_text_field_hint" = "Ingresa un nombre";
"modal_seed_title" = "Tu frase de recuperación";
"modal_seed_explanation" = "Esta es tu frase de recuperación. Con ella, puedes restaurar o migrar tu ID de Session a un nuevo dispositivo.";
"modal_clear_all_data_title" = "Borrar todos los datos";
"modal_clear_all_data_explanation" = "Esto eliminará permanentemente tu ID de Session, incluyendo todos los mensajes, sesiones y contactos.";
"vc_qr_code_title" = "Código QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Ver mi código QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR";
"vc_qr_code_view_scan_qr_code_explanation" = "Escanea el código QR de una persona para comenzar una conversación con ella";
"vc_view_my_qr_code_explanation" = "Este es tu código QR. Otros usuarios pueden escanearlo para empezar una Session contigo.";
"vc_view_my_qr_code_share_title" = "Compartir código QR";
"view_friend_request_accept_button_title" = "Aceptar";
"view_friend_request_reject_button_title" = "Rechazar";
"view_friend_request_incoming_pending_message" = "%1$s te envió una solicitud de Session";
"view_friend_request_incoming_accepted_message" = "Has aceptado la solicitud de Session de %1$s";
"view_friend_request_incoming_declined_message" = "Has rechazado la solicitud de Session de %1$s";
"view_friend_request_incoming_expired_message" = "La solicitud de Session de %1$s ha expirado";
"view_friend_request_outgoing_pending_message" = "Le has enviado una solicitud de Session a %1$s";
"view_friend_request_outgoing_accepted_message" = "%1$s aceptó tu solicitud de Session";
"view_friend_request_outgoing_expired_message" = "Tu solicitud de Session para %1$s ha expirado";
"session_reset_banner_message" = "¿Quieres restaurar tu Session con %s?";
"session_reset_banner_dismiss_button_title" = "Descartar";
"session_reset_banner_restore_button_title" = "Restaurar";
"vc_contact_selection_contacts_title" = "Contactos";
"vc_contact_selection_closed_groups_title" = "Grupos cerrados";
"vc_contact_selection_open_groups_title" = "Grupos abiertos";

View File

@ -2554,206 +2554,209 @@
// MARK: - Loki Messenger:
"Link Device" = "Relier un appareil";
"Prevent Session previews from appearing in the app switcher." = "Empêcher les aperçus de Session dapparaître dans le sélecteur dapplis.";
"Require Touch ID, Face ID or your device passcode to unlock Sessions screen. You can still receive notifications when Screen Lock is enabled. Use Sessions notification settings to customise the information displayed in notifications." = "Requiert Touch ID, Face ID ou le code d'accès de votre appareil pour débloquer Session. Vous continuerez de recevoir des notifications lorsque le verrouillage de l'écran est activé. Utilisez les paramètres de notification de Session pour personnaliser les informations affichées dans les notifications.";
"Cancel" = "Annuler";
"You haven't linked any devices yet" = "Vous n'avez encore relié aucun appareil";
"Link a Device (Beta)" = "Relier un appareil";
"Waiting for Device" = "En attente dune demande de liaison";
"Linking Request Received" = "Demande de liaison reçue";
"%@ sent you a session request" = "%@ vous a envoyé une demande de session";
"You've accepted %@'s session request" = "Vous avez accepté la demande de Session de %@";
"You've declined %@'s session request" = "Vous avez refusé la demande de Session de %@";
"%@'s session request has expired" = "La demande de Session de %@ a expiré";
"You've sent %@ a session request" = "Vous avez envoyé une demande de session à %@";
"%@ accepted your session request" = "%@ a accepté votre demande de Session";
"Your session request to %@ has expired" = "Votre demande de Session à %@ a expiré";
"Pending session request" = "Demande de session en attente";
"Accept" = "Accepter";
"Decline" = "Refuser";
"Secure session reset in progress" = "Réinitialisation sécurisée de la session en cours";
"Please check that the words below match those shown on your other device" = "Veuillez vérifier que les mots ci-dessous correspondent à ceux affichés sur votre autre appareil";
"Waiting for Authorization" = "En attente dautorisation";
"Authorize" = "Autoriser";
"Device Link Authorized" = "Liaison de l'appareil autorisée";
"Your device has been linked successfully" = "Votre appareil a été connecté avec succès";
"Your device was unlinked successfully" = "Votre appareil a été déconnecté avec succès";
"Couldn't Link Device" = "Impossible de relier l'appareil.";
"Couldn't Unlink Device" = "Impossible de déconnecter l'appareil";
"Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters" = "Veuillez choisir un nom d'utilisateur composé uniquement de caractères a-z, A-Z, 0-9 et _";
"Please pick a shorter display name" = "Veuillez choisir un nom d'utilisateur plus court";
"Please pick a display name" = "Veuillez choisir un nom d'utilisateur";
"Multi Device Limit Reached" = "Limite d'appareils atteinte";
"It's currently not allowed to link more than one device." = "Il n'est actuellement pas possible de relier plus d'un appareil.";
"Enter a Name" = "Saisissez un nom";
"Change Name" = "Changer de nom";
"Change Device Name" = "Modifier le nom de lappareil";
"Enter the new display name for your device below" = "Saisissez ci-dessous le nouveau nom pour votre appareil";
"Unlink" = "Déconnecter l'appareil";
// MARK: - Session
"continue_2" = "Continuer";
"copy" = "Copier";
"invalid_url" = "URL non valide";
"copied_to_clipboard" = "Copié dans le presse-papier";
"device_linking_failed" = "Impossible de relier l'appareil.";
"next" = "Suivant";
"share" = "Partager";
"invalid_session_id" = "Session ID non valide";
"cancel" = "Annuler";
"your_session_id" = "Votre Session ID";
"Messages" = "Messages";
"Note to Self" = "Note à mon intention";
"New Group" = "Nouveau groupe";
"Delete" = "Supprimer";
"Search" = "Recherche";
"New Session" = "Nouvelle Session";
"Enter a Session ID" = "Saisir un Session ID";
"Users can share their Session ID from their account settings, or by sharing their QR code." = "Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.";
"Scan a users QR code to start a session. QR codes can be found by tapping the QR code icon in account settings." = "Scannez le code QR d'un utilisateur pour démarrer une session. Les codes QR peuvent se trouver en appuyant sur l'icône du code QR dans les paramètres du compte.";
"Your Session ID" = "Votre Session ID";
"Copy" = "Copier";
"Copied" = "Copié";
"Share" = "Partager";
"Next" = "Suivant";
"Session needs camera access to scan QR codes" = "Session a besoin d'accéder à l'appareil photo pour scanner les codes QR";
"Enable Camera Access" = "Autoriser l'accès";
"Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Session's in-app settings and tapping \"Show QR Code\"." = "Scannez le code QR de la personne avec laquelle vous souhaitez établir une conversation sécurisée. Le code QR si situe dans les paramètres de l'application en cliquant sur \"Afficher le code QR\".";
"Enter Session ID" = "Saisir un Session ID";
"Open Group URL" = "URL du groupe public";
"Scan QR Code" = "Scanner un Code QR";
"Scan the QR code of the open group you'd like to join" = "Scannez le code QR du groupe public que vous souhaitez rejoindre";
"Join Open Group" = "Joindre un groupe public";
"Enter an open group URL" = "Saisissez une URL de groupe public";
"Invalid URL" = "URL non valide";
"Please check the URL you entered and try again" = "Vérifiez l'URL saisie, puis réessayez";
"Couldn't Join" = "Impossible de rejoindre le groupe";
"Settings" = "Paramètres";
"Privacy" = "Confidentialité";
"Notifications" = "Notifications";
"Devices" = "Appareils reliés";
"Recovery Phrase" = "Phrase de récupération";
"Clear All Data" = "Effacer toutes les données";
"This will permanently delete your messages, sessions, and contacts." = "Cela supprimera définitivement vos messages, vos sessions et vos contacts.";
"Delete" = "Supprimer";
"This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device." = "Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil.";
"The information shown in notifications when your phone is locked." = "Les informations affichées dans les notifications quand votre appareil est verrouillé.";
"Notifications" = "Notifications";
"Back" = "Retour";
"View My QR Code" = "Afficher mon code QR";
"Scan someone's QR code to start a conversation with them" = "Scannez le code QR d'un autre utilisateur pour démarrer une session";
"QR Code" = "Code QR";
"Scan Me" = "Scannez moi";
"This is your QR code. Other users can scan it to start a session with you." = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.";
"Privacy" = "Confidentialité";
"Unlock Session's screen using Touch ID, Face ID, or your iOS device passcode. You can still receive message notifications while Screen Lock is enabled. Session's notification settings allow you to customize the information that is displayed." = "Débloquez Session à l'aide de Touch ID, Face ID, ou votre mot de passe iOS. Vous Continuerez de recevoir des notifications lorsque l'écran est verrouillé. Les paramètres de notifications de Session vous permettent de personnaliser les informations affichées.";
"Sound" = "Son";
"Content" = "Contenu";
"Update Profile Picture" = "Mettre à jour la photo de profil";
"Couldn't Update Profile Picture" = "La photo de profil n'a pas pu être mise à jour";
"Clear" = "Effacer";
"Enter a display name" = "Saisissez un nom dutilisateur";
"Your Session begins here..." = "Votre Session débute ici...";
"What's Session?" = "Qu'est-ce que Session?";
"It's a decentralized, encrypted messaging app." = "C'est une application de messagerie décentralisée et cryptée.";
"So it doesn't collect my personal information or my conversation metadata? How does it work?" = "Elle ne recueille donc pas mes informations personnelles ou mes métadonnées de conversations? Comment ça marche?";
"Using a combination of advanced anonymous routing and end-to-end encryption technologies." = "En utilisant une combinaison de technologies avancées de routage anonyme et de chiffrement de bout en bout.";
"Friends don't let friends use compromised messengers. You're welcome." = "Les vrais amis ne laissent pas leurs amis utiliser des outils de messagerie compromis. De rien.";
"Create Session ID" = "Créer un Session ID";
"Continue your Session" = "Continuer votre Session";
"Say hello to your Session ID" = "Dites bonjour à votre Session ID";
"Continue" = "Continuer";
"Copy Session ID" = "Copier le Session ID";
"Pick your display name" = "Choisissez votre nom d'utilisateur";
"Enter a display name" = "Saisissez un nom d'utilisateur";
"Restore your account" = "Restaurez votre compte";
"Enter your recovery phrase" = "Saisissez votre phrase de récupération";
"Message" = "Message";
"You" = "Vous";
"Encrypting message" = "Chiffrement du message";
"Tracing a path" = "Traçage du chemin";
"Sending message" = "Envoi du message";
"Message sent securely" = "Message sécurisé envoyé";
"Message failed to send" = "Échec de l'envoi du message";
"Secure your account by saving your recovery phrase" = "Sécurisez votre compte en sauvegardant votre phrase de récupération";
"Continue" = "Continuer";
"Your Recovery Phrase" = "Votre phrase de récupération";
"Meet your recovery phrase" = "Voici votre phrase de récupération";
"Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone." = "Votre phrase de récupération est la clé principale de votre Session ID - vous pouvez l'utiliser pour restaurer votre Session ID si vous perdez l'accès à votre appareil. Conservez la dans un endroit sûr et ne la donnez à personne.";
"Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID." = "Appuyez et maintenez les mots masqués pour révéler votre phrase de récupération, puis stockez-la en toute sécurité pour sécuriser votre Session ID.";
"Hold to reveal" = "Appuyer pour révéler";
"Make sure to store your recovery phrase in a safe place" = "Assurez-vous de conserver votre phrase de récupération dans un endroit sûr";
"Link to an existing account" = "Relier à un compte existant";
"Enter your public key" = "Saisissez votre clé publique";
"Link to your existing account by going into your in-app settings and clicking \"Devices\"." = "Associez à votre compte existant en accédant aux paramètres de l'application et en cliquant sur \"Appareil reliés\".";
"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." = "Téléchargez Session sur votre autre appareil, puis cliquez sur \"Relier à un compte existant\" en bas de l'écran de daccueil. Si vous possédez déjà un compte sur votre autre appareil, vous devez d'abord le supprimer.";
"Group Settings" = "Paramètres du groupe";
"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." = "Votre Session ID est l'identifiant unique que les gens utilisent pour vous contacter dans Session. Sans lien avec votre identité réelle, votre Session ID est complètement anonyme et privé.";
"Enter the recovery phrase that was given to you when you signed up to restore your account." = "Pour restaurer votre compte, veuillez entrer la phrase de récupération qui vous a été fournie lors de la création de votre compte.";
"Enter Session ID" = "Saisir un Session ID";
"Link your device" = "Reliez votre appareil";
"Enter your Session ID to start the linking process." = "Saisissez votre Session ID pour démarrer le processus de liaison.";
"Enter your Session ID" = "Saisissez votre Session ID";
"Recent Chats" = "Conversations récentes";
"Other Chats" = "Autres conversations";
"See and share when messages are being typed (applies to all sessions)." = "Permet de voir et de partager quand les messages sont saisis (s'applique à toutes les sessions).";
"Disable Preview in App Switcher" = "Désactiver l'aperçu dans le sélecteur d'app";
"Are you sure? This cannot be undone." = "Êtes-vous sûr? Cette action est irréversible.";
"When enabled, messages between you and %@ will disappear after they have been seen." = "Lorsque activé, les messages entre vous et %@ disparaîtront après avoir été vus.";
"This will be your name when you use Session. It can be your real name, an alias, or anything else you like." = "Ce sera votre nom lorsque vous utiliserez Session. Il peut s'agir de votre vrai nom, d'un pseudo ou de ce que vous voulez.";
"Session Out of Sync" = "La session est désynchronisée";
"Would you like to restore your session? This can help resolve issues. Your messages will be preserved." = "Voulez-vous restaurer votre session? Cela peut aider à résoudre les problèmes. Vos messages seront conservés.";
"Would you like to restore your session with %@? This can help resolve issues. Your messages will be preserved." = "Voulez-vous restaurer votre session avec %@? Cela peut aider à résoudre les problèmes. Vos messages seront conservés.";
"Restore" = "Restaurer";
"Dismiss" = "Fermer";
"New Closed Group" = "Nouveau groupe privé";
"Group Members" = "Membres du groupe";
"You don't have any contacts yet" = "Vous navez pas encore de contacts.";
"Start a Session" = "Démarrer une session";
"Enter a group name" = "Saisissez un nom de groupe";
"Please enter a group name" = "Veuillez saisir un nom de groupe";
"Please enter a shorter group name" = "Veuillez saisir un nom de groupe plus court";
"Please pick at least 2 group members" = "Veuillez sélectionner au moins 2 membres";
"Enable Link Previews?" = "Activer les aperçus de liens?";
"You will not have full metadata protection when sending or receiving link previews." = "Vous ne disposez pas d'une protection totale des métadonnées lorsque vous envoyer ou recevez des aperçus de liens.";
"Open groups can be joined by anyone and do not provide full privacy protection" = "Les groupes publics peuvent être rejoints par n'importe qui et ne fournissent pas une confidentialité totale";
"Search GIFs?" = "Rechercher des GIFs?";
"You will not have full metadata protection when sending GIFs." = "Vous ne disposez pas d'une protection totale des métadonnées lorsque vous envoyer des GIFs.";
"The ability to add members to a closed group is coming soon." = "La possibilité d'ajouter des membres à un groupe privé arrive bientôt.";
"A closed group cannot have more than 10 members" = "Un groupe privé ne peut pas avoir plus de 10 membres";
"A closed group cannot have more than 50 members" = "Un groupe privé ne peut pas avoir plus de 50 membres";
"Closed groups support up to 10 members" = "Les groupes privés prennent en charge jusqu'à 10 membres";
"Closed groups support up to 50 members" = "Les groupes privés prennent en charge jusqu'à 50 membres";
"No messages yet" = "Aucun messages";
"Would you like to join the Session Public Chat?" = " Voulez-vous rejoindre le chat public de Session?";
"Join Public Chat" = "Rejoindre le chat public";
"No, thank you" = "Non merci";
"Report" = "Rapport";
"Please Pick an Option" = "Veuillez choisir une option";
"There are two ways Session can handle push notifications. Make sure to read the descriptions carefully before you choose." = "Session peut gérer les notifications push de deux manières. Assurez-vous de lire attentivement les détails avant de faire votre choix.";
"Apple Push Notification Service" = "Service Apple Push Notification";
"Session will use the Apple Push Notification service to receive push notifications. You'll be notified of new messages reliably and immediately. Using APNs means that your IP address and device token will be exposed to Apple. If you use push notifications for other apps, this will already be the case. Your IP address and device token will also be exposed to Loki, but your messages will still be onion-routed and end-to-end encrypted, so the contents of your messages will remain completely private." = "Session utilisera le service Apple Push Notification (APNs) pour la réception des notifications. Vous serez notifié immédiatement des nouveaux messages. L'utilisation d'APNs signifie que votre adresse IP et votre jeton d'appareil seront exposés à Apple. Cela est déjà le cas si vous utilisez des notifications push dans dautres applications. Ceux-ci seront également exposés à Loki. Vos messages seront toujours routés anonymement et chiffrés de bout en bout, ainsi leur contenu restera totalement confidentiel.";
"Background Polling" = "Consultation d'arrière-plan";
"Session will occasionally check for new messages in the background. This guarantees full metadata protection, but message notifications may be significantly delayed." = "Session vérifiera de temps en temps les nouveaux messages en arrière-plan. Cette option garantit une confidentialité totale, mais les notifications de messages peuvent être retardées.";
"Use APNs" = "Utiliser APNs";
"Recommended" = "Recommandé";
"Notification Strategy" = "Stratégie de notification";
"Session now features two ways to handle push notifications. Make sure to read the descriptions carefully before you choose." = "Session propose désormais deux façons de gérer les notifications push. Assurez-vous de lire attentivement les descriptions avant de choisir.";
"Push Notifications" = "Notifications Push";
"Confirm" = "Confirmer";
"Skip" = "Ignorer";
"Link Previews" = "Aperçus de liens";
"Invalid Session ID" = "Session ID non valide";
"Please make sure the Session ID you entered is correct and try again." = "Veuillez vérifier que le Session ID saisi est correct, puis réessayez.";
"Device Linking Failed" = "Échec de liaison de lappareil";
"Please check your internet connection and try again" = "Veuillez vérifiez votre connexion à Internet, puis réessayez";
"Authorizing Device Link" = "Autorisation de la liaison de l'appareil";
"Please wait while the device link is created. This can take up to a minute." = "Veuillez patienter pendant la création de la liaison. Cela peut prendre jusqu'à une minute.";
"Path" = "Chemin";
"Session hides your IP by bouncing your messages through several Service Nodes in Sessions decentralized network. These are the countries your connection is currently being bounced through:" = "Session occulte votre adresse IP en envoyant vos messages via plusieurs nœuds de service dans le réseau décentralisé de Session. Voici les pays par le biais desquels votre connexion est actuellement envoyée :";
"Entry Node" = "Noeud dentrée";
"Service Node" = "Noeud de service";
"You" = "Vous";
"Destination" = "Destination";
"Learn More" = "En savoir plus";
"Please ask the open group operator to add you to the group." = "Veuillez demander à l'opérateur du groupe public de vous ajouter au groupe.";
"Unauthorized" = "Non autorisé";
"Closed group created" = "Groupe privé créé";
"Couldn't Create Group" = "Impossible de créer le groupe";
"Please check your internet connection and try again." = "Veuillez vérifiez votre connexion à Internet, puis réessayez.";
"vc_landing_title_2" = "Votre Session débute ici...";
"vc_landing_register_button_title" = "Créer un Session ID";
"vc_landing_restore_button_title" = "Continuez votre Session";
"vc_landing_link_button_title" = "Relier à un compte existant";
"vc_landing_device_unlinked_modal_title" = "Votre appareil a été déconnecté avec succès";
"view_fake_chat_bubble_1" = "Qu'est-ce que Session ?";
"view_fake_chat_bubble_2" = "C'est une application de messagerie décentralisée et cryptée";
"view_fake_chat_bubble_3" = "Elle ne recueille donc pas mes informations personnelles ou mes métadonnées de conversations ? Comment ça marche ?";
"view_fake_chat_bubble_4" = "En utilisant une combinaison de technologies avancées de routage anonyme et de chiffrement de bout en bout.";
"view_fake_chat_bubble_5" = "Les vrais amis ne laissent pas leurs amis utiliser des outils de messagerie compromis. De rien.";
"vc_register_title" = "Dites bonjour à votre Session ID";
"vc_register_explanation" = "Votre Session ID est l'identifiant unique que les gens utilisent pour vous contacter dans Session. Sans lien avec votre identité réelle, votre Session ID est complètement anonyme et privé.";
"vc_register_public_key_copied_message" = "Copié dans le presse-papier";
"vc_restore_title" = "Restaurez votre compte";
"vc_restore_explanation" = "Pour restaurer votre compte, veuillez entrer la phrase de récupération qui vous a été fournie lors de la création de votre compte.";
"vc_restore_seed_text_field_hint" = "Saisissez votre phrase de récupération";
"vc_link_device_title" = "Relier un appareil";
"vc_link_device_enter_session_id_tab_title" = "Saisir un Session ID";
"vc_link_device_scan_qr_code_tab_title" = "Scanner un code QR";
"vc_link_device_scan_qr_code_explanation" = "Rendez-vous dans Paramètres > Appareils reliés > Relier un appareil sur votre autre appareil, puis scannez le code QR affiché pour démarrer le processus de liaison.";
"vc_enter_session_id_title" = "Reliez votre appareil";
"vc_enter_session_id_explanation" = "Rendez-vous dans Paramètres > Appareils reliés > Relier un appareil sur votre autre appareil, puis saisissez votre Session ID pour démarrer le processus de liaison.";
"vc_enter_session_id_text_field_hint" = "Saisissez votre Session ID";
"vc_display_name_title_2" = "Choisissez votre nom d'utilisateur";
"vc_display_name_explanation" = "Ce sera votre nom lorsque vous utiliserez Session. Il peut s'agir de votre vrai nom, d'un pseudo ou de ce que vous voulez.";
"vc_display_name_text_field_hint" = "Saisissez un nom d'utilisateur";
"vc_display_name_display_name_missing_error" = "Veuillez choisir un nom d'utilisateur";
"vc_display_name_display_name_invalid_error" = "Veuillez choisir un nom d'utilisateur composé uniquement de caractères a-z, A-Z, 0-9 et _";
"vc_display_name_display_name_too_long_error" = "Veuillez choisir un nom d'utilisateur plus court";
"vc_pn_mode_title" = "Notifications push";
"vc_pn_mode_explanation" = "Session peut gérer les notifications push de deux manières. Assurez-vous de lire attentivement les détails avant de faire votre choix.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "Session utilisera le service Apple Push Notification Service pour la réception des notifications. Vous serez notifié immédiatement des nouveaux messages. L'utilisation de APNs signifie que votre adresse IP et votre jeton d'appareil seront exposés à Apple. Cela est déjà le cas si vous utilisez des notifications push dans dautres applications. Ceux-ci seront également exposés à Loki. Vos messages seront toujours routés anonymement et chiffrés de bout en bout, ainsi leur contenu restera totalement confidentiel.";
"vc_pn_mode_background_polling_option_title" = "Consultation d'arrière-plan";
"vc_pn_mode_background_polling_option_explanation" = "Session vérifiera de temps en temps les nouveaux messages en arrière-plan. Cette option garantit une confidentialité totale, mais les notifications de messages peuvent être retardées.";
"vc_pn_mode_recommended_option_tag" = "Recommandé";
"vc_pn_mode_no_option_picked_modal_title" = "Veuillez choisir une option";
"vc_home_empty_state_message" = "Vous n'avez pas encore de contacts";
"vc_home_empty_state_button_title" = "Démarrez une Session";
"vc_home_leave_group_modal_message" = "Voulez-vous vraiment quitter ce groupe ?";
"vc_home_leaving_group_failed_message" = "Impossible de quitter le groupe";
"vc_home_delete_conversation_modal_message" = "Voulez-vous vraiment supprimer cette conversation ?";
"vc_home_conversation_deleted_message" = "Conversation supprimée";
"sheet_pn_mode_title" = "Notifications push";
"sheet_pn_mode_explanation" = "Session propose désormais deux façons de gérer les notifications push. Assurez-vous de lire attentivement les descriptions avant de choisir.";
"sheet_pn_mode_apns_option_title" = "Messagerie Cloud Firebase";
"sheet_pn_mode_apns_option_explanation" = "Session utilisera le service Apple Push Notification Service pour la réception des notifications. Vous serez notifié immédiatement des nouveaux messages. L'utilisation de APNs signifie que votre adresse IP et votre jeton d'appareil seront exposés à Apple. Cela est déjà le cas si vous utilisez des notifications push dans dautres applications. Ceux-ci seront également exposés à Loki. Vos messages seront toujours routés anonymement et chiffrés de bout en bout, ainsi leur contenu restera totalement confidentiel.";
"sheet_pn_mode_background_polling_option_title" = "Consultation d'arrière-plan";
"sheet_pn_mode_background_polling_option_explanation" = "Session vérifiera de temps en temps les nouveaux messages en arrière-plan. Cette option garantit une confidentialité totale, mais les notifications de messages peuvent être retardées.";
"sheet_pn_mode_recommended_option_tag" = "Recommandé";
"sheet_pn_mode_no_option_picked_modal_title" = "Veuillez choisir une option";
"sheet_pn_mode_confirm_button_title" = "Confirmer";
"sheet_pn_mode_skip_button_title" = "Ignorer";
"vc_seed_title" = "Votre phrase de récupération";
"vc_seed_title_2" = "Voici votre phrase de récupération";
"vc_seed_explanation" = "Votre phrase de récupération est la clé principale de votre Session ID - vous pouvez l'utiliser pour restaurer votre Session ID si vous perdez l'accès à votre appareil. Conservez la dans un endroit sûr et ne la donnez à personne.";
"vc_seed_reveal_button_title" = "Appuyer pour révéler";
"view_seed_reminder_subtitle_1" = "Sécurisez votre compte en sauvegardant votre phrase de récupération";
"view_seed_reminder_subtitle_2" = "Appuyez et maintenez les mots masqués pour révéler votre phrase de récupération, puis stockez-la en toute sécurité pour sécuriser votre Session ID.";
"view_seed_reminder_subtitle_3" = "Assurez-vous de conserver votre phrase de récupération dans un endroit sûr";
"vc_path_title" = "Chemin";
"vc_path_explanation" = "Session occulte votre adresse IP en envoyant vos messages via plusieurs nœuds de service dans le réseau décentralisé de Session. Voici les pays par le biais desquels votre connexion est actuellement envoyée :";
"vc_path_device_row_title" = "Vous";
"vc_path_guard_node_row_title" = "Noeud dentrée";
"vc_path_service_node_row_title" = "Noeud de service";
"vc_path_destination_row_title" = "Destination";
"vc_path_learn_more_button_title" = "En savoir plus";
"vc_create_private_chat_title" = "Nouvelle Session";
"vc_create_private_chat_enter_session_id_tab_title" = "Saisir un Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Scanner un Code QR";
"vc_create_private_chat_scan_qr_code_explanation" = "Scannez le code QR d'un utilisateur pour démarrer une session. Les codes QR peuvent se trouver en appuyant sur l'icône du code QR dans les paramètres du compte.";
"vc_enter_public_key_text_field_hint" = "Saisissez le Session ID du destinataire";
"vc_enter_public_key_explanation" = "Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.";
"vc_scan_qr_code_camera_access_explanation" = "Session a besoin d'accéder à l'appareil photo pour scanner les codes QR";
"vc_scan_qr_code_grant_camera_access_button_title" = "Autoriser l'accès";
"vc_create_closed_group_title" = "Nouveau groupe privé";
"vc_create_closed_group_text_field_hint" = "Saisissez un nom de groupe";
"vc_create_closed_group_explanation" = "Les groupes privés prennent en charge jusqu'à 10 membres et offrent le même niveau de confidentialité que les sessions individuelles.";
"vc_create_closed_group_empty_state_message" = "Vous n'avez pas encore de contacts";
"vc_create_closed_group_empty_state_button_title" = "Démarrer une session";
"vc_create_closed_group_group_name_missing_error" = "Veuillez saisir un nom de groupe";
"vc_create_closed_group_group_name_too_long_error" = "Veuillez saisir un nom de groupe plus court";
"vc_create_closed_group_not_enough_group_members_error" = "Veuillez sélectionner au moins 2 membres";
"vc_create_closed_group_too_many_group_members_error" = "Un groupe privé ne peut pas avoir plus de 10 membres";
"vc_create_closed_group_invalid_session_id_error" = "Un des membres de votre groupe a un Session ID non valide";
"vc_join_public_chat_title" = "Joindre un groupe public";
"vc_join_public_chat_error" = "Impossible de rejoindre le groupe";
"vc_join_public_chat_enter_group_url_tab_title" = "URL du groupe public";
"vc_join_public_chat_scan_qr_code_tab_title" = "Scannez le code QR";
"vc_join_public_chat_scan_qr_code_explanation" = "Scannez le code QR du groupe public que vous souhaitez rejoindre";
"vc_enter_chat_url_text_field_hint" = "Saisissez une URL de groupe public";
"vc_enter_chat_url_privacy_warning" = "Les groupes publics peuvent être rejoints par n'importe qui et ne fournissent pas une confidentialité totale";
"vc_settings_title" = "Paramètres";
"vc_settings_display_name_text_field_hint" = "Saisissez un nom d'utilisateur";
"vc_settings_display_name_missing_error" = "Veuillez choisir un nom d'utilisateur";
"vc_settings_invalid_display_name_error" = "Veuillez choisir un nom d'utilisateur composé uniquement de caractères a-z, A-Z, 0-9 et _";
"vc_settings_display_name_too_long_error" = "Veuillez choisir un nom d'utilisateur plus court";
"vc_settings_privacy_button_title" = "Confidientalité ";
"vc_settings_notifications_button_title" = "Notifications";
"vc_settings_chats_button_title" = "Chats";
"vc_settings_devices_button_title" = "Appareils reliés";
"vc_settings_recovery_phrase_button_title" = "Phrase de récupération";
"vc_settings_clear_all_data_button_title" = "Effacer les données";
"vc_notification_settings_title" = "Notifications";
"vc_notification_settings_style_section_title" = "Style de notification";
"vc_notification_settings_content_section_title" = "Contenu de notification";
"vc_privacy_settings_title" = "Confidientalité";
"vc_chat_settings_title" = "Chats";
"vc_linked_devices_title" = "Appareils reliés";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Limite d'appareils atteinte";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "Il n'est actuellement pas possible de relier plus d'un appareil.";
"vc_linked_devices_unlinking_failed_message" = "Impossible de déconnecter l'appareil.";
"vc_linked_devices_unlinking_successful_message" = "Votre appareil a été déconnecté avec succès";
"vc_linked_devices_linking_failed_message" = "Impossible de relier l'appareil.";
"vc_linked_devices_empty_state_message" = "Vous n'avez encore relié aucun appareil";
"vc_linked_devices_empty_state_button_title" = "Relier un appareil";
"preferences_notifications_strategy_category_title" = "Stratégie de notification";
"preferences_notifications_use_apns_option_title" = "Utiliser APNs";
"preferences_notifications_use_apns_option_explanation" = "L'utilisation de Apple Push Notification Service permet des notifications push plus fiables, mais expose votre IP et votre jeton d'appareil à Apple et à Loki.";
"modal_link_device_slave_mode_title_1" = "En attente d'autorisation";
"modal_link_device_slave_mode_title_2" = "Liaison de l'appareil autorisée";
"modal_link_device_slave_mode_explanation_1" = "Veuillez vérifier que les mots ci-dessous correspondent à ceux affichés sur votre autre appareil.";
"modal_link_device_slave_mode_explanation_2" = "Votre appareil a été connecté avec succès";
"modal_link_device_master_mode_title_1" = "En attente d'une demande de liaison";
"modal_link_device_master_mode_title_2" = "Demande de liaison reçue";
"modal_link_device_master_mode_title_3" = "Autorisation de la liaison de l'appareil";
"modal_link_device_master_mode_explanation_1" = "Téléchargez Session sur votre autre appareil, puis cliquez sur \"Relier à un compte existant\" en bas de l'écran de daccueil. Si vous possédez déjà un compte sur votre autre appareil, vous devrez d'abord le supprimer.";
"modal_link_device_master_mode_explanation_2" = "Veuillez vérifier que les mots ci-dessous correspondent à ceux affichés sur votre autre appareil.";
"modal_link_device_master_mode_explanation_3" = "Veuillez patienter pendant la création de la liaison. Cela peut prendre jusqu'à une minute.";
"modal_link_device_master_mode_authorize_button_title" = "Autoriser";
"vc_device_list_bottom_sheet_change_name_button_title" = "Modifier le nom";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Déconnecter l'appareil";
"modal_edit_device_name_text_field_hint" = "Saisissez un nom";
"modal_seed_title" = "Votre phrase de récupération";
"modal_seed_explanation" = "Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil.";
"modal_clear_all_data_title" = "Effacer toutes les données";
"modal_clear_all_data_explanation" = "Cela supprimera définitivement vos messages, vos sessions et vos contacts.";
"vc_qr_code_title" = "Code QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Afficher mon code QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scanner le code QR";
"vc_qr_code_view_scan_qr_code_explanation" = "Scannez le code QR d'un autre utilisateur pour démarrer une session";
"vc_view_my_qr_code_explanation" = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.";
"vc_view_my_qr_code_share_title" = "Partager le code QR";
"view_friend_request_accept_button_title" = "Accepter";
"view_friend_request_reject_button_title" = "Refuser";
"view_friend_request_incoming_pending_message" = "%@ vous a envoyé une demande de Session";
"view_friend_request_incoming_accepted_message" = "Vous avez accepté la demande de Session de %@";
"view_friend_request_incoming_declined_message" = "Vous avez refusé la demande de Session de %@";
"view_friend_request_incoming_expired_message" = "La demande de Session de %@ a expiré";
"view_friend_request_outgoing_pending_message" = "Vous avez envoyé une demande de Session à %@";
"view_friend_request_outgoing_accepted_message" = "%@ a accepté votre demande de Session";
"view_friend_request_outgoing_expired_message" = "Votre demande de Session à %@ a expiré";
"session_reset_banner_message" = "Voulez-vous restaurer votre session avec %@ ?";
"session_reset_banner_dismiss_button_title" = "Fermer";
"session_reset_banner_restore_button_title" = "Restaurer";
"vc_contact_selection_contacts_title" = "Contacts";
"vc_contact_selection_closed_groups_title" = "Groupes privés";
"vc_contact_selection_open_groups_title" = "Groupes publics";

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Hai impostato la scomparsa dei messaggi a %@.";
// MARK: - Session
"continue_2" = "Continua";
"copy" = "Copia";
"invalid_url" = "URL non valido";
"copied_to_clipboard" = "Copiato negli appunti";
"device_linking_failed" = "Impossibile collegare il dispositivo.";
"next" = "Successivo";
"share" = "Condividi";
"invalid_session_id" = "Sessione ID non valido";
"cancel" = "Annulla";
"your_session_id" = "La tua Sessione ID";
"vc_landing_title_2" = "La tua Sessione inizia qui...";
"vc_landing_register_button_title" = "Crea Sessione ID";
"vc_landing_restore_button_title" = "Continua la Sessione";
"vc_landing_link_button_title" = "Collegamento a un account esistente";
"vc_landing_device_unlinked_modal_title" = "Il dispositivo è stato scollegato correttamente";
"view_fake_chat_bubble_1" = "Che cos'è una Sessione?";
"view_fake_chat_bubble_2" = "È un'app di messaggistica decentralizzato e crittografato";
"view_fake_chat_bubble_3" = "Quindi non raccoglie informazioni personali o metadati di conversazione? Come funziona?";
"view_fake_chat_bubble_4" = "Utilizza una combinazione di routing anonimo avanzato e tecnologie di crittografia end-to-end.";
"view_fake_chat_bubble_5" = "Gli amici non lasciano i suoi amici di utilizzare messaggistica compromessa. Prego.";
"vc_register_title" = "Ecco la tua Sessione ID";
"vc_register_explanation" = "La Sessione ID è l'indirizzo univoco che le persone possono utilizzare per contattarti su una Sessione. Senza alcuna connessione con la tua vera identità, la Sessione ID è totalmente anonimo e privato fin dal incezione.";
"vc_register_public_key_copied_message" = "Copiato negli appunti";
"vc_restore_title" = "Ripristina il tuo account";
"vc_restore_explanation" = "Inserisci la frase di recupero che ti è stata data quando ti sei registrato per ripristinare il tuo account.";
"vc_restore_seed_text_field_hint" = "Inserisci la frase di recupero";
"vc_link_device_title" = "Collega dispositivo";
"vc_link_device_enter_session_id_tab_title" = "Inserisci la Sessione ID";
"vc_link_device_scan_qr_code_tab_title" = "Scansiona il codice QR";
"vc_link_device_scan_qr_code_explanation" = "Vai su Impostazioni > Dispositivi > Collega un dispositivo su un altro dispositivo, quindi effettua la scansione del codice QR visualizzato per avviare il processo di collegamento.";
"vc_enter_session_id_title" = "Collega il dispositivo";
"vc_enter_session_id_explanation" = "Vai su Impostazioni > Dispositivi > Collega un dispositivo su un altro dispositivo e inserisci la Sessione ID per avviare il processo di collegamento.";
"vc_enter_session_id_text_field_hint" = "Inserisci la Sessione ID";
"vc_display_name_title_2" = "Scegli il nome da visualizzare";
"vc_display_name_explanation" = "Questo sarà il tuo nome quando usi una Sessione. Può essere il tuo vero nome, un soprannome o qualsiasi altra cosa.";
"vc_display_name_text_field_hint" = "Inserisci il nome da visualizzare";
"vc_display_name_display_name_missing_error" = "Scegli il nome da visualizzare";
"vc_display_name_display_name_invalid_error" = "Il nome visualizzare può contenere solo i caratteri a-z, AZ, 0-9 e _ ";
"vc_display_name_display_name_too_long_error" = "Scegli un nome più breve";
"vc_pn_mode_title" = "Notifiche push";
"vc_pn_mode_explanation" = "La Sessione può gestire le notifiche push in due modi. Assicurati di leggere attentamente le descrizioni prima di scegliere.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "La Sessione utilizzerà il Apple Push Notification Service per ricevere notifiche push. Riceverai una notifica per i nuovi messaggi in modo affidabile e immediato. L'utilizzo di APNs implica che il tuo indirizzo IP e il token del dispositivo siano esposti a Apple. Se usi le notifiche push per altre app, ciò succede già. Il tuo indirizzo IP e il token del dispositivo saranno esposti anche a Loki, ma i messaggi saranno comunque resi anonimi tramite onion routing e crittografia end-to-end, pertanto il contenuto dei messaggi rimarrà completamente privato.";
"vc_pn_mode_background_polling_option_title" = "Polling nel background";
"vc_pn_mode_background_polling_option_explanation" = "Occasionalmente la Sessione verificherà la presenza di nuovi messaggi in background. Ciò garantisce la protezione completa dei metadati, ma le notifiche dei messaggi potrebbero subire notevoli ritardi.";
"vc_pn_mode_recommended_option_tag" = "Consigliato";
"vc_pn_mode_no_option_picked_modal_title" = "Scegli un'opzione";
"vc_home_empty_state_message" = "Non hai ancora nessun contatto";
"vc_home_empty_state_button_title" = "Inizia una sessione";
"vc_home_leave_group_modal_message" = "Sei sicuro di voler lasciare questo gruppo?";
"vc_home_leaving_group_failed_message" = "Impossibile lasciare il gruppo";
"vc_home_delete_conversation_modal_message" = "Sei sicuro di voler eliminare questa conversazione?";
"vc_home_conversation_deleted_message" = "Conversazione eliminata";
"sheet_pn_mode_title" = "Notifiche push";
"sheet_pn_mode_explanation" = "Ora la Sessione offre due modi per gestire le notifiche push. Assicurati di leggere attentamente le descrizioni prima di scegliere.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "La Sessione utilizzerà il Apple Push Notification Service per ricevere notifiche push. Riceverai una notifica per i nuovi messaggi in modo affidabile e immediato. L'utilizzo di APNs implica che il tuo indirizzo IP e il token del dispositivo siano esposti a Apple. Se usi le notifiche push per altre app, ciò succede già. Il tuo indirizzo IP e il token del dispositivo saranno esposti anche a Loki, ma i messaggi saranno comunque resi anonimi tramite onion routing e crittografia end-to-end, pertanto il contenuto dei messaggi rimarrà completamente privato.";
"sheet_pn_mode_background_polling_option_title" = "Polling nel background";
"sheet_pn_mode_background_polling_option_explanation" = "Occasionalmente la Sessione verificherà la presenza di nuovi messaggi in background. Ciò garantisce la protezione completa dei metadati, ma le notifiche dei messaggi potrebbero subire notevoli ritardi.";
"sheet_pn_mode_recommended_option_tag" = "Consigliato";
"sheet_pn_mode_no_option_picked_modal_title" = "Scegli un'opzione";
"sheet_pn_mode_confirm_button_title" = "Conferma";
"sheet_pn_mode_skip_button_title" = "Salta";
"vc_seed_title" = "Frase di recupero";
"vc_seed_title_2" = "La frase di recupero";
"vc_seed_explanation" = "La frase di recupero è la chiave principale per la Sessione ID: puoi usarla per ripristinare la Sessione ID se perdi l'accesso al dispositivo. Conserva la frase di recupero in un luogo sicuro e non rivelarla a nessuno.";
"vc_seed_reveal_button_title" = "Tieni premuto per rivelare";
"view_seed_reminder_subtitle_1" = "Proteggi il tuo account salvando la frase di recupero";
"view_seed_reminder_subtitle_2" = "Tocca e tieni premute le parole redatte per rivelare la frase di recupero, salva in modo sicuro per proteggere la tua Sessione ID.";
"view_seed_reminder_subtitle_3" = "Assicurati di salvare la frase di recupero in un luogo sicuro";
"vc_path_title" = "Percorso";
"vc_path_explanation" = "La Sessione nasconde il tuo IP facendo rimbalzare i messaggi attraverso diversi nodi di servizio nella sua rete decentralizzata. Questi sono i paesi in cui la connessione viene rimbalzata attualmente:";
"vc_path_device_row_title" = "Tu";
"vc_path_guard_node_row_title" = "Nodo di entrata";
"vc_path_service_node_row_title" = "Nodo di servizio";
"vc_path_destination_row_title" = "Destinazione";
"vc_path_learn_more_button_title" = "Per saperne di più";
"vc_create_private_chat_title" = "Nuova sessione";
"vc_create_private_chat_enter_session_id_tab_title" = "Inserisci la Sessione ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Scansiona il codice QR";
"vc_create_private_chat_scan_qr_code_explanation" = "Scansiona il codice QR di un utente per avviare una sessione. Puoi trovare i codici QR toccando l'icona Codice QR nelle impostazioni dell'account.";
"vc_enter_public_key_text_field_hint" = "Inserisci la Sessione ID del destinatario";
"vc_enter_public_key_explanation" = "Gli utenti possono condividere la propria Sessione ID accedendo alle impostazioni del proprio account e toccando Condividi la Sessione ID o condividendo il proprio codice QR.";
"vc_scan_qr_code_camera_access_explanation" = "La Sessione richiede l'accesso alla fotocamera per scansionare i codici QR";
"vc_scan_qr_code_grant_camera_access_button_title" = "Concedi l'accesso alla fotocamera";
"vc_create_closed_group_title" = "Nuovo gruppo chiuso";
"vc_create_closed_group_text_field_hint" = "Inserisci un nome per il gruppo";
"vc_create_closed_group_explanation" = "I gruppi chiusi supportano fino a 10 membri e forniscono le stesse protezioni per la privacy delle sessioni one-to-one.";
"vc_create_closed_group_empty_state_message" = "Non hai ancora nessun contatto";
"vc_create_closed_group_empty_state_button_title" = "Inizia una sessione";
"vc_create_closed_group_group_name_missing_error" = "Inserisci un nome per il gruppo";
"vc_create_closed_group_group_name_too_long_error" = "Inserisci un nome gruppo più breve";
"vc_create_closed_group_not_enough_group_members_error" = "Scegli almeno 2 membri del gruppo";
"vc_create_closed_group_too_many_group_members_error" = "Un gruppo chiuso non può avere più di 10 membri";
"vc_create_closed_group_invalid_session_id_error" = "Uno dei membri del tuo gruppo ha una Sessione ID non valido";
"vc_join_public_chat_title" = "Unisciti a un gruppo aperto";
"vc_join_public_chat_error" = "Impossibile unirsi al gruppo";
"vc_join_public_chat_enter_group_url_tab_title" = "Apri l'URL del gruppo";
"vc_join_public_chat_scan_qr_code_tab_title" = "Scansiona il codice QR";
"vc_join_public_chat_scan_qr_code_explanation" = "Scansiona il codice QR del gruppo aperto a cui desideri partecipare";
"vc_enter_chat_url_text_field_hint" = "Inserisci l'URL di un gruppo aperto";
"vc_enter_chat_url_privacy_warning" = "I gruppi aperti non offrono una protezione completa della privacy e chiunque può unirsi ad essi";
"vc_settings_title" = "Impostazioni";
"vc_settings_display_name_text_field_hint" = "Inserisci il nome da visualizzare";
"vc_settings_display_name_missing_error" = "Scegli il nome da visualizzare";
"vc_settings_invalid_display_name_error" = "Il nome visualizzare può contenere solo i caratteri a-z, AZ, 0-9 e _ ";
"vc_settings_display_name_too_long_error" = "Scegli un nome più breve";
"vc_settings_privacy_button_title" = "Privacy";
"vc_settings_notifications_button_title" = "Notifiche";
"vc_settings_chats_button_title" = "Chat";
"vc_settings_devices_button_title" = "Dispositivi";
"vc_settings_recovery_phrase_button_title" = "Frase di recupero";
"vc_settings_clear_all_data_button_title" = "Elimina dati";
"vc_notification_settings_title" = "Notifiche";
"vc_notification_settings_style_section_title" = "Stile della notifica";
"vc_notification_settings_content_section_title" = "Contenuto della notifica";
"vc_privacy_settings_title" = "Privacy";
"vc_chat_settings_title" = "Chat";
"vc_linked_devices_title" = "Dispositivi";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Limite del dispositivo raggiunto";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "Al momento non è consentito collegare più di un dispositivo.";
"vc_linked_devices_unlinking_failed_message" = "Impossibile scollegare il dispositivo.";
"vc_linked_devices_unlinking_successful_message" = "Il dispositivo è stato scollegato correttamente";
"vc_linked_devices_linking_failed_message" = "Impossibile collegare il dispositivo.";
"vc_linked_devices_empty_state_message" = "Non hai ancora collegato nessun dispositivo";
"vc_linked_devices_empty_state_button_title" = "Collega un dispositivo";
"preferences_notifications_strategy_category_title" = "Strategia di notifica";
"preferences_notifications_use_apns_option_title" = "Usa APNs";
"preferences_notifications_use_apns_option_explanation" = "L'uso di Apple Push Notification Service consente notifiche push più affidabili, ma espone l'IP e il token del dispositivo a Apple e Loki.";
"modal_link_device_slave_mode_title_1" = "In attesa di autorizzazione";
"modal_link_device_slave_mode_title_2" = "Collegamento al dispositivo autorizzato";
"modal_link_device_slave_mode_explanation_1" = "Verifica che le seguenti parole corrispondano a quelle visualizzate sull'altro dispositivo.";
"modal_link_device_slave_mode_explanation_2" = "Il dispositivo è stato collegato correttamente";
"modal_link_device_master_mode_title_1" = "In attesa del dispositivo";
"modal_link_device_master_mode_title_2" = "Richiesta di collegamento ricevuta";
"modal_link_device_master_mode_title_3" = "Autorizzazione al collegamento del dispositivo";
"modal_link_device_master_mode_explanation_1" = "Scarica la Sessione sull'altro dispositivo e tocca Collega a un account esistente nella parte inferiore della schermata di destinazione. Se disponi già di un account sull'altro dispositivo, dovrai prima eliminarlo.";
"modal_link_device_master_mode_explanation_2" = "Verifica che le seguenti parole corrispondano a quelle visualizzate sull'altro dispositivo.";
"modal_link_device_master_mode_explanation_3" = "Attendi mentre viene creato il collegamento del dispositivo. Ciò può richiedere fino a un minuto.";
"modal_link_device_master_mode_authorize_button_title" = "Autorizza";
"vc_device_list_bottom_sheet_change_name_button_title" = "Cambia nome";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Scollega dispositivo";
"modal_edit_device_name_text_field_hint" = "Inserisci un nome";
"modal_seed_title" = "Frase di recupero";
"modal_seed_explanation" = "Questa è la tua frase di recupero. Usala per ripristinare o migrare la Sessione ID a un nuovo dispositivo.";
"modal_clear_all_data_title" = "Elimina tutti i dati";
"modal_clear_all_data_explanation" = "Ciò eliminerà permanentemente i tuoi messaggi, sessioni e contatti.";
"vc_qr_code_title" = "Codice QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Visualizza il mio codice QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scansiona il codice QR";
"vc_qr_code_view_scan_qr_code_explanation" = "Scansiona il codice QR di un utente per iniziare una conversazione con questa persona";
"vc_view_my_qr_code_explanation" = "Questo è il tuo codice QR. Altri utenti possono scansionarlo per iniziare una sessione con te.";
"vc_view_my_qr_code_share_title" = "Condividi codice QR";
"view_friend_request_accept_button_title" = "Accetta";
"view_friend_request_reject_button_title" = "Rifiuta";
"view_friend_request_incoming_pending_message" = "%@ ti ha inviato una richiesta";
"view_friend_request_incoming_accepted_message" = "Hai accettato la richiesta di %@";
"view_friend_request_incoming_declined_message" = "Hai rifiutato la richiesta di %@";
"view_friend_request_incoming_expired_message" = "La richiesta di %@ è scaduta";
"view_friend_request_outgoing_pending_message" = "Hai inviato una richiesta a %@";
"view_friend_request_outgoing_accepted_message" = "%@ ha accettato la richiesta";
"view_friend_request_outgoing_expired_message" = "La tua richiesta a %@ è scaduta";
"session_reset_banner_message" = "Desideri ripristinare la sessione con %@?";
"session_reset_banner_dismiss_button_title" = "Rimuovi";
"session_reset_banner_restore_button_title" = "Ripristina";
"vc_contact_selection_contacts_title" = "Contatti";
"vc_contact_selection_closed_groups_title" = "Gruppi chiusi";
"vc_contact_selection_open_groups_title" = "Gruppi aperti";

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Você definiu a expiração de mensagens efêmeras para %@.";
// MARK: - Session
"continue_2" = "Continuar";
"copy" = "Copiar";
"invalid_url" = "URL inválido";
"copied_to_clipboard" = "Copiado para a área de transferência";
"device_linking_failed" = "Não foi possível sincronizar o dispositivo.";
"next" = "Próximo";
"share" = "Compartilhar";
"invalid_session_id" = "ID Session inválido";
"cancel" = "Cancelar";
"your_session_id" = "Seu ID Session";
"vc_landing_title_2" = "O Session começa aqui...";
"vc_landing_register_button_title" = "Criar ID Session";
"vc_landing_restore_button_title" = "Continuar com seu Session";
"vc_landing_link_button_title" = "Link para uma conta existente";
"vc_landing_device_unlinked_modal_title" = "O seu dispositivo foi dessincronizado com sucesso";
"view_fake_chat_bubble_1" = "O que é o Session?";
"view_fake_chat_bubble_2" = "É um aplicativo de mensagens criptografado e descentralizado";
"view_fake_chat_bubble_3" = "Então ele não coleta minhas informações pessoais ou meus metadados de conversa? Como funciona?";
"view_fake_chat_bubble_4" = "Usando uma combinação de tecnologias avançadas de roteamento anônimo e criptografia de ponta a ponta.";
"view_fake_chat_bubble_5" = "Amigos não deixam amigos usarem aplicativos de mensagem comprometidos. De nada.";
"vc_register_title" = "Diga olá ao seu ID Session";
"vc_register_explanation" = "Seu ID Session é o endereço exclusivo que as pessoas podem usar para entrar em contato com você no Session. Sem conexão com sua identidade real, seu ID Session é totalmente anônimo e privado por definição.";
"vc_register_public_key_copied_message" = "Copiado para a área de transferência";
"vc_restore_title" = "Restaurar sua conta";
"vc_restore_explanation" = "Digite a frase de recuperação que lhe foi fornecida quando você se inscreveu para restaurar sua conta.";
"vc_restore_seed_text_field_hint" = "Digite sua frase de recuperação";
"vc_link_device_title" = "Sincronize dispositivo";
"vc_link_device_enter_session_id_tab_title" = "Digite o ID Session";
"vc_link_device_scan_qr_code_tab_title" = "Escanear código QR";
"vc_link_device_scan_qr_code_explanation" = "Vá para Configurações > Dispositivos > Sincronize um dispositivo no seu outro dispositivo e, em seguida, escaneie o código QR que aparece para iniciar o processo de sincronização.";
"vc_enter_session_id_title" = "Sincronize seu dispositivo";
"vc_enter_session_id_explanation" = "Vá para Configurações > Dispositivos > Sincronizar um dispositivo no seu outro dispositivo e insira seu ID Session aqui para iniciar o processo de sincronização.";
"vc_enter_session_id_text_field_hint" = "Digite seu ID Session";
"vc_display_name_title_2" = "Escolha seu nome de exibição";
"vc_display_name_explanation" = "Este será o seu nome quando usar o Session. Pode ser seu nome verdadeiro, um apelido ou qualquer outra coisa que você quiser.";
"vc_display_name_text_field_hint" = "Digite um nome de exibição";
"vc_display_name_display_name_missing_error" = "Escolha um nome de exibição";
"vc_display_name_display_name_invalid_error" = "Escolha um nome de exibição que contenha apenas caracteres az, AZ, 0-9 e _";
"vc_display_name_display_name_too_long_error" = "Escolha um nome de exibição mais curto";
"vc_pn_mode_title" = "Notificações via push";
"vc_pn_mode_explanation" = "Há duas maneiras pelas quais o Session pode lidar com notificações push. Certifique-se de ler as descrições cuidadosamente antes de escolher.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "O Session usará o serviço Apple Push Notification Service para receber notificações push. Você será notificado sobre novas mensagens de maneira confiável e imediata. Usar o APNs significa que seu endereço IP e token do dispositivo serão expostos ao Apple. Se você usar notificações push para outros aplicativos, esse já será o caso. Seu endereço IP e o token do dispositivo também serão expostos ao Loki, mas suas mensagens ainda serão roteadas anonimamente e criptografadas de ponta a ponta, para que o conteúdo de suas mensagens permaneça completamente privado.";
"vc_pn_mode_background_polling_option_title" = "Pesquisa profunda";
"vc_pn_mode_background_polling_option_explanation" = "Ocasionalmente, o Session verifica novas mensagens em segundo plano. Isso garante proteção total aos metadados, mas as notificações de mensagens podem sofrer um atraso significativo.";
"vc_pn_mode_recommended_option_tag" = "Recomendado";
"vc_pn_mode_no_option_picked_modal_title" = "Escolha uma opção";
"vc_home_empty_state_message" = "Você ainda não possui contatos";
"vc_home_empty_state_button_title" = "Iniciar uma sessão";
"vc_home_leave_group_modal_message" = "Tem certeza de que deseja sair deste grupo?";
"vc_home_leaving_group_failed_message" = "Não foi possível sair do grupo";
"vc_home_delete_conversation_modal_message" = "Tem certeza de que deseja excluir esta conversa?";
"vc_home_conversation_deleted_message" = "Conversa excluída";
"sheet_pn_mode_title" = "Notificações via push";
"sheet_pn_mode_explanation" = "O Session agora apresenta duas maneiras de lidar com notificações push. Certifique-se de ler as descrições cuidadosamente antes de escolher.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "O Session usará o serviço Apple Push Notification Service para receber notificações push. Você será notificado sobre novas mensagens de maneira confiável e imediata. Usar o APNs significa que seu endereço IP e token do dispositivo serão expostos ao Apple. Se você usar notificações push para outros aplicativos, esse já será o caso. Seu endereço IP e o token do dispositivo também serão expostos ao Loki, mas suas mensagens ainda serão roteadas anonimamente e criptografadas de ponta a ponta, para que o conteúdo de suas mensagens permaneça completamente privado.";
"sheet_pn_mode_background_polling_option_title" = "Pesquisa profunda";
"sheet_pn_mode_background_polling_option_explanation" = "Ocasionalmente, o Session verificará novas mensagens em segundo plano. Isso garante proteção total aos metadados, mas as notificações de mensagens podem sofrer um atraso significativo.";
"sheet_pn_mode_recommended_option_tag" = "Recomendado";
"sheet_pn_mode_no_option_picked_modal_title" = "Escolha uma opção";
"sheet_pn_mode_confirm_button_title" = "Confirmar";
"sheet_pn_mode_skip_button_title" = "Pular";
"vc_seed_title" = "Sua frase de recuperação";
"vc_seed_title_2" = "Revele sua frase de recuperação";
"vc_seed_explanation" = "Sua frase de recuperação é a chave mestra do seu ID Session - você pode usá-la para restaurar seu ID Session se perder o acesso ao seu dispositivo. Armazene sua frase de recuperação em um local seguro e não a entregue a ninguém.";
"vc_seed_reveal_button_title" = "Segure para revelar";
"view_seed_reminder_subtitle_1" = "Proteja sua conta salvando sua frase de recuperação";
"view_seed_reminder_subtitle_2" = "Toque e segure as palavras editadas para revelar sua frase de recuperação e armazene-a com segurança para proteger seu ID Session.";
"view_seed_reminder_subtitle_3" = "Guarde sua frase de recuperação em um local seguro";
"vc_path_title" = "Caminho";
"vc_path_explanation" = "O Session oculta seu IP ao enviar suas mensagens através de vários Nós de Serviço na rede descentralizada do Session. Estes são os países pelos quais sua conexão está sendo ricocheteada no momento:";
"vc_path_device_row_title" = "Você";
"vc_path_guard_node_row_title" = "Nó de Entrada";
"vc_path_service_node_row_title" = "Nó de Serviço";
"vc_path_destination_row_title" = "Destino";
"vc_path_learn_more_button_title" = "Saber mais";
"vc_create_private_chat_title" = "Nova Sessão";
"vc_create_private_chat_enter_session_id_tab_title" = "Digite o ID Session";
"vc_create_private_chat_scan_qr_code_tab_title" = "Escanear código QR";
"vc_create_private_chat_scan_qr_code_explanation" = "Escaneie o código QR de um usuário para iniciar uma sessão. Os códigos QR podem ser encontrados tocando no ícone de código QR nas configurações da conta.";
"vc_enter_public_key_text_field_hint" = "Digite o ID Session do destinatário";
"vc_enter_public_key_explanation" = "Os usuários podem compartilhar seus IDs Session acessando as configurações da conta e tocando em Compartilhar ID Session, ou compartilhando o código QR.";
"vc_scan_qr_code_camera_access_explanation" = "O Session precisa de acesso à câmera para escanear códigos QR";
"vc_scan_qr_code_grant_camera_access_button_title" = "Conceder acesso à câmera";
"vc_create_closed_group_title" = "Novo grupo fechado";
"vc_create_closed_group_text_field_hint" = "Digite o nome do grupo";
"vc_create_closed_group_explanation" = "Grupos fechados suportam até 10 membros e fornecem as mesmas proteções de privacidade que as sessões individuais.";
"vc_create_closed_group_empty_state_message" = "Você ainda não possui contatos";
"vc_create_closed_group_empty_state_button_title" = "Iniciar uma sessão";
"vc_create_closed_group_group_name_missing_error" = "Digite um nome de grupo";
"vc_create_closed_group_group_name_too_long_error" = "Digite um nome de grupo mais curto";
"vc_create_closed_group_not_enough_group_members_error" = "Escolha pelo menos 2 membros do grupo";
"vc_create_closed_group_too_many_group_members_error" = "Um grupo fechado não pode ter mais de 10 membros";
"vc_create_closed_group_invalid_session_id_error" = "Um dos membros do seu grupo tem um ID Session inválido";
"vc_join_public_chat_title" = "Participar em grupo aberto";
"vc_join_public_chat_error" = "Não foi possível entrar no grupo";
"vc_join_public_chat_enter_group_url_tab_title" = "URL do grupo aberto";
"vc_join_public_chat_scan_qr_code_tab_title" = "Escanear código QR";
"vc_join_public_chat_scan_qr_code_explanation" = "Escaneie o código QR do grupo aberto no qual você deseja entrar";
"vc_enter_chat_url_text_field_hint" = "Digite a URL do grupo aberto";
"vc_enter_chat_url_privacy_warning" = "Grupos abertos podem ser compostos por qualquer pessoa e não oferecem proteção total à privacidade";
"vc_settings_title" = "Configurações";
"vc_settings_display_name_text_field_hint" = "Digite um nome de exibição";
"vc_settings_display_name_missing_error" = "Escolha um nome de exibição";
"vc_settings_invalid_display_name_error" = "Escolha um nome de exibição que contenha apenas caracteres az, AZ, 0-9 e _";
"vc_settings_display_name_too_long_error" = "Escolha um nome de exibição mais curto";
"vc_settings_privacy_button_title" = "Privacidade";
"vc_settings_notifications_button_title" = "Notificações";
"vc_settings_chats_button_title" = "Bate-papos";
"vc_settings_devices_button_title" = "Dispositivos";
"vc_settings_recovery_phrase_button_title" = "Frase de recuperação";
"vc_settings_clear_all_data_button_title" = "Apagar os dados";
"vc_notification_settings_title" = "Notificações";
"vc_notification_settings_style_section_title" = "Estilo de notificação";
"vc_notification_settings_content_section_title" = "Conteúdo da notificação";
"vc_privacy_settings_title" = "Privacidade";
"vc_chat_settings_title" = "Bate-papos";
"vc_linked_devices_title" = "Dispositivos";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Limite de dispositivos atingido";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "No momento, não é permitido sincronizar mais de um dispositivo.";
"vc_linked_devices_unlinking_failed_message" = "Não foi possível dessincronizar o dispositivo.";
"vc_linked_devices_unlinking_successful_message" = "O seu dispositivo foi dessincronizado com sucesso";
"vc_linked_devices_linking_failed_message" = "Não foi possível sincronizar o dispositivo.";
"vc_linked_devices_empty_state_message" = "Você ainda não sincronizou nenhum dispositivo";
"vc_linked_devices_empty_state_button_title" = "Sincronizar um dispositivo";
"preferences_notifications_strategy_category_title" = "Estratégia de notificação";
"preferences_notifications_use_apns_option_title" = "Usar APNs";
"preferences_notifications_use_apns_option_explanation" = "O uso do Apple Push Notification Service permite notificações push mais confiáveis, mas expõe seu IP e token de dispositivo ao Apple e ao Loki.";
"modal_link_device_slave_mode_title_1" = "Esperando autorização";
"modal_link_device_slave_mode_title_2" = "sincronização de dispositivo autorizada";
"modal_link_device_slave_mode_explanation_1" = "Verifique se as palavras abaixo correspondem às mostradas em seu outro dispositivo.";
"modal_link_device_slave_mode_explanation_2" = "Seu dispositivo foi sincronizado com sucesso";
"modal_link_device_master_mode_title_1" = "Aguardando dispositivo";
"modal_link_device_master_mode_title_2" = "Solicitação de sincronização recebida";
"modal_link_device_master_mode_title_3" = "Autorizando a sincronização de dispositivo";
"modal_link_device_master_mode_explanation_1" = "Baixe o Session em seu outro dispositivo e toque em Sincronizar a uma conta existente na parte inferior da tela de início. Se você já possui uma conta em seu outro dispositivo, precisará excluí-la primeiro.";
"modal_link_device_master_mode_explanation_2" = "Verifique se as palavras abaixo correspondem às mostradas em seu outro dispositivo.";
"modal_link_device_master_mode_explanation_3" = "Aguarde enquanto a sincronização do dispositivo é criada. Isso pode levar até um minuto.";
"modal_link_device_master_mode_authorize_button_title" = "Autorizar";
"vc_device_list_bottom_sheet_change_name_button_title" = "Mudar o nome";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Dessincronizar dispositivo";
"modal_edit_device_name_text_field_hint" = "Insira o nome";
"modal_seed_title" = "Sua frase de recuperação";
"modal_seed_explanation" = "Esta é sua frase de recuperação. Com ela, você pode restaurar ou migrar seu ID Session para um novo dispositivo.";
"modal_clear_all_data_title" = "Limpar todos os dados";
"modal_clear_all_data_explanation" = "Isso excluirá permanentemente suas mensagens, sessões e contatos.";
"vc_qr_code_title" = "Código QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Ver meu código QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR";
"vc_qr_code_view_scan_qr_code_explanation" = "Escaneie o código QR de alguém para iniciar uma conversa com essa pessoa";
"vc_view_my_qr_code_explanation" = "Este é o seu código QR. Outros usuários podem escaneá-lo para iniciar uma sessão com você.";
"vc_view_my_qr_code_share_title" = "Compartilhar código QR";
"view_friend_request_accept_button_title" = "Aceitar";
"view_friend_request_reject_button_title" = "Recusar";
"view_friend_request_incoming_pending_message" = "%@ enviou uma solicitação de sessão para você";
"view_friend_request_incoming_accepted_message" = "Você aceitou a solicitação de sessão de %@";
"view_friend_request_incoming_declined_message" = "Você recusou a solicitação de sessão de %@";
"view_friend_request_incoming_expired_message" = "A solicitação de sessão de %@ expirou";
"view_friend_request_outgoing_pending_message" = "Você enviou a %@ uma solicitação de sessão";
"view_friend_request_outgoing_accepted_message" = "%@ aceitou sua solicitação de sessão";
"view_friend_request_outgoing_expired_message" = "Sua solicitação de sessão para %@ expirou";
"session_reset_banner_message" = "Deseja restaurar sua sessão com %@?";
"session_reset_banner_dismiss_button_title" = "Dispensar";
"session_reset_banner_restore_button_title" = "Restaurar";
"vc_contact_selection_contacts_title" = "Contatos";
"vc_contact_selection_closed_groups_title" = "Grupos fechados";
"vc_contact_selection_open_groups_title" = "Grupos abertos";

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Вы установили время исчезновения сообщений — %@.";
// MARK: - Session
"continue_2" = "Продолжить";
"copy" = "Копировать";
"invalid_url" = "Неверная ссылка";
"copied_to_clipboard" = "Скопировано в буфер обмена";
"device_linking_failed" = "Не удалось привязать устройство.";
"next" = "Далее";
"share" = "Поделиться";
"invalid_session_id" = "Неверный Session ID";
"cancel" = "Отмена";
"your_session_id" = "Ваш Session ID";
"vc_landing_title_2" = "Здесь начинается Session...";
"vc_landing_register_button_title" = "Создать Session ID";
"vc_landing_restore_button_title" = "Восстановить Session ID";
"vc_landing_link_button_title" = "Привязать к существующему аккаунту";
"vc_landing_device_unlinked_modal_title" = "Ваше устройство успешно отвязано";
"view_fake_chat_bubble_1" = "Что такое Session?";
"view_fake_chat_bubble_2" = "Это децентрализованное, зашифрованное приложение для обмена сообщениями";
"view_fake_chat_bubble_3" = "Значит ли это, что оно не собирает мою личную информацию или метаданные моего разговора? Как оно работает?";
"view_fake_chat_bubble_4" = "С использованием комбинации передовых технологий анонимной маршрутизации и сквозного шифрования.";
"view_fake_chat_bubble_5" = "Друзья не позволят друзьям использовать ненадежные мессенджеры. Пользуйтесь на здоровье.";
"vc_register_title" = "Познакомьтесь со своим Session ID";
"vc_register_explanation" = "Ваш Session ID - это уникальный адрес, который могут использовать другие люди для связи с вами при помощи Session. Поскольку ваш Session ID никак не связан с вашей настоящей личностью, он по определению является полностью анонимным и конфиденциальным.";
"vc_register_public_key_copied_message" = "Скопировано в буфер обмена";
"vc_restore_title" = "Восстановите свой аккаунт";
"vc_restore_explanation" = "Для восстановления учетной записи введите секретную фразу, которая была предоставлена вам при регистрации.";
"vc_restore_seed_text_field_hint" = "Введите секретную фразу";
"vc_link_device_title" = "Привязать устройство";
"vc_link_device_enter_session_id_tab_title" = "Введите Session ID";
"vc_link_device_scan_qr_code_tab_title" = "Сканировать QR-код";
"vc_link_device_scan_qr_code_explanation" = "Перейдите в «Настройки» > «Устройства» > «Привязать устройство» на другом устройстве, а затем отсканируйте появившийся QR-код, чтобы начать процесс привязки.";
"vc_enter_session_id_title" = "Привяжите свое устройство";
"vc_enter_session_id_explanation" = "Перейдите в «Настройки» > «Устройства» > «Привязать устройство» на другом устройстве и введите сюда свой Session ID, чтобы начать процесс привязки.";
"vc_enter_session_id_text_field_hint" = "Введите свой Session ID";
"vc_display_name_title_2" = "Выберите ваше отображаемое имя";
"vc_display_name_explanation" = "Это имя будет отображаться, когда вы используете Session. Это может быть ваше настоящее имя, псевдоним или что угодно по вашему выбору.";
"vc_display_name_text_field_hint" = "Введите отображаемое имя";
"vc_display_name_display_name_missing_error" = "Пожалуйста, выберите отображаемое имя";
"vc_display_name_display_name_invalid_error" = "Пожалуйста, выберите отображаемое имя состоящее только из символов a-z, A-Z, 0-9 и _";
"vc_display_name_display_name_too_long_error" = "Пожалуйста, выберите более короткое отображаемое имя";
"vc_pn_mode_title" = "Всплывающие уведомления";
"vc_pn_mode_explanation" = "Есть два метода, при помощи которых Session может присылать всплывающие уведомления. Внимательно прочитайте их отличия прежде чем сделать выбор.";
"vc_pn_mode_apns_option_title" = "Apple Push Notification Service";
"vc_pn_mode_apns_option_explanation" = "Session будет использовать сервис Apple Push Notification Service (APNs) для получения всплывающих уведомлений. Вы будете надежно и незамедлительно получать уведомления о новых сообщениях. Использование APNs означает, что ваш IP-адрес и токен устройства будут доступны Apple. Обратите внимание что эта информация уже доступна Apple если вы используете всплывающие уведомления в других приложениях. Ваш IP-адрес и токен устройства также будут доступны Loki, но ваши сообщения будут по-прежнему маршрутизироваться и шифроваться, поэтому содержимое ваших сообщений останется полностью конфиденциальным.";
"vc_pn_mode_background_polling_option_title" = "Фоновые запросы";
"vc_pn_mode_background_polling_option_explanation" = "Session в фоновом режиме будет периодически проверять наличие новых сообщений. При этом гарантируется полная защита метаданных, однако возможна значительная задержка при уведомлении о сообщениях.";
"vc_pn_mode_recommended_option_tag" = "Рекомендуется";
"vc_pn_mode_no_option_picked_modal_title" = "Пожалуйста, выберите метод";
"vc_home_empty_state_message" = "У вас еще нет контактов";
"vc_home_empty_state_button_title" = "Начать Сессию";
"vc_home_leave_group_modal_message" = "Вы уверены, что хотите покинуть эту группу?";
"vc_home_leaving_group_failed_message" = "Не удалось покинуть группу";
"vc_home_delete_conversation_modal_message" = "Вы уверены, что хотите удалить этот разговор?";
"vc_home_conversation_deleted_message" = "Разговор удален";
"sheet_pn_mode_title" = "Всплывающее уведомление";
"sheet_pn_mode_explanation" = "Теперь при работе Session имеется два метода обработки всплывающих уведомлений. Внимательно прочитайте их отличия прежде чем сделать выбор.";
"sheet_pn_mode_apns_option_title" = "Apple Push Notification Service";
"sheet_pn_mode_apns_option_explanation" = "Session будет использовать сервис Apple Push Notification Service для получения всплывающих уведомлений. Вы будете надежно и незамедлительно получать уведомления о новых сообщениях. Использование APNs означает, что ваш IP-адрес и токен устройства будут доступны Apple. Так будет, если вы используете всплывающие уведомления для других приложений. Ваш IP-адрес и токен устройства также будут доступны Loki, но ваши сообщения будут по-прежнему маршрутизироваться и шифроваться, поэтому содержимое ваших сообщений останется полностью конфиденциальным.";
"sheet_pn_mode_background_polling_option_title" = "Фоновые запросы";
"sheet_pn_mode_background_polling_option_explanation" = "Session в фоновом режиме будет периодически проверять наличие новых сообщений. При этом гарантируется полная защита метаданных, однако возможна значительная задержка при уведомлении о сообщениях.";
"sheet_pn_mode_recommended_option_tag" = "Рекомендуется";
"sheet_pn_mode_no_option_picked_modal_title" = "Пожалуйста, выберите метод";
"sheet_pn_mode_confirm_button_title" = "Подтвердить";
"sheet_pn_mode_skip_button_title" = "Пропустить";
"vc_seed_title" = "Ваша секретная фраза для восстановления";
"vc_seed_title_2" = "А вот и ваша секретная фраза для восстановления";
"vc_seed_explanation" = "Ваша секретная фраза является главным ключом к вашему Session ID. Вы можете использовать ее для восстановления Session ID, если потеряете доступ к своему устройству. Сохраните свою секретную фразу в безопасном месте, и никому её не передавайте.";
"vc_seed_reveal_button_title" = "Удерживайте, чтобы показать";
"view_seed_reminder_subtitle_1" = "Защитите свой аккаунт, сохранив секретную фразу";
"view_seed_reminder_subtitle_2" = "Нажмите и удерживайте сокращенные слова, чтобы открыть секретную фразу, а затем сохраните ее в надежном месте, чтобы защитить свой Session ID.";
"view_seed_reminder_subtitle_3" = "Обязательно сохраните секретную фразу в надежном месте.";
"vc_path_title" = "Маршрут";
"vc_path_explanation" = "Session скрывает ваш IP, перенаправляя ваши сообщения через несколько сервисных узлов своей децентрализованной сети. Вот страны, через которые в данный момент проходит ваш сеанс связи:";
"vc_path_device_row_title" = "Вы";
"vc_path_guard_node_row_title" = "Узел входа";
"vc_path_service_node_row_title" = "Служебный узел";
"vc_path_destination_row_title" = "Место назначения";
"vc_path_learn_more_button_title" = "Узнать больше";
"vc_create_private_chat_title" = "Новый Диалог";
"vc_create_private_chat_enter_session_id_tab_title" = "Введите Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Сканировать QR-код";
"vc_create_private_chat_scan_qr_code_explanation" = "Сканируйте QR-код пользователя, чтобы начать сессию. QR-коды можно найти, нажав значок QR-кода в настройках учетной записи.";
"vc_enter_public_key_text_field_hint" = "Введите Session ID получателя";
"vc_enter_public_key_explanation" = "Пользователи могут поделиться своим Session ID, зайдя в настройки своей учетной записи и нажав «Отправить Session ID», или поделившись своим QR-кодом.";
"vc_scan_qr_code_camera_access_explanation" = "Session нужен доступ к камере для сканирования QR-кодов";
"vc_scan_qr_code_grant_camera_access_button_title" = "Предоставить доступ к камере";
"vc_create_closed_group_title" = "Новая закрытая группа";
"vc_create_closed_group_text_field_hint" = "Введите название группы";
"vc_create_closed_group_explanation" = "Закрытые группы поддерживают до 10 участников и обеспечивают те же меры защиты конфиденциальности, что и сессии один-на-один.";
"vc_create_closed_group_empty_state_message" = "У вас еще нет контактов";
"vc_create_closed_group_empty_state_button_title" = "Начать Сессию";
"vc_create_closed_group_group_name_missing_error" = "Пожалуйста, введите название группы";
"vc_create_closed_group_group_name_too_long_error" = "Пожалуйста, введите более короткое имя группы";
"vc_create_closed_group_not_enough_group_members_error" = "Пожалуйста, выберите как минимум 2 участников группы";
"vc_create_closed_group_too_many_group_members_error" = "В закрытой группе не может быть больше 10 участников";
"vc_create_closed_group_invalid_session_id_error" = "Один из участников вашей группы имеет недопустимый Session ID";
"vc_join_public_chat_title" = "Присоединиться к открытой группе";
"vc_join_public_chat_error" = "Не удалось присоединиться к группе";
"vc_join_public_chat_enter_group_url_tab_title" = "URL открытой группы";
"vc_join_public_chat_scan_qr_code_tab_title" = "Сканировать QR-код";
"vc_join_public_chat_scan_qr_code_explanation" = "Отсканируйте QR-код открытой группы, в которую вы хотите вступить";
"vc_enter_chat_url_text_field_hint" = "Введите URL открытой группы";
"vc_enter_chat_url_privacy_warning" = "К открытым группам может присоединиться кто угодно. Они не обеспечивают полной защиты конфиденциальности";
"vc_settings_title" = "Настройки";
"vc_settings_display_name_text_field_hint" = "Введите отображаемое имя";
"vc_settings_display_name_missing_error" = "Пожалуйста, выберите отображаемое имя";
"vc_settings_invalid_display_name_error" = "Пожалуйста, выберите отображаемое имя состоящее только из символов a-z, A-Z, 0-9 и _";
"vc_settings_display_name_too_long_error" = "Пожалуйста, выберите более короткое отображаемое имя";
"vc_settings_privacy_button_title" = "Конфиденциальность";
"vc_settings_notifications_button_title" = "Уведомления";
"vc_settings_chats_button_title" = "Чаты";
"vc_settings_devices_button_title" = "Устройства";
"vc_settings_recovery_phrase_button_title" = "Секретная фраза";
"vc_settings_clear_all_data_button_title" = "Очистить данные";
"vc_notification_settings_title" = "Уведомления";
"vc_notification_settings_style_section_title" = "Стиль уведомлений";
"vc_notification_settings_content_section_title" = "Содержание уведомления";
"vc_privacy_settings_title" = "Конфиденциальность";
"vc_chat_settings_title" = "Чаты";
"vc_linked_devices_title" = "Устройства";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "Достигнуто предельное кол-во устройств";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "В настоящее время запрещено привязывать более одного устройства.";
"vc_linked_devices_unlinking_failed_message" = "Не удалось отвязать устройство.";
"vc_linked_devices_unlinking_successful_message" = "Ваше устройство успешно отвязано";
"vc_linked_devices_linking_failed_message" = "Не удалось привязать устройство.";
"vc_linked_devices_empty_state_message" = "Вы еще не привязали ни одного устройства";
"vc_linked_devices_empty_state_button_title" = "Привязать устройство";
"preferences_notifications_strategy_category_title" = "Метод уведомлений";
"preferences_notifications_use_apns_option_title" = "Использовать APNs";
"preferences_notifications_use_apns_option_explanation" = "Использование Apple Push Notification Service позволяет получать более надежные всплывающие уведомления, но предоставляет ваш IP и токен устройства в адрес Apple и Loki.";
"modal_link_device_slave_mode_title_1" = "Ожидание авторизации";
"modal_link_device_slave_mode_title_2" = "Привязка устройства авторизована";
"modal_link_device_slave_mode_explanation_1" = "Пожалуйста, убедитесь, что слова ниже соответствуют тем, которые показаны на вашем другом устройстве.";
"modal_link_device_slave_mode_explanation_2" = "Ваше устройство успешно привязано";
"modal_link_device_master_mode_title_1" = "Ожидание устройства";
"modal_link_device_master_mode_title_2" = "Запрос на привязывание получен";
"modal_link_device_master_mode_title_3" = "Ссылка на авторизирующее устройство";
"modal_link_device_master_mode_explanation_1" = "Загрузите Session на другое устройство и нажмите «Привязать к существующей учетной записи» в нижней части целевого экрана. Если у вас уже есть учетная запись на другом устройстве, вам придется сначала удалить ту учетную запись.";
"modal_link_device_master_mode_explanation_2" = "Пожалуйста, убедитесь, что слова ниже соответствуют тем, которые показаны на вашем другом устройстве.";
"modal_link_device_master_mode_explanation_3" = "Пожалуйста, подождите, пока будет создана ссылка на устройство. Это может занять до минуты.";
"modal_link_device_master_mode_authorize_button_title" = "Авторизация";
"vc_device_list_bottom_sheet_change_name_button_title" = "Сменить имя";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "Отключить устройство";
"modal_edit_device_name_text_field_hint" = "Введите имя";
"modal_seed_title" = "Ваша секретная фраза";
"modal_seed_explanation" = "Это ваша секретная фраза. С ее помощью вы можете восстановить или перенести свой Session ID на новое устройство.";
"modal_clear_all_data_title" = "Очистить все данные";
"modal_clear_all_data_explanation" = "Это навсегда удалит ваши сообщения, сессии и контакты.";
"vc_qr_code_title" = "QR-код";
"vc_qr_code_view_my_qr_code_tab_title" = "Посмотреть мой QR-код";
"vc_qr_code_view_scan_qr_code_tab_title" = "Сканировать QR-код";
"vc_qr_code_view_scan_qr_code_explanation" = "Отсканируйте QR-код другого человека, чтобы начать с ним разговор";
"vc_view_my_qr_code_explanation" = "Это ваш QR-код. Другие пользователи могут сканировать его, чтобы начать диалог с вами.";
"vc_view_my_qr_code_share_title" = "Поделиться QR-кодом";
"view_friend_request_accept_button_title" = "Принять";
"view_friend_request_reject_button_title" = "Отклонить";
"view_friend_request_incoming_pending_message" = "%@ отправил(а) вам запрос на сессию";
"view_friend_request_incoming_accepted_message" = "Вы приняли запрос на сессию от %@";
"view_friend_request_incoming_declined_message" = "Вы отклонили запрос на сессию от %@";
"view_friend_request_incoming_expired_message" = "Время запроса на сессию от %@ истекло";
"view_friend_request_outgoing_pending_message" = "Вы отправили %@ запрос на сессию";
"view_friend_request_outgoing_accepted_message" = "%@ принял ваш запрос на сессию";
"view_friend_request_outgoing_expired_message" = "Время вашего запроса на сессию с %@ истекло";
"session_reset_banner_message" = "Хотите восстановить сессию с %@?";
"session_reset_banner_dismiss_button_title" = "Отклонить";
"session_reset_banner_restore_button_title" = "Восстановить";
"vc_contact_selection_contacts_title" = "Контакты";
"vc_contact_selection_closed_groups_title" = "Закрытые группы";
"vc_contact_selection_open_groups_title" = "Открытые группы";

View File

@ -2542,3 +2542,211 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "您设置了消息%@后消失。";
// MARK: - Session
"continue_2" = "继续";
"copy" = "复制";
"invalid_url" = "无效的网址";
"copied_to_clipboard" = "复制到剪贴板";
"device_linking_failed" = "无法链接设备。";
"next" = "下一步";
"share" = "分享";
"invalid_session_id" = "无效的Session编号";
"cancel" = "取消";
"your_session_id" = "您的Session编号";
"vc_landing_title_2" = "会话从这里开始...";
"vc_landing_register_button_title" = "注册Session";
"vc_landing_restore_button_title" = "恢复Session";
"vc_landing_link_button_title" = "链接到现有帐号";
"vc_landing_device_unlinked_modal_title" = "您的设备已成功取消链接";
"view_fake_chat_bubble_1" = "什么是Session";
"view_fake_chat_bubble_2" = "这是一个去中心化的加密消息应用程序";
"view_fake_chat_bubble_3" = "它不会收集我的个人信息或对话元数据。";
"view_fake_chat_bubble_4" = "结合高效的匿名路由和端到端的加密技术。";
"view_fake_chat_bubble_5" = "不允许朋友使用受损的Session。";
"vc_register_title" = "向Session打个招呼";
"vc_register_explanation" = "Session ID 是人们在会话中与您联系的唯一地址。与您的真实身份无关Session ID 完全是匿名的,在设计上是私有的。";
"vc_register_public_key_copied_message" = "复制到剪贴板";
"vc_restore_title" = "恢复您的帐号";
"vc_restore_explanation" = "输入您注册帐号时提供的恢复短语。";
"vc_restore_seed_text_field_hint" = "输入您的恢复短语";
"vc_link_device_title" = "链接设备";
"vc_link_device_enter_session_id_tab_title" = "输入Session ID";
"vc_link_device_scan_qr_code_tab_title" = "扫描二维码";
"vc_link_device_scan_qr_code_explanation" = "导航到“设置”>“设备”>“在其他设备上链接此设备”,然后扫描出现的二维码以开始链接过程。";
"vc_enter_session_id_title" = "关联您的设备";
"vc_enter_session_id_explanation" = "导航到“设置”>“设备”>“链接另一台设备”然后在此处输入Session ID 以开始链接过程。";
"vc_enter_session_id_text_field_hint" = "输入Session ID";
"vc_display_name_title_2" = "选择您的显示名称";
"vc_display_name_explanation" = "使用Session时这就是您的名字。它可以是您的真实姓名别名或您喜欢的其他任何名称。";
"vc_display_name_text_field_hint" = "输入显示名称";
"vc_display_name_display_name_missing_error" = "请选择一个显示名称";
"vc_display_name_display_name_invalid_error" = "请选择一个仅包含 azAZ0-9 和_字符的显示名称";
"vc_display_name_display_name_too_long_error" = "请选择一个较短的显示名称";
"vc_pn_mode_title" = "推送通知";
"vc_pn_mode_explanation" = "Session可以通过两种方式处理推送通知。选择之前请务必仔细阅读说明。";
"vc_pn_mode_apns_option_title" = "Apple通知推送服务";
"vc_pn_mode_apns_option_explanation" = "会话将使用Apple通知推送服务。您会立即可靠地收到新消息的通知。使用 APNs 意味着您的 IP 地址和设备令牌将向 Apple 公开。如果您对其他应用程序使用推送通知,则情况也如此。您的 IP 地址和设备令牌也将向 Loki 公开,但是您的消息仍将经过洋葱路由和端到端加密,因此消息的内容将完全保密。";
"vc_pn_mode_background_polling_option_title" = "背景轮询";
"vc_pn_mode_background_polling_option_explanation" = "Session偶尔会在后台检查新消息。这样可以保证完整的元数据保护但是消息通知可能会大大延迟。";
"vc_pn_mode_recommended_option_tag" = "推荐的选项";
"vc_pn_mode_no_option_picked_modal_title" = "请选择一个选项";
"vc_home_empty_state_message" = "您还没有任何联系人";
"vc_home_empty_state_button_title" = "开始对话";
"vc_home_leave_group_modal_message" = "您确定要离开这个群组吗?";
"vc_home_leaving_group_failed_message" = "无法离开群组";
"vc_home_delete_conversation_modal_message" = "您确定要删除此对话吗?";
"vc_home_conversation_deleted_message" = "对话已删除";
"sheet_pn_mode_title" = "推送通知";
"sheet_pn_mode_explanation" = "会话现在具有两种处理推送通知的方式。选择之前,请务必仔细阅读说明。";
"sheet_pn_mode_apns_option_title" = "Apple通知推送服务";
"sheet_pn_mode_apns_option_explanation" = "会话将使用Apple通知推送服务。您会立即可靠地收到新消息的通知。使用 APNs 意味着您的 IP 地址和设备令牌将向 Apple 公开。如果您对其他应用程序使用推送通知,则情况也如此。您的 IP 地址和设备令牌也将向 Loki 公开,但是您的消息仍将经过洋葱路由和端到端加密,因此消息的内容将完全保密。";
"sheet_pn_mode_background_polling_option_title" = "背景轮询";
"sheet_pn_mode_background_polling_option_explanation" = "Session偶尔会在后台检查新消息。这样可以保证完整的元数据保护但是消息通知可能会大大延迟。";
"sheet_pn_mode_recommended_option_tag" = "推荐的";
"sheet_pn_mode_no_option_picked_modal_title" = "请选择一个选项";
"sheet_pn_mode_confirm_button_title" = "确认";
"sheet_pn_mode_skip_button_title" = "跳过";
"vc_seed_title" = "您的恢复短语";
"vc_seed_title_2" = "符合您的恢复短语";
"vc_seed_explanation" = "恢复短语是Session ID 的主密钥 - 如果您无法访问设备则可以使用它来恢复Session ID。将您的恢复用语存储在安全的地方不要将其提供给任何人。";
"vc_seed_reveal_button_title" = "短语揭示";
"view_seed_reminder_subtitle_1" = "保存恢复短语以保护您的帐号安全";
"view_seed_reminder_subtitle_2" = "点击并按住删除的单词以显示您的恢复短语然后安全地存储它以保护Session ID。";
"view_seed_reminder_subtitle_3" = "确保将恢复短语存储在安全的地方";
"vc_path_title" = "路径";
"vc_path_explanation" = "会话通过Session的分散网络中的多个服务节点返回消息以隐藏 IP。在有些国家您的连接目前通过以下方式进行退回";
"vc_path_device_row_title" = "您";
"vc_path_guard_node_row_title" = "入口节点";
"vc_path_service_node_row_title" = "服务节点";
"vc_path_destination_row_title" = "目的地";
"vc_path_learn_more_button_title" = "学习更多";
"vc_create_private_chat_title" = "新建私人聊天";
"vc_create_private_chat_enter_session_id_tab_title" = "输入Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "扫描二维码";
"vc_create_private_chat_scan_qr_code_explanation" = "扫描用户的二维码以开始使用Session。可以通过在帐号设置中点击二维码图标找到二维码。";
"vc_enter_public_key_text_field_hint" = "输入对方的Session ID";
"vc_enter_public_key_explanation" = "用户可以通过进入帐号设置并点击共享Session ID 或共享其二维码来共享其Session ID。";
"vc_scan_qr_code_camera_access_explanation" = "Session需要摄像头访问才能扫描二维码";
"vc_scan_qr_code_grant_camera_access_button_title" = "授予摄像头访问权限";
"vc_create_closed_group_title" = "创建私密群组";
"vc_create_closed_group_text_field_hint" = "输入群组名称";
"vc_create_closed_group_explanation" = "私密群组最多支持 10 位成员,并提供与一对一会话相同的隐私保护。";
"vc_create_closed_group_empty_state_message" = "您还没有任何联系人";
"vc_create_closed_group_empty_state_button_title" = "开始对话";
"vc_create_closed_group_group_name_missing_error" = "请输入群组名称";
"vc_create_closed_group_group_name_too_long_error" = "请输入较短的群组名称";
"vc_create_closed_group_not_enough_group_members_error" = "请选择至少 2 位小组成员";
"vc_create_closed_group_too_many_group_members_error" = "私密群组成员不得超过 10 个";
"vc_create_closed_group_invalid_session_id_error" = "您群组中的一位成员的Session ID 无效";
"vc_join_public_chat_title" = "加入公开群组";
"vc_join_public_chat_error" = "无法加入群组";
"vc_join_public_chat_enter_group_url_tab_title" = "公开群组网址";
"vc_join_public_chat_scan_qr_code_tab_title" = "扫描二维码";
"vc_join_public_chat_scan_qr_code_explanation" = "扫描您想加入的公开群组的二维码";
"vc_enter_chat_url_text_field_hint" = "输入一个公开群组网址";
"vc_enter_chat_url_privacy_warning" = "公开群组可以被任何人加入,并且不能提供全面的隐私保护";
"vc_settings_title" = "设置";
"vc_settings_display_name_text_field_hint" = "输入显示的名称";
"vc_settings_display_name_missing_error" = "请选择一个显示名称";
"vc_settings_invalid_display_name_error" = "请选择一个仅包含 azAZ0-9 和 _ 字符的显示名称";
"vc_settings_display_name_too_long_error" = "请选择一个较短的显示名称";
"vc_settings_privacy_button_title" = "隐私";
"vc_settings_notifications_button_title" = "通知";
"vc_settings_chats_button_title" = "聊天";
"vc_settings_devices_button_title" = "设备";
"vc_settings_recovery_phrase_button_title" = "恢复短语";
"vc_settings_clear_all_data_button_title" = "清除数据";
"vc_notification_settings_title" = "通知";
"vc_notification_settings_style_section_title" = "通知类型";
"vc_notification_settings_content_section_title" = "通知内容";
"vc_privacy_settings_title" = "隐私";
"vc_chat_settings_title" = "聊天";
"vc_linked_devices_title" = "设备";
"vc_linked_devices_multi_device_limit_reached_modal_title" = "达到设备限制";
"vc_linked_devices_multi_device_limit_reached_modal_explanation" = "当前不允许链接多个设备。";
"vc_linked_devices_unlinking_failed_message" = "无法取消链接设备。";
"vc_linked_devices_unlinking_successful_message" = "您的设备已成功取消链接";
"vc_linked_devices_linking_failed_message" = "无法链接设备。";
"vc_linked_devices_empty_state_message" = "您尚未链接任何设备";
"vc_linked_devices_empty_state_button_title" = "链接设备(测试版)";
"preferences_notifications_strategy_category_title" = "通知选项";
"preferences_notifications_use_apns_option_title" = "使用 APNs";
"preferences_notifications_use_apns_option_explanation" = "使用Apple通知推送服务可以提供更可靠的推送通知但会将您的 IP 和设备令牌公开给 Apple 和 Loki。";
"modal_link_device_slave_mode_title_1" = "等待授权";
"modal_link_device_slave_mode_title_2" = "设备链接授权";
"modal_link_device_slave_mode_explanation_1" = "请检查以下单词是否与您其他设备上显示的单词匹配。";
"modal_link_device_slave_mode_explanation_2" = "您的设备已成功链接";
"modal_link_device_master_mode_title_1" = "等待设备";
"modal_link_device_master_mode_title_2" = "收到链接请求";
"modal_link_device_master_mode_title_3" = "授权设备链接";
"modal_link_device_master_mode_explanation_1" = "在其他设备上下载Session然后点击登录屏幕底部链接到现有帐号。如果您的其他设备上已有一个帐号则必须先删除该帐号。";
"modal_link_device_master_mode_explanation_2" = "请检查以下单词是否与您其他设备上显示的单词匹配。";
"modal_link_device_master_mode_explanation_3" = "创建设备关联时,请等待。这可能需要一分钟的时间。";
"modal_link_device_master_mode_authorize_button_title" = "授权";
"vc_device_list_bottom_sheet_change_name_button_title" = "更换名字";
"vc_device_list_bottom_sheet_unlink_device_button_title" = "取消链接设备";
"modal_edit_device_name_text_field_hint" = "输入名字";
"modal_seed_title" = "您的恢复短语";
"modal_seed_explanation" = "这是您的恢复短语。有了它您可以将Session ID 还原或迁移到新设备上。";
"modal_clear_all_data_title" = "清除所有数据";
"modal_clear_all_data_explanation" = "这将永久删除您的消息、会话和联系人。";
"vc_qr_code_title" = "二维码";
"vc_qr_code_view_my_qr_code_tab_title" = "查看我的二维码";
"vc_qr_code_view_scan_qr_code_tab_title" = "扫描二维码";
"vc_qr_code_view_scan_qr_code_explanation" = "扫描对方的二维码与他们开始对话";
"vc_view_my_qr_code_explanation" = "这是您的二维码。其他用户可以对其进行扫描以开始对话。";
"vc_view_my_qr_code_share_title" = "分享二维码";
"view_friend_request_accept_button_title" = "接受";
"view_friend_request_reject_button_title" = "拒绝";
"view_friend_request_incoming_pending_message" = "%@向您发送了一个会话请求";
"view_friend_request_incoming_accepted_message" = "您已接受%@的会话请求";
"view_friend_request_incoming_declined_message" = "您拒绝了%@的会话请求";
"view_friend_request_incoming_expired_message" = "%@的会话请求已过期";
"view_friend_request_outgoing_pending_message" = "您已向%@发送了会话请求";
"view_friend_request_outgoing_accepted_message" = "%@接受了会话请求";
"view_friend_request_outgoing_expired_message" = "您对%@的会话请求已过期";
"session_reset_banner_message" = "您要恢复与%@的会话吗?";
"session_reset_banner_dismiss_button_title" = "解散";
"session_reset_banner_restore_button_title" = "恢复";
"vc_contact_selection_contacts_title" = "联系人";
"vc_contact_selection_closed_groups_title" = "私密群组";
"vc_contact_selection_open_groups_title" = "公开群组";

View File

@ -201,7 +201,7 @@ NS_ASSUME_NONNULL_BEGIN
// Existing threads are listed first, ordered by most recently active
OWSTableSection *recentChatsSection = [OWSTableSection new];
recentChatsSection.headerTitle = NSLocalizedString(@"Recent Chats", @"");
recentChatsSection.headerTitle = NSLocalizedString(@"SELECT_THREAD_TABLE_RECENT_CHATS_TITLE", @"");
for (TSThread *thread in [self filteredThreadsWithSearchText]) {
[recentChatsSection
addItem:[OWSTableItem
@ -277,7 +277,7 @@ NS_ASSUME_NONNULL_BEGIN
// Contacts who don't yet have a thread are listed last
OWSTableSection *otherContactsSection = [OWSTableSection new];
otherContactsSection.headerTitle = NSLocalizedString(@"Other Chats", @"");
otherContactsSection.headerTitle = @"Other Chats";
NSArray<SignalAccount *> *filteredSignalAccounts = [self filteredSignalAccountsWithSearchText];
for (SignalAccount *signalAccount in filteredSignalAccounts) {
[otherContactsSection

View File

@ -551,7 +551,7 @@ public class SignalAttachment: NSObject {
}
let dataSource = DataSourceValue.dataSource(with: data, utiType: dataUTI)
// Pasted images _SHOULD _NOT_ be resized, if possible.
return attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
return attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
}
}
for dataUTI in videoUTISet {

View File

@ -95,11 +95,6 @@ NS_ASSUME_NONNULL_BEGIN
if ([self isVersion:previousVersion atLeast:@"2.0.0" andLessThan:@"2.3.0"] && [self.tsAccountManager isRegistered]) {
[self clearBloomFilterCache];
}
// Loki
if ([self isVersion:previousVersion lessThan:@"1.2.1"] && [self.tsAccountManager isRegistered]) {
[self updatePublicChatMapping];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion];
@ -168,43 +163,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
# pragma mark Loki - Upgrading to Public Chat Manager
// Versions less than or equal to 1.2.0 didn't store public chat mappings
+ (void)updatePublicChatMapping
{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id] transaction:transaction];
if (thread != nil) {
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
} else {
// Update the group type and group ID for private group chat version.
// If the thread is still using the old group ID, it needs to be updated.
thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction];
if (thread != nil) {
thread.groupModel.groupType = openGroup;
[thread.groupModel updateGroupId:[LKGroupUtilities getEncodedOpenGroupIDAsData:chat.id]];
[thread saveWithTransaction:transaction];
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
}
}
}
// Update RSS feeds here
LKRSSFeed *lokiNewsFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:NSLocalizedString(@"Loki News", @"") isDeletable:true];
LKRSSFeed *lokiMessengerUpdatesFeed = [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:NSLocalizedString(@"Session Updates", @"") isDeletable:false];
NSArray *feeds = @[ lokiNewsFeed, lokiMessengerUpdatesFeed ];
for (LKRSSFeed *feed in feeds) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:[feed.id dataUsingEncoding:NSUTF8StringEncoding] transaction:transaction];
if (thread != nil) {
thread.groupModel.groupType = rssFeed;
[thread.groupModel updateGroupId:[LKGroupUtilities getEncodedRSSFeedIDAsData:feed.id]];
[thread saveWithTransaction:transaction];
}
}
} error:nil];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,127 +0,0 @@
import PromiseKit
import SessionMetadataKit
internal class LokiFileServerProxy : LokiHTTPClient {
private let server: String
private let keyPair = Curve25519.generateKeyPair()
private static let fileServerPublicKey: Data = {
let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc"
let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString()
let hexEncodedPublicKey = hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded()
return Data(hex: hexEncodedPublicKey)
}()
// MARK: Error
internal enum Error : LocalizedError {
case symmetricKeyGenerationFailed
case endpointParsingFailed
case proxyResponseParsingFailed
case fileServerHTTPError(code: Int, message: Any?)
internal var errorDescription: String? {
switch self {
case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key."
case .endpointParsingFailed: return "Couldn't parse endpoint."
case .proxyResponseParsingFailed: return "Couldn't parse file server proxy response."
case .fileServerHTTPError(let httpStatusCode, let message): return "File server returned \(httpStatusCode) with description: \(message ?? "no description provided.")."
}
}
}
// MARK: Initialization
internal init(for server: String) {
self.server = server
super.init()
}
// MARK: Proxying
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
let isLokiFileServer = (server == FileServerAPI.server)
guard isLokiFileServer else { return super.perform(request, withCompletionQueue: queue) } // Don't proxy open group requests for now
return performLokiFileServerNSURLRequest(request, withCompletionQueue: queue)
}
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
var headers = getCanonicalHeaders(for: request)
return Promise<SnodeAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global(qos: .userInitiated).async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
SnodeAPI.getRandomSnode().then2 { proxy -> Promise<Any> in
let url = "\(proxy.address):\(proxy.port)/file_proxy"
guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound,
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [ .fragmentsAllowed ])
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
}
let proxyRequestParameters: JSON = [
"body" : parametersAsString,
"endpoint": endpoint,
"method" : request.httpMethod,
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [ .fragmentsAllowed ])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
let proxyRequestHeaders = [
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
"X-Loki-File-Server-Verb" : "POST",
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
"Connection" : "close", // TODO: Is this necessary?
"Content-Type" : "application/json"
]
let (promise, resolver) = SnodeAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map2 { rawResponse in
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: [ .fragmentsAllowed ]) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
print("[Loki] Received an invalid response.")
throw Error.proxyResponseParsingFailed
}
let isSuccess = (200...299) ~= statusCode
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: nil, underlyingError: Error.fileServerHTTPError(code: statusCode, message: nil)) }
let uncheckedJSONAsData = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
if uncheckedJSONAsData.isEmpty { return () }
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: [ .fragmentsAllowed ]) as? JSON
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
return json
}.done2 { rawResponse in
seal.fulfill(rawResponse)
}.catch2 { error in
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
seal.reject(HTTPError.from(error: error) ?? error)
}
}
}
}
}

View File

@ -1,75 +0,0 @@
import PromiseKit
/// Base class for `LokiSnodeProxy` and `LokiFileServerProxy`.
public class LokiHTTPClient {
internal lazy var httpSession: AFHTTPSessionManager = {
let result = AFHTTPSessionManager(sessionConfiguration: .ephemeral)
let securityPolicy = AFSecurityPolicy.default()
securityPolicy.allowInvalidCertificates = true
securityPolicy.validatesDomainName = false
result.securityPolicy = securityPolicy
result.responseSerializer = AFHTTPResponseSerializer()
result.completionQueue = DispatchQueue.global(qos: .default)
return result
}()
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> SnodeAPI.RawResponsePromise in
throw HTTPError.from(error: error) ?? error
}
}
internal func getCanonicalHeaders(for request: NSURLRequest) -> [String:Any] {
guard let headers = request.allHTTPHeaderFields else { return [:] }
return headers.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
}
}
// MARK: - HTTP Error
public extension LokiHTTPClient {
public enum HTTPError : LocalizedError {
case networkError(code: Int, response: Any?, underlyingError: Error?)
internal static func from(error: Error) -> LokiHTTPClient.HTTPError? {
if let error = error as? NetworkManagerError {
if case NetworkManagerError.taskError(_, let underlyingError) = error, let nsError = underlyingError as? NSError {
var response = nsError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]
// Deserialize response if needed
if let data = response as? Data, let json = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
response = json
}
return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: response, underlyingError: underlyingError)
}
return LokiHTTPClient.HTTPError.networkError(code: error.statusCode, response: nil, underlyingError: error)
}
return nil
}
public var errorDescription: String? {
switch self {
case .networkError(let code, let body, let underlyingError): return underlyingError?.localizedDescription ?? "HTTP request failed with status code: \(code), message: \(body ?? "nil")."
}
}
internal var statusCode: Int {
switch self {
case .networkError(let code, _, _): return code
}
}
internal var isNetworkError: Bool {
switch self {
case .networkError(_, _, let underlyingError): return underlyingError != nil && IsNSErrorNetworkFailure(underlyingError)
}
}
}
}

View File

@ -68,7 +68,11 @@ public class DotNetAPI : NSObject {
let queryParameters = "pubKey=\(getUserHexEncodedPublicKey())"
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse in
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(FileServerAPI.fileServerPublicKey) }
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map2 { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
throw DotNetAPIError.parsingFailed
@ -92,7 +96,11 @@ public class DotNetAPI : NSObject {
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "token" : token ]
let request = TSRequest(url: url, method: "POST", parameters: parameters)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { _ in token }
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(FileServerAPI.fileServerPublicKey) }
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
return serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.map2 { _ in token }
}
// MARK: Public API
@ -126,8 +134,7 @@ public class DotNetAPI : NSObject {
data = unencryptedAttachmentData
}
// Check the file size if needed
let isLokiFileServer = (server == FileServerAPI.server)
if isLokiFileServer && data.count > FileServerAPI.maxFileSize {
if data.count > FileServerAPI.maxFileSize {
return seal.reject(DotNetAPIError.maxFileSizeExceeded)
}
// Create the request
@ -143,10 +150,16 @@ public class DotNetAPI : NSObject {
return seal.reject(error)
}
// Send the request
func parseResponse(_ responseObject: Any) {
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise { $0.fulfill(FileServerAPI.fileServerPublicKey) }
: PublicChatAPI.getOpenGroupServerPublicKey(for: server)
attachment.isUploaded = false
attachment.save()
let _ = serverPublicKeyPromise.then2 { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
}.done2 { json in
// Parse the server ID & download URL
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(responseObject).")
guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse attachment from: \(json).")
return seal.reject(DotNetAPIError.parsingFailed)
}
// Update the attachment
@ -155,38 +168,8 @@ public class DotNetAPI : NSObject {
attachment.downloadURL = downloadURL
attachment.save()
seal.fulfill(())
}
let isProxyingRequired = (server == FileServerAPI.server) // Don't proxy open group requests for now
if isProxyingRequired {
attachment.isUploaded = false
attachment.save()
let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done2 { responseObject in
parseResponse(responseObject)
}.catch2 { error in
seal.reject(error)
}
} else {
let task = AFURLSessionManager(sessionConfiguration: .default).uploadTask(withStreamedRequest: request as URLRequest, progress: { rawProgress in
// Broadcast progress updates
let progress = max(0.1, rawProgress.fractionCompleted)
let userInfo: [String:Any] = [ kAttachmentUploadProgressKey : progress, kAttachmentUploadAttachmentIDKey : attachmentID ]
DispatchQueue.main.async {
NotificationCenter.default.post(name: .attachmentUploadProgress, object: nil, userInfo: userInfo)
}
}, completionHandler: { response, responseObject, error in
if let error = error {
print("[Loki] Couldn't upload attachment due to error: \(error).")
return seal.reject(error)
}
let statusCode = (response as! HTTPURLResponse).statusCode
let isSuccessful = (200...299) ~= statusCode
guard isSuccessful else {
print("[Loki] Couldn't upload attachment.")
return seal.reject(DotNetAPIError.generic)
}
parseResponse(responseObject)
})
task.resume()
}.catch2 { error in
seal.reject(error)
}
}
if server == FileServerAPI.server {
@ -210,7 +193,7 @@ internal extension Promise {
internal func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
return recover2 { error -> Promise<T> in
if let error = error as? NetworkManagerError, (error.statusCode == 401 || error.statusCode == 403) {
if case HTTP.Error.httpRequestFailed(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
print("[Loki] Auth token for: \(server) expired; dropping it.")
DotNetAPI.clearAuthToken(for: server)
}

View File

@ -4,8 +4,10 @@ import PromiseKit
public final class FileServerAPI : DotNetAPI {
// MARK: Settings
private static let deviceLinkType = "network.loki.messenger.devicemapping"
private static let attachmentType = "net.app.core.oembed"
private static let deviceLinkType = "network.loki.messenger.devicemapping"
internal static let fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
public static let maxFileSize = 10_000_000 // 10 MB
@ -46,8 +48,8 @@ public final class FileServerAPI : DotNetAPI {
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse -> Set<DeviceLink> in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { rawResponse -> Set<DeviceLink> in
guard let data = rawResponse["data"] as? [JSON] else {
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
@ -60,7 +62,7 @@ public final class FileServerAPI : DotNetAPI {
return []
}
return rawDeviceLinks.compactMap { rawDeviceLink in
guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String,
let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else {
print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).")
return nil
@ -72,8 +74,8 @@ public final class FileServerAPI : DotNetAPI {
masterSignature = nil
}
let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature)
let master = DeviceLink.Device(publicKey: masterHexEncodedPublicKey, signature: masterSignature)
let slave = DeviceLink.Device(publicKey: slaveHexEncodedPublicKey, signature: slaveSignature)
let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature)
let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature)
let deviceLink = DeviceLink(between: master, and: slave)
if let masterSignature = masterSignature {
guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else {
@ -108,9 +110,9 @@ public final class FileServerAPI : DotNetAPI {
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) {
LokiFileServerProxy(for: server).perform(request).map2 { _ in }
OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { _ in }
}.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in
print("Couldn't update device links due to error: \(error).")
print("[Loki] Couldn't update device links due to error: \(error).")
throw error
}
}
@ -164,13 +166,32 @@ public final class FileServerAPI : DotNetAPI {
print("[Loki] Couldn't upload profile picture due to error: \(error).")
return Promise(error: error)
}
return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { json in
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
print("[Loki] Couldn't parse profile picture from: \(json).")
throw DotNetAPIError.parsingFailed
}
UserDefaults.standard[.lastProfilePictureUpload] = Date()
return downloadURL
}
}
// MARK: Open Group Server Public Key
public static func getPublicKey(for openGroupServer: String) -> Promise<String> {
let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")!
let request = TSRequest(url: url)
let token = "loki" // Tokenless request; use a dummy token
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { json in
guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON }
guard let base64EncodedPublicKey = body["data"] as? String else {
print("[Loki] Couldn't parse open group public key from: \(body).")
throw DotNetAPIError.parsingFailed
}
let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)!
let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
}
}
}

View File

@ -18,31 +18,39 @@ extension OnionRequestAPI {
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, forSnode snode: Snode) throws -> EncryptionResult {
private static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String) throws -> EncryptionResult {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") }
guard let hexEncodedSnodeX25519PublicKey = snode.publicKeySet?.x25519Key else { throw Error.snodePublicKeySetMissing }
let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey)
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
let ephemeralKeyPair = Curve25519.generateKeyPair()
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: snodeX25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
let salt = "LOKI"
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey))
return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey)
}
/// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, forTargetSnode snode: Snode) -> Promise<EncryptionResult> {
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, forSnode: snode)
seal.fulfill(result)
// Wrapping isn't needed for file server or open group onion requests
switch destination {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, using: snodeX25519PublicKey)
seal.fulfill(result)
case .server(_, let serverX25519PublicKey):
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, using: serverX25519PublicKey)
seal.fulfill(result)
}
} catch (let error) {
seal.reject(error)
}
@ -51,18 +59,31 @@ extension OnionRequestAPI {
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
internal static func encryptHop(from lhs: Snode, to rhs: Snode, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let parameters: JSON = [
"ciphertext" : previousEncryptionResult.ciphertext.base64EncodedString(),
"ephemeral_key" : previousEncryptionResult.ephemeralPublicKey.toHexString(),
"destination" : rhs.publicKeySet!.ed25519Key
]
var parameters: JSON
switch rhs {
case .snode(let snode):
guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, _):
parameters = [ "host" : host, "target" : "/loki/v1/lsrpc", "method" : "POST" ]
}
parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString()
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
x25519PublicKey = snodeX25519PublicKey
case .server(_, let serverX25519PublicKey):
x25519PublicKey = serverX25519PublicKey
}
do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
let result = try encrypt(plaintext, forSnode: lhs)
let result = try encrypt(plaintext, using: x25519PublicKey)
seal.fulfill(result)
} catch (let error) {
seal.reject(error)

View File

@ -19,10 +19,17 @@ public enum OnionRequestAPI {
private static var guardSnodeCount: UInt { return pathCount } // One per path
// MARK: Destination
internal enum Destination {
case snode(Snode)
case server(host: String, x25519PublicKey: String)
}
// MARK: Error
internal enum Error : LocalizedError {
case httpRequestFailedAtTargetSnode(statusCode: UInt, json: JSON)
case insufficientSnodes
case invalidURL
case missingSnodeVersion
case randomDataGenerationFailed
case snodePublicKeySetMissing
@ -32,6 +39,7 @@ public enum OnionRequestAPI {
switch self {
case .httpRequestFailedAtTargetSnode(let statusCode): return "HTTP request failed at target snode with status code: \(statusCode)."
case .insufficientSnodes: return "Couldn't find enough snodes to build a path."
case .invalidURL: return "Invalid URL"
case .missingSnodeVersion: return "Missing snode version."
case .randomDataGenerationFailed: return "Couldn't generate random data."
case .snodePublicKeySetMissing: return "Missing snode public key set."
@ -44,7 +52,7 @@ public enum OnionRequestAPI {
public typealias Path = [Snode]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data)
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, destinationSymmetricKey: Data)
// MARK: Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
@ -138,7 +146,7 @@ public enum OnionRequestAPI {
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
///
/// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: Snode) -> Promise<Path> {
private static func getPath(excluding snode: Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
if paths.count < pathCount {
let storage = OWSPrimaryStorage.shared()
@ -152,11 +160,19 @@ public enum OnionRequestAPI {
// randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= pathCount {
return Promise<Path> { seal in
seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!)
if let snode = snode {
seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!)
} else {
seal.fulfill(paths.randomElement()!)
}
}
} else {
return buildPaths().map2 { paths in
return paths.filter { !$0.contains(snode) }.randomElement()!
if let snode = snode {
return paths.filter { !$0.contains(snode) }.randomElement()!
} else {
return paths.randomElement()!
}
}
}
}
@ -173,24 +189,26 @@ public enum OnionRequestAPI {
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt snode: Snode) -> Promise<OnionBuildingResult> {
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: EncryptionResult!
return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in
var snodeToExclude: Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude).then2 { path -> Promise<EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the target snode first
return encrypt(payload, forTargetSnode: snode).then2 { r -> Promise<EncryptionResult> in
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination).then2 { r -> Promise<EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = snode
var rhs = destination
func addLayer() -> Promise<EncryptionResult> {
if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = path.removeLast()
let lhs = Destination.snode(path.removeLast())
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
encryptionResult = r
rhs = lhs
@ -205,12 +223,69 @@ public enum OnionRequestAPI {
// MARK: Internal API
/// Sends an onion request to `snode`. Builds new paths as needed.
internal static func sendOnionRequest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
}
/// Sends an onion request to `server`. Builds new paths as needed.
internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise<JSON> {
let rawHeaders = request.allHTTPHeaderFields ?? [:]
var headers: JSON = rawHeaders.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
var endpoint = ""
if server.count < url.count {
guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) }
let endpointStartIndex = url.index(after: serverEndIndex)
endpoint = String(url[endpointStartIndex..<url.endIndex])
}
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let tsRequestParameters = tsRequest.parameters
if !tsRequestParameters.isEmpty {
guard let parameters = try? JSONSerialization.data(withJSONObject: tsRequestParameters, options: [ .fragmentsAllowed ]) else {
return Promise(error: HTTP.Error.invalidJSON)
}
parametersAsString = String(bytes: parameters, encoding: .utf8) ?? "null"
} else {
parametersAsString = "null"
}
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parameters = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parameters.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
}
let payload: JSON = [
"body" : parametersAsString,
"endpoint": endpoint,
"method" : request.httpMethod,
"headers" : headers
]
let destination = Destination.server(host: host, x25519PublicKey: x25519PublicKey)
return sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired).recover2 { error -> Promise<JSON> in
print("[Loki] [Onion Request API] Couldn't reach server: \(server) due to error: \(error).")
throw error
}
}
internal static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Snode!
DispatchQueue.global(qos: .userInitiated).async {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
buildOnion(around: payload, targetedAt: snode).done2 { intermediate in
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req"
let finalEncryptionResult = intermediate.finalEncryptionResult
@ -219,7 +294,7 @@ public enum OnionRequestAPI {
"ciphertext" : onion.base64EncodedString(),
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
]
let targetSnodeSymmetricKey = intermediate.targetSnodeSymmetricKey
let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, parameters: parameters).done2 { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String,
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext) else { return seal.reject(HTTP.Error.invalidJSON) }
@ -227,18 +302,27 @@ public enum OnionRequestAPI {
let ciphertext = ivAndCiphertext[Int(ivSize)...]
do {
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let aes = try AES(key: targetSnodeSymmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let aes = try AES(key: destinationSymmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let data = Data(try aes.decrypt(ciphertext.bytes))
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
let bodyAsString = json["body"] as? String, let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
if statusCode == 406 { // Clock out of sync
print("[Loki] The user's clock is out of sync with the service node network.")
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
} else {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
} else if let bodyAsString = json["body"] as? String {
let body: JSON
if !isJSONRequired {
body = [ "result" : bodyAsString ]
} else {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
body = b
}
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
} else {
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: json)) }
seal.fulfill(json)
}
} catch (let error) {
seal.reject(error)
@ -255,10 +339,6 @@ public enum OnionRequestAPI {
dropAllPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode)
}
promise.recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
return promise
}
}

View File

@ -7,7 +7,7 @@ public final class PublicChatAPI : DotNetAPI {
@objc public static let defaultChats: [PublicChat] = [] // Currently unused
public static var displayNameUpdatees: [String:Set<String>] = [:]
// MARK: Settings
private static let attachmentType = "net.app.core.oembed"
private static let channelInfoType = "net.patter-app.settings"
@ -73,6 +73,27 @@ public final class PublicChatAPI : DotNetAPI {
public static func clearCaches(for channel: UInt64, on server: String) {
removeLastMessageServerID(for: channel, on: server)
removeLastDeletionServerID(for: channel, on: server)
try! Storage.writeSync { transaction in
Storage.removeOpenGroupPublicKey(for: server, using: transaction)
}
}
// MARK: Open Group Public Key Validation
public static func getOpenGroupServerPublicKey(for server: String) -> Promise<String> {
if let publicKey = Storage.getOpenGroupPublicKey(for: server) {
return Promise.value(publicKey)
} else {
return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise<String> in
let url = URL(string: server)!
let request = TSRequest(url: url)
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in
try! Storage.writeSync { transaction in
Storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
}
return publicKey
}
}
}
}
// MARK: Receiving
@ -88,79 +109,81 @@ public final class PublicChatAPI : DotNetAPI {
} else {
queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0"
}
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
guard !isDeleted else { return nil }
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON,
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
let body = message["text"] as? String, let user = message["user"] as? JSON, let senderPublicKey = user["username"] as? String,
let timestamp = value["timestamp"] as? UInt64 else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
var profilePicture: PublicChatMessage.ProfilePicture? = nil
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url)
}
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: PublicChatMessage.Quote?
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String,
let quotedMessageBody = quoteAsJSON["text"] as? String {
let quotedMessageServerID = message["reply_to"] as? UInt64
quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType }
let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString),
let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil }
let fileName = value["fileName"] as? String ?? UUID().description
let width = value["width"] as? UInt ?? 0
let height = value["height"] as? UInt ?? 0
let flags = (value["flags"] as? UInt) ?? 0
let caption = value["caption"] as? String
let linkPreviewURL = value["linkPreviewUrl"] as? String
let linkPreviewTitle = value["linkPreviewTitle"] as? String
if kind == .linkPreview {
guard linkPreviewURL != nil && linkPreviewTitle != nil else {
print("[Loki] Ignoring public chat message with invalid link preview.")
return rawMessages.flatMap { message in
let isDeleted = (message["is_deleted"] as? Int == 1)
guard !isDeleted else { return nil }
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON,
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String,
let timestamp = value["timestamp"] as? UInt64 else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
}
return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
}
let result = PublicChatMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture,
body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
var existingMessageID: String? = nil
storage.dbReadConnection.read { transaction in
existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
}
guard existingMessageID == nil else {
print("[Loki] Ignoring duplicate public chat message.")
return nil
}
return result
}.sorted { $0.timestamp < $1.timestamp }
var profilePicture: PublicChatMessage.ProfilePicture? = nil
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url)
}
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: PublicChatMessage.Quote?
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String,
let quotedMessageBody = quoteAsJSON["text"] as? String {
let quotedMessageServerID = message["reply_to"] as? UInt64
quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType }
let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString),
let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil }
let fileName = value["fileName"] as? String ?? UUID().description
let width = value["width"] as? UInt ?? 0
let height = value["height"] as? UInt ?? 0
let flags = (value["flags"] as? UInt) ?? 0
let caption = value["caption"] as? String
let linkPreviewURL = value["linkPreviewUrl"] as? String
let linkPreviewTitle = value["linkPreviewTitle"] as? String
if kind == .linkPreview {
guard linkPreviewURL != nil && linkPreviewTitle != nil else {
print("[Loki] Ignoring public chat message with invalid link preview.")
return nil
}
}
return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
}
let result = PublicChatMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
var existingMessageID: String? = nil
storage.dbReadConnection.read { transaction in
existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!), in: transaction)
}
guard existingMessageID == nil else {
print("[Loki] Ignoring duplicate public chat message.")
return nil
}
return result
}.sorted { $0.timestamp < $1.timestamp }
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -177,23 +200,25 @@ public final class PublicChatAPI : DotNetAPI {
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) }
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatMessage> in
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatMessage> in
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
let displayName = userDisplayName
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}.done(on: DispatchQueue.global(qos: .default)) { message in
@ -214,23 +239,25 @@ public final class PublicChatAPI : DotNetAPI {
} else {
queryParameters = "count=\(fallbackBatchCount)"
}
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).")
return nil
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).")
return nil
}
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
@ -246,12 +273,14 @@ public final class PublicChatAPI : DotNetAPI {
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -260,25 +289,27 @@ public final class PublicChatAPI : DotNetAPI {
// MARK: Display Name & Profile Picture
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
let publicChatID = "\(server).\(channel)"
guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
guard let publicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
displayNameUpdatees[publicChatID] = []
print("[Loki] Getting display names for: \(hexEncodedPublicKeys).")
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
try! Storage.writeSync { transaction in
data.forEach { data in
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
let endIndex = hexEncodedPublicKey.endIndex
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
transaction.setObject(displayName, forKey: hexEncodedPublicKey, inCollection: "\(server).\(channel)")
print("[Loki] Getting display names for: \(publicKeys).")
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
let url = URL(string: "\(server)/users?\(queryParameters)")!
let request = TSRequest(url: url)
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse display names for users: \(publicKeys) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
try! Storage.writeSync { transaction in
data.forEach { data in
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
let endIndex = hexEncodedPublicKey.endIndex
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
transaction.setObject(displayName, forKey: hexEncodedPublicKey, inCollection: "\(server).\(channel)")
}
}
}
}
@ -294,13 +325,15 @@ public final class PublicChatAPI : DotNetAPI {
print("[Loki] Updating display name on server: \(server).")
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("Couldn't update display name due to error: \(error).")
throw error
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("Couldn't update display name due to error: \(error).")
throw error
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -319,13 +352,15 @@ public final class PublicChatAPI : DotNetAPI {
}
let parameters: JSON = [ "annotations" : [ annotation ] ]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("[Loki] Couldn't update profile picture due to error: \(error).")
throw error
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/users/me")!
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
print("[Loki] Couldn't update profile picture due to error: \(error).")
throw error
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -349,10 +384,10 @@ public final class PublicChatAPI : DotNetAPI {
let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction)
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
if let avatarURL = info.profilePictureURL {
if let profilePictureURL = info.profilePictureURL {
let configuration = URLSessionConfiguration.default
let manager = AFURLSessionManager.init(sessionConfiguration: configuration)
let url = URL(string: "\(server)\(avatarURL)")!
let url = URL(string: "\(server)\(profilePictureURL)")!
let request = URLRequest(url: url)
let task = manager.downloadTask(with: request, progress: nil,
destination: { (targetPath: URL, response: URLResponse) -> URL in
@ -381,33 +416,35 @@ public final class PublicChatAPI : DotNetAPI {
public static func objc_getInfo(for channel: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(getInfo(for: channel, on: server))
}
public static func getInfo(for channel: UInt64, on server: String) -> Promise<PublicChatInfo> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatInfo> in
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String,
let profilePictureURL = info["avatar"] as? String,
let countInfo = data["counts"] as? JSON,
let memberCount = countInfo["subscribers"] as? Int else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<PublicChatInfo> in
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String,
let profilePictureURL = info["avatar"] as? String,
let countInfo = data["counts"] as? JSON,
let memberCount = countInfo["subscribers"] as? Int else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let storage = OWSPrimaryStorage.shared()
try! Storage.writeSync { transaction in
storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction)
}
let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo)
return publicChatInfo
}
let storage = OWSPrimaryStorage.shared()
try! Storage.writeSync { transaction in
storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction)
}
let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo)
return publicChatInfo
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -415,12 +452,14 @@ public final class PublicChatAPI : DotNetAPI {
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -428,12 +467,14 @@ public final class PublicChatAPI : DotNetAPI {
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { result -> Void in
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
}
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}
@ -449,27 +490,31 @@ public final class PublicChatAPI : DotNetAPI {
let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")!
let request = TSRequest(url: url, method: "POST", parameters: [:])
// Only used for the Loki Public Chat which doesn't require authentication
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { _ in }
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in}
}
}
// MARK: Moderators
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
return getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw DotNetAPIError.parsingFailed
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![channel] = moderatorsAsSet
} else {
self.moderators[server] = [ channel : moderatorsAsSet ]
}
return moderatorsAsSet
}
let moderatorsAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![channel] = moderatorsAsSet
} else {
self.moderators[server] = [ channel : moderatorsAsSet ]
}
return moderatorsAsSet
}
}.handlingInvalidAuthTokenIfNeeded(for: server)
}

View File

@ -56,9 +56,7 @@ public final class PublicChatManager : NSObject {
return Promise(error: Error.chatCreationFailed)
}
}
return PublicChatAPI.getAuthToken(for: server).then2 { token in
return PublicChatAPI.getInfo(for: channel, on: server)
}.map2 { channelInfo -> PublicChat in
return PublicChatAPI.getInfo(for: channel, on: server).map2 { channelInfo -> PublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
return chat
}

View File

@ -0,0 +1,22 @@
public extension Storage {
// MARK: Open Group Public Keys
internal static let openGroupPublicKeyCollection = "LokiOpenGroupPublicKeyCollection"
internal static func getOpenGroupPublicKey(for server: String) -> String? {
var result: String? = nil
read { transaction in
result = transaction.object(forKey: server, inCollection: openGroupPublicKeyCollection) as? String
}
return result
}
internal static func setOpenGroupPublicKey(for server: String, to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(publicKey, forKey: server, inCollection: openGroupPublicKeyCollection)
}
internal static func removeOpenGroupPublicKey(for server: String, using transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: server, inCollection: openGroupPublicKeyCollection)
}
}

View File

@ -1,17 +0,0 @@
@objc(LKRSSFeed)
public final class LokiRSSFeed : NSObject {
@objc public let id: String
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public init(id: String, server: String, displayName: String, isDeletable: Bool) {
self.id = "rss://\(id)"
self.server = server
self.displayName = displayName
self.isDeletable = isDeletable
}
override public var description: String { return displayName }
}

View File

@ -1,26 +0,0 @@
import PromiseKit
public enum LokiRSSFeedProxy {
public enum Error : LocalizedError {
case proxyResponseParsingFailed
public var errorDescription: String? {
switch self {
case .proxyResponseParsingFailed: return "Couldn't parse RSS feed proxy response."
}
}
}
public static func fetchContent(for url: String) -> Promise<String> {
let server = FileServerAPI.server
let endpoints = [ "messenger-updates/feed" : "loki/v1/rss/messenger", "loki.network/feed" : "loki/v1/rss/loki" ]
let endpoint = endpoints.first { url.lowercased().contains($0.key) }!.value
let url = URL(string: server + "/" + endpoint)!
let request = TSRequest(url: url)
return LokiFileServerProxy(for: server).perform(request).map2 { response -> String in
guard let json = response as? JSON, let xml = json["data"] as? String else { throw Error.proxyResponseParsingFailed }
return xml
}
}
}

View File

@ -47,7 +47,7 @@ public final class SnodeAPI : NSObject {
// MARK: Core
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
if useOnionRequests {
return OnionRequestAPI.sendOnionRequest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
} else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in

View File

@ -1,11 +1,11 @@
import PromiseKit
internal enum HTTP {
public enum HTTP {
private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil)
private static let urlSessionDelegate = URLSessionDelegateImplementation()
// MARK: Settings
private static let timeout: TimeInterval = 20
public static let timeout: TimeInterval = 20
// MARK: URL Session Delegate Implementation
private final class URLSessionDelegateImplementation : NSObject, URLSessionDelegate {
@ -17,7 +17,7 @@ internal enum HTTP {
}
// MARK: Verb
internal enum Verb : String {
public enum Verb : String {
case get = "GET"
case put = "PUT"
case post = "POST"
@ -25,12 +25,12 @@ internal enum HTTP {
}
// MARK: Error
internal enum Error : LocalizedError {
public enum Error : LocalizedError {
case generic
case httpRequestFailed(statusCode: UInt, json: JSON?)
case invalidJSON
var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
@ -40,7 +40,7 @@ internal enum HTTP {
}
// MARK: Main
internal static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<JSON> {
public static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<JSON> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
if let parameters = parameters {

View File

@ -954,7 +954,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return promise
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .original)
if loadedItem.isConvertibleToContactShare {
Logger.info("isConvertibleToContactShare")
attachment.isConvertibleToContactShare = true