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
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
extension MediaInfoVC {
|
||||
final class MediaInfoView: UIView {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
extension MediaInfoVC {
|
||||
final class MediaPreviewView: UIView {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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-----
|
||||
|
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 369 B |
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 893 B |
|
@ -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
|
||||
}
|
||||
}
|
BIN
Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@2x.png
vendored
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@3x.png
vendored
Normal file
After Width: | Height: | Size: 5.8 KiB |
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 d’Apple.";
|
||||
"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 d’uniquement 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 d’authentification";
|
||||
/* 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 n’ont 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 n’ont 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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|