Merge branch 'dev' into cleanup

This commit is contained in:
Niels Andriesse 2021-05-20 16:25:59 +10:00
commit 3fda8daec9
68 changed files with 1869 additions and 522 deletions

View File

@ -194,6 +194,7 @@
B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */; };
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */; };
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D448247E1BE300DB3608 /* PathVC.swift */; };
B87EF17126367CF800124B3C /* FileServerAPIV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87EF17026367CF800124B3C /* FileServerAPIV2.swift */; };
@ -258,6 +259,8 @@
B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84EA225DF745A005A043E /* LinkPreviewState.swift */; };
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */; };
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */; };
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */; };
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F52825EC4F8A003BF8D4 /* BlockListUIUtils.m */; };
B8F5F54E25EC50A5003BF8D4 /* BlockListUIUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F5F52725EC4F6A003BF8D4 /* BlockListUIUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */; };
@ -1150,6 +1153,7 @@
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameVC.swift; sourceTree = "<group>"; };
B82B408F239DD75000A248E7 /* RestoreVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreVC.swift; sourceTree = "<group>"; };
B834C6DD26533AE5001091B2 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
B835246D25C38ABF0089A44F /* ConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationVC.swift; sourceTree = "<group>"; };
B835247825C38D880089A44F /* MessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = "<group>"; };
B835249A25C3AB650089A44F /* VisibleMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleMessageCell.swift; sourceTree = "<group>"; };
@ -1173,6 +1177,7 @@
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
B87588582644CA9D000E60D0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinOpenGroupModal.swift; sourceTree = "<group>"; };
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = "<group>"; };
B879D448247E1BE300DB3608 /* PathVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathVC.swift; sourceTree = "<group>"; };
B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = "<group>"; };
@ -1228,6 +1233,8 @@
B8D8F1BC25661C6F0092EF10 /* Storage+OnionRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OnionRequests.swift"; sourceTree = "<group>"; };
B8D8F1EF256621180092EF10 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "MessageSender+Convenience.swift"; path = "../../SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift"; sourceTree = "<group>"; };
B8EB20E6263F7E4B00773E52 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = "<group>"; };
B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView.swift; sourceTree = "<group>"; };
B8F5F52725EC4F6A003BF8D4 /* BlockListUIUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlockListUIUtils.h; sourceTree = "<group>"; };
B8F5F52825EC4F8A003BF8D4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = "<group>"; };
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Contacts.swift"; sourceTree = "<group>"; };
@ -2074,6 +2081,7 @@
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */,
B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */,
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */,
B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */,
);
path = "Content Views";
sourceTree = "<group>";
@ -2098,6 +2106,7 @@
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */,
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */,
B821494525D4D6FF009C0F2A /* URLModal.swift */,
B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */,
B821494E25D4E163009C0F2A /* BodyTextView.swift */,
B82149B725D60393009C0F2A /* BlockedModal.swift */,
C374EEE125DA26740073A857 /* LinkPreviewModal.swift */,
@ -2344,6 +2353,7 @@
C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */,
C3C2A7672553A3D900C340D1 /* VisibleMessage+Contact.swift */,
C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */,
B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */,
);
path = "Visible Messages";
sourceTree = "<group>";
@ -3961,6 +3971,7 @@
"id-ID",
sk,
nl,
"zh-Hant",
);
mainGroup = D221A07E169C9E5E00537ABF;
productRefGroup = D221A08A169C9E5E00537ABF /* Products */;
@ -4648,6 +4659,7 @@
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */,
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */,
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */,
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */,
@ -4832,6 +4844,7 @@
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */,
B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */,
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */,
@ -4881,6 +4894,7 @@
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */,
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */,
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */,
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */,
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
@ -5004,6 +5018,7 @@
C3F0A5B2255C915C007BE2A3 /* en */,
B8EB20E6263F7E4B00773E52 /* sk */,
B87588582644CA9D000E60D0 /* nl */,
B834C6DD26533AE5001091B2 /* zh-Hant */,
);
name = Localizable.strings;
path = Meta/Translations;
@ -5033,7 +5048,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5054,7 +5069,7 @@
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5102,7 +5117,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5128,7 +5143,7 @@
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -5163,7 +5178,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -5182,7 +5197,7 @@
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -5233,7 +5248,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -5257,7 +5272,7 @@
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6118,7 +6133,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6154,7 +6169,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -6186,7 +6201,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 249;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6222,7 +6237,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 1.11.0;
MARKETING_VERSION = 1.11.2;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -447,6 +447,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
// Scroll to the source of the reply
guard let indexPath = viewModel.ensureLoadWindowContainsQuotedReply(reply) else { return }
messagesTableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: true)
} else if let message = viewItem.interaction as? TSIncomingMessage, let name = message.openGroupInvitationName,
let url = message.openGroupInvitationURL {
joinOpenGroup(name: name, url: url)
}
default: break
}
@ -552,6 +555,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
present(urlModal, animated: true, completion: nil)
}
func joinOpenGroup(name: String, url: String) {
// Open groups can be unsafe, so always ask the user whether they want to join one
let joinOpenGroupModal = JoinOpenGroupModal(name: name, url: url)
joinOpenGroupModal.modalPresentationStyle = .overFullScreen
joinOpenGroupModal.modalTransitionStyle = .crossDissolve
present(joinOpenGroupModal, animated: true, completion: nil)
}
func handleReplyButtonTapped(for viewItem: ConversationViewItem) {
reply(viewItem)
}

View File

@ -354,6 +354,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// the previous value when the keyboard is shown.
self.messagesTableView.reloadData()
}
self.markAllAsRead()
}
if shouldAnimate {
messagesTableView.performBatchUpdates(batchUpdates, completion: batchUpdatesCompletion)

View File

@ -0,0 +1,81 @@
final class OpenGroupInvitationView : UIView {
private let name: String
private let rawURL: String
private let textColor: UIColor
private let isOutgoing: Bool
private lazy var url: String = {
if let range = rawURL.range(of: "?public_key=") {
return String(rawURL[..<range.lowerBound])
} else {
return rawURL
}
}()
// MARK: Settings
private static let iconSize: CGFloat = 24
private static let iconImageViewSize: CGFloat = 48
// MARK: Lifecycle
init(name: String, url: String, textColor: UIColor, isOutgoing: Bool) {
self.name = name
self.rawURL = url
self.textColor = textColor
self.isOutgoing = isOutgoing
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(name:url:textColor:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(name:url:textColor:) instead.")
}
private func setUpViewHierarchy() {
// Title
let titleLabel = UILabel()
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.text = name
titleLabel.textColor = textColor
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
// Subtitle
let subtitleLabel = UILabel()
subtitleLabel.lineBreakMode = .byTruncatingTail
subtitleLabel.text = NSLocalizedString("view_open_group_invitation_description", comment: "")
subtitleLabel.textColor = textColor
subtitleLabel.font = .systemFont(ofSize: Values.smallFontSize)
// URL
let urlLabel = UILabel()
urlLabel.lineBreakMode = .byCharWrapping
urlLabel.text = url
urlLabel.textColor = textColor
urlLabel.numberOfLines = 0
urlLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
// Label stack
let labelStackView = UIStackView(arrangedSubviews: [ titleLabel, UIView.vSpacer(2), subtitleLabel, UIView.vSpacer(4), urlLabel ])
labelStackView.axis = .vertical
// Icon
let iconSize = OpenGroupInvitationView.iconSize
let iconName = isOutgoing ? "Globe" : "Plus"
let icon = UIImage(named: iconName)?.withTint(.white)?.resizedImage(to: CGSize(width: iconSize, height: iconSize))
let iconImageViewSize = OpenGroupInvitationView.iconImageViewSize
let iconImageView = UIImageView(image: icon)
iconImageView.contentMode = .center
iconImageView.layer.cornerRadius = iconImageViewSize / 2
iconImageView.layer.masksToBounds = true
iconImageView.backgroundColor = Colors.accent
iconImageView.set(.width, to: iconImageViewSize)
iconImageView.set(.height, to: iconImageViewSize)
// Main stack
let mainStackView = UIStackView(arrangedSubviews: [ iconImageView, labelStackView ])
mainStackView.axis = .horizontal
mainStackView.spacing = Values.mediumSpacing
mainStackView.alignment = .center
addSubview(mainStackView)
mainStackView.pin(to: self, withInset: Values.mediumSpacing)
}
}

View File

@ -238,7 +238,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
bubbleView.backgroundColor = (direction == .incoming) ? Colors.receivedMessageBackground : Colors.sentMessageBackground
updateBubbleViewCorners()
// Content view
populateContentView(for: viewItem)
populateContentView(for: viewItem, message: message)
// Date break
headerViewTopConstraint.constant = shouldInsetHeader ? Values.mediumSpacing : 1
headerView.subviews.forEach { $0.removeFromSuperview() }
@ -297,7 +297,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
dateBreakLabel.set(.height, to: dateBreakLabelSize.height)
}
private func populateContentView(for viewItem: ConversationViewItem) {
private func populateContentView(for viewItem: ConversationViewItem, message: TSMessage) {
snContentView.subviews.forEach { $0.removeFromSuperview() }
func showMediaPlaceholder() {
let mediaPlaceholderView = MediaPlaceholderView(viewItem: viewItem, textColor: bodyLabelTextColor)
@ -317,6 +317,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment)
snContentView.addSubview(linkPreviewView)
linkPreviewView.pin(to: snContentView)
} else if let openGroupInvitationName = message.openGroupInvitationName, let openGroupInvitationURL = message.openGroupInvitationURL {
let openGroupInvitationView = OpenGroupInvitationView(name: openGroupInvitationName, url: openGroupInvitationURL, textColor: bodyLabelTextColor, isOutgoing: isOutgoing)
snContentView.addSubview(openGroupInvitationView)
openGroupInvitationView.pin(to: snContentView)
} else {
// Stack view
let stackView = UIStackView(arrangedSubviews: [])

View File

@ -312,6 +312,20 @@ CGFloat kIconViewLength = 24;
actionBlock:^{
[weakSelf showMediaGallery];
}]];
if (self.isOpenGroup) {
[mainSection addItem:[OWSTableItem
itemWithCustomCellBlock:^{
return [weakSelf
disclosureCellWithName:NSLocalizedString(@"vc_conversation_settings_invite_button_title", "")
iconName:@"ic_plus_24"
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
OWSConversationSettingsViewController, @"invite")];
}
actionBlock:^{
[weakSelf inviteUsersToOpenGroup];
}]];
}
[mainSection addItem:[OWSTableItem
itemWithCustomCellBlock:^{
@ -1089,6 +1103,31 @@ CGFloat kIconViewLength = 24;
UIPasteboard.generalPasteboard.string = ((TSContactThread *)self.thread).contactSessionID;
}
- (void)inviteUsersToOpenGroup
{
NSString *threadID = self.thread.uniqueId;
SNOpenGroupV2 *openGroup = [LKStorage.shared getV2OpenGroupForThreadID:threadID];
NSString *url = [NSString stringWithFormat:@"%@/%@?public_key=%@", openGroup.server, openGroup.room, openGroup.publicKey];
SNUserSelectionVC *userSelectionVC = [[SNUserSelectionVC alloc] initWithTitle:NSLocalizedString(@"vc_conversation_settings_invite_button_title", @"")
excluding:[NSSet new]
completion:^(NSSet<NSString *> *selectedUsers) {
for (NSString *user in selectedUsers) {
SNVisibleMessage *message = [SNVisibleMessage new];
message.sentTimestamp = [NSDate millisecondTimestamp];
message.openGroupInvitation = [[SNOpenGroupInvitation alloc] initWithName:openGroup.name url:url];
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactSessionID:user];
TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:thread];
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[tsMessage saveWithTransaction:transaction];
}];
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[SNMessageSender send:message inThread:thread usingTransaction:transaction];
}];
}
}];
[self.navigationController pushViewController:userSelectionVC animated:YES];
}
- (void)showMediaGallery
{
OWSLogDebug(@"");

View File

@ -0,0 +1,85 @@
final class JoinOpenGroupModal : Modal {
private let name: String
private let url: String
// MARK: Lifecycle
init(name: String, url: String) {
self.name = name
self.url = url
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(name:url:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(name:url:) instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
titleLabel.text = "Join \(name)?"
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = "Are you sure you want to join the \(name) open group?";
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: name))
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Join button
let joinButton = UIButton()
joinButton.set(.height, to: Values.mediumButtonHeight)
joinButton.layer.cornerRadius = Modal.buttonCornerRadius
joinButton.backgroundColor = Colors.buttonBackground
joinButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
joinButton.setTitleColor(Colors.text, for: UIControl.State.normal)
joinButton.setTitle("Join", for: UIControl.State.normal)
joinButton.addTarget(self, action: #selector(joinOpenGroup), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, joinButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = Values.largeSpacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
}
// MARK: Interaction
@objc private func joinOpenGroup() {
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url), Features.useV2OpenGroups else {
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
return presentingViewController!.present(alert, animated: true, completion: nil)
}
presentingViewController!.dismiss(animated: true, completion: nil)
Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
.done(on: DispatchQueue.main) { _ in
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
}
.catch(on: DispatchQueue.main) { error in
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentingViewController.present(alert, animated: true, completion: nil)
}
}
}
}

View File

@ -142,7 +142,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
if OWSIdentityManager.shared().identityKeyPair() != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPollerIfNeeded()
appDelegate.startClosedGroupPoller()
appDelegate.startOpenGroupPollersIfNeeded()
// Do this only if we created a new Session ID, or if we already received the initial configuration message
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
@ -192,9 +192,16 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
// This code is very finicky and crashes easily
// NOTE: This code is very finicky and crashes easily. Modify with care.
AssertIsOnMainThread()
let notifications = dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
// If we don't capture `threads` here, a race condition can occur where the
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
// `false`, but `threads` then changes between that check and the
// `ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
let threads = threads!
// Create a stable state for the connection and jump to the latest commit
let notifications = dbConnection.beginLongLivedReadTransaction()
guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
@ -217,12 +224,26 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
default: break
}
}
tableView.endUpdates()
// HACK: Moves can have conflicts with the other 3 types of change.
// Just batch perform all the moves separately to prevent crashing.
// Since all the changes are from the original state to the final state,
// it will still be correct if we pick the moves out.
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
}
}
tableView.endUpdates()
emptyStateView.isHidden = (threadCount != 0)
}

View File

@ -10,8 +10,6 @@ extern NSString *const AppDelegateStoryboardMain;
- (void)startPollerIfNeeded;
- (void)stopPoller;
- (void)startClosedGroupPollerIfNeeded;
- (void)stopClosedGroupPoller;
- (void)startOpenGroupPollersIfNeeded;
- (void)stopOpenGroupPollers;

View File

@ -48,7 +48,6 @@ static NSTimeInterval launchStartedAt;
@property (nonatomic) BOOL areVersionMigrationsComplete;
@property (nonatomic) BOOL didAppLaunchFail;
@property (nonatomic) LKPoller *poller;
@property (nonatomic) LKClosedGroupPoller *closedGroupPoller;
@end
@ -413,7 +412,7 @@ static NSTimeInterval launchStartedAt;
[[SNSnodeAPI getSnodePool] retainUntilComplete];
[self startPollerIfNeeded];
[self startClosedGroupPollerIfNeeded];
[self startClosedGroupPoller];
[self startOpenGroupPollersIfNeeded];
if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
@ -563,7 +562,7 @@ static NSTimeInterval launchStartedAt;
[self.readReceiptManager setAreReadReceiptsEnabled:YES];
[self startPollerIfNeeded];
[self startClosedGroupPollerIfNeeded];
[self startClosedGroupPoller];
[self startOpenGroupPollersIfNeeded];
}
}
@ -726,19 +725,6 @@ static NSTimeInterval launchStartedAt;
- (void)stopPoller { [self.poller stop]; }
- (void)startClosedGroupPollerIfNeeded
{
if (self.closedGroupPoller == nil) {
NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey];
if (userPublicKey != nil) {
self.closedGroupPoller = [[LKClosedGroupPoller alloc] init];
}
}
[self.closedGroupPoller startIfNeeded];
}
- (void)stopClosedGroupPoller { [self.closedGroupPoller stop]; }
- (void)startOpenGroupPollersIfNeeded
{
[SNOpenGroupManagerV2.shared startPolling];

View File

@ -31,4 +31,13 @@ extension AppDelegate {
}
return promise
}
@objc func startClosedGroupPoller() {
guard OWSIdentityManager.shared().identityKeyPair() != nil else { return }
ClosedGroupPoller.shared.start()
}
@objc func stopClosedGroupPoller() {
ClosedGroupPoller.shared.stop()
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "SessionWhite40.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "SessionWhite40@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "SessionWhite40@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blockieren?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Freigeben";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ wurde blockiert.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Benutzer blockiert";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockierte Benutzer werden dich nicht anrufen oder dir Nachrichten senden können.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Dein Konto kann nicht aktiviert werden, bevor du den dir gesendeten Code verifizierst.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Sofort";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authentifizieren, um Session zu öffnen.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentifizierung gescheitert";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "We can't activate your account until you verify the code we sent you.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instant";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to open Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -525,3 +531,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -3,7 +3,7 @@
/* Message format for the 'new app version available' alert. Embeds: {{The latest app version number}} */
"APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT" = "La versión %@ está disponible en la App Store.";
/* Title for the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_TITLE" = "Hay disponible una nueva versión de Session";
"APP_UPDATE_NAG_ALERT_TITLE" = "Hay una nueva versión de Session disponible";
/* Label for the 'update' button in the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_UPDATE_BUTTON" = "Actualizar";
/* No comment provided by engineer. */
@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "¿Bloquear %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ ha sido bloqueado.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Contacto bloqueado";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Los contactos bloqueados no podrán llamarte ni enviarte mensajes.";
/* Label for generic done button. */
@ -97,11 +101,11 @@
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "No podrás enviar o recibir más mensajes en este grupo.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "¿De verdad deseas abandonar el grupo?";
"CONFIRM_LEAVE_GROUP_TITLE" = "¿De verdad quieres abandonar el grupo?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Este paso no se puede deshacer.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "¿Eliminar chat?";
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "¿Eliminar conversación?";
/* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "Sin resultados";
/* keyboard toolbar label when exactly 1 message matches the search string */
@ -137,7 +141,7 @@
/* Title for the 'crop/scale image' dialog. */
"CROP_SCALE_IMAGE_VIEW_TITLE" = "Editar foto";
/* Subtitle shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_SUBTITLE" = "Puede tomar unos minutos.";
"DATABASE_VIEW_OVERLAY_SUBTITLE" = "Esto puede tomar unos minutos.";
/* Title shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_TITLE" = "Optimizando base de datos";
/* Format string for a relative time, expressed as a certain number of hours in the past. Embeds {{The number of hours}}. */
@ -181,7 +185,7 @@
/* No comment provided by engineer. */
"GROUP_MEMBER_LEFT" = "%@ ha abandonado el grupo.";
/* No comment provided by engineer. */
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
"GROUP_MEMBER_REMOVED" = " Fue eliminado del grupo. ";
/* No comment provided by engineer. */
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
/* No comment provided by engineer. */
@ -191,7 +195,7 @@
/* No comment provided by engineer. */
"GROUP_YOU_LEFT" = "Has abandonado el grupo.";
/* No comment provided by engineer. */
"YOU_WERE_REMOVED" = " You were removed from the group. ";
"YOU_WERE_REMOVED" = " Has sido eliminado del grupo. ";
/* Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared. */
"IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "No se pueden compartir más de %@ objetos.";
/* alert title */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "No podemos activar tu cuenta hasta que no verifiques el código que te hemos enviado.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Inmediato";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Identifícate para acceder a Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Fallo al identificarse";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -337,7 +343,7 @@
/* Setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS" = "Enviar previsualizaciones";
/* Footer for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_FOOTER" = "Previews are supported for Imgur, Instagram, Pinterest, Reddit, and YouTube links.";
"SETTINGS_LINK_PREVIEWS_FOOTER" = "Las previsualizaciones están disponibles para enlaces hacia Imgur, Instagram, Pinterest, Reddit y YouTube.";
/* Header for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_HEADER" = "Previsualizar enlaces";
/* table section header */
@ -446,8 +452,8 @@
"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_guard_node_row_title" = "Nodo de entrada";
"vc_path_service_node_row_title" = "Nodo de Servicio";
"vc_path_destination_row_title" = "Destino";
"vc_path_learn_more_button_title" = "Saber Más";
"vc_create_private_chat_title" = "Nueva Session";
@ -490,36 +496,39 @@
"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.";
// MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers.";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background.";
"vc_pn_mode_title" = "Message Notifications";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code.";
"vc_enter_recovery_phrase_title" = "Recovery Phrase";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up.";
"vc_enter_public_key_text_field_hint" = "Enter Session ID or ONS name";
"vc_home_title" = "Messages";
"admin_group_leave_warning" = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone.";
"vc_join_open_group_suggestions_title" = "Or join one of these...";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session";
"copied" = "Copied";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID";
"vc_conversation_input_prompt" = "Message";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel";
"modal_download_attachment_title" = "Trust %@?";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?";
"modal_download_button_title" = "Download";
"modal_open_url_title" = "Open URL?";
"modal_open_url_explanation" = "Are you sure you want to open %@?";
"modal_open_url_button_title" = "Open";
"modal_blocked_title" = "Unblock %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?";
"modal_blocked_button_title" = "Unblock";
"modal_link_previews_title" = "Enable Link Previews?";
"modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings.";
"modal_link_previews_button_title" = "Enable";
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"fast_mode_explanation" = "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Apple.";
"fast_mode" = "Fast Mode";
"slow_mode_explanation" = "Session comprobará ocasionalmente si hay nuevos mensajes en segundo plano.";
"slow_mode" = "Modo lento";
"vc_pn_mode_title" = "Notificaciones de mensaje";
"vc_notification_settings_notification_mode_title" = "Usar modo rápido";
"vc_link_device_recovery_phrase_tab_title" = "Frase de recuperación";
"vc_link_device_scan_qr_code_explanation" = "Navega a Ajustes → Frase de recuperación en tu otro dispositivo para mostrar tu código QR.";
"vc_enter_recovery_phrase_title" = "Frase de recuperación";
"vc_enter_recovery_phrase_explanation" = "Para vincular tú dispositivo, introduce la frase de recuperación que se le dio cuando se registró.";
"vc_enter_public_key_text_field_hint" = "Introduzca el ID de Session o el nombre ONS";
"vc_home_title" = "Mensajes";
"admin_group_leave_warning" = "Debido a que eres el creador de este grupo, se eliminará para todos. Esto no se puede deshacer.";
"vc_join_open_group_suggestions_title" = "O únase a uno de estos...";
"vc_settings_invite_a_friend_button_title" = "Invita a un amigo";
"vc_settings_help_us_translate_button_title" = "Ayúdanos a traducir Session";
"copied" = "Copiado";
"vc_conversation_settings_copy_session_id_button_title" = "Copiar ID de Session";
"vc_conversation_input_prompt" = "Mensaje";
"vc_conversation_voice_message_cancel_message" = "Desliza para cancelar";
"modal_download_attachment_title" = "¿Confiar en %@?";
"modal_download_attachment_explanation" = "¿Estás seguro de que quieres descargar este archivo?";
"modal_download_button_title" = "Descargar";
"modal_open_url_title" = "¿Abrir URL?";
"modal_open_url_explanation" = "¿Estás seguro de que quieres abrir %@?";
"modal_open_url_button_title" = "Abrir";
"modal_blocked_title" = "¿Desbloquear a %@?";
"modal_blocked_explanation" = "¿Estás seguro de que quieres desbloquear a %@?";
"modal_blocked_button_title" = "Desbloquear";
"modal_link_previews_title" = "¿Habilitar Previsualizaciones de Enlace?";
"modal_link_previews_explanation" = "Activar vista previa de enlaces mostrará las vistas previas para las URL que envíe y reciba. Esto puede ser útil, pero Session tendrá que ponerse en contacto con los sitios web enlazados para generar vistas previas. Siempre puedes desactivar las vistas previas de enlaces en la configuración de Session.";
"modal_link_previews_button_title" = "Activar";
"vc_share_title" = "Compartir en Session";
"vc_share_loading_message" = "Preparando archivos adjuntos...";
"vc_share_sending_message" = "Enviando...";
"view_open_group_invitation_description" = "Abrir invitación de grupo";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ مسدود شود؟";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "رفع مسدودی";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ مسدود شد.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "کاربر مسدود شده است";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "کاربری که مسدود شده است، امکان تماس یا ارسال پیام به شما را ندارد.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "لطفا برای فعال شدن حساب خود، کدی که به شما فرستاده‌ایم را تائید کنید.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "لحظه";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "برای باز کردن Session هویت خود را احراز کنید";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "احراز هویت ناموفق بود";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "اشتراک گذاری با Session";
"vc_share_loading_message" = "آماده سازی پیوست‌ها...";
"vc_share_sending_message" = "در حال ارسال...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquer %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Débloquer";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ a été bloqué.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Lutilisateur est bloqué";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Les utilisateurs bloqués ne pourront ni vous appeler ni vous envoyer des messages.";
/* Label for generic done button. */
@ -282,16 +286,18 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Nous ne pouvons pas activer votre compte tant que vous naurez pas vérifié le code que nous vous avons envoyé.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instantané";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authentifiez-vous pour ouvrir Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Échec dauthentification";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
"SEND_MEDIA_ABANDON_TITLE" = "Discard Media?";
"SEND_MEDIA_ABANDON_TITLE" = "Abandonner le média ?";
/* alert action, confirming the user wants to exit the media flow and abandon any photos they've taken */
"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "Discard Media";
"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "Abandonner le média";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_CAMERA" = "Return to Camera";
"SEND_MEDIA_RETURN_TO_CAMERA" = "Retourner à lappareil photo";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "Return to Media Library";
"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "Retourner à la bibliothèque média";
/* Format string for the default 'Note' sound. Embeds the system {{sound name}}. */
"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (par défaut)";
/* Label for the backup view in app settings. */
@ -359,7 +365,7 @@
/* Label for the 'typing indicators' setting. */
"SETTINGS_TYPING_INDICATORS" = "Indicateurs de saisie";
/* Label for the 'no sound' option that allows users to disable sounds for notifications, etc. */
"SOUNDS_NONE" = "None";
"SOUNDS_NONE" = "Aucun";
/* {{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 days}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS" = "%@ jours";
/* Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings */
@ -490,36 +496,39 @@
"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.";
// MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers.";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background.";
"vc_pn_mode_title" = "Message Notifications";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code.";
"vc_enter_recovery_phrase_title" = "Recovery Phrase";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up.";
"vc_enter_public_key_text_field_hint" = "Enter Session ID or ONS name";
"fast_mode_explanation" = "Vous serez notifiés de nouveaux messages de manière certaine et immédiate en utilisant les serveurs de notification dApple.";
"fast_mode" = "Mode rapide";
"slow_mode_explanation" = "Session vérifiera occasionnellement la présence de nouveaux message en tâche de fond.";
"slow_mode" = "Mode lent";
"vc_pn_mode_title" = "Notifications de message";
"vc_notification_settings_notification_mode_title" = "Utiliser le mode rapide";
"vc_link_device_recovery_phrase_tab_title" = "Phrase de récupération";
"vc_link_device_scan_qr_code_explanation" = "Allez dans paramètre → Phrase de récupération sur votre autre appareil pour afficher votre QR Code.";
"vc_enter_recovery_phrase_title" = "Phrase de récupération";
"vc_enter_recovery_phrase_explanation" = "Pour lier votre appareil, entrez la phrase de récupération qui vous a été donné lors de la création du compte.";
"vc_enter_public_key_text_field_hint" = "Entrez un ID Session ou un nom ONS";
"vc_home_title" = "Messages";
"admin_group_leave_warning" = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone.";
"vc_join_open_group_suggestions_title" = "Or join one of these...";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session";
"copied" = "Copied";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID";
"admin_group_leave_warning" = "Puisque vous êtes le créateur de ce groupe, il sera supprimé pour tout le monde. Ceci ne peut pas être annulé.";
"vc_join_open_group_suggestions_title" = "Ou rejoignez un de ceux-ci...";
"vc_settings_invite_a_friend_button_title" = "Inviter un ami";
"vc_settings_help_us_translate_button_title" = "Aidez-nous à traduire Session";
"copied" = "Copié";
"vc_conversation_settings_copy_session_id_button_title" = "Copier lID Session";
"vc_conversation_input_prompt" = "Message";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel";
"modal_download_attachment_title" = "Trust %@?";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?";
"modal_download_button_title" = "Download";
"modal_open_url_title" = "Open URL?";
"modal_open_url_explanation" = "Are you sure you want to open %@?";
"modal_open_url_button_title" = "Open";
"modal_blocked_title" = "Unblock %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?";
"modal_blocked_button_title" = "Unblock";
"modal_link_previews_title" = "Enable Link Previews?";
"modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings.";
"modal_link_previews_button_title" = "Enable";
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"vc_conversation_voice_message_cancel_message" = "Glisser pour annuler";
"modal_download_attachment_title" = "Faire confiance à %@?";
"modal_download_attachment_explanation" = "Êtes-vous sûr de vouloir télécharger la sélection de %@ ?";
"modal_download_button_title" = "Télécharger";
"modal_open_url_title" = "Ouvrir l'URL?";
"modal_open_url_explanation" = "Êtes-vous sûr de vouloir ouvrir %@?";
"modal_open_url_button_title" = "Ouvrir";
"modal_blocked_title" = "Débloquer %@?";
"modal_blocked_explanation" = "Confirmez-vous le déblocage de %@ ?";
"modal_blocked_button_title" = "Débloquer";
"modal_link_previews_title" = "Activer les aperçus de lien?";
"modal_link_previews_explanation" = "L'activation des aperçus de lien affichera des aperçus pour les URL que vous envoyez et recevez. Cela peut être utile, mais Session devra contacter les sites Web liés pour générer des aperçus. Vous pouvez toujours désactiver les aperçus de lien dans les paramètres de Session.";
"modal_link_previews_button_title" = "Activer";
"vc_share_title" = "Partager en Session";
"vc_share_loading_message" = "Préparation des pièces jointes ...";
"vc_share_sending_message" = "Envoi...";
"view_open_group_invitation_description" = "Invitation à un groupe ouvert";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokir %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Buka blokir";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Pengguna telah diblokir";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Pengguna terblokir tidak bisa menghubungi atau mengirimkan pesan kepada Anda.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Kami tidak bisa mengaktifkan akun Anda hingga setelah Anda memverifikasi kode yang kami kirimkan.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instan";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Autentikasi untuk membuka Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Autentikasi gagal.";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloccare %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Sblocca";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ è stato bloccato.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Utente bloccato";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Gli utenti bloccati non potranno chiamarti o inviarti messaggi.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Non possiamo attivare il tuo accanto fintanto che non verificherai il codice che ti abbiamo inviato.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Immediato";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Autenticarsi per aprire Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Autenticazione fallita";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ をブロックしますか?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "ブロックを解除する";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ はブロックされています。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "ユーザがブロックされました";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "ブロックされたユーザは、あなたにメッセージや通話を発信することができなくなります。";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "アカウントを有効化するには,お送りしたコードの確認を行ってください。";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "即座";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Sessionの起動を認証する";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "認証失敗";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -29,7 +29,7 @@
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Document kiezen mislukt.";
/* Alert body when picking a document fails because user picked a directory/bundle */
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY" = "Please create a compressed archive of this file or directory and try sending that instead.";
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY" = "Maak een gecomprimeerd archief aan van deze bestand of map en probeer dat te verzenden.";
/* Alert title when picking a document fails because user picked a directory/bundle */
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE" = "Niet-ondersteund bestand";
/* Short text label for a voice message attachment, used for thread preview and on the lock screen */
@ -43,7 +43,7 @@
/* Indicates that the backup export is being configured. */
"BACKUP_EXPORT_PHASE_CONFIGURATION" = "Backup initialiseren";
/* Indicates that the database data is being exported. */
"BACKUP_EXPORT_PHASE_DATABASE_EXPORT" = "Gegevens exporteren";
"BACKUP_EXPORT_PHASE_DATABASE_EXPORT" = "Exporteer gegevens";
/* Indicates that the backup export data is being exported. */
"BACKUP_EXPORT_PHASE_EXPORT" = "Backup exporteren";
/* Indicates that the backup export data is being uploaded. */
@ -53,7 +53,7 @@
/* Indicates that the backup import is being configured. */
"BACKUP_IMPORT_PHASE_CONFIGURATION" = "Backup configuratie";
/* Indicates that the backup import data is being downloaded. */
"BACKUP_IMPORT_PHASE_DOWNLOAD" = "Download van backup...";
"BACKUP_IMPORT_PHASE_DOWNLOAD" = "Back-up gegevens downloaden";
/* Indicates that the backup import data is being finalized. */
"BACKUP_IMPORT_PHASE_FINALIZING" = "Backup afronden";
/* Indicates that the backup import data is being imported. */
@ -61,7 +61,7 @@
/* Indicates that the backup database is being restored. */
"BACKUP_IMPORT_PHASE_RESTORING_DATABASE" = "Database Herstellen";
/* Indicates that the backup import data is being restored. */
"BACKUP_IMPORT_PHASE_RESTORING_FILES" = "Bestanden herstellen…";
"BACKUP_IMPORT_PHASE_RESTORING_FILES" = "Bestand terugzetten";
/* Label for the backup restore decision section. */
"BACKUP_RESTORE_DECISION_TITLE" = "Backup beschikbaar";
/* Label for the backup restore description. */
@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blokkeren?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokkeren";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ is geblokkeerd.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Gebruiker Geblokkeerd";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Geblokkeerde gebruikers zijn niet in staat om u te bellen of berichten te sturen.";
/* Label for generic done button. */
@ -93,435 +97,438 @@
/* Error indicating that user does not have an iCloud account. */
"CLOUDKIT_STATUS_NO_ACCOUNT" = "Geen iCloud-account niet bepalen. log in met je iCloud-account in de iOS-instellingen app om een backup te maken van je Session gegevens.";
/* Error indicating that the app was prevented from accessing the user's iCloud account. */
"CLOUDKIT_STATUS_RESTRICTED" = "Session was denied access your iCloud account for backups. Grant Session access to your iCloud Account in the iOS settings app to backup your Session data.";
"CLOUDKIT_STATUS_RESTRICTED" = "Session heeft geprobeerd toegang the krijgen tot uw iCloud account met backups Maar werd geweigerd. log in met uw iCloud-account in de iOS-instellingen app om een backup te maken van je Session gegevens.";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group.";
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Je kunt geen berichten meer versturen of ontvangen in deze groep.";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "Do you really want to leave?";
"CONFIRM_LEAVE_GROUP_TITLE" = "Wilt u echt deze groep verlaten?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone.";
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Dit kan niet ongedaan worden gemaakt.";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Gesprek verwijderen?";
/* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "No matches";
"CONVERSATION_SEARCH_NO_RESULTS" = "Geen overeenkomsten";
/* keyboard toolbar label when exactly 1 message matches the search string */
"CONVERSATION_SEARCH_ONE_RESULT" = "1 match";
"CONVERSATION_SEARCH_ONE_RESULT" = "1 overeenkomst";
/* keyboard toolbar label when more than 1 message matches the search string. Embeds {{number/position of the 'currently viewed' result}} and the {{total number of results}} */
"CONVERSATION_SEARCH_RESULTS_FORMAT" = "%d of %d matches";
"CONVERSATION_SEARCH_RESULTS_FORMAT" = "%d van de %d overeenkomsten";
/* title for conversation settings screen */
"CONVERSATION_SETTINGS" = "Conversation Settings";
"CONVERSATION_SETTINGS" = "Conversatie instellingen";
/* table cell label in conversation settings */
"CONVERSATION_SETTINGS_BLOCK_THIS_USER" = "Block This User";
"CONVERSATION_SETTINGS_BLOCK_THIS_USER" = "Deze gebruiker blokkeren";
/* Title of the 'mute this thread' action sheet. */
"CONVERSATION_SETTINGS_MUTE_ACTION_SHEET_TITLE" = "Mute";
"CONVERSATION_SETTINGS_MUTE_ACTION_SHEET_TITLE" = "Dempen";
/* label for 'mute thread' cell in conversation settings */
"CONVERSATION_SETTINGS_MUTE_LABEL" = "Mute";
"CONVERSATION_SETTINGS_MUTE_LABEL" = "Dempen";
/* Indicates that the current thread is not muted. */
"CONVERSATION_SETTINGS_MUTE_NOT_MUTED" = "Not muted";
"CONVERSATION_SETTINGS_MUTE_NOT_MUTED" = "Niet gedempt";
/* Label for button to mute a thread for a day. */
"CONVERSATION_SETTINGS_MUTE_ONE_DAY_ACTION" = "Mute for one day";
"CONVERSATION_SETTINGS_MUTE_ONE_DAY_ACTION" = "Demp voor 1 dag";
/* Label for button to mute a thread for a hour. */
"CONVERSATION_SETTINGS_MUTE_ONE_HOUR_ACTION" = "Mute for one hour";
"CONVERSATION_SETTINGS_MUTE_ONE_HOUR_ACTION" = "Demp voor 1 uur";
/* Label for button to mute a thread for a minute. */
"CONVERSATION_SETTINGS_MUTE_ONE_MINUTE_ACTION" = "Mute for one minute";
"CONVERSATION_SETTINGS_MUTE_ONE_MINUTE_ACTION" = "Een minuut dempen";
/* Label for button to mute a thread for a week. */
"CONVERSATION_SETTINGS_MUTE_ONE_WEEK_ACTION" = "Mute for one week";
"CONVERSATION_SETTINGS_MUTE_ONE_WEEK_ACTION" = "Een week dempen";
/* Label for button to mute a thread for a year. */
"CONVERSATION_SETTINGS_MUTE_ONE_YEAR_ACTION" = "Mute for one year";
"CONVERSATION_SETTINGS_MUTE_ONE_YEAR_ACTION" = "Een jaar dempen";
/* Indicates that this thread is muted until a given date or time. Embeds {{The date or time which the thread is muted until}}. */
"CONVERSATION_SETTINGS_MUTED_UNTIL_FORMAT" = "until %@";
"CONVERSATION_SETTINGS_MUTED_UNTIL_FORMAT" = "tot %@";
/* Table cell label in conversation settings which returns the user to the conversation with 'search mode' activated */
"CONVERSATION_SETTINGS_SEARCH" = "Search Conversation";
"CONVERSATION_SETTINGS_SEARCH" = "Zoek gesprek";
/* Label for button to unmute a thread. */
"CONVERSATION_SETTINGS_UNMUTE_ACTION" = "Unmute";
"CONVERSATION_SETTINGS_UNMUTE_ACTION" = "Niet langer dempen";
/* Title for the 'crop/scale image' dialog. */
"CROP_SCALE_IMAGE_VIEW_TITLE" = "Move and Scale";
"CROP_SCALE_IMAGE_VIEW_TITLE" = "Verplaatsen en in grootte veranderen";
/* Subtitle shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_SUBTITLE" = "This can take a few minutes.";
"DATABASE_VIEW_OVERLAY_SUBTITLE" = "Dit kan een paar minuten duren.";
/* Title shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_TITLE" = "Optimizing Database";
"DATABASE_VIEW_OVERLAY_TITLE" = "Optimaliseer Database";
/* Format string for a relative time, expressed as a certain number of hours in the past. Embeds {{The number of hours}}. */
"DATE_HOURS_AGO_FORMAT" = "%@ Hr Ago";
"DATE_HOURS_AGO_FORMAT" = "%@ Uur geleden";
/* Format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}. */
"DATE_MINUTES_AGO_FORMAT" = "%@ Min Ago";
"DATE_MINUTES_AGO_FORMAT" = "%@ Min geleden";
/* The present; the current time. */
"DATE_NOW" = "Now";
"DATE_NOW" = "Nu";
/* The current day. */
"DATE_TODAY" = "Today";
"DATE_TODAY" = "Vandaag";
/* The day before today. */
"DATE_YESTERDAY" = "Yesterday";
"DATE_YESTERDAY" = "Gisteren";
/* table cell label in conversation settings */
"DISAPPEARING_MESSAGES" = "Disappearing Messages";
"DISAPPEARING_MESSAGES" = "Zelf-wissende berichten";
/* Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear, see the *_TIME_AMOUNT strings for context. */
"DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT" = "Messages in this conversation will disappear after %@.";
"DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT" = "Berichten in dit gesprek zullen na %@ verdwijnen.";
/* table cell label in conversation settings */
"EDIT_GROUP_ACTION" = "Edit Group";
"EDIT_GROUP_ACTION" = "Groep bewerken";
/* Label indicating media gallery is empty */
"GALLERY_TILES_EMPTY_GALLERY" = "You don't have any media in this conversation.";
"GALLERY_TILES_EMPTY_GALLERY" = "U heeft geen media in dit gesprek.";
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Media…";
"GALLERY_TILES_LOADING_MORE_RECENT_LABEL" = "Nieuwere media laden…";
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_OLDER_LABEL" = "Loading Older Media…";
"GALLERY_TILES_LOADING_OLDER_LABEL" = "Oudere media laden…";
/* Error displayed when there is a failure fetching a GIF from the remote service. */
"GIF_PICKER_ERROR_FETCH_FAILURE" = "Failed to fetch the requested GIF. Please verify you are online.";
"GIF_PICKER_ERROR_FETCH_FAILURE" = "Het ophalen van de gevraagde GIF, is mislukt. Controleer of u online bent.";
/* Generic error displayed when picking a GIF */
"GIF_PICKER_ERROR_GENERIC" = "An unknown error occurred.";
"GIF_PICKER_ERROR_GENERIC" = "Er is een onbekende fout opgetreden.";
/* Shown when selected GIF couldn't be fetched */
"GIF_PICKER_FAILURE_ALERT_TITLE" = "Unable to Choose GIF";
"GIF_PICKER_FAILURE_ALERT_TITLE" = "Kan GIF-Bestand niet kiezen";
/* Alert message shown when user tries to search for GIFs without entering any search terms. */
"GIF_PICKER_VIEW_MISSING_QUERY" = "Please enter your search.";
"GIF_PICKER_VIEW_MISSING_QUERY" = "Vul alstublieft uw zoekopdracht in.";
/* Indicates that an error occurred while searching. */
"GIF_VIEW_SEARCH_ERROR" = "Error. Tap to Retry.";
"GIF_VIEW_SEARCH_ERROR" = "Fout. Tik om opnieuw te proberen.";
/* Indicates that the user's search had no results. */
"GIF_VIEW_SEARCH_NO_RESULTS" = "No Results.";
"GIF_VIEW_SEARCH_NO_RESULTS" = "Geen resultaten.";
/* No comment provided by engineer. */
"GROUP_CREATED" = "Group created";
"GROUP_CREATED" = "Groep aangemaakt";
/* No comment provided by engineer. */
"GROUP_MEMBER_JOINED" = " %@ joined the group. ";
"GROUP_MEMBER_JOINED" = " %@ is toegevoegd aan de groep. ";
/* No comment provided by engineer. */
"GROUP_MEMBER_LEFT" = " %@ left the group. ";
"GROUP_MEMBER_LEFT" = " %@ heeft de groep verlaten. ";
/* No comment provided by engineer. */
"GROUP_MEMBER_REMOVED" = " %@ was removed from the group. ";
"GROUP_MEMBER_REMOVED" = " %@ is verwijderd uit de groep. ";
/* No comment provided by engineer. */
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
"GROUP_MEMBERS_REMOVED" = " %@ zijn uit de groep verwijderd. ";
/* No comment provided by engineer. */
"GROUP_TITLE_CHANGED" = "Title is now '%@'. ";
"GROUP_TITLE_CHANGED" = "Titel is nu '%@'. ";
/* No comment provided by engineer. */
"GROUP_UPDATED" = "Group updated.";
"GROUP_UPDATED" = "Groep bijgewerkt.";
/* No comment provided by engineer. */
"GROUP_YOU_LEFT" = "You have left the group.";
"GROUP_YOU_LEFT" = "U heeft de groep verlaten.";
/* No comment provided by engineer. */
"YOU_WERE_REMOVED" = " You were removed from the group. ";
"YOU_WERE_REMOVED" = " Je bent verwijderd uit deze groep. ";
/* Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared. */
"IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "You can't share more than %@ items.";
"IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "Je kunt niet meer dan %@ items delen.";
/* alert title */
"IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS" = "Failed to select attachment.";
"IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS" = "Fout bij het selecteren van de bijlage.";
/* Message for the alert indicating that an audio file is invalid. */
"INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE" = "Invalid audio file.";
"INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE" = "Ongeldig audio-bestand.";
/* Slider label when disappearing messages is off */
"KEEP_MESSAGES_FOREVER" = "Messages do not disappear.";
"KEEP_MESSAGES_FOREVER" = "Berichten verdwijnen niet.";
/* Confirmation button within contextual alert */
"LEAVE_BUTTON_TITLE" = "Leave";
"LEAVE_BUTTON_TITLE" = "Verlaten";
/* table cell label in conversation settings */
"LEAVE_GROUP_ACTION" = "Leave Group";
"LEAVE_GROUP_ACTION" = "Verlaat groep";
/* Title for the 'long text message' view. */
"LONG_TEXT_VIEW_TITLE" = "Message";
"LONG_TEXT_VIEW_TITLE" = "Bericht";
/* nav bar button item */
"MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON" = "All Media";
"MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON" = "Alle media";
/* media picker option to choose from library */
"MEDIA_FROM_LIBRARY_BUTTON" = "Photo Library";
"MEDIA_FROM_LIBRARY_BUTTON" = "Fotobibliotheek";
/* Confirmation button text to delete selected media from the gallery, embeds {{number of messages}} */
"MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT" = "Delete %d Messages";
"MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT" = "Verwijder %d Berichten";
/* Confirmation button text to delete selected media message from the gallery */
"MEDIA_GALLERY_DELETE_SINGLE_MESSAGE" = "Delete Message";
"MEDIA_GALLERY_DELETE_SINGLE_MESSAGE" = "Verwijder bericht";
/* embeds {{sender name}} and {{sent datetime}}, e.g. 'Sarah on 10/30/18, 3:29' */
"MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT" = "%@ on %@";
"MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT" = "%@ op %@";
/* Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}. */
"MEDIA_GALLERY_MORE_ITEMS_FORMAT" = "+%@";
/* Short sender label for media sent by you */
"MEDIA_GALLERY_SENDER_NAME_YOU" = "You";
"MEDIA_GALLERY_SENDER_NAME_YOU" = "U";
/* Section header in media gallery collection view */
"MEDIA_GALLERY_THIS_MONTH_HEADER" = "This Month";
"MEDIA_GALLERY_THIS_MONTH_HEADER" = "Deze maand";
/* message status for message delivered to their recipient. */
"MESSAGE_STATUS_DELIVERED" = "Delivered";
"MESSAGE_STATUS_DELIVERED" = "Afgeleverd";
/* status message for failed messages */
"MESSAGE_STATUS_FAILED" = "Sending failed.";
"MESSAGE_STATUS_FAILED" = "Verzenden mislukt.";
/* status message for failed messages */
"MESSAGE_STATUS_FAILED_SHORT" = "Failed";
"MESSAGE_STATUS_FAILED_SHORT" = "Mislukt";
/* status message for read messages */
"MESSAGE_STATUS_READ" = "Read";
"MESSAGE_STATUS_READ" = "Lees";
/* message status if message delivery to a recipient is skipped. We skip delivering group messages to users who have left the group or unregistered their Session account. */
"MESSAGE_STATUS_RECIPIENT_SKIPPED" = "Skipped";
"MESSAGE_STATUS_RECIPIENT_SKIPPED" = "Overgeslagen";
/* message status while message is sending. */
"MESSAGE_STATUS_SENDING" = "Sending…";
"MESSAGE_STATUS_SENDING" = "Verzenden…";
/* status message for sent messages */
"MESSAGE_STATUS_SENT" = "Sent";
"MESSAGE_STATUS_SENT" = "Verzonden";
/* status message while attachment is uploading */
"MESSAGE_STATUS_UPLOADING" = "Uploading…";
"MESSAGE_STATUS_UPLOADING" = "Uploaden…";
/* Alert body when user has previously denied media library access */
"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE" = "You can enable this permission in the iOS Settings app.";
"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE" = "U kunt deze rechten inschakelen in de iOS-instellingen app.";
/* Alert title when user has previously denied media library access */
"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE" = "Session requires access to your photos for this feature.";
"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE" = "Sessie vereist toegang tot uw foto's voor deze functie.";
/* An explanation of the consequences of muting a thread. */
"MUTE_BEHAVIOR_EXPLANATION" = "You will not receive notifications for muted conversations.";
"MUTE_BEHAVIOR_EXPLANATION" = "U zal geen meldingen ontvangen voor gedempte gesprekken.";
/* notification title. Embeds {{author name}} and {{group name}} */
"NEW_GROUP_MESSAGE_NOTIFICATION_TITLE" = "%@ to %@";
"NEW_GROUP_MESSAGE_NOTIFICATION_TITLE" = "%@ naar %@";
/* Label for 1:1 conversation with yourself. */
"NOTE_TO_SELF" = "Note to Self";
"NOTE_TO_SELF" = "Notitie aan mezelf";
/* Lock screen notification text presented after user powers on their device without unlocking. Embeds {{device model}} (either 'iPad' or 'iPhone') */
"NOTIFICATION_BODY_PHONE_LOCKED_FORMAT" = "You may have received messages while your %@ was restarting.";
"NOTIFICATION_BODY_PHONE_LOCKED_FORMAT" = "U heeft mogelijk berichten ontvangen terwijl u %@ opnieuw aan het opstarten was.";
/* No comment provided by engineer. */
"NOTIFICATIONS_FOOTER_WARNING" = "Due to known bugs in Apple's push framework, message previews will only be shown if the message is retrieved within 30 seconds after being sent. The application badge might be inaccurate as a result.";
"NOTIFICATIONS_FOOTER_WARNING" = "Vanwege bekende bugs in het push-framework van Apple worden berichtvoorbeelden alleen getoond als het bericht binnen 30 seconden na verzending wordt opgehaald. Hierdoor kan de applicatiebadge onnauwkeurig zijn.";
/* Table cell switch label. When disabled, Session will not play notification sounds while the app is in the foreground. */
"NOTIFICATIONS_SECTION_INAPP" = "Play While App is Open";
"NOTIFICATIONS_SECTION_INAPP" = "Speel terwijl de app geopend is";
/* Label for settings UI that allows user to change the notification sound. */
"NOTIFICATIONS_SECTION_SOUNDS" = "Sounds";
"NOTIFICATIONS_SECTION_SOUNDS" = "Geluiden";
/* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_AND_MESSAGE" = "Name and Content";
"NOTIFICATIONS_SENDER_AND_MESSAGE" = "Naam en inhoud";
/* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_ONLY" = "Name Only";
"NOTIFICATIONS_SENDER_ONLY" = "Alleen naam";
/* No comment provided by engineer. */
"NOTIFICATIONS_NONE" = "No Name or Content";
"NOTIFICATIONS_NONE" = "Geen naam of inhoud";
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Show";
"NOTIFICATIONS_SHOW" = "Tonen";
/* No comment provided by engineer. */
"OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages.";
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ heeft zelf-wissende berichten uitgeschakeld.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ set disappearing message time to %@";
"OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ heeft de timer voor zelf-wissende berichten op %@ ingesteld";
/* alert title, generic error preventing user from capturing a photo */
"PHOTO_CAPTURE_GENERIC_ERROR" = "Unable to capture image.";
"PHOTO_CAPTURE_GENERIC_ERROR" = "Kan afbeelding niet vastleggen.";
/* alert title */
"PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE" = "Unable to capture image.";
"PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE" = "Kan afbeelding niet vastleggen.";
/* alert title */
"PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA" = "Failed to configure camera.";
"PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA" = "Instellen van de camera mislukt.";
/* label for system photo collections which have no name. */
"PHOTO_PICKER_UNNAMED_COLLECTION" = "Unnamed Album";
"PHOTO_PICKER_UNNAMED_COLLECTION" = "Naamloze Album";
/* alert body during registration */
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "We can't activate your account until you verify the code we sent you.";
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "We kunnen uw account pas activeren nadat u de code die we u hebben gestuurd verifieert.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instant";
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Direct";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to open Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
"SCREEN_LOCK_UNLOCK_FAILED" = "Verificatie mislukt";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
"SEND_MEDIA_ABANDON_TITLE" = "Discard Media?";
"SEND_MEDIA_ABANDON_TITLE" = "Media verwijderen?";
/* alert action, confirming the user wants to exit the media flow and abandon any photos they've taken */
"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "Discard Media";
"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "Media verwijderen";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_CAMERA" = "Return to Camera";
"SEND_MEDIA_RETURN_TO_CAMERA" = "Terugkeren naar Camera";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "Return to Media Library";
"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "Terug naar mediabibliotheek";
/* Format string for the default 'Note' sound. Embeds the system {{sound name}}. */
"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (default)";
"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (standaard)";
/* Label for the backup view in app settings. */
"SETTINGS_BACKUP" = "Backup";
"SETTINGS_BACKUP" = "Back-up";
/* Label for 'backup now' button in the backup settings view. */
"SETTINGS_BACKUP_BACKUP_NOW" = "Backup Now";
"SETTINGS_BACKUP_BACKUP_NOW" = "Nu back-up maken";
/* Label for 'cancel backup' button in the backup settings view. */
"SETTINGS_BACKUP_CANCEL_BACKUP" = "Cancel Backup";
"SETTINGS_BACKUP_CANCEL_BACKUP" = "Back-up annuleren";
/* Label for switch in settings that controls whether or not backup is enabled. */
"SETTINGS_BACKUP_ENABLING_SWITCH" = "Backup Enabled";
"SETTINGS_BACKUP_ENABLING_SWITCH" = "Back-up Ingeschakeld";
/* Label for iCloud status row in the in the backup settings view. */
"SETTINGS_BACKUP_ICLOUD_STATUS" = "iCloud Status";
/* Indicates that the last backup restore failed. */
"SETTINGS_BACKUP_IMPORT_STATUS_FAILED" = "Backup Restore Failed";
"SETTINGS_BACKUP_IMPORT_STATUS_FAILED" = "Back-up herstellen mislukt";
/* Indicates that app is not restoring up. */
"SETTINGS_BACKUP_IMPORT_STATUS_IDLE" = "Backup Restore Idle";
"SETTINGS_BACKUP_IMPORT_STATUS_IDLE" = "Back-up herstellen inactief";
/* Indicates that app is restoring up. */
"SETTINGS_BACKUP_IMPORT_STATUS_IN_PROGRESS" = "Backup Restore In Progress";
"SETTINGS_BACKUP_IMPORT_STATUS_IN_PROGRESS" = "Back-up terugzetten bezig";
/* Indicates that the last backup restore succeeded. */
"SETTINGS_BACKUP_IMPORT_STATUS_SUCCEEDED" = "Backup Restore Succeeded";
"SETTINGS_BACKUP_IMPORT_STATUS_SUCCEEDED" = "Back-up terugzetten geslaagd";
/* Label for phase row in the in the backup settings view. */
"SETTINGS_BACKUP_PHASE" = "Phase";
"SETTINGS_BACKUP_PHASE" = "Fase";
/* Label for phase row in the in the backup settings view. */
"SETTINGS_BACKUP_PROGRESS" = "Progress";
"SETTINGS_BACKUP_PROGRESS" = "Vooruitgang";
/* Label for backup status row in the in the backup settings view. */
"SETTINGS_BACKUP_STATUS" = "Status";
/* Indicates that the last backup failed. */
"SETTINGS_BACKUP_STATUS_FAILED" = "Backup Failed";
"SETTINGS_BACKUP_STATUS_FAILED" = "Back-up maken mislukt";
/* Indicates that app is not backing up. */
"SETTINGS_BACKUP_STATUS_IDLE" = "Waiting";
"SETTINGS_BACKUP_STATUS_IDLE" = "Bezig met wachten";
/* Indicates that app is backing up. */
"SETTINGS_BACKUP_STATUS_IN_PROGRESS" = "Backing Up";
"SETTINGS_BACKUP_STATUS_IN_PROGRESS" = "Bezig met Back-up maken";
/* Indicates that the last backup succeeded. */
"SETTINGS_BACKUP_STATUS_SUCCEEDED" = "Backup Successful";
"SETTINGS_BACKUP_STATUS_SUCCEEDED" = "Back-up succesvol";
/* No comment provided by engineer. */
"SETTINGS_CLEAR_HISTORY" = "Clear Conversation History";
"SETTINGS_CLEAR_HISTORY" = "Gespreksgeschiedenis wissen";
/* Confirmation text for button which deletes all message, calling, attachments, etc. */
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON" = "Delete Everything";
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON" = "Verwijder Alles";
/* Section header */
"SETTINGS_HISTORYLOG_TITLE" = "Clear Conversation History";
"SETTINGS_HISTORYLOG_TITLE" = "Gespreksgeschiedenis wissen";
/* Label for settings view that allows user to change the notification sound. */
"SETTINGS_ITEM_NOTIFICATION_SOUND" = "Message Sound";
"SETTINGS_ITEM_NOTIFICATION_SOUND" = "Geluid van bericht";
/* Setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS" = "Send Link Previews";
"SETTINGS_LINK_PREVIEWS" = "Link Previews verzenden";
/* Footer for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_FOOTER" = "Previews are supported for Imgur, Instagram, Pinterest, Reddit, and YouTube links.";
"SETTINGS_LINK_PREVIEWS_FOOTER" = "Voorbeelden worden ondersteund van Imgur, Instagram, Pinterest Reddit, en YouTube links.";
/* Header for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_HEADER" = "Link Previews";
"SETTINGS_LINK_PREVIEWS_HEADER" = "Link voorbeelden";
/* table section header */
"SETTINGS_NOTIFICATION_CONTENT_TITLE" = "Notification Content";
"SETTINGS_NOTIFICATION_CONTENT_TITLE" = "Notificatie Inhoud";
/* Label for the 'read receipts' setting. */
"SETTINGS_READ_RECEIPT" = "Read Receipts";
"SETTINGS_READ_RECEIPT" = "Leesbevestigingen";
/* An explanation of the 'read receipts' setting. */
"SETTINGS_READ_RECEIPTS_SECTION_FOOTER" = "See and share when messages have been read. This setting is optional and applies to all conversations.";
"SETTINGS_READ_RECEIPTS_SECTION_FOOTER" = "Bekijk en deel wanneer berichten zijn gelezen. Deze instelling is optioneel en geldt voor alle gesprekken.";
/* Label for the 'screen lock activity timeout' setting of the privacy settings. */
"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT" = "Screen Lock Timeout";
"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT" = "Time-out schermvergrendeling";
/* Title for the 'screen lock' section of the privacy settings. */
"SETTINGS_SCREEN_LOCK_SECTION_TITLE" = "Screen Lock";
"SETTINGS_SCREEN_LOCK_SECTION_TITLE" = "Schermvergrendeling";
/* Label for the 'enable screen lock' switch of the privacy settings. */
"SETTINGS_SCREEN_LOCK_SWITCH_LABEL" = "Screen Lock";
"SETTINGS_SCREEN_LOCK_SWITCH_LABEL" = "Schermvergrendeling";
/* Header Label for the sounds section of settings views. */
"SETTINGS_SECTION_SOUNDS" = "Sounds";
"SETTINGS_SECTION_SOUNDS" = "Geluiden";
/* Section header */
"SETTINGS_SECURITY_TITLE" = "Screen Security";
"SETTINGS_SECURITY_TITLE" = "Scherm beveiliging";
/* Label for the 'typing indicators' setting. */
"SETTINGS_TYPING_INDICATORS" = "Typing Indicators";
"SETTINGS_TYPING_INDICATORS" = "Typindicatoren";
/* Label for the 'no sound' option that allows users to disable sounds for notifications, etc. */
"SOUNDS_NONE" = "None";
"SOUNDS_NONE" = "Geen";
/* {{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 days}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS" = "%@ days";
"TIME_AMOUNT_DAYS" = "%@ dagen";
/* Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS_SHORT_FORMAT" = "%@d";
/* {{number of hours}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 hours}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_HOURS" = "%@ hours";
"TIME_AMOUNT_HOURS" = "%@ uren";
/* Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_HOURS_SHORT_FORMAT" = "%@h";
/* {{number of minutes}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 minutes}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_MINUTES" = "%@ minutes";
"TIME_AMOUNT_MINUTES" = "%@ minuten";
/* Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_MINUTES_SHORT_FORMAT" = "%@m";
/* {{number of seconds}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 seconds}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SECONDS" = "%@ seconds";
"TIME_AMOUNT_SECONDS" = "%@ seconden";
/* Label text below navbar button, embeds {{number of seconds}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5s' not '5 s'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SECONDS_SHORT_FORMAT" = "%@s";
/* {{1 day}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 day}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_DAY" = "%@ day";
"TIME_AMOUNT_SINGLE_DAY" = "%@ dag";
/* {{1 hour}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 hour}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_HOUR" = "%@ hour";
"TIME_AMOUNT_SINGLE_HOUR" = "%@ uur";
/* {{1 minute}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 minute}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_MINUTE" = "%@ minute";
"TIME_AMOUNT_SINGLE_MINUTE" = "%@ minuut";
/* {{1 week}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 week}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_WEEK" = "%@ week";
/* {{number of weeks}}, embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 weeks}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_WEEKS" = "%@ weeks";
"TIME_AMOUNT_WEEKS" = "%@ weken";
/* Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_WEEKS_SHORT_FORMAT" = "%@w";
/* Label for the cancel button in an alert or action sheet. */
"TXT_CANCEL_TITLE" = "Cancel";
"TXT_CANCEL_TITLE" = "Annuleren";
/* No comment provided by engineer. */
"TXT_DELETE_TITLE" = "Delete";
"TXT_DELETE_TITLE" = "Wissen";
/* Filename for voice messages. */
"VOICE_MESSAGE_FILE_NAME" = "Voice Message";
"VOICE_MESSAGE_FILE_NAME" = "Spraakbericht";
/* Message for the alert indicating the 'voice message' needs to be held to be held down to record. */
"VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE" = "Tap and hold to record a voice message.";
"VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE" = "Houd deze knop ingedrukt om een audio bericht te versturen.";
/* Title for the alert indicating the 'voice message' needs to be held to be held down to record. */
"VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE" = "Voice Message";
"VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE" = "Spraakbericht";
/* Info Message when you disable disappearing messages */
"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You disabled disappearing messages.";
"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Je hebt zelf-wissende berichten uitgeschakeld.";
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You set disappearing message time to %@";
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "U heeft de timer voor zelf-wissende berichten op %@ ingesteld";
// MARK: - Session
"continue_2" = "Continue";
"copy" = "Copy";
"invalid_url" = "Invalid URL";
"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 a Device";
"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_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 a Device";
"vc_link_device_scan_qr_code_tab_title" = "Scan QR Code";
"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_too_long_error" = "Please pick a shorter display name";
"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_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 routing your messages through multiple Service Nodes in Session's decentralized network. These are the countries your connection is currently being routed 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_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_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_too_many_group_members_error" = "A closed group cannot have more than 100 members";
"vc_join_public_chat_title" = "Join Open 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_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_display_name_too_long_error" = "Please pick a shorter display name";
"continue_2" = "Doorgaan";
"copy" = "Kopieer";
"invalid_url" = "Ongeldige URL";
"next" = "Volgende";
"share" = "Delen";
"invalid_session_id" = "Verkeerde Session ID";
"cancel" = "Annuleren";
"your_session_id" = "Uw Session-ID";
"vc_landing_title_2" = "Uw Sessie begint hier...";
"vc_landing_register_button_title" = "Session-ID aanmaken";
"vc_landing_restore_button_title" = "Doorgaan met je sessie";
"vc_landing_link_button_title" = "Koppel een apparaat";
"view_fake_chat_bubble_1" = "Wat is Session?";
"view_fake_chat_bubble_2" = "Het is een gedecentraliseerde, versleutelde berichten-app";
"view_fake_chat_bubble_3" = "Dus het verzamelt niet mijn persoonlijke informatie of de metagegevens van mijn gesprek? Hoe werkt het?";
"view_fake_chat_bubble_4" = "Met behulp van een combinatie van geavanceerde anonieme routing en end-to-end encryptietechnologieën.";
"view_fake_chat_bubble_5" = "Vrienden laten vrienden geen gecompromitteerde berichten apps gebruiken. Graag gedaan.";
"vc_register_title" = "Zeg hallo tegen uw Session-ID";
"vc_register_explanation" = "Uw Session-ID is het unieke adres dat mensen kunnen gebruiken om contact met u op te nemen via Session. Zonder verbinding met je echte identiteit, is je Session-ID volledig anoniem en privé.";
"vc_restore_title" = "Account herstellen";
"vc_restore_explanation" = "Voer de herstel zin in die je hebt gekregen toen je je hebt aangemeld om je account te herstellen.";
"vc_restore_seed_text_field_hint" = "Voer uw herstel zin in";
"vc_link_device_title" = "Koppel een apparaat";
"vc_link_device_scan_qr_code_tab_title" = "Scan QR-code";
"vc_display_name_title_2" = "Kies je weergavenaam";
"vc_display_name_explanation" = "Dit is je naam wanneer je Session gebruikt. Het kan je echte naam zijn, een alias, of wat je maar wilt.";
"vc_display_name_text_field_hint" = "Kies een weergavenaam";
"vc_display_name_display_name_missing_error" = "Voer a. u. b. een weergave naam in";
"vc_display_name_display_name_too_long_error" = "Kies een kortere weergavenaam";
"vc_pn_mode_recommended_option_tag" = "Aanbevolen";
"vc_pn_mode_no_option_picked_modal_title" = "Gelieve een optie selecteren";
"vc_home_empty_state_message" = "U heeft nog geen contactpersonen";
"vc_home_empty_state_button_title" = "Sessie starten";
"vc_seed_title" = "Uw Herstel Zin";
"vc_seed_title_2" = "Maak kennis met uw herstel zin";
"vc_seed_explanation" = "Uw herstel zin is de hoofdsleutel van uw Session-ID - u kunt deze gebruiken om uw Session-ID te herstellen als u de toegang tot uw apparaat verliest. Sla uw herstel zin op een veilige plek op en geef deze aan niemand op.";
"vc_seed_reveal_button_title" = "Ingedrukt houden om te onthullen";
"view_seed_reminder_subtitle_1" = "Beveilig je account door je herstel zin op te slaan";
"view_seed_reminder_subtitle_2" = "Houd de aangepaste woorden ingedrukt om uw herstelzin te onthullen, en sla het vervolgens veilig op om uw Session-ID te beveiligen.";
"view_seed_reminder_subtitle_3" = "Zorg ervoor dat u uw herstel zin op een veilige plek opslaat";
"vc_path_title" = "Locatie";
"vc_path_explanation" = "Session verbergt uw IP door uw berichten te routeren via meerdere Service Nodes in het gedecentraliseerde netwerk van Session. Dit zijn de landen die uw verbinding momenteel doorvoert:";
"vc_path_device_row_title" = "U";
"vc_path_guard_node_row_title" = "Invoer node";
"vc_path_service_node_row_title" = "Service node";
"vc_path_destination_row_title" = "Bestemming";
"vc_path_learn_more_button_title" = "Kom meer te weten";
"vc_create_private_chat_title" = "Nieuwe sessie";
"vc_create_private_chat_enter_session_id_tab_title" = "Voer Session-ID in";
"vc_create_private_chat_scan_qr_code_tab_title" = "Scan QR-code";
"vc_create_private_chat_scan_qr_code_explanation" = "Scan de QR-code van een gebruiker om een sessie te starten. QR-codes kunnen worden gevonden door op het QR-icoon in de accountinstellingen te tikken.";
"vc_enter_public_key_explanation" = "Gebruikers kunnen hun Session-ID delen door naar hun accountinstellingen te gaan en op \"Deel Session-ID\" te tikken, of door hun QR-code te delen.";
"vc_scan_qr_code_camera_access_explanation" = "Session heeft toegang nodig tot de camera om QR codes te scannen";
"vc_scan_qr_code_grant_camera_access_button_title" = "Toegang tot camera verlenen";
"vc_create_closed_group_title" = "Nieuwe Gesloten Groep";
"vc_create_closed_group_text_field_hint" = "Vul een groepsnaam in";
"vc_create_closed_group_empty_state_message" = "U heeft nog geen contactpersonen";
"vc_create_closed_group_empty_state_button_title" = "Sessie starten";
"vc_create_closed_group_group_name_missing_error" = "Vul een groepsnaam in";
"vc_create_closed_group_group_name_too_long_error" = "Vul a. u. b een kortere groepsnaam in";
"vc_create_closed_group_too_many_group_members_error" = "Een gesloten groep kan niet meer dan 100 leden hebben";
"vc_join_public_chat_title" = "Deelnemen aan Open groep";
"vc_join_public_chat_enter_group_url_tab_title" = "Open Groep-URL openen";
"vc_join_public_chat_scan_qr_code_tab_title" = "QR-code scannen";
"vc_join_public_chat_scan_qr_code_explanation" = "Scan de QR-code van de open groep waar u zich bij wilt aansluiten";
"vc_enter_chat_url_text_field_hint" = "Voer een open groep URL in";
"vc_settings_title" = "Instellingen";
"vc_settings_display_name_text_field_hint" = "Kies een weergavenaam";
"vc_settings_display_name_missing_error" = "Voer a. u. b. een weergave naam in";
"vc_settings_display_name_too_long_error" = "Kies een kortere weergavenaam";
"vc_settings_privacy_button_title" = "Privacy";
"vc_settings_notifications_button_title" = "Notifications";
"vc_settings_recovery_phrase_button_title" = "Recovery Phrase";
"vc_settings_clear_all_data_button_title" = "Clear Data";
"vc_notification_settings_title" = "Notifications";
"vc_settings_notifications_button_title" = "Meldingen";
"vc_settings_recovery_phrase_button_title" = "Herstel zin";
"vc_settings_clear_all_data_button_title" = "Gegevens wissen";
"vc_notification_settings_title" = "Meldingen";
"vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Notification Strategy";
"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.";
"preferences_notifications_strategy_category_title" = "Notificatie Inhoud";
"modal_seed_title" = "Uw Herstel Zin";
"modal_seed_explanation" = "Dit is uw herstel zin, Hiermee kun je je sessie-ID herstellen of migreren naar een nieuw apparaat.";
"modal_clear_all_data_title" = "Wis alle gegevens";
"modal_clear_all_data_explanation" = "Hiermee worden uw berichten, sessies en contacten permanent verwijderd.";
"vc_qr_code_title" = "QR-code";
"vc_qr_code_view_my_qr_code_tab_title" = "Bekijk mijn QR-code";
"vc_qr_code_view_scan_qr_code_tab_title" = "QR-code scannen";
"vc_qr_code_view_scan_qr_code_explanation" = "Scan iemands QR-code om een gesprek te beginnen";
"vc_view_my_qr_code_explanation" = "Dit is je QR-code. Andere gebruikers kunnen deze scannen om een sessie met je te starten.";
// MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers.";
"fast_mode" = "Fast Mode";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background.";
"slow_mode" = "Slow Mode";
"vc_pn_mode_title" = "Message Notifications";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code.";
"vc_enter_recovery_phrase_title" = "Recovery Phrase";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up.";
"vc_enter_public_key_text_field_hint" = "Enter Session ID or ONS name";
"vc_home_title" = "Messages";
"admin_group_leave_warning" = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone.";
"vc_join_open_group_suggestions_title" = "Or join one of these...";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session";
"copied" = "Copied";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID";
"vc_conversation_input_prompt" = "Message";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel";
"modal_download_attachment_title" = "Trust %@?";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?";
"modal_download_button_title" = "Download";
"modal_open_url_title" = "Open URL?";
"modal_open_url_explanation" = "Are you sure you want to open %@?";
"modal_open_url_button_title" = "Open";
"modal_blocked_title" = "Unblock %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?";
"modal_blocked_button_title" = "Unblock";
"modal_link_previews_title" = "Enable Link Previews?";
"modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings.";
"modal_link_previews_button_title" = "Enable";
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"fast_mode_explanation" = "Je wordt op een betrouwbare en onmiddellijke manier op de hoogte gebracht van nieuwe berichten via Apple's notificatieservers.";
"fast_mode" = "Snelle Modus";
"slow_mode_explanation" = "Sessie controleert af en toe op nieuwe berichten in de achtergrond.";
"slow_mode" = "Langzame modus";
"vc_pn_mode_title" = "Berichtmeldingen";
"vc_notification_settings_notification_mode_title" = "Gebruik snelle modus";
"vc_link_device_recovery_phrase_tab_title" = "Herstel zin";
"vc_link_device_scan_qr_code_explanation" = "Navigeer naar Instellingen → Herstel zin op je andere apparaat om je QR-code te tonen.";
"vc_enter_recovery_phrase_title" = "Herstel zin";
"vc_enter_recovery_phrase_explanation" = "Om je apparaat te koppelen, voer je de herstelzin in die je kreeg toen je je aanmeldde.";
"vc_enter_public_key_text_field_hint" = "Voer Session-ID of ONS naam in";
"vc_home_title" = "Berichten";
"admin_group_leave_warning" = "Omdat u de maker van deze groep bent, wordt het voor iedereen verwijderd. Dit kan niet ongedaan worden gemaakt.";
"vc_join_open_group_suggestions_title" = "Of neem deel aan een van deze...";
"vc_settings_invite_a_friend_button_title" = "Nodig een vriend uit";
"vc_settings_help_us_translate_button_title" = "Help ons om Session the vertalen";
"copied" = "Gekopieerd";
"vc_conversation_settings_copy_session_id_button_title" = "Session-ID kopiëren";
"vc_conversation_input_prompt" = "Bericht";
"vc_conversation_voice_message_cancel_message" = "Veeg om te annuleren";
"modal_download_attachment_title" = "Vertrouw %@?";
"modal_download_attachment_explanation" = "Weet je zeker dat je deze media van %@ wilt downloaden?";
"modal_download_button_title" = "Downloaden";
"modal_open_url_title" = "URL openen?";
"modal_open_url_explanation" = "Weet u zeker dat u %@ wilt openen?";
"modal_open_url_button_title" = "Openen";
"modal_blocked_title" = "Blokkering opheffen voor %@?";
"modal_blocked_explanation" = "Weet je zeker dat je %@ weer wilt toelaten?";
"modal_blocked_button_title" = "Deblokkeren";
"modal_link_previews_title" = "Linkvoorbeeld inschakelen?";
"modal_link_previews_explanation" = "Link previews inschakelen zal previews tonen voor URLs die u verstuurt en ontvangt. Dit kan nuttig zijn, maar Session moet contact opnemen met gekoppelde websites om previews te genereren. U kunt links altijd uitschakelen in de Sessions-instellingen.";
"modal_link_previews_button_title" = "Inschakelen";
"vc_share_title" = "Delen naar de Session";
"vc_share_loading_message" = "Bijlagen voorbereiden...";
"vc_share_sending_message" = "Aan het verzenden...";
"view_open_group_invitation_description" = "Open groepsuitnodiging";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Zablokować %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokuj";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "Zablokowano %@.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Zablokowano użytkownika";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Zablokowani użytkownicy nie będą mogli do Ciebie dzwonić ani wysyłać Ci wiadomości.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Nie możemy aktywować Twojego konta, dopóki nie zweryfikujesz kodu, który Ci wysłaliśmy.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Brak";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Uwierzytelnij, by używać Session";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Uwierzytelnianie nie powiodło się.";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquear %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ foi bloqueado.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Pessoa sob Bloqueio";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Você não receberá mais ligações e mensagens de quem bloquear.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Não podemos ativar sua conta até que você verifique o código que enviamos.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instante";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Autentique-se para abrir o Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Falha na Autenticação";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -491,7 +497,9 @@
"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ê.";
// MARK: - Not Yet Translated
"fast_mode_explanation" = "Você será notificado de forma confiável e imediata sobre novas mensagens usando os servidores de notificação da Apple.";
"fast_mode" = "Modo Rápido";
"slow_mode_explanation" = "O session verificará ocasionalmente por novas mensagens em segundo plano.";
"slow_mode" = "Modo Lento";
"vc_pn_mode_title" = "Notificação de Mensangens";
"vc_notification_settings_notification_mode_title" = "Usar Modo Rápido";
"vc_link_device_recovery_phrase_tab_title" = "Frase de Recuperação";
@ -523,3 +531,4 @@
"vc_share_title" = "Compartilhar no Session";
"vc_share_loading_message" = "Preparando anexos...";
"vc_share_sending_message" = "Enviando...";
"view_open_group_invitation_description" = "Convite para grupo aberto";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Заблокировать %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Разблокировать";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ был(-а) заблокирован(-а).";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "Пользователь заблокирован";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Заблокированные пользователи не смогут звонить или отправлять сообщения Вам.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Невозможно активировать аккаунт до подтверждения отправленного вам кода.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Мгновенно";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Аутентификация для открытия Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Ошибка аутентификации";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokovať %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokovať";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokovaný používateľ vám nebude mocť volať ani posielať správy.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Nemôžeme aktivovať váš účet kým neoveríte kód, ktorý sme vám poslali.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Okamžite";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to open Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Overenie zlyhalo";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "%@ has been blocked.";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "User Blocked";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "We can't activate your account until you verify the code we sent you.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instant";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to open Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -0,0 +1,535 @@
/* Label for the 'dismiss' button in the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_DISMISS_BUTTON" = "稍後";
/* Message format for the 'new app version available' alert. Embeds: {{The latest app version number}} */
"APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT" = "版本:%@ 現在已經可以下載。";
/* Title for the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_TITLE" = "更新版本的 Session 已推出";
/* Label for the 'update' button in the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_UPDATE_BUTTON" = "更新";
/* No comment provided by engineer. */
"ATTACHMENT" = "附件";
/* One-line label indicating the user can add no more text to the attachment caption. */
"ATTACHMENT_APPROVAL_CAPTION_LENGTH_LIMIT_REACHED" = "附件標題長度已達限制";
/* placeholder text for an empty captioning field */
"ATTACHMENT_APPROVAL_CAPTION_PLACEHOLDER" = "新增標題⋯";
/* Title for 'caption' mode of the attachment approval view. */
"ATTACHMENT_APPROVAL_CAPTION_TITLE" = "標題";
/* Format string for file extension label in call interstitial view */
"ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT" = "檔案類型: %@";
/* Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}. */
"ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT" = "大小:%@";
/* One-line label indicating the user can add no more text to the media message field. */
"ATTACHMENT_APPROVAL_MESSAGE_LENGTH_LIMIT_REACHED" = "訊息長度已達限制";
/* Label for 'send' button in the 'attachment approval' dialog. */
"ATTACHMENT_APPROVAL_SEND_BUTTON" = "發送";
/* Generic filename for an attachment with no known name */
"ATTACHMENT_DEFAULT_FILENAME" = "附件";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "寄送附件時發生錯誤";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "選取檔案時發生錯誤";
/* Alert body when picking a document fails because user picked a directory/bundle */
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY" = "請嘗試將其轉換為壓縮檔或者再嘗試重新發送一次";
/* Alert title when picking a document fails because user picked a directory/bundle */
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE" = "不支援的檔案類型";
/* Short text label for a voice message attachment, used for thread preview and on the lock screen */
"ATTACHMENT_TYPE_VOICE_MESSAGE" = "語音訊息";
/* Error indicating the backup export could not export the user's data. */
"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT" = "備份的檔案無法被匯出";
/* Error indicating that the app received an invalid response from CloudKit. */
"BACKUP_EXPORT_ERROR_INVALID_CLOUDKIT_RESPONSE" = "系統傳送了無法辨識的回覆";
/* Indicates that the cloud is being cleaned up. */
"BACKUP_EXPORT_PHASE_CLEAN_UP" = "清除備份";
/* Indicates that the backup export is being configured. */
"BACKUP_EXPORT_PHASE_CONFIGURATION" = "正在初始化備份程序";
/* Indicates that the database data is being exported. */
"BACKUP_EXPORT_PHASE_DATABASE_EXPORT" = "輸出檔案中";
/* Indicates that the backup export data is being exported. */
"BACKUP_EXPORT_PHASE_EXPORT" = "輸出備份中";
/* Indicates that the backup export data is being uploaded. */
"BACKUP_EXPORT_PHASE_UPLOAD" = "上傳備份中";
/* Error indicating the backup import could not import the user's data. */
"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT" = "無法匯入備份";
/* Indicates that the backup import is being configured. */
"BACKUP_IMPORT_PHASE_CONFIGURATION" = "正在處理備份";
/* Indicates that the backup import data is being downloaded. */
"BACKUP_IMPORT_PHASE_DOWNLOAD" = "下載備份檔案";
/* Indicates that the backup import data is being finalized. */
"BACKUP_IMPORT_PHASE_FINALIZING" = "即將完成";
/* Indicates that the backup import data is being imported. */
"BACKUP_IMPORT_PHASE_IMPORT" = "導入備份中";
/* Indicates that the backup database is being restored. */
"BACKUP_IMPORT_PHASE_RESTORING_DATABASE" = "正在回復備份數據";
/* Indicates that the backup import data is being restored. */
"BACKUP_IMPORT_PHASE_RESTORING_FILES" = "正在回復匯入的備份";
/* Label for the backup restore decision section. */
"BACKUP_RESTORE_DECISION_TITLE" = "可以執行備份";
/* Label for the backup restore description. */
"BACKUP_RESTORE_DESCRIPTION" = "從備份回復";
/* Label for the backup restore progress. */
"BACKUP_RESTORE_PROGRESS" = "進度";
/* Label for the backup restore status. */
"BACKUP_RESTORE_STATUS" = "狀態";
/* Error shown when backup fails due to an unexpected error. */
"BACKUP_UNEXPECTED_ERROR" = "不明的錯誤,請聯繫開發者";
/* Button label for the 'block' button */
"BLOCK_LIST_BLOCK_BUTTON" = "封鎖";
/* A format for the 'block user' action sheet title. Embeds {{the blocked user's name or phone number}}. */
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "封鎖 %@";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "解除封鎖";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已封鎖 %@。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "使用者已封鎖";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被您封鎖的使用者將無法傳送訊息與撥打電話給您";
/* Label for generic done button. */
"BUTTON_DONE" = "完成";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "選擇";
/* The label for the 'do not restore backup' button. */
"CHECK_FOR_BACKUP_DO_NOT_RESTORE" = "不要回復備份";
/* The label for the 'restore backup' button. */
"CHECK_FOR_BACKUP_RESTORE" = "回復";
/* Error indicating that the app could not determine that user's iCloud account status */
"CLOUDKIT_STATUS_COULD_NOT_DETERMINE" = "Session 無法確定您的 iCloud 帳號狀態,請進入設定值重新登入您的 iCloud 帳號以備份您的資料。";
/* Error indicating that user does not have an iCloud account. */
"CLOUDKIT_STATUS_NO_ACCOUNT" = "沒有偵測到 iCloud 帳戶,請進入設定登入您的 iCloud 帳號。";
/* Error indicating that the app was prevented from accessing the user's iCloud account. */
"CLOUDKIT_STATUS_RESTRICTED" = "Session 因為您的 iCloud 帳號設定並沒有將 Session 的備份選項開啟請進入設定並打開您iCloud 的設定開啟 Session 的備份功能。";
/* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "您已經無法再於此群組傳送訊息或撥打電話。";
/* Alert title */
"CONFIRM_LEAVE_GROUP_TITLE" = "您真的想要離開嗎?";
/* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "這個動作無法被撤銷。";
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "刪除對話?";
/* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "沒有結果。";
/* keyboard toolbar label when exactly 1 message matches the search string */
"CONVERSATION_SEARCH_ONE_RESULT" = "一個結果。";
/* keyboard toolbar label when more than 1 message matches the search string. Embeds {{number/position of the 'currently viewed' result}} and the {{total number of results}} */
"CONVERSATION_SEARCH_RESULTS_FORMAT" = "%d/%d) 符合。";
/* title for conversation settings screen */
"CONVERSATION_SETTINGS" = "對話設定";
/* table cell label in conversation settings */
"CONVERSATION_SETTINGS_BLOCK_THIS_USER" = "封鎖此用戶";
/* Title of the 'mute this thread' action sheet. */
"CONVERSATION_SETTINGS_MUTE_ACTION_SHEET_TITLE" = "靜音";
/* label for 'mute thread' cell in conversation settings */
"CONVERSATION_SETTINGS_MUTE_LABEL" = "靜音";
/* Indicates that the current thread is not muted. */
"CONVERSATION_SETTINGS_MUTE_NOT_MUTED" = "未靜音";
/* Label for button to mute a thread for a day. */
"CONVERSATION_SETTINGS_MUTE_ONE_DAY_ACTION" = "靜音一天";
/* Label for button to mute a thread for a hour. */
"CONVERSATION_SETTINGS_MUTE_ONE_HOUR_ACTION" = "靜音一個小時";
/* Label for button to mute a thread for a minute. */
"CONVERSATION_SETTINGS_MUTE_ONE_MINUTE_ACTION" = "靜音一分鐘";
/* Label for button to mute a thread for a week. */
"CONVERSATION_SETTINGS_MUTE_ONE_WEEK_ACTION" = "靜音一個禮拜";
/* Label for button to mute a thread for a year. */
"CONVERSATION_SETTINGS_MUTE_ONE_YEAR_ACTION" = "靜音一年";
/* Indicates that this thread is muted until a given date or time. Embeds {{The date or time which the thread is muted until}}. */
"CONVERSATION_SETTINGS_MUTED_UNTIL_FORMAT" = "直到:%@";
/* Table cell label in conversation settings which returns the user to the conversation with 'search mode' activated */
"CONVERSATION_SETTINGS_SEARCH" = "搜尋對話";
/* Label for button to unmute a thread. */
"CONVERSATION_SETTINGS_UNMUTE_ACTION" = "解除靜音";
/* Title for the 'crop/scale image' dialog. */
"CROP_SCALE_IMAGE_VIEW_TITLE" = "移動與裁切";
/* Subtitle shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_SUBTITLE" = "這會花上幾分鐘。";
/* Title shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_TITLE" = "最佳化資料庫中";
/* Format string for a relative time, expressed as a certain number of hours in the past. Embeds {{The number of hours}}. */
"DATE_HOURS_AGO_FORMAT" = "%@ 個小時前";
/* Format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}. */
"DATE_MINUTES_AGO_FORMAT" = "%@ 分鐘前";
/* The present; the current time. */
"DATE_NOW" = "現在";
/* The current day. */
"DATE_TODAY" = "今天";
/* The day before today. */
"DATE_YESTERDAY" = "昨天";
/* table cell label in conversation settings */
"DISAPPEARING_MESSAGES" = "消失的訊息";
/* Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear, see the *_TIME_AMOUNT strings for context. */
"DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT" = "在這則對話的訊息將於 %@ 後消失";
/* table cell label in conversation settings */
"EDIT_GROUP_ACTION" = "編輯群組";
/* Label indicating media gallery is empty */
"GALLERY_TILES_EMPTY_GALLERY" = "此對話中沒有媒體。";
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_MORE_RECENT_LABEL" = "讀取新媒體中⋯";
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_OLDER_LABEL" = "讀取舊媒體中⋯";
/* Error displayed when there is a failure fetching a GIF from the remote service. */
"GIF_PICKER_ERROR_FETCH_FAILURE" = "無法讀取此 GIF。請驗證您是否連結到網路。";
/* Generic error displayed when picking a GIF */
"GIF_PICKER_ERROR_GENERIC" = "發生不知名的錯誤。";
/* Shown when selected GIF couldn't be fetched */
"GIF_PICKER_FAILURE_ALERT_TITLE" = "無法選取此 GIF。";
/* Alert message shown when user tries to search for GIFs without entering any search terms. */
"GIF_PICKER_VIEW_MISSING_QUERY" = "請輸入您的搜尋內容";
/* Indicates that an error occurred while searching. */
"GIF_VIEW_SEARCH_ERROR" = "錯誤,請重試。";
/* Indicates that the user's search had no results. */
"GIF_VIEW_SEARCH_NO_RESULTS" = "沒有結果";
/* No comment provided by engineer. */
"GROUP_CREATED" = "已創立群組";
/* No comment provided by engineer. */
"GROUP_MEMBER_JOINED" = " %@ 已加入群組。 ";
/* No comment provided by engineer. */
"GROUP_MEMBER_LEFT" = " %@ 已離開群組。 ";
/* No comment provided by engineer. */
"GROUP_MEMBER_REMOVED" = " %@ 被踢出群組。 ";
/* No comment provided by engineer. */
"GROUP_MEMBERS_REMOVED" = " %@ 已被群組踢出 ";
/* No comment provided by engineer. */
"GROUP_TITLE_CHANGED" = "標題已更改為 %@ ";
/* No comment provided by engineer. */
"GROUP_UPDATED" = "群組已更新";
/* No comment provided by engineer. */
"GROUP_YOU_LEFT" = "您已離開群組";
/* No comment provided by engineer. */
"YOU_WERE_REMOVED" = " 您已被群組踢出 ";
/* Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared. */
"IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "無法分享超過 %@ 個項目。";
/* alert title */
"IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS" = "無法選取此附件。";
/* Message for the alert indicating that an audio file is invalid. */
"INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE" = "無效的聲音檔案。";
/* Slider label when disappearing messages is off */
"KEEP_MESSAGES_FOREVER" = "訊息不會消失。";
/* Confirmation button within contextual alert */
"LEAVE_BUTTON_TITLE" = "離開";
/* table cell label in conversation settings */
"LEAVE_GROUP_ACTION" = "離開群組";
/* Title for the 'long text message' view. */
"LONG_TEXT_VIEW_TITLE" = "訊息";
/* nav bar button item */
"MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON" = "全部媒體";
/* media picker option to choose from library */
"MEDIA_FROM_LIBRARY_BUTTON" = "照片圖庫";
/* Confirmation button text to delete selected media from the gallery, embeds {{number of messages}} */
"MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT" = "刪除 %d 訊息";
/* Confirmation button text to delete selected media message from the gallery */
"MEDIA_GALLERY_DELETE_SINGLE_MESSAGE" = "刪除訊息";
/* embeds {{sender name}} and {{sent datetime}}, e.g. 'Sarah on 10/30/18, 3:29' */
"MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT" = "%@ 於 %@";
/* Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}. */
"MEDIA_GALLERY_MORE_ITEMS_FORMAT" = "+%@";
/* Short sender label for media sent by you */
"MEDIA_GALLERY_SENDER_NAME_YOU" = "您";
/* Section header in media gallery collection view */
"MEDIA_GALLERY_THIS_MONTH_HEADER" = "這個月";
/* message status for message delivered to their recipient. */
"MESSAGE_STATUS_DELIVERED" = "已傳送";
/* status message for failed messages */
"MESSAGE_STATUS_FAILED" = "傳送失敗";
/* status message for failed messages */
"MESSAGE_STATUS_FAILED_SHORT" = "失敗";
/* status message for read messages */
"MESSAGE_STATUS_READ" = "已讀";
/* message status if message delivery to a recipient is skipped. We skip delivering group messages to users who have left the group or unregistered their Session account. */
"MESSAGE_STATUS_RECIPIENT_SKIPPED" = "已跳過傳送";
/* message status while message is sending. */
"MESSAGE_STATUS_SENDING" = "傳送中⋯";
/* status message for sent messages */
"MESSAGE_STATUS_SENT" = "已傳送";
/* status message while attachment is uploading */
"MESSAGE_STATUS_UPLOADING" = "上傳中⋯";
/* Alert body when user has previously denied media library access */
"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE" = "您可以於設定中啟用這項權限(系統設定)。";
/* Alert title when user has previously denied media library access */
"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE" = "Session 需要存取您的相片圖庫以使用此功能。";
/* An explanation of the consequences of muting a thread. */
"MUTE_BEHAVIOR_EXPLANATION" = "您將不會收到被禁音的對話的通知。";
/* notification title. Embeds {{author name}} and {{group name}} */
"NEW_GROUP_MESSAGE_NOTIFICATION_TITLE" = "%@ 傳送到 %@";
/* Label for 1:1 conversation with yourself. */
"NOTE_TO_SELF" = "小筆記";
/* Lock screen notification text presented after user powers on their device without unlocking. Embeds {{device model}} (either 'iPad' or 'iPhone') */
"NOTIFICATION_BODY_PHONE_LOCKED_FORMAT" = "在您的設備 %@ 重新啟動時接收到訊息。";
/* No comment provided by engineer. */
"NOTIFICATIONS_FOOTER_WARNING" = "因為 Apple 系統推送通知的一個已知漏洞訊息預覽只會在傳送後30秒內被接收的狀況下顯示圖示的通知數字角標可能會不同。";
/* Table cell switch label. When disabled, Session will not play notification sounds while the app is in the foreground. */
"NOTIFICATIONS_SECTION_INAPP" = "當 App 開啟時播放通知聲音";
/* Label for settings UI that allows user to change the notification sound. */
"NOTIFICATIONS_SECTION_SOUNDS" = "音效";
/* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_AND_MESSAGE" = "名稱與內容";
/* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_ONLY" = "只有名稱";
/* No comment provided by engineer. */
"NOTIFICATIONS_NONE" = "沒有名稱與內容";
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "顯示";
/* No comment provided by engineer. */
"OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 取消了閱後即焚模式";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 設定閱後即焚模式時間至 %@";
/* alert title, generic error preventing user from capturing a photo */
"PHOTO_CAPTURE_GENERIC_ERROR" = "無法讀取圖片。";
/* alert title */
"PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE" = "無法讀取圖片。";
/* alert title */
"PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA" = "無法打開相機。";
/* label for system photo collections which have no name. */
"PHOTO_PICKER_UNNAMED_COLLECTION" = "未命名相簿";
/* alert body during registration */
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "我們無法為您啟用帳號,直到您驗證了傳送給您的代碼。";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "即時";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to open Session.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "驗證失敗";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
"SEND_MEDIA_ABANDON_TITLE" = "放棄媒體?";
/* alert action, confirming the user wants to exit the media flow and abandon any photos they've taken */
"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "放棄媒體";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_CAMERA" = "回到相機";
/* alert action when the user decides not to cancel the media flow after all. */
"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "回到媒體庫";
/* Format string for the default 'Note' sound. Embeds the system {{sound name}}. */
"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (default)";
/* Label for the backup view in app settings. */
"SETTINGS_BACKUP" = "備份";
/* Label for 'backup now' button in the backup settings view. */
"SETTINGS_BACKUP_BACKUP_NOW" = "現在備份";
/* Label for 'cancel backup' button in the backup settings view. */
"SETTINGS_BACKUP_CANCEL_BACKUP" = "取消備份";
/* Label for switch in settings that controls whether or not backup is enabled. */
"SETTINGS_BACKUP_ENABLING_SWITCH" = "已啟用備份";
/* Label for iCloud status row in the in the backup settings view. */
"SETTINGS_BACKUP_ICLOUD_STATUS" = "iCloud 狀態";
/* Indicates that the last backup restore failed. */
"SETTINGS_BACKUP_IMPORT_STATUS_FAILED" = "回復失敗。";
/* Indicates that app is not restoring up. */
"SETTINGS_BACKUP_IMPORT_STATUS_IDLE" = "回復已停止。";
/* Indicates that app is restoring up. */
"SETTINGS_BACKUP_IMPORT_STATUS_IN_PROGRESS" = "回復中⋯";
/* Indicates that the last backup restore succeeded. */
"SETTINGS_BACKUP_IMPORT_STATUS_SUCCEEDED" = "回復完成。";
/* Label for phase row in the in the backup settings view. */
"SETTINGS_BACKUP_PHASE" = "階段";
/* Label for phase row in the in the backup settings view. */
"SETTINGS_BACKUP_PROGRESS" = "進度";
/* Label for backup status row in the in the backup settings view. */
"SETTINGS_BACKUP_STATUS" = "狀態";
/* Indicates that the last backup failed. */
"SETTINGS_BACKUP_STATUS_FAILED" = "備份失敗。";
/* Indicates that app is not backing up. */
"SETTINGS_BACKUP_STATUS_IDLE" = "等待中";
/* Indicates that app is backing up. */
"SETTINGS_BACKUP_STATUS_IN_PROGRESS" = "備份中⋯";
/* Indicates that the last backup succeeded. */
"SETTINGS_BACKUP_STATUS_SUCCEEDED" = "備份完成。";
/* No comment provided by engineer. */
"SETTINGS_CLEAR_HISTORY" = "清除對話";
/* Confirmation text for button which deletes all message, calling, attachments, etc. */
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON" = "刪除所有內容";
/* Section header */
"SETTINGS_HISTORYLOG_TITLE" = "刪除對話紀錄";
/* Label for settings view that allows user to change the notification sound. */
"SETTINGS_ITEM_NOTIFICATION_SOUND" = "訊息通知";
/* Setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS" = "送出的連結預覽";
/* Footer for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_FOOTER" = "預覽功能支援 Imgur, Instagram, Pinterest, Reddit, 跟 YouTube 連結。";
/* Header for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_HEADER" = "連結預覽";
/* table section header */
"SETTINGS_NOTIFICATION_CONTENT_TITLE" = "通知內容";
/* Label for the 'read receipts' setting. */
"SETTINGS_READ_RECEIPT" = "已讀回條";
/* An explanation of the 'read receipts' setting. */
"SETTINGS_READ_RECEIPTS_SECTION_FOOTER" = "See and share when messages have been read. This setting is optional and applies to all conversations.";
/* Label for the 'screen lock activity timeout' setting of the privacy settings. */
"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT" = "Screen Lock Timeout";
/* Title for the 'screen lock' section of the privacy settings. */
"SETTINGS_SCREEN_LOCK_SECTION_TITLE" = "Screen Lock";
/* Label for the 'enable screen lock' switch of the privacy settings. */
"SETTINGS_SCREEN_LOCK_SWITCH_LABEL" = "Screen Lock";
/* Header Label for the sounds section of settings views. */
"SETTINGS_SECTION_SOUNDS" = "Sounds";
/* Section header */
"SETTINGS_SECURITY_TITLE" = "Screen Security";
/* Label for the 'typing indicators' setting. */
"SETTINGS_TYPING_INDICATORS" = "Typing Indicators";
/* Label for the 'no sound' option that allows users to disable sounds for notifications, etc. */
"SOUNDS_NONE" = "None";
/* {{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 days}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS" = "%@ days";
/* Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS_SHORT_FORMAT" = "%@d";
/* {{number of hours}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 hours}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_HOURS" = "%@ hours";
/* Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_HOURS_SHORT_FORMAT" = "%@h";
/* {{number of minutes}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 minutes}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_MINUTES" = "%@ minutes";
/* Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_MINUTES_SHORT_FORMAT" = "%@m";
/* {{number of seconds}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 seconds}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SECONDS" = "%@ seconds";
/* Label text below navbar button, embeds {{number of seconds}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5s' not '5 s'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SECONDS_SHORT_FORMAT" = "%@s";
/* {{1 day}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 day}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_DAY" = "%@ day";
/* {{1 hour}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 hour}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_HOUR" = "%@ hour";
/* {{1 minute}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 minute}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_MINUTE" = "%@ minute";
/* {{1 week}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 week}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_SINGLE_WEEK" = "%@ week";
/* {{number of weeks}}, embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 weeks}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_WEEKS" = "%@ weeks";
/* Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 characters, The space is intentionally omitted between the text and the embedded duration so that we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_WEEKS_SHORT_FORMAT" = "%@w";
/* Label for the cancel button in an alert or action sheet. */
"TXT_CANCEL_TITLE" = "Cancel";
/* No comment provided by engineer. */
"TXT_DELETE_TITLE" = "Delete";
/* Filename for voice messages. */
"VOICE_MESSAGE_FILE_NAME" = "Voice Message";
/* Message for the alert indicating the 'voice message' needs to be held to be held down to record. */
"VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE" = "Tap and hold to record a voice message.";
/* Title for the alert indicating the 'voice message' needs to be held to be held down to record. */
"VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE" = "Voice Message";
/* Info Message when you disable disappearing messages */
"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You disabled disappearing messages.";
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You set disappearing message time to %@";
// MARK: - Session
"continue_2" = "Continue";
"copy" = "Copy";
"invalid_url" = "Invalid URL";
"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 a Device";
"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_restore_title" = "Restore your account";
"vc_restore_explanation" = "請輸入註冊時的回復用字句來回復中您的帳號。";
"vc_restore_seed_text_field_hint" = "請輸入您的回復用字句";
"vc_link_device_title" = "連結設備";
"vc_link_device_scan_qr_code_tab_title" = "掃描 QR Code";
"vc_display_name_title_2" = "請輸入您的名稱";
"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_too_long_error" = "Please pick a shorter display name";
"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_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 routing your messages through multiple Service Nodes in Session's decentralized network. These are the countries your connection is currently being routed 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_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_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_too_many_group_members_error" = "A closed group cannot have more than 100 members";
"vc_join_public_chat_title" = "Join Open 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_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_display_name_too_long_error" = "Please pick a shorter display name";
"vc_settings_privacy_button_title" = "隱私權條款";
"vc_settings_notifications_button_title" = "通知";
"vc_settings_recovery_phrase_button_title" = "回復用字句";
"vc_settings_clear_all_data_button_title" = "清除資料";
"vc_notification_settings_title" = "通知";
"vc_privacy_settings_title" = "隱私權條款";
"preferences_notifications_strategy_category_title" = "通知類型";
"modal_seed_title" = "您的回復用字句";
"modal_seed_explanation" = "這是您的回復用字句,您可以利用此字句來回復或轉移您的帳號至新的裝置上。";
"modal_clear_all_data_title" = "清除所有資料";
"modal_clear_all_data_explanation" = "這樣做將永久清除您的訊息,帳號與聯絡人。";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "查看我的 QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "掃描 QR Code";
"vc_qr_code_view_scan_qr_code_explanation" = "掃描朋友的 QR Code 來開始對話。";
"vc_view_my_qr_code_explanation" = "這是您的 QR Code其他使用者可以掃描來與您對話。";
// MARK: - Not Yet Translated
"fast_mode_explanation" = "您將會透過 Apple 的通知服務可靠且迅速的收到通知。";
"fast_mode" = "性能模式";
"slow_mode_explanation" = "Session 會偶爾在背景執行時檢查新訊息。";
"slow_mode" = "慢速模式";
"vc_pn_mode_title" = "訊息通知";
"vc_notification_settings_notification_mode_title" = "使用性能模式";
"vc_link_device_recovery_phrase_tab_title" = "回復用字句";
"vc_link_device_scan_qr_code_explanation" = "請使用您其他的裝置並前往設定 → 回復用字句 來顯示您的QR Code。";
"vc_enter_recovery_phrase_title" = "回復用字句";
"vc_enter_recovery_phrase_explanation" = "如您需要連結您的裝置,請輸入申請帳號時您的回復用字句。";
"vc_enter_public_key_text_field_hint" = "請輸入您的 ID 或 ONS 的名稱";
"vc_home_title" = "訊息";
"admin_group_leave_warning" = "因為您是此群組的創立人所以將會刪除所有人的群組,此動作將不能被撤銷。";
"vc_join_open_group_suggestions_title" = "或加入這些群組⋯";
"vc_settings_invite_a_friend_button_title" = "邀請好友";
"vc_settings_help_us_translate_button_title" = "幫助我們翻譯 Session";
"copied" = "已複製";
"vc_conversation_settings_copy_session_id_button_title" = "複製 Session ID";
"vc_conversation_input_prompt" = "訊息";
"vc_conversation_voice_message_cancel_message" = "滑動以取消";
"modal_download_attachment_title" = "是否信任 %@";
"modal_download_attachment_explanation" = "您確定要下載 %@ 傳送的媒體嗎?";
"modal_download_button_title" = "下載";
"modal_open_url_title" = "打開連結?";
"modal_open_url_explanation" = "您確定要打開 %@ 嗎?";
"modal_open_url_button_title" = "打開";
"modal_blocked_title" = "解除封鎖 %@ 嗎?";
"modal_blocked_explanation" = "您確定要解除封鎖 %@ 嗎?";
"modal_blocked_button_title" = "解除封鎖";
"modal_link_previews_title" = "是否要啟用連結預覽?";
"modal_link_previews_explanation" = "啟用連結預覽將會讓您送出與接收的 URLs 啟用預覽,這是一項好用的功能,但是 Session 會需要連結這些網站來產生預覽,您可以隨時關閉連結預覽功能。";
"modal_link_previews_button_title" = "啟用";
"vc_share_title" = "分享至 Session";
"vc_share_loading_message" = "準備附件中⋯";
"vc_share_sending_message" = "傳送中⋯";
"view_open_group_invitation_description" = "打開群組邀請";
"vc_conversation_settings_invite_button_title" = "新增成員";

View File

@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "屏蔽 %@";
/* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "从黑名单中移除";
/* The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT" = "已屏蔽 %@。";
/* The title of the 'user blocked' alert. */
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE" = "该用户已被加入黑名单";
/* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被屏蔽的用户将无法向您发起通话,或发送消息。";
/* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "在验证发给你的验证码之前,我们无法激活你的帐户.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "立即";
/* Description of how and why Session iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "认证来打开 Session。";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "认证失败";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */
@ -523,3 +529,5 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";

View File

@ -155,7 +155,7 @@ extension OpenGroupSuggestionGrid {
stackView.spacing = Values.smallSpacing
snContentView.addSubview(stackView)
stackView.center(.vertical, in: snContentView)
stackView.pin(.leading, to: .leading, of: snContentView, withInset: Values.smallSpacing)
stackView.pin(.leading, to: .leading, of: snContentView, withInset: 4)
snContentView.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: Values.smallSpacing).isActive = true
snContentView.pin(to: self, withInset: Cell.contentViewInset)
}

View File

@ -1,4 +1,5 @@
@objc(SNUserSelectionVC)
final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
private let navBarTitle: String
private let usersToExclude: Set<String>
@ -25,7 +26,8 @@ final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate
}()
// MARK: Lifecycle
@objc init(with title: String, excluding usersToExclude: Set<String>, completion: @escaping (Set<String>) -> Void) {
@objc(initWithTitle:excluding:completion:)
init(with title: String, excluding usersToExclude: Set<String>, completion: @escaping (Set<String>) -> Void) {
self.navBarTitle = title
self.usersToExclude = usersToExclude
self.completion = completion

View File

@ -21,7 +21,8 @@ public final class BackgroundPoller : NSObject {
}
when(resolved: promises).done { _ in
completionHandler(.newData)
}.catch { _ in
}.catch { error in
SNLog("Background poll failed due to error: \(error)")
completionHandler(.failed)
}
}
@ -37,19 +38,21 @@ public final class BackgroundPoller : NSObject {
}
private static func getMessages(for publicKey: String) -> Promise<Void> {
return SnodeAPI.getSwarm(for: publicKey).then2 { swarm -> Promise<Void> in
return SnodeAPI.getSwarm(for: publicKey).then(on: DispatchQueue.main) { swarm -> Promise<Void> in
guard let snode = swarm.randomElement() else { throw SnodeAPI.Error.generic }
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let promises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, isBackgroundPoll: true)
return job.execute()
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let promises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, isBackgroundPoll: true)
return job.execute()
}
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}
}
}

View File

@ -69,9 +69,6 @@ void VerifyRegistrationsForPrimaryStorage(OWSStorage *storage)
_dbReadPool = [[YapDatabaseConnectionPool alloc] initWithDatabase:self.database];
_dbReadWriteConnection = [self newDatabaseConnection];
_uiDatabaseConnection = [self newDatabaseConnection];
// Vacuum the database
[self.dbReadWriteConnection vacuum];
// Increase object cache limit. Default is 250.
_uiDatabaseConnection.objectCacheLimit = 500;

View File

@ -5,8 +5,10 @@ import SessionSnodeKit
public final class FileServerAPIV2 : NSObject {
// MARK: Settings
@objc public static let server = "http://88.99.175.227"
public static let serverPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
@objc public static let oldServer = "http://88.99.175.227"
public static let oldServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
@objc public static let server = "http://filev2.getsession.org"
public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
public static let maxFileSize = 10_000_000 // 10 MB
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
@ -57,7 +59,9 @@ public final class FileServerAPIV2 : NSObject {
}
// MARK: Convenience
private static func send(_ request: Request) -> Promise<JSON> {
private static func send(_ request: Request, useOldServer: Bool) -> Promise<JSON> {
let server = useOldServer ? oldServer : server
let serverPublicKey = useOldServer ? oldServerPublicKey : serverPublicKey
let tsRequest: TSRequest
switch request.verb {
case .get:
@ -84,27 +88,28 @@ public final class FileServerAPIV2 : NSObject {
// MARK: File Storage
@objc(upload:)
public static func objc_upload(file: Data) -> AnyPromise {
return AnyPromise.from(upload(file))
return AnyPromise.from(upload(file).map { String($0) })
}
public static func upload(_ file: Data) -> Promise<UInt64> {
let base64EncodedFile = file.base64EncodedString()
let parameters = [ "file" : base64EncodedFile ]
let request = Request(verb: .post, endpoint: "files", parameters: parameters)
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let fileID = json["result"] as? UInt64 else { throw Error.parsingFailed }
return fileID
}
}
@objc(download:)
public static func objc_download(file: UInt64) -> AnyPromise {
return AnyPromise.from(download(file))
@objc(download:useOldServer:)
public static func objc_download(file: String, useOldServer: Bool) -> AnyPromise {
guard let id = UInt64(file) else { return AnyPromise.from(Promise<Data>(error: Error.invalidURL)) }
return AnyPromise.from(download(id, useOldServer: useOldServer))
}
public static func download(_ file: UInt64) -> Promise<Data> {
public static func download(_ file: UInt64, useOldServer: Bool) -> Promise<Data> {
let request = Request(verb: .get, endpoint: "files/\(file)")
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed }
return file
}

View File

@ -97,7 +97,8 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
return handleFailure(Error.invalidURL)
}
FileServerAPIV2.download(file).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
let useOldServer = pointer.downloadURL.contains(FileServerAPIV2.oldServer)
FileServerAPIV2.download(file, useOldServer: useOldServer).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
}.catch(on: DispatchQueue.global()) { error in
handleFailure(error)

View File

@ -1,6 +1,6 @@
@objc(SNJob)
public protocol Job : class, NSCoding {
public protocol Job : NSCoding {
var delegate: JobDelegate? { get set }
var id: String? { get set }
var failureCount: UInt { get set }

View File

@ -80,11 +80,11 @@ public final class ConfigurationMessage : ControlMessage {
public override var description: String {
"""
ConfigurationMessage(
closedGroups: \([ClosedGroup](closedGroups).prettifiedDescription)
openGroups: \([String](openGroups).prettifiedDescription)
displayName: \(displayName ?? "null")
profilePictureURL: \(profilePictureURL ?? "null")
profileKey: \(profileKey?.toHexString() ?? "null")
closedGroups: \([ClosedGroup](closedGroups).prettifiedDescription),
openGroups: \([String](openGroups).prettifiedDescription),
displayName: \(displayName ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null"),
profileKey: \(profileKey?.toHexString() ?? "null"),
contacts: \([Contact](contacts).prettifiedDescription)
)
"""

View File

@ -78,7 +78,7 @@ public final class ExpirationTimerUpdate : ControlMessage {
public override var description: String {
"""
ExpirationTimerUpdate(
syncTarget: \(syncTarget ?? "null")
syncTarget: \(syncTarget ?? "null"),
duration: \(duration?.description ?? "null")
)
"""

View File

@ -20,7 +20,9 @@ public extension TSIncomingMessage {
quotedMessage: quotedMessage,
linkPreview: linkPreview,
serverTimestamp: visibleMessage.openGroupServerTimestamp as NSNumber?,
wasReceivedByUD: true
wasReceivedByUD: true,
openGroupInvitationName: visibleMessage.openGroupInvitation?.name,
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url
)
result.openGroupServerMessageID = openGroupServerMessageID
return result

View File

@ -58,7 +58,9 @@ NS_ASSUME_NONNULL_BEGIN
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
serverTimestamp:(nullable NSNumber *)serverTimestamp
wasReceivedByUD:(BOOL)wasReceivedByUD NS_DESIGNATED_INITIALIZER;
wasReceivedByUD:(BOOL)wasReceivedByUD
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

@ -53,6 +53,8 @@ NS_ASSUME_NONNULL_BEGIN
linkPreview:(nullable OWSLinkPreview *)linkPreview
serverTimestamp:(nullable NSNumber *)serverTimestamp
wasReceivedByUD:(BOOL)wasReceivedByUD
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
{
self = [super initMessageWithTimestamp:timestamp
inThread:thread
@ -61,7 +63,9 @@ NS_ASSUME_NONNULL_BEGIN
expiresInSeconds:expiresInSeconds
expireStartedAt:0
quotedMessage:quotedMessage
linkPreview:linkPreview];
linkPreview:linkPreview
openGroupInvitationName:openGroupInvitationName
openGroupInvitationURL:openGroupInvitationURL];
if (!self) {
return self;
@ -140,6 +144,17 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
BOOL areAllAttachmentsDownloaded = YES;
for (NSString *attachmentId in self.attachmentIds) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
areAllAttachmentsDownloaded = areAllAttachmentsDownloaded && attachment.isDownloaded;
if (!areAllAttachmentsDownloaded) break;
}
if (!areAllAttachmentsDownloaded) {
return;
}
_read = YES;
[self saveWithTransaction:transaction];

View File

@ -55,7 +55,9 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
expiresInSeconds:0
expireStartedAt:0
quotedMessage:nil
linkPreview:nil];
linkPreview:nil
openGroupInvitationName:nil
openGroupInvitationURL:nil];
if (!self) {
return self;

View File

@ -36,6 +36,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
@property (nonatomic) uint64_t openGroupServerMessageID;
@property (nonatomic, readonly) BOOL isOpenGroupMessage;
@property (nonatomic, readonly, nullable) NSString *openGroupInvitationName;
@property (nonatomic, readonly, nullable) NSString *openGroupInvitationURL;
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;
@ -46,7 +48,9 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
expiresInSeconds:(uint32_t)expiresInSeconds
expireStartedAt:(uint64_t)expireStartedAt
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview NS_DESIGNATED_INITIALIZER;
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

@ -63,6 +63,8 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
expireStartedAt:(uint64_t)expireStartedAt
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
{
self = [super initInteractionWithTimestamp:timestamp inThread:thread];
@ -80,6 +82,8 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
_quotedMessage = quotedMessage;
_linkPreview = linkPreview;
_openGroupServerMessageID = 0;
_openGroupInvitationName = openGroupInvitationName;
_openGroupInvitationURL = openGroupInvitationURL;
return self;
}
@ -342,6 +346,8 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
return bodyDescription;
} else if (attachmentDescription.length > 0) {
return attachmentDescription;
} else if (self.openGroupInvitationName != nil) {
return @"😎 Open group invitation";
} else {
// TODO: We should do better here.
return @"";

View File

@ -28,7 +28,9 @@ import SessionUtilitiesKit
isVoiceMessage: false,
groupMetaMessage: .unspecified,
quotedMessage: TSQuotedMessage.from(visibleMessage.quote),
linkPreview: OWSLinkPreview.from(visibleMessage.linkPreview)
linkPreview: OWSLinkPreview.from(visibleMessage.linkPreview),
openGroupInvitationName: visibleMessage.openGroupInvitation?.name,
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url
)
}
}

View File

@ -92,7 +92,9 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
isVoiceMessage:(BOOL)isVoiceMessage
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview NS_DESIGNATED_INITIALIZER;
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

@ -152,7 +152,9 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
isVoiceMessage:NO
groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:quotedMessage
linkPreview:linkPreview];
linkPreview:linkPreview
openGroupInvitationName:nil
openGroupInvitationURL:nil];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
@ -169,7 +171,9 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
isVoiceMessage:NO
groupMetaMessage:groupMetaMessage
quotedMessage:nil
linkPreview:nil];
linkPreview:nil
openGroupInvitationName:nil
openGroupInvitationURL:nil];
}
- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp
@ -182,6 +186,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
{
self = [super initMessageWithTimestamp:timestamp
inThread:thread
@ -190,7 +196,9 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
expiresInSeconds:expiresInSeconds
expireStartedAt:expireStartedAt
quotedMessage:quotedMessage
linkPreview:linkPreview];
linkPreview:linkPreview
openGroupInvitationName:openGroupInvitationName
openGroupInvitationURL:openGroupInvitationURL];
if (!self) {
return self;
}

View File

@ -61,8 +61,8 @@ public extension VisibleMessage {
public override var description: String {
"""
LinkPreview(
title: \(title ?? "null")
url: \(url ?? "null")
title: \(title ?? "null"),
url: \(url ?? "null"),
attachmentID: \(attachmentID ?? "null")
)
"""

View File

@ -0,0 +1,56 @@
import SessionUtilitiesKit
public extension VisibleMessage {
@objc(SNOpenGroupInvitation)
class OpenGroupInvitation : NSObject, NSCoding {
public var name: String?
public var url: String?
@objc
public init(name: String, url: String) {
self.name = name
self.url = url
}
public required init?(coder: NSCoder) {
if let name = coder.decodeObject(forKey: "name") as! String? { self.name = name }
if let url = coder.decodeObject(forKey: "url") as! String? { self.url = url }
}
public func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(url, forKey: "url")
}
public static func fromProto(_ proto: SNProtoDataMessageOpenGroupInvitation) -> OpenGroupInvitation? {
let url = proto.url
let name = proto.name
return OpenGroupInvitation(name: name, url: url)
}
public func toProto() -> SNProtoDataMessageOpenGroupInvitation? {
guard let url = url, let name = name else {
SNLog("Couldn't construct open group invitation proto from: \(self).")
return nil
}
let openGroupInvitationProto = SNProtoDataMessageOpenGroupInvitation.builder(url: url, name: name)
do {
return try openGroupInvitationProto.build()
} catch {
SNLog("Couldn't construct open group invitation proto from: \(self).")
return nil
}
}
// MARK: Description
public override var description: String {
"""
OpenGroupInvitation(
name: \(name ?? "null"),
url: \(url ?? "null")
)
"""
}
}
}

View File

@ -62,8 +62,8 @@ public extension VisibleMessage {
public override var description: String {
"""
Profile(
displayName: \(displayName ?? "null")
profileKey: \(profileKey?.description ?? "null")
displayName: \(displayName ?? "null"),
profileKey: \(profileKey?.description ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null")
)
"""

View File

@ -88,9 +88,9 @@ public extension VisibleMessage {
public override var description: String {
"""
Quote(
timestamp: \(timestamp?.description ?? "null")
publicKey: \(publicKey ?? "null")
text: \(text ?? "null")
timestamp: \(timestamp?.description ?? "null"),
publicKey: \(publicKey ?? "null"),
text: \(text ?? "null"),
attachmentID: \(attachmentID ?? "null")
)
"""

View File

@ -12,6 +12,7 @@ public final class VisibleMessage : Message {
@objc public var linkPreview: LinkPreview?
@objc public var contact: Contact?
@objc public var profile: Profile?
@objc public var openGroupInvitation: OpenGroupInvitation?
public override var isSelfSendValid: Bool { true }
@ -22,6 +23,7 @@ public final class VisibleMessage : Message {
public override var isValid: Bool {
guard super.isValid else { return false }
if !attachmentIDs.isEmpty { return true }
if openGroupInvitation != nil { return true }
if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true }
return false
}
@ -36,6 +38,7 @@ public final class VisibleMessage : Message {
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
// TODO: Contact
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile }
if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation }
}
public override func encode(with coder: NSCoder) {
@ -47,6 +50,7 @@ public final class VisibleMessage : Message {
coder.encode(linkPreview, forKey: "linkPreview")
// TODO: Contact
coder.encode(profile, forKey: "profile")
coder.encode(openGroupInvitation, forKey: "openGroupInvitation")
}
// MARK: Proto Conversion
@ -59,6 +63,8 @@ public final class VisibleMessage : Message {
if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
// TODO: Contact
if let profile = Profile.fromProto(dataMessage) { result.profile = profile }
if let openGroupInvitationProto = dataMessage.openGroupInvitation,
let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation }
result.syncTarget = dataMessage.syncTarget
return result
}
@ -95,6 +101,8 @@ public final class VisibleMessage : Message {
let attachmentProtos = attachments.compactMap { $0.buildProto() }
dataMessage.setAttachments(attachmentProtos)
// TODO: Contact
// Open group invitation
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
@ -128,12 +136,13 @@ public final class VisibleMessage : Message {
public override var description: String {
"""
VisibleMessage(
text: \(text ?? "null")
attachmentIDs: \(attachmentIDs)
quote: \(quote?.description ?? "null")
linkPreview: \(linkPreview?.description ?? "null")
contact: \(contact?.description ?? "null")
text: \(text ?? "null"),
attachmentIDs: \(attachmentIDs),
quote: \(quote?.description ?? "null"),
linkPreview: \(linkPreview?.description ?? "null"),
contact: \(contact?.description ?? "null"),
profile: \(profile?.description ?? "null")
"openGroupInvitation": \(openGroupInvitation?.description ?? "null")
)
"""
}

View File

@ -4,8 +4,8 @@ public final class OpenGroupV2 : NSObject, NSCoding { // NSObject/NSCoding confo
@objc public let server: String
@objc public let room: String
public let id: String
public let name: String
public let publicKey: String
@objc public let name: String
@objc public let publicKey: String
/// The ID with which the image can be retrieved from the server.
public let imageID: String?

View File

@ -1294,6 +1294,118 @@ extension SNProtoDataMessageLokiProfile.SNProtoDataMessageLokiProfileBuilder {
#endif
// MARK: - SNProtoDataMessageOpenGroupInvitation
@objc public class SNProtoDataMessageOpenGroupInvitation: NSObject {
// MARK: - SNProtoDataMessageOpenGroupInvitationBuilder
@objc public class func builder(url: String, name: String) -> SNProtoDataMessageOpenGroupInvitationBuilder {
return SNProtoDataMessageOpenGroupInvitationBuilder(url: url, name: name)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SNProtoDataMessageOpenGroupInvitationBuilder {
let builder = SNProtoDataMessageOpenGroupInvitationBuilder(url: url, name: name)
return builder
}
@objc public class SNProtoDataMessageOpenGroupInvitationBuilder: NSObject {
private var proto = SessionProtos_DataMessage.OpenGroupInvitation()
@objc fileprivate override init() {}
@objc fileprivate init(url: String, name: String) {
super.init()
setUrl(url)
setName(name)
}
@objc public func setUrl(_ valueParam: String) {
proto.url = valueParam
}
@objc public func setName(_ valueParam: String) {
proto.name = valueParam
}
@objc public func build() throws -> SNProtoDataMessageOpenGroupInvitation {
return try SNProtoDataMessageOpenGroupInvitation.parseProto(proto)
}
@objc public func buildSerializedData() throws -> Data {
return try SNProtoDataMessageOpenGroupInvitation.parseProto(proto).serializedData()
}
}
fileprivate let proto: SessionProtos_DataMessage.OpenGroupInvitation
@objc public let url: String
@objc public let name: String
private init(proto: SessionProtos_DataMessage.OpenGroupInvitation,
url: String,
name: String) {
self.proto = proto
self.url = url
self.name = name
}
@objc
public func serializedData() throws -> Data {
return try self.proto.serializedData()
}
@objc public class func parseData(_ serializedData: Data) throws -> SNProtoDataMessageOpenGroupInvitation {
let proto = try SessionProtos_DataMessage.OpenGroupInvitation(serializedData: serializedData)
return try parseProto(proto)
}
fileprivate class func parseProto(_ proto: SessionProtos_DataMessage.OpenGroupInvitation) throws -> SNProtoDataMessageOpenGroupInvitation {
guard proto.hasURL else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: url")
}
let url = proto.url
guard proto.hasName else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: name")
}
let name = proto.name
// MARK: - Begin Validation Logic for SNProtoDataMessageOpenGroupInvitation -
// MARK: - End Validation Logic for SNProtoDataMessageOpenGroupInvitation -
let result = SNProtoDataMessageOpenGroupInvitation(proto: proto,
url: url,
name: name)
return result
}
@objc public override var debugDescription: String {
return "\(proto)"
}
}
#if DEBUG
extension SNProtoDataMessageOpenGroupInvitation {
@objc public func serializedDataIgnoringErrors() -> Data? {
return try! self.serializedData()
}
}
extension SNProtoDataMessageOpenGroupInvitation.SNProtoDataMessageOpenGroupInvitationBuilder {
@objc public func buildIgnoringErrors() -> SNProtoDataMessageOpenGroupInvitation? {
return try! self.build()
}
}
#endif
// MARK: - SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper
@objc public class SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper: NSObject {
@ -1696,6 +1808,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
if let _value = profile {
builder.setProfile(_value)
}
if let _value = openGroupInvitation {
builder.setOpenGroupInvitation(_value)
}
if let _value = closedGroupControlMessage {
builder.setClosedGroupControlMessage(_value)
}
@ -1763,6 +1878,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
proto.profile = valueParam.proto
}
@objc public func setOpenGroupInvitation(_ valueParam: SNProtoDataMessageOpenGroupInvitation) {
proto.openGroupInvitation = valueParam.proto
}
@objc public func setClosedGroupControlMessage(_ valueParam: SNProtoDataMessageClosedGroupControlMessage) {
proto.closedGroupControlMessage = valueParam.proto
}
@ -1792,6 +1911,8 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
@objc public let profile: SNProtoDataMessageLokiProfile?
@objc public let openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?
@objc public let closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?
@objc public var body: String? {
@ -1851,6 +1972,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
quote: SNProtoDataMessageQuote?,
preview: [SNProtoDataMessagePreview],
profile: SNProtoDataMessageLokiProfile?,
openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?,
closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) {
self.proto = proto
self.attachments = attachments
@ -1858,6 +1980,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
self.quote = quote
self.preview = preview
self.profile = profile
self.openGroupInvitation = openGroupInvitation
self.closedGroupControlMessage = closedGroupControlMessage
}
@ -1893,6 +2016,11 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
profile = try SNProtoDataMessageLokiProfile.parseProto(proto.profile)
}
var openGroupInvitation: SNProtoDataMessageOpenGroupInvitation? = nil
if proto.hasOpenGroupInvitation {
openGroupInvitation = try SNProtoDataMessageOpenGroupInvitation.parseProto(proto.openGroupInvitation)
}
var closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage? = nil
if proto.hasClosedGroupControlMessage {
closedGroupControlMessage = try SNProtoDataMessageClosedGroupControlMessage.parseProto(proto.closedGroupControlMessage)
@ -1908,6 +2036,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
quote: quote,
preview: preview,
profile: profile,
openGroupInvitation: openGroupInvitation,
closedGroupControlMessage: closedGroupControlMessage)
return result
}

View File

@ -440,6 +440,15 @@ struct SessionProtos_DataMessage {
/// Clears the value of `profile`. Subsequent reads from it will return its default value.
mutating func clearProfile() {_uniqueStorage()._profile = nil}
var openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation {
get {return _storage._openGroupInvitation ?? SessionProtos_DataMessage.OpenGroupInvitation()}
set {_uniqueStorage()._openGroupInvitation = newValue}
}
/// Returns true if `openGroupInvitation` has been explicitly set.
var hasOpenGroupInvitation: Bool {return _storage._openGroupInvitation != nil}
/// Clears the value of `openGroupInvitation`. Subsequent reads from it will return its default value.
mutating func clearOpenGroupInvitation() {_uniqueStorage()._openGroupInvitation = nil}
var closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage {
get {return _storage._closedGroupControlMessage ?? SessionProtos_DataMessage.ClosedGroupControlMessage()}
set {_uniqueStorage()._closedGroupControlMessage = newValue}
@ -670,6 +679,39 @@ struct SessionProtos_DataMessage {
fileprivate var _profilePicture: String? = nil
}
struct OpenGroupInvitation {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var url: String {
get {return _url ?? String()}
set {_url = newValue}
}
/// Returns true if `url` has been explicitly set.
var hasURL: Bool {return self._url != nil}
/// Clears the value of `url`. Subsequent reads from it will return its default value.
mutating func clearURL() {self._url = nil}
/// @required
var name: String {
get {return _name ?? String()}
set {_name = newValue}
}
/// Returns true if `name` has been explicitly set.
var hasName: Bool {return self._name != nil}
/// Clears the value of `name`. Subsequent reads from it will return its default value.
mutating func clearName() {self._name = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _url: String? = nil
fileprivate var _name: String? = nil
}
struct ClosedGroupControlMessage {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@ -1633,6 +1675,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
8: .same(proto: "quote"),
10: .same(proto: "preview"),
101: .same(proto: "profile"),
102: .same(proto: "openGroupInvitation"),
104: .same(proto: "closedGroupControlMessage"),
105: .same(proto: "syncTarget"),
]
@ -1648,6 +1691,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
var _quote: SessionProtos_DataMessage.Quote? = nil
var _preview: [SessionProtos_DataMessage.Preview] = []
var _profile: SessionProtos_DataMessage.LokiProfile? = nil
var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil
var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil
var _syncTarget: String? = nil
@ -1666,6 +1710,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
_quote = source._quote
_preview = source._preview
_profile = source._profile
_openGroupInvitation = source._openGroupInvitation
_closedGroupControlMessage = source._closedGroupControlMessage
_syncTarget = source._syncTarget
}
@ -1684,6 +1729,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
if let v = _storage._group, !v.isInitialized {return false}
if let v = _storage._quote, !v.isInitialized {return false}
if !SwiftProtobuf.Internal.areAllInitialized(_storage._preview) {return false}
if let v = _storage._openGroupInvitation, !v.isInitialized {return false}
if let v = _storage._closedGroupControlMessage, !v.isInitialized {return false}
return true
}
@ -1704,6 +1750,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
case 8: try decoder.decodeSingularMessageField(value: &_storage._quote)
case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview)
case 101: try decoder.decodeSingularMessageField(value: &_storage._profile)
case 102: try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation)
case 104: try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage)
case 105: try decoder.decodeSingularStringField(value: &_storage._syncTarget)
default: break
@ -1744,6 +1791,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
if let v = _storage._profile {
try visitor.visitSingularMessageField(value: v, fieldNumber: 101)
}
if let v = _storage._openGroupInvitation {
try visitor.visitSingularMessageField(value: v, fieldNumber: 102)
}
if let v = _storage._closedGroupControlMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 104)
}
@ -1769,6 +1819,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
if _storage._quote != rhs_storage._quote {return false}
if _storage._preview != rhs_storage._preview {return false}
if _storage._profile != rhs_storage._profile {return false}
if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false}
if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false}
if _storage._syncTarget != rhs_storage._syncTarget {return false}
return true
@ -2058,6 +2109,47 @@ extension SessionProtos_DataMessage.LokiProfile: SwiftProtobuf.Message, SwiftPro
}
}
extension SessionProtos_DataMessage.OpenGroupInvitation: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".OpenGroupInvitation"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "url"),
3: .same(proto: "name"),
]
public var isInitialized: Bool {
if self._url == nil {return false}
if self._name == nil {return false}
return true
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularStringField(value: &self._url)
case 3: try decoder.decodeSingularStringField(value: &self._name)
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._url {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._name {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SessionProtos_DataMessage.OpenGroupInvitation, rhs: SessionProtos_DataMessage.OpenGroupInvitation) -> Bool {
if lhs._url != rhs._url {return false}
if lhs._name != rhs._name {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".ClosedGroupControlMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View File

@ -101,6 +101,13 @@ message DataMessage {
optional string profilePicture = 2;
}
message OpenGroupInvitation {
// @required
required string url = 1;
// @required
required string name = 3;
}
message ClosedGroupControlMessage {
enum Type {
@ -140,6 +147,7 @@ message DataMessage {
optional Quote quote = 8;
repeated Preview preview = 10;
optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
}

View File

@ -523,6 +523,7 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
// Ignore "invalid audio file" errors.
return 0.f;
}
[audioPlayer prepareToPlay];
if (!error) {
return (CGFloat)[audioPlayer duration];
} else {

View File

@ -400,6 +400,8 @@ extension MessageReceiver {
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: groupPublicKey, using: transaction)
// Store the formation timestamp
Storage.shared.setClosedGroupFormationTimestamp(to: messageSentTimestamp, for: groupPublicKey, using: transaction)
// Start polling
ClosedGroupPoller.shared.startPolling(for: groupPublicKey)
// Notify the PN server
let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey())
}
@ -531,6 +533,7 @@ extension MessageReceiver {
if wasCurrentUserRemoved {
Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction)
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
ClosedGroupPoller.shared.stopPolling(for: groupPublicKey)
let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
}
let storage = SNMessagingKitConfiguration.shared.storage

View File

@ -54,11 +54,6 @@ public enum MessageReceiver {
// Parse the envelope
let envelope = try SNProtoEnvelope.parseData(data)
let storage = SNMessagingKitConfiguration.shared.storage
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
// for this issue.
guard !Set(storage.getReceivedMessageTimestamps(using: transaction)).contains(envelope.timestamp) || isRetry else { throw Error.duplicateMessage }
storage.addReceivedMessageTimestamp(envelope.timestamp, using: transaction)
// Decrypt the contents
guard let ciphertext = envelope.content else { throw Error.noData }
var plaintext: Data!
@ -92,22 +87,22 @@ public enum MessageReceiver {
}
groupPublicKey = envelope.source
try decrypt()
// do {
// try decrypt()
// } catch {
// do {
// let now = Date()
// // Don't spam encryption key pair requests
// let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true
// if shouldRequestEncryptionKeyPair {
// try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction)
// lastEncryptionKeyPairRequest[groupPublicKey!] = now
// }
// }
// throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one)
// }
/*
do {
try decrypt()
} catch {
do {
let now = Date()
// Don't spam encryption key pair requests
let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true
if shouldRequestEncryptionKeyPair {
try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction)
lastEncryptionKeyPairRequest[groupPublicKey!] = now
}
}
throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one)
}
*/
default: throw Error.unknownEnvelopeType
}
}
@ -159,6 +154,19 @@ public enum MessageReceiver {
guard isValid else {
throw Error.invalidMessage
}
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
// for this issue.
if let message = message as? ClosedGroupControlMessage, case .new = message.kind {
// Allow duplicates in this case to avoid the following situation:
// The app performed a background poll or received a push notification
// This method was invoked and the received message timestamps table was updated
// Processing wasn't finished
// The user doesn't see the new closed group
} else {
guard !Set(storage.getReceivedMessageTimestamps(using: transaction)).contains(envelope.timestamp) || isRetry else { throw Error.duplicateMessage }
storage.addReceivedMessageTimestamp(envelope.timestamp, using: transaction)
}
// Return
return (message, proto)
} else {

View File

@ -43,6 +43,8 @@ extension MessageSender {
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupCreated)
infoMessage.save(with: transaction)
// Start polling
ClosedGroupPoller.shared.startPolling(for: groupPublicKey)
// Return
return when(fulfilled: promises).map2 { thread }
}
@ -272,6 +274,7 @@ extension MessageSender {
// Remove the group from the database and unsubscribe from PNs
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction)
ClosedGroupPoller.shared.stopPolling(for: groupPublicKey)
let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
}
}.map { _ in }

View File

@ -9,7 +9,6 @@ public final class MessageSender : NSObject {
public enum Error : LocalizedError {
case invalidMessage
case protoConversionFailed
case proofOfWorkCalculationFailed
case noUserX25519KeyPair
case noUserED25519KeyPair
case signingFailed
@ -22,7 +21,7 @@ public final class MessageSender : NSObject {
internal var isRetryable: Bool {
switch self {
case .invalidMessage, .protoConversionFailed, .proofOfWorkCalculationFailed, .invalidClosedGroupUpdate, .signingFailed, .encryptionFailed: return false
case .invalidMessage, .protoConversionFailed, .invalidClosedGroupUpdate, .signingFailed, .encryptionFailed: return false
default: return true
}
}
@ -31,7 +30,6 @@ public final class MessageSender : NSObject {
switch self {
case .invalidMessage: return "Invalid message."
case .protoConversionFailed: return "Couldn't convert message to proto."
case .proofOfWorkCalculationFailed: return "Proof of work calculation failed."
case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair."
case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair."
case .signingFailed: return "Couldn't sign message."
@ -48,8 +46,6 @@ public final class MessageSender : NSObject {
// MARK: Initialization
private override init() { }
public static let shared = MessageSender() // FIXME: Remove once requestSenderKey is static
// MARK: Preparation
public static func prep(_ signalAttachments: [SignalAttachment], for message: VisibleMessage, using transaction: YapDatabaseReadWriteTransaction) {
guard let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) else {

View File

@ -3,11 +3,12 @@ import PromiseKit
@objc(LKClosedGroupPoller)
public final class ClosedGroupPoller : NSObject {
private var isPolling = false
private var timer: Timer?
private var isPolling: [String:Bool] = [:]
private var timers: [String:Timer] = [:]
// MARK: Settings
private static let pollInterval: TimeInterval = 2
private static let minPollInterval: Double = 4
private static let maxPollInterval: Double = 2 * 60
// MARK: Error
private enum Error : LocalizedError {
@ -22,65 +23,117 @@ public final class ClosedGroupPoller : NSObject {
}
}
// MARK: Initialization
public static let shared = ClosedGroupPoller()
private override init() { }
// MARK: Public API
@objc public func startIfNeeded() {
@objc public func start() {
#if DEBUG
assert(Thread.current.isMainThread) // Timers don't do well on background queues
#endif
guard !isPolling else { return }
isPolling = true
timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in
let _ = self?.poll()
}
let storage = SNMessagingKitConfiguration.shared.storage
let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
allGroupPublicKeys.forEach { startPolling(for: $0) }
}
public func pollOnce() -> [Promise<Void>] {
guard !isPolling else { return [] }
isPolling = true
return poll()
public func startPolling(for groupPublicKey: String) {
guard !isPolling(for: groupPublicKey) else { return }
setUpPolling(for: groupPublicKey)
isPolling[groupPublicKey] = true
}
@objc public func stop() {
isPolling = false
timer?.invalidate()
let storage = SNMessagingKitConfiguration.shared.storage
let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
allGroupPublicKeys.forEach { stopPolling(for: $0) }
}
public func stopPolling(for groupPublicKey: String) {
timers[groupPublicKey]?.invalidate()
isPolling[groupPublicKey] = false
}
// MARK: Private API
private func poll() -> [Promise<Void>] {
guard isPolling else { return [] }
let publicKeys = Storage.shared.getUserClosedGroupPublicKeys()
return publicKeys.map { publicKey in
let promise = SnodeAPI.getSwarm(for: publicKey).then2 { [weak self] swarm -> Promise<[JSON]> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling else { return Promise(error: Error.pollingCanceled) }
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).map2 {
SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: publicKey)
}
private func setUpPolling(for groupPublicKey: String) {
poll(groupPublicKey).done2 { [weak self] _ in
DispatchQueue.main.async { // Timers don't do well on background queues
self?.pollRecursively(groupPublicKey)
}
promise.done2 { [weak self] messages in
guard let self = self, self.isPolling else { return }
if !messages.isEmpty {
SNLog("Received \(messages.count) new message(s) in closed group with public key: \(publicKey).")
}
messages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
} catch {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
}.catch2 { [weak self] error in
// The error is logged in poll(_:)
DispatchQueue.main.async { // Timers don't do well on background queues
self?.pollRecursively(groupPublicKey)
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).")
}
promise.retainUntilComplete()
return promise.map { _ in }
}
}
private func pollRecursively(_ groupPublicKey: String) {
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
guard isPolling(for: groupPublicKey),
let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID)) else { return }
// Get the received date of the last message in the thread. If we don't have any messages yet, pick some
// reasonable fake time interval to use instead.
let lastMessageDate =
(thread.numberOfInteractions() > 0) ? thread.lastInteraction.receivedAtDate() : Date().addingTimeInterval(-5 * 60)
let timeSinceLastMessage = Date().timeIntervalSince(lastMessageDate)
let minPollInterval = ClosedGroupPoller.minPollInterval
let limit: Double = 12 * 60 * 60
let a = (ClosedGroupPoller.maxPollInterval - minPollInterval) / limit
let nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
SNLog("Next poll interval for closed group with public key: \(groupPublicKey) is \(nextPollInterval) s.")
timers[groupPublicKey] = Timer.scheduledTimer(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in
timer.invalidate()
self?.poll(groupPublicKey).done2 { _ in
DispatchQueue.main.async { // Timers don't do well on background queues
self?.pollRecursively(groupPublicKey)
}
}.catch2 { error in
// The error is logged in poll(_:)
DispatchQueue.main.async { // Timers don't do well on background queues
self?.pollRecursively(groupPublicKey)
}
}
}
}
private func poll(_ groupPublicKey: String) -> Promise<Void> {
guard isPolling(for: groupPublicKey) else { return Promise.value(()) }
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<[JSON]> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling(for: groupPublicKey) else { return Promise(error: Error.pollingCanceled) }
return SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey).map2 {
SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
}
}
promise.done2 { [weak self] rawMessages in
guard let self = self, self.isPolling(for: groupPublicKey) else { return }
if !rawMessages.isEmpty {
SNLog("Received \(rawMessages.count) new message(s) in closed group with public key: \(groupPublicKey).")
}
rawMessages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
} catch {
SNLog("Failed to deserialize envelope due to error: \(error).")
}
}
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")
}
return promise.map { _ in }
}
// MARK: Convenience
private func isPolling(for groupPublicKey: String) -> Bool {
return isPolling[groupPublicKey] ?? false
}
}

View File

@ -9,7 +9,7 @@ public final class Poller : NSObject {
private var pollCount = 0
// MARK: Settings
private static let pollInterval: TimeInterval = 1
private static let pollInterval: TimeInterval = 1.5
private static let retryInterval: TimeInterval = 0.25
/// After polling a given snode this many times we always switch to a new one.
///

View File

@ -184,10 +184,12 @@ public final class SnodeAPI : NSObject {
}
let snodePoolPromises: [Promise<Set<Snode>>] = snodes.map { snode in
return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
// Don't specify a limit in the request. Service nodes return a shuffled
// list of nodes so if we specify a limit the 3 responses we get might have
// very little overlap.
let parameters: JSON = [
"endpoint" : "get_service_nodes",
"params" : [
"limit" : 256,
"active_only" : true,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
@ -212,9 +214,11 @@ public final class SnodeAPI : NSObject {
}
let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Snode> in
var result: Set<Snode> = results[0]
results.forEach { result = result.union($0) }
results.forEach { result = result.intersection($0) }
if result.count > 24 { // We want the snodes to agree on at least this many snodes
return result
// Limit the snode pool size to 256 so that we don't go too long without
// refreshing it
return (result.count > 256) ? Set([Snode](result)[0..<256]) : result
} else {
throw Error.inconsistentSnodePools
}

View File

@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
NSArray<NSString *> *allGroups = [unreadMessages allGroups];
for (NSString *groupID in allGroups) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:groupID transaction:transaction];
if (thread.isMuted) continue;
[unreadMessages enumerateKeysAndObjectsInGroup:groupID
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {

View File

@ -364,7 +364,8 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
AnyPromise *promise = [SNFileServerAPIV2 upload:encryptedAvatarData];
[promise.thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
[promise.thenOn(dispatch_get_main_queue(), ^(NSString *fileID) {
NSString *downloadURL = [NSString stringWithFormat:@"%@/files/%@", SNFileServerAPIV2.server, fileID];
[NSUserDefaults.standardUserDefaults setObject:[NSDate new] forKey:@"lastProfilePictureUpload"];
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
successBlock(downloadURL);
@ -792,8 +793,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
NSString *profilePictureURL = userProfile.avatarUrlPath;
uint64_t *file = (uint64_t)[[profilePictureURL lastPathComponent] intValue];
AnyPromise *promise = [SNFileServerAPIV2 download:file];
NSString *file = [profilePictureURL lastPathComponent];
BOOL useOldServer = [profilePictureURL containsString:SNFileServerAPIV2.oldServer];
AnyPromise *promise = [SNFileServerAPIV2 download:file useOldServer:useOldServer];
[promise.then(^(NSData *data) {
@synchronized(self.currentAvatarDownloads)