Merge pull request #886 from mpretty-cyro/feature/blinded-message-request-setting
Community message request setting
This commit is contained in:
commit
968f50f2fc
|
@ -1 +1 @@
|
|||
Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2
|
||||
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>>] {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -31,6 +31,7 @@ extension MessageReceiver {
|
|||
db,
|
||||
publicKey: sender,
|
||||
name: profile.displayName,
|
||||
blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests,
|
||||
avatarUpdate: {
|
||||
guard
|
||||
let profilePictureUrl: String = profile.profilePictureUrl,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -573,7 +573,8 @@ private extension SessionUtil {
|
|||
count: ProfileManager.avatarAES256KeyByteLength
|
||||
)
|
||||
),
|
||||
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000)
|
||||
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
|
||||
lastBlocksCommunityMessageRequests: 0
|
||||
)
|
||||
|
||||
result[contactId] = ContactData(
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 &&
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import Quick
|
|||
import Nimble
|
||||
import SessionUIKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@testable import Session
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -4,9 +4,9 @@ import Combine
|
|||
import GRDB
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
import SessionUIKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@testable import Session
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } }
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue