Added a setting to control community message request polling

Added logic to broadcast the community message request acceptance to SOGS so we can communicate it to message request senders
Fixed an issue where database setting changes wouldn't trigger a live update on a settings screen
Fixed an issue where some setting toggles wouldn't animate the state change
Fixed a rarw force-unwrap crash
This commit is contained in:
Morgan Pretty 2023-08-11 18:02:06 +10:00
parent 4d098914b2
commit d863004e6d
65 changed files with 766 additions and 141 deletions

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

View File

@ -531,6 +531,8 @@
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 */; };
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 */; };
@ -1689,6 +1691,8 @@
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>"; };
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>"; };
@ -3618,6 +3622,7 @@
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */,
FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */,
FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */,
FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -3763,6 +3768,7 @@
FD2B4B022949886900AB4848 /* Database */ = {
isa = PBXGroup;
children = (
FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */,
FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */,
);
path = Database;
@ -5914,12 +5920,14 @@
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */,
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */,
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */,
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
FD245C56285065EA00B966DD /* SNProto.swift in Sources */,
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */,
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */,
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,

View File

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

View File

@ -178,6 +178,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()
@ -235,6 +236,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(
@ -577,7 +580,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: (
(
@ -615,7 +621,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: (
(
@ -661,7 +670,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(),
rightAccessory: .toggle(
.boolValue(threadViewModel.threadIsBlocked == true)
.boolValue(
threadViewModel.threadIsBlocked == true,
oldValue: (originalState.threadIsBlocked == true)
)
),
accessibility: Accessibility(
identifier: "\(ThreadSettingsViewModel.self).block",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API
(Features.useSharedUtilForUserConfig(db) ?
_014_GenerateInitialUserConfigDumps.self :
(nil as Migration.Type?)
)
),
_015_BlockCommunityMessageRequests.self
].compactMap { $0 }
]
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
// 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 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
{
db[.checkForCommunityMessageRequests] = true
}
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,10 @@ internal extension SessionUtil {
Profile.Columns.profileEncryptionKey
]
static let syncedSettings: [String] = [
Setting.BoolKey.checkForCommunityMessageRequests.rawValue
]
// MARK: - Incoming Changes
static func handleUserProfileUpdate(
@ -115,6 +119,17 @@ internal extension SessionUtil {
}
}
// Update settings if needed
let updatedAllowBlindedMessageRequests: Int32 = user_profile_get_blinded_msgreqs(conf)
let updatedAllowBlindedMessageRequestsBoolValue: Bool = (updatedAllowBlindedMessageRequests >= 1)
if
updatedAllowBlindedMessageRequests >= 0 &&
updatedAllowBlindedMessageRequestsBoolValue != db[.checkForCommunityMessageRequests]
{
db[.checkForCommunityMessageRequests] = updatedAllowBlindedMessageRequestsBoolValue
}
// Create a contact for the current user if needed (also force-approve the current user
// in case the account got into a weird state or restored directly from a migration)
let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey)
@ -159,4 +174,15 @@ 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))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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