Merge remote-tracking branch 'upstream/dev' into feature/updated-user-config-handling

# Conflicts:
#	Session/Closed Groups/NewClosedGroupVC.swift
#	Session/Conversations/ConversationVC+Interaction.swift
#	Session/Conversations/ConversationVC.swift
#	Session/Conversations/ConversationViewModel.swift
#	Session/Conversations/Settings/ThreadSettingsViewModel.swift
#	Session/Home/GlobalSearch/GlobalSearchViewController.swift
#	Session/Home/HomeVC.swift
#	Session/Home/New Conversation/NewDMVC.swift
#	Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift
#	Session/Meta/Translations/de.lproj/Localizable.strings
#	Session/Meta/Translations/en.lproj/Localizable.strings
#	Session/Meta/Translations/es.lproj/Localizable.strings
#	Session/Meta/Translations/fa.lproj/Localizable.strings
#	Session/Meta/Translations/fi.lproj/Localizable.strings
#	Session/Meta/Translations/fr.lproj/Localizable.strings
#	Session/Meta/Translations/hi.lproj/Localizable.strings
#	Session/Meta/Translations/hr.lproj/Localizable.strings
#	Session/Meta/Translations/id-ID.lproj/Localizable.strings
#	Session/Meta/Translations/it.lproj/Localizable.strings
#	Session/Meta/Translations/ja.lproj/Localizable.strings
#	Session/Meta/Translations/nl.lproj/Localizable.strings
#	Session/Meta/Translations/pl.lproj/Localizable.strings
#	Session/Meta/Translations/pt_BR.lproj/Localizable.strings
#	Session/Meta/Translations/ru.lproj/Localizable.strings
#	Session/Meta/Translations/si.lproj/Localizable.strings
#	Session/Meta/Translations/sk.lproj/Localizable.strings
#	Session/Meta/Translations/sv.lproj/Localizable.strings
#	Session/Meta/Translations/th.lproj/Localizable.strings
#	Session/Meta/Translations/vi-VN.lproj/Localizable.strings
#	Session/Meta/Translations/zh-Hant.lproj/Localizable.strings
#	Session/Meta/Translations/zh_CN.lproj/Localizable.strings
#	Session/Settings/BlockedContactsViewController.swift
#	Session/Settings/NukeDataModal.swift
#	Session/Settings/SettingsViewModel.swift
#	SessionMessagingKit/Shared Models/SessionThreadViewModel.swift
#	SessionUIKit/Components/ConfirmationModal.swift
This commit is contained in:
Morgan Pretty 2023-05-18 17:34:25 +10:00
commit 534343f8b0
86 changed files with 1342 additions and 797 deletions

View File

@ -1,40 +0,0 @@
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.
Before we begin, please note that this tracker is only for issues. It is not for questions, comments, or feature requests.
If you are looking for support, please email team@oxen.io.
Let's begin with a checklist: Replace the empty checkboxes [ ] below with checked ones [x] accordingly. -->
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-ios/blob/master/CODE_OF_CONDUCT.md).
- [ ] I have searched open and closed issues for duplicates
- [ ] I am submitting a bug report for existing functionality that does not work as intended
- [ ] This isn't a feature request or a discussion topic
----------------------------------------
### Bug description
Describe here the issue that you are experiencing.
### Steps to reproduce
- using hyphens as bullet points
- list the steps
- that reproduce the bug
#### Actual result:
Describe here what happens after you run the steps above (i.e. the buggy behaviour).
#### Expected result:
Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour).
### Screenshots
<!-- you can drag and drop images below -->
### Device info
<!-- replace the examples with your info -->
**Device**: iDevice X
**iOS version**: X.Y.Z
**Session version:** X.Y.Z

View File

@ -1,34 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Code of conduct**
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-ios/blob/master/CODE_OF_CONDUCT.md).
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior.
**Screenshots or logs**
If applicable, add screenshots or logs to help explain your problem.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone 6]
- OS: [e.g. iOS 8.1]
- Version of Session or latest commit hash
**Additional context**
Add any other context about the problem here.

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: 🐞 Bug Report
description: Create a report to help us improve
title: "[BUG] <title>"
labels: [bug]
body:
- type: checkboxes
attributes:
label: Code of conduct
description: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-ios/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-ios/blob/master/CODE_OF_CONDUCT.md)
required: true
- type: checkboxes
attributes:
label: Self-training on how to write a bug report
description: High quality bug report can help the team save time and improve the chance of getting fixed. Please read [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report) before submitting your issue.
options:
- label: I have learned [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report)
required: true
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: input
attributes:
label: iOS Version
description: What version of iOS are you running?
placeholder: ex. iOS 16.0
validations:
required: false
- type: input
attributes:
label: Session Version
description: What version of Session are you running? (This can be found at the bottom of the app settings)
placeholder: ex. 2.0.0 (375)
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Add any other context about the problem here.
Tip: You can attach screenshots or log files to help explain your problem by clicking this area to highlight it and then dragging files in.
validations:
required: false

View File

@ -0,0 +1,26 @@
name: 🚀 Feature request
description: Suggest an idea for Session
title: '[Feature] <title>'
labels: [feature-request]
body:
- type: checkboxes
attributes:
label: Is there an existing request for feature?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: What feature would you like?
description: |
A clear and concise description of the feature you would like added to Session
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Add any other context or screenshots about the feature request here
validations:
required: false

View File

@ -12,7 +12,7 @@ PODS:
- DifferenceKit/Core (1.3.0)
- DifferenceKit/UIKitExtension (1.3.0):
- DifferenceKit/Core
- GRDB.swift/SQLCipher (6.10.1):
- GRDB.swift/SQLCipher (6.13.0):
- SQLCipher (>= 3.4.2)
- libwebp (1.2.1):
- libwebp/demux (= 1.2.1)
@ -193,7 +193,7 @@ SPEC CHECKSUMS:
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7
GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667

View File

@ -6365,7 +6365,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6389,7 +6389,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6437,7 +6437,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6466,7 +6466,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6502,7 +6502,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6525,7 +6525,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6576,7 +6576,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6604,7 +6604,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7484,7 +7484,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7522,7 +7522,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7555,7 +7555,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 404;
CURRENT_PROJECT_VERSION = 406;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7593,7 +7593,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.13;
MARKETING_VERSION = 2.2.14;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -511,7 +511,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
targetView: self.view,
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -306,7 +306,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text

View File

@ -69,7 +69,7 @@ extension ConversationVC:
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_call_permission_request_title".localized(),
explanation: "modal_call_permission_request_explanation".localized(),
body: .text("modal_call_permission_request_explanation".localized()),
confirmTitle: "vc_settings_title".localized(),
confirmAccessibility: Accessibility(identifier: "Settings"),
dismissOnConfirm: false // Custom dismissal logic
@ -132,11 +132,13 @@ extension ConversationVC:
format: "modal_blocked_title".localized(),
self.viewModel.threadData.displayName
),
attributedExplanation: NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: self.viewModel.threadData.displayName)
),
body: .attributedText(
NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: self.viewModel.threadData.displayName)
)
),
confirmTitle: "modal_blocked_button_title".localized(),
confirmAccessibility: Accessibility(identifier: "Confirm block"),
cancelAccessibility: Accessibility(identifier: "Cancel block"),
@ -205,7 +207,7 @@ extension ConversationVC:
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "GIPHY_PERMISSION_TITLE".localized(),
explanation: "GIPHY_PERMISSION_MESSAGE".localized(),
body: .text("GIPHY_PERMISSION_MESSAGE".localized()),
confirmTitle: "continue_2".localized()
) { [weak self] _ in
Storage.shared.writeAsync(
@ -295,7 +297,7 @@ extension ConversationVC:
targetView: self?.view,
info: ConfirmationModal.Info(
title: "Session",
explanation: "An error occurred.",
body: .text("An error occurred."),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -312,7 +314,7 @@ extension ConversationVC:
targetView: self?.view,
info: ConfirmationModal.Info(
title: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE".localized(),
explanation: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized(),
body: .text("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -408,7 +410,7 @@ extension ConversationVC:
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_send_seed_title".localized(),
explanation: "modal_send_seed_explanation".localized(),
body: .text("modal_send_seed_explanation".localized()),
confirmTitle: "modal_send_seed_send_button_title".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
@ -540,7 +542,7 @@ extension ConversationVC:
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_send_seed_title".localized(),
explanation: "modal_send_seed_explanation".localized(),
body: .text("modal_send_seed_explanation".localized()),
confirmTitle: "modal_send_seed_send_button_title".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
@ -659,7 +661,7 @@ extension ConversationVC:
let linkPreviewModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_link_previews_title".localized(),
explanation: "modal_link_previews_explanation".localized(),
body: .text("modal_link_previews_explanation".localized()),
confirmTitle: "modal_link_previews_button_title".localized()
) { [weak self] _ in
Storage.shared.writeAsync { db in
@ -905,11 +907,13 @@ extension ConversationVC:
format: "modal_download_attachment_title".localized(),
cellViewModel.authorName
),
attributedExplanation: NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: cellViewModel.authorName)
),
body: .attributedText(
NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: cellViewModel.authorName)
)
),
confirmTitle: "modal_download_button_title".localized(),
confirmAccessibility: Accessibility(identifier: "Download media"),
cancelAccessibility: Accessibility(identifier: "Don't download media"),
@ -1577,11 +1581,13 @@ extension ConversationVC:
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Join \(finalName)?",
attributedExplanation: NSMutableAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: finalName)
),
body: .attributedText(
NSMutableAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: finalName)
)
),
confirmTitle: "JOIN_COMMUNITY_BUTTON_TITLE".localized(),
onConfirm: { modal in
guard let presentingViewController: UIViewController = modal.presentingViewController else {
@ -1620,7 +1626,7 @@ extension ConversationVC:
let errorModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "COMMUNITY_ERROR_GENERIC".localized(),
explanation: error.localizedDescription,
body: .text(error.localizedDescription),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -2091,7 +2097,7 @@ extension ConversationVC:
targetView: self.view,
info: ConfirmationModal.Info(
title: "Session",
explanation: "This will ban the selected user from this room. It won't ban them from other rooms.",
body: .text("This will ban the selected user from this room. It won't ban them from other rooms."),
confirmTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
@ -2122,7 +2128,7 @@ extension ConversationVC:
targetView: self?.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "context_menu_ban_user_error_alert_message".localized(),
body: .text("context_menu_ban_user_error_alert_message".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -2148,7 +2154,7 @@ extension ConversationVC:
targetView: self.view,
info: ConfirmationModal.Info(
title: "Session",
explanation: "This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.",
body: .text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there."),
confirmTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
@ -2179,7 +2185,7 @@ extension ConversationVC:
targetView: self?.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "context_menu_ban_user_error_alert_message".localized(),
body: .text("context_menu_ban_user_error_alert_message".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -2290,7 +2296,7 @@ extension ConversationVC:
targetView: self.view,
info: ConfirmationModal.Info(
title: "VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE".localized(),
explanation: "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized(),
body: .text("VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -2361,7 +2367,7 @@ extension ConversationVC:
targetView: self.view,
info: ConfirmationModal.Info(
title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(),
explanation: (attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage),
body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: onDismiss

View File

@ -125,7 +125,6 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
var scrollButtonBottomConstraint: NSLayoutConstraint?
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
var scrollButtonPendingMessageRequestInfoBottomConstraint: NSLayoutConstraint?
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
var messageRequestDescriptionLabelBottomConstraint: NSLayoutConstraint?
@ -171,7 +170,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}()
lazy var snInputView: InputView = InputView(
threadVariant: self.viewModel.threadData.threadVariant,
threadVariant: self.viewModel.initialThreadVariant,
delegate: self
)
@ -182,6 +181,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2)
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
result.set(.height, to: ConversationVC.unreadCountViewSize)
result.isHidden = true
return result
}()
@ -415,7 +415,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
emptyStateLabel.pin(.top, to: .top, of: view, withInset: Values.largeSpacing)
emptyStateLabel.pin(.leading, to: .leading, of: view, withInset: Values.veryLargeSpacing)
emptyStateLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.veryLargeSpacing)
messageRequestStackView.addArrangedSubview(messageRequestBlockButton)
messageRequestStackView.addArrangedSubview(messageRequestDescriptionContainerView)
messageRequestStackView.addArrangedSubview(messageRequestActionStackView)
@ -436,6 +436,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
messageRequestDescriptionLabel.pin(.trailing, to: .trailing, of: messageRequestDescriptionContainerView, withInset: -20)
self.messageRequestDescriptionLabelBottomConstraint = messageRequestDescriptionLabel.pin(.bottom, to: .bottom, of: messageRequestDescriptionContainerView, withInset: -20)
messageRequestActionStackView.pin(.top, to: .bottom, of: messageRequestDescriptionContainerView)
messageRequestDeleteButton.set(.width, to: .width, of: messageRequestAcceptButton)
messageRequestBackgroundView.pin(.top, to: .top, of: messageRequestStackView)
messageRequestBackgroundView.pin(.leading, to: .leading, of: view)
@ -564,7 +565,11 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges(didReturnFromBackground: true)
}
recoverInputView()
if !isShowingSearchUI && self.presentedViewController == nil {
@ -1441,7 +1446,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
targetView: self?.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(),
body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -53,29 +53,77 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// MARK: - Initialization
init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionInfo: Interaction.TimestampInfo?) {
// If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest
// unread interaction and start focused around that one
let targetInteractionInfo: Interaction.TimestampInfo? = {
if let focusedInteractionInfo: Interaction.TimestampInfo = focusedInteractionInfo {
return focusedInteractionInfo
}
typealias InitialData = (
targetInteractionInfo: Interaction.TimestampInfo?,
threadIsBlocked: Bool,
currentUserIsClosedGroupMember: Bool?,
openGroupPermissions: OpenGroup.Permissions?,
blindedKey: String?
)
let initialData: InitialData? = Storage.shared.read { db -> InitialData in
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
return Storage.shared.read { db in
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return try Interaction
// If we have a specified 'focusedInteractionInfo' then use that, otherwise retrieve the oldest
// unread interaction and start focused around that one
let targetInteractionInfo: Int64? = (focusedInteractionInfo != nil ? focusedInteractionInfo :
try Interaction
.select(.id, .timestampMs)
.filter(interaction[.wasRead] == false)
.filter(interaction[.threadId] == threadId)
.order(interaction[.timestampMs].asc)
.asRequest(of: Interaction.TimestampInfo.self)
.fetchOne(db)
}
}()
)
let threadIsBlocked: Bool= (threadVariant != .contact ? false :
try Contact
.filter(id: threadId)
.select(.isBlocked)
.asRequest(of: Bool.self)
.fetchOne(db)
.defaulting(to: false)
)
let currentUserIsClosedGroupMember: Bool? = (![.legacyGroup, .group].contains(threadVariant) ? nil :
try GroupMember
.filter(groupMember[.groupId] == threadId)
.filter(groupMember[.profileId] == getUserHexEncodedPublicKey(db))
.filter(groupMember[.role] == GroupMember.Role.standard)
.isNotEmpty(db)
)
let openGroupPermissions: OpenGroup.Permissions? = (threadVariant != .community ? nil :
try OpenGroup
.filter(id: threadId)
.select(.permissions)
.asRequest(of: OpenGroup.Permissions.self)
.fetchOne(db)
)
let blindedKey: String? = SessionThread.getUserHexEncodedBlindedKey(
db,
threadId: threadId,
threadVariant: threadVariant
)
return (
targetInteractionInfo,
threadIsBlocked,
currentUserIsClosedGroupMember,
openGroupPermissions,
blindedKey
)
}
self.threadId = threadId
self.initialThreadVariant = threadVariant
self.focusedInteractionInfo = targetInteractionInfo
self.threadData = SessionThreadViewModel(
threadId: threadId,
threadVariant: threadVariant,
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
threadIsBlocked: threadIsBlocked,
currentUserIsClosedGroupMember: initialData?.currentUserIsClosedGroupMember,
openGroupPermissions: initialData?.openGroupPermissions
).populatingCurrentUserBlindedKey(currentUserBlindedPublicKeyForThisThread: initialData?.blindedKey)
self.pagedDataObserver = nil
// Note: Since this references self we need to finish initializing before setting it, we
@ -93,7 +141,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// Run the initial query on a background thread so we don't block the push transition
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
// If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query
// If we don't have a `initialFocusedInfo` then default to `.pageBefore` (it'll query
// from a `0` offset)
guard let initialFocusedInfo: Interaction.TimestampInfo = targetInteractionInfo else {
self?.pagedDataObserver?.load(.pageBefore)
@ -107,40 +155,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// MARK: - Thread Data
/// This value is the current state of the view
public private(set) lazy var threadData: SessionThreadViewModel = SessionThreadViewModel(
threadId: self.threadId,
threadVariant: self.initialThreadVariant,
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
threadIsBlocked: (self.initialThreadVariant != .contact ? false :
Storage.shared.read { db in
try Contact
.filter(id: self.threadId)
.select(.isBlocked)
.asRequest(of: Bool.self)
.fetchOne(db)
.defaulting(to: false)
}
),
currentUserIsClosedGroupMember: (![.legacyGroup, .group].contains(self.initialThreadVariant) ? nil :
Storage.shared.read { db in
GroupMember
.filter(GroupMember.Columns.groupId == self.threadId)
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
.isNotEmpty(db)
}
),
openGroupPermissions: (self.initialThreadVariant != .community ? nil :
Storage.shared.read { db in
try OpenGroup
.filter(id: threadId)
.select(.permissions)
.asRequest(of: OpenGroup.Permissions.self)
.fetchOne(db)
}
)
)
.populatingCurrentUserBlindedKey()
public private(set) var threadData: SessionThreadViewModel
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance

View File

@ -39,8 +39,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
set { inputTextView.selectedRange = newValue }
}
var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder }
var enabledMessageTypes: MessageInputTypes = .all {
didSet {
setEnabledMessageTypes(enabledMessageTypes, message: nil)
@ -448,10 +446,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
override func resignFirstResponder() -> Bool {
inputTextView.resignFirstResponder()
}
func inputTextViewBecomeFirstResponder() {
inputTextView.becomeFirstResponder()
}
func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
// Not relevant in this case

View File

@ -3,6 +3,7 @@
import UIKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
final class QuoteView: UIView {
static let thumbnailSize: CGFloat = 48
@ -237,17 +238,27 @@ final class QuoteView: UIView {
.compactMap { $0 }
.asSet()
.contains(authorId)
let authorLabel = UILabel()
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
authorLabel.text = (isCurrentUser ?
"MEDIA_GALLERY_SENDER_NAME_YOU".localized() :
Profile.displayName(
authorLabel.text = {
guard !isCurrentUser else { return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() }
guard body != nil else {
// When we can't find the quoted message we want to hide the author label
return Profile.displayNameNoFallback(
id: authorId,
threadVariant: threadVariant
)
}
return Profile.displayName(
id: authorId,
threadVariant: threadVariant
)
)
}()
authorLabel.themeTextColor = targetThemeColor
authorLabel.lineBreakMode = .byTruncatingTail
authorLabel.isHidden = (authorLabel.text == nil)
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
authorLabel.set(.height, to: authorLabelSize.height)

View File

@ -3,6 +3,7 @@
import Foundation
import Combine
import GRDB
import YYImage
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
@ -503,7 +504,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
confirmationInfo: ConfirmationModal.Info(
title: "leave_group_confirmation_alert_title".localized(),
attributedExplanation: {
body: .attributedText({
if currentUserIsClosedGroupAdmin {
return NSAttributedString(string: "admin_group_leave_warning".localized())
}
@ -520,7 +521,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName)
)
return mutableAttributedString
}(),
}()),
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text
@ -674,9 +675,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
threadViewModel.displayName
)
}(),
explanation: (threadViewModel.threadIsBlocked == true ?
nil :
"BLOCK_USER_BEHAVIOR_EXPLANATION".localized()
body: (threadViewModel.threadIsBlocked == true ? .none :
.text("BLOCK_USER_BEHAVIOR_EXPLANATION".localized())
),
confirmTitle: (threadViewModel.threadIsBlocked == true ?
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
@ -813,17 +813,16 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
displayName
)
),
explanation: (oldBlockedState == false ?
body: (oldBlockedState == true ? .none : .text(
String(
format: "BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT".localized(),
displayName
) :
nil
)
),
accessibility: Accessibility(
identifier: "Test_name",
label: (oldBlockedState == false ? "User blocked" : "Confirm unblock")
),
)),
cancelTitle: "BUTTON_OK".localized(),
cancelAccessibility: Accessibility(identifier: "OK_BUTTON"),
cancelStyle: .alert_text

View File

@ -101,14 +101,17 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
setupNavigationBar()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchBar.becomeFirstResponder()
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
searchBar.resignFirstResponder()
UIView.performWithoutAnimation {
searchBar.resignFirstResponder()
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@ -148,10 +151,6 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
}
}
private func reloadTableData() {
tableView.reloadData()
}
// MARK: - Update Search Results
private func refreshSearchResults() {
@ -165,9 +164,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
let searchText = rawSearchText.stripped
guard searchText.count > 0 else {
guard searchText != (lastSearchText ?? "") else { return }
searchResultSet = defaultSearchResults
lastSearchText = nil
reloadTableData()
tableView.reloadData()
return
}
guard force || lastSearchText != searchText else { return }
@ -229,7 +230,7 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
.compactMap { $0 }
.flatMap { $0 }
self?.isLoading = false
self?.reloadTableData()
self?.tableView.reloadData()
self?.refreshTimer = nil
default: break
@ -310,18 +311,12 @@ extension GlobalSearchViewController {
return
}
if let presentedVC = self.presentedViewController {
presentedVC.dismiss(animated: false, completion: nil)
}
let viewControllers: [UIViewController] = (self.navigationController?
.viewControllers)
.defaulting(to: [])
.appending(
ConversationVC(threadId: threadId, threadVariant: threadVariant, focusedInteractionInfo: focusedInteractionInfo)
)
self.navigationController?.setViewControllers(viewControllers, animated: true)
let viewController: ConversationVC = ConversationVC(
threadId: threadId,
threadVariant: threadVariant,
focusedInteractionInfo: focusedInteractionInfo
)
self.navigationController?.pushViewController(viewController, animated: true)
}
// MARK: - UITableViewDataSource

View File

@ -315,7 +315,10 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges(didReturnFromBackground: true)
}
}
@objc func applicationDidResignActive(_ notification: Notification) {
@ -400,8 +403,18 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
// in from a frame of CGRect.zero)
guard hasLoadedInitialThreadData else {
hasLoadedInitialThreadData = true
UIView.performWithoutAnimation {
handleThreadUpdates(updatedData, changeset: changeset, initialLoad: true)
UIView.performWithoutAnimation { [weak self] in
// Hide the 'loading conversations' label (now that we have received conversation data)
self?.loadingConversationsLabel.isHidden = true
// Show the empty state if there is no data
self?.emptyStateView.isHidden = (
!updatedData.isEmpty &&
updatedData.contains(where: { !$0.elements.isEmpty })
)
self?.viewModel.updateThreadData(updatedData)
}
return
}
@ -656,7 +669,7 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
viewController: self
)
)
default: return nil
}
}

View File

@ -166,7 +166,10 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges(didReturnFromBackground: true)
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

View File

@ -225,12 +225,22 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
default: break
}
}
let message: String = {
if let messageOrNil: String = messageOrNil {
return messageOrNil
}
return (maybeSessionId?.prefix == .blinded ?
"DM_ERROR_DIRECT_BLINDED_ID".localized() :
"DM_ERROR_INVALID".localized()
)
}()
let modal: ConfirmationModal = ConfirmationModal(
targetView: self?.view,
info: ConfirmationModal.Info(
title: "ALERT_ERROR_TITLE".localized(),
explanation: (messageOrNil ?? "DM_ERROR_INVALID".localized()),
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -120,7 +120,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges()
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges()
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

View File

@ -370,7 +370,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
targetView: self?.view,
info: ConfirmationModal.Info(
title: "GIF_PICKER_FAILURE_ALERT_TITLE".localized(),
explanation: error.localizedDescription,
body: .text(error.localizedDescription),
confirmTitle: CommonStrings.retryButton,
cancelTitle: CommonStrings.dismissButton,
cancelStyle: .alert_text,
@ -461,7 +461,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
targetView: self.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "GIF_PICKER_VIEW_MISSING_QUERY".localized(),
body: .text("GIF_PICKER_VIEW_MISSING_QUERY".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -3,6 +3,8 @@
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import SignalUtilitiesKit
extension MediaInfoVC {
final class MediaInfoView: UIView {

View File

@ -3,6 +3,7 @@
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
extension MediaInfoVC {
final class MediaPreviewView: UIView {

View File

@ -2,7 +2,9 @@
import UIKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate {
internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing

View File

@ -245,7 +245,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges()
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges()
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

View File

@ -176,7 +176,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges(didReturnFromBackground: true)
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

View File

@ -336,7 +336,7 @@ class PhotoCaptureViewController: OWSViewController {
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: error.localizedDescription,
body: .text(error.localizedDescription),
cancelTitle: CommonStrings.dismissButton,
cancelStyle: .alert_text,
afterClosed: { [weak self] in self?.dismiss(animated: true) }

View File

@ -484,19 +484,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
guard CurrentAppContext().isMainApp else { return }
CurrentAppContext().setMainAppBadgeNumber(
Storage.shared
/// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database
/// read pools (up to a few seconds), since this read is blocking we want to dispatch it to run async to ensure
/// we don't block user interaction while it's running
DispatchQueue.global(qos: .default).async {
let unreadCount: Int = Storage.shared
.read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(
// Exclude outgoing and deleted messages from the count
Interaction.Columns.variant != Interaction.Variant.standardOutgoing &&
Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted
)
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
@ -520,7 +519,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
.fetchCount(db)
}
.defaulting(to: 0)
)
DispatchQueue.main.async {
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
}
}
}
}

View File

@ -1,24 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIULagRXXdxagFp2IRBaWWNeO5dK+IwDQYJKoZIhvcNAQEL
MIIEDTCCAvWgAwIBAgIUEZkKsCM3Leodz+JB0ADefbWoRbswDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTY1M1oX
DTI1MDQxMTEyNTY1M1owejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
HTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMB4XDTIzMDUxNzAyNDAwOFoX
DTI1MDQxMjAyNDAwOFowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23lBHUMU8xl3ZBPhQJuupNk9pqAW
8UvqyMX2BYWVc6bGpgRiqnf2Rc58Ol9jSM4VT29jXHD+PXXQLIvoZmni/5fbdkZl
zFAvnPFoWf4g4xCdREEpJ7m/sWh8aG6Bf7Eh+sTP6qaspJUPo5q4ovUd4tUoTt7f
bVlnzncXI1z2bhrmxWR8ahl9SwMjd/qKZMFKL3o12f4xhYu0Jfp1aFeKdrRImfZR
X6hzXM6uUe5X+/3mrmKvYCVnNoNCwsdyxTZp4JYXCqhG/g29CbWDFTTqxWVXySFK
+mujbHfWIBvRheYvO9x7Wb2jsPq5VbyP1MoqxPThKjF+FeCfU7X0+Fy+3QIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRXwt1MJe73lcOBv+JHmjqWyypB2DAfBgNVHSMEGDAW
gBRXwt1MJe73lcOBv+JHmjqWyypB2DAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4Yz/kIXn5t+VMATXsortcyK3DFF
hjNICxAt8qdLwyCCJDnedBdfeQb7zrn2A3btzfKrBD0x3JrbVHabUrtI+wFqfDLS
id2WOIIM/8RP2V/e4zanpKsk9yB/euKga+M+fybfTn1WTqQU5nEuU6eZyyEEZBk6
1rzWJstxWhcfN4rfl+ciSWLcmFLC2LuNZqwm6To77oLPj+DGrUHyRKFZ4Tw9ilcU
TpMKFaMmNzrHEzS5lPJIRa+2LD5vDYR/sv+lPiKMXTb64OTOJjTfucdsyZqWrI0R
mV2pBcrYBoDbxO+7pnr8GrJIcFqTLDI6MbjH6eseZqRHJSYKrNCyGlDeSQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRUYnrMlCbDZo6YXpnivhBui51XhDAfBgNVHSMEGDAW
gBRUYnrMlCbDZo6YXpnivhBui51XhDAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQzLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQAb+5FUjLXfgF0QmeBJrpC4B+3gIyw6QGTnbMXM5zVt
zKANoZxeQesZXkSGDTlszI4XnBs/bDzf87AROxDuT0guxt33+PhyXNw+9FdV3CAG
t/8FyRMPyJI8xog0mlPgjVqSw2PGjXtj2uVEkB7gkm6+AoPUfZYdPOplezrpvRES
tMVbjsxxiMiOQAOm1bS69dC16xQ6bZ8++QNZXPhj9o1a+tQCb71Bp2sYI66hCfmy
DRSJEDW7fCPb/da1D8cN68qr5vxIJjm5cWaF4xlN9pc9pywssTbPYhPSluravRDg
qyqfraj2YhdDNOSRj/U6IuYbL+jKWuaTcrEFYyNExxkq
CSqGSIb3DQEBCwUAA4IBAQBFYRlRODyQTIhNQC+pTapKtHdS9GJqKvyJX6NVFF6w
+oBzZGNYsDTmzaelraAuUz+uS7d0vngu5cV+3jG0DgksELT6hbpuHcad1rxAhuDv
wv/f02qJyB1F2luXma2n+NHgRFhvIYulWjV/DSSmwea2XD4DH+ZKcYeEXyT71b2T
VZfGnxLPVMz99iA6sQxsNfccFMvDxKofha7teRkUJ+SVzyutrneYySqrjGie6+Nb
oOw4CnpiqiUKIf47B6ZKlsJ8MAS8zAo6O9UqfmNdVoXFrZDjaQGPAjSH1oxL7iP5
pED6BUMytm8spiTEVBYIer/gcXaA4zWSKZ/Fd24OK0GL
-----END CERTIFICATE-----

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 893 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -61,7 +61,7 @@
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Sélectionner";
/* keyboard toolbar label when starting to search with no current results */
"CONVERSATION_SEARCH_SEARCHING" = "Searching...";
"CONVERSATION_SEARCH_SEARCHING" = "Recherche...";
/* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "Aucune correspondance";
/* keyboard toolbar label when exactly 1 message matches the search string */
@ -289,7 +289,7 @@
"vc_create_private_chat_title" = "Nouvelle Session";
"vc_create_private_chat_enter_session_id_tab_title" = "Saisir un Session ID";
"vc_create_private_chat_scan_qr_code_tab_title" = "Scanner un Code QR";
"vc_enter_public_key_explanation" = "Start a new conversation by entering someone's Session ID or share your Session ID with them.";
"vc_enter_public_key_explanation" = "Démarrez une nouvelle conversation en saisissant l'ID Session de quelqu'un ou en partageant votre ID Session avec eux.";
"vc_scan_qr_code_camera_access_explanation" = "Session a besoin d'accéder à l'appareil photo pour scanner les codes QR";
"vc_scan_qr_code_grant_camera_access_button_title" = "Autoriser l'accès";
"vc_create_closed_group_title" = "Nouveau groupe privé";
@ -304,10 +304,10 @@
"vc_join_public_chat_scan_qr_code_tab_title" = "Scannez le code QR";
"vc_enter_chat_url_text_field_hint" = "Saisissez une URL de groupe public";
"vc_settings_title" = "Paramètres";
"vc_group_settings_title" = "Group Settings";
"vc_group_settings_title" = "Paramètres de Groupe";
"vc_settings_display_name_missing_error" = "Veuillez choisir un nom d'utilisateur";
"vc_settings_display_name_too_long_error" = "Veuillez choisir un nom d'utilisateur plus court";
"vc_settings_privacy_button_title" = "Confidientalité ";
"vc_settings_privacy_button_title" = "Confidentialité ";
"vc_settings_notifications_button_title" = "Notifications";
"vc_settings_recovery_phrase_button_title" = "Phrase de récupération";
"vc_settings_clear_all_data_button_title" = "Effacer les données";
@ -319,13 +319,13 @@
// MARK: - Not Yet Translated
"fast_mode_explanation" = "Vous serez notifiés de nouveaux messages de manière certaine et immédiate en utilisant les serveurs de notification dApple.";
"fast_mode" = "Mode rapide";
"slow_mode_explanation" = "Session vérifiera occasionnellement la présence de nouveaux message en tâche de fond.";
"slow_mode_explanation" = "Session vérifiera occasionnellement la présence de nouveaux messages en tâche de fond.";
"slow_mode" = "Mode lent";
"vc_pn_mode_title" = "Notifications de message";
"vc_link_device_recovery_phrase_tab_title" = "Phrase de récupération";
"vc_link_device_scan_qr_code_explanation" = "Allez dans paramètre → Phrase de récupération sur votre autre appareil pour afficher votre QR Code.";
"vc_enter_recovery_phrase_title" = "Phrase de récupération";
"vc_enter_recovery_phrase_explanation" = "Pour lier votre appareil, entrez la phrase de récupération qui vous a été donné lors de la création du compte.";
"vc_enter_recovery_phrase_explanation" = "Pour lier votre appareil, entrez la phrase de récupération qui vous a été donnée lors de la création du compte.";
"vc_enter_public_key_text_field_hint" = "Entrez un ID Session ou un nom ONS";
"admin_group_leave_warning" = "Puisque vous êtes le créateur de ce groupe, il sera supprimé pour tout le monde. Ceci ne peut pas être annulé.";
"vc_join_open_group_suggestions_title" = "Ou rejoignez un de ceux-ci...";
@ -360,7 +360,7 @@
"modal_send_seed_explanation" = "Voici votre phrase de récupération. Si vous l'envoyez à quelqu'un, cette personne aura un accès complet à votre compte.";
"modal_send_seed_send_button_title" = "Envoyer";
"vc_conversation_settings_notify_for_mentions_only_title" = "Activer les notifications que sur mention";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "Quand activer, vous recevrez les notifications duniquement les messages vous notifiant.";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "Quand activé, vous recevrez uniquement les notifications des messages vous mentionnant.";
"view_conversation_title_notify_for_mentions_only" = "Me notifier que si je suis mentionné(e)";
"message_deleted" = "Ce message a été supprimé";
"delete_message_for_me" = "Supprimer pour moi uniquement";
@ -371,7 +371,7 @@
"context_menu_save" = "Enregistrer";
"context_menu_ban_user" = "Bannir l'utilisateur";
"context_menu_ban_and_delete_all" = "Bannir et supprimer tout";
"context_menu_ban_user_error_alert_message" = "Unable to ban user";
"context_menu_ban_user_error_alert_message" = "Impossible de bannir l'utilisateur";
"accessibility_expanding_attachments_button" = "Ajouter une pièce jointe";
"accessibility_gif_button" = "Gif";
"accessibility_document_button" = "Document";
@ -401,27 +401,27 @@
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Êtes-vous sûr de vouloir supprimer toutes les demandes de messages ?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Effacer";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Êtes-vous sûr de vouloir supprimer cette demande de message ?";
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Êtes-vous sûr de vouloir bloquer ce contact ?";
"MESSAGE_REQUESTS_INFO" = "Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message.";
"MESSAGE_REQUESTS_ACCEPTED" = "Votre demande de message a été réceptionnée.";
"MESSAGE_REQUESTS_NOTIFICATION" = "Vous avez une nouvelle demande de message";
"TXT_HIDE_TITLE" = "Masquer";
"TXT_DELETE_ACCEPT" = "Accepter";
"TXT_BLOCK_USER_TITLE" = "Block User";
"TXT_BLOCK_USER_TITLE" = "Bloquer Utilisateur";
"ALERT_ERROR_TITLE" = "Erreur";
"modal_call_permission_request_title" = "Autorisations d'appel requises";
"modal_call_permission_request_title" = "Autorisation d'appel requise";
"modal_call_permission_request_explanation" = "Vous pouvez activer la permission \"Appels vocaux et vidéo\" dans les paramètres de confidentialité.";
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
"LOADING_CONVERSATIONS" = "Loading Conversations...";
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
"RECOVERY_PHASE_ERROR_LAST_WORD" = "You seem to be missing the last word of your recovery phrase. Please check what you entered and try again.";
"RECOVERY_PHASE_ERROR_INVALID_WORD" = "There appears to be an invalid word in your recovery phrase. Please check what you entered and try again.";
"RECOVERY_PHASE_ERROR_FAILED" = "Your recovery phrase couldn't be verified. Please check what you entered and try again.";
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oups, une erreur est survenue";
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
"LOADING_CONVERSATIONS" = "Chargement des conversations...";
"DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines";
"RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
"RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
"RECOVERY_PHASE_ERROR_LAST_WORD" = "Il semble qu'il vous manque le dernier mot de votre phrase de récupération. Vérifiez votre saisie et réessayez s'il vous plaît.";
"RECOVERY_PHASE_ERROR_INVALID_WORD" = "Il semble qu'il y a un mot invalide dans votre phrase de récupération. Vérifiez votre saisie et réessayez s'il vous plaît.";
"RECOVERY_PHASE_ERROR_FAILED" = "Votre phrase de récupération n'a pas pu être validée. Vérifiez votre saisie et réessayez s'il vous plaît.";
/* Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode. */
"SCREEN_LOCK_ENABLE_UNKNOWN_ERROR" = "Authentication could not be accessed.";
"SCREEN_LOCK_ENABLE_UNKNOWN_ERROR" = "L'authentification a échoué.";
/* Indicates that Touch ID/Face ID/Phone Passcode authentication failed. */
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED" = "Échec dauthentification";
/* Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures. */
@ -433,200 +433,207 @@
/* Indicates that Touch ID/Face ID/Phone Passcode passcode is not set. */
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET" = "Vous devez activer un code dans vos réglages iOS pour utiliser le verrou décran.";
/* Label for the button to send a message */
"SEND_BUTTON_TITLE" = "Send";
"SEND_BUTTON_TITLE" = "Envoyer";
/* Generic text for button that retries whatever the last action was. */
"RETRY_BUTTON_TEXT" = "Retry";
"RETRY_BUTTON_TEXT" = "Réessayer";
/* notification action */
"SHOW_THREAD_BUTTON_TITLE" = "Show Chat";
"SHOW_THREAD_BUTTON_TITLE" = "Montrer Discussion";
/* notification body */
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
"MEDIA_TAB_TITLE" = "Media";
"SEND_FAILED_NOTIFICATION_BODY" = "Échec d'envoi de votre message.";
"INVALID_SESSION_ID_MESSAGE" = "Veuillez vérifier l'ID Session et réessayez.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Veuillez vérifier la phrase de récupération et réessayez.";
"QUOTED_MESSAGE_NOT_FOUND" = "Message original non trouvé.";
"MEDIA_TAB_TITLE" = "Média";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "Vous n'avez aucun document dans cette conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Chargement des documents les plus récents…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Chargement des documents les plus anciens…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activités";
/* The name for the emoji category 'Animals & Nature' */
"EMOJI_CATEGORY_ANIMALS_NAME" = "Animals & Nature";
"EMOJI_CATEGORY_ANIMALS_NAME" = "Animaux & Nature";
/* The name for the emoji category 'Flags' */
"EMOJI_CATEGORY_FLAGS_NAME" = "Flags";
"EMOJI_CATEGORY_FLAGS_NAME" = "Drapeaux";
/* The name for the emoji category 'Food & Drink' */
"EMOJI_CATEGORY_FOOD_NAME" = "Food & Drink";
"EMOJI_CATEGORY_FOOD_NAME" = "Nourriture & Boissons";
/* The name for the emoji category 'Objects' */
"EMOJI_CATEGORY_OBJECTS_NAME" = "Objects";
"EMOJI_CATEGORY_OBJECTS_NAME" = "Objets";
/* The name for the emoji category 'Recents' */
"EMOJI_CATEGORY_RECENTS_NAME" = "Recently Used";
"EMOJI_CATEGORY_RECENTS_NAME" = "Récents";
/* The name for the emoji category 'Smileys & People' */
"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & People";
"EMOJI_CATEGORY_SMILEYSANDPEOPLE_NAME" = "Smileys & Personnes";
/* The name for the emoji category 'Symbols' */
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symboles";
/* The name for the emoji category 'Travel & Places' */
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
"EMOJI_CATEGORY_TRAVEL_NAME" = "Voyages et Lieux";
"EMOJI_REACTS_NOTIFICATION" = "%@ a réagi au message de %@.";
"EMOJI_REACTS_MORE_REACTORS_ONE" = "et 1 autre personne a réagi %@ à ce message.";
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "et %@ autres personnes ont réagi %@ à ce message.";
"EMOJI_REACTS_SHOW_LESS" = "Moins";
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Ralentissez ! Vous avez envoyé trop de réactions Emojis. Réessayez un peu plus tard.";
/* New conversation screen*/
"vc_new_conversation_title" = "New Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Create";
"JOIN_COMMUNITY_BUTTON_TITLE" = "Join";
"PRIVACY_TITLE" = "Confidientalité";
"vc_new_conversation_title" = "Nouvelle Conversation";
"CREATE_GROUP_BUTTON_TITLE" = "Créer";
"JOIN_COMMUNITY_BUTTON_TITLE" = "Rejoindre";
"PRIVACY_TITLE" = "Confidentialité";
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sécurité de lécran";
"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_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_READ_RECEIPTS" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_TITLE" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Envoyer un accusé réception dans les conversations 1 à 1.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Indicateurs de saisie";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Indicateurs de saisie";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "Voir et partager l'indicateur de saisie dans les conversions 1 à 1.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Aperçus des liens";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Envoyer des aperçus de liens.";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";
"PRIVACY_SECTION_CALLS" = "Calls (Beta)";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Générer un lien d'aperçu pour les URL supportées.";
"PRIVACY_SECTION_CALLS" = "Appels (Béta)";
"PRIVACY_CALLS_TITLE" = "Appels audio et vidéo";
"PRIVACY_CALLS_DESCRIPTION" = "Enables voice and video calls to and from other users.";
"PRIVACY_CALLS_WARNING_TITLE" = "Voice and Video Calls (Beta)";
"PRIVACY_CALLS_WARNING_DESCRIPTION" = "Your IP address is visible to your call partner and an Oxen Foundation server while using beta calls. Are you sure you want to enable Voice and Video Calls?";
"PRIVACY_CALLS_DESCRIPTION" = "Active les appels voix et vidéos de et vers d'autres utilisateurs.";
"PRIVACY_CALLS_WARNING_TITLE" = "Appels voix et vidéo (Béta)";
"PRIVACY_CALLS_WARNING_DESCRIPTION" = "Votre adresse IP est visible de votre partenaire d'appel et d'un serveur de Oxen Foundation pendant l'utilisation d'un appel. Êtes-vous certain de vouloir activer les appels voix et vidéo ?";
"NOTIFICATIONS_TITLE" = "Notifications";
"NOTIFICATIONS_SECTION_STRATEGY" = "Stratégie de notification";
"NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE" = "Utiliser le mode rapide";
"NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION" = "You'll be notified of new message reliably and immediately using Apple's notification servers.";
"NOTIFICATIONS_STRATEGY_FAST_MODE_ACTION" = "Go to device notification settings";
"NOTIFICATIONS_SECTION_STYLE" = "Notification Style";
"NOTIFICATIONS_STYLE_SOUND_TITLE" = "Sound";
"NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE" = "Sound When App is Open";
"NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION" = "Vous serez notifiés des nouveaux messages de manière fiable et rapide en utilisant les serveurs de notifications d'Apple.";
"NOTIFICATIONS_STRATEGY_FAST_MODE_ACTION" = "Aller aux paramètres de notification";
"NOTIFICATIONS_SECTION_STYLE" = "Style de notification";
"NOTIFICATIONS_STYLE_SOUND_TITLE" = "Son";
"NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE" = "Son quand l'application est ouverte";
"NOTIFICATIONS_STYLE_CONTENT_TITLE" = "Contenu des notifications";
"NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION" = "The information shown in notifications.";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name & Content";
"NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION" = "L'information qui apparaît dans les notifications.";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Nom et contenu";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Nom seulement";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "Ni nom ni contenu";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_SINGLE" = "Are you sure you want to unblock %@?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_FALLBACK" = "this contact";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1" = "Are you sure you want to unblock %@";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_2_SINGLE" = "and %@?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_3" = "and %d others?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_INC_QUOTE" = "How are you?";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_INC_MESSAGE" = "I'm good thanks, you?";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_OUT_MESSAGE" = "I'm doing great, thanks.";
"APPEARANCE_NIGHT_MODE_TITLE" = "Auto night-mode";
"APPEARANCE_NIGHT_MODE_TOGGLE" = "Match system settings";
"HELP_TITLE" = "Help";
"HELP_REPORT_BUG_TITLE" = "Report a Bug";
"HELP_REPORT_BUG_DESCRIPTION" = "Export your logs, then upload the file though Session's Help Desk.";
"HELP_REPORT_BUG_ACTION_TITLE" = "Export Logs";
"HELP_TRANSLATE_TITLE" = "Translate Session";
"HELP_FEEDBACK_TITLE" = "We'd love your Feedback";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Épuration des messages";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Épuration des communautés";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Supprimer les messages datant de plus de 6 mois dans les communautés ayant plus de 2000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Messages audio";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Jouer automatiquement les messages audio";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Jouer automatiquement les messages audio de manière consécutive.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Contacts bloqués";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "Vous n'avez aucun contact bloqué.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Débloquer";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_SINGLE" = "Êtes-vous sûr de vouloir débloquer %@?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_FALLBACK" = "Ce contact";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1" = "Êtes-vous sûr de vouloir débloquer %@";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_2_SINGLE" = "et %@?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_3" = "et %d autres?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Débloquer";
"APPEARANCE_TITLE" = "Apparence";
"APPEARANCE_THEMES_TITLE" = "Thèmes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Couleur primaire";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_INC_QUOTE" = "Comment allez-vous ?";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_INC_MESSAGE" = "Je vais bien, et vous ?";
"APPEARANCE_PRIMARY_COLOR_PREVIEW_OUT_MESSAGE" = "Je vais très bien. Merci.";
"APPEARANCE_NIGHT_MODE_TITLE" = "Mode nuit automatique";
"APPEARANCE_NIGHT_MODE_TOGGLE" = "Se conformer aux paramètres système";
"HELP_TITLE" = "Aide";
"HELP_REPORT_BUG_TITLE" = "Rapporter un bogue";
"HELP_REPORT_BUG_DESCRIPTION" = "Exporter votre journal d'application et téléverser le fichier via le support Session.";
"HELP_REPORT_BUG_ACTION_TITLE" = "Exporter Journal";
"HELP_TRANSLATE_TITLE" = "Traduire Session";
"HELP_FEEDBACK_TITLE" = "Nous aimerions votre retour d'expérience";
"HELP_FAQ_TITLE" = "FAQ";
"HELP_SUPPORT_TITLE" = "Support";
"modal_clear_all_data_title" = "Effacer toutes les données";
"modal_clear_all_data_explanation" = "This will permanently delete your messages and contacts. Would you like to clear this device only, or delete your data from the network as well?";
"modal_clear_all_data_explanation_2" = "Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.";
"modal_clear_all_data_device_only_button_title" = "Clear Device Only";
"modal_clear_all_data_entire_account_button_title" = "Clear Device and Network";
"modal_clear_all_data_explanation" = "Ceci supprimera de manière permanente vos messages et contacts. Voulez-vous effacer vos données sur cet appareil seulement ou sur le réseau aussi ?";
"modal_clear_all_data_explanation_2" = "Êtes-vous sûr de vouloir effacer les données sur le réseau ? Si vous continuez, vous ne pourrez restaurer ni vos messages ni vos contacts.";
"modal_clear_all_data_device_only_button_title" = "Effacer sur l'appareil seulement";
"modal_clear_all_data_entire_account_button_title" = "Effacer sur l'appareil et le réseau";
"dialog_clear_all_data_deletion_failed_1" = "Les données nont pas été supprimées sur un nœud de service. ID du nœud de service : %@.";
"dialog_clear_all_data_deletion_failed_2" = "Les données nont pas été supprimées sur %@ nœuds de service. ID des nœuds de service : %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_clear_all_data_confirm" = "Effacer";
"modal_seed_title" = "Votre phrase de récupération";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_seed_explanation" = "Vous pouvez utiliser votre phrase de récupération pour restaurer votre compte ou pour lier un autre appareil.";
"modal_permission_explanation" = "Session a besoin de l'accès %@ pour pouvoir continuer. Vous pouvez donner cet accès depuis les paramètres iOS.";
"modal_permission_settings_title" = "Paramètres";
"modal_permission_camera" = "caméra";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";
"DISAPPEARING_MESSAGES_OFF" = "Off";
"DISAPPEARING_MESSAGES_SUBTITLE_OFF" = "Off";
"DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER" = "Disappear After: %@";
"COPY_GROUP_URL" = "Copy Group URL";
"modal_permission_library" = "disque";
"DISAPPEARING_MESSAGES_OFF" = "Éteint";
"DISAPPEARING_MESSAGES_SUBTITLE_OFF" = "Éteint";
"DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER" = "Disparaît après : %@";
"COPY_GROUP_URL" = "Copier l'URL de Groupe";
"NEW_CONVERSATION_CONTACTS_SECTION_TITLE" = "Contacts";
"GROUP_ERROR_NO_MEMBER_SELECTION" = "Please pick at least 1 group member";
"GROUP_CREATION_PLEASE_WAIT" = "Please wait while the group is created...";
"GROUP_CREATION_ERROR_TITLE" = "Couldn't Create Group";
"GROUP_CREATION_ERROR_MESSAGE" = "Please check your internet connection and try again.";
"GROUP_UPDATE_ERROR_TITLE" = "Couldn't Update Group";
"GROUP_UPDATE_ERROR_MESSAGE" = "Can't leave while adding or removing other members.";
"GROUP_ACTION_REMOVE" = "Remove";
"GROUP_TITLE_MEMBERS" = "Members";
"GROUP_TITLE_FALLBACK" = "Group";
"DM_ERROR_DIRECT_BLINDED_ID" = "You can only send messages to Blinded IDs from within a Community";
"DM_ERROR_INVALID" = "Please check the Session ID or ONS name and try again";
"COMMUNITY_ERROR_INVALID_URL" = "Please check the URL you entered and try again.";
"COMMUNITY_ERROR_GENERIC" = "Couldn't Join";
"DISAPPERING_MESSAGES_TITLE" = "Disappearing Messages";
"DISAPPERING_MESSAGES_TYPE_TITLE" = "Delete Type";
"DISAPPERING_MESSAGES_TYPE_AFTER_READ_TITLE" = "Disappear After Read";
"DISAPPERING_MESSAGES_TYPE_AFTER_READ_DESCRIPTION" = "Messages delete after they have been read.";
"DISAPPERING_MESSAGES_TYPE_AFTER_SEND_TITLE" = "Disappear After Send";
"DISAPPERING_MESSAGES_TYPE_AFTER_SEND_DESCRIPTION" = "Messages delete after they have been sent.";
"DISAPPERING_MESSAGES_TIMER_TITLE" = "Timer";
"DISAPPERING_MESSAGES_SAVE_TITLE" = "Set";
"DISAPPERING_MESSAGES_GROUP_WARNING" = "This setting applies to everyone in this conversation.";
"DISAPPERING_MESSAGES_GROUP_WARNING_ADMIN_ONLY" = "This setting applies to everyone in this conversation. Only group admins can change this setting.";
"DISAPPERING_MESSAGES_SUMMARY" = "Disappear After %@ - %@";
"DISAPPERING_MESSAGES_INFO_ENABLE" = "%@ has set messages to disappear %@ after they have been %@";
"DISAPPERING_MESSAGES_INFO_UPDATE" = "%@ has changed messages to disappear %@ after they have been %@";
"DISAPPERING_MESSAGES_INFO_DISABLE" = "%@ has turned off disappearing messages";
"MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent";
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices";
"delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";
"mark_unread_button_text" = "Mark unread";
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"group_you_leaving" = "Leaving...";
"group_leave_error" = "Failed to leave Group!";
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"GROUP_ERROR_NO_MEMBER_SELECTION" = "Veuillez choisir au moins 1 membre de groupe";
"GROUP_CREATION_PLEASE_WAIT" = "Veuillez patienter pendant la création du groupe...";
"GROUP_CREATION_ERROR_TITLE" = "Impossible de créer le groupe";
"GROUP_CREATION_ERROR_MESSAGE" = "Veuillez vérifier votre connexion internet et réessayez.";
"GROUP_UPDATE_ERROR_TITLE" = "Impossible de mettre à jour le groupe";
"GROUP_UPDATE_ERROR_MESSAGE" = "Impossible de quitter pendant l'ajout ou la suppression d'un autre membre.";
"GROUP_ACTION_REMOVE" = "Retirer";
"GROUP_TITLE_MEMBERS" = "Membres";
"GROUP_TITLE_FALLBACK" = "Groupe";
"DM_ERROR_DIRECT_BLINDED_ID" = "Vous pouvez seulement envoyer des messages à des IDs anonymes depuis une communauté";
"DM_ERROR_INVALID" = "Veuillez vérifier l'ID Session ou l'ONS et réessayez";
"COMMUNITY_ERROR_INVALID_URL" = "Veuillez vérifier l'URL et réessayez";
"COMMUNITY_ERROR_GENERIC" = "Impossible de rejoindre";
"DISAPPERING_MESSAGES_TITLE" = "Messages éphémères";
"DISAPPERING_MESSAGES_TYPE_TITLE" = "Type de suppression";
"DISAPPERING_MESSAGES_TYPE_AFTER_READ_TITLE" = "Disparaît après lecture";
"DISAPPERING_MESSAGES_TYPE_AFTER_READ_DESCRIPTION" = "Les messages disparaissent une fois lus.";
"DISAPPERING_MESSAGES_TYPE_AFTER_SEND_TITLE" = "Disparaît après envoi";
"DISAPPERING_MESSAGES_TYPE_AFTER_SEND_DESCRIPTION" = "Les messages disparaissent une fois envoyés.";
"DISAPPERING_MESSAGES_TIMER_TITLE" = "Compteur";
"DISAPPERING_MESSAGES_SAVE_TITLE" = "Sauver";
"DISAPPERING_MESSAGES_GROUP_WARNING" = "Ce paramètre s'applique à toutes les personnes de cette conversation.";
"DISAPPERING_MESSAGES_GROUP_WARNING_ADMIN_ONLY" = "Ce paramètre s'applique à toutes les personnes de cette conversation. Seuls les administrateurs du groupe peuvent changer ce paramètre.";
"DISAPPERING_MESSAGES_SUMMARY" = "Disparaît après %@ - %@";
"DISAPPERING_MESSAGES_INFO_ENABLE" = "%@ a paramétré les messages pour qu'ils disparaissent %@ après avoir été %@";
"DISAPPERING_MESSAGES_INFO_UPDATE" = "%@ a paramétré les messages pour qu'ils disparaissent %@ après avoir été %@";
"DISAPPERING_MESSAGES_INFO_DISABLE" = "%@ a désactivé les messages éphémères.";
"MESSAGE_STATE_READ" = "Lu";
"MESSAGE_STATE_SENT" = "Envoyé";
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "Vous pourrez envoyer des messages et des pièces jointes une fois que le destinataire aura approuvé votre demande.";
"MESSAGE_DELIVERY_STATUS_SENDING" = "Envoi";
"MESSAGE_DELIVERY_STATUS_SENT" = "Envoyé";
"MESSAGE_DELIVERY_STATUS_READ" = "Lu";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Échec de l'envoi";
"MESSAGE_INFO_SENT" = "Envoyé";
"MESSAGE_INFO_RECEIVED" = "Reçu";
"MESSAGE_INFO_FROM" = "De";
"ATTACHMENT_INFO_FILE_ID" = "ID Fichier";
"ATTACHMENT_INFO_FILE_TYPE" = "Type Fichier";
"ATTACHMENT_INFO_FILE_SIZE" = "Taille Fichier";
"ATTACHMENT_INFO_RESOLUTION" = "Résolution";
"ATTACHMENT_INFO_DURATION" = "Durée";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Échec de synchronisation";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Synchronisation";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Échec d'envoi du message";
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Échec de synchronisation vers vos autres appareils";
"delete_message_for_me_and_my_devices" = "Effacer sur mes autres appareils";
"context_menu_resend" = "Réenvoyer";
"context_menu_resync" = "Resynchroniser";
"GIPHY_PERMISSION_TITLE" = "Rechercher GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session va se connecter à Giphy. Envoyer des GIFs empêchera la protection de vos métadonnées.";
"message_info_title" = "Info Message";
"mute_button_text" = "Silence";
"unmute_button_text" = "Actif";
"mark_read_button_text" = "Marquer comme lu";
"mark_unread_button_text" = "Marquer comme non lu";
"leave_group_confirmation_alert_title" = "Quitter le groupe";
"leave_community_confirmation_alert_title" = "Quitter la communauté";
"leave_community_confirmation_alert_message" = "Êtes-vous sûr de vouloir quitter %@?";
"group_you_leaving" = "Quitter...";
"group_leave_error" = "Impossible de quitter le groupe!";
"group_unable_to_leave" = "Impossible de quitter le groupe, veuillez réessayer";
"delete_group_confirmation_alert_title" = "Delete Group";
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Supprimer conversation";
"delete_conversation_confirmation_alert_message" = "Êtes-vous sûr de vouloir supprimer votre conversation avec %@ ?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -626,7 +626,14 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"REMOVE_AVATAR" = "Remove";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
"update_profile_modal_max_size_error_message" = "Please select a smaller photo and try again";
"update_profile_modal_error_title" = "Couldn't Update Profile";
"update_profile_modal_error_message" = "Please check your internet connection and try again";
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
@ -634,4 +641,3 @@
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -172,7 +172,7 @@ final class DisplayNameVC: BaseVC {
targetView: self.view,
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -138,7 +138,7 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "invalid_recovery_phrase".localized(),
explanation: "INVALID_RECOVERY_PHRASE_MESSAGE".localized(),
body: .text("INVALID_RECOVERY_PHRASE_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: { [weak self] in
@ -301,7 +301,7 @@ private final class RecoveryPhraseVC: UIViewController {
targetView: self.view,
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -186,7 +186,7 @@ final class RestoreVC: BaseVC {
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -215,7 +215,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -218,9 +218,21 @@ final class PathVC: BaseVC {
}
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "")
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let country: String = (IP2Country.isInitialized ?
IP2Country.shared.countryNamesCache.wrappedValue[snode.ip].defaulting(to: "Resolving...") :
"Resolving..."
)
return getPathRow(
title: (isGuardSnode ?
"vc_path_guard_node_row_title".localized() :
"vc_path_service_node_row_title".localized()
),
subtitle: country,
location: location,
dotAnimationStartDelay: dotAnimationStartDelay,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
}
// MARK: - Interaction

View File

@ -38,8 +38,9 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
// Check if the user selected an animated image (if so then don't crop, just
// set the avatar directly
guard
let type: URLResourceValues = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey]),
let typeString: String = type.typeIdentifier,
let resourceValues: URLResourceValues = (try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey])),
let type: Any = resourceValues.allValues.first?.value,
let typeString: String = type as? String,
MIMETypeUtil.supportedAnimatedImageUTITypes().contains(typeString)
else {
let viewController: CropScaleImageViewController = CropScaleImageViewController(

View File

@ -9,8 +9,8 @@ import SignalUtilitiesKit
final class NukeDataModal: Modal {
// MARK: - Initialization
override init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
super.init(targetView: targetView, afterClosed: afterClosed)
override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) {
super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed)
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve
@ -135,7 +135,7 @@ final class NukeDataModal: Modal {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "modal_clear_all_data_title".localized(),
explanation: "modal_clear_all_data_explanation_2".localized(),
body: .text("modal_clear_all_data_explanation_2".localized()),
confirmTitle: "modal_clear_all_data_confirm".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
@ -177,7 +177,7 @@ final class NukeDataModal: Modal {
targetView: self?.view,
info: ConfirmationModal.Info(
title: "ALERT_ERROR_TITLE".localized(),
explanation: error.localizedDescription,
body: .text(error.localizedDescription),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
@ -207,7 +207,7 @@ final class NukeDataModal: Modal {
targetView: self?.view,
info: ConfirmationModal.Info(
title: "ALERT_ERROR_TITLE".localized(),
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -214,8 +214,8 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
),
confirmationInfo: ConfirmationModal.Info(
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
stateToShow: .whenDisabled,
body: .text("PRIVACY_CALLS_WARNING_DESCRIPTION".localized()),
showCondition: .disabled,
confirmTitle: "continue_2".localized(),
confirmAccessibility: Accessibility(identifier: "Enable"),
confirmStyle: .textPrimary,

View File

@ -130,7 +130,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
targetView: self.view,
info: ConfirmationModal.Info(
title: "invalid_session_id".localized(),
explanation: "INVALID_SESSION_ID_MESSAGE".localized(),
body: .text("INVALID_SESSION_ID_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -16,8 +16,8 @@ final class SeedModal: Modal {
// MARK: - Initialization
override init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
super.init(targetView: targetView, afterClosed: afterClosed)
override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) {
super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed)
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve

View File

@ -73,7 +73,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
onImagePicked: { [weak self] resultImage in
guard let oldDisplayName: String = self?.oldDisplayName else { return }
self?.updateProfile(
self?.updatedProfilePictureSelected(
name: oldDisplayName,
avatarUpdate: .uploadImage(resultImage)
)
@ -81,7 +81,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
onImageFilePicked: { [weak self] resultImagePath in
guard let oldDisplayName: String = self?.oldDisplayName else { return }
self?.updateProfile(
self?.updatedProfilePictureSelected(
name: oldDisplayName,
avatarUpdate: .uploadFilePath(resultImagePath)
)
@ -89,6 +89,8 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
)
fileprivate var oldDisplayName: String
private var editedDisplayName: String?
private var editProfilePictureModal: ConfirmationModal?
private var editProfilePictureModalInfo: ConfirmationModal.Info?
// MARK: - Initialization
@ -153,11 +155,13 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
}
override var rightNavItems: AnyPublisher<[NavItem]?, Never> {
navState
.map { [weak self] navState -> [NavItem] in
switch navState {
case .standard:
return [
let userSessionId: String = self.userSessionId
return navState
.map { [weak self] navState -> [NavItem] in
switch navState {
case .standard:
return [
NavItem(
id: .qrCode,
image: UIImage(named: "QRCode")?
@ -168,10 +172,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self?.transitionToScreen(QRCodeVC())
}
)
]
]
case .editing:
return [
case .editing:
return [
NavItem(
id: .done,
systemItem: .done,
@ -258,11 +262,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
label: "Profile picture"
),
onTap: {
self?.updateProfilePicture(
hasCustomImage: ProfileManager.hasProfileImageData(
with: profile.profilePictureFileName
)
)
self?.updateProfilePicture(currentFileName: profile.profilePictureFileName)
}
),
SessionCell.Info(
@ -485,32 +485,73 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
// MARK: - Functions
private func updateProfilePicture(hasCustomImage: Bool) {
let actionSheet: UIAlertController = UIAlertController(
title: "UPDATE_PROFILE_TITLE".localized(),
message: nil,
preferredStyle: .actionSheet
)
actionSheet.addAction(UIAlertAction(
title: "MEDIA_FROM_LIBRARY_BUTTON".localized(),
style: .default,
handler: { [weak self] _ in
self?.showPhotoLibraryForAvatar()
private func updateProfilePicture(currentFileName: String?) {
let existingDisplayName: String = self.oldDisplayName
let existingImage: UIImage? = currentFileName
.map { ProfileManager.loadProfileData(with: $0) }
.map { UIImage(data: $0) }
let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info(
title: "update_profile_modal_title".localized(),
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: existingImage,
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
confirmTitle: "update_profile_modal_upload".localized(),
confirmEnabled: false,
cancelTitle: "update_profile_modal_remove".localized(),
cancelEnabled: (existingImage != nil),
hasCloseButton: true,
dismissOnConfirm: false,
onConfirm: { modal in modal.close() },
onCancel: { [weak self] modal in
self?.updateProfile(
name: existingDisplayName,
avatarUpdate: .remove,
onComplete: { [weak modal] in modal?.close() }
)
},
afterClosed: { [weak self] in
self?.editProfilePictureModal = nil
self?.editProfilePictureModalInfo = nil
}
))
)
let modal: ConfirmationModal = ConfirmationModal(info: editProfilePictureModalInfo)
self.editProfilePictureModalInfo = editProfilePictureModalInfo
self.editProfilePictureModal = modal
self.transitionToScreen(modal, transitionType: .present)
}
fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
// Only have the 'remove' button if there is a custom avatar set
if hasCustomImage {
actionSheet.addAction(UIAlertAction(
title: "REMOVE_AVATAR".localized(),
style: .destructive,
handler: { [weak self] _ in self?.removeProfileImage() }
))
}
actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel, handler: nil))
self.transitionToScreen(actionSheet, transitionType: .present)
self.editProfilePictureModal?.updateContent(
with: info.with(
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: {
switch avatarUpdate {
case .uploadImage(let image): return image
case .uploadFilePath(let filePath): UIImage(contentsOfFile: filePath)
default: return nil
}
}(),
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
confirmEnabled: true,
onConfirm: { [weak self] modal in
self?.updateProfile(
name: name,
avatarUpdate: avatarUpdate,
onComplete: { [weak modal] in modal?.close() }
)
}
)
)
}
private func showPhotoLibraryForAvatar() {
@ -526,47 +567,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
}
}
private func removeProfileImage() {
let oldDisplayName: String = self.oldDisplayName
let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in
ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default),
profileName: oldDisplayName,
avatarUpdate: .remove,
success: { db in
// Wait for the database transaction to complete before updating the UI
db.afterNextTransactionNested { _ in
DispatchQueue.main.async {
modalActivityIndicator.dismiss(completion: {})
}
}
},
failure: { [weak self] _ in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
self?.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: "Unable to remove avatar image",
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)
),
transitionType: .present
)
}
}
}
)
}
self.transitionToScreen(viewController, transitionType: .present)
}
fileprivate func updateProfile(
name: String,
avatarUpdate: ProfileManager.AvatarUpdate
avatarUpdate: ProfileManager.AvatarUpdate,
onComplete: (() -> ())? = nil
) {
let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in
ProfileManager.updateLocal(
@ -577,28 +581,42 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
// Wait for the database transaction to complete before updating the UI
db.afterNextTransactionNested { _ in
DispatchQueue.main.async {
modalActivityIndicator.dismiss(completion: {})
modalActivityIndicator.dismiss(completion: {
onComplete?()
})
}
}
},
failure: { [weak self] error in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
let isMaxFileSizeExceeded: Bool = (error == .avatarUploadMaxFileSizeExceeded)
let title: String = {
switch (avatarUpdate, error) {
case (.remove, _): return "update_profile_modal_remove_error_title".localized()
case (_, .avatarUploadMaxFileSizeExceeded):
return "update_profile_modal_max_size_error_title".localized()
default: return "update_profile_modal_error_title".localized()
}
}()
let message: String? = {
switch (avatarUpdate, error) {
case (.remove, _): return nil
case (_, .avatarUploadMaxFileSizeExceeded):
return "update_profile_modal_max_size_error_message".localized()
default: return "update_profile_modal_error_message".localized()
}
}()
self?.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: (isMaxFileSizeExceeded ?
"Maximum File Size Exceeded" :
"Couldn't Update Profile"
),
explanation: (isMaxFileSizeExceeded ?
"Please select a smaller photo and try again" :
"Please check your internet connection and try again"
),
title: title,
body: (message.map { .text($0) } ?? .none),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
dismissType: .single
)
),
transitionType: .present

View File

@ -9,6 +9,11 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
public static let mutePrefix: String = "\u{e067} "
public static let unreadCountViewSize: CGFloat = 20
private static let statusIndicatorSize: CGFloat = 14
// If a message is much too long, it will take forever to calculate its width and
// cause the app to be frozen. So if a search result string is longer than 100
// characters, we assume it cannot be shown within one line and need to be truncated
// to avoid the calculation.
private static let maxApproxCharactersCanBeShownInOneLine: Int = 100
// MARK: - UI
@ -738,14 +743,25 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
return authorPrefix
.appending(
truncatingIfNeeded(
approxWidth: (authorPrefix.size().width + result.size().width),
approxWidth: (
authorPrefix.size().width +
(
result.length > Self.maxApproxCharactersCanBeShownInOneLine ?
bounds.width :
result.size().width
)
),
content: result
)
)
}
.defaulting(
to: truncatingIfNeeded(
approxWidth: result.size().width,
approxWidth: (
result.length > Self.maxApproxCharactersCanBeShownInOneLine ?
bounds.width :
result.size().width
),
content: result
)
)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
@ -272,7 +273,7 @@ class ScreenLockUI {
targetView: screenBlockingWindow.rootViewController?.view,
info: ConfirmationModal.Info(
title: "SCREEN_LOCK_UNLOCK_FAILED".localized(),
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: { [weak self] in self?.ensureUI() } // After the alert, update the UI

View File

@ -157,7 +157,10 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges()
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges()
}
}
@objc func applicationDidResignActive(_ notification: Notification) {
@ -432,13 +435,15 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
self?.navigationController?.pushViewController(viewController, animated: true)
case .present:
let presenter: UIViewController? = (self?.presentedViewController ?? self)
if UIDevice.current.isIPad {
viewController.popoverPresentationController?.permittedArrowDirections = []
viewController.popoverPresentationController?.sourceView = self?.view
viewController.popoverPresentationController?.sourceRect = (self?.view.bounds ?? UIScreen.main.bounds)
viewController.popoverPresentationController?.sourceView = presenter?.view
viewController.popoverPresentationController?.sourceRect = (presenter?.view.bounds ?? UIScreen.main.bounds)
}
self?.present(viewController, animated: true)
presenter?.present(viewController, animated: true)
}
}
.store(in: &disposables)
@ -599,7 +604,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
guard
let confirmationInfo: ConfirmationModal.Info = info.confirmationInfo,
confirmationInfo.stateToShow.shouldShow(for: info.currentBoolValue)
confirmationInfo.showCondition.shouldShow(for: info.currentBoolValue)
else {
performAction()
return

View File

@ -3,16 +3,17 @@ import GRDB
import SessionSnodeKit
final class IP2Country {
var countryNamesCache: [String:String] = [:]
var countryNamesCache: Atomic<[String: String]> = Atomic([:])
private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
static var isInitialized = false
// MARK: Tables
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains the **lower** bound of an IP
/// range and the "registered_country_geoname_id" column contains the ID of the country corresponding to that range. We look up an IP by finding the first index in the
/// network column where the value is greater than the IP we're looking up (converted to an integer). The IP we're looking up must then be in the range **before** that
/// range.
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
/// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
/// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
/// up (converted to an integer). The IP we're looking up must then be in the range **before** that range.
private lazy var ipv4Table: [String:[Int]] = {
let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)!
let data = try! Data(contentsOf: url)
@ -36,15 +37,23 @@ final class IP2Country {
NotificationCenter.default.removeObserver(self)
}
// MARK: Implementation
private func cacheCountry(for ip: String) -> String {
if let result = countryNamesCache[ip] { return result }
let ipAsInt = IPv4.toInt(ip)
guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
let result = countryNamesTable["country_name"]![countryNamesTableIndex]
countryNamesCache[ip] = result
// MARK: - Implementation
@discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
if let result: String = cache[ip] { return result }
let ipAsInt: Int = IPv4.toInt(ip)
guard
let ipv4TableIndex = ipv4Table["network"]?.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }),
let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex],
let countryNamesTableIndex = countryNamesTable["geoname_id"]?.firstIndex(of: String(countryID)),
let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex]
else {
return "Unknown Country" // Relies on the array being sorted
}
cache[ip] = result
return result
}
@ -58,9 +67,12 @@ final class IP2Country {
func populateCacheIfNeeded() -> Bool {
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
pathToDisplay.forEach { snode in
let _ = self.cacheCountry(for: snode.ip) // Preload if needed
countryNamesCache.mutate { [weak self] cache in
pathToDisplay.forEach { snode in
self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
}
}
DispatchQueue.main.async {
IP2Country.isInitialized = true
NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)

View File

@ -3,7 +3,9 @@
import UIKit
import Photos
import PhotosUI
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
public enum Permissions {
@discardableResult public static func requestCameraPermissionIfNeeded(
@ -21,9 +23,11 @@ public enum Permissions {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Session",
explanation: String(
format: "modal_permission_explanation".localized(),
"modal_permission_camera".localized()
body: .text(
String(
format: "modal_permission_explanation".localized(),
"modal_permission_camera".localized()
)
),
confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false
@ -59,9 +63,11 @@ public enum Permissions {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Session",
explanation: String(
format: "modal_permission_explanation".localized(),
"modal_permission_microphone".localized()
body: .text(
String(
format: "modal_permission_explanation".localized(),
"modal_permission_microphone".localized()
)
),
confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false,
@ -128,9 +134,11 @@ public enum Permissions {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Session",
explanation: String(
format: "modal_permission_explanation".localized(),
"modal_permission_library".localized()
body: .text(
String(
format: "modal_permission_explanation".localized(),
"modal_permission_library".localized()
)
),
confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false

View File

@ -402,7 +402,7 @@ public extension UIContextualAction {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: confirmationModalTitle,
attributedExplanation: confirmationModalExplanation,
body: .attributedText(confirmationModalExplanation),
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmAccessibility: Accessibility(
identifier: "Leave"
@ -500,7 +500,7 @@ public extension UIContextualAction {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: confirmationModalTitle,
attributedExplanation: confirmationModalExplanation,
body: .attributedText(confirmationModalExplanation),
confirmTitle: "TXT_DELETE_TITLE".localized(),
confirmAccessibility: Accessibility(
identifier: "Confirm delete"

View File

@ -34,7 +34,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let halfResolution: Double = LinkPreview.timstampResolution
return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) AND (\(linkPreview[.timestamp]) + \(halfResolution)))"
return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) * 1000 AND (\(linkPreview[.timestamp]) + \(halfResolution)) * 1000)"
}()
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
@ -251,12 +251,11 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
let halfResolution: Double = LinkPreview.timstampResolution
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
return request(for: Interaction.linkPreview)
.filter(
(Interaction.Columns.timestampMs >= (LinkPreview.Columns.timestamp - halfResolution)) &&
(Interaction.Columns.timestampMs <= (LinkPreview.Columns.timestamp + halfResolution))
(timestampMs >= (LinkPreview.Columns.timestamp - halfResolution) * 1000) &&
(timestampMs <= (LinkPreview.Columns.timestamp + halfResolution) * 1000)
)
}

View File

@ -103,8 +103,14 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var canWrite: Bool {
switch threadVariant {
case .contact: return true
case .legacyGroup, .group: return (currentUserIsClosedGroupMember == true && interactionVariant?.isGroupLeavingStatus != true)
case .community: return (openGroupPermissions?.contains(.write) ?? false)
case .legacyGroup, .group:
return (
currentUserIsClosedGroupMember == true &&
interactionVariant?.isGroupLeavingStatus != true
)
case .community:
return (openGroupPermissions?.contains(.write) ?? false)
}
}

View File

@ -111,7 +111,7 @@ public struct ProfileManager {
.fileExists(atPath: ProfileManager.profileAvatarFilepath(filename: fileName))
}
private static func loadProfileData(with fileName: String) -> Data? {
public static func loadProfileData(with fileName: String) -> Data? {
let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName)
return try? Data(contentsOf: URL(fileURLWithPath: filePath))

View File

@ -163,7 +163,7 @@ final class SAEScreenLockViewController: ScreenLockViewController {
targetView: self.view,
info: ConfirmationModal.Info(
title: "SCREEN_LOCK_UNLOCK_FAILED".localized(),
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: { [weak self] in self?.ensureUI() } // After the alert, update the UI

View File

@ -226,7 +226,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
targetView: self.view,
info: ConfirmationModal.Info(
title: "Session",
explanation: error.localizedDescription,
body: .text(error.localizedDescription),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: { [weak self] in self?.extensionContext?.cancelRequest(withError: error) }

View File

@ -84,7 +84,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges()
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges()
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

View File

@ -3,133 +3,14 @@
import UIKit
import SessionUtilitiesKit
// FIXME: Refactor as part of the Groups Rebuild
public class ConfirmationModal: Modal {
public struct Info: Equatable, Hashable {
public enum State {
case whenEnabled
case whenDisabled
case always
public func shouldShow(for value: Bool) -> Bool {
switch self {
case .whenEnabled: return (value == true)
case .whenDisabled: return (value == false)
case .always: return true
}
}
}
let title: String
let explanation: String?
let attributedExplanation: NSAttributedString?
let accessibility: Accessibility?
public let stateToShow: State
let confirmTitle: String?
let confirmAccessibility: Accessibility?
let confirmStyle: ThemeValue
let cancelTitle: String
let cancelAccessibility: Accessibility?
let cancelStyle: ThemeValue
let dismissOnConfirm: Bool
let onConfirm: ((UIViewController) -> ())?
let afterClosed: (() -> ())?
// MARK: - Initialization
public init(
title: String,
explanation: String? = nil,
attributedExplanation: NSAttributedString? = nil,
accessibility: Accessibility? = nil,
stateToShow: State = .always,
confirmTitle: String? = nil,
confirmAccessibility: Accessibility? = nil,
confirmStyle: ThemeValue = .alert_text,
cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
cancelAccessibility: Accessibility? = Accessibility(
identifier: "Cancel"
),
cancelStyle: ThemeValue = .danger,
dismissOnConfirm: Bool = true,
onConfirm: ((UIViewController) -> ())? = nil,
afterClosed: (() -> ())? = nil
) {
self.title = title
self.explanation = explanation
self.attributedExplanation = attributedExplanation
self.accessibility = accessibility
self.stateToShow = stateToShow
self.confirmTitle = confirmTitle
self.confirmAccessibility = confirmAccessibility
self.confirmStyle = confirmStyle
self.cancelTitle = cancelTitle
self.cancelAccessibility = cancelAccessibility
self.cancelStyle = cancelStyle
self.dismissOnConfirm = dismissOnConfirm
self.onConfirm = onConfirm
self.afterClosed = afterClosed
}
// MARK: - Mutation
public func with(
onConfirm: ((UIViewController) -> ())? = nil,
afterClosed: (() -> ())? = nil
) -> Info {
return Info(
title: self.title,
explanation: self.explanation,
attributedExplanation: self.attributedExplanation,
accessibility: self.accessibility,
stateToShow: self.stateToShow,
confirmTitle: self.confirmTitle,
confirmAccessibility: self.confirmAccessibility,
confirmStyle: self.confirmStyle,
cancelTitle: self.cancelTitle,
cancelAccessibility: self.cancelAccessibility,
cancelStyle: self.cancelStyle,
dismissOnConfirm: self.dismissOnConfirm,
onConfirm: (onConfirm ?? self.onConfirm),
afterClosed: (afterClosed ?? self.afterClosed)
)
}
// MARK: - Confirmance
public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool {
return (
lhs.title == rhs.title &&
lhs.explanation == rhs.explanation &&
lhs.attributedExplanation == rhs.attributedExplanation &&
lhs.accessibility == rhs.accessibility &&
lhs.stateToShow == rhs.stateToShow &&
lhs.confirmTitle == rhs.confirmTitle &&
lhs.confirmAccessibility == rhs.confirmAccessibility &&
lhs.confirmStyle == rhs.confirmStyle &&
lhs.cancelTitle == rhs.cancelTitle &&
lhs.cancelAccessibility == rhs.cancelAccessibility &&
lhs.cancelStyle == rhs.cancelStyle &&
lhs.dismissOnConfirm == rhs.dismissOnConfirm
)
}
public func hash(into hasher: inout Hasher) {
title.hash(into: &hasher)
explanation.hash(into: &hasher)
attributedExplanation.hash(into: &hasher)
accessibility.hash(into: &hasher)
stateToShow.hash(into: &hasher)
confirmTitle.hash(into: &hasher)
confirmAccessibility.hash(into: &hasher)
confirmStyle.hash(into: &hasher)
cancelTitle.hash(into: &hasher)
cancelAccessibility.hash(into: &hasher)
cancelStyle.hash(into: &hasher)
dismissOnConfirm.hash(into: &hasher)
}
}
private static let imageSize: CGFloat = 80
private static let closeSize: CGFloat = 24
private let internalOnConfirm: (UIViewController) -> ()
private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil
private var internalOnCancel: ((ConfirmationModal) -> ())? = nil
private var internalOnBodyTap: (() -> ())? = nil
// MARK: - Components
@ -151,6 +32,24 @@ public class ConfirmationModal: Modal {
result.textAlignment = .center
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
result.isHidden = true
return result
}()
private lazy var imageViewContainer: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var imageView: UIImageView = {
let result: UIImageView = UIImageView()
result.clipsToBounds = true
result.contentMode = .scaleAspectFill
result.set(.width, to: ConfirmationModal.imageSize)
result.set(.height, to: ConfirmationModal.imageSize)
return result
}()
@ -174,7 +73,7 @@ public class ConfirmationModal: Modal {
}()
private lazy var contentStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ])
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ])
result.axis = .vertical
result.spacing = Values.smallSpacing
result.isLayoutMarginsRelativeArrangement = true
@ -185,13 +84,41 @@ public class ConfirmationModal: Modal {
right: Values.largeSpacing
)
let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(bodyTapped)
)
result.addGestureRecognizer(gestureRecogniser)
return result
}()
private lazy var mainStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
result.axis = .vertical
result.spacing = Values.largeSpacing - Values.smallFontSize / 2
return result
}()
private lazy var closeButton: UIButton = {
let result: UIButton = UIButton()
result.setImage(
UIImage(named: "X")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.imageView?.contentMode = .scaleAspectFit
result.themeTintColor = .textPrimary
result.contentEdgeInsets = UIEdgeInsets(
top: 6,
left: 6,
bottom: 6,
right: 6
)
result.set(.width, to: ConfirmationModal.closeSize)
result.set(.height, to: ConfirmationModal.closeSize)
result.addTarget(self, action: #selector(close), for: .touchUpInside)
result.isHidden = true
return result
}()
@ -199,50 +126,11 @@ public class ConfirmationModal: Modal {
// MARK: - Lifecycle
public init(targetView: UIView? = nil, info: Info) {
self.internalOnConfirm = { viewController in
if info.dismissOnConfirm {
viewController.dismiss(animated: true)
}
info.onConfirm?(viewController)
}
super.init(targetView: targetView, afterClosed: info.afterClosed)
super.init(targetView: targetView, dismissType: info.dismissType, afterClosed: info.afterClosed)
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve
// Set the content based on the provided info
titleLabel.text = info.title
// Note: We should only set the appropriate explanation/attributedExplanation value (as
// setting both when one is null can result in the other being removed)
if let explanation: String = info.explanation {
explanationLabel.text = explanation
}
if let attributedExplanation: NSAttributedString = info.attributedExplanation {
explanationLabel.attributedText = attributedExplanation
}
explanationLabel.isHidden = (
info.explanation == nil &&
info.attributedExplanation == nil
)
confirmButton.accessibilityLabel = info.confirmAccessibility?.label
confirmButton.accessibilityIdentifier = info.confirmAccessibility?.identifier
confirmButton.isAccessibilityElement = true
confirmButton.setTitle(info.confirmTitle, for: .normal)
confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
confirmButton.isHidden = (info.confirmTitle == nil)
cancelButton.accessibilityLabel = info.cancelAccessibility?.label
cancelButton.accessibilityIdentifier = info.cancelAccessibility?.identifier
cancelButton.isAccessibilityElement = true
cancelButton.setTitle(info.cancelTitle, for: .normal)
cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal)
contentView.accessibilityLabel = info.accessibility?.label
contentView.accessibilityIdentifier = info.accessibility?.identifier
self.updateContent(with: info)
}
required init?(coder: NSCoder) {
@ -251,13 +139,320 @@ public class ConfirmationModal: Modal {
public override func populateContentView() {
contentView.addSubview(mainStackView)
contentView.addSubview(closeButton)
imageViewContainer.addSubview(imageView)
imageView.center(.horizontal, in: imageViewContainer)
imageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15)
imageView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -15)
mainStackView.pin(to: contentView)
closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
closeButton.pin(.right, to: .right, of: contentView, withInset: -8)
}
// MARK: - Content
public func updateContent(with info: Info) {
internalOnBodyTap = nil
internalOnConfirm = { modal in
if info.dismissOnConfirm {
modal.close()
}
info.onConfirm?(modal)
}
internalOnCancel = { modal in
guard info.onCancel != nil else { return modal.close() }
info.onCancel?(modal)
}
// Set the content based on the provided info
titleLabel.text = info.title
switch info.body {
case .none:
mainStackView.spacing = Values.smallSpacing
case .text(let text):
mainStackView.spacing = Values.smallSpacing
explanationLabel.text = text
explanationLabel.isHidden = false
case .attributedText(let attributedText):
mainStackView.spacing = Values.smallSpacing
explanationLabel.attributedText = attributedText
explanationLabel.isHidden = false
case .image(let placeholder, let value, let style, let onClick):
mainStackView.spacing = 0
imageView.image = (value ?? placeholder)
imageView.layer.cornerRadius = (style == .circular ?
(ConfirmationModal.imageSize / 2) :
0
)
imageViewContainer.isHidden = false
internalOnBodyTap = onClick
}
confirmButton.accessibilityLabel = info.confirmAccessibility?.label
confirmButton.accessibilityIdentifier = info.confirmAccessibility?.identifier
confirmButton.isAccessibilityElement = true
confirmButton.setTitle(info.confirmTitle, for: .normal)
confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
confirmButton.setThemeTitleColor(.disabled, for: .disabled)
confirmButton.isHidden = (info.confirmTitle == nil)
confirmButton.isEnabled = info.confirmEnabled
cancelButton.accessibilityLabel = info.cancelAccessibility?.label
cancelButton.accessibilityIdentifier = info.cancelAccessibility?.identifier
cancelButton.isAccessibilityElement = true
cancelButton.setTitle(info.cancelTitle, for: .normal)
cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal)
cancelButton.setThemeTitleColor(.disabled, for: .disabled)
cancelButton.isEnabled = info.cancelEnabled
closeButton.isHidden = !info.hasCloseButton
contentView.accessibilityLabel = info.accessibility?.label
contentView.accessibilityIdentifier = info.accessibility?.identifier
}
// MARK: - Interaction
@objc private func bodyTapped() {
internalOnBodyTap?()
}
@objc private func confirmationPressed() {
internalOnConfirm(self)
internalOnConfirm?(self)
}
override public func cancel() {
internalOnCancel?(self)
}
}
// MARK: - Types
public extension ConfirmationModal {
struct Info: Equatable, Hashable {
let title: String
let body: Body
let accessibility: Accessibility?
public let showCondition: ShowCondition
let confirmTitle: String?
let confirmAccessibility: Accessibility?
let confirmStyle: ThemeValue
let confirmEnabled: Bool
let cancelTitle: String
let cancelAccessibility: Accessibility?
let cancelStyle: ThemeValue
let cancelEnabled: Bool
let hasCloseButton: Bool
let dismissOnConfirm: Bool
let dismissType: Modal.DismissType
let onConfirm: ((ConfirmationModal) -> ())?
let onCancel: ((ConfirmationModal) -> ())?
let afterClosed: (() -> ())?
// MARK: - Initialization
public init(
title: String,
body: Body = .none,
accessibility: Accessibility? = nil,
showCondition: ShowCondition = .none,
confirmTitle: String? = nil,
confirmAccessibility: Accessibility? = nil,
confirmStyle: ThemeValue = .alert_text,
confirmEnabled: Bool = true,
cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
cancelAccessibility: Accessibility? = Accessibility(
identifier: "Cancel"
),
cancelStyle: ThemeValue = .danger,
cancelEnabled: Bool = true,
hasCloseButton: Bool = false,
dismissOnConfirm: Bool = true,
dismissType: Modal.DismissType = .recursive,
onConfirm: ((ConfirmationModal) -> ())? = nil,
onCancel: ((ConfirmationModal) -> ())? = nil,
afterClosed: (() -> ())? = nil
) {
self.title = title
self.body = body
self.accessibility = accessibility
self.showCondition = showCondition
self.confirmTitle = confirmTitle
self.confirmAccessibility = confirmAccessibility
self.confirmStyle = confirmStyle
self.confirmEnabled = confirmEnabled
self.cancelTitle = cancelTitle
self.cancelAccessibility = cancelAccessibility
self.cancelStyle = cancelStyle
self.cancelEnabled = cancelEnabled
self.hasCloseButton = hasCloseButton
self.dismissOnConfirm = dismissOnConfirm
self.dismissType = dismissType
self.onConfirm = onConfirm
self.onCancel = onCancel
self.afterClosed = afterClosed
}
// MARK: - Mutation
public func with(
body: Body? = nil,
confirmEnabled: Bool? = nil,
cancelEnabled: Bool? = nil,
onConfirm: ((ConfirmationModal) -> ())? = nil,
onCancel: ((ConfirmationModal) -> ())? = nil,
afterClosed: (() -> ())? = nil
) -> Info {
return Info(
title: self.title,
body: (body ?? self.body),
accessibility: self.accessibility,
showCondition: self.showCondition,
confirmTitle: self.confirmTitle,
confirmAccessibility: self.confirmAccessibility,
confirmStyle: self.confirmStyle,
confirmEnabled: (confirmEnabled ?? self.confirmEnabled),
cancelTitle: self.cancelTitle,
cancelAccessibility: self.cancelAccessibility,
cancelStyle: self.cancelStyle,
cancelEnabled: (cancelEnabled ?? self.cancelEnabled),
hasCloseButton: self.hasCloseButton,
dismissOnConfirm: self.dismissOnConfirm,
dismissType: self.dismissType,
onConfirm: (onConfirm ?? self.onConfirm),
onCancel: (onCancel ?? self.onCancel),
afterClosed: (afterClosed ?? self.afterClosed)
)
}
// MARK: - Confirmance
public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool {
return (
lhs.title == rhs.title &&
lhs.body == rhs.body &&
lhs.accessibility == rhs.accessibility &&
lhs.showCondition == rhs.showCondition &&
lhs.confirmTitle == rhs.confirmTitle &&
lhs.confirmAccessibility == rhs.confirmAccessibility &&
lhs.confirmStyle == rhs.confirmStyle &&
lhs.confirmEnabled == rhs.confirmEnabled &&
lhs.cancelTitle == rhs.cancelTitle &&
lhs.cancelAccessibility == rhs.cancelAccessibility &&
lhs.cancelStyle == rhs.cancelStyle &&
lhs.cancelEnabled == rhs.cancelEnabled &&
lhs.hasCloseButton == rhs.hasCloseButton &&
lhs.dismissOnConfirm == rhs.dismissOnConfirm &&
lhs.dismissType == rhs.dismissType
)
}
public func hash(into hasher: inout Hasher) {
title.hash(into: &hasher)
body.hash(into: &hasher)
accessibility.hash(into: &hasher)
showCondition.hash(into: &hasher)
confirmTitle.hash(into: &hasher)
confirmAccessibility.hash(into: &hasher)
confirmStyle.hash(into: &hasher)
confirmEnabled.hash(into: &hasher)
cancelTitle.hash(into: &hasher)
cancelAccessibility.hash(into: &hasher)
cancelStyle.hash(into: &hasher)
cancelEnabled.hash(into: &hasher)
hasCloseButton.hash(into: &hasher)
dismissOnConfirm.hash(into: &hasher)
dismissType.hash(into: &hasher)
}
}
}
public extension ConfirmationModal.Info {
// MARK: - ShowCondition
enum ShowCondition {
case none
case enabled
case disabled
public func shouldShow(for value: Bool) -> Bool {
switch self {
case .none: return true
case .enabled: return (value == true)
case .disabled: return (value == false)
}
}
}
// MARK: - Body
enum Body: Equatable, Hashable {
public enum ImageStyle: Equatable, Hashable {
case inherit
case circular
}
case none
case text(String)
case attributedText(NSAttributedString)
// FIXME: Implement these
// case input(placeholder: String, value: String?)
// case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)])
case image(
placeholder: UIImage?,
value: UIImage?,
style: ImageStyle,
onClick: (() -> ())
)
public static func == (lhs: ConfirmationModal.Info.Body, rhs: ConfirmationModal.Info.Body) -> Bool {
switch (lhs, rhs) {
case (.none, .none): return true
case (.text(let lhsText), .text(let rhsText)): return (lhsText == rhsText)
case (.attributedText(let lhsText), .attributedText(let rhsText)): return (lhsText == rhsText)
// FIXME: Implement these
//case (.input(let lhsPlaceholder, let lhsValue), .input(let rhsPlaceholder, let rhsValue)):
// return (
// lhsPlaceholder == rhsPlaceholder &&
// lhsValue == rhsValue &&
// )
// FIXME: Implement these
//case (.radio(let lhsExplanation, let lhsOptions), .radio(let rhsExplanation, let rhsOptions)):
// return (
// lhsExplanation == rhsExplanation &&
// lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" }
// )
case (.image(let lhsPlaceholder, let lhsValue, let lhsStyle, _), .image(let rhsPlaceholder, let rhsValue, let rhsStyle, _)):
return (
lhsPlaceholder == rhsPlaceholder &&
lhsValue == rhsValue &&
lhsStyle == rhsStyle
)
default: return false
}
}
public func hash(into hasher: inout Hasher) {
switch self {
case .none: break
case .text(let text): text.hash(into: &hasher)
case .attributedText(let text): text.hash(into: &hasher)
case .image(let placeholder, let value, let style, _):
placeholder.hash(into: &hasher)
value.hash(into: &hasher)
style.hash(into: &hasher)
}
}
}
}

View File

@ -6,6 +6,12 @@ import SessionUtilitiesKit
open class Modal: UIViewController, UIGestureRecognizerDelegate {
private static let cornerRadius: CGFloat = 11
public enum DismissType: Equatable, Hashable {
case single
case recursive
}
private let dismissType: DismissType
private let afterClosed: (() -> ())?
// MARK: - Components
@ -47,14 +53,19 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate {
public lazy var cancelButton: UIButton = {
let result: UIButton = Modal.createButton(title: "cancel".localized(), titleColor: .textPrimary)
result.addTarget(self, action: #selector(close), for: .touchUpInside)
result.addTarget(self, action: #selector(cancel), for: .touchUpInside)
return result
}()
// MARK: - Lifecycle
public init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
public init(
targetView: UIView? = nil,
dismissType: DismissType = .recursive,
afterClosed: (() -> ())? = nil
) {
self.dismissType = dismissType
self.afterClosed = afterClosed
super.init(nibName: nil, bundle: nil)
@ -129,13 +140,22 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate {
// MARK: - Interaction
@objc func close() {
@objc public func cancel() {
close()
}
@objc public final func close() {
// Recursively dismiss all modals (ie. find the first modal presented by a non-modal
// and get that to dismiss it's presented view controller)
var targetViewController: UIViewController? = self
while targetViewController?.presentingViewController is Modal {
targetViewController = targetViewController?.presentingViewController
switch dismissType {
case .single: break
case .recursive:
while targetViewController?.presentingViewController is Modal {
targetViewController = targetViewController?.presentingViewController
}
}
targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in

View File

@ -21,7 +21,7 @@ public final class Values : NSObject {
@objc public static let smallButtonHeight = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(28)
@objc public static let mediumButtonHeight = isIPhone5OrSmaller ? CGFloat(30) : CGFloat(34)
@objc public static let largeButtonHeight = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45)
@objc public static let alertButtonHeight: CGFloat = 50
@objc public static let alertButtonHeight: CGFloat = 51 // 19px tall font with 16px margins
@objc public static let accentLineThickness = CGFloat(4)

View File

@ -1,4 +1,4 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -661,7 +661,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
targetView: CurrentAppContext().frontmostViewController()?.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(),
body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)