Merge remote-tracking branch 'upstream/dev' into feature/job-runner-unit-tests

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	SessionUtilitiesKit/Database/Models/Job.swift
#	SessionUtilitiesKit/Database/Models/JobDependencies.swift
#	SessionUtilitiesKit/JobRunner/JobRunner.swift
This commit is contained in:
Morgan Pretty 2023-05-23 09:52:42 +10:00
commit 77b6faccb3
141 changed files with 2988 additions and 1164 deletions

View File

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

View File

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

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

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

View File

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

12
Podfile
View File

@ -105,6 +105,9 @@ post_install do |installer|
enable_whole_module_optimization_for_crypto_swift(installer)
set_minimum_deployment_target(installer)
enable_fts5_support(installer)
#FIXME: Remove this workaround once an official fix is released (hopefully Cocoapods 1.12.1)
xcode_14_3_workaround(installer)
end
def enable_whole_module_optimization_for_crypto_swift(installer)
@ -135,3 +138,12 @@ def enable_fts5_support(installer)
end
end
end
# Workaround for Xcode 14.3:
# Sourced from https://github.com/flutter/flutter/issues/123852#issuecomment-1493232105
def xcode_14_3_workaround(installer)
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks.sh\'')
system('sed -i \'\' \'44s/readlink/readlink -f/\' \'Pods/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks.sh\'')
end

View File

@ -27,7 +27,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)
@ -222,7 +222,7 @@ SPEC CHECKSUMS:
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7
GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 97324ae5888b01db2f2adc4dcc239e2e7d6867f7
PODFILE CHECKSUM: e9443a8235dbff1fc342aa9bf08bbc66923adf68
COCOAPODS: 1.11.3

View File

@ -109,7 +109,13 @@
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */; };
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */; };
7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; };
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */; };
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */; };
7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */; };
7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
@ -182,9 +188,6 @@
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; };
B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */; };
B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */; };
B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B926157F20004D1FE1 /* storage-seed-3.crt */; };
B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B726157F20004D1FE1 /* storage-seed-1.crt */; };
B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; };
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; };
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
@ -428,9 +431,6 @@
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.swift */; };
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; };
C38EF48A255B7E3F007E1867 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
C3A01E05261D24C400290BEB /* public-loki-foundation.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E02261D24C400290BEB /* public-loki-foundation.der */; };
C3A01E06261D24C400290BEB /* storage-seed-1.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E03261D24C400290BEB /* storage-seed-1.der */; };
C3A01E07261D24C400290BEB /* storage-seed-3.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E04261D24C400290BEB /* storage-seed-3.der */; };
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */; };
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D0A2558989C0043A11F /* MessageWrapper.swift */; };
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1C25589AC30043A11F /* WebSocketProto.swift */; };
@ -832,6 +832,12 @@
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; };
FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; };
FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; };
FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */; };
FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */; };
FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */; };
FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */; };
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; };
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; };
@ -1184,8 +1190,14 @@
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = "<group>"; };
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = "<group>"; };
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = "<group>"; };
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = "<group>"; };
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselViewDelegate.swift; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
@ -1271,9 +1283,6 @@
B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = "<group>"; };
B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedConversationCell.swift; sourceTree = "<group>"; };
B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerVC.swift; sourceTree = "<group>"; };
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; sourceTree = "<group>"; };
B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = "<group>"; };
B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = "<group>"; };
B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = "<group>"; };
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
@ -1547,9 +1556,6 @@
C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
C396469E2509D40400B0B9F5 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "vi-VN.lproj/Localizable.strings"; sourceTree = "<group>"; };
C396469F2509D41100B0B9F5 /* id-ID */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "id-ID"; path = "id-ID.lproj/Localizable.strings"; sourceTree = "<group>"; };
C3A01E02261D24C400290BEB /* public-loki-foundation.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "public-loki-foundation.der"; sourceTree = "<group>"; };
C3A01E03261D24C400290BEB /* storage-seed-1.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "storage-seed-1.der"; sourceTree = "<group>"; };
C3A01E04261D24C400290BEB /* storage-seed-3.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "storage-seed-3.der"; sourceTree = "<group>"; };
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKReachabilityManager.swift; sourceTree = "<group>"; };
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageWrapper.swift; sourceTree = "<group>"; };
C3A71D1C25589AC30043A11F /* WebSocketProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketProto.swift; sourceTree = "<group>"; };
@ -1914,6 +1920,12 @@
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = "<group>"; };
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed1-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed2-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed3-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = "<group>"; };
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = "<group>"; };
FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = "<group>"; };
@ -2374,12 +2386,12 @@
B81D260326158DF5004D1FE1 /* Certificates */ = {
isa = PBXGroup;
children = (
B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */,
C3A01E02261D24C400290BEB /* public-loki-foundation.der */,
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */,
C3A01E03261D24C400290BEB /* storage-seed-1.der */,
B81D25B926157F20004D1FE1 /* storage-seed-3.crt */,
C3A01E04261D24C400290BEB /* storage-seed-3.der */,
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */,
FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */,
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */,
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */,
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */,
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */,
);
path = Certificates;
sourceTree = "<group>";
@ -2596,6 +2608,9 @@
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */,
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */,
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -2999,6 +3014,9 @@
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */,
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */,
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */,
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */,
);
path = "Media Viewing & Editing";
sourceTree = "<group>";
@ -4727,19 +4745,16 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */,
B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */,
B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */,
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */,
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */,
34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */,
C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */,
B6F509971AA53F760068F56A /* Localizable.strings in Resources */,
C3A01E05261D24C400290BEB /* public-loki-foundation.der in Resources */,
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */,
7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */,
34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */,
FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */,
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */,
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */,
34C3C78D20409F320000134C /* Opening.m4r in Resources */,
@ -4751,12 +4766,12 @@
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */,
7B0EFDF4275490EA00FFAAE7 /* ringing.mp3 in Resources */,
45B74A852044AAB600CD42F8 /* bamboo.aifc in Resources */,
C3A01E06261D24C400290BEB /* storage-seed-1.der in Resources */,
45B74A782044AAB600CD42F8 /* bamboo-quiet.aifc in Resources */,
45B74A7B2044AAB600CD42F8 /* chord.aifc in Resources */,
45B74A812044AAB600CD42F8 /* chord-quiet.aifc in Resources */,
45B74A832044AAB600CD42F8 /* circles.aifc in Resources */,
45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */,
FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */,
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */,
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */,
B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */,
@ -4769,8 +4784,11 @@
45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */,
7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */,
45B74A792044AAB600CD42F8 /* input.aifc in Resources */,
FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */,
C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */,
45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */,
FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */,
FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */,
45B74A7A2044AAB600CD42F8 /* keys.aifc in Resources */,
45B74A762044AAB600CD42F8 /* keys-quiet.aifc in Resources */,
45B74A862044AAB600CD42F8 /* note.aifc in Resources */,
@ -4780,7 +4798,7 @@
45B74A822044AAB600CD42F8 /* pulse.aifc in Resources */,
C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */,
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */,
C3A01E07261D24C400290BEB /* storage-seed-3.der in Resources */,
FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */,
45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */,
45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */,
45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */,
@ -5623,6 +5641,7 @@
buildActionMask = 2147483647;
files = (
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */,
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
@ -5637,6 +5656,7 @@
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */,
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */,
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */,
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */,
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
@ -5680,6 +5700,7 @@
FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */,
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */,
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */,
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */,
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
@ -5739,6 +5760,7 @@
FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */,
FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */,
FD37E9DB28A244E9003AE748 /* ThemePreviewView.swift in Sources */,
7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */,
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */,
@ -5787,6 +5809,7 @@
FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */,
7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */,
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */,
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */,
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,
7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */,
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
@ -5808,6 +5831,7 @@
FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */,
FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */,
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */,
7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */,
FD37E9CC28A1E578003AE748 /* AppearanceViewController.swift in Sources */,
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */,
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
@ -6092,7 +6116,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6117,7 +6141,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6165,7 +6189,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6195,7 +6219,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6231,7 +6255,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6254,7 +6278,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6305,7 +6329,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6333,7 +6357,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7233,7 +7257,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7272,7 +7296,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7305,7 +7329,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 398;
CURRENT_PROJECT_VERSION = 406;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7344,7 +7368,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.9;
MARKETING_VERSION = 2.2.14;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -356,6 +356,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
webRTCSession.attachLocalRenderer(renderer)
}
func removeLocalVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.removeLocalRenderer(renderer)
}
// MARK: - Delegate
public func webRTCIsConnected() {

View File

@ -8,6 +8,9 @@ import SessionMessagingKit
import SessionUtilitiesKit
final class CallVC: UIViewController, VideoPreviewDelegate {
static let floatingVideoViewWidth: CGFloat = UIDevice.current.isIPad ? 160 : 80
static let floatingVideoViewHeight: CGFloat = UIDevice.current.isIPad ? 346: 173
let call: SessionCall
var latestKnownAudioOutputDeviceName: String?
var durationTimer: Timer?
@ -21,27 +24,90 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result
}()
enum FloatingViewVideoSource {
case local
case remote
}
var floatingViewVideoSource: FloatingViewVideoSource = .local
// MARK: - UI Components
private lazy var localVideoView: LocalVideoView = {
private lazy var floatingLocalVideoView: LocalVideoView = {
let result = LocalVideoView()
result.clipsToBounds = true
result.alpha = 0
result.themeBackgroundColor = .backgroundSecondary
result.isHidden = !call.isVideoEnabled
result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
result.layer.masksToBounds = true
result.set(.width, to: LocalVideoView.width)
result.set(.height, to: LocalVideoView.height)
result.makeViewDraggable()
result.set(.width, to: Self.floatingVideoViewWidth)
result.set(.height, to: Self.floatingVideoViewHeight)
return result
}()
private lazy var remoteVideoView: RemoteVideoView = {
private lazy var floatingRemoteVideoView: RemoteVideoView = {
let result = RemoteVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundSecondary
result.set(.width, to: Self.floatingVideoViewWidth)
result.set(.height, to: Self.floatingVideoViewHeight)
return result
}()
private lazy var fullScreenLocalVideoView: LocalVideoView = {
let result = LocalVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundPrimary
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped)))
return result
}()
private lazy var fullScreenRemoteVideoView: RemoteVideoView = {
let result = RemoteVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundPrimary
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped)))
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped)))
return result
}()
private lazy var floatingViewContainer: UIView = {
let result = UIView()
result.isHidden = true
result.clipsToBounds = true
result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
result.layer.masksToBounds = true
result.themeBackgroundColor = .backgroundSecondary
result.makeViewDraggable()
let noVideoIcon: UIImageView = UIImageView(
image: UIImage(systemName: "video.slash")?
.withRenderingMode(.alwaysTemplate)
)
noVideoIcon.themeTintColor = .textPrimary
noVideoIcon.set(.width, to: 34)
noVideoIcon.set(.height, to: 28)
result.addSubview(noVideoIcon)
noVideoIcon.center(in: result)
result.addSubview(floatingLocalVideoView)
floatingLocalVideoView.pin(to: result)
result.addSubview(floatingRemoteVideoView)
floatingRemoteVideoView.pin(to: result)
let swappingVideoIcon: UIImageView = UIImageView(
image: UIImage(systemName: "arrow.2.squarepath")?
.withRenderingMode(.alwaysTemplate)
)
swappingVideoIcon.themeTintColor = .textPrimary
swappingVideoIcon.set(.width, to: 16)
swappingVideoIcon.set(.height, to: 12)
result.addSubview(swappingVideoIcon)
swappingVideoIcon.pin(.top, to: .top, of: result, withInset: Values.smallSpacing)
swappingVideoIcon.pin(.trailing, to: .trailing, of: result, withInset: -Values.smallSpacing)
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(switchVideo)))
return result
}()
@ -265,7 +331,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
self.call.remoteVideoStateDidChange = { isEnabled in
DispatchQueue.main.async {
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = isEnabled ? 1 : 0
}
if self.callInfoLabel.alpha < 0.5 {
@ -346,7 +413,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if shouldRestartCamera { cameraManager.prepare() }
touch(call.videoCapturer)
_ = call.videoCapturer // Force the lazy var to instantiate
titleLabel.text = self.call.contactName
AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
DispatchQueue.main.async {
@ -375,13 +442,16 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
view.addSubview(profilePictureContainer)
// Remote video view
call.attachRemoteVideoRenderer(remoteVideoView)
view.addSubview(remoteVideoView)
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
remoteVideoView.pin(to: view)
call.attachRemoteVideoRenderer(fullScreenRemoteVideoView)
view.addSubview(fullScreenRemoteVideoView)
fullScreenRemoteVideoView.translatesAutoresizingMaskIntoConstraints = false
fullScreenRemoteVideoView.pin(to: view)
// Local video view
call.attachLocalVideoRenderer(localVideoView)
call.attachLocalVideoRenderer(floatingLocalVideoView)
view.addSubview(fullScreenLocalVideoView)
fullScreenLocalVideoView.translatesAutoresizingMaskIntoConstraints = false
fullScreenLocalVideoView.pin(to: view)
// Fade view
view.addSubview(fadeView)
@ -398,7 +468,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.center(.vertical, in: minimizeButton)
titleLabel.center(.horizontal, in: view)
titleLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
titleLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
// Response Panel
view.addSubview(responsePanel)
@ -431,12 +502,12 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
callDurationLabel.center(in: callInfoLabelContainer)
}
private func addLocalVideoView() {
private func addFloatingVideoView() {
let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets
CurrentAppContext().mainWindow?.addSubview(localVideoView)
localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
CurrentAppContext().mainWindow?.addSubview(floatingViewContainer)
floatingViewContainer.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
let topMargin = (safeAreaInsets?.top ?? 0) + Values.veryLargeSpacing
localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
floatingViewContainer.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
}
override func viewDidAppear(_ animated: Bool) {
@ -445,7 +516,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
shouldRestartCamera = true
addLocalVideoView()
addFloatingVideoView()
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0)
}
@ -454,7 +526,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
localVideoView.removeFromSuperview()
floatingViewContainer.removeFromSuperview()
}
// MARK: - Orientation
@ -501,7 +573,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
self.callInfoLabel.text = "Call Ended"
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = 0
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = 0
self.operationPanel.alpha = 1
self.responsePanel.alpha = 1
self.callInfoLabel.alpha = 1
@ -559,7 +632,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
@objc private func operateCamera() {
if (call.isVideoEnabled) {
localVideoView.isHidden = true
floatingViewContainer.isHidden = true
cameraManager.stop()
videoButton.themeTintColor = .textPrimary
videoButton.themeBackgroundColor = .backgroundSecondary
@ -575,7 +648,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
}
func cameraDidConfirmTurningOn() {
localVideoView.isHidden = false
floatingViewContainer.isHidden = false
let localVideoView: LocalVideoView = self.floatingViewVideoSource == .local ? self.floatingLocalVideoView : self.fullScreenLocalVideoView
localVideoView.alpha = 1
cameraManager.prepare()
cameraManager.start()
videoButton.themeTintColor = .backgroundSecondary
@ -584,6 +659,34 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
call.isVideoEnabled = true
}
@objc private func switchVideo() {
if self.floatingViewVideoSource == .remote {
call.removeRemoteVideoRenderer(self.floatingRemoteVideoView)
call.removeLocalVideoRenderer(self.fullScreenLocalVideoView)
self.floatingRemoteVideoView.alpha = 0
self.floatingLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0
self.fullScreenRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0
self.fullScreenLocalVideoView.alpha = 0
self.floatingViewVideoSource = .local
call.attachRemoteVideoRenderer(self.fullScreenRemoteVideoView)
call.attachLocalVideoRenderer(self.floatingLocalVideoView)
} else {
call.removeRemoteVideoRenderer(self.fullScreenRemoteVideoView)
call.removeLocalVideoRenderer(self.floatingLocalVideoView)
self.floatingRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0
self.floatingLocalVideoView.alpha = 0
self.fullScreenRemoteVideoView.alpha = 0
self.fullScreenLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0
self.floatingViewVideoSource = .remote
call.attachRemoteVideoRenderer(self.floatingRemoteVideoView)
call.attachLocalVideoRenderer(self.fullScreenLocalVideoView)
}
}
@objc private func switchCamera() {
cameraManager.switchCamera()
}
@ -645,7 +748,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
}
}
@objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) {
@objc private func handleFullScreenVideoViewTapped(gesture: UITapGestureRecognizer) {
let isHidden = callDurationLabel.alpha < 0.5
UIView.animate(withDuration: 0.5) {

View File

@ -87,10 +87,7 @@ class RemoteVideoView: TargetView {
// MARK: LocalVideoView
class LocalVideoView: TargetView {
static let width: CGFloat = UIDevice.current.isIPad ? 160 : 80
static let height: CGFloat = UIDevice.current.isIPad ? 346: 173
override func renderFrame(_ frame: RTCVideoFrame?) {
super.renderFrame(frame)
DispatchMainThreadSafe {

View File

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

View File

@ -305,7 +305,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
@ -350,7 +350,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
targetView: self?.view,
info: ConfirmationModal.Info(
title: "GROUP_CREATION_ERROR_TITLE".localized(),
explanation: "GROUP_CREATION_ERROR_MESSAGE".localized(),
body: .text("GROUP_CREATION_ERROR_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -35,6 +35,14 @@ extension ContextMenuVC {
// MARK: - Actions
static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_info"),
title: "context_menu_info".localized(),
accessibilityLabel: "Message info"
) { delegate?.info(cellViewModel) }
}
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(systemName: "arrow.triangle.2.circlepath"),
@ -207,6 +215,8 @@ extension ContextMenuVC {
return !currentThreadIsMessageRequest
}()
let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false)
let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate) : nil),
(canReply ? Action.reply(cellViewModel, delegate) : nil),
@ -216,6 +226,7 @@ extension ContextMenuVC {
(canDelete ? Action.delete(cellViewModel, delegate) : nil),
(canBan ? Action.ban(cellViewModel, delegate) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate) : nil),
]
.appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) })
.appending(Action.emojiPlusButton(cellViewModel, delegate))
@ -230,6 +241,7 @@ extension ContextMenuVC {
// MARK: - Delegate
protocol ContextMenuActionDelegate {
func info(_ cellViewModel: MessageViewModel)
func retry(_ cellViewModel: MessageViewModel)
func reply(_ cellViewModel: MessageViewModel)
func copy(_ cellViewModel: MessageViewModel)

View File

@ -164,7 +164,9 @@ final class ContextMenuVC: UIViewController {
let menuStackView = UIStackView(
arrangedSubviews: actions
.filter { !$0.isEmojiAction && !$0.isEmojiPlus && !$0.isDismissAction }
.map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) }
.map { action -> ActionView in
ActionView(for: action, dismiss: snDismiss)
}
)
menuStackView.axis = .vertical
menuBackgroundView.addSubview(menuStackView)

View File

@ -68,7 +68,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(),
confirmAccessibilityLabel: "Settings",
cancelAccessibilityLabel: "Cancel",
@ -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(),
confirmAccessibilityLabel: "Confirm block",
cancelAccessibilityLabel: "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
)
@ -410,7 +412,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,
@ -646,7 +648,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
@ -661,6 +663,10 @@ extension ConversationVC:
}
func inputTextViewDidChangeContent(_ inputTextView: InputTextView) {
// Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to
// appear (as that is not expected/correct behaviour)
guard !viewIsAppearing else { return }
let newText: String = (inputTextView.text ?? "")
if !newText.isEmpty {
@ -886,11 +892,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(),
confirmAccessibilityLabel: "Download media",
cancelAccessibilityLabel: "Don't download media",
@ -1537,11 +1545,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 {
@ -1578,7 +1588,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
)
@ -1596,6 +1606,17 @@ extension ConversationVC:
// MARK: - ContextMenuActionDelegate
func info(_ cellViewModel: MessageViewModel) {
let mediaInfoVC = MediaInfoVC(
attachments: (cellViewModel.attachments ?? []),
isOutgoing: (cellViewModel.variant == .standardOutgoing),
threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
interactionId: cellViewModel.id
)
navigationController?.pushViewController(mediaInfoVC, animated: true)
}
func retry(_ cellViewModel: MessageViewModel) {
Storage.shared.writeAsync { [weak self] db in
guard
@ -2033,7 +2054,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
@ -2057,7 +2078,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
)
@ -2082,7 +2103,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
@ -2106,7 +2127,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
)
@ -2216,7 +2237,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
)
@ -2287,7 +2308,7 @@ extension ConversationVC:
targetView: self.view,
info: ConfirmationModal.Info(
title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(),
explanation: (attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage),
body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: onDismiss

View File

@ -169,7 +169,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}()
lazy var snInputView: InputView = InputView(
threadVariant: self.viewModel.threadData.threadVariant,
threadVariant: self.viewModel.initialThreadVariant,
delegate: self
)
@ -180,6 +180,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2)
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
result.set(.height, to: ConversationVC.unreadCountViewSize)
result.isHidden = true
return result
}()
@ -361,12 +362,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
scrollButton.pin(.right, to: .right, of: view, withInset: -20)
messageRequestView.pin(.left, to: .left, of: view)
messageRequestView.pin(.right, to: .right, of: view)
self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
self.scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16)
messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16)
scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16)
messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10)
messageRequestBlockButton.center(.horizontal, in: messageRequestView)
@ -441,11 +442,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
didFinishInitialLayout = true
viewIsAppearing = false
if delayFirstResponder || isShowingSearchUI {
delayFirstResponder = false
@ -457,7 +453,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}
}
recoverInputView()
recoverInputView { [weak self] in
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
self?.didFinishInitialLayout = true
self?.viewIsAppearing = false
}
}
override func viewWillDisappear(_ animated: Bool) {
@ -483,7 +484,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}
@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 {
@ -1261,11 +1266,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view)
}
func recoverInputView() {
func recoverInputView(completion: (() -> ())? = nil) {
// This is a workaround for an issue where the textview is not scrollable
// after the app goes into background and goes back in foreground.
DispatchQueue.main.async {
self.snInputView.text = self.snInputView.text
completion?()
}
}
@ -1298,7 +1304,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
targetView: self?.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(),
body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -53,27 +53,65 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// MARK: - Initialization
init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionId: Int64?) {
// If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest
// unread interaction and start focused around that one
let targetInteractionId: Int64? = {
if let focusedInteractionId: Int64 = focusedInteractionId { return focusedInteractionId }
typealias InitialData = (
targetInteractionId: Int64?,
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 'focusedInteractionId' then use that, otherwise retrieve the oldest
// unread interaction and start focused around that one
let targetInteractionId: Int64? = (focusedInteractionId != nil ? focusedInteractionId :
try Interaction
.select(.id)
.filter(interaction[.wasRead] == false)
.filter(interaction[.threadId] == threadId)
.order(interaction[.timestampMs].asc)
.asRequest(of: Int64.self)
.fetchOne(db)
}
}()
)
let currentUserIsClosedGroupMember: Bool? = (threadVariant != .closedGroup ? 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 != .openGroup ? 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 (
targetInteractionId,
currentUserIsClosedGroupMember,
openGroupPermissions,
blindedKey
)
}
self.threadId = threadId
self.initialThreadVariant = threadVariant
self.focusedInteractionId = targetInteractionId
self.focusedInteractionId = initialData?.targetInteractionId
self.threadData = SessionThreadViewModel(
threadId: threadId,
threadVariant: threadVariant,
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 +131,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
// If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query
// from a `0` offset)
guard let initialFocusedId: Int64 = targetInteractionId else {
guard let initialFocusedId: Int64 = initialData?.targetInteractionId else {
self?.pagedDataObserver?.load(.pageBefore)
return
}
@ -105,21 +143,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,
currentUserIsClosedGroupMember: (self.initialThreadVariant != .closedGroup ?
nil :
Storage.shared.read { db in
try GroupMember
.filter(GroupMember.Columns.groupId == self.threadId)
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
.isNotEmpty(db)
}
)
)
.populatingCurrentUserBlindedKey()
public private(set) var threadData: SessionThreadViewModel
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance

View File

@ -37,8 +37,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)
@ -440,10 +438,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
override func resignFirstResponder() -> Bool {
inputTextView.resignFirstResponder()
}
func inputTextViewBecomeFirstResponder() {
inputTextView.becomeFirstResponder()
}
func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {
// Not relevant in this case

View File

@ -46,7 +46,7 @@ final class DocumentView: UIView {
// Size label
let sizeLabel = UILabel()
sizeLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
sizeLabel.text = OWSFormat.formatFileSize(UInt(attachment.byteCount))
sizeLabel.text = OWSFormat.formatFileSize(attachment.byteCount)
sizeLabel.themeTextColor = textColor
sizeLabel.lineBreakMode = .byTruncatingTail

View File

@ -29,7 +29,7 @@ public class MediaAlbumView: UIStackView {
mediaCache: mediaCache,
attachment: $0,
isOutgoing: isOutgoing,
maxMessageWidth: maxMessageWidth
cornerRadius: VisibleMessageCell.largeCornerRadius
)
}

View File

@ -16,10 +16,9 @@ public class MediaView: UIView {
// MARK: -
private let mediaCache: NSCache<NSString, AnyObject>
private let mediaCache: NSCache<NSString, AnyObject>?
public let attachment: Attachment
private let isOutgoing: Bool
private let maxMessageWidth: CGFloat
private var loadBlock: (() -> Void)?
private var unloadBlock: (() -> Void)?
@ -46,22 +45,21 @@ public class MediaView: UIView {
// MARK: - Initializers
public required init(
mediaCache: NSCache<NSString, AnyObject>,
mediaCache: NSCache<NSString, AnyObject>? = nil,
attachment: Attachment,
isOutgoing: Bool,
maxMessageWidth: CGFloat
cornerRadius: CGFloat
) {
self.mediaCache = mediaCache
self.attachment = attachment
self.isOutgoing = isOutgoing
self.maxMessageWidth = maxMessageWidth
super.init(frame: .zero)
themeBackgroundColor = .backgroundSecondary
clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = VisibleMessageCell.largeCornerRadius
layer.cornerRadius = cornerRadius
createContents()
}
@ -396,7 +394,7 @@ public class MediaView: UIView {
applyMediaBlock(media)
self?.mediaCache.setObject(media, forKey: cacheKey as NSString)
self?.mediaCache?.setObject(media, forKey: cacheKey as NSString)
self?.loadState.mutate { $0 = .loaded }
}
@ -405,7 +403,7 @@ public class MediaView: UIView {
return
}
if let media: AnyObject = self.mediaCache.object(forKey: cacheKey as NSString) {
if let media: AnyObject = self.mediaCache?.object(forKey: cacheKey as NSString) {
Logger.verbose("media cache hit")
guard Thread.isMainThread else {

View File

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

View File

@ -6,6 +6,7 @@
#import "OWSMath.h"
#import "UIView+OWS.h"
#import <QuartzCore/QuartzCore.h>
#import <PureLayout/PureLayout.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
#import <SessionSnodeKit/SessionSnodeKit.h>

View File

@ -98,7 +98,12 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableSettingsData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, config] db -> [SectionModel] in
.trackingConstantRegion { [weak self, config, dependencies, threadId = self.threadId] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db)
return [
SectionModel(
model: .content,
@ -109,6 +114,10 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
rightAccessory: .radio(
isSelected: { (self?.currentSelection.value == 0) }
),
isEnabled: (
maybeThreadViewModel?.threadVariant != .closedGroup ||
maybeThreadViewModel?.currentUserIsClosedGroupMember == true
),
onTap: { self?.currentSelection.send(0) }
)
].appending(
@ -122,6 +131,10 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
rightAccessory: .radio(
isSelected: { (self?.currentSelection.value == duration) }
),
isEnabled: (
maybeThreadViewModel?.threadVariant != .closedGroup ||
maybeThreadViewModel?.currentUserIsClosedGroupMember == true
),
onTap: { self?.currentSelection.send(duration) }
)
}

View File

@ -3,6 +3,7 @@
import Foundation
import Combine
import GRDB
import YYImage
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
@ -395,7 +396,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
accessibilityLabel: "Leave group",
confirmationInfo: ConfirmationModal.Info(
title: "leave_group_confirmation_alert_title".localized(),
attributedExplanation: {
body: .attributedText({
if currentUserIsClosedGroupAdmin {
return NSAttributedString(string: "admin_group_leave_warning".localized())
}
@ -412,7 +413,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
@ -548,9 +549,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() :
@ -688,13 +688,12 @@ 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
),
)
)),
accessibilityLabel: oldBlockedState == false ? "User blocked" : "Confirm unblock",
accessibilityId: "Test_name",
cancelTitle: "BUTTON_OK".localized(),

View File

@ -91,14 +91,17 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
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) {
@ -138,10 +141,6 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
}
}
private func reloadTableData() {
tableView.reloadData()
}
// MARK: - Update Search Results
private func refreshSearchResults() {
@ -155,9 +154,11 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
let searchText = rawSearchText.stripped
guard searchText.count > 0 else {
guard searchText != (lastSearchText ?? "") else { return }
searchResultSet = defaultSearchResults
lastSearchText = nil
reloadTableData()
tableView.reloadData()
return
}
guard lastSearchText != searchText else { return }
@ -212,7 +213,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
.compactMap { $0 }
.flatMap { $0 }
self?.isLoading = false
self?.reloadTableData()
self?.tableView.reloadData()
self?.refreshTimer = nil
default: break
@ -283,18 +284,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, focusedInteractionId: focusedInteractionId)
)
self.navigationController?.setViewControllers(viewControllers, animated: true)
let viewController: ConversationVC = ConversationVC(
threadId: threadId,
threadVariant: threadVariant,
focusedInteractionId: focusedInteractionId
)
self.navigationController?.pushViewController(viewController, animated: true)
}
// MARK: - UITableViewDataSource

View File

@ -308,7 +308,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
}
@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) {
@ -393,8 +396,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
// 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
}
@ -739,7 +752,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "delete_conversation_confirmation_alert_title".localized(),
attributedExplanation: confirmationModalExplanation,
body: .attributedText(confirmationModalExplanation),
confirmTitle: "TXT_DELETE_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
@ -824,7 +837,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: confirmationModalTitle,
attributedExplanation: confirmationModalExplanation,
body: .attributedText(confirmationModalExplanation),
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,

View File

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

View File

@ -214,7 +214,7 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
targetView: self?.view,
info: ConfirmationModal.Info(
title: "ALERT_ERROR_TITLE".localized(),
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

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

View File

@ -389,7 +389,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,
@ -458,7 +458,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
targetView: self.view,
info: ConfirmationModal.Info(
title: CommonStrings.errorAlertTitle,
explanation: "GIF_PICKER_VIEW_MISSING_QUERY".localized(),
body: .text("GIF_PICKER_VIEW_MISSING_QUERY".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

@ -0,0 +1,193 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import SignalUtilitiesKit
extension MediaInfoVC {
final class MediaInfoView: UIView {
private static let cornerRadius: CGFloat = 12
private var attachment: Attachment?
private let width: CGFloat = MediaInfoVC.mediaSize - 2 * MediaInfoVC.arrowSize.width
// MARK: - UI
private lazy var fileIdLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var fileTypeLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var fileSizeLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var resolutionLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var durationLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
// MARK: - Lifecycle
init(attachment: Attachment?) {
self.attachment = attachment
super.init(frame: CGRect.zero)
self.accessibilityLabel = "Media info"
setUpViewHierarchy()
update(attachment: attachment)
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
let backgroundView: UIView = UIView()
backgroundView.clipsToBounds = true
backgroundView.themeBackgroundColor = .contextMenu_background
backgroundView.layer.cornerRadius = Self.cornerRadius
addSubview(backgroundView)
backgroundView.pin(to: self)
let container: UIView = UIView()
container.set(.width, to: self.width)
// File ID
let fileIdTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_ID".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileIdContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileIdTitleLabel, fileIdLabel ])
fileIdContainerStackView.axis = .vertical
fileIdContainerStackView.spacing = 6
container.addSubview(fileIdContainerStackView)
fileIdContainerStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: container)
// File Type
let fileTypeTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_TYPE".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileTypeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileTypeTitleLabel, fileTypeLabel ])
fileTypeContainerStackView.axis = .vertical
fileTypeContainerStackView.spacing = 6
container.addSubview(fileTypeContainerStackView)
fileTypeContainerStackView.pin(.leading, to: .leading, of: container)
fileTypeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing)
// File Size
let fileSizeTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_SIZE".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileSizeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileSizeTitleLabel, fileSizeLabel ])
fileSizeContainerStackView.axis = .vertical
fileSizeContainerStackView.spacing = 6
container.addSubview(fileSizeContainerStackView)
fileSizeContainerStackView.pin(.trailing, to: .trailing, of: container)
fileSizeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing)
fileSizeContainerStackView.set(.width, to: 90)
// Resolution
let resolutionTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_RESOLUTION".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let resolutionContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ resolutionTitleLabel, resolutionLabel ])
resolutionContainerStackView.axis = .vertical
resolutionContainerStackView.spacing = 6
container.addSubview(resolutionContainerStackView)
resolutionContainerStackView.pin(.leading, to: .leading, of: container)
resolutionContainerStackView.pin(.top, to: .bottom, of: fileTypeContainerStackView, withInset: Values.largeSpacing)
// Duration
let durationTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_DURATION".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let durationContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ durationTitleLabel, durationLabel ])
durationContainerStackView.axis = .vertical
durationContainerStackView.spacing = 6
container.addSubview(durationContainerStackView)
durationContainerStackView.pin(.trailing, to: .trailing, of: container)
durationContainerStackView.pin(.top, to: .bottom, of: fileSizeContainerStackView, withInset: Values.largeSpacing)
durationContainerStackView.set(.width, to: 90)
container.pin(.bottom, to: .bottom, of: durationContainerStackView)
backgroundView.addSubview(container)
container.pin(to: backgroundView, withInset: Values.largeSpacing)
}
// MARK: - Interaction
public func update(attachment: Attachment?) {
guard let attachment: Attachment = attachment else { return }
self.attachment = attachment
fileIdLabel.text = attachment.serverId
fileTypeLabel.text = attachment.contentType
fileSizeLabel.text = OWSFormat.formatFileSize(attachment.byteCount)
resolutionLabel.text = {
guard let width = attachment.width, let height = attachment.height else { return "N/A" }
return "\(width)×\(height)"
}()
durationLabel.text = {
guard let duration = attachment.duration else { return "N/A" }
return floor(duration).formatted(format: .videoDuration)
}()
}
}
}

View File

@ -0,0 +1,63 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
extension MediaInfoVC {
final class MediaPreviewView: UIView {
private static let cornerRadius: CGFloat = 8
private let attachment: Attachment
private let isOutgoing: Bool
// MARK: - UI
private lazy var mediaView: MediaView = {
let result: MediaView = MediaView.init(
attachment: attachment,
isOutgoing: isOutgoing,
cornerRadius: 0
)
return result
}()
// MARK: - Lifecycle
init(attachment: Attachment, isOutgoing: Bool) {
self.attachment = attachment
self.isOutgoing = isOutgoing
super.init(frame: CGRect.zero)
self.accessibilityLabel = "Media info"
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
set(.width, to: MediaInfoVC.mediaSize)
set(.height, to: MediaInfoVC.mediaSize)
addSubview(mediaView)
mediaView.pin(to: self)
mediaView.loadMedia()
}
// MARK: - Copy
/// This function is used to make sure the carousel view contains this class can loop infinitely
func copyView() -> MediaPreviewView {
return MediaPreviewView(attachment: self.attachment, isOutgoing: self.isOutgoing)
}
}
}

View File

@ -0,0 +1,151 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
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
internal static let arrowSize: CGSize = CGSize(width: 20, height: 30)
private let attachments: [Attachment]
private let isOutgoing: Bool
private let threadId: String
private let threadVariant: SessionThread.Variant
private let interactionId: Int64
private var currentPage: Int = 0
// MARK: - UI
private lazy var mediaInfoView: MediaInfoView = MediaInfoView(attachment: nil)
private lazy var mediaCarouselView: SessionCarouselView = {
let slices: [MediaPreviewView] = self.attachments.map {
MediaPreviewView(
attachment: $0,
isOutgoing: self.isOutgoing
)
}
let result: SessionCarouselView = SessionCarouselView(
info: SessionCarouselView.Info(
slices: slices,
copyOfFirstSlice: slices.first?.copyView(),
copyOfLastSlice: slices.last?.copyView(),
sliceSize: CGSize(
width: Self.mediaSize,
height: Self.mediaSize
),
shouldShowPageControl: true,
pageControlStyle: SessionCarouselView.PageControlStyle(
size: .medium,
backgroundColor: .init(white: 0, alpha: 0.4),
bottomInset: Values.mediumSpacing
),
shouldShowArrows: true,
arrowsSize: Self.arrowSize,
cornerRadius: 8
)
)
result.set(.height, to: Self.mediaSize)
result.delegate = self
return result
}()
private lazy var fullScreenButton: UIButton = {
let result: UIButton = UIButton(type: .custom)
result.setImage(
UIImage(systemName: "arrow.up.left.and.arrow.down.right")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.backgroundColor = .init(white: 0, alpha: 0.4)
result.layer.cornerRadius = 14
result.set(.width, to: 28)
result.set(.height, to: 28)
result.addTarget(self, action: #selector(showMediaFullScreen), for: .touchUpInside)
return result
}()
// MARK: - Initialization
init(
attachments: [Attachment],
isOutgoing: Bool,
threadId: String,
threadVariant: SessionThread.Variant,
interactionId: Int64
) {
self.threadId = threadId
self.threadVariant = threadVariant
self.interactionId = interactionId
self.isOutgoing = isOutgoing
self.attachments = attachments
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(attachments:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachments:) instead.")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
ViewControllerUtilities.setUpDefaultSessionStyle(
for: self,
title: "message_info_title".localized(),
hasCustomBackButton: false
)
let mediaStackView: UIStackView = UIStackView()
mediaStackView.axis = .horizontal
mediaInfoView.update(attachment: attachments[0])
mediaCarouselView.addSubview(fullScreenButton)
fullScreenButton.pin(.trailing, to: .trailing, of: mediaCarouselView, withInset: -(Values.smallSpacing + Values.veryLargeSpacing))
fullScreenButton.pin(.bottom, to: .bottom, of: mediaCarouselView, withInset: -Values.smallSpacing)
let stackView: UIStackView = UIStackView(arrangedSubviews: [ mediaCarouselView, mediaInfoView ])
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = Values.largeSpacing
self.view.addSubview(stackView)
stackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self.view)
stackView.pin(.top, to: .top, of: self.view, withInset: Values.veryLargeSpacing)
}
// MARK: - Interaction
@objc func showMediaFullScreen() {
let attachment = self.attachments[self.currentPage]
let viewController: UIViewController? = MediaGalleryViewModel.createDetailViewController(
for: self.threadId,
threadVariant: self.threadVariant,
interactionId: self.interactionId,
selectedAttachmentId: attachment.id,
options: [ .sliderEnabled ]
)
if let viewController: UIViewController = viewController {
viewController.transitioningDelegate = nil
self.present(viewController, animated: true)
}
}
// MARK: - SessionCarouselViewDelegate
func carouselViewDidScrollToNewSlice(currentPage: Int) {
self.currentPage = currentPage
mediaInfoView.update(attachment: attachments[currentPage])
}
}

View File

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

View File

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

View File

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

View File

@ -446,19 +446,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 ||
@ -482,7 +481,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
.fetchCount(db)
}
.defaulting(to: 0)
)
DispatchQueue.main.async {
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
}
}
}
}

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgIUY9RQqbjhsQEkdeSgV9L0os9xZ7AwDQYJKoZIhvcNAQEL
BQAwfDELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HzAdBgNVBAMMFnB1YmxpYy5sb2tpLmZvdW5kYXRpb24wHhcNMjEwNDA3MDExMDMx
WhcNMjMwNDA3MDExMDMxWjB8MQswCQYDVQQGEwJBVTERMA8GA1UECAwIVmljdG9y
aWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2YWN5IFRl
Y2ggRm91bmRhdGlvbjEfMB0GA1UEAwwWcHVibGljLmxva2kuZm91bmRhdGlvbjCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5dBJSIR5+VNNUxUOo6FG0e
RmZteRqBt50KXGbOi2A23a6sa57pLFh9Yw3hmlWV+QCL7ipG1X4IC55OStgoesf+
K65VwEMP6Mtq0sSJS3R5TiuV2ZSRdSZTVjUyRXVe5T4Aw6wXVTAbc/HsyS780tDh
GclfDHhonPhZpmTAnSbfMOS+BfOnBNvDxdto0kVh6k5nrGlkT4ECloulHTQF2lwJ
0D6IOtv9AJplPdg6s2c4dY7durOdvr3NNVfvn5PTeRvbEPqzZur4WUUKIPNGu6mY
PxImqd4eUsL0Vod4aAsTIx4YMmCTi0m9W6zJI6nXcK/6a+iiA3+NTNMzEA9gQhEC
AwEAAaOBjDCBiTAdBgNVHQ4EFgQU/zahokxLvvFUpbnM6z/pwS1KsvwwHwYDVR0j
BBgwFoAU/zahokxLvvFUpbnM6z/pwS1KsvwwDwYDVR0TAQH/BAUwAwEB/zAhBgNV
HREEGjAYghZwdWJsaWMubG9raS5mb3VuZGF0aW9uMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMA0GCSqGSIb3DQEBCwUAA4IBAQBql+JvoqpaYrFFTOuDn08U+pdcd3GM7tbI
zRH5LU+YnIpp9aRheek+2COW8DXsIy/kUngETCMLmX6ZaUj/WdHnTDkB0KTgxSHv
ad3ZznKPKZ26qJOklr+0ZWj4J3jHbisSzql6mqq7R2Kp4ESwzwqxvkbykM5RUnmz
Go/3Ol7bpN/ZVwwEkGfD/5rRHf57E/gZn2pBO+zotlQgr7HKRsIXQ2hIXVQqWmPQ
lvfIwrwAZlfES7BARFnHOpyVQxV8uNcV5K5eXzuVFjHBqvq+BtyGhWkP9yKJCHS9
OUXxch0rzRsH2C/kRVVhEk0pI3qlFiRC8pCJs98SNE9l69EQtG7I
-----END CERTIFICATE-----

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUPwyEuBgX6kfxt+G2tQ4GNTZErMMwDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTYyMloX
DTI1MDQxMTEyNTYyMlowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwxkbApgfFA1upIFj47y+7k+qrM0l
MLDvtX3U95icVgb7HGhxKzkzbCOscKZnVsq1N90drYVh7to0H69b2t6y7l+9q6Zd
Ytzi9U0NoL/OabmR6F+w/XpokRM7CMz9zeg84VLnyu2yRdR26keG4/AZRXk+j8Dy
6xp09+hTF7kfdfzL3HdYyUsyx+/CqoyzU01yn4aVgJ9aufYu38QKnnjfROiVahJf
Xm1MvHLmDCe+WbDFgsp2Y0NjNbpASUgrOEPNnIJeY3Lw4kzwNVGsbSBHgvLgSfaD
p5L6k89TUUKA0onlGFAN/MDXL4DNfjSpmfzHyhM8XwKJ9COSXsvvpX5hHQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAfBgNVHSMEGDAW
gBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQxLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQBW8q3DzJWVXZew9pJ1MqjqsMuNt2OlnptwIZUme/Lh
krhqBj5o87218542ao1Hkgph4IuuwEQPwJvUoUbh7dT/k+4D6Ua3oUxhmdeyFUv+
mjQKZ1mfcfrwW+6rCWJRa2mAVYfOhdfBQZgLP7NqYdskVQF5LWXSs1IF3XLTyROy
gCeapTexTvKlr/TMW4spE4ewaQ4AfB2c24iVLcpAWT+12GaJ0AYO+gY2o7LQqywN
qIxt2mbvXyf2wuhr489tmGz53mKa3Xu7JC1uU6g9zqJ4FGMYsI8pa0Ec2ODRBb8s
8W54r5LN472aTYn+UGgV8wadzPFd0FZtQABkDTuWSZY7
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUaPiMYcZh7cZZfacCni2NwT5DKh4wDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTY0NVoX
DTI1MDQxMTEyNTY0NVowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh2UcfW0I+1QWRa3cj7RnMGelYkGK
7l4V6q7je1IkudXBNretkvVF1NCpfZ8dz72JmdGPJ5/uIEW15HDD2L63OmSDVPhA
2JCb/NqmXfeO91lyxgb0sDnN1UH0wzuS75aBjaQ0nXQV3ffmqKnNNv0HK+LTMFD+
Dv2yGDtZTWH6H3VzPLCvHHYXVdyuQHwchAcNQar5k4dbdEIcYIV+ANccPg7iQ81a
ITZ9bCeACdMqbB9gILq21KWdkxCu1fwSXs/B6n+U4UpJyv87fprvAyU3HqQhqlU7
dHnzA1dPn8D4a/3CMYZogVm8USNjv4HmWIwKbYDX+VahvuZwEi6+pwEurQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRxVM4+gFFipZFAg+Fs4x580js+2TAfBgNVHSMEGDAW
gBRxVM4+gFFipZFAg+Fs4x580js+2TAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQyLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQBIFj6hsOgNVr2kZufimTxoT1TE8uvycIWyt04q6/nP
8h33u/sHuNPdnr2UewqRyDRFefxrGlqBUQAQJVyzJGIlju/HTZaBnVB0H2smCRtK
ZRHAJ/cwcnAp+STjqgPqt1ZZ6JcfFwJZID4pPmrW8WaQNAtQPi2Ly2JLQ+Ym5wus
aGxGjbDRQSWGmUpg5TE+XdDsHeJtCl6HAEjvtXfq1uzKedRzmqYfIa8Rd7b2tmuy
dN27swR4DRJOK4rAxHnI8jt7GKVtPXnYfRuk2+0dVZ4CD6qHw+CO5mcdCabnflgT
XS8BYlOvkAyVbtmZNAacoUZvPRx3o186BMJoK2coQyFN
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUEZkKsCM3Leodz+JB0ADefbWoRbswDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMB4XDTIzMDUxNzAyNDAwOFoX
DTI1MDQxMjAyNDAwOFowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4Yz/kIXn5t+VMATXsortcyK3DFF
hjNICxAt8qdLwyCCJDnedBdfeQb7zrn2A3btzfKrBD0x3JrbVHabUrtI+wFqfDLS
id2WOIIM/8RP2V/e4zanpKsk9yB/euKga+M+fybfTn1WTqQU5nEuU6eZyyEEZBk6
1rzWJstxWhcfN4rfl+ciSWLcmFLC2LuNZqwm6To77oLPj+DGrUHyRKFZ4Tw9ilcU
TpMKFaMmNzrHEzS5lPJIRa+2LD5vDYR/sv+lPiKMXTb64OTOJjTfucdsyZqWrI0R
mV2pBcrYBoDbxO+7pnr8GrJIcFqTLDI6MbjH6eseZqRHJSYKrNCyGlDeSQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRUYnrMlCbDZo6YXpnivhBui51XhDAfBgNVHSMEGDAW
gBRUYnrMlCbDZo6YXpnivhBui51XhDAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQzLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQBFYRlRODyQTIhNQC+pTapKtHdS9GJqKvyJX6NVFF6w
+oBzZGNYsDTmzaelraAuUz+uS7d0vngu5cV+3jG0DgksELT6hbpuHcad1rxAhuDv
wv/f02qJyB1F2luXma2n+NHgRFhvIYulWjV/DSSmwea2XD4DH+ZKcYeEXyT71b2T
VZfGnxLPVMz99iA6sQxsNfccFMvDxKofha7teRkUJ+SVzyutrneYySqrjGie6+Nb
oOw4CnpiqiUKIf47B6ZKlsJ8MAS8zAo6O9UqfmNdVoXFrZDjaQGPAjSH1oxL7iP5
pED6BUMytm8spiTEVBYIer/gcXaA4zWSKZ/Fd24OK0GL
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUJsox1ZQPK/6iDsCC+MUJfNAlFuYwDQYJKoZIhvcNAQEL
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQxLmxva2kubmV0d29yazAeFw0yMTA0MDcw
MTE5MjZaFw0yMzA0MDcwMTE5MjZaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMS5sb2tp
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtWH3Rz8Dd
kEmM7tcBWHrJ/G8drr/+qidboEVYzxpyRjszaDxKXVhx4eBBsAD5RuCWuTuZmM8k
TKEDLtf8xfb5SQ7YNX+346s9NXS5Poy4CIPASiW/QWXgIHFbVdv2hC+cKOP61OLM
OGnOxfig6tQyd6EaCkedpY1DvSa2lPnQSOwC/jXCx6Vboc0zTY5R2bHtNc9hjIFP
F4VClLAQSh2F4R1V9MH5KZMW+CCP6oaJY658W9JYXYRwlLrL2EFOVxHgcxq/6+fw
+axXK9OXJrGZjuA+hiz+L/uAOtE4WuxrSeuNMHSrMtM9QqVn4bBuMJ21mAzfNoMP
OIwgMT9DwUjVAgMBAAGjgZAwgY0wHQYDVR0OBBYEFOubJp9SoXIw+ONiWgkOaW8K
zI/TMB8GA1UdIwQYMBaAFOubJp9SoXIw+ONiWgkOaW8KzI/TMA8GA1UdEwEB/wQF
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMS5sb2tpLm5ldHdvcmswEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAIiHNhNrjYvwXVWs
gacx8T/dpqpu9GE3L17LotgQr4R+IYHpNtcmwOTdtWWFfUTr75OCs+c3DqgRKEoj
lnULOsVcalpAGIvW15/fmZWOf66Dpa4+ljDmAc3SOQiD0gGNtqblgI5zG1HF38QP
hjYRhCZ5CVeGOLucvQ8tVVwQvArPFIkBr0jH9jHVgRWEI2MeI3FsU2H93D4TfGln
N4SmmCfYBqygaaZBWkJEt0bYhn8uGHdU9UY9L2FPtfHVKkmFgO7cASGlvXS7B/TT
/8IgbtM3O8mZc2asmdQhGwoAKz93ryyCd8X2UZJg/IwCSCayOlYZWY2fR4OPQmmV
gxJsm+g=
-----END CERTIFICATE-----

View File

@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUc486Dy9Y00bUFfDeYmJIgSS5xREwDQYJKoZIhvcNAQEL
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQzLmxva2kubmV0d29yazAeFw0yMTA0MDcw
MTIwNTJaFw0yMzA0MDcwMTIwNTJaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMy5sb2tp
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtokMlsFzf
piYeD0EVNikMyvjltpF6fUEde9NOVrTtNTQT6kkDk+/0HF5LYgPaatv6v7fpUQHi
kIwd6F0LTRGeWDFdsaWMdtlR1n/GxLPrOROsE8dcLt6GLavPf9rDabgva93m/JD6
XW+Ne+MPEwqS8dAmFGhZd0gju6AtKFoSHnIf5pSQN6fSZUF/JQtHLVprAKKWKDiS
ZwmWbmrZR2aofLD/VRpetabajnZlv9EeWloQwvUsw1C1hkAmmtFeeXtg7ePwrOzo
6CnmcUJwOmi+LWqQV4A+58RZPFKaZoC5pzaKd0OYB8eZ8HB1F41UjGJgheX5Cyl4
+amfF3l8dSq1AgMBAAGjgZAwgY0wHQYDVR0OBBYEFM9VSq4pGydjtX92Beul4+ml
jBKtMB8GA1UdIwQYMBaAFM9VSq4pGydjtX92Beul4+mljBKtMA8GA1UdEwEB/wQF
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMy5sb2tpLm5ldHdvcmswEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAAYxmhhkcKE1n6g1
JqOa3UCBo4EfbqY5+FDZ0FVqv/cwemwVpKLbe6luRIS8poomdPCyMOS45V7wN3H9
cFpfJ1TW19ydPVKmCXrl29ngmnY1q7YDwE/4qi3VK/UiqDkTHMKWjVPkenOyi8u6
VVQANXSnKrn6GtigNFjGyD38O+j7AUSXBtXOJczaoF6r6BWgwQZ2WmgjuwvKTWSN
4r8uObERoAQYVaeXfgdr4e9X/JdskBDaLFfoW/rrSozHB4FqVNFW96k+aIUgRa5p
9kv115QcBPCSh9qOyTHij4tswS6SyOFaiKrNC4hgHQXP4QgioKmtsR/2Y+qJ6ddH
6oo+4QU=
-----END CERTIFICATE-----

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 893 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -66,17 +66,17 @@
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>public.loki.foundation</key>
<key>seed1.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>storage.seed1.loki.network</key>
<key>seed2.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>storage.seed3.loki.network</key>
<key>seed3.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
@ -84,21 +84,19 @@
</dict>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>Signal needs to use Apple Music to play media attachments.</string>
<string>Session needs to use Apple Music to play media attachments.</string>
<key>NSCameraUsageDescription</key>
<string>Session needs camera access to take pictures and scan QR codes.</string>
<key>NSContactsUsageDescription</key>
<string>Signal uses your contacts to find users you know. We do not store your contacts on the server.</string>
<key>NSFaceIDUsageDescription</key>
<string>Session's Screen Lock feature uses Face ID.</string>
<key>NSHumanReadableCopyright</key>
<string>com.loki-project.loki-messenger</string>
<key>NSMicrophoneUsageDescription</key>
<string>Session needs access to your microphone to record media.</string>
<string>Session needs access to your microphone for calls and to send to audio messages.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Session needs access to your library to save photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Session needs access to your library to send photos.</string>
<string>Session needs access to your library to update your avatar and send photos.</string>
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>
<key>UIAppFonts</key>

View File

@ -4,19 +4,6 @@
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudDocuments</string>
<string>CloudKit</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.loki-project.loki-messenger</string>

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Nur für mich löschen";
"delete_message_for_everyone" = "Für jeden löschen";
"delete_message_for_me_and_recipient" = "Für mich und %@ löschen";
"context_menu_info" = "Info";
"context_menu_reply" = "Antworten";
"context_menu_save" = "Speichern";
"context_menu_ban_user" = "Nutzer sperren";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Eliminar solo para mí";
"delete_message_for_everyone" = "Eliminar para todos";
"delete_message_for_me_and_recipient" = "Eliminar para mí y para %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Responder";
"context_menu_save" = "Guardar";
"context_menu_ban_user" = "Banear Usuario";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "حذف برای من";
"delete_message_for_everyone" = "حذف برای همه";
"delete_message_for_me_and_recipient" = "حذف برای من و %@";
"context_menu_info" = "Info";
"context_menu_reply" = "پاسخ";
"context_menu_save" = "ذخیره";
"context_menu_ban_user" = "مسدود کردن کاربر";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Poista vain minun nähtäväksi";
"delete_message_for_everyone" = "Poista kaikkien näkyviltä";
"delete_message_for_me_and_recipient" = "Poista minulta ja vastaanottajalta";
"context_menu_info" = "Info";
"context_menu_reply" = "Vastaa";
"context_menu_save" = "Tallenna";
"context_menu_ban_user" = "Estä Käyttäjä";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

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

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Izbriši samo za mene";
"delete_message_for_everyone" = "Izbriši za sve";
"delete_message_for_me_and_recipient" = "Izbriši za mene i %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Odgovori";
"context_menu_save" = "Spremi";
"context_menu_ban_user" = "Zabrani korisnik";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Elimina solo per me";
"delete_message_for_everyone" = "Elimina per tutti";
"delete_message_for_me_and_recipient" = "Elimina per me e %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Rispondi";
"context_menu_save" = "Salva";
"context_menu_ban_user" = "Banna utente";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "自分の端末から削除";
"delete_message_for_everyone" = "全員の端末から削除";
"delete_message_for_me_and_recipient" = "自分と %@ の端末から削除する";
"context_menu_info" = "Info";
"context_menu_reply" = "返信";
"context_menu_save" = "保存";
"context_menu_ban_user" = "ユーザーをBAN";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Verwijder alleen voor mij";
"delete_message_for_everyone" = "Verwijder voor iedereen";
"delete_message_for_me_and_recipient" = "Verwijderen voor mij en %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Antwoord";
"context_menu_save" = "Opslaan";
"context_menu_ban_user" = "Gebruiker verbannen";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,11 +366,12 @@
"delete_message_for_me" = "Usuń tylko dla mnie";
"delete_message_for_everyone" = "Usuń dla wszystkich";
"delete_message_for_me_and_recipient" = "Usuń dla mnie i %@";
"context_menu_ban_user_error_alert_message" = "Unable to ban user";
"context_menu_info" = "Info";
"context_menu_reply" = "Odpowiedz";
"context_menu_save" = "Zapisz";
"context_menu_ban_user" = "Zbanuj użytkownika";
"context_menu_ban_and_delete_all" = "Zbanuj i usuń wszystko";
"context_menu_ban_user_error_alert_message" = "Unable to ban user";
"accessibility_expanding_attachments_button" = "Dodaj załączniki";
"accessibility_gif_button" = "Gif";
"accessibility_document_button" = "Dokument";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Apagar para mim";
"delete_message_for_everyone" = "Apagar para todos";
"delete_message_for_me_and_recipient" = "Apagar para mim e para %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Responder";
"context_menu_save" = "Salvar";
"context_menu_ban_user" = "Banir Usuário";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Удалить только для меня";
"delete_message_for_everyone" = "Удалить для всех";
"delete_message_for_me_and_recipient" = "Удалить для меня и %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Ответить";
"context_menu_save" = "Сохранить";
"context_menu_ban_user" = "Заблокировать пользователя";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "පිළිතුරු";
"context_menu_save" = "සුරකින්න";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Vymazať len u mňa";
"delete_message_for_everyone" = "Vymazať u všetkých";
"delete_message_for_me_and_recipient" = "Vymazať pre mňa a %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Odpovedať";
"context_menu_save" = "Uložiť";
"context_menu_ban_user" = "Zablokovanie používateľa";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Spara";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "只為我自己刪除";
"delete_message_for_everyone" = "從所有人的裝置上刪除";
"delete_message_for_me_and_recipient" = "為我和 %@ 刪除";
"context_menu_info" = "Info";
"context_menu_reply" = "回覆";
"context_menu_save" = "儲存";
"context_menu_ban_user" = "封鎖用戶";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "仅为我删除";
"delete_message_for_everyone" = "为所有人删除";
"delete_message_for_me_and_recipient" = "为我和 %@ 删除";
"context_menu_info" = "Info";
"context_menu_reply" = "回复";
"context_menu_save" = "保存";
"context_menu_ban_user" = "封禁用户";
@ -592,6 +593,14 @@
"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";
@ -601,6 +610,7 @@
"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";
@ -613,3 +623,6 @@
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_remove" = "Remove";

View File

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

View File

@ -143,7 +143,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
@ -321,7 +321,7 @@ private final class RecoveryPhraseVC: UIViewController {
targetView: self.view,
info: ConfirmationModal.Info(
title: title,
explanation: message,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
)

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
@ -184,7 +184,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
)
@ -199,7 +199,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
)

View File

@ -209,8 +209,8 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
accessibilityLabel: "Allow voice and video calls",
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(),
confirmAccessibilityLabel: "Enable",
confirmStyle: .textPrimary,

View File

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

View File

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

View File

@ -50,6 +50,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler(viewModel: self)
fileprivate var oldDisplayName: String
private var editedDisplayName: String?
private var editProfilePictureModal: ConfirmationModal?
private var editProfilePictureModalInfo: ConfirmationModal.Info?
private var editedProfilePicture: UIImage?
private var editedProfilePictureFileName: String?
// MARK: - Initialization
@ -102,11 +106,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")?
@ -117,10 +123,10 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self?.transitionToScreen(QRCodeVC())
}
)
]
]
case .editing:
return [
case .editing:
return [
NavItem(
id: .done,
systemItem: .done,
@ -161,7 +167,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self?.updateProfile(
name: updatedNickname,
profilePicture: nil,
profilePictureFilePath: nil,
profilePictureFilePath: ProfileManager.profileAvatarFilepath(id: userSessionId),
isUpdatingDisplayName: true,
isUpdatingProfilePicture: false
)
@ -389,23 +395,90 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
}
private func updateProfilePicture() {
let actionSheet: UIAlertController = UIAlertController(
title: "Update Profile Picture",
message: nil,
preferredStyle: .actionSheet
)
let action = UIAlertAction(
title: "MEDIA_FROM_LIBRARY_BUTTON".localized(),
style: .default,
handler: { [weak self] _ in
self?.showPhotoLibraryForAvatar()
let existingDisplayName: String = self.oldDisplayName
let existingImage: UIImage? = ProfileManager
.profileAvatar(id: self.userSessionId)
.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: { [weak self] modal in
self?.updateProfile(
name: existingDisplayName,
profilePicture: self?.editedProfilePicture,
profilePictureFilePath: self?.editedProfilePictureFileName,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true,
onComplete: { [weak modal] in modal?.close() }
)
},
onCancel: { [weak self] modal in
self?.updateProfile(
name: existingDisplayName,
profilePicture: nil,
profilePictureFilePath: nil,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true,
onComplete: { [weak modal] in modal?.close() }
)
},
afterClosed: { [weak self] in
self?.editedProfilePicture = nil
self?.editedProfilePictureFileName = nil
self?.editProfilePictureModal = nil
self?.editProfilePictureModalInfo = nil
}
)
action.accessibilityLabel = "Photo library"
actionSheet.addAction(action)
actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel, handler: nil))
let modal: ConfirmationModal = ConfirmationModal(info: editProfilePictureModalInfo)
self.editProfilePictureModalInfo = editProfilePictureModalInfo
self.editProfilePictureModal = modal
self.transitionToScreen(modal, transitionType: .present)
}
fileprivate func updatedProfilePictureSelected(image: UIImage?, filePath: String?) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
self.transitionToScreen(actionSheet, transitionType: .present)
self.editedProfilePicture = image
self.editedProfilePictureFileName = filePath
if let image: UIImage = image {
self.editProfilePictureModal?.updateContent(
with: info.with(
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: image,
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
confirmEnabled: true
)
)
}
else if let filePath: String = filePath {
self.editProfilePictureModal?.updateContent(
with: info.with(
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: UIImage(contentsOfFile: filePath),
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
confirmEnabled: true
)
)
}
}
private func showPhotoLibraryForAvatar() {
@ -421,24 +494,20 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
}
}
fileprivate func updateProfile(
private func updateProfile(
name: String,
profilePicture: UIImage?,
profilePictureFilePath: String?,
isUpdatingDisplayName: Bool,
isUpdatingProfilePicture: Bool
isUpdatingProfilePicture: Bool,
onComplete: (() -> ())? = nil
) {
let imageFilePath: String? = (
profilePictureFilePath ??
ProfileManager.profileAvatarFilepath(id: self.userSessionId)
)
let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in
ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default),
profileName: name,
image: profilePicture,
imageFilePath: imageFilePath,
imageFilePath: profilePictureFilePath,
success: { db, updatedProfile in
if isUpdatingDisplayName {
UserDefaults.standard[.lastDisplayNameUpdate] = Date()
@ -453,7 +522,9 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
// Wait for the database transaction to complete before updating the UI
db.afterNextTransaction { _ in
DispatchQueue.main.async {
modalActivityIndicator.dismiss(completion: {})
modalActivityIndicator.dismiss(completion: {
onComplete?()
})
}
}
},
@ -469,12 +540,13 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
"Maximum File Size Exceeded" :
"Couldn't Update Profile"
),
explanation: (isMaxFileSizeExceeded ?
body: .text(isMaxFileSizeExceeded ?
"Please select a smaller photo and try again" :
"Please check your internet connection and try again"
),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
dismissType: .single
)
),
transitionType: .present
@ -497,8 +569,6 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
DispatchQueue.main.async {
button.isUserInteractionEnabled = false
UIView.transition(
with: button,
duration: 0.25,
@ -560,7 +630,6 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
picker.presentingViewController?.dismiss(animated: true)
return
}
let name: String = self.viewModel.oldDisplayName
picker.presentingViewController?.dismiss(animated: true) { [weak self] in
// Check if the user selected an animated image (if so then don't crop, just
@ -574,12 +643,9 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
let viewController: CropScaleImageViewController = CropScaleImageViewController(
srcImage: rawAvatar,
successCompletion: { resultImage in
self?.viewModel.updateProfile(
name: name,
profilePicture: resultImage,
profilePictureFilePath: nil,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true
self?.viewModel.updatedProfilePictureSelected(
image: resultImage,
filePath: nil
)
}
)
@ -587,12 +653,9 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
return
}
self?.viewModel.updateProfile(
name: name,
profilePicture: nil,
profilePictureFilePath: imageUrl.path,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true
self?.viewModel.updatedProfilePictureSelected(
image: nil,
filePath: imageUrl.path
)
}
}

View File

@ -8,6 +8,11 @@ import SessionMessagingKit
public final class FullConversationCell: UITableViewCell {
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
@ -657,14 +662,25 @@ public final class FullConversationCell: UITableViewCell {
return authorPrefix
.appending(
truncatingIfNeeded(
approxWidth: (authorPrefix.size().width + result.size().width),
approxWidth: (
authorPrefix.size().width +
(
result.length > Self.maxApproxCharactersCanBeShownInOneLine ?
bounds.width :
result.size().width
)
),
content: result
)
)
}
.defaulting(
to: truncatingIfNeeded(
approxWidth: result.size().width,
approxWidth: (
result.length > Self.maxApproxCharactersCanBeShownInOneLine ?
bounds.width :
result.size().width
),
content: result
)
)

View File

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

View File

@ -0,0 +1,72 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
extension SessionCarouselView {
public struct Info {
let slices: [UIView]
let copyOfFirstSlice: UIView?
let copyOfLastSlice: UIView?
let sliceSize: CGSize
let sliceCount: Int
let shouldShowPageControl: Bool
let pageControlStyle: PageControlStyle
let shouldShowArrows: Bool
let arrowsSize: CGSize
let cornerRadius: CGFloat
// MARK: - Initialization
init(
slices: [UIView] = [],
copyOfFirstSlice: UIView? = nil,
copyOfLastSlice: UIView? = nil,
sliceSize: CGSize = .zero,
shouldShowPageControl: Bool = true,
pageControlStyle: PageControlStyle,
shouldShowArrows: Bool = true,
arrowsSize: CGSize = .zero,
cornerRadius: CGFloat = 0
) {
self.slices = slices
self.copyOfFirstSlice = copyOfFirstSlice
self.copyOfLastSlice = copyOfLastSlice
self.sliceSize = sliceSize
self.sliceCount = slices.count
self.shouldShowPageControl = shouldShowPageControl && (self.sliceCount > 1)
self.pageControlStyle = pageControlStyle
self.shouldShowArrows = shouldShowArrows && (self.sliceCount > 1)
self.arrowsSize = arrowsSize
self.cornerRadius = cornerRadius
}
}
public struct PageControlStyle {
enum DotSize: CGFloat {
case mini = 0.5
case medium = 0.8
case original = 1
}
let height: CGFloat?
let size: DotSize
let backgroundColor: UIColor
let bottomInset: CGFloat
// MARK: - Initialization
init(
height: CGFloat? = nil,
size: DotSize = .original,
backgroundColor: UIColor = .clear,
bottomInset: CGFloat = 0
) {
self.height = height
self.size = size
self.backgroundColor = backgroundColor
self.bottomInset = bottomInset
}
}
}

View File

@ -0,0 +1,202 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
final class SessionCarouselView: UIView, UIScrollViewDelegate {
private let slicesForLoop: [UIView]
private let info: SessionCarouselView.Info
var delegate: SessionCarouselViewDelegate?
// MARK: - UI
private lazy var scrollView: UIScrollView = {
let result: UIScrollView = UIScrollView()
result.delegate = self
result.isPagingEnabled = true
result.showsHorizontalScrollIndicator = false
result.showsVerticalScrollIndicator = false
result.contentSize = CGSize(
width: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count),
height: self.info.sliceSize.height
)
result.layer.cornerRadius = self.info.cornerRadius
result.layer.masksToBounds = true
return result
}()
private lazy var pageControl: UIPageControl = {
let result: UIPageControl = UIPageControl()
result.numberOfPages = self.info.sliceCount
result.currentPage = 0
result.isHidden = !self.info.shouldShowPageControl
result.transform = CGAffineTransform(
scaleX: self.info.pageControlStyle.size.rawValue,
y: self.info.pageControlStyle.size.rawValue
)
return result
}()
private lazy var arrowLeft: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToPreviousSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
private lazy var arrowRight: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.right")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToNextSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
// MARK: - Lifecycle
init(info: SessionCarouselView.Info) {
self.info = info
if self.info.sliceCount > 1,
let copyOfFirstSlice: UIView = self.info.copyOfFirstSlice,
let copyOfLastSlice: UIView = self.info.copyOfLastSlice
{
self.slicesForLoop = [copyOfLastSlice]
.appending(contentsOf: self.info.slices)
.appending(copyOfFirstSlice)
} else {
self.slicesForLoop = self.info.slices
}
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
set(.width, to: self.info.sliceSize.width + Values.largeSpacing + 2 * self.info.arrowsSize.width)
set(.height, to: self.info.sliceSize.height)
let stackView: UIStackView = UIStackView(arrangedSubviews: self.slicesForLoop)
stackView.axis = .horizontal
stackView.set(.width, to: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count))
stackView.set(.height, to: self.info.sliceSize.height)
addSubview(self.scrollView)
scrollView.center(in: self)
scrollView.set(.width, to: self.info.sliceSize.width)
scrollView.set(.height, to: self.info.sliceSize.height)
scrollView.addSubview(stackView)
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * (self.info.sliceCount > 1 ? 1 : 0),
y: 0
),
animated: false
)
addSubview(self.pageControl)
self.pageControl.center(.horizontal, in: self)
self.pageControl.pin(.bottom, to: .bottom, of: self)
addSubview(self.arrowLeft)
self.arrowLeft.pin(.leading, to: .leading, of: self)
self.arrowLeft.center(.vertical, in: self)
addSubview(self.arrowRight)
self.arrowRight.pin(.trailing, to: .trailing, of: self)
self.arrowRight.center(.vertical, in: self)
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex: Int = {
let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/self.info.sliceSize.width))
if self.info.sliceCount > 1 {
if maybeCurrentPageIndex == 0 {
return pageControl.numberOfPages - 1
}
if maybeCurrentPageIndex == self.slicesForLoop.count - 1 {
return 0
}
return maybeCurrentPageIndex - 1
}
return maybeCurrentPageIndex
}()
pageControl.currentPage = pageIndex
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
private func setCorrectCotentOffsetIfNeeded(_ scrollView: UIScrollView) {
if pageControl.currentPage == 0 {
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * 1,
y: 0
),
animated: false
)
}
if pageControl.currentPage == pageControl.numberOfPages - 1 {
let realLastIndex: Int = self.slicesForLoop.count - 2
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * realLastIndex,
y: 0
),
animated: false
)
}
}
// MARK: - Interaction
@objc func scrollToNextSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x + self.info.sliceSize.width,
y: 0
),
animated: true
)
}
@objc func scrollToPreviousSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x - self.info.sliceSize.width,
y: 0
),
animated: true
)
}
}

View File

@ -0,0 +1,7 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public protocol SessionCarouselViewDelegate: AnyObject {
func carouselViewDidScrollToNewSlice(currentPage: Int)
}

View File

@ -132,7 +132,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) {
@ -336,13 +339,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)
@ -520,7 +525,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
guard
let confirmationInfo: ConfirmationModal.Info = info.confirmationInfo,
confirmationInfo.stateToShow.shouldShow(for: info.currentBoolValue)
confirmationInfo.showCondition.shouldShow(for: info.currentBoolValue)
else {
performAction()
return

View File

@ -301,19 +301,33 @@ extension SessionCell {
let wasOldSelection: Bool = (!isSelected && storedSelection)
radioBorderView.isHidden = false
radioBorderView.themeBorderColor = (isSelected ?
.radioButton_selectedBorder :
.radioButton_unselectedBorder
)
radioBorderView.themeBorderColor = {
guard isEnabled else { return .radioButton_disabledBorder }
return (isSelected ?
.radioButton_selectedBorder :
.radioButton_unselectedBorder
)
}()
radioBorderView.layer.cornerRadius = (size.borderSize / 2)
radioView.accessibilityLabel = accessibilityLabel
radioView.alpha = (wasOldSelection ? 0.3 : 1)
radioView.isHidden = (!isSelected && !storedSelection)
radioView.themeBackgroundColor = (isSelected || wasOldSelection ?
.radioButton_selectedBackground :
.radioButton_unselectedBackground
)
radioView.themeBackgroundColor = {
guard isEnabled else {
return (isSelected || wasOldSelection ?
.radioButton_disabledSelectedBackground :
.radioButton_disabledUnselectedBackground
)
}
return (isSelected || wasOldSelection ?
.radioButton_selectedBackground :
.radioButton_unselectedBackground
)
}()
radioView.layer.cornerRadius = (size.selectionSize / 2)
radioViewWidthConstraint.constant = size.selectionSize

View File

@ -29,6 +29,14 @@ public extension Date {
return "DATE_NOW".localized()
}
var fromattedForMessageInfo: String {
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "h:mm a EEE, DD/MM/YYYY"
return formatter.string(from: self)
}
}
// MARK: - Formatters

Some files were not shown because too many files have changed in this diff Show More