Merge pull request #886 from mpretty-cyro/feature/blinded-message-request-setting

Community message request setting
This commit is contained in:
Morgan Pretty 2023-08-11 18:58:35 +10:00 committed by GitHub
commit 968f50f2fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 947 additions and 146 deletions

@ -1 +1 @@
Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d

View File

@ -515,6 +515,9 @@
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; };
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; };
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; };
FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; };
FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */; };
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; };
FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; };
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; };
@ -1671,6 +1674,9 @@
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = "<group>"; };
FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = "<group>"; };
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Utilities.swift"; sourceTree = "<group>"; };
FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = "<group>"; };
FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = "<group>"; };
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = "<group>"; };
FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = "<group>"; };
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = "<group>"; };
@ -3601,6 +3607,7 @@
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */,
FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */,
FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */,
FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -3666,6 +3673,7 @@
children = (
FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */,
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */,
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */,
@ -3746,6 +3754,7 @@
FD2B4B022949886900AB4848 /* Database */ = {
isa = PBXGroup;
children = (
FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */,
FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */,
);
path = Database;
@ -5672,6 +5681,7 @@
FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */,
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */,
FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */,
FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */,
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
@ -5924,12 +5934,14 @@
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */,
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */,
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */,
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
FD245C56285065EA00B966DD /* SNProto.swift in Sources */,
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */,
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */,
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,

View File

@ -208,17 +208,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}()
private lazy var emptyStateLabel: UILabel = {
let text: String = String(
format: {
switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
viewModel.threadData.displayName
)
let text: String = emptyStateText(for: viewModel.threadData)
let result: UILabel = UILabel()
result.accessibilityLabel = "Empty state label"
result.translatesAutoresizingMaskIntoConstraints = false
@ -698,6 +688,24 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
self.viewModel.onInteractionChange = nil
}
private func emptyStateText(for threadData: SessionThreadViewModel) -> String {
return String(
format: {
switch (threadData.threadIsNoteToSelf, threadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false):
return (threadData.profile?.blocksCommunityMessageRequests == true ?
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE".localized() :
"CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
)
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
threadData.displayName
)
}
private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) {
// Ensure the first load or a load when returning from a child screen runs without animations (if
// we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition)
@ -738,17 +746,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
)
// Update the empty state
let text: String = String(
format: {
switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
updatedThreadData.displayName
)
let text: String = emptyStateText(for: updatedThreadData)
emptyStateLabel.attributedText = NSAttributedString(string: text)
.adding(
attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)],
@ -791,8 +789,10 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
updatedThreadData.threadRequiresApproval == true
)
self?.messageRequestStackView.isHidden = (
updatedThreadData.threadIsMessageRequest == false &&
updatedThreadData.threadRequiresApproval == false
!updatedThreadData.canWrite || (
updatedThreadData.threadIsMessageRequest == false &&
updatedThreadData.threadRequiresApproval == false
)
)
self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true)
self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20)

View File

@ -179,6 +179,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Content
private var originalState: SessionThreadViewModel?
override var title: String {
switch threadVariant {
case .contact: return "vc_settings_title".localized()
@ -236,6 +237,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
threadViewModel.currentUserIsClosedGroupAdmin == true
)
let editIcon: UIImage? = UIImage(named: "icon_edit")
let originalState: SessionThreadViewModel = (self?.originalState ?? threadViewModel)
self?.originalState = threadViewModel
return [
SectionModel(
@ -578,7 +581,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
title: "vc_conversation_settings_notify_for_mentions_only_title".localized(),
subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(),
rightAccessory: .toggle(
.boolValue(threadViewModel.threadOnlyNotifyForMentions == true)
.boolValue(
threadViewModel.threadOnlyNotifyForMentions == true,
oldValue: (originalState.threadOnlyNotifyForMentions == true)
)
),
isEnabled: (
(
@ -616,7 +622,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(),
rightAccessory: .toggle(
.boolValue(threadViewModel.threadMutedUntilTimestamp != nil)
.boolValue(
threadViewModel.threadMutedUntilTimestamp != nil,
oldValue: (originalState.threadMutedUntilTimestamp != nil)
)
),
isEnabled: (
(
@ -662,7 +671,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(),
rightAccessory: .toggle(
.boolValue(threadViewModel.threadIsBlocked == true)
.boolValue(
threadViewModel.threadIsBlocked == true,
oldValue: (originalState.threadIsBlocked == true)
)
),
accessibility: Accessibility(
identifier: "\(ThreadSettingsViewModel.self).block",

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Bildschirmschutz";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Lesebestätigungen";
"PRIVACY_READ_RECEIPTS_TITLE" = "Lesebestätigungen";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Protección de pantalla";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Notificaciones de lectura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Notificaciones de lectura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "امنیت صفحه";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "قفل Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = " برای باز کردن قفل Session به شناسه لمسی، شناسه صورت و یا رمز عبوری ضرورت است.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "رسیدهای خواندن";
"PRIVACY_READ_RECEIPTS_TITLE" = "رسیدهای خواندن";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "رسیدهای خواندن در چت‌های یک به یک روان شود.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Näytön suojaus";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Lukukuittaukset";
"PRIVACY_READ_RECEIPTS_TITLE" = "Lukukuittaukset";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sécurité de lécran";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Verrouiller Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Requiert Touch ID, Face ID ou votre code pour déverrouiller Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_TITLE" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Envoyer un accusé réception dans les conversations 1 à 1.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sigurnost zaslona";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potvrda o čitanju";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potvrda o čitanju";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Layar Aman";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Pesan terbaca diterima";
"PRIVACY_READ_RECEIPTS_TITLE" = "Pesan terbaca diterima";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sicurezza schermo";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Ricevute di lettura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Ricevute di lettura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "スクリーン・セキュリティ";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "既読確認";
"PRIVACY_READ_RECEIPTS_TITLE" = "既読確認";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Scherm beveiliging";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Leesbevestigingen";
"PRIVACY_READ_RECEIPTS_TITLE" = "Leesbevestigingen";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Ochrona ekranu";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potwierdzenia odczytania";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potwierdzenia odczytania";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Segurança de Tela";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Confirmações de Leitura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Confirmações de Leitura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Защита экрана";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Уведомления о прочтении";
"PRIVACY_READ_RECEIPTS_TITLE" = "Уведомления о прочтении";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "තිරයේ ආරක්ෂාව";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "කියවූ බවට ලදුපත්";
"PRIVACY_READ_RECEIPTS_TITLE" = "කියවූ බවට ලදුපත්";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Zabezpečenie obrazovky";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potvrdenia o prečítaní";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potvrdenia o prečítaní";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Skärmsäkerhet";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Läskvittenser";
"PRIVACY_READ_RECEIPTS_TITLE" = "Läskvittenser";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "ความปลอดภัยหน้าจอ";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "แจ้งการอ่านข้อความ";
"PRIVACY_READ_RECEIPTS_TITLE" = "แจ้งการอ่านข้อความ";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "螢幕顯示安全";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "已讀回條";
"PRIVACY_READ_RECEIPTS_TITLE" = "已讀回條";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "屏幕安全";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "已读回执";
"PRIVACY_READ_RECEIPTS_TITLE" = "已读回执";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,7 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";

View File

@ -33,6 +33,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
// MARK: - Content
private struct State: Equatable {
let trimOpenGroupMessagesOlderThanSixMonths: Bool
let shouldAutoPlayConsecutiveAudioMessages: Bool
}
override var title: String { "CONVERSATION_SETTINGS_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData }
@ -45,7 +50,17 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in
.trackingConstantRegion { [weak self] db -> State in
State(
trimOpenGroupMessagesOlderThanSixMonths: db[.trimOpenGroupMessagesOlderThanSixMonths],
shouldAutoPlayConsecutiveAudioMessages: db[.shouldAutoPlayConsecutiveAudioMessages]
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [
SectionModel(
model: .messageTrimming,
@ -55,7 +70,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(),
rightAccessory: .toggle(
.settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
.boolValue(
key: .trimOpenGroupMessagesOlderThanSixMonths,
value: current.trimOpenGroupMessagesOlderThanSixMonths,
oldValue: (previous ?? current).trimOpenGroupMessagesOlderThanSixMonths
)
),
onTap: {
Storage.shared.write { db in
@ -73,7 +92,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(),
rightAccessory: .toggle(
.settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
.boolValue(
key: .shouldAutoPlayConsecutiveAudioMessages,
value: current.shouldAutoPlayConsecutiveAudioMessages,
oldValue: (previous ?? current).shouldAutoPlayConsecutiveAudioMessages
)
),
onTap: {
Storage.shared.write { db in
@ -103,8 +126,5 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
)
]
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self)
}

View File

@ -7,7 +7,7 @@ import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Setting> {
class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Item> {
// MARK: - Config
public enum Section: SessionTableSection {
@ -31,7 +31,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
}
}
public enum Setting: Differentiable {
public enum Item: Differentiable {
case strategyUseFastMode
case strategyDeviceSettings
case styleSound
@ -41,6 +41,13 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
// MARK: - Content
private struct State: Equatable {
let isUsingFullAPNs: Bool
let notificationSound: Preferences.Sound
let playNotificationSoundInForeground: Bool
let previewType: Preferences.NotificationPreviewType
}
override var title: String { "NOTIFICATIONS_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData }
@ -53,12 +60,30 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in
let notificationSound: Preferences.Sound = db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound)
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
.defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType)
.trackingConstantRegion { db -> State in
State(
isUsingFullAPNs: false, // Set later the the data flow
notificationSound: db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound),
playNotificationSoundInForeground: db[.playNotificationSoundInForeground],
previewType: db[.preferencesNotificationPreviewType]
.defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType)
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.manualRefreshFrom(forcedRefresh)
.map { dbState -> State in
State(
isUsingFullAPNs: UserDefaults.standard[.isUsingFullAPNs],
notificationSound: dbState.notificationSound,
playNotificationSoundInForeground: dbState.playNotificationSoundInForeground,
previewType: dbState.previewType
)
}
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [
SectionModel(
model: .strategy,
@ -68,20 +93,24 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
title: "NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE".localized(),
subtitle: "NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION".localized(),
rightAccessory: .toggle(
.userDefaults(UserDefaults.standard, key: "isUsingFullAPNs")
.boolValue(
current.isUsingFullAPNs,
oldValue: (previous ?? current).isUsingFullAPNs
)
),
styling: SessionCell.StyleInfo(
allowedSeparators: [.top],
customPadding: SessionCell.Padding(bottom: Values.verySmallSpacing)
),
onTap: {
onTap: { [weak self] in
UserDefaults.standard.set(
!UserDefaults.standard.bool(forKey: "isUsingFullAPNs"),
forKey: "isUsingFullAPNs"
)
// Force sync the push tokens on change
SyncPushTokensJob.run(uploadOnlyIfStale: false)
self?.forceRefresh()
}
),
SessionCell.Info(
@ -106,7 +135,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
id: .styleSound,
title: "NOTIFICATIONS_STYLE_SOUND_TITLE".localized(),
rightAccessory: .dropDown(
.dynamicString { notificationSound.displayName }
.dynamicString { current.notificationSound.displayName }
),
onTap: { [weak self] in
self?.transitionToScreen(
@ -117,7 +146,13 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
SessionCell.Info(
id: .styleSoundWhenAppIsOpen,
title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(),
rightAccessory: .toggle(.settingBool(key: .playNotificationSoundInForeground)),
rightAccessory: .toggle(
.boolValue(
key: .playNotificationSoundInForeground,
value: current.playNotificationSoundInForeground,
oldValue: (previous ?? current).playNotificationSoundInForeground
)
),
onTap: {
Storage.shared.write { db in
db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground]
@ -134,7 +169,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
title: "NOTIFICATIONS_STYLE_CONTENT_TITLE".localized(),
subtitle: "NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION".localized(),
rightAccessory: .dropDown(
.dynamicString { previewType.name }
.dynamicString { current.previewType.name }
),
onTap: { [weak self] in
self?.transitionToScreen(
@ -146,8 +181,5 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
)
]
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self)
}

View File

@ -28,6 +28,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
public enum Section: SessionTableSection {
case screenSecurity
case messageRequests
case readReceipts
case typingIndicators
case linkPreviews
@ -36,6 +37,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
var title: String? {
switch self {
case .screenSecurity: return "PRIVACY_SECTION_SCREEN_SECURITY".localized()
case .messageRequests: return "PRIVACY_SECTION_MESSAGE_REQUESTS".localized()
case .readReceipts: return "PRIVACY_SECTION_READ_RECEIPTS".localized()
case .typingIndicators: return "PRIVACY_SECTION_TYPING_INDICATORS".localized()
case .linkPreviews: return "PRIVACY_SECTION_LINK_PREVIEWS".localized()
@ -48,6 +50,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
public enum Item: Differentiable {
case screenLock
case communityMessageRequests
case screenshotNotifications
case readReceipts
case typingIndicators
@ -75,6 +78,15 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
// MARK: - Content
private struct State: Equatable {
let isScreenLockEnabled: Bool
let checkForCommunityMessageRequests: Bool
let areReadReceiptsEnabled: Bool
let typingIndicatorsEnabled: Bool
let areLinkPreviewsEnabled: Bool
let areCallsEnabled: Bool
}
override var title: String { "PRIVACY_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData }
@ -87,7 +99,21 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in
.trackingConstantRegion { [weak self] db -> State in
State(
isScreenLockEnabled: db[.isScreenLockEnabled],
checkForCommunityMessageRequests: db[.checkForCommunityMessageRequests],
areReadReceiptsEnabled: db[.areReadReceiptsEnabled],
typingIndicatorsEnabled: db[.typingIndicatorsEnabled],
areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled],
areCallsEnabled: db[.areCallsEnabled]
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [
SectionModel(
model: .screenSecurity,
@ -96,7 +122,13 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .screenLock,
title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(),
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)),
rightAccessory: .toggle(
.boolValue(
key: .isScreenLockEnabled,
value: current.isScreenLockEnabled,
oldValue: (previous ?? current).isScreenLockEnabled
)
),
onTap: { [weak self] in
// Make sure the device has a passcode set before allowing screen lock to
// be enabled (Note: This will always return true on a simulator)
@ -115,7 +147,32 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
}
Storage.shared.write { db in
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled])
}
}
)
]
),
SectionModel(
model: .messageRequests,
elements: [
SessionCell.Info(
id: .communityMessageRequests,
title: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE".localized(),
subtitle: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION".localized(),
rightAccessory: .toggle(
.boolValue(
key: .checkForCommunityMessageRequests,
value: current.checkForCommunityMessageRequests,
oldValue: (previous ?? current).checkForCommunityMessageRequests
)
),
onTap: { [weak self] in
Storage.shared.write { db in
try db.setAndUpdateConfig(
.checkForCommunityMessageRequests,
to: !db[.checkForCommunityMessageRequests]
)
}
}
)
@ -128,10 +185,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .readReceipts,
title: "PRIVACY_READ_RECEIPTS_TITLE".localized(),
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)),
rightAccessory: .toggle(
.boolValue(
key: .areReadReceiptsEnabled,
value: current.areReadReceiptsEnabled,
oldValue: (previous ?? current).areReadReceiptsEnabled
)
),
onTap: {
Storage.shared.write { db in
db[.areReadReceiptsEnabled] = !db[.areReadReceiptsEnabled]
try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled])
}
}
)
@ -176,10 +239,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
return result
}
),
rightAccessory: .toggle(.settingBool(key: .typingIndicatorsEnabled)),
rightAccessory: .toggle(
.boolValue(
key: .typingIndicatorsEnabled,
value: current.typingIndicatorsEnabled,
oldValue: (previous ?? current).typingIndicatorsEnabled
)
),
onTap: {
Storage.shared.write { db in
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled]
try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled])
}
}
)
@ -192,10 +261,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .linkPreviews,
title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(),
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)),
rightAccessory: .toggle(
.boolValue(
key: .areLinkPreviewsEnabled,
value: current.areLinkPreviewsEnabled,
oldValue: (previous ?? current).areLinkPreviewsEnabled
)
),
onTap: {
Storage.shared.write { db in
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled]
try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled])
}
}
)
@ -208,7 +283,13 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .calls,
title: "PRIVACY_CALLS_TITLE".localized(),
subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areCallsEnabled)),
rightAccessory: .toggle(
.boolValue(
key: .areCallsEnabled,
value: current.areCallsEnabled,
oldValue: (previous ?? current).areCallsEnabled
)
),
accessibility: Accessibility(
label: "Allow voice and video calls"
),
@ -223,7 +304,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
),
onTap: {
Storage.shared.write { db in
db[.areCallsEnabled] = !db[.areCallsEnabled]
try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled])
}
}
)
@ -231,8 +312,5 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
)
]
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self)
}

View File

@ -262,7 +262,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
reloadSectionsAnimation: .none,
deleteRowsAnimation: .fade,
insertRowsAnimation: .fade,
reloadRowsAnimation: .fade,
reloadRowsAnimation: .none,
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
) { [weak self] updatedData in
self?.viewModel.updateTableData(updatedData)
@ -339,6 +339,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables)
viewModel.leftNavItems
.receive(on: DispatchQueue.main)
.sink { [weak self] maybeItems in
self?.navigationItem.setLeftBarButtonItems(
maybeItems.map { items in
@ -360,6 +361,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables)
viewModel.rightNavItems
.receive(on: DispatchQueue.main)
.sink { [weak self] maybeItems in
self?.navigationItem.setRightBarButtonItems(
maybeItems.map { items in
@ -381,18 +383,21 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables)
viewModel.emptyStateTextPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] text in
self?.emptyStateLabel.text = text
}
.store(in: &disposables)
viewModel.footerView
.receive(on: DispatchQueue.main)
.sink { [weak self] footerView in
self?.tableView.tableFooterView = footerView
}
.store(in: &disposables)
viewModel.footerButtonInfo
.receive(on: DispatchQueue.main)
.sink { [weak self] buttonInfo in
if let buttonInfo: SessionButton.Info = buttonInfo {
self?.footerButton.setTitle(buttonInfo.title, for: .normal)
@ -627,7 +632,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
) {
// Try update the existing cell to have a nice animation instead of reloading the cell
if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell {
existingCell.update(with: info)
existingCell.update(with: info, isManualReload: true)
}
else {
tableView.reloadRows(at: [indexPath], with: .none)

View File

@ -27,6 +27,9 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
open var leftNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
private let _forcedRefresh: PassthroughSubject<Void, Never> = PassthroughSubject()
lazy var forcedRefresh: AnyPublisher<Void, Never> = _forcedRefresh
.shareReplay(0)
private let _showToast: PassthroughSubject<(String, ThemeValue), Never> = PassthroughSubject()
lazy var showToast: AnyPublisher<(String, ThemeValue), Never> = _showToast
.shareReplay(0)
@ -62,6 +65,10 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
// MARK: - Functions
func forceRefresh() {
_forcedRefresh.send(())
}
func setIsEditing(_ isEditing: Bool) {
_isEditing.send(isEditing)
}
@ -101,7 +108,7 @@ extension Array {
}
}
extension AnyPublisher {
extension Publisher {
func mapToSessionTableViewData<Nav, Section, Item>(
for viewModel: SessionTableViewModel<Nav, Section, Item>
) -> AnyPublisher<(Output, StagedChangeset<Output>), Failure> where Output == [ArraySection<Section, SessionCell.Info<Item>>] {

View File

@ -394,19 +394,30 @@ extension SessionCell.Accessory {
extension SessionCell.Accessory {
public enum DataSource: Hashable, Equatable {
case boolValue(Bool)
case boolValue(key: String, value: Bool, oldValue: Bool)
case dynamicString(() -> String?)
case userDefaults(UserDefaults, key: String)
case settingBool(key: Setting.BoolKey)
static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource {
return .boolValue(key: "", value: value, oldValue: oldValue)
}
static func boolValue(key: Setting.BoolKey, value: Bool, oldValue: Bool) -> DataSource {
return .boolValue(key: key.rawValue, value: value, oldValue: oldValue)
}
// MARK: - Convenience
public var currentBoolValue: Bool {
switch self {
case .boolValue(let value): return value
case .boolValue(_, let value, _): return value
case .dynamicString: return false
case .userDefaults(let defaults, let key): return defaults.bool(forKey: key)
case .settingBool(let key): return Storage.shared[key]
}
}
public var oldBoolValue: Bool {
switch self {
case .boolValue(_, _, let oldValue): return oldValue
default: return false
}
}
@ -421,27 +432,27 @@ extension SessionCell.Accessory {
public func hash(into hasher: inout Hasher) {
switch self {
case .boolValue(let value): value.hash(into: &hasher)
case .boolValue(let key, let value, let oldValue):
key.hash(into: &hasher)
value.hash(into: &hasher)
oldValue.hash(into: &hasher)
case .dynamicString(let generator): generator().hash(into: &hasher)
case .userDefaults(_, let key): key.hash(into: &hasher)
case .settingBool(let key): key.hash(into: &hasher)
}
}
public static func == (lhs: DataSource, rhs: DataSource) -> Bool {
switch (lhs, rhs) {
case (.boolValue(let lhsValue), .boolValue(let rhsValue)):
return (lhsValue == rhsValue)
case (.boolValue(let lhsKey, let lhsValue, let lhsOldValue), .boolValue(let rhsKey, let rhsValue, let rhsOldValue)):
return (
lhsKey == rhsKey &&
lhsValue == rhsValue &&
lhsOldValue == rhsOldValue
)
case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)):
return (lhsGenerator() == rhsGenerator())
case (.userDefaults(_, let lhsKey), .userDefaults(_, let rhsKey)):
return (lhsKey == rhsKey)
case (.settingBool(let lhsKey), .settingBool(let rhsKey)):
return (lhsKey == rhsKey)
default: return false
}
}

View File

@ -277,7 +277,8 @@ extension SessionCell {
public func update(
with accessory: Accessory?,
tintColor: ThemeValue,
isEnabled: Bool
isEnabled: Bool,
isManualReload: Bool
) {
guard let accessory: Accessory = accessory else { return }
@ -356,10 +357,15 @@ extension SessionCell {
fixedWidthConstraint.isActive = true
toggleSwitchConstraints.forEach { $0.isActive = true }
let newValue: Bool = dataSource.currentBoolValue
if newValue != toggleSwitch.isOn {
toggleSwitch.setOn(newValue, animated: true)
if !isManualReload {
toggleSwitch.setOn(dataSource.oldBoolValue, animated: false)
// Dispatch so the cell reload doesn't conflict with the setting change animation
if dataSource.oldBoolValue != dataSource.currentBoolValue {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak toggleSwitch] in
toggleSwitch?.setOn(dataSource.currentBoolValue, animated: true)
}
}
}
case .dropDown(let dataSource, let accessibility):

View File

@ -313,7 +313,7 @@ public class SessionCell: UITableViewCell {
botSeparator.isHidden = true
}
public func update<ID: Hashable & Differentiable>(with info: Info<ID>) {
public func update<ID: Hashable & Differentiable>(with info: Info<ID>, isManualReload: Bool = false) {
interactionMode = (info.title?.interaction ?? .none)
shouldHighlightTitle = (info.title?.interaction != .copy)
titleExtraView = info.title?.extraViewGenerator?()
@ -332,7 +332,8 @@ public class SessionCell: UITableViewCell {
leftAccessoryView.update(
with: info.leftAccessory,
tintColor: info.styling.tintColor,
isEnabled: info.isEnabled
isEnabled: info.isEnabled,
isManualReload: isManualReload
)
titleStackView.isHidden = (info.title == nil && info.subtitle == nil)
titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy)
@ -356,7 +357,8 @@ public class SessionCell: UITableViewCell {
rightAccessoryView.update(
with: info.rightAccessory,
tintColor: info.styling.tintColor,
isEnabled: info.isEnabled
isEnabled: info.isEnabled,
isManualReload: isManualReload
)
contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading)

View File

@ -5,6 +5,9 @@ import SessionUtilitiesKit
public extension Date {
var formattedForDisplay: String {
// If we don't have a date then
guard self.timeIntervalSince1970 > 0 else { return "" }
let dateNow: Date = Date()
guard Calendar.current.isDate(self, equalTo: dateNow, toGranularity: .year) else {

View File

@ -100,7 +100,8 @@ enum MockDataGenerator {
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
.joined(),
lastNameUpdate: Date().timeIntervalSince1970,
lastProfilePictureUpdate: Date().timeIntervalSince1970
lastProfilePictureUpdate: Date().timeIntervalSince1970,
lastBlocksCommunityMessageRequests: 0
)
.saved(db)
@ -181,7 +182,8 @@ enum MockDataGenerator {
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined(),
lastNameUpdate: Date().timeIntervalSince1970,
lastProfilePictureUpdate: Date().timeIntervalSince1970
lastProfilePictureUpdate: Date().timeIntervalSince1970,
lastBlocksCommunityMessageRequests: 0
)
.saved(db)
@ -311,7 +313,8 @@ enum MockDataGenerator {
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined(),
lastNameUpdate: Date().timeIntervalSince1970,
lastProfilePictureUpdate: Date().timeIntervalSince1970
lastProfilePictureUpdate: Date().timeIntervalSince1970,
lastBlocksCommunityMessageRequests: 0
)
.saved(db)

View File

@ -31,7 +31,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API
_011_AddPendingReadReceipts.self,
_012_AddFTSIfNeeded.self,
_013_SessionUtilChanges.self,
_014_GenerateInitialUserConfigDumps.self
_014_GenerateInitialUserConfigDumps.self,
_015_BlockCommunityMessageRequests.self
]
]
)

View File

@ -422,7 +422,8 @@ enum _003_YDBToGRDBMigration: Migration {
profilePictureUrl: legacyContact.profilePictureURL,
profilePictureFileName: legacyContact.profilePictureFileName,
profileEncryptionKey: legacyContact.profileEncryptionKey?.keyData,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
lastBlocksCommunityMessageRequests: 0
).migrationSafeInsert(db)
/// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they
@ -645,7 +646,8 @@ enum _003_YDBToGRDBMigration: Migration {
id: profileId,
name: profileId,
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
lastBlocksCommunityMessageRequests: 0
).migrationSafeSave(db)
}
@ -1059,7 +1061,8 @@ enum _003_YDBToGRDBMigration: Migration {
id: quotedMessage.authorId,
name: quotedMessage.authorId,
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
lastBlocksCommunityMessageRequests: 0
).migrationSafeSave(db)
}

View File

@ -9,7 +9,7 @@ enum _005_FixDeletedMessageReadState: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "FixDeletedMessageReadState"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
_ = try Interaction

View File

@ -10,7 +10,7 @@ enum _006_FixHiddenModAdminSupport: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "FixHiddenModAdminSupport"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
try db.alter(table: GroupMember.self) { t in

View File

@ -9,7 +9,7 @@ enum _007_HomeQueryOptimisationIndexes: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "HomeQueryOptimisationIndexes"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
try db.create(

View File

@ -9,7 +9,7 @@ enum _008_EmojiReacts: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "EmojiReacts"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
try db.create(table: Reaction.self) { t in

View File

@ -8,7 +8,7 @@ enum _009_OpenGroupPermission: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "OpenGroupPermission"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: GRDB.Database) throws {
try db.alter(table: OpenGroup.self) { t in

View File

@ -10,7 +10,7 @@ enum _011_AddPendingReadReceipts: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddPendingReadReceipts"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
try db.create(table: PendingReadReceipt.self) { t in

View File

@ -9,7 +9,7 @@ enum _012_AddFTSIfNeeded: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddFTSIfNeeded"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
// Fix an issue that the fullTextSearchTable was dropped unintentionally and global search won't work.

View File

@ -0,0 +1,44 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
/// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests
enum _015_BlockCommunityMessageRequests: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "BlockCommunityMessageRequests"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01
static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded]
static func migrate(_ db: Database) throws {
// Add the new 'Profile' properties
try db.alter(table: Profile.self) { t in
t.add(.blocksCommunityMessageRequests, .boolean)
t.add(.lastBlocksCommunityMessageRequests, .integer)
.notNull()
.defaults(to: 0)
}
// If the user exists and the 'checkForCommunityMessageRequests' hasn't already been set then default it to "false"
if
Identity.userExists(db),
(try Setting.exists(db, id: Setting.BoolKey.checkForCommunityMessageRequests.rawValue)) == false
{
let rawBlindedMessageRequestValue: Int32 = try SessionUtil
.config(for: .userProfile, publicKey: getUserHexEncodedPublicKey(db))
.wrappedValue
.map { conf -> Int32 in try SessionUtil.rawBlindedMessageRequestValue(in: conf) }
.defaulting(to: -1)
// Use the value in the config if we happen to have one, otherwise use the default
db[.checkForCommunityMessageRequests] = (rawBlindedMessageRequestValue < 0 ?
true :
(rawBlindedMessageRequestValue > 0)
)
}
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}
}

View File

@ -27,6 +27,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
case profilePictureFileName
case profileEncryptionKey
case lastProfilePictureUpdate
case blocksCommunityMessageRequests
case lastBlocksCommunityMessageRequests
}
/// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant)
@ -53,6 +56,12 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
/// The timestamp (in seconds since epoch) that the profile picture was last updated
public let lastProfilePictureUpdate: TimeInterval
/// A flag indicating whether this profile has reported that it blocks community message requests
public let blocksCommunityMessageRequests: Bool?
/// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated
public let lastBlocksCommunityMessageRequests: TimeInterval
// MARK: - Initialization
public init(
@ -63,7 +72,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
profilePictureUrl: String? = nil,
profilePictureFileName: String? = nil,
profileEncryptionKey: Data? = nil,
lastProfilePictureUpdate: TimeInterval
lastProfilePictureUpdate: TimeInterval,
blocksCommunityMessageRequests: Bool? = nil,
lastBlocksCommunityMessageRequests: TimeInterval
) {
self.id = id
self.name = name
@ -73,6 +84,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
self.profilePictureFileName = profilePictureFileName
self.profileEncryptionKey = profileEncryptionKey
self.lastProfilePictureUpdate = lastProfilePictureUpdate
self.blocksCommunityMessageRequests = blocksCommunityMessageRequests
self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests
}
// MARK: - Description
@ -114,7 +127,9 @@ public extension Profile {
profilePictureUrl: profilePictureUrl,
profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName),
profileEncryptionKey: profileKey,
lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate)
lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate),
blocksCommunityMessageRequests: try? container.decode(Bool.self, forKey: .blocksCommunityMessageRequests),
lastBlocksCommunityMessageRequests: try container.decode(TimeInterval.self, forKey: .lastBlocksCommunityMessageRequests)
)
}
@ -129,6 +144,8 @@ public extension Profile {
try container.encodeIfPresent(profilePictureFileName, forKey: .profilePictureFileName)
try container.encodeIfPresent(profileEncryptionKey, forKey: .profileEncryptionKey)
try container.encode(lastProfilePictureUpdate, forKey: .lastProfilePictureUpdate)
try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests)
try container.encode(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests)
}
}
@ -156,7 +173,9 @@ public extension Profile {
profilePictureUrl: profilePictureUrl,
profilePictureFileName: nil,
profileEncryptionKey: profileKey,
lastProfilePictureUpdate: sentTimestamp
lastProfilePictureUpdate: sentTimestamp,
blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil),
lastBlocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? sentTimestamp : 0)
)
}
@ -242,7 +261,9 @@ public extension Profile {
profilePictureUrl: nil,
profilePictureFileName: nil,
profileEncryptionKey: nil,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
blocksCommunityMessageRequests: nil,
lastBlocksCommunityMessageRequests: 0
)
}

View File

@ -10,15 +10,22 @@ public extension VisibleMessage {
public let displayName: String?
public let profileKey: Data?
public let profilePictureUrl: String?
public let blocksCommunityMessageRequests: Bool?
// MARK: - Initialization
internal init(displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil) {
internal init(
displayName: String,
profileKey: Data? = nil,
profilePictureUrl: String? = nil,
blocksCommunityMessageRequests: Bool? = nil
) {
let hasUrlAndKey: Bool = (profileKey != nil && profilePictureUrl != nil)
self.displayName = displayName
self.profileKey = (hasUrlAndKey ? profileKey : nil)
self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil)
self.blocksCommunityMessageRequests = blocksCommunityMessageRequests
}
// MARK: - Proto Conversion
@ -32,7 +39,8 @@ public extension VisibleMessage {
return VMProfile(
displayName: displayName,
profileKey: proto.profileKey,
profilePictureUrl: profileProto.profilePicture
profilePictureUrl: profileProto.profilePicture,
blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil)
)
}
@ -45,6 +53,10 @@ public extension VisibleMessage {
let profileProto = SNProtoLokiProfile.builder()
profileProto.setDisplayName(displayName)
if let blocksCommunityMessageRequests: Bool = self.blocksCommunityMessageRequests {
dataMessageProto.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests)
}
if let profileKey = profileKey, let profilePictureUrl = profilePictureUrl {
dataMessageProto.setProfileKey(profileKey)
profileProto.setProfilePicture(profilePictureUrl)
@ -112,10 +124,14 @@ public extension VisibleMessage {
// MARK: - Conversion
extension VisibleMessage.VMProfile {
init(profile: Profile) {
init(
profile: Profile,
blocksCommunityMessageRequests: Bool?
) {
self.displayName = profile.name
self.profileKey = profile.profileEncryptionKey
self.profilePictureUrl = profile.profilePictureUrl
self.blocksCommunityMessageRequests = blocksCommunityMessageRequests
}
}

View File

@ -109,10 +109,12 @@ public enum OpenGroupAPI {
// The 'inbox' and 'outbox' only work with blinded keys so don't bother polling them if not blinded
!capabilities.contains(.blind) ? [] :
[
// Inbox
(lastInboxMessageId == 0 ?
try preparedInbox(db, on: server, using: dependencies) :
try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies)
// Inbox (only check the inbox if the user want's community message requests)
(!db[.checkForCommunityMessageRequests] ? nil :
(lastInboxMessageId == 0 ?
try preparedInbox(db, on: server, using: dependencies) :
try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies)
)
),
// Outbox
@ -120,7 +122,7 @@ public enum OpenGroupAPI {
try preparedOutbox(db, on: server, using: dependencies) :
try preparedOutboxSince(db, id: lastOutboxMessageId, on: server, using: dependencies)
),
]
].compactMap { $0 }
)
)

View File

@ -2497,6 +2497,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
if let _value = syncTarget {
builder.setSyncTarget(_value)
}
if hasBlocksCommunityMessageRequests {
builder.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests)
}
return builder
}
@ -2570,6 +2573,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
proto.syncTarget = valueParam
}
@objc public func setBlocksCommunityMessageRequests(_ valueParam: Bool) {
proto.blocksCommunityMessageRequests = valueParam
}
@objc public func build() throws -> SNProtoDataMessage {
return try SNProtoDataMessage.parseProto(proto)
}
@ -2646,6 +2653,13 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr
return proto.hasSyncTarget
}
@objc public var blocksCommunityMessageRequests: Bool {
return proto.blocksCommunityMessageRequests
}
@objc public var hasBlocksCommunityMessageRequests: Bool {
return proto.hasBlocksCommunityMessageRequests
}
private init(proto: SessionProtos_DataMessage,
attachments: [SNProtoAttachmentPointer],
quote: SNProtoDataMessageQuote?,

View File

@ -600,7 +600,7 @@ struct SessionProtos_DataMessage {
set {_uniqueStorage()._attachments = newValue}
}
/// optional GroupContext group = 3; // No longer used
/// optional GroupContext group = 3; // No longer used
var flags: UInt32 {
get {return _storage._flags ?? 0}
set {_uniqueStorage()._flags = newValue}
@ -696,6 +696,15 @@ struct SessionProtos_DataMessage {
/// Clears the value of `syncTarget`. Subsequent reads from it will return its default value.
mutating func clearSyncTarget() {_uniqueStorage()._syncTarget = nil}
var blocksCommunityMessageRequests: Bool {
get {return _storage._blocksCommunityMessageRequests ?? false}
set {_uniqueStorage()._blocksCommunityMessageRequests = newValue}
}
/// Returns true if `blocksCommunityMessageRequests` has been explicitly set.
var hasBlocksCommunityMessageRequests: Bool {return _storage._blocksCommunityMessageRequests != nil}
/// Clears the value of `blocksCommunityMessageRequests`. Subsequent reads from it will return its default value.
mutating func clearBlocksCommunityMessageRequests() {_uniqueStorage()._blocksCommunityMessageRequests = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
enum Flags: SwiftProtobuf.Enum {
@ -1665,6 +1674,43 @@ extension SessionProtos_SharedConfigMessage.Kind: CaseIterable {
#endif // swift(>=4.2)
#if swift(>=5.5) && canImport(_Concurrency)
extension SessionProtos_Envelope: @unchecked Sendable {}
extension SessionProtos_Envelope.TypeEnum: @unchecked Sendable {}
extension SessionProtos_TypingMessage: @unchecked Sendable {}
extension SessionProtos_TypingMessage.Action: @unchecked Sendable {}
extension SessionProtos_UnsendRequest: @unchecked Sendable {}
extension SessionProtos_MessageRequestResponse: @unchecked Sendable {}
extension SessionProtos_Content: @unchecked Sendable {}
extension SessionProtos_CallMessage: @unchecked Sendable {}
extension SessionProtos_CallMessage.TypeEnum: @unchecked Sendable {}
extension SessionProtos_KeyPair: @unchecked Sendable {}
extension SessionProtos_DataExtractionNotification: @unchecked Sendable {}
extension SessionProtos_DataExtractionNotification.TypeEnum: @unchecked Sendable {}
extension SessionProtos_LokiProfile: @unchecked Sendable {}
extension SessionProtos_DataMessage: @unchecked Sendable {}
extension SessionProtos_DataMessage.Flags: @unchecked Sendable {}
extension SessionProtos_DataMessage.Quote: @unchecked Sendable {}
extension SessionProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {}
extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: @unchecked Sendable {}
extension SessionProtos_DataMessage.Preview: @unchecked Sendable {}
extension SessionProtos_DataMessage.Reaction: @unchecked Sendable {}
extension SessionProtos_DataMessage.Reaction.Action: @unchecked Sendable {}
extension SessionProtos_DataMessage.OpenGroupInvitation: @unchecked Sendable {}
extension SessionProtos_DataMessage.ClosedGroupControlMessage: @unchecked Sendable {}
extension SessionProtos_DataMessage.ClosedGroupControlMessage.TypeEnum: @unchecked Sendable {}
extension SessionProtos_DataMessage.ClosedGroupControlMessage.KeyPairWrapper: @unchecked Sendable {}
extension SessionProtos_ConfigurationMessage: @unchecked Sendable {}
extension SessionProtos_ConfigurationMessage.ClosedGroup: @unchecked Sendable {}
extension SessionProtos_ConfigurationMessage.Contact: @unchecked Sendable {}
extension SessionProtos_ReceiptMessage: @unchecked Sendable {}
extension SessionProtos_ReceiptMessage.TypeEnum: @unchecked Sendable {}
extension SessionProtos_AttachmentPointer: @unchecked Sendable {}
extension SessionProtos_AttachmentPointer.Flags: @unchecked Sendable {}
extension SessionProtos_SharedConfigMessage: @unchecked Sendable {}
extension SessionProtos_SharedConfigMessage.Kind: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "SessionProtos"
@ -2288,6 +2334,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
102: .same(proto: "openGroupInvitation"),
104: .same(proto: "closedGroupControlMessage"),
105: .same(proto: "syncTarget"),
106: .same(proto: "blocksCommunityMessageRequests"),
]
fileprivate class _StorageClass {
@ -2304,6 +2351,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil
var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil
var _syncTarget: String? = nil
var _blocksCommunityMessageRequests: Bool? = nil
static let defaultInstance = _StorageClass()
@ -2323,6 +2371,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
_openGroupInvitation = source._openGroupInvitation
_closedGroupControlMessage = source._closedGroupControlMessage
_syncTarget = source._syncTarget
_blocksCommunityMessageRequests = source._blocksCommunityMessageRequests
}
}
@ -2366,6 +2415,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
case 102: try { try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation) }()
case 104: try { try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage) }()
case 105: try { try decoder.decodeSingularStringField(value: &_storage._syncTarget) }()
case 106: try { try decoder.decodeSingularBoolField(value: &_storage._blocksCommunityMessageRequests) }()
default: break
}
}
@ -2417,6 +2467,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
try { if let v = _storage._syncTarget {
try visitor.visitSingularStringField(value: v, fieldNumber: 105)
} }()
try { if let v = _storage._blocksCommunityMessageRequests {
try visitor.visitSingularBoolField(value: v, fieldNumber: 106)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -2439,6 +2492,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa
if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false}
if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false}
if _storage._syncTarget != rhs_storage._syncTarget {return false}
if _storage._blocksCommunityMessageRequests != rhs_storage._blocksCommunityMessageRequests {return false}
return true
}
if !storagesAreEqual {return false}

View File

@ -218,6 +218,13 @@ extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable {
#endif // swift(>=4.2)
#if swift(>=5.5) && canImport(_Concurrency)
extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "WebSocketProtos"

View File

@ -192,20 +192,21 @@ message DataMessage {
optional uint32 expirationTimer = 8;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
// optional GroupContext group = 3; // No longer used
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Preview preview = 10;
optional Reaction reaction = 11;
optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional string body = 1;
repeated AttachmentPointer attachments = 2;
// optional GroupContext group = 3; // No longer used
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Preview preview = 10;
optional Reaction reaction = 11;
optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
}
message ConfigurationMessage {

View File

@ -31,6 +31,7 @@ extension MessageReceiver {
db,
publicKey: sender,
name: profile.displayName,
blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests,
avatarUpdate: {
guard
let profilePictureUrl: String = profile.profilePictureUrl,

View File

@ -436,7 +436,8 @@ public final class MessageSender {
// Attach the user's profile
message.profile = VisibleMessage.VMProfile(
profile: Profile.fetchOrCreateCurrentUser()
profile: Profile.fetchOrCreateCurrentUser(db),
blocksCommunityMessageRequests: !db[.checkForCommunityMessageRequests]
)
if (message.profile?.displayName ?? "").isEmpty {

View File

@ -573,7 +573,8 @@ private extension SessionUtil {
count: ProfileManager.avatarAES256KeyByteLength
)
),
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000)
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
lastBlocksCommunityMessageRequests: 0
)
result[contactId] = ContactData(

View File

@ -210,6 +210,46 @@ internal extension SessionUtil {
return updated
}
static func hasSetting(_ db: Database, forKey key: String) throws -> Bool {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
// Currently the only synced setting is 'checkForCommunityMessageRequests'
switch key {
case Setting.BoolKey.checkForCommunityMessageRequests.rawValue:
return try SessionUtil
.config(for: .userProfile, publicKey: userPublicKey)
.wrappedValue
.map { conf -> Bool in (try SessionUtil.rawBlindedMessageRequestValue(in: conf) >= 0) }
.defaulting(to: false)
default: return false
}
}
static func updatingSetting(_ db: Database, _ updated: Setting?) throws {
// Don't current support any nullable settings
guard let updatedSetting: Setting = updated else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
// Currently the only synced setting is 'checkForCommunityMessageRequests'
switch updatedSetting.id {
case Setting.BoolKey.checkForCommunityMessageRequests.rawValue:
try SessionUtil.performAndPushChange(
db,
for: .userProfile,
publicKey: userPublicKey
) { conf in
try SessionUtil.updateSettings(
checkForCommunityMessageRequests: updatedSetting.unsafeValue(as: Bool.self),
in: conf
)
}
default: break
}
}
static func kickFromConversationUIIfNeeded(removedThreadIds: [String]) {
guard !removedThreadIds.isEmpty else { return }

View File

@ -12,6 +12,10 @@ internal extension SessionUtil {
Profile.Columns.profileEncryptionKey
]
static let syncedSettings: [String] = [
Setting.BoolKey.checkForCommunityMessageRequests.rawValue
]
// MARK: - Incoming Changes
static func handleUserProfileUpdate(
@ -115,6 +119,17 @@ internal extension SessionUtil {
}
}
// Update settings if needed
let updatedAllowBlindedMessageRequests: Int32 = user_profile_get_blinded_msgreqs(conf)
let updatedAllowBlindedMessageRequestsBoolValue: Bool = (updatedAllowBlindedMessageRequests >= 1)
if
updatedAllowBlindedMessageRequests >= 0 &&
updatedAllowBlindedMessageRequestsBoolValue != db[.checkForCommunityMessageRequests]
{
db[.checkForCommunityMessageRequests] = updatedAllowBlindedMessageRequestsBoolValue
}
// Create a contact for the current user if needed (also force-approve the current user
// in case the account got into a weird state or restored directly from a migration)
let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey)
@ -159,4 +174,25 @@ internal extension SessionUtil {
user_profile_set_nts_priority(conf, priority)
}
static func updateSettings(
checkForCommunityMessageRequests: Bool? = nil,
in conf: UnsafeMutablePointer<config_object>?
) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject }
if let blindedMessageRequests: Bool = checkForCommunityMessageRequests {
user_profile_set_blinded_msgreqs(conf, (blindedMessageRequests ? 1 : 0))
}
}
}
// MARK: - Direct Values
extension SessionUtil {
static func rawBlindedMessageRequestValue(in conf: UnsafeMutablePointer<config_object>?) throws -> Int32 {
guard conf != nil else { throw SessionUtilError.nilConfigObject }
return user_profile_get_blinded_msgreqs(conf)
}
}

View File

@ -0,0 +1,58 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public extension Database {
func setAndUpdateConfig(_ key: Setting.BoolKey, to newValue: Bool) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
func setAndUpdateConfig(_ key: Setting.DoubleKey, to newValue: Double?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
func setAndUpdateConfig(_ key: Setting.IntKey, to newValue: Int?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
func setAndUpdateConfig(_ key: Setting.StringKey, to newValue: String?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
func setAndUpdateConfig<T: EnumIntSetting>(_ key: Setting.EnumKey, to newValue: T?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
func setAndUpdateConfig<T: EnumStringSetting>(_ key: Setting.EnumKey, to newValue: T?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
/// Value will be stored as a timestamp in seconds since 1970
func setAndUpdateConfig(_ key: Setting.DateKey, to newValue: Date?) throws {
try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue))
}
private func updateConfigIfNeeded(
_ db: Database,
key: String,
updatedSetting: Setting?
) throws {
// Before we do anything custom make sure the setting should trigger a change
guard SessionUtil.syncedSettings.contains(key) else { return }
defer {
// If we changed a column that requires a config update then we may as well automatically
// enqueue a new config sync job once the transaction completes (but only enqueue it once
// per transaction - doing it more than once is pointless)
let userPublicKey: String = getUserHexEncodedPublicKey(db)
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
}
}
try SessionUtil.updatingSetting(db, updatedSetting)
}
}

View File

@ -104,7 +104,11 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var canWrite: Bool {
switch threadVariant {
case .contact: return true
case .contact:
guard threadIsMessageRequest == true else { return true }
return (profile?.blocksCommunityMessageRequests != true)
case .legacyGroup, .group:
return (
currentUserIsClosedGroupMember == true &&

View File

@ -65,6 +65,9 @@ public extension Setting.BoolKey {
/// Controls whether concurrent audio messages should automatically be played after the one the user starts
/// playing finishes
static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages"
/// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint)
static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests"
}
public extension Setting.StringKey {

View File

@ -498,6 +498,7 @@ public struct ProfileManager {
_ db: Database,
publicKey: String,
name: String?,
blocksCommunityMessageRequests: Bool? = nil,
avatarUpdate: AvatarUpdate,
sentTimestamp: TimeInterval,
calledFromConfigHandling: Bool = false,
@ -515,6 +516,12 @@ public struct ProfileManager {
}
}
// Blocks community message requets flag
if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests {
profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests))
profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp))
}
// Profile picture & profile key
var avatarNeedsDownload: Bool = false
var targetAvatarUrl: String? = nil

View File

@ -285,6 +285,17 @@ class ConfigUserProfileSpec {
)
user_profile_set_pic(conf2, p2)
user_profile_set_nts_expiry(conf2, 86400)
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 0)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0))
user_profile_set_blinded_msgreqs(conf2, -1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
@ -364,6 +375,10 @@ class ConfigUserProfileSpec {
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
expect(user_profile_get_nts_priority(conf2)).to(equal(9))
expect(user_profile_get_nts_expiry(conf)).to(equal(86400))
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()

View File

@ -243,10 +243,12 @@ class OpenGroupAPISpec: QuickSpec {
}
}
// MARK: -- when blinded
context("when blinded") {
// MARK: -- when blinded and checking for message requests
context("when blinded and checking for message requests") {
beforeEach {
mockStorage.write { db in
db[.checkForCommunityMessageRequests] = true
_ = try Capability.deleteAll(db)
try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db)
try Capability(openGroupServer: "testserver", variant: .blind, isMissing: false).insert(db)
@ -339,6 +341,69 @@ class OpenGroupAPISpec: QuickSpec {
expect(preparedRequest?.batchEndpoints).to(contain(.outboxSince(id: 125)))
}
}
// MARK: -- when blinded and not checking for message requests
context("when blinded and not checking for message requests") {
beforeEach {
mockStorage.write { db in
db[.checkForCommunityMessageRequests] = false
_ = try Capability.deleteAll(db)
try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db)
try Capability(openGroupServer: "testserver", variant: .blind, isMissing: false).insert(db)
}
}
// MARK: ---- includes the inbox and outbox endpoints
it("does not include the inbox endpoint") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
hasPerformedInitialPoll: false,
timeSinceLastPoll: 0,
using: dependencies
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox))
}
// MARK: ---- does not retrieve recent inbox messages if there was no last message
it("does not retrieve recent inbox messages if there was no last message") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
hasPerformedInitialPoll: true,
timeSinceLastPoll: 0,
using: dependencies
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox))
}
// MARK: ---- does not retrieve inbox messages since the last message if there was one
it("does not retrieve inbox messages since the last message if there was one") {
mockStorage.write { db in
try OpenGroup
.updateAll(db, OpenGroup.Columns.inboxLatestMessageId.set(to: 124))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
hasPerformedInitialPoll: true,
timeSinceLastPoll: 0,
using: dependencies
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inboxSince(id: 124)))
}
}
}
// MARK: - when preparing a capabilities request

View File

@ -226,11 +226,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
}
func shareViewWasCompleted() {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
func shareViewWasCancelled() {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
func shareViewFailed(error: Error) {

View File

@ -6,6 +6,7 @@ import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit
@testable import Session

View File

@ -6,6 +6,7 @@ import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit
@testable import Session
@ -60,14 +61,16 @@ class ThreadSettingsViewModelSpec: QuickSpec {
id: "05\(TestConstants.publicKey)",
name: "TestMe",
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
lastBlocksCommunityMessageRequests: 0
).insert(db)
try Profile(
id: "TestId",
name: "TestUser",
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
lastProfilePictureUpdate: 0,
lastBlocksCommunityMessageRequests: 0
).insert(db)
}
viewModel = ThreadSettingsViewModel(

View File

@ -4,9 +4,9 @@ import Combine
import GRDB
import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit
@testable import Session

View File

@ -65,6 +65,23 @@ public extension Publisher {
return self.receive(on: scheduler, options: options)
.eraseToAnyPublisher()
}
func manualRefreshFrom(_ refreshTrigger: some Publisher<Void, Never>) -> AnyPublisher<Output, Failure> {
return Publishers
.CombineLatest(refreshTrigger.prepend(()).setFailureType(to: Failure.self), self)
.map { _, value in value }
.eraseToAnyPublisher()
}
func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
.compactMap { $0 }
.eraseToAnyPublisher()
}
func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
}
}
// MARK: - Convenience

View File

@ -15,6 +15,7 @@ public struct Setting: Codable, Identifiable, FetchableRecord, PersistableRecord
}
public var id: String { key }
public var rawValue: Data { value }
let key: String
let value: Data
@ -53,7 +54,7 @@ extension Setting {
self.value = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
}
fileprivate func value(as type: Bool.Type) -> Bool? {
public func unsafeValue(as type: Bool.Type) -> Bool? {
// Note: The 'assumingMemoryBound' is essentially going to try to convert
// the memory into the provided type so can result in invalid data being
// returned if the type is incorrect. But it does seem safer than the 'load'
@ -189,7 +190,7 @@ public extension Database {
subscript(key: Setting.BoolKey) -> Bool {
get {
// Default to false if it doesn't exist
(self[key.rawValue]?.value(as: Bool.self) ?? false)
(self[key.rawValue]?.unsafeValue(as: Bool.self) ?? false)
}
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
}
@ -245,4 +246,47 @@ public extension Database {
)
}
}
func setting(key: Setting.BoolKey, to newValue: Bool) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue)
self[key.rawValue] = result
return result
}
func setting(key: Setting.DoubleKey, to newValue: Double?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue)
self[key.rawValue] = result
return result
}
func setting(key: Setting.IntKey, to newValue: Int?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue)
self[key.rawValue] = result
return result
}
func setting(key: Setting.StringKey, to newValue: String?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue)
self[key.rawValue] = result
return result
}
func setting<T: EnumIntSetting>(key: Setting.EnumKey, to newValue: T?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue)
self[key.rawValue] = result
return result
}
func setting<T: EnumStringSetting>(key: Setting.EnumKey, to newValue: T?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue)
self[key.rawValue] = result
return result
}
/// Value will be stored as a timestamp in seconds since 1970
func setting(key: Setting.DateKey, to newValue: Date?) -> Setting? {
let result: Setting? = Setting(key: key.rawValue, value: newValue.map { $0.timeIntervalSince1970 })
self[key.rawValue] = result
return result
}
}

View File

@ -47,8 +47,10 @@ open class Storage {
fileprivate var dbWriter: DatabaseWriter?
internal var testDbWriter: DatabaseWriter? { dbWriter }
private var unprocessedMigrationRequirements: Atomic<[MigrationRequirement]> = Atomic(MigrationRequirement.allCases)
private var migrator: DatabaseMigrator?
private var migrationProgressUpdater: Atomic<((String, CGFloat) -> ())>?
private var migrationRequirementProcesser: Atomic<(Database?, MigrationRequirement) -> ()>?
// MARK: - Initialization
@ -77,6 +79,7 @@ open class Storage {
migrationTargets: (customMigrationTargets ?? []),
async: false,
onProgressUpdate: nil,
onMigrationRequirement: { _, _ in },
onComplete: { _, _ in }
)
return
@ -148,6 +151,7 @@ open class Storage {
migrationTargets: [MigratableTarget.Type],
async: Bool = true,
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
onMigrationRequirement: @escaping (Database?, MigrationRequirement) -> (),
onComplete: @escaping (Swift.Result<Void, Error>, Bool) -> ()
) {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
@ -232,13 +236,24 @@ open class Storage {
onProgressUpdate?(totalProgress, totalMinExpectedDuration)
}
})
self.migrationRequirementProcesser = Atomic(onMigrationRequirement)
// Store the logic to run when the migration completes
let migrationCompleted: (Swift.Result<Void, Error>) -> () = { [weak self] result in
// Process any unprocessed requirements which need to be processed before completion
// then clear out the state
self?.unprocessedMigrationRequirements.wrappedValue
.filter { $0.shouldProcessAtCompletionIfNotRequired }
.forEach { self?.migrationRequirementProcesser?.wrappedValue(nil, $0) }
self?.migrationsCompleted.mutate { $0 = true }
self?.migrationProgressUpdater = nil
self?.migrationRequirementProcesser = nil
SUKLegacy.clearLegacyDatabaseInstance()
// Reset in case there is a requirement on a migration which runs when returning from
// the background
self?.unprocessedMigrationRequirements.mutate { $0 = MigrationRequirement.allCases }
// Don't log anything in the case of a 'success' or if the database is suspended (the
// latter will happen if the user happens to return to the background too quickly on
// launch so is unnecessarily alarming, it also gets caught and logged separately by
@ -288,6 +303,22 @@ open class Storage {
}
}
public func willStartMigration(_ db: Database, _ migration: Migration.Type) {
let unprocessedRequirements: Set<MigrationRequirement> = migration.requirements.asSet()
.intersection(unprocessedMigrationRequirements.wrappedValue.asSet())
// No need to do anything if there are no unprocessed requirements
guard !unprocessedRequirements.isEmpty else { return }
// Process all of the requirements for this migration
unprocessedRequirements.forEach { migrationRequirementProcesser?.wrappedValue(db, $0) }
// Remove any processed requirements from the list (don't want to process them multiple times)
unprocessedMigrationRequirements.mutate {
$0 = Array($0.asSet().subtracting(migration.requirements.asSet()))
}
}
public static func update(
progress: CGFloat,
for migration: Migration.Type,

View File

@ -8,17 +8,21 @@ public protocol Migration {
static var identifier: String { get }
static var needsConfigSync: Bool { get }
static var minExpectedRunDuration: TimeInterval { get }
static var requirements: [MigrationRequirement] { get }
static func migrate(_ db: Database) throws
}
public extension Migration {
static var requirements: [MigrationRequirement] { [] }
static func loggedMigrate(
_ storage: Storage?,
targetIdentifier: TargetMigrations.Identifier
) -> ((_ db: Database) throws -> ()) {
return { (db: Database) in
SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))")
storage?.willStartMigration(db, self)
storage?.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) }
defer { storage?.internalCurrentlyRunningMigration.mutate { $0 = nil } }

View File

@ -0,0 +1,12 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public enum MigrationRequirement: CaseIterable {
case sessionUtilStateLoaded
var shouldProcessAtCompletionIfNotRequired: Bool {
switch self {
case .sessionUtilStateLoaded: return true
}
}
}

View File

@ -82,6 +82,20 @@ public enum AppSetup {
SNUIKit.self
],
onProgressUpdate: migrationProgressChanged,
onMigrationRequirement: { db, requirement in
switch requirement {
case .sessionUtilStateLoaded:
guard Identity.userExists(db) else { return }
// After the migrations have run but before the migration completion we load the
// SessionUtil state
SessionUtil.loadState(
db,
userPublicKey: getUserHexEncodedPublicKey(db),
ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey
)
}
},
onComplete: { result, needsConfigSync in
// After the migrations have run but before the migration completion we load the
// SessionUtil state and update the 'needsConfigSync' flag based on whether the
@ -93,6 +107,8 @@ public enum AppSetup {
)
}
// The 'needsConfigSync' flag should be based on whether either a migration or the
// configs need to be sync'ed
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))
// The 'if' is only there to prevent the "variable never read" warning from showing