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

View File

@ -447,6 +447,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
// Scroll to the source of the reply // Scroll to the source of the reply
guard let indexPath = viewModel.ensureLoadWindowContainsQuotedReply(reply) else { return } guard let indexPath = viewModel.ensureLoadWindowContainsQuotedReply(reply) else { return }
messagesTableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: true) 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 default: break
} }
@ -552,6 +555,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
present(urlModal, animated: true, completion: nil) 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) { func handleReplyButtonTapped(for viewItem: ConversationViewItem) {
reply(viewItem) reply(viewItem)
} }

View File

@ -354,6 +354,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// the previous value when the keyboard is shown. // the previous value when the keyboard is shown.
self.messagesTableView.reloadData() self.messagesTableView.reloadData()
} }
self.markAllAsRead()
} }
if shouldAnimate { if shouldAnimate {
messagesTableView.performBatchUpdates(batchUpdates, completion: batchUpdatesCompletion) 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 bubbleView.backgroundColor = (direction == .incoming) ? Colors.receivedMessageBackground : Colors.sentMessageBackground
updateBubbleViewCorners() updateBubbleViewCorners()
// Content view // Content view
populateContentView(for: viewItem) populateContentView(for: viewItem, message: message)
// Date break // Date break
headerViewTopConstraint.constant = shouldInsetHeader ? Values.mediumSpacing : 1 headerViewTopConstraint.constant = shouldInsetHeader ? Values.mediumSpacing : 1
headerView.subviews.forEach { $0.removeFromSuperview() } headerView.subviews.forEach { $0.removeFromSuperview() }
@ -297,7 +297,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
dateBreakLabel.set(.height, to: dateBreakLabelSize.height) 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() } snContentView.subviews.forEach { $0.removeFromSuperview() }
func showMediaPlaceholder() { func showMediaPlaceholder() {
let mediaPlaceholderView = MediaPlaceholderView(viewItem: viewItem, textColor: bodyLabelTextColor) let mediaPlaceholderView = MediaPlaceholderView(viewItem: viewItem, textColor: bodyLabelTextColor)
@ -317,6 +317,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment) linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment)
snContentView.addSubview(linkPreviewView) snContentView.addSubview(linkPreviewView)
linkPreviewView.pin(to: snContentView) 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 { } else {
// Stack view // Stack view
let stackView = UIStackView(arrangedSubviews: []) let stackView = UIStackView(arrangedSubviews: [])

View File

@ -312,6 +312,20 @@ CGFloat kIconViewLength = 24;
actionBlock:^{ actionBlock:^{
[weakSelf showMediaGallery]; [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 [mainSection addItem:[OWSTableItem
itemWithCustomCellBlock:^{ itemWithCustomCellBlock:^{
@ -1089,6 +1103,31 @@ CGFloat kIconViewLength = 24;
UIPasteboard.generalPasteboard.string = ((TSContactThread *)self.thread).contactSessionID; 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 - (void)showMediaGallery
{ {
OWSLogDebug(@""); 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 { if OWSIdentityManager.shared().identityKeyPair() != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded() appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPollerIfNeeded() appDelegate.startClosedGroupPoller()
appDelegate.startOpenGroupPollersIfNeeded() appDelegate.startOpenGroupPollersIfNeeded()
// Do this only if we created a new Session ID, or if we already received the initial configuration message // Do this only if we created a new Session ID, or if we already received the initial configuration message
if UserDefaults.standard[.hasSyncedInitialConfiguration] { if UserDefaults.standard[.hasSyncedInitialConfiguration] {
@ -192,9 +192,16 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
} }
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) { @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() 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 } guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications) let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
@ -217,12 +224,26 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
switch rowChange.type { switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], 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) case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
default: break default: break
} }
} }
tableView.endUpdates() 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) emptyStateView.isHidden = (threadCount != 0)
} }

View File

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

View File

@ -48,7 +48,6 @@ static NSTimeInterval launchStartedAt;
@property (nonatomic) BOOL areVersionMigrationsComplete; @property (nonatomic) BOOL areVersionMigrationsComplete;
@property (nonatomic) BOOL didAppLaunchFail; @property (nonatomic) BOOL didAppLaunchFail;
@property (nonatomic) LKPoller *poller; @property (nonatomic) LKPoller *poller;
@property (nonatomic) LKClosedGroupPoller *closedGroupPoller;
@end @end
@ -413,7 +412,7 @@ static NSTimeInterval launchStartedAt;
[[SNSnodeAPI getSnodePool] retainUntilComplete]; [[SNSnodeAPI getSnodePool] retainUntilComplete];
[self startPollerIfNeeded]; [self startPollerIfNeeded];
[self startClosedGroupPollerIfNeeded]; [self startClosedGroupPoller];
[self startOpenGroupPollersIfNeeded]; [self startOpenGroupPollersIfNeeded];
if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
@ -563,7 +562,7 @@ static NSTimeInterval launchStartedAt;
[self.readReceiptManager setAreReadReceiptsEnabled:YES]; [self.readReceiptManager setAreReadReceiptsEnabled:YES];
[self startPollerIfNeeded]; [self startPollerIfNeeded];
[self startClosedGroupPollerIfNeeded]; [self startClosedGroupPoller];
[self startOpenGroupPollersIfNeeded]; [self startOpenGroupPollersIfNeeded];
} }
} }
@ -726,19 +725,6 @@ static NSTimeInterval launchStartedAt;
- (void)stopPoller { [self.poller stop]; } - (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 - (void)startOpenGroupPollersIfNeeded
{ {
[SNOpenGroupManagerV2.shared startPolling]; [SNOpenGroupManagerV2.shared startPolling];

View File

@ -31,4 +31,13 @@ extension AppDelegate {
} }
return promise 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?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blockieren?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Freigeben"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blockierte Benutzer werden dich nicht anrufen oder dir Nachrichten senden können.";
/* Label for generic done button. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Sofort"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentifizierung gescheitert"; "SCREEN_LOCK_UNLOCK_FAILED" = "Authentifizierung gescheitert";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */ /* 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."; "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. */ /* 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" = "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed"; "SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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}} */ /* 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."; "APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT" = "La versión %@ está disponible en la App Store.";
/* Title for the 'new app version available' alert. */ /* 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. */ /* Label for the 'update' button in the 'new app version available' alert. */
"APP_UPDATE_NAG_ALERT_UPDATE_BUTTON" = "Actualizar"; "APP_UPDATE_NAG_ALERT_UPDATE_BUTTON" = "Actualizar";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "¿Bloquear %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "¿Bloquear %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear"; "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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Los contactos bloqueados no podrán llamarte ni enviarte mensajes."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Los contactos bloqueados no podrán llamarte ni enviarte mensajes.";
/* Label for generic done button. */ /* Label for generic done button. */
@ -97,11 +101,11 @@
/* Alert body */ /* Alert body */
"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "No podrás enviar o recibir más mensajes en este grupo."; "CONFIRM_LEAVE_GROUP_DESCRIPTION" = "No podrás enviar o recibir más mensajes en este grupo.";
/* Alert title */ /* 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. */ /* Message for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Este paso no se puede deshacer."; "CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Este paso no se puede deshacer.";
/* Title for the 'conversation delete confirmation' alert. */ /* 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 */ /* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "Sin resultados"; "CONVERSATION_SEARCH_NO_RESULTS" = "Sin resultados";
/* keyboard toolbar label when exactly 1 message matches the search string */ /* keyboard toolbar label when exactly 1 message matches the search string */
@ -137,7 +141,7 @@
/* Title for the 'crop/scale image' dialog. */ /* Title for the 'crop/scale image' dialog. */
"CROP_SCALE_IMAGE_VIEW_TITLE" = "Editar foto"; "CROP_SCALE_IMAGE_VIEW_TITLE" = "Editar foto";
/* Subtitle shown while the app is updating its database. */ /* 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. */ /* Title shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_TITLE" = "Optimizando base de datos"; "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}}. */ /* 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. */ /* No comment provided by engineer. */
"GROUP_MEMBER_LEFT" = "%@ ha abandonado el grupo."; "GROUP_MEMBER_LEFT" = "%@ ha abandonado el grupo.";
/* No comment provided by engineer. */ /* 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. */ /* No comment provided by engineer. */
"GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. "; "GROUP_MEMBERS_REMOVED" = " %@ were removed from the group. ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
@ -191,7 +195,7 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"GROUP_YOU_LEFT" = "Has abandonado el grupo."; "GROUP_YOU_LEFT" = "Has abandonado el grupo.";
/* No comment provided by engineer. */ /* 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. */ /* 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."; "IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "No se pueden compartir más de %@ objetos.";
/* alert title */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Inmediato"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Fallo al identificarse"; "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 */ /* 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. */ /* Setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS" = "Enviar previsualizaciones"; "SETTINGS_LINK_PREVIEWS" = "Enviar previsualizaciones";
/* Footer for setting for enabling & disabling link previews. */ /* 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. */ /* Header for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_HEADER" = "Previsualizar enlaces"; "SETTINGS_LINK_PREVIEWS_HEADER" = "Previsualizar enlaces";
/* table section header */ /* table section header */
@ -446,8 +452,8 @@
"vc_path_title" = "Ruta"; "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_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_device_row_title" = "Tú";
"vc_path_guard_node_row_title" = "Entry Node"; "vc_path_guard_node_row_title" = "Nodo de entrada";
"vc_path_service_node_row_title" = "Service Node"; "vc_path_service_node_row_title" = "Nodo de Servicio";
"vc_path_destination_row_title" = "Destino"; "vc_path_destination_row_title" = "Destino";
"vc_path_learn_more_button_title" = "Saber Más"; "vc_path_learn_more_button_title" = "Saber Más";
"vc_create_private_chat_title" = "Nueva Session"; "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_qr_code_view_scan_qr_code_explanation" = "Escanea el código QR de una persona para comenzar una conversación con ella";
"vc_view_my_qr_code_explanation" = "Este es tu código QR. Otros usuarios pueden escanearlo para empezar una Session contigo."; "vc_view_my_qr_code_explanation" = "Este es tu código QR. Otros usuarios pueden escanearlo para empezar una Session contigo.";
// MARK: - Not Yet Translated // MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers."; "fast_mode_explanation" = "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Apple.";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background."; "fast_mode" = "Fast Mode";
"vc_pn_mode_title" = "Message Notifications"; "slow_mode_explanation" = "Session comprobará ocasionalmente si hay nuevos mensajes en segundo plano.";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode"; "slow_mode" = "Modo lento";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase"; "vc_pn_mode_title" = "Notificaciones de mensaje";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code."; "vc_notification_settings_notification_mode_title" = "Usar modo rápido";
"vc_enter_recovery_phrase_title" = "Recovery Phrase"; "vc_link_device_recovery_phrase_tab_title" = "Frase de recuperación";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up."; "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_public_key_text_field_hint" = "Enter Session ID or ONS name"; "vc_enter_recovery_phrase_title" = "Frase de recuperación";
"vc_home_title" = "Messages"; "vc_enter_recovery_phrase_explanation" = "Para vincular tú dispositivo, introduce la frase de recuperación que se le dio cuando se registró.";
"admin_group_leave_warning" = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."; "vc_enter_public_key_text_field_hint" = "Introduzca el ID de Session o el nombre ONS";
"vc_join_open_group_suggestions_title" = "Or join one of these..."; "vc_home_title" = "Mensajes";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend"; "admin_group_leave_warning" = "Debido a que eres el creador de este grupo, se eliminará para todos. Esto no se puede deshacer.";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session"; "vc_join_open_group_suggestions_title" = "O únase a uno de estos...";
"copied" = "Copied"; "vc_settings_invite_a_friend_button_title" = "Invita a un amigo";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID"; "vc_settings_help_us_translate_button_title" = "Ayúdanos a traducir Session";
"vc_conversation_input_prompt" = "Message"; "copied" = "Copiado";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel"; "vc_conversation_settings_copy_session_id_button_title" = "Copiar ID de Session";
"modal_download_attachment_title" = "Trust %@?"; "vc_conversation_input_prompt" = "Mensaje";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?"; "vc_conversation_voice_message_cancel_message" = "Desliza para cancelar";
"modal_download_button_title" = "Download"; "modal_download_attachment_title" = "¿Confiar en %@?";
"modal_open_url_title" = "Open URL?"; "modal_download_attachment_explanation" = "¿Estás seguro de que quieres descargar este archivo?";
"modal_open_url_explanation" = "Are you sure you want to open %@?"; "modal_download_button_title" = "Descargar";
"modal_open_url_button_title" = "Open"; "modal_open_url_title" = "¿Abrir URL?";
"modal_blocked_title" = "Unblock %@?"; "modal_open_url_explanation" = "¿Estás seguro de que quieres abrir %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?"; "modal_open_url_button_title" = "Abrir";
"modal_blocked_button_title" = "Unblock"; "modal_blocked_title" = "¿Desbloquear a %@?";
"modal_link_previews_title" = "Enable Link Previews?"; "modal_blocked_explanation" = "¿Estás seguro de que quieres desbloquear a %@?";
"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_blocked_button_title" = "Desbloquear";
"modal_link_previews_button_title" = "Enable"; "modal_link_previews_title" = "¿Habilitar Previsualizaciones de Enlace?";
"vc_share_title" = "Share to Session"; "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.";
"vc_share_loading_message" = "Preparing attachments..."; "modal_link_previews_button_title" = "Activar";
"vc_share_sending_message" = "Sending..."; "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" = "%@ مسدود شود؟"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ مسدود شود؟";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "کاربری که مسدود شده است، امکان تماس یا ارسال پیام به شما را ندارد."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "کاربری که مسدود شده است، امکان تماس یا ارسال پیام به شما را ندارد.";
/* Label for generic done button. */ /* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "لطفا برای فعال شدن حساب خود، کدی که به شما فرستاده‌ایم را تائید کنید."; "REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "لطفا برای فعال شدن حساب خود، کدی که به شما فرستاده‌ایم را تائید کنید.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "لحظه"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "احراز هویت ناموفق بود"; "SCREEN_LOCK_UNLOCK_FAILED" = "احراز هویت ناموفق بود";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "اشتراک گذاری با Session";
"vc_share_loading_message" = "آماده سازی پیوست‌ها..."; "vc_share_loading_message" = "آماده سازی پیوست‌ها...";
"vc_share_sending_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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquer %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Débloquer"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Les utilisateurs bloqués ne pourront ni vous appeler ni vous envoyer des messages.";
/* Label for generic done button. */ /* 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é."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instantané"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Échec dauthentification"; "SCREEN_LOCK_UNLOCK_FAILED" = "Échec dauthentification";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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 */ /* 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. */ /* 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. */ /* 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}}. */ /* Format string for the default 'Note' sound. Embeds the system {{sound name}}. */
"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (par défaut)"; "SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT" = "%@ (par défaut)";
/* Label for the backup view in app settings. */ /* Label for the backup view in app settings. */
@ -359,7 +365,7 @@
/* Label for the 'typing indicators' setting. */ /* Label for the 'typing indicators' setting. */
"SETTINGS_TYPING_INDICATORS" = "Indicateurs de saisie"; "SETTINGS_TYPING_INDICATORS" = "Indicateurs de saisie";
/* Label for the 'no sound' option that allows users to disable sounds for notifications, etc. */ /* 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 */ /* {{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"; "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 */ /* 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_qr_code_view_scan_qr_code_explanation" = "Scannez le code QR d'un autre utilisateur pour démarrer une session";
"vc_view_my_qr_code_explanation" = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous."; "vc_view_my_qr_code_explanation" = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.";
// MARK: - Not Yet Translated // MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers."; "fast_mode_explanation" = "Vous serez notifiés de nouveaux messages de manière certaine et immédiate en utilisant les serveurs de notification dApple.";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background."; "fast_mode" = "Mode rapide";
"vc_pn_mode_title" = "Message Notifications"; "slow_mode_explanation" = "Session vérifiera occasionnellement la présence de nouveaux message en tâche de fond.";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode"; "slow_mode" = "Mode lent";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase"; "vc_pn_mode_title" = "Notifications de message";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code."; "vc_notification_settings_notification_mode_title" = "Utiliser le mode rapide";
"vc_enter_recovery_phrase_title" = "Recovery Phrase"; "vc_link_device_recovery_phrase_tab_title" = "Phrase de récupération";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up."; "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_public_key_text_field_hint" = "Enter Session ID or ONS name"; "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"; "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."; "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" = "Or join one of these..."; "vc_join_open_group_suggestions_title" = "Ou rejoignez un de ceux-ci...";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend"; "vc_settings_invite_a_friend_button_title" = "Inviter un ami";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session"; "vc_settings_help_us_translate_button_title" = "Aidez-nous à traduire Session";
"copied" = "Copied"; "copied" = "Copié";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID"; "vc_conversation_settings_copy_session_id_button_title" = "Copier lID Session";
"vc_conversation_input_prompt" = "Message"; "vc_conversation_input_prompt" = "Message";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel"; "vc_conversation_voice_message_cancel_message" = "Glisser pour annuler";
"modal_download_attachment_title" = "Trust %@?"; "modal_download_attachment_title" = "Faire confiance à %@?";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?"; "modal_download_attachment_explanation" = "Êtes-vous sûr de vouloir télécharger la sélection de %@ ?";
"modal_download_button_title" = "Download"; "modal_download_button_title" = "Télécharger";
"modal_open_url_title" = "Open URL?"; "modal_open_url_title" = "Ouvrir l'URL?";
"modal_open_url_explanation" = "Are you sure you want to open %@?"; "modal_open_url_explanation" = "Êtes-vous sûr de vouloir ouvrir %@?";
"modal_open_url_button_title" = "Open"; "modal_open_url_button_title" = "Ouvrir";
"modal_blocked_title" = "Unblock %@?"; "modal_blocked_title" = "Débloquer %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?"; "modal_blocked_explanation" = "Confirmez-vous le déblocage de %@ ?";
"modal_blocked_button_title" = "Unblock"; "modal_blocked_button_title" = "Débloquer";
"modal_link_previews_title" = "Enable Link Previews?"; "modal_link_previews_title" = "Activer les aperçus de lien?";
"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_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" = "Enable"; "modal_link_previews_button_title" = "Activer";
"vc_share_title" = "Share to Session"; "vc_share_title" = "Partager en Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Préparation des pièces jointes ...";
"vc_share_sending_message" = "Sending..."; "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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokir %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Buka blokir"; "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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Pengguna terblokir tidak bisa menghubungi atau mengirimkan pesan kepada Anda."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Pengguna terblokir tidak bisa menghubungi atau mengirimkan pesan kepada Anda.";
/* Label for generic done button. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instan"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Autentikasi gagal."; "SCREEN_LOCK_UNLOCK_FAILED" = "Autentikasi gagal.";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloccare %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Sblocca"; "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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Gli utenti bloccati non potranno chiamarti o inviarti messaggi."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Gli utenti bloccati non potranno chiamarti o inviarti messaggi.";
/* Label for generic done button. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Immediato"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Autenticazione fallita"; "SCREEN_LOCK_UNLOCK_FAILED" = "Autenticazione fallita";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ をブロックしますか?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "ブロックされたユーザは、あなたにメッセージや通話を発信することができなくなります。"; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "ブロックされたユーザは、あなたにメッセージや通話を発信することができなくなります。";
/* Label for generic done button. */ /* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "アカウントを有効化するには,お送りしたコードの確認を行ってください。"; "REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "アカウントを有効化するには,お送りしたコードの確認を行ってください。";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "即座"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "認証失敗"; "SCREEN_LOCK_UNLOCK_FAILED" = "認証失敗";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 */ /* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Document kiezen mislukt."; "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Document kiezen mislukt.";
/* Alert body when picking a document fails because user picked a directory/bundle */ /* 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 */ /* Alert title when picking a document fails because user picked a directory/bundle */
"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE" = "Niet-ondersteund bestand"; "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 */ /* 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. */ /* Indicates that the backup export is being configured. */
"BACKUP_EXPORT_PHASE_CONFIGURATION" = "Backup initialiseren"; "BACKUP_EXPORT_PHASE_CONFIGURATION" = "Backup initialiseren";
/* Indicates that the database data is being exported. */ /* 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. */ /* Indicates that the backup export data is being exported. */
"BACKUP_EXPORT_PHASE_EXPORT" = "Backup exporteren"; "BACKUP_EXPORT_PHASE_EXPORT" = "Backup exporteren";
/* Indicates that the backup export data is being uploaded. */ /* Indicates that the backup export data is being uploaded. */
@ -53,7 +53,7 @@
/* Indicates that the backup import is being configured. */ /* Indicates that the backup import is being configured. */
"BACKUP_IMPORT_PHASE_CONFIGURATION" = "Backup configuratie"; "BACKUP_IMPORT_PHASE_CONFIGURATION" = "Backup configuratie";
/* Indicates that the backup import data is being downloaded. */ /* 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. */ /* Indicates that the backup import data is being finalized. */
"BACKUP_IMPORT_PHASE_FINALIZING" = "Backup afronden"; "BACKUP_IMPORT_PHASE_FINALIZING" = "Backup afronden";
/* Indicates that the backup import data is being imported. */ /* Indicates that the backup import data is being imported. */
@ -61,7 +61,7 @@
/* Indicates that the backup database is being restored. */ /* Indicates that the backup database is being restored. */
"BACKUP_IMPORT_PHASE_RESTORING_DATABASE" = "Database Herstellen"; "BACKUP_IMPORT_PHASE_RESTORING_DATABASE" = "Database Herstellen";
/* Indicates that the backup import data is being restored. */ /* 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. */ /* Label for the backup restore decision section. */
"BACKUP_RESTORE_DECISION_TITLE" = "Backup beschikbaar"; "BACKUP_RESTORE_DECISION_TITLE" = "Backup beschikbaar";
/* Label for the backup restore description. */ /* Label for the backup restore description. */
@ -78,6 +78,10 @@
"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blokkeren?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "%@ blokkeren?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Deblokkeren"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Geblokkeerde gebruikers zijn niet in staat om u te bellen of berichten te sturen.";
/* Label for generic done button. */ /* Label for generic done button. */
@ -93,435 +97,438 @@
/* Error indicating that user does not have an iCloud account. */ /* 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."; "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. */ /* 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 */ /* 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 */ /* 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. */ /* 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. */ /* 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 */ /* 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 */ /* 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}} */ /* 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 */ /* title for conversation settings screen */
"CONVERSATION_SETTINGS" = "Conversation Settings"; "CONVERSATION_SETTINGS" = "Conversatie instellingen";
/* table cell label in conversation settings */ /* 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. */ /* 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 */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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}}. */ /* 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 */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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}}. */ /* 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}}. */ /* 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. */ /* The present; the current time. */
"DATE_NOW" = "Now"; "DATE_NOW" = "Nu";
/* The current day. */ /* The current day. */
"DATE_TODAY" = "Today"; "DATE_TODAY" = "Vandaag";
/* The day before today. */ /* The day before today. */
"DATE_YESTERDAY" = "Yesterday"; "DATE_YESTERDAY" = "Gisteren";
/* table cell label in conversation settings */ /* 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. */ /* 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 */ /* table cell label in conversation settings */
"EDIT_GROUP_ACTION" = "Edit Group"; "EDIT_GROUP_ACTION" = "Groep bewerken";
/* Label indicating media gallery is empty */ /* 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 */ /* 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 */ /* 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. */ /* 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 */ /* 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 */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* No comment provided by engineer. */
"GROUP_CREATED" = "Group created"; "GROUP_CREATED" = "Groep aangemaakt";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"GROUP_MEMBER_JOINED" = " %@ joined the group. "; "GROUP_MEMBER_JOINED" = " %@ is toegevoegd aan de groep. ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"GROUP_MEMBER_LEFT" = " %@ left the group. "; "GROUP_MEMBER_LEFT" = " %@ heeft de groep verlaten. ";
/* No comment provided by engineer. */ /* 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. */ /* 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. */ /* No comment provided by engineer. */
"GROUP_TITLE_CHANGED" = "Title is now '%@'. "; "GROUP_TITLE_CHANGED" = "Titel is nu '%@'. ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"GROUP_UPDATED" = "Group updated."; "GROUP_UPDATED" = "Groep bijgewerkt.";
/* No comment provided by engineer. */ /* 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. */ /* 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. */ /* 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 */ /* 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. */ /* 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 */ /* 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 */ /* Confirmation button within contextual alert */
"LEAVE_BUTTON_TITLE" = "Leave"; "LEAVE_BUTTON_TITLE" = "Verlaten";
/* table cell label in conversation settings */ /* table cell label in conversation settings */
"LEAVE_GROUP_ACTION" = "Leave Group"; "LEAVE_GROUP_ACTION" = "Verlaat groep";
/* Title for the 'long text message' view. */ /* Title for the 'long text message' view. */
"LONG_TEXT_VIEW_TITLE" = "Message"; "LONG_TEXT_VIEW_TITLE" = "Bericht";
/* nav bar button item */ /* 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 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}} */ /* 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 */ /* 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' */ /* 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}}. */ /* Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}. */
"MEDIA_GALLERY_MORE_ITEMS_FORMAT" = "+%@"; "MEDIA_GALLERY_MORE_ITEMS_FORMAT" = "+%@";
/* Short sender label for media sent by you */ /* 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 */ /* 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 for message delivered to their recipient. */
"MESSAGE_STATUS_DELIVERED" = "Delivered"; "MESSAGE_STATUS_DELIVERED" = "Afgeleverd";
/* status message for failed messages */ /* status message for failed messages */
"MESSAGE_STATUS_FAILED" = "Sending failed."; "MESSAGE_STATUS_FAILED" = "Verzenden mislukt.";
/* status message for failed messages */ /* status message for failed messages */
"MESSAGE_STATUS_FAILED_SHORT" = "Failed"; "MESSAGE_STATUS_FAILED_SHORT" = "Mislukt";
/* status message for read messages */ /* 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 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 while message is sending. */
"MESSAGE_STATUS_SENDING" = "Sending…"; "MESSAGE_STATUS_SENDING" = "Verzenden…";
/* status message for sent messages */ /* status message for sent messages */
"MESSAGE_STATUS_SENT" = "Sent"; "MESSAGE_STATUS_SENT" = "Verzonden";
/* status message while attachment is uploading */ /* status message while attachment is uploading */
"MESSAGE_STATUS_UPLOADING" = "Uploading…"; "MESSAGE_STATUS_UPLOADING" = "Uploaden…";
/* Alert body when user has previously denied media library access */ /* 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 */ /* 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. */ /* 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}} */ /* 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. */ /* 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') */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_AND_MESSAGE" = "Name and Content"; "NOTIFICATIONS_SENDER_AND_MESSAGE" = "Naam en inhoud";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"NOTIFICATIONS_SENDER_ONLY" = "Name Only"; "NOTIFICATIONS_SENDER_ONLY" = "Alleen naam";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"NOTIFICATIONS_NONE" = "No Name or Content"; "NOTIFICATIONS_NONE" = "Geen naam of inhoud";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Show"; "NOTIFICATIONS_SHOW" = "Tonen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"OK" = "OK"; "OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */ /* 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. */ /* 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 */ /* 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 */ /* alert title */
"PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE" = "Unable to capture image."; "PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE" = "Kan afbeelding niet vastleggen.";
/* alert title */ /* 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. */ /* 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 */ /* 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. */ /* 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. */ /* 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 */ /* 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 */ /* 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. */ /* 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. */ /* 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}}. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* Label for iCloud status row in the in the backup settings view. */
"SETTINGS_BACKUP_ICLOUD_STATUS" = "iCloud Status"; "SETTINGS_BACKUP_ICLOUD_STATUS" = "iCloud Status";
/* Indicates that the last backup restore failed. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* Label for backup status row in the in the backup settings view. */
"SETTINGS_BACKUP_STATUS" = "Status"; "SETTINGS_BACKUP_STATUS" = "Status";
/* Indicates that the last backup failed. */ /* 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. */ /* 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. */ /* 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. */ /* Indicates that the last backup succeeded. */
"SETTINGS_BACKUP_STATUS_SUCCEEDED" = "Backup Successful"; "SETTINGS_BACKUP_STATUS_SUCCEEDED" = "Back-up succesvol";
/* No comment provided by engineer. */ /* 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. */ /* 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 */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* Header for setting for enabling & disabling link previews. */
"SETTINGS_LINK_PREVIEWS_HEADER" = "Link Previews"; "SETTINGS_LINK_PREVIEWS_HEADER" = "Link voorbeelden";
/* table section header */ /* table section header */
"SETTINGS_NOTIFICATION_CONTENT_TITLE" = "Notification Content"; "SETTINGS_NOTIFICATION_CONTENT_TITLE" = "Notificatie Inhoud";
/* Label for the 'read receipts' setting. */ /* Label for the 'read receipts' setting. */
"SETTINGS_READ_RECEIPT" = "Read Receipts"; "SETTINGS_READ_RECEIPT" = "Leesbevestigingen";
/* An explanation of the 'read receipts' setting. */ /* 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. */ /* 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. */ /* 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. */ /* 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. */ /* Header Label for the sounds section of settings views. */
"SETTINGS_SECTION_SOUNDS" = "Sounds"; "SETTINGS_SECTION_SOUNDS" = "Geluiden";
/* Section header */ /* Section header */
"SETTINGS_SECURITY_TITLE" = "Screen Security"; "SETTINGS_SECURITY_TITLE" = "Scherm beveiliging";
/* Label for the 'typing indicators' setting. */ /* 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. */ /* 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 */ /* {{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 */ /* 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"; "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 */ /* {{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 */ /* 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"; "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 */ /* {{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 */ /* 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"; "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 */ /* {{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 */ /* 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"; "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 */ /* {{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 */ /* {{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 */ /* {{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 */ /* {{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"; "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 */ /* {{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 */ /* 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"; "TIME_AMOUNT_WEEKS_SHORT_FORMAT" = "%@w";
/* Label for the cancel button in an alert or action sheet. */ /* Label for the cancel button in an alert or action sheet. */
"TXT_CANCEL_TITLE" = "Cancel"; "TXT_CANCEL_TITLE" = "Annuleren";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"TXT_DELETE_TITLE" = "Delete"; "TXT_DELETE_TITLE" = "Wissen";
/* Filename for voice messages. */ /* 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. */ /* 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. */ /* 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 */ /* 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. */ /* 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 // MARK: - Session
"continue_2" = "Continue"; "continue_2" = "Doorgaan";
"copy" = "Copy"; "copy" = "Kopieer";
"invalid_url" = "Invalid URL"; "invalid_url" = "Ongeldige URL";
"next" = "Next"; "next" = "Volgende";
"share" = "Share"; "share" = "Delen";
"invalid_session_id" = "Invalid Session ID"; "invalid_session_id" = "Verkeerde Session ID";
"cancel" = "Cancel"; "cancel" = "Annuleren";
"your_session_id" = "Your Session ID"; "your_session_id" = "Uw Session-ID";
"vc_landing_title_2" = "Your Session begins here..."; "vc_landing_title_2" = "Uw Sessie begint hier...";
"vc_landing_register_button_title" = "Create Session ID"; "vc_landing_register_button_title" = "Session-ID aanmaken";
"vc_landing_restore_button_title" = "Continue Your Session"; "vc_landing_restore_button_title" = "Doorgaan met je sessie";
"vc_landing_link_button_title" = "Link a Device"; "vc_landing_link_button_title" = "Koppel een apparaat";
"view_fake_chat_bubble_1" = "What's Session?"; "view_fake_chat_bubble_1" = "Wat is Session?";
"view_fake_chat_bubble_2" = "It's a decentralized, encrypted messaging app"; "view_fake_chat_bubble_2" = "Het is een gedecentraliseerde, versleutelde berichten-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_3" = "Dus het verzamelt niet mijn persoonlijke informatie of de metagegevens van mijn gesprek? Hoe werkt het?";
"view_fake_chat_bubble_4" = "Using a combination of advanced anonymous routing and end-to-end encryption technologies."; "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" = "Friends don't let friends use compromised messengers. You're welcome."; "view_fake_chat_bubble_5" = "Vrienden laten vrienden geen gecompromitteerde berichten apps gebruiken. Graag gedaan.";
"vc_register_title" = "Say hello to your Session ID"; "vc_register_title" = "Zeg hallo tegen uw Session-ID";
"vc_register_explanation" = "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design."; "vc_register_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" = "Restore your account"; "vc_restore_title" = "Account herstellen";
"vc_restore_explanation" = "Enter the recovery phrase that was given to you when you signed up to restore your account."; "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" = "Enter your recovery phrase"; "vc_restore_seed_text_field_hint" = "Voer uw herstel zin in";
"vc_link_device_title" = "Link a Device"; "vc_link_device_title" = "Koppel een apparaat";
"vc_link_device_scan_qr_code_tab_title" = "Scan QR Code"; "vc_link_device_scan_qr_code_tab_title" = "Scan QR-code";
"vc_display_name_title_2" = "Pick your display name"; "vc_display_name_title_2" = "Kies je weergavenaam";
"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_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" = "Enter a display name"; "vc_display_name_text_field_hint" = "Kies een weergavenaam";
"vc_display_name_display_name_missing_error" = "Please pick a display name"; "vc_display_name_display_name_missing_error" = "Voer a. u. b. een weergave naam in";
"vc_display_name_display_name_too_long_error" = "Please pick a shorter display name"; "vc_display_name_display_name_too_long_error" = "Kies een kortere weergavenaam";
"vc_pn_mode_recommended_option_tag" = "Recommended"; "vc_pn_mode_recommended_option_tag" = "Aanbevolen";
"vc_pn_mode_no_option_picked_modal_title" = "Please Pick an Option"; "vc_pn_mode_no_option_picked_modal_title" = "Gelieve een optie selecteren";
"vc_home_empty_state_message" = "You don't have any contacts yet"; "vc_home_empty_state_message" = "U heeft nog geen contactpersonen";
"vc_home_empty_state_button_title" = "Start a Session"; "vc_home_empty_state_button_title" = "Sessie starten";
"vc_seed_title" = "Your Recovery Phrase"; "vc_seed_title" = "Uw Herstel Zin";
"vc_seed_title_2" = "Meet your recovery phrase"; "vc_seed_title_2" = "Maak kennis met uw herstel zin";
"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_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" = "Hold to reveal"; "vc_seed_reveal_button_title" = "Ingedrukt houden om te onthullen";
"view_seed_reminder_subtitle_1" = "Secure your account by saving your recovery phrase"; "view_seed_reminder_subtitle_1" = "Beveilig je account door je herstel zin op te slaan";
"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_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" = "Make sure to store your recovery phrase in a safe place"; "view_seed_reminder_subtitle_3" = "Zorg ervoor dat u uw herstel zin op een veilige plek opslaat";
"vc_path_title" = "Path"; "vc_path_title" = "Locatie";
"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_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" = "You"; "vc_path_device_row_title" = "U";
"vc_path_guard_node_row_title" = "Entry Node"; "vc_path_guard_node_row_title" = "Invoer node";
"vc_path_service_node_row_title" = "Service Node"; "vc_path_service_node_row_title" = "Service node";
"vc_path_destination_row_title" = "Destination"; "vc_path_destination_row_title" = "Bestemming";
"vc_path_learn_more_button_title" = "Learn More"; "vc_path_learn_more_button_title" = "Kom meer te weten";
"vc_create_private_chat_title" = "New Session"; "vc_create_private_chat_title" = "Nieuwe sessie";
"vc_create_private_chat_enter_session_id_tab_title" = "Enter Session ID"; "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_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_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" = "Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code."; "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 needs camera access to scan QR codes"; "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" = "Grant Camera Access"; "vc_scan_qr_code_grant_camera_access_button_title" = "Toegang tot camera verlenen";
"vc_create_closed_group_title" = "New Closed Group"; "vc_create_closed_group_title" = "Nieuwe Gesloten Groep";
"vc_create_closed_group_text_field_hint" = "Enter a group name"; "vc_create_closed_group_text_field_hint" = "Vul een groepsnaam in";
"vc_create_closed_group_empty_state_message" = "You don't have any contacts yet"; "vc_create_closed_group_empty_state_message" = "U heeft nog geen contactpersonen";
"vc_create_closed_group_empty_state_button_title" = "Start a Session"; "vc_create_closed_group_empty_state_button_title" = "Sessie starten";
"vc_create_closed_group_group_name_missing_error" = "Please enter a group name"; "vc_create_closed_group_group_name_missing_error" = "Vul een groepsnaam in";
"vc_create_closed_group_group_name_too_long_error" = "Please enter a shorter group name"; "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" = "A closed group cannot have more than 100 members"; "vc_create_closed_group_too_many_group_members_error" = "Een gesloten groep kan niet meer dan 100 leden hebben";
"vc_join_public_chat_title" = "Join Open Group"; "vc_join_public_chat_title" = "Deelnemen aan Open groep";
"vc_join_public_chat_enter_group_url_tab_title" = "Open Group URL"; "vc_join_public_chat_enter_group_url_tab_title" = "Open Groep-URL openen";
"vc_join_public_chat_scan_qr_code_tab_title" = "Scan QR Code"; "vc_join_public_chat_scan_qr_code_tab_title" = "QR-code scannen";
"vc_join_public_chat_scan_qr_code_explanation" = "Scan the QR code of the open group you'd like to join"; "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" = "Enter an open group URL"; "vc_enter_chat_url_text_field_hint" = "Voer een open groep URL in";
"vc_settings_title" = "Settings"; "vc_settings_title" = "Instellingen";
"vc_settings_display_name_text_field_hint" = "Enter a display name"; "vc_settings_display_name_text_field_hint" = "Kies een weergavenaam";
"vc_settings_display_name_missing_error" = "Please pick a display name"; "vc_settings_display_name_missing_error" = "Voer a. u. b. een weergave naam in";
"vc_settings_display_name_too_long_error" = "Please pick a shorter display name"; "vc_settings_display_name_too_long_error" = "Kies een kortere weergavenaam";
"vc_settings_privacy_button_title" = "Privacy"; "vc_settings_privacy_button_title" = "Privacy";
"vc_settings_notifications_button_title" = "Notifications"; "vc_settings_notifications_button_title" = "Meldingen";
"vc_settings_recovery_phrase_button_title" = "Recovery Phrase"; "vc_settings_recovery_phrase_button_title" = "Herstel zin";
"vc_settings_clear_all_data_button_title" = "Clear Data"; "vc_settings_clear_all_data_button_title" = "Gegevens wissen";
"vc_notification_settings_title" = "Notifications"; "vc_notification_settings_title" = "Meldingen";
"vc_privacy_settings_title" = "Privacy"; "vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Notification Strategy"; "preferences_notifications_strategy_category_title" = "Notificatie Inhoud";
"modal_seed_title" = "Your Recovery Phrase"; "modal_seed_title" = "Uw Herstel Zin";
"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device."; "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" = "Clear All Data"; "modal_clear_all_data_title" = "Wis alle gegevens";
"modal_clear_all_data_explanation" = "This will permanently delete your messages, sessions, and contacts."; "modal_clear_all_data_explanation" = "Hiermee worden uw berichten, sessies en contacten permanent verwijderd.";
"vc_qr_code_title" = "QR Code"; "vc_qr_code_title" = "QR-code";
"vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code"; "vc_qr_code_view_my_qr_code_tab_title" = "Bekijk mijn QR-code";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code"; "vc_qr_code_view_scan_qr_code_tab_title" = "QR-code scannen";
"vc_qr_code_view_scan_qr_code_explanation" = "Scan someone's QR code to start a conversation with them"; "vc_qr_code_view_scan_qr_code_explanation" = "Scan iemands QR-code om een gesprek te beginnen";
"vc_view_my_qr_code_explanation" = "This is your QR code. Other users can scan it to start a session with you."; "vc_view_my_qr_code_explanation" = "Dit is je QR-code. Andere gebruikers kunnen deze scannen om een sessie met je te starten.";
// MARK: - Not Yet Translated // MARK: - Not Yet Translated
"fast_mode_explanation" = "Youll be notified of new messages reliably and immediately using Apples notification servers."; "fast_mode_explanation" = "Je wordt op een betrouwbare en onmiddellijke manier op de hoogte gebracht van nieuwe berichten via Apple's notificatieservers.";
"fast_mode" = "Fast Mode"; "fast_mode" = "Snelle Modus";
"slow_mode_explanation" = "Session will occasionally check for new messages in the background."; "slow_mode_explanation" = "Sessie controleert af en toe op nieuwe berichten in de achtergrond.";
"slow_mode" = "Slow Mode"; "slow_mode" = "Langzame modus";
"vc_pn_mode_title" = "Message Notifications"; "vc_pn_mode_title" = "Berichtmeldingen";
"vc_notification_settings_notification_mode_title" = "Use Fast Mode"; "vc_notification_settings_notification_mode_title" = "Gebruik snelle modus";
"vc_link_device_recovery_phrase_tab_title" = "Recovery Phrase"; "vc_link_device_recovery_phrase_tab_title" = "Herstel zin";
"vc_link_device_scan_qr_code_explanation" = "Navigate to Settings → Recovery Phrase on your other device to show your QR code."; "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" = "Recovery Phrase"; "vc_enter_recovery_phrase_title" = "Herstel zin";
"vc_enter_recovery_phrase_explanation" = "To link your device, enter the recovery phrase that was given to you when you signed up."; "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" = "Enter Session ID or ONS name"; "vc_enter_public_key_text_field_hint" = "Voer Session-ID of ONS naam in";
"vc_home_title" = "Messages"; "vc_home_title" = "Berichten";
"admin_group_leave_warning" = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."; "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" = "Or join one of these..."; "vc_join_open_group_suggestions_title" = "Of neem deel aan een van deze...";
"vc_settings_invite_a_friend_button_title" = "Invite a Friend"; "vc_settings_invite_a_friend_button_title" = "Nodig een vriend uit";
"vc_settings_help_us_translate_button_title" = "Help us Translate Session"; "vc_settings_help_us_translate_button_title" = "Help ons om Session the vertalen";
"copied" = "Copied"; "copied" = "Gekopieerd";
"vc_conversation_settings_copy_session_id_button_title" = "Copy Session ID"; "vc_conversation_settings_copy_session_id_button_title" = "Session-ID kopiëren";
"vc_conversation_input_prompt" = "Message"; "vc_conversation_input_prompt" = "Bericht";
"vc_conversation_voice_message_cancel_message" = "Slide to Cancel"; "vc_conversation_voice_message_cancel_message" = "Veeg om te annuleren";
"modal_download_attachment_title" = "Trust %@?"; "modal_download_attachment_title" = "Vertrouw %@?";
"modal_download_attachment_explanation" = "Are you sure you want to download media sent by %@?"; "modal_download_attachment_explanation" = "Weet je zeker dat je deze media van %@ wilt downloaden?";
"modal_download_button_title" = "Download"; "modal_download_button_title" = "Downloaden";
"modal_open_url_title" = "Open URL?"; "modal_open_url_title" = "URL openen?";
"modal_open_url_explanation" = "Are you sure you want to open %@?"; "modal_open_url_explanation" = "Weet u zeker dat u %@ wilt openen?";
"modal_open_url_button_title" = "Open"; "modal_open_url_button_title" = "Openen";
"modal_blocked_title" = "Unblock %@?"; "modal_blocked_title" = "Blokkering opheffen voor %@?";
"modal_blocked_explanation" = "Are you sure you want to unblock %@?"; "modal_blocked_explanation" = "Weet je zeker dat je %@ weer wilt toelaten?";
"modal_blocked_button_title" = "Unblock"; "modal_blocked_button_title" = "Deblokkeren";
"modal_link_previews_title" = "Enable Link Previews?"; "modal_link_previews_title" = "Linkvoorbeeld inschakelen?";
"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_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" = "Enable"; "modal_link_previews_button_title" = "Inschakelen";
"vc_share_title" = "Share to Session"; "vc_share_title" = "Delen naar de Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Bijlagen voorbereiden...";
"vc_share_sending_message" = "Sending..."; "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ć %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Zablokować %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokuj"; "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. */ /* 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."; "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. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Brak"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Uwierzytelnianie nie powiodło się."; "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 */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Bloquear %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Desbloquear"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Você não receberá mais ligações e mensagens de quem bloquear.";
/* Label for generic done button. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Instante"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Falha na Autenticação"; "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 */ /* 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ê."; "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 // 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_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_explanation" = "O session verificará ocasionalmente por novas mensagens em segundo plano.";
"slow_mode" = "Modo Lento";
"vc_pn_mode_title" = "Notificação de Mensangens"; "vc_pn_mode_title" = "Notificação de Mensangens";
"vc_notification_settings_notification_mode_title" = "Usar Modo Rápido"; "vc_notification_settings_notification_mode_title" = "Usar Modo Rápido";
"vc_link_device_recovery_phrase_tab_title" = "Frase de Recuperação"; "vc_link_device_recovery_phrase_tab_title" = "Frase de Recuperação";
@ -523,3 +531,4 @@
"vc_share_title" = "Compartilhar no Session"; "vc_share_title" = "Compartilhar no Session";
"vc_share_loading_message" = "Preparando anexos..."; "vc_share_loading_message" = "Preparando anexos...";
"vc_share_sending_message" = "Enviando..."; "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" = "Заблокировать %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Заблокировать %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "Заблокированные пользователи не смогут звонить или отправлять сообщения Вам."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Заблокированные пользователи не смогут звонить или отправлять сообщения Вам.";
/* Label for generic done button. */ /* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Невозможно активировать аккаунт до подтверждения отправленного вам кода."; "REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "Невозможно активировать аккаунт до подтверждения отправленного вам кода.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Мгновенно"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Ошибка аутентификации"; "SCREEN_LOCK_UNLOCK_FAILED" = "Ошибка аутентификации";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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ť %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Blokovať %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Odblokovať"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blokovaný používateľ vám nebude mocť volať ani posielať správy.";
/* Label for generic done button. */ /* 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."; "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. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "Okamžite"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Overenie zlyhalo"; "SCREEN_LOCK_UNLOCK_FAILED" = "Overenie zlyhalo";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 %@?"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "Block %@?";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_UNBLOCK_BUTTON" = "Unblock"; "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. */ /* 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."; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
/* Label for generic done button. */ /* 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."; "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. */ /* 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" = "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed"; "SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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" = "屏蔽 %@"; "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT" = "屏蔽 %@";
/* Button label for the 'unblock' button */ /* Button label for the 'unblock' button */
"BLOCK_LIST_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. */ /* An explanation of the consequences of blocking another user. */
"BLOCK_USER_BEHAVIOR_EXPLANATION" = "被屏蔽的用户将无法向您发起通话,或发送消息。"; "BLOCK_USER_BEHAVIOR_EXPLANATION" = "被屏蔽的用户将无法向您发起通话,或发送消息。";
/* Label for generic done button. */ /* Label for generic done button. */
@ -282,6 +286,8 @@
"REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "在验证发给你的验证码之前,我们无法激活你的帐户."; "REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "在验证发给你的验证码之前,我们无法激活你的帐户.";
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */ /* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "立即"; "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. */ /* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "认证失败"; "SCREEN_LOCK_UNLOCK_FAILED" = "认证失败";
/* alert title when user attempts to leave the send media flow when they have an in-progress album */ /* 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_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments..."; "vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending..."; "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 stackView.spacing = Values.smallSpacing
snContentView.addSubview(stackView) snContentView.addSubview(stackView)
stackView.center(.vertical, in: snContentView) 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.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: Values.smallSpacing).isActive = true
snContentView.pin(to: self, withInset: Cell.contentViewInset) snContentView.pin(to: self, withInset: Cell.contentViewInset)
} }

View File

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

View File

@ -21,7 +21,8 @@ public final class BackgroundPoller : NSObject {
} }
when(resolved: promises).done { _ in when(resolved: promises).done { _ in
completionHandler(.newData) completionHandler(.newData)
}.catch { _ in }.catch { error in
SNLog("Background poll failed due to error: \(error)")
completionHandler(.failed) completionHandler(.failed)
} }
} }
@ -37,19 +38,21 @@ public final class BackgroundPoller : NSObject {
} }
private static func getMessages(for publicKey: String) -> Promise<Void> { 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 } 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 return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey) return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let promises = messages.compactMap { json -> Promise<Void>? in let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
// Use a best attempt approach here; we don't want to fail the entire process if one of the let promises = messages.compactMap { json -> Promise<Void>? in
// messages failed to parse. // Use a best attempt approach here; we don't want to fail the entire process if one of the
guard let envelope = SNProtoEnvelope.from(json), // messages failed to parse.
let data = try? envelope.serializedData() else { return nil } guard let envelope = SNProtoEnvelope.from(json),
let job = MessageReceiveJob(data: data, isBackgroundPoll: true) let data = try? envelope.serializedData() else { return nil }
return job.execute() 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]; _dbReadPool = [[YapDatabaseConnectionPool alloc] initWithDatabase:self.database];
_dbReadWriteConnection = [self newDatabaseConnection]; _dbReadWriteConnection = [self newDatabaseConnection];
_uiDatabaseConnection = [self newDatabaseConnection]; _uiDatabaseConnection = [self newDatabaseConnection];
// Vacuum the database
[self.dbReadWriteConnection vacuum];
// Increase object cache limit. Default is 250. // Increase object cache limit. Default is 250.
_uiDatabaseConnection.objectCacheLimit = 500; _uiDatabaseConnection.objectCacheLimit = 500;

View File

@ -5,8 +5,10 @@ import SessionSnodeKit
public final class FileServerAPIV2 : NSObject { public final class FileServerAPIV2 : NSObject {
// MARK: Settings // MARK: Settings
@objc public static let server = "http://88.99.175.227" @objc public static let oldServer = "http://88.99.175.227"
public static let serverPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" 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 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 /// 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 /// 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 // 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 let tsRequest: TSRequest
switch request.verb { switch request.verb {
case .get: case .get:
@ -84,27 +88,28 @@ public final class FileServerAPIV2 : NSObject {
// MARK: File Storage // MARK: File Storage
@objc(upload:) @objc(upload:)
public static func objc_upload(file: Data) -> AnyPromise { 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> { public static func upload(_ file: Data) -> Promise<UInt64> {
let base64EncodedFile = file.base64EncodedString() let base64EncodedFile = file.base64EncodedString()
let parameters = [ "file" : base64EncodedFile ] let parameters = [ "file" : base64EncodedFile ]
let request = Request(verb: .post, endpoint: "files", parameters: parameters) 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 } guard let fileID = json["result"] as? UInt64 else { throw Error.parsingFailed }
return fileID return fileID
} }
} }
@objc(download:) @objc(download:useOldServer:)
public static func objc_download(file: UInt64) -> AnyPromise { public static func objc_download(file: String, useOldServer: Bool) -> AnyPromise {
return AnyPromise.from(download(file)) 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)") 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 } guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed }
return file 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 { guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
return handleFailure(Error.invalidURL) 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) self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
}.catch(on: DispatchQueue.global()) { error in }.catch(on: DispatchQueue.global()) { error in
handleFailure(error) handleFailure(error)

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,9 @@ NS_ASSUME_NONNULL_BEGIN
quotedMessage:(nullable TSQuotedMessage *)quotedMessage quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview linkPreview:(nullable OWSLinkPreview *)linkPreview
serverTimestamp:(nullable NSNumber *)serverTimestamp 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; - (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

@ -53,6 +53,8 @@ NS_ASSUME_NONNULL_BEGIN
linkPreview:(nullable OWSLinkPreview *)linkPreview linkPreview:(nullable OWSLinkPreview *)linkPreview
serverTimestamp:(nullable NSNumber *)serverTimestamp serverTimestamp:(nullable NSNumber *)serverTimestamp
wasReceivedByUD:(BOOL)wasReceivedByUD wasReceivedByUD:(BOOL)wasReceivedByUD
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
{ {
self = [super initMessageWithTimestamp:timestamp self = [super initMessageWithTimestamp:timestamp
inThread:thread inThread:thread
@ -61,7 +63,9 @@ NS_ASSUME_NONNULL_BEGIN
expiresInSeconds:expiresInSeconds expiresInSeconds:expiresInSeconds
expireStartedAt:0 expireStartedAt:0
quotedMessage:quotedMessage quotedMessage:quotedMessage
linkPreview:linkPreview]; linkPreview:linkPreview
openGroupInvitationName:openGroupInvitationName
openGroupInvitationURL:openGroupInvitationURL];
if (!self) { if (!self) {
return self; return self;
@ -140,6 +144,17 @@ NS_ASSUME_NONNULL_BEGIN
return; 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; _read = YES;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];

View File

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

View File

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

View File

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

View File

@ -28,7 +28,9 @@ import SessionUtilitiesKit
isVoiceMessage: false, isVoiceMessage: false,
groupMetaMessage: .unspecified, groupMetaMessage: .unspecified,
quotedMessage: TSQuotedMessage.from(visibleMessage.quote), 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 isVoiceMessage:(BOOL)isVoiceMessage
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
quotedMessage:(nullable TSQuotedMessage *)quotedMessage 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; - (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

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

View File

@ -61,8 +61,8 @@ public extension VisibleMessage {
public override var description: String { public override var description: String {
""" """
LinkPreview( LinkPreview(
title: \(title ?? "null") title: \(title ?? "null"),
url: \(url ?? "null") url: \(url ?? "null"),
attachmentID: \(attachmentID ?? "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 { public override var description: String {
""" """
Profile( Profile(
displayName: \(displayName ?? "null") displayName: \(displayName ?? "null"),
profileKey: \(profileKey?.description ?? "null") profileKey: \(profileKey?.description ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null") profilePictureURL: \(profilePictureURL ?? "null")
) )
""" """

View File

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

View File

@ -12,6 +12,7 @@ public final class VisibleMessage : Message {
@objc public var linkPreview: LinkPreview? @objc public var linkPreview: LinkPreview?
@objc public var contact: Contact? @objc public var contact: Contact?
@objc public var profile: Profile? @objc public var profile: Profile?
@objc public var openGroupInvitation: OpenGroupInvitation?
public override var isSelfSendValid: Bool { true } public override var isSelfSendValid: Bool { true }
@ -22,6 +23,7 @@ public final class VisibleMessage : Message {
public override var isValid: Bool { public override var isValid: Bool {
guard super.isValid else { return false } guard super.isValid else { return false }
if !attachmentIDs.isEmpty { return true } if !attachmentIDs.isEmpty { return true }
if openGroupInvitation != nil { return true }
if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true } if let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return true }
return false return false
} }
@ -36,6 +38,7 @@ public final class VisibleMessage : Message {
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview } if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
// TODO: Contact // TODO: Contact
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile } 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) { public override func encode(with coder: NSCoder) {
@ -47,6 +50,7 @@ public final class VisibleMessage : Message {
coder.encode(linkPreview, forKey: "linkPreview") coder.encode(linkPreview, forKey: "linkPreview")
// TODO: Contact // TODO: Contact
coder.encode(profile, forKey: "profile") coder.encode(profile, forKey: "profile")
coder.encode(openGroupInvitation, forKey: "openGroupInvitation")
} }
// MARK: Proto Conversion // 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 } if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
// TODO: Contact // TODO: Contact
if let profile = Profile.fromProto(dataMessage) { result.profile = profile } 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 result.syncTarget = dataMessage.syncTarget
return result return result
} }
@ -95,6 +101,8 @@ public final class VisibleMessage : Message {
let attachmentProtos = attachments.compactMap { $0.buildProto() } let attachmentProtos = attachments.compactMap { $0.buildProto() }
dataMessage.setAttachments(attachmentProtos) dataMessage.setAttachments(attachmentProtos)
// TODO: Contact // TODO: Contact
// Open group invitation
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
// Expiration timer // Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation // 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... // 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 { public override var description: String {
""" """
VisibleMessage( VisibleMessage(
text: \(text ?? "null") text: \(text ?? "null"),
attachmentIDs: \(attachmentIDs) attachmentIDs: \(attachmentIDs),
quote: \(quote?.description ?? "null") quote: \(quote?.description ?? "null"),
linkPreview: \(linkPreview?.description ?? "null") linkPreview: \(linkPreview?.description ?? "null"),
contact: \(contact?.description ?? "null") contact: \(contact?.description ?? "null"),
profile: \(profile?.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 server: String
@objc public let room: String @objc public let room: String
public let id: String public let id: String
public let name: String @objc public let name: String
public let publicKey: String @objc public let publicKey: String
/// The ID with which the image can be retrieved from the server. /// The ID with which the image can be retrieved from the server.
public let imageID: String? public let imageID: String?

View File

@ -1294,6 +1294,118 @@ extension SNProtoDataMessageLokiProfile.SNProtoDataMessageLokiProfileBuilder {
#endif #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 // MARK: - SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper
@objc public class SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper: NSObject { @objc public class SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper: NSObject {
@ -1696,6 +1808,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
if let _value = profile { if let _value = profile {
builder.setProfile(_value) builder.setProfile(_value)
} }
if let _value = openGroupInvitation {
builder.setOpenGroupInvitation(_value)
}
if let _value = closedGroupControlMessage { if let _value = closedGroupControlMessage {
builder.setClosedGroupControlMessage(_value) builder.setClosedGroupControlMessage(_value)
} }
@ -1763,6 +1878,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
proto.profile = valueParam.proto proto.profile = valueParam.proto
} }
@objc public func setOpenGroupInvitation(_ valueParam: SNProtoDataMessageOpenGroupInvitation) {
proto.openGroupInvitation = valueParam.proto
}
@objc public func setClosedGroupControlMessage(_ valueParam: SNProtoDataMessageClosedGroupControlMessage) { @objc public func setClosedGroupControlMessage(_ valueParam: SNProtoDataMessageClosedGroupControlMessage) {
proto.closedGroupControlMessage = valueParam.proto proto.closedGroupControlMessage = valueParam.proto
} }
@ -1792,6 +1911,8 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
@objc public let profile: SNProtoDataMessageLokiProfile? @objc public let profile: SNProtoDataMessageLokiProfile?
@objc public let openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?
@objc public let closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage? @objc public let closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?
@objc public var body: String? { @objc public var body: String? {
@ -1851,6 +1972,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
quote: SNProtoDataMessageQuote?, quote: SNProtoDataMessageQuote?,
preview: [SNProtoDataMessagePreview], preview: [SNProtoDataMessagePreview],
profile: SNProtoDataMessageLokiProfile?, profile: SNProtoDataMessageLokiProfile?,
openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?,
closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) { closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) {
self.proto = proto self.proto = proto
self.attachments = attachments self.attachments = attachments
@ -1858,6 +1980,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
self.quote = quote self.quote = quote
self.preview = preview self.preview = preview
self.profile = profile self.profile = profile
self.openGroupInvitation = openGroupInvitation
self.closedGroupControlMessage = closedGroupControlMessage self.closedGroupControlMessage = closedGroupControlMessage
} }
@ -1893,6 +2016,11 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
profile = try SNProtoDataMessageLokiProfile.parseProto(proto.profile) profile = try SNProtoDataMessageLokiProfile.parseProto(proto.profile)
} }
var openGroupInvitation: SNProtoDataMessageOpenGroupInvitation? = nil
if proto.hasOpenGroupInvitation {
openGroupInvitation = try SNProtoDataMessageOpenGroupInvitation.parseProto(proto.openGroupInvitation)
}
var closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage? = nil var closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage? = nil
if proto.hasClosedGroupControlMessage { if proto.hasClosedGroupControlMessage {
closedGroupControlMessage = try SNProtoDataMessageClosedGroupControlMessage.parseProto(proto.closedGroupControlMessage) closedGroupControlMessage = try SNProtoDataMessageClosedGroupControlMessage.parseProto(proto.closedGroupControlMessage)
@ -1908,6 +2036,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
quote: quote, quote: quote,
preview: preview, preview: preview,
profile: profile, profile: profile,
openGroupInvitation: openGroupInvitation,
closedGroupControlMessage: closedGroupControlMessage) closedGroupControlMessage: closedGroupControlMessage)
return result 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. /// Clears the value of `profile`. Subsequent reads from it will return its default value.
mutating func clearProfile() {_uniqueStorage()._profile = nil} 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 { var closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage {
get {return _storage._closedGroupControlMessage ?? SessionProtos_DataMessage.ClosedGroupControlMessage()} get {return _storage._closedGroupControlMessage ?? SessionProtos_DataMessage.ClosedGroupControlMessage()}
set {_uniqueStorage()._closedGroupControlMessage = newValue} set {_uniqueStorage()._closedGroupControlMessage = newValue}
@ -670,6 +679,39 @@ struct SessionProtos_DataMessage {
fileprivate var _profilePicture: String? = nil 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 { struct ClosedGroupControlMessage {
// SwiftProtobuf.Message conformance is added in an extension below. See the // SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for // `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"), 8: .same(proto: "quote"),
10: .same(proto: "preview"), 10: .same(proto: "preview"),
101: .same(proto: "profile"), 101: .same(proto: "profile"),
102: .same(proto: "openGroupInvitation"),
104: .same(proto: "closedGroupControlMessage"), 104: .same(proto: "closedGroupControlMessage"),
105: .same(proto: "syncTarget"), 105: .same(proto: "syncTarget"),
] ]
@ -1648,6 +1691,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
var _quote: SessionProtos_DataMessage.Quote? = nil var _quote: SessionProtos_DataMessage.Quote? = nil
var _preview: [SessionProtos_DataMessage.Preview] = [] var _preview: [SessionProtos_DataMessage.Preview] = []
var _profile: SessionProtos_DataMessage.LokiProfile? = nil var _profile: SessionProtos_DataMessage.LokiProfile? = nil
var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil
var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil
var _syncTarget: String? = nil var _syncTarget: String? = nil
@ -1666,6 +1710,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
_quote = source._quote _quote = source._quote
_preview = source._preview _preview = source._preview
_profile = source._profile _profile = source._profile
_openGroupInvitation = source._openGroupInvitation
_closedGroupControlMessage = source._closedGroupControlMessage _closedGroupControlMessage = source._closedGroupControlMessage
_syncTarget = source._syncTarget _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._group, !v.isInitialized {return false}
if let v = _storage._quote, !v.isInitialized {return false} if let v = _storage._quote, !v.isInitialized {return false}
if !SwiftProtobuf.Internal.areAllInitialized(_storage._preview) {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} if let v = _storage._closedGroupControlMessage, !v.isInitialized {return false}
return true return true
} }
@ -1704,6 +1750,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
case 8: try decoder.decodeSingularMessageField(value: &_storage._quote) case 8: try decoder.decodeSingularMessageField(value: &_storage._quote)
case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview) case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview)
case 101: try decoder.decodeSingularMessageField(value: &_storage._profile) 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 104: try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage)
case 105: try decoder.decodeSingularStringField(value: &_storage._syncTarget) case 105: try decoder.decodeSingularStringField(value: &_storage._syncTarget)
default: break default: break
@ -1744,6 +1791,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
if let v = _storage._profile { if let v = _storage._profile {
try visitor.visitSingularMessageField(value: v, fieldNumber: 101) try visitor.visitSingularMessageField(value: v, fieldNumber: 101)
} }
if let v = _storage._openGroupInvitation {
try visitor.visitSingularMessageField(value: v, fieldNumber: 102)
}
if let v = _storage._closedGroupControlMessage { if let v = _storage._closedGroupControlMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 104) 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._quote != rhs_storage._quote {return false}
if _storage._preview != rhs_storage._preview {return false} if _storage._preview != rhs_storage._preview {return false}
if _storage._profile != rhs_storage._profile {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._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false}
if _storage._syncTarget != rhs_storage._syncTarget {return false} if _storage._syncTarget != rhs_storage._syncTarget {return false}
return true 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 { extension SessionProtos_DataMessage.ClosedGroupControlMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".ClosedGroupControlMessage" static let protoMessageName: String = SessionProtos_DataMessage.protoMessageName + ".ClosedGroupControlMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View File

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

View File

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

View File

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

View File

@ -54,11 +54,6 @@ public enum MessageReceiver {
// Parse the envelope // Parse the envelope
let envelope = try SNProtoEnvelope.parseData(data) let envelope = try SNProtoEnvelope.parseData(data)
let storage = SNMessagingKitConfiguration.shared.storage 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 // Decrypt the contents
guard let ciphertext = envelope.content else { throw Error.noData } guard let ciphertext = envelope.content else { throw Error.noData }
var plaintext: Data! var plaintext: Data!
@ -92,22 +87,22 @@ public enum MessageReceiver {
} }
groupPublicKey = envelope.source groupPublicKey = envelope.source
try decrypt() try decrypt()
/*
// do { do {
// try decrypt() try decrypt()
// } catch { } catch {
// do { do {
// let now = Date() let now = Date()
// // Don't spam encryption key pair requests // Don't spam encryption key pair requests
// let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true
// if shouldRequestEncryptionKeyPair { if shouldRequestEncryptionKeyPair {
// try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction) try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction)
// lastEncryptionKeyPairRequest[groupPublicKey!] = now lastEncryptionKeyPairRequest[groupPublicKey!] = now
// } }
// } }
// throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one) throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one)
// } }
*/
default: throw Error.unknownEnvelopeType default: throw Error.unknownEnvelopeType
} }
} }
@ -159,6 +154,19 @@ public enum MessageReceiver {
guard isValid else { guard isValid else {
throw Error.invalidMessage 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
return (message, proto) return (message, proto)
} else { } else {

View File

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

View File

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

View File

@ -3,11 +3,12 @@ import PromiseKit
@objc(LKClosedGroupPoller) @objc(LKClosedGroupPoller)
public final class ClosedGroupPoller : NSObject { public final class ClosedGroupPoller : NSObject {
private var isPolling = false private var isPolling: [String:Bool] = [:]
private var timer: Timer? private var timers: [String:Timer] = [:]
// MARK: Settings // MARK: Settings
private static let pollInterval: TimeInterval = 2 private static let minPollInterval: Double = 4
private static let maxPollInterval: Double = 2 * 60
// MARK: Error // MARK: Error
private enum Error : LocalizedError { 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 // MARK: Public API
@objc public func startIfNeeded() { @objc public func start() {
#if DEBUG #if DEBUG
assert(Thread.current.isMainThread) // Timers don't do well on background queues assert(Thread.current.isMainThread) // Timers don't do well on background queues
#endif #endif
guard !isPolling else { return } let storage = SNMessagingKitConfiguration.shared.storage
isPolling = true let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in allGroupPublicKeys.forEach { startPolling(for: $0) }
let _ = self?.poll()
}
} }
public func pollOnce() -> [Promise<Void>] { public func startPolling(for groupPublicKey: String) {
guard !isPolling else { return [] } guard !isPolling(for: groupPublicKey) else { return }
isPolling = true setUpPolling(for: groupPublicKey)
return poll() isPolling[groupPublicKey] = true
} }
@objc public func stop() { @objc public func stop() {
isPolling = false let storage = SNMessagingKitConfiguration.shared.storage
timer?.invalidate() let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
allGroupPublicKeys.forEach { stopPolling(for: $0) }
}
public func stopPolling(for groupPublicKey: String) {
timers[groupPublicKey]?.invalidate()
isPolling[groupPublicKey] = false
} }
// MARK: Private API // MARK: Private API
private func poll() -> [Promise<Void>] { private func setUpPolling(for groupPublicKey: String) {
guard isPolling else { return [] } poll(groupPublicKey).done2 { [weak self] _ in
let publicKeys = Storage.shared.getUserClosedGroupPublicKeys() DispatchQueue.main.async { // Timers don't do well on background queues
return publicKeys.map { publicKey in self?.pollRecursively(groupPublicKey)
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)
}
} }
promise.done2 { [weak self] messages in }.catch2 { [weak self] error in
guard let self = self, self.isPolling else { return } // The error is logged in poll(_:)
if !messages.isEmpty { DispatchQueue.main.async { // Timers don't do well on background queues
SNLog("Received \(messages.count) new message(s) in closed group with public key: \(publicKey).") self?.pollRecursively(groupPublicKey)
}
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).")
}
}
} }
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 private var pollCount = 0
// MARK: Settings // MARK: Settings
private static let pollInterval: TimeInterval = 1 private static let pollInterval: TimeInterval = 1.5
private static let retryInterval: TimeInterval = 0.25 private static let retryInterval: TimeInterval = 0.25
/// After polling a given snode this many times we always switch to a new one. /// 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 let snodePoolPromises: [Promise<Set<Snode>>] = snodes.map { snode in
return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { 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 = [ let parameters: JSON = [
"endpoint" : "get_service_nodes", "endpoint" : "get_service_nodes",
"params" : [ "params" : [
"limit" : 256,
"active_only" : true, "active_only" : true,
"fields" : [ "fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true "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 let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Snode> in
var result: Set<Snode> = results[0] 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 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 { } else {
throw Error.inconsistentSnodePools throw Error.inconsistentSnodePools
} }

View File

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

View File

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