Merge branch 'dev' of https://github.com/oxen-io/session-ios into voice-calls-2

This commit is contained in:
Ryan Zhao 2022-02-04 14:21:54 +11:00
commit 2d9f962a97
103 changed files with 3623 additions and 1679 deletions

View File

@ -22,10 +22,8 @@
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; };
34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; };
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; };
346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; };
34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */; };
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
@ -155,6 +153,15 @@
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; };
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; };
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; };
7BAF54CE27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CB27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift */; };
7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */; };
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CD27ACCEEC003D12F8 /* EmptySearchResultCell.swift */; };
7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */; };
7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; };
7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; };
7BAF54D927ACD0E3003D12F8 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */; };
7BAF54DA27ACD0E3003D12F8 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */; };
7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; };
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; };
@ -989,16 +996,12 @@
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = "<group>"; };
34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareAppExtensionContext.h; sourceTree = "<group>"; };
34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareAppExtensionContext.m; sourceTree = "<group>"; };
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = "<group>"; };
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = "<group>"; };
344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = "<group>"; };
346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = "<group>"; };
346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = "<group>"; };
34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAEScreenLockViewController.h; sourceTree = "<group>"; };
34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAEScreenLockViewController.m; sourceTree = "<group>"; };
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
@ -1158,6 +1161,15 @@
7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = "<group>"; };
7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = "<group>"; };
7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = "<group>"; };
7BAF54CB27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+RecentSearchResults.swift"; sourceTree = "<group>"; };
7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = "<group>"; };
7BAF54CD27ACCEEC003D12F8 /* EmptySearchResultCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptySearchResultCell.swift; sourceTree = "<group>"; };
7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -2042,11 +2054,9 @@
isa = PBXGroup;
children = (
C31C21A4255BCA4800EC2D66 /* Meta */,
7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */,
7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */,
4535186C1FC635DD00210559 /* MainInterface.storyboard */,
34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */,
34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */,
34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */,
34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */,
C3ADC66026426688005F1414 /* ShareVC.swift */,
B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */,
B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */,
@ -2116,6 +2126,16 @@
path = "Call Management";
sourceTree = "<group>";
};
7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */ = {
isa = PBXGroup;
children = (
7BAF54CB27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift */,
7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */,
7BAF54CD27ACCEEC003D12F8 /* EmptySearchResultCell.swift */,
);
path = GlobalSearch;
sourceTree = "<group>";
};
7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */ = {
isa = PBXGroup;
children = (
@ -2374,6 +2394,9 @@
B8A582B0258C66C900AFD84C /* General */ = {
isa = PBXGroup;
children = (
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */,
7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */,
7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */,
C33FDB8A255A581200E217F9 /* AppContext.h */,
C33FDB85255A581100E217F9 /* AppContext.m */,
C3C2A5D12553860800C340D1 /* Array+Description.swift */,
@ -2905,6 +2928,7 @@
C360968E25AD16E8008B62B2 /* Home */ = {
isa = PBXGroup;
children = (
7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */,
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
);
@ -3452,6 +3476,7 @@
C3CA3B11255CF17200F4C6D4 /* Utilities */ = {
isa = PBXGroup;
children = (
7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */,
C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */,
C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */,
C38EF2ED255B6DBB007E1867 /* DisplayableText.swift */,
@ -4486,10 +4511,10 @@
buildActionMask = 2147483647;
files = (
B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */,
34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */,
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */,
34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */,
7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */,
B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */,
7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4596,6 +4621,7 @@
C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */,
C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */,
C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */,
7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */,
C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */,
C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */,
C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */,
@ -4697,6 +4723,7 @@
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
7BAF54DA27ACD0E3003D12F8 /* UITableView+ReusableView.swift in Sources */,
C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */,
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */,
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
@ -4716,6 +4743,7 @@
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */,
7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */,
C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */,
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */,
@ -4736,6 +4764,7 @@
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */,
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
7BAF54D927ACD0E3003D12F8 /* String+Localization.swift in Sources */,
B8856D23256F116B001CE70E /* Weak.swift in Sources */,
C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */,
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
@ -4910,6 +4939,7 @@
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */,
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */,
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
@ -4936,6 +4966,7 @@
B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */,
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */,
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */,
7BAF54CE27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift in Sources */,
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */,
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */,
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
@ -4999,6 +5030,7 @@
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */,
B848A4C5269EAAA200617031 /* UserDetailsSheet.swift in Sources */,
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */,
7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */,
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */,
B8544E3323D50E4900299F14 /* SNAppearance.swift in Sources */,
4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */,

View File

@ -306,7 +306,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
// MARK: Convenience
private func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
}

View File

@ -151,7 +151,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
@objc private func createClosedGroup() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else {
@ -184,7 +184,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
let title = "Couldn't Create Group"
let message = "Please check your internet connection and try again."
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
}
}

View File

@ -1,6 +1,8 @@
import UIKit
import CoreServices
import Photos
import PhotosUI
import SignalUtilitiesKit
extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuActionDelegate, ScrollToBottomButtonDelegate,
SendMediaNavDelegate, UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, GifPickerViewControllerDelegate,
@ -101,11 +103,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
sendAttachments(attachments, with: messageText ?? "")
sendAttachments(attachments, with: messageText ?? "") { [weak self] in
self?.dismiss(animated: true, completion: nil)
}
scrollToBottom(isAnimated: false)
resetMentions()
self.snInputView.text = ""
dismiss(animated: true) { }
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -220,7 +224,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
if !attachment.hasError {
self?.showAttachmentApprovalDialog(for: [ attachment ])
} else {
self?.showErrorAlert(for: attachment)
self?.showErrorAlert(for: attachment, onDismiss: nil)
}
}
}.retainUntilComplete()
@ -270,11 +274,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
})
}
func sendAttachments(_ attachments: [SignalAttachment], with text: String) {
func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) {
guard !showBlockedModalIfNeeded() else { return }
for attachment in attachments {
if attachment.hasError {
return showErrorAlert(for: attachment)
return showErrorAlert(for: attachment, onDismiss: onComplete)
}
}
let thread = self.thread
@ -294,6 +298,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
self?.scrollToBottom(isAnimated: false)
})
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
onComplete?()
})
}
@ -871,7 +878,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
dataSource.sourceFilename = fileName
let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4Audio as String)
guard !attachment.hasError else {
return showErrorAlert(for: attachment)
return showErrorAlert(for: attachment, onDismiss: nil)
}
// Send attachment
sendAttachments([ attachment ], with: "")
@ -910,10 +917,99 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
}
}
// MARK: Convenience
func showErrorAlert(for attachment: SignalAttachment) {
// MARK: Requesting Permission
func requestCameraPermissionIfNeeded() -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return true
case .denied, .restricted:
let modal = PermissionMissingModal(permission: "camera") { }
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
return false
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in })
return false
default: return false
}
}
func requestMicrophonePermissionIfNeeded(onNotGranted: @escaping () -> Void) {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted: break
case .denied:
onNotGranted()
let modal = PermissionMissingModal(permission: "microphone") {
onNotGranted()
}
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
case .undetermined:
onNotGranted()
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
default: break
}
}
func requestLibraryPermissionIfNeeded(onAuthorized: @escaping () -> Void) {
let authorizationStatus: PHAuthorizationStatus
if #available(iOS 14, *) {
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
if authorizationStatus == .notDetermined {
// When the user chooses to select photos (which is the .limit status),
// the PHPhotoUI will present the picker view on the top of the front view.
// Since we have the ScreenLockUI showing when we request premissions,
// the picker view will be presented on the top of the ScreenLockUI.
// However, the ScreenLockUI will dismiss with the permission request alert view, so
// the picker view then will dismiss, too. The selection process cannot be finished
// this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI
// from showing when we request the photo library permission.
Environment.shared.isRequestingPermission = true
let appMode = AppModeManager.shared.currentAppMode
// FIXME: Rather than setting the app mode to light and then to dark again once we're done,
// it'd be better to just customize the appearance of the image picker. There doesn't currently
// appear to be a good way to do so though...
AppModeManager.shared.setCurrentAppMode(to: .light)
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
DispatchQueue.main.async {
AppModeManager.shared.setCurrentAppMode(to: appMode)
}
Environment.shared.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized()
}
}
}
} else {
authorizationStatus = PHPhotoLibrary.authorizationStatus()
if authorizationStatus == .notDetermined {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
onAuthorized()
}
}
}
}
switch authorizationStatus {
case .authorized, .limited:
onAuthorized()
case .denied, .restricted:
let modal = PermissionMissingModal(permission: "library") { }
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
default: return
}
}
// MARK: - Convenience
func showErrorAlert(for attachment: SignalAttachment, onDismiss: (() -> ())?) {
let title = NSLocalizedString("ATTACHMENT_ERROR_ALERT_TITLE", comment: "")
let message = attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage
OWSAlerts.showAlert(title: title, message: message)
OWSAlerts.showAlert(title: title, message: message, buttonTitle: nil) { _ in
onDismiss?()
}
}
}

View File

@ -7,7 +7,8 @@
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let isUnsendRequestsEnabled = true // Set to true once unsend requests are done on all platforms
let thread: TSThread
let focusedMessageID: String? // This isn't actually used ATM
let focusedMessageID: String? // This is used for global search
var focusedMessageIndexPath: IndexPath?
var unreadViewItems: [ConversationViewItem] = []
var scrollButtonConstraint: NSLayoutConstraint?
// Search
@ -112,7 +113,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
let result = UIView()
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
let size = ConversationVC.unreadCountViewSize
result.set(.width, to: size)
result.set(.width, greaterThanOrEqualTo: size)
result.set(.height, to: size)
result.layer.masksToBounds = true
result.layer.cornerRadius = size / 2
@ -192,7 +193,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// Unread count view
view.addSubview(unreadCountView)
unreadCountView.addSubview(unreadCountLabel)
unreadCountLabel.pin(to: unreadCountView)
unreadCountLabel.pin(.top, to: .top, of: unreadCountView)
unreadCountLabel.pin(.bottom, to: .bottom, of: unreadCountView)
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
unreadCountView.centerYAnchor.constraint(equalTo: scrollButton.topAnchor).isActive = true
unreadCountView.center(.horizontal, in: scrollButton)
updateUnreadCountView()
@ -236,13 +240,17 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// unreadIndicatorIndex is calculated during loading of the viewItems, so it's
// supposed to be accurate.
DispatchQueue.main.async {
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
?? (self.viewItems.count - self.unreadViewItems.count)
if unreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
self.unreadCountView.alpha = self.scrollButton.alpha
if let focusedMessageID = self.focusedMessageID {
self.scrollToInteraction(with: focusedMessageID, isAnimated: false, highlighted: true)
} else {
self.scrollToBottom(isAnimated: false)
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
?? (self.viewItems.count - self.unreadViewItems.count)
if unreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
self.unreadCountView.alpha = self.scrollButton.alpha
} else {
self.scrollToBottom(isAnimated: false)
}
}
self.scrollButton.alpha = self.getScrollButtonOpacity()
}
@ -251,6 +259,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
highlightFocusedMessageIfNeeded()
didFinishInitialLayout = true
markAllAsRead()
self.becomeFirstResponder()
@ -324,6 +333,13 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
}
private func highlightFocusedMessageIfNeeded() {
if let indexPath = focusedMessageIndexPath, let cell = messagesTableView.cellForRow(at: indexPath) as? VisibleMessageCell {
cell.highlight()
focusedMessageIndexPath = nil
}
}
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
if (newHeight > 0 && baselineKeyboardHeight == 0) {
@ -505,8 +521,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
unreadViewItems.remove(at: index)
}
let unreadCount = unreadViewItems.count
unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+"
let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8
unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+"
let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8
unreadCountLabel.font = .boldSystemFont(ofSize: fontSize)
unreadCountView.isHidden = (unreadCount == 0)
}
@ -553,6 +569,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
func showSearchUI() {
isShowingSearchUI = true
// Search bar
// FIXME: This code is duplicated with SearchBar
let searchBar = searchController.uiSearchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.barStyle = .black
@ -631,8 +648,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
scrollToInteraction(with: interactionID)
}
func scrollToInteraction(with interactionID: String, position: UITableView.ScrollPosition = .middle, isAnimated: Bool = true) {
func scrollToInteraction(with interactionID: String, position: UITableView.ScrollPosition = .middle, isAnimated: Bool = true, highlighted: Bool = false) {
guard let indexPath = viewModel.ensureLoadWindowContainsInteractionId(interactionID) else { return }
messagesTableView.scrollToRow(at: indexPath, at: position, animated: isAnimated)
if highlighted {
focusedMessageIndexPath = indexPath
}
}
}

View File

@ -56,7 +56,7 @@ final class MediaTextOverlayView : UIView {
self.readMoreButton = readMoreButton
readMoreButton.setTitle("Read More", for: UIControl.State.normal)
readMoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize)
readMoreButton.setTitleColor(.white, for: UIControl.State.normal)
readMoreButton.setTitleColor(self.textColor, for: UIControl.State.normal)
readMoreButton.addTarget(self, action: #selector(readMore), for: UIControl.Event.touchUpInside)
addSubview(readMoreButton)
readMoreButton.pin(.left, to: .left, of: self, withInset: inset)

View File

@ -65,7 +65,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
lazy var bubbleView: UIView = {
let result = UIView()
result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
return result
}()
@ -131,6 +131,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
private var bodyLabelTextColor: UIColor {
switch (direction, AppModeManager.shared.currentAppMode) {
case (.outgoing, .dark), (.incoming, .light): return .black
case (.outgoing, .light): return Colors.grey
default: return .white
}
}
@ -430,10 +431,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
private func updateBubbleViewCorners() {
let maskPath = UIBezierPath(roundedRect: bubbleView.bounds, byRoundingCorners: getCornersToRound(),
let cornersToRound = getCornersToRound()
let maskPath = UIBezierPath(roundedRect: bubbleView.bounds, byRoundingCorners: cornersToRound,
cornerRadii: CGSize(width: VisibleMessageCell.largeCornerRadius, height: VisibleMessageCell.largeCornerRadius))
bubbleViewMaskLayer.path = maskPath.cgPath
bubbleView.layer.mask = bubbleViewMaskLayer
bubbleView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
bubbleView.layer.maskedCorners = getCornerMask(from: cornersToRound)
}
override func prepareForReuse() {
@ -469,6 +472,18 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
return abs(v.x) > abs(v.y) // It has to be more horizontal than vertical
} else {
return true
}
}
func highlight() {
let shawdowColour = isLightMode ? UIColor.black.cgColor : Colors.accent.cgColor
let opacity : Float = isLightMode ? 0.5 : 1
bubbleView.setShadow(radius: 10, opacity: opacity, offset: .zero, color: shawdowColour)
DispatchQueue.main.async {
UIView.animate(withDuration: 1.6) {
self.bubbleView.setShadow(radius: 0, opacity: 0, offset: .zero, color: UIColor.clear.cgColor)
}
}
}
@ -570,6 +585,19 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
return result
}
private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask {
var cornerMask = CACornerMask()
if rectCorner.contains(.allCorners) {
cornerMask = [ .layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
} else {
if rectCorner.contains(.topRight) { cornerMask.insert(.layerMaxXMinYCorner) }
if rectCorner.contains(.topLeft) { cornerMask.insert(.layerMinXMinYCorner) }
if rectCorner.contains(.bottomRight) { cornerMask.insert(.layerMaxXMaxYCorner) }
if rectCorner.contains(.bottomLeft) { cornerMask.insert(.layerMinXMaxYCorner) }
}
return cornerMask
}
private static func getFontSize(for viewItem: ConversationViewItem) -> CGFloat {
let baselineFontSize = Values.mediumFontSize
switch viewItem.displayableBodyText?.jumbomojiCount {

View File

@ -65,7 +65,7 @@ final class JoinOpenGroupModal : Modal {
@objc private func joinOpenGroup() {
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url) else {
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
return presentingViewController!.present(alert, animated: true, completion: nil)
}
presentingViewController!.dismiss(animated: true, completion: nil)
@ -77,7 +77,7 @@ final class JoinOpenGroupModal : Modal {
}
.catch(on: DispatchQueue.main) { error in
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentingViewController.present(alert, animated: true, completion: nil)
}
}

View File

@ -158,7 +158,7 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
}
let message = messageOrNil ?? "Please check the Session ID or ONS name and try again"
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
}
}

View File

@ -0,0 +1,59 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import NVActivityIndicatorView
class EmptySearchResultCell: UITableViewCell {
static let reuseIdentifier = "EmptySearchResultCell"
private lazy var messageLabel: UILabel = {
let result = UILabel()
result.textAlignment = .center
result.numberOfLines = 3
result.textColor = Colors.text
result.text = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "")
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
result.set(.width, to: 40)
result.set(.height, to: 40)
return result
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
contentView.addSubview(messageLabel)
messageLabel.autoSetDimension(.height, toSize: 150)
messageLabel.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual)
messageLabel.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
messageLabel.autoVCenterInSuperview()
messageLabel.autoHCenterInSuperview()
messageLabel.setContentHuggingHigh()
messageLabel.setCompressionResistanceHigh()
contentView.addSubview(spinner)
spinner.autoCenterInSuperview()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
public func configure(isLoading: Bool) {
if isLoading {
// Calling stopAnimating() here is a workaround for
// the spinner won't change its colour as the theme changed.
spinner.stopAnimating()
spinner.startAnimating()
messageLabel.isHidden = true
} else {
spinner.stopAnimating()
messageLabel.isHidden = false
}
}
}

View File

@ -0,0 +1,380 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
@objc
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
let isRecentSearchResultsEnabled = false
@objc public var searchText = "" {
didSet {
AssertIsOnMainThread()
// Use a slight delay to debounce updates.
refreshSearchResults()
}
}
var recentSearchResults: [String] = Array(Storage.shared.getRecentSearchResults().reversed())
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
private var lastSearchText: String?
var searcher: FullTextSearcher {
return FullTextSearcher.shared
}
var isLoading = false
enum SearchSection: Int {
case noResults
case contacts
case messages
case recent
}
// MARK: UI Components
internal lazy var searchBar: SearchBar = {
let result = SearchBar()
result.tintColor = Colors.text
result.delegate = self
result.showsCancelButton = true
return result
}()
internal lazy var tableView: UITableView = {
let result = UITableView(frame: .zero, style: .grouped)
result.rowHeight = UITableView.automaticDimension
result.estimatedRowHeight = 60
result.separatorStyle = .none
result.keyboardDismissMode = .onDrag
result.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier)
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
result.showsVerticalScrollIndicator = false
return result
}()
// MARK: Dependencies
var dbReadConnection: YapDatabaseConnection {
return OWSPrimaryStorage.shared().dbReadConnection
}
// MARK: View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
setUpGradientBackground()
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.pin(.leading, to: .leading, of: view)
tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
tableView.pin(.trailing, to: .trailing, of: view)
tableView.pin(.bottom, to: .bottom, of: view)
navigationItem.hidesBackButton = true
setupNavigationBar()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
searchBar.becomeFirstResponder()
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
searchBar.resignFirstResponder()
}
private func setupNavigationBar() {
// This is a workaround for a UI issue that the navigation bar can be a bit higher if
// the search bar is put directly to be the titleView. And this can cause the tableView
// in home screen doing a weird scrolling when going back to home screen.
let searchBarContainer = UIView()
searchBarContainer.layoutMargins = UIEdgeInsets.zero
searchBar.sizeToFit()
searchBar.layoutMargins = UIEdgeInsets.zero
searchBarContainer.set(.height, to: 44)
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
searchBarContainer.addSubview(searchBar)
searchBar.autoPinEdgesToSuperviewMargins()
navigationItem.titleView = searchBarContainer
}
private func reloadTableData() {
tableView.reloadData()
}
// MARK: Update Search Results
var refreshTimer: Timer?
private func refreshSearchResults() {
guard !searchResultSet.isEmpty else {
// To avoid incorrectly showing the "no results" state,
// always search immediately if the current result set is empty.
refreshTimer?.invalidate()
refreshTimer = nil
updateSearchResults(searchText: searchText)
return
}
if refreshTimer != nil {
// Don't start a new refresh timer if there's already one active.
return
}
refreshTimer?.invalidate()
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
guard let self = self else {
return
}
self.updateSearchResults(searchText: self.searchText)
self.refreshTimer = nil
}
}
private func updateSearchResults(searchText rawSearchText: String) {
let searchText = rawSearchText.stripped
guard searchText.count > 0 else {
searchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
lastSearchText = nil
reloadTableData()
return
}
guard lastSearchText != searchText else { return }
lastSearchText = searchText
var searchResults: HomeScreenSearchResultSet?
self.dbReadConnection.asyncRead({[weak self] transaction in
guard let self = self else { return }
self.isLoading = true
// The max search result count is set according to the keyword length. This is just a workaround for performance issue.
// The longer and more accurate the keyword is, the less search results should there be.
searchResults = self.searcher.searchForHomeScreen(searchText: searchText, maxSearchResults: min(searchText.count * 50, 500), transaction: transaction)
}, completionBlock: { [weak self] in
AssertIsOnMainThread()
guard let self = self, let results = searchResults, self.lastSearchText == searchText else { return }
self.searchResultSet = results
self.isLoading = false
self.reloadTableData()
})
}
// MARK: Interaction
@objc func clearRecentSearchResults() {
recentSearchResults = []
tableView.reloadSections([ SearchSection.recent.rawValue ], with: .top)
Storage.shared.clearRecentSearchResults()
}
}
// MARK: - UISearchBarDelegate
extension GlobalSearchViewController: UISearchBarDelegate {
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
self.updateSearchText()
}
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
self.updateSearchText()
}
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.updateSearchText()
}
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = nil
searchBar.resignFirstResponder()
self.navigationController?.popViewController(animated: true)
}
func updateSearchText() {
guard let searchText = searchBar.text?.ows_stripped() else { return }
self.searchText = searchText
}
}
// MARK: - UITableViewDelegate & UITableViewDataSource
extension GlobalSearchViewController {
// MARK: UITableViewDelegate
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
guard let searchSection = SearchSection(rawValue: indexPath.section) else { return }
switch searchSection {
case .noResults:
SNLog("shouldn't be able to tap 'no results' section")
case .contacts:
let sectionResults = searchResultSet.conversations
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
show(thread, highlightedMessageID: nil, animated: true)
case .messages:
let sectionResults = searchResultSet.messages
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
show(thread, highlightedMessageID: searchResult.messageId, animated: true)
case .recent:
guard let threadId = recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId) else { return }
show(thread, highlightedMessageID: nil, animated: true, isFromRecent: true)
}
}
private func show(_ thread: TSThread, highlightedMessageID: String?, animated: Bool, isFromRecent: Bool = false) {
if let threadId = thread.uniqueId {
recentSearchResults = Array(Storage.shared.addSearchResults(threadID: threadId).reversed())
}
DispatchMainThreadSafe {
if let presentedVC = self.presentedViewController {
presentedVC.dismiss(animated: false, completion: nil)
}
let conversationVC = ConversationVC(thread: thread, focusedMessageID: highlightedMessageID)
var viewControllers = self.navigationController?.viewControllers
if isFromRecent, let index = viewControllers?.firstIndex(of: self) { viewControllers?.remove(at: index) }
viewControllers?.append(conversationVC)
self.navigationController?.setViewControllers(viewControllers!, animated: true)
}
}
// MARK: UITableViewDataSource
public func numberOfSections(in tableView: UITableView) -> Int {
return 4
}
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
UIView()
}
public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
.leastNonzeroMagnitude
}
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard nil != self.tableView(tableView, titleForHeaderInSection: section) else {
return .leastNonzeroMagnitude
}
return UITableView.automaticDimension
}
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let searchSection = SearchSection(rawValue: section) else { return nil }
guard let title = self.tableView(tableView, titleForHeaderInSection: section) else {
return UIView()
}
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
let container = UIView()
container.backgroundColor = Colors.cellBackground
container.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, left: Values.mediumSpacing, bottom: Values.smallSpacing, right: Values.mediumSpacing)
container.addSubview(titleLabel)
titleLabel.autoPinEdgesToSuperviewMargins()
if searchSection == .recent {
let clearButton = UIButton()
clearButton.setTitle("Clear", for: .normal)
clearButton.setTitleColor(Colors.text, for: UIControl.State.normal)
clearButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize)
clearButton.addTarget(self, action: #selector(clearRecentSearchResults), for: .touchUpInside)
container.addSubview(clearButton)
clearButton.autoPinTrailingToSuperviewMargin()
clearButton.autoVCenterInSuperview()
}
return container
}
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let searchSection = SearchSection(rawValue: section) else { return nil }
switch searchSection {
case .noResults:
return nil
case .contacts:
if searchResultSet.conversations.count > 0 {
return NSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "")
} else {
return nil
}
case .messages:
if searchResultSet.messages.count > 0 {
return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "")
} else {
return nil
}
case .recent:
if recentSearchResults.count > 0 && searchText.isEmpty && isRecentSearchResultsEnabled {
return NSLocalizedString("SEARCH_SECTION_RECENT", comment: "")
} else {
return nil
}
}
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let searchSection = SearchSection(rawValue: section) else { return 0 }
switch searchSection {
case .noResults:
return (searchText.count > 0 && searchResultSet.isEmpty) ? 1 : 0
case .contacts:
return searchResultSet.conversations.count
case .messages:
return searchResultSet.messages.count
case .recent:
return searchText.isEmpty && isRecentSearchResultsEnabled ? recentSearchResults.count : 0
}
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
return UITableViewCell()
}
switch searchSection {
case .noResults:
guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell, indexPath.row == 0 else { return UITableViewCell() }
cell.configure(isLoading: isLoading)
return cell
case .contacts:
let sectionResults = searchResultSet.conversations
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.isShowingGlobalSearchResult = true
let searchResult = sectionResults[safe: indexPath.row]
cell.threadViewModel = searchResult?.thread
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText)
return cell
case .messages:
let sectionResults = searchResultSet.messages
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.isShowingGlobalSearchResult = true
let searchResult = sectionResults[safe: indexPath.row]
cell.threadViewModel = searchResult?.thread
var message: TSMessage? = nil
if let messageId = searchResult?.messageId { message = TSMessage.fetch(uniqueId: messageId) }
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText, message: message)
return cell
case .recent:
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.isShowingGlobalSearchResult = true
dbReadConnection.read { transaction in
guard let threadId = self.recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId, transaction: transaction) else { return }
cell.threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
}
cell.configureForRecent()
return cell
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
extension Storage{
private static let recentSearchResultDatabaseCollection = "RecentSearchResultDatabaseCollection"
private static let recentSearchResultKey = "RecentSearchResult"
public func getRecentSearchResults() -> [String] {
var result: [String]?
Storage.read { transaction in
result = transaction.object(forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection) as? [String]
}
return result ?? []
}
public func clearRecentSearchResults() {
Storage.write { transaction in
transaction.removeObject(forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection)
}
}
public func addSearchResults(threadID: String) -> [String] {
var recentSearchResults = getRecentSearchResults()
if recentSearchResults.count > 20 { recentSearchResults.remove(at: 0) } // Limit the size of the collection to 20
if let index = recentSearchResults.firstIndex(of: threadID) { recentSearchResults.remove(at: index) }
recentSearchResults.append(threadID)
Storage.write { transaction in
transaction.setObject(recentSearchResults, forKey: Storage.recentSearchResultKey, inCollection: Storage.recentSearchResultDatabaseCollection)
}
return recentSearchResults
}
}

View File

@ -2,7 +2,6 @@
// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and
// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
// more information on database handling.
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
private var threads: YapDatabaseViewMappings!
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
@ -89,7 +88,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
setUpNavBarStyle()
}
updateNavBarButtons()
setNavBarTitle(NSLocalizedString("vc_home_title", comment: ""))
setUpNavBarSessionHeading()
// Recovery phrase reminder
let hasViewedSeed = UserDefaults.standard[.hasViewedSeed]
if !hasViewedSeed {
@ -266,6 +265,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
private func updateNavBarButtons() {
// Profile picture view
let profilePictureSize = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button"
@ -276,32 +276,27 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
profilePictureView.set(.height, to: profilePictureSize)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
// Path status indicator
let pathStatusView = PathStatusView()
pathStatusView.accessibilityLabel = "Current onion routing path indicator"
pathStatusView.set(.width, to: PathStatusView.size)
pathStatusView.set(.height, to: PathStatusView.size)
// Container view
let profilePictureViewContainer = UIView()
profilePictureViewContainer.accessibilityLabel = "Settings button"
profilePictureViewContainer.addSubview(profilePictureView)
profilePictureView.pin(.leading, to: .leading, of: profilePictureViewContainer, withInset: 4)
profilePictureView.pin(.top, to: .top, of: profilePictureViewContainer)
profilePictureView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
profilePictureView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
profilePictureView.autoPinEdgesToSuperviewEdges()
profilePictureViewContainer.addSubview(pathStatusView)
pathStatusView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
pathStatusView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
// Left bar button item
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
leftBarButtonItem.accessibilityLabel = "Settings button"
leftBarButtonItem.isAccessibilityElement = true
navigationItem.leftBarButtonItem = leftBarButtonItem
let pathStatusViewContainer = UIView()
pathStatusViewContainer.accessibilityLabel = "Current onion routing path button"
let pathStatusViewContainerSize = Values.verySmallProfilePictureSize // Match the profile picture view
pathStatusViewContainer.set(.width, to: pathStatusViewContainerSize)
pathStatusViewContainer.set(.height, to: pathStatusViewContainerSize)
let pathStatusView = PathStatusView()
pathStatusView.accessibilityLabel = "Current onion routing path button"
pathStatusView.set(.width, to: PathStatusView.size)
pathStatusView.set(.height, to: PathStatusView.size)
pathStatusViewContainer.addSubview(pathStatusView)
pathStatusView.center(.horizontal, in: pathStatusViewContainer)
pathStatusView.center(.vertical, in: pathStatusViewContainer)
pathStatusViewContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showPath)))
let rightBarButtonItem = UIBarButtonItem(customView: pathStatusViewContainer)
rightBarButtonItem.accessibilityLabel = "Current onion routing path button"
// Right bar button item - search button
let rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(showSearchUI))
rightBarButtonItem.accessibilityLabel = "Search button"
rightBarButtonItem.isAccessibilityElement = true
navigationItem.rightBarButtonItem = rightBarButtonItem
}
@ -418,10 +413,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
present(navigationController, animated: true, completion: nil)
}
@objc private func showPath() {
let pathVC = PathVC()
let navigationController = OWSNavigationController(rootViewController: pathVC)
present(navigationController, animated: true, completion: nil)
@objc private func showSearchUI() {
if let presentedVC = self.presentedViewController {
presentedVC.dismiss(animated: false, completion: nil)
}
let searchController = GlobalSearchViewController()
self.navigationController?.setViewControllers([ self, searchController ], animated: true)
}
@objc func joinOpenGroup() {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "heading.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -160,6 +160,10 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
return [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
}
- (BOOL)isShareExtension {
return NO;
}
- (BOOL)isRTL
{
static BOOL isRTL = NO;

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Anhang";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Fehler beim Senden des Anhangs";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Bild kann nicht konvertiert werden.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video kann nicht verarbeitet werden.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Bild kann nicht geparst werden.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metadaten können nicht aus dem Bild entfernt werden.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Bildgröße kann nicht geändert werden.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anhang ist zu groß.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Anhang besitzt ungültigen Inhalt.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anhang besitzt ein ungültiges Dateiformat.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Anhang ist leer.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Auswählen des Dokuments gescheitert.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Anzeigen";
/* No comment provided by engineer. */
"OK" = "Okay";
"BUTTON_OK" = "Okay";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ hat verschwindende Nachrichten deaktiviert.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Mit Session teilen";
"vc_share_loading_message" = "Anlagen werden vorbereitet...";
"vc_share_sending_message" = "Wird gesendet ...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Gruppeneinladung öffnen";
"vc_conversation_settings_invite_button_title" = "Mitglieder hinzufügen";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Attachment";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Show";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -551,6 +569,10 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";
"vc_settings_faq_button_title" = "FAQ";
@ -600,3 +622,7 @@
"modal_call_missed_tips_explanation" = "Call missed from '%@' because you needed to enable the 'Voice and video calls' permission in the Privacy Settings.";
"meida_saved" = "Media saved by %@.";
"screenshot_taken" = "%@ took a screenshot.";
"SEARCH_SECTION_CONTACTS" = "Contacts and Groups";
"SEARCH_SECTION_MESSAGES" = "Messages";
"SEARCH_SECTION_RECENT" = "Recent";
"RECENT_SEARCH_LAST_MESSAGE_DATETIME" = "last message: %@";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Adjunto";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Fallo al enviar archivo adjunto";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Bild kann nicht konvertiert werden.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video kann nicht verarbeitet werden.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Bild kann nicht geparst werden.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metadaten können nicht aus dem Bild entfernt werden.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Bildgröße kann nicht geändert werden.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anhang ist zu groß.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Anhang besitzt ungültigen Inhalt.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anhang besitzt ein ungültiges Dateiformat.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Anhang ist leer.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Fallo al seleccionar documento.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Ver";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ha desactivado la desaparición de mensajes.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Compartir en Session";
"vc_share_loading_message" = "Preparando archivos adjuntos...";
"vc_share_sending_message" = "Enviando...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Abrir invitación de grupo";
"vc_conversation_settings_invite_button_title" = "Añadir Miembros";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "ضميمه";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "خطا در ارسال فایل ضمیمه";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "خطا در انتخاب سند.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "نمایش";
/* No comment provided by engineer. */
"OK" = "باشه";
"BUTTON_OK" = "باشه";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ پیام‌های محوشونده را غیرفعال کرده است.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "اشتراک گذاری با Session";
"vc_share_loading_message" = "آماده سازی پیوست‌ها...";
"vc_share_sending_message" = "در حال ارسال...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Liite";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Virhe liitteen lähettämisessä";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Kuvaa ei voitu muuntaa.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Videon prosessointi ei onnistu.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Kuvaa ei voitu jäsentää.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metatietojen poistaminen kuvasta ei onnistunut.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Kuvan kokoa ei voitu muuttaa.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Liitetiedosto on liian suuri.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Liitetiedoston sisältö on virheellinen.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Liitetiedoston muoto on virheellinen.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Liitetiedosto on tyhjä.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Dokumentin valinta epäonnistui.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Näytä";
/* No comment provided by engineer. */
"OK" = "Ok";
"BUTTON_OK" = "Ok";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ poisti katoavat viestit käytöstä.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Jaa Sessioniin";
"vc_share_loading_message" = "Valmistellaan liitteitä...";
"vc_share_sending_message" = "Lähetetään...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Avaa ryhmäkutsu";
"vc_conversation_settings_invite_button_title" = "Lisää jäseniä";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Fichier joint";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Erreur denvoi du fichier joint";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Impossible de convertir limage.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Impossible de traiter la vidéo.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Impossible danalyser limage.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Impossible de supprimer les métadonnées de limage.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Impossible de redimensionner limage.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Le fichier joint est trop volumineux.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Le fichier joint comporte du contenu non valide.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Le fichier joint présente un format de fichier invalide.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Le fichier joint est vide.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Échec de sélection du document.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Afficher";
/* No comment provided by engineer. */
"OK" = "Valider";
"BUTTON_OK" = "Valider";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ a désactivé les messages éphémères.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Partager en Session";
"vc_share_loading_message" = "Préparation des pièces jointes ...";
"vc_share_sending_message" = "Envoi...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Invitation à un groupe ouvert";
"vc_conversation_settings_invite_button_title" = "Ajouter des membres";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "अटैचमेंट";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "अनुलग्नक भेजने में त्रुटि";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "दस्तावेज़ चुनने में विफल.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "दिखाएं";
/* No comment provided by engineer. */
"OK" = "ठीक है";
"BUTTON_OK" = "ठीक है";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ने गायब संदेश अक्षम कर दिए हैं।";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "सत्र में साझा करें";
"vc_share_loading_message" = "अटैचमेंट तैयार किए जा रहे हैं...";
"vc_share_sending_message" = "भेजा जा रहा है...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "ग्रुप आमंत्रण खोलें";
"vc_conversation_settings_invite_button_title" = "सदस्य जोड़ें";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Privitak";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Pogreška kod slanja privitka";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Neuspješna pretvorba slike";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Neuspješna obrada videozapisa.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Neuspješna analiza slike.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Neuspješno uklanjanje metapodataka iz slike.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Neuspješna promjena veličine slike.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Privitak je prevelik.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Privitak sadrži nevažeći sadržaj.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Privitak ima nevažeći format datoteke.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Privitak je prazan.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Odabir dokumenta neuspješan.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Prikaži";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ je onemogućio nestajuće poruke.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Podijeli sa Session-om";
"vc_share_loading_message" = "Priprema privitaka...";
"vc_share_sending_message" = "Slanje...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Otvori pozivnicu za grupu";
"vc_conversation_settings_invite_button_title" = "Dodaj članove";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Lampiran";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Gagal Mengirim Lampiran";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Tidak dapat mengonversi gambar.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video tidak dapat diproses.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Tidak dapat menguraikan gambar.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Tidak dapat menghapus metadata dari gambar.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Tidak dapat mengubah ukuran gambar.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Lampiran terlalu besar.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Lampiran memuat konten yang tidak valid.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Lampiran memiliki format berkas yang tidak valid.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Lampiran kosong.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Gagal memilih dokumen.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Tampilkan";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@menonaktifkan pesan tersembunyi";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Allegato";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Errore invio allegato";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Nepavyko kovertuoti paveikslo.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Nepavyko apdoroti vaizdo įrašo.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Nepavyko išnagrinėti paveikslo.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Nepavyko pašalinti metaduomenų iš paveikslo.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nepavyko pakeisti paveikslo dydžio.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Priedas yra per didelis.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Priede yra neteisingas turinys.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Priedas yra neteisingo failo formato.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Priedas yra tuščias.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Scelta del documento fallita.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Mostra";
/* No comment provided by engineer. */
"OK" = "OK,";
"BUTTON_OK" = "OK,";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ha disabilitato la scomparsa dei messaggi.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Condividi con Session";
"vc_share_loading_message" = "Preparazione allegati...";
"vc_share_sending_message" = "Invio...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Apri invito di gruppo";
"vc_conversation_settings_invite_button_title" = "Aggiungi membri";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "添付ファイル";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "添付ファイルの送信でエラー";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "画像を変換できません。";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "動画を処理できません。";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "画像をパースできません。";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "画像からメタデータを消去できませんでした。";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "画像のサイズを変更できません。";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "添付ファイルが大きすぎます。";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "添付ファイルが無効です。";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "添付ファイルのフォーマットが不正です。";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "添付ファイルの中身が空です。";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "ドキュメントの選択に失敗しました";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "表示内容";
/* No comment provided by engineer. */
"OK" = "確定";
"BUTTON_OK" = "確定";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@がメッセージの消失をオフにしました";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Sessionと共有";
"vc_share_loading_message" = "添付ファイルを準備しています...";
"vc_share_sending_message" = "送信中…";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "公開グループからの招待";
"vc_conversation_settings_invite_button_title" = "メンバーを追加する";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Bijlage";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Versturen bijlage mislukt";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Kan afbeelding niet naar JPEG converteren.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Kan video niet verwerken.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Kan afbeelding niet verwerken.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Kan metagegevens niet uit afbeelding verwijderen.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Afbeelding verkleinen mislukt.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Bijlage is te groot.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Bijlage bevat inhoud die niet wordt ondersteund of niet correct is geformatteerd.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Bijlage is een bestandstype welke niet wordt ondersteund.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Bijlage is leeg.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Document kiezen mislukt.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Tonen";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ heeft zelf-wissende berichten uitgeschakeld.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Delen naar de Session";
"vc_share_loading_message" = "Bijlagen voorbereiden...";
"vc_share_sending_message" = "Aan het verzenden...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open groepsuitnodiging";
"vc_conversation_settings_invite_button_title" = "Voeg deelnemers toe";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Załącznik";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Wystąpił błąd podczas wysyłania załącznika";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Nie można przekonwertować obrazu.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Nie można przetworzyć wideo.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Nie można przetworzyć załączonej grafiki.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Nie można usunąć metadanych z obrazu.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nie można zmienić rozmiaru obrazu.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Załącznik jest za duży.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Załącznik zawiera nieprawidłową zawartość.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Załącznik ma nieprawidłowy format pliku.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Załącznik jest pusty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Nie udało się wybrać dokumentu.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Pokaż";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ wyłączył(a) znikające wiadomości.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Udostępnij w Session";
"vc_share_loading_message" = "Przygotowywanie załączników...";
"vc_share_sending_message" = "Wysyłanie...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Otwórz zaproszenie do grupy";
"vc_conversation_settings_invite_button_title" = "Dodaj użytkowników";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Anexo";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Erro ao Enviar Anexo";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Impossível converter imagem.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Não foi possível processar o vídeo.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Não foi possível analisar a imagem.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Não foi possível suprimir os metadados da imagem.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Impossível redimensionar imagem.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anexo excede o tamanho possível.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Anexo inclui conteúdo inválido.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anexo tem um formato de arquivo inválido.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Anexo vazio.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Falha ao selecionar o documento.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Exibir";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ desabilitou mensagens efêmeras.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Compartilhar no Session";
"vc_share_loading_message" = "Preparando anexos...";
"vc_share_sending_message" = "Enviando...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Convite para grupo aberto";
"vc_conversation_settings_invite_button_title" = "Adicionar Membros";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Вложение";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Ошибка отправки вложения";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Не удалось выбрать документ.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Показывать";
/* No comment provided by engineer. */
"OK" = "Ок";
"BUTTON_OK" = "Ок";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ отключил(а) исчезающие сообщения.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Поделиться в Session";
"vc_share_loading_message" = "Подготовка вложений...";
"vc_share_sending_message" = "Отправка...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Открыть приглашение в группу";
"vc_conversation_settings_invite_button_title" = "Добавить участников";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Attachment";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Show";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Príloha";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Chyba pri posielaní prílohy";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Obrázok sa nepodarilo skonvertovať.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video sa nepodarilo spracovať.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Obrázok sa nepodarilo spracovať.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Z obrázku sa nepodarilo odstrániť metadáta.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nepodarilo sa zmeniť veľkosť obrázka.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Príloha je priliš veľká.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Príloha obsahuje neplatný obsah.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Príloha má neplatný formát súboru.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Príloha je prázdna.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Nepodarilo sa vybrať dokument.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Zobraziť";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Pripravujú sa prílohy...";
"vc_share_sending_message" = "Odosiela sa...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Otvoriť skupinovú pozvánku";
"vc_conversation_settings_invite_button_title" = "Pridať členov";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Bilaga";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Fel vid sändning av bilaga";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Det går inte att konvertera bilden.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Det går inte att bearbeta videon.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Det går inte att tolka bilden.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Det går inte att ta bort metadata från bilden.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Det går inte att ändra storlek på bilden.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Bilagan är för stor.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Bilagan innehåller ogiltigt innehåll.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Bilagan har ett ogiltigt filformat.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Bilagan är tom.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Det gick inte att välja dokument.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Visa";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ inaktiverade försvinnande meddelanden.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Dela i Session";
"vc_share_loading_message" = "Förbereder bilagor...";
"vc_share_sending_message" = "Skickar...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Öppen gruppinbjudan";
"vc_conversation_settings_invite_button_title" = "Lägg till medlemmar";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "ไฟล์แนบ";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "ส่งไฟล์แนบโดนผิดพลาด";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "เลือกไฟล์ไม่ได้สำเร็จ";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "แสดง";
/* No comment provided by engineer. */
"OK" = "ตกลง";
"BUTTON_OK" = "ตกลง";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ได้ปิดใช้งานข้อความที่ลบตัวเองแล้ว";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "เชิญมาใช้ Session";
"vc_share_loading_message" = "รวบรวมสิ่งแนบ...";
"vc_share_sending_message" = "กำลังส่ง...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "การเชิญเข้าร่วมกลุ่ม";
"vc_conversation_settings_invite_button_title" = "เพิ่มสมาชิก";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "Attachment";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document.";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "Show";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages.";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "Share to Session";
"vc_share_loading_message" = "Preparing attachments...";
"vc_share_sending_message" = "Sending...";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "Open group invitation";
"vc_conversation_settings_invite_button_title" = "Add Members";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "附件";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "寄送附件時發生錯誤";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image.";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video.";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image.";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image.";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image.";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large.";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format.";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "選取檔案時發生錯誤";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "顯示";
/* No comment provided by engineer. */
"OK" = "OK";
"BUTTON_OK" = "OK";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 取消了閱後即焚模式";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "分享至 Session";
"vc_share_loading_message" = "準備附件中⋯";
"vc_share_sending_message" = "傳送中⋯";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "打開群組邀請";
"vc_conversation_settings_invite_button_title" = "新增成員";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -26,6 +26,24 @@
"ATTACHMENT_DEFAULT_FILENAME" = "附件";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "附件发送错误";
/* Attachment error message for image attachments which could not be converted to JPEG */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "无法转换图片。";
/* Attachment error message for video attachments which could not be converted to MP4 */
"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "无法处理该视频。";
/* Attachment error message for image attachments which cannot be parsed */
"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "无法解析图片。";
/* Attachment error message for image attachments in which metadata could not be removed */
"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "无法删除图片的元数据。";
/* Attachment error message for image attachments which could not be resized */
"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "无法调整图像大小。";
/* Attachment error message for attachments whose data exceed file size limits */
"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "附件过大。";
/* Attachment error message for attachments with invalid data */
"ATTACHMENT_ERROR_INVALID_DATA" = "附件中存在无效内容。";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "附件的文件格式无效。";
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "附件为空。";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "文件选取失败。";
/* Alert body when picking a document fails because user picked a directory/bundle */
@ -269,7 +287,7 @@
/* No comment provided by engineer. */
"NOTIFICATIONS_SHOW" = "显示";
/* No comment provided by engineer. */
"OK" = "好";
"BUTTON_OK" = "好";
/* Info Message when {{other user}} disables or doesn't support disappearing messages */
"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 取消了阅后即焚。";
/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */
@ -543,6 +561,10 @@
"vc_share_title" = "分享到 Session";
"vc_share_loading_message" = "正在准备附件......";
"vc_share_sending_message" = "正在发送…";
"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link";
"vc_share_link_previews_error" = "Unable to load preview";
"vc_share_link_previews_disabled_title" = "Link Previews Disabled";
"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings.";
"view_open_group_invitation_description" = "打开群组邀请";
"vc_conversation_settings_invite_button_title" = "添加成员";
"vc_settings_faq_button_title" = "FAQ";

View File

@ -124,7 +124,7 @@ final class DisplayNameVC : BaseVC {
@objc private func register() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

View File

@ -124,7 +124,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
func continueWithSeed(_ seed: Data) {
if (seed.count != 16) {
let alert = UIAlertController(title: NSLocalizedString("invalid_recovery_phrase", comment: ""), message: NSLocalizedString("Please check the Recovery Phrase and try again.", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: { _ in
self.scanQRCodeWrapperVC.startCapture()
}))
presentAlert(alert)
@ -273,7 +273,7 @@ private final class RecoveryPhraseVC : UIViewController {
@objc private func handleContinueButtonTapped() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
let mnemonic = mnemonicTextView.text!.lowercased()

View File

@ -90,7 +90,7 @@ final class PNModeVC : BaseVC, OptionViewDelegate {
guard selectedOptionView != nil else {
let title = NSLocalizedString("vc_pn_mode_no_option_picked_modal_title", comment: "")
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
return present(alert, animated: true, completion: nil)
}
UserDefaults.standard[.isUsingFullAPNs] = (selectedOptionView == apnsOptionView)

View File

@ -157,7 +157,7 @@ final class RestoreVC : BaseVC {
@objc private func restore() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
let mnemonic = mnemonicTextView.text!.lowercased()

View File

@ -161,7 +161,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
// MARK: Convenience
private func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
}

View File

@ -40,10 +40,6 @@ final class PathVC : BaseVC {
private func setUpNavBar() {
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("vc_path_title", comment: ""))
// Set up close button
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
navigationItem.leftBarButtonItem = closeButton
}
private func setUpViewHierarchy() {
@ -167,10 +163,6 @@ final class PathVC : BaseVC {
}
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@objc private func learnMore() {
let urlAsString = "https://getsession.org/faq/#onion-routing"
let url = URL(string: urlAsString)!

View File

@ -146,13 +146,13 @@ final class NukeDataModal : Modal {
message = String(format: NSLocalizedString("dialog_clear_all_data_deletion_failed_2", comment: ""), String(potentiallyMaliciousSnodes.count), potentiallyMaliciousSnodes.joined(separator: ", "))
}
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
}
}.catch(on: DispatchQueue.main) { error in
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
}
}

View File

@ -123,7 +123,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: NSLocalizedString("Please check the Session ID and try again.", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
} else {
let thread = TSContactThread.getOrCreateThread(contactSessionID: hexEncodedPublicKey)

View File

@ -244,7 +244,21 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
button.set(.height, to: SettingsVC.buttonHeight)
return button
}
let pathButton = getSettingButton(withTitle: NSLocalizedString("vc_path_title", comment: ""), color: Colors.text, action: #selector(showPath))
let pathStatusView = PathStatusView()
pathStatusView.set(.width, to: PathStatusView.size)
pathStatusView.set(.height, to: PathStatusView.size)
pathButton.addSubview(pathStatusView)
pathStatusView.pin(.leading, to: .trailing, of: pathButton.titleLabel!, withInset: Values.smallSpacing)
pathStatusView.autoVCenterInSuperview()
pathButton.titleEdgeInsets = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: Values.smallSpacing)
return [
getSeparator(),
pathButton,
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
getSeparator(),
@ -377,7 +391,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
@ -443,7 +457,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
@objc private func handleSaveDisplayNameButtonTapped() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
@ -480,6 +494,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
navigationController!.present(shareVC, animated: true, completion: nil)
}
@objc private func showPath() {
let pathVC = PathVC()
navigationController!.pushViewController(pathVC, animated: true)
}
@objc private func showPrivacySettings() {
let privacySettingsVC = PrivacySettingsTableViewController()
navigationController!.pushViewController(privacySettingsVC, animated: true)

View File

@ -75,6 +75,16 @@ class BaseVC : UIViewController {
crossfadeLabel.pin(to: container)
navigationItem.titleView = container
}
internal func setUpNavBarSessionHeading() {
let headingImageView = UIImageView()
headingImageView.tintColor = Colors.sessionHeading
headingImageView.image = UIImage(named: "SessionHeading")?.withRenderingMode(.alwaysTemplate)
headingImageView.contentMode = .scaleAspectFit
headingImageView.set(.width, to: 150)
headingImageView.set(.height, to: Values.mediumFontSize)
navigationItem.titleView = headingImageView
}
internal func setUpNavBarSessionIcon() {
let logoImageView = UIImageView()

View File

@ -2,7 +2,12 @@ import UIKit
import SessionUIKit
final class ConversationCell : UITableViewCell {
var threadViewModel: ThreadViewModel! { didSet { update() } }
var isShowingGlobalSearchResult = false
var threadViewModel: ThreadViewModel! {
didSet {
isShowingGlobalSearchResult ? updateForSearchResult() : update()
}
}
static let reuseIdentifier = "ConversationCell"
@ -23,7 +28,7 @@ final class ConversationCell : UITableViewCell {
let result = UIView()
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
let size = ConversationCell.unreadCountViewSize
result.set(.width, to: size)
result.set(.width, greaterThanOrEqualTo: size)
result.set(.height, to: size)
result.layer.masksToBounds = true
result.layer.cornerRadius = size / 2
@ -96,6 +101,22 @@ final class ConversationCell : UITableViewCell {
return result
}()
private lazy var topLabelStackView: UIStackView = {
let result = UIStackView()
result.axis = .horizontal
result.alignment = .center
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
return result
}()
private lazy var bottomLabelStackView: UIStackView = {
let result = UIStackView()
result.axis = .horizontal
result.alignment = .center
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
return result
}()
// MARK: Settings
private static let unreadCountViewSize: CGFloat = 20
private static let statusIndicatorSize: CGFloat = 14
@ -129,27 +150,28 @@ final class ConversationCell : UITableViewCell {
profilePictureView.size = profilePictureViewSize
// Unread count view
unreadCountView.addSubview(unreadCountLabel)
unreadCountLabel.pin(to: unreadCountView)
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
// Has mention view
hasMentionView.addSubview(hasMentionLabel)
hasMentionLabel.pin(to: hasMentionView)
// Label stack view
let topLabelSpacer = UIView.hStretchingSpacer()
let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ])
topLabelStackView.axis = .horizontal
topLabelStackView.alignment = .center
topLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
topLabelStackView.addArrangedSubview(view)
}
let snippetLabelContainer = UIView()
snippetLabelContainer.addSubview(snippetLabel)
snippetLabelContainer.addSubview(typingIndicatorView)
let bottomLabelSpacer = UIView.hStretchingSpacer()
let bottomLabelStackView = UIStackView(arrangedSubviews: [ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ])
bottomLabelStackView.axis = .horizontal
bottomLabelStackView.alignment = .center
bottomLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
let labelContainerView = UIView()
labelContainerView.addSubview(topLabelStackView)
labelContainerView.addSubview(bottomLabelStackView)
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
bottomLabelStackView.addArrangedSubview(view)
}
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
labelContainerView.axis = .vertical
labelContainerView.alignment = .leading
labelContainerView.spacing = 6
// Main stack view
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
stackView.axis = .horizontal
@ -172,16 +194,6 @@ final class ConversationCell : UITableViewCell {
snippetLabel.pin(to: snippetLabelContainer)
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
// HACK: Not using a stack view for this is part of a workaround for a weird layout bug
topLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
topLabelStackView.pin(.top, to: .top, of: labelContainerView, withInset: 12)
topLabelStackView.pin(.trailing, to: .trailing, of: labelContainerView)
bottomLabelStackView.pin(.leading, to: .leading, of: labelContainerView)
bottomLabelStackView.pin(.top, to: .bottom, of: topLabelStackView, withInset: 6)
labelContainerView.pin(.bottom, to: .bottom, of: bottomLabelStackView, withInset: 12)
// HACK: The two lines below are part of a workaround for a weird layout bug
labelContainerView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - Values.mediumSpacing - profilePictureViewSize - Values.mediumSpacing - Values.mediumSpacing)
labelContainerView.set(.height, to: cellHeight)
stackView.pin(.leading, to: .leading, of: contentView)
stackView.pin(.top, to: .top, of: contentView)
// HACK: The two lines below are part of a workaround for a weird layout bug
@ -189,6 +201,79 @@ final class ConversationCell : UITableViewCell {
stackView.set(.height, to: cellHeight)
}
// MARK: Updating for search results
private func updateForSearchResult() {
AssertIsOnMainThread()
guard let thread = threadViewModel?.threadRecord else { return }
profilePictureView.update(for: thread)
isPinnedIcon.isHidden = true
unreadCountView.isHidden = true
hasMentionView.isHidden = true
}
public func configureForRecent() {
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
bottomLabelStackView.isHidden = false
let snippet = String(format: NSLocalizedString("RECENT_SEARCH_LAST_MESSAGE_DATETIME", comment: ""), DateUtil.formatDate(forDisplay: threadViewModel.lastMessageDate))
snippetLabel.attributedText = NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text.withAlphaComponent(Values.lowOpacity)])
timestampLabel.isHidden = true
}
public func configure(messageDate: Date?, snippet: String?, searchText: String, message: TSMessage? = nil) {
let normalizedSearchText = searchText.lowercased()
if let messageDate = messageDate, let snippet = snippet {
// Message
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
timestampLabel.isHidden = false
timestampLabel.text = DateUtil.formatDate(forDisplay: messageDate)
bottomLabelStackView.isHidden = false
var rawSnippet = snippet
if let message = message, let name = getMessageAuthorName(message: message) {
rawSnippet = "\(name): \(snippet)"
}
snippetLabel.attributedText = getHighlightedSnippet(snippet: rawSnippet, searchText: normalizedSearchText, fontSize: Values.smallFontSize)
} else {
// Contact
if threadViewModel.isGroupThread, let thread = threadViewModel.threadRecord as? TSGroupThread {
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayName(), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
bottomLabelStackView.isHidden = false
let context: Contact.Context = thread.isOpenGroup ? .openGroup : .regular
var rawSnippet: String = ""
thread.groupModel.groupMemberIds.forEach{ id in
if let displayName = Storage.shared.getContact(with: id)?.displayName(for: context) {
if !rawSnippet.isEmpty {
rawSnippet += ", \(displayName)"
}
if displayName.lowercased().contains(normalizedSearchText) {
rawSnippet = displayName
}
}
}
snippetLabel.attributedText = getHighlightedSnippet(snippet: rawSnippet, searchText: normalizedSearchText, fontSize: Values.smallFontSize)
} else {
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayNameForSearch(threadViewModel.contactSessionID!), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
bottomLabelStackView.isHidden = true
}
timestampLabel.isHidden = true
}
}
private func getHighlightedSnippet(snippet: String, searchText: String, fontSize: CGFloat) -> NSMutableAttributedString {
guard snippet != NSLocalizedString("NOTE_TO_SELF", comment: "") else {
return NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text])
}
let result = NSMutableAttributedString(string: snippet, attributes: [.foregroundColor:Colors.text.withAlphaComponent(Values.lowOpacity)])
let normalizedSnippet = snippet.lowercased() as NSString
guard normalizedSnippet.contains(searchText) else { return result }
let range = normalizedSnippet.range(of: searchText)
result.addAttribute(.foregroundColor, value: Colors.text, range: range)
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: range)
return result
}
// MARK: Updating
private func update() {
AssertIsOnMainThread()
@ -210,8 +295,8 @@ final class ConversationCell : UITableViewCell {
isPinnedIcon.isHidden = !threadViewModel.isPinned
unreadCountView.isHidden = !threadViewModel.hasUnreadMessages
let unreadCount = threadViewModel.unreadCount
unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+"
let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8
unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+"
let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8
unreadCountLabel.font = .boldSystemFont(ofSize: fontSize)
hasMentionView.isHidden = !(threadViewModel.hasUnreadMentions && thread.isGroupThread())
profilePictureView.update(for: thread)
@ -246,6 +331,27 @@ final class ConversationCell : UITableViewCell {
}
}
private func getMessageAuthorName(message: TSMessage) -> String? {
guard threadViewModel.isGroupThread else { return nil }
if let incomingMessage = message as? TSIncomingMessage {
return Storage.shared.getContact(with: incomingMessage.authorId)?.displayName(for: .regular) ?? "Anonymous"
}
return nil
}
private func getDisplayNameForSearch(_ sessionID: String) -> String {
if threadViewModel.threadRecord.isNoteToSelf() {
return NSLocalizedString("NOTE_TO_SELF", comment: "")
} else {
var result = sessionID
if let contact = Storage.shared.getContact(with: sessionID), let name = contact.name {
result = name
if let nickname = contact.nickname { result += "(\(nickname))"}
}
return result
}
}
private func getDisplayName() -> String {
if threadViewModel.isGroupThread {
if threadViewModel.name.isEmpty {
@ -275,9 +381,12 @@ final class ConversationCell : UITableViewCell {
result.append(imageString)
result.append(NSAttributedString(string: " ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
}
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
if threadViewModel.isGroupThread, let message = threadViewModel.lastMessageForInbox as? TSMessage, let name = getMessageAuthorName(message: message) {
result.append(NSAttributedString(string: "\(name): ", attributes: [ .font : font, .foregroundColor : Colors.text ]))
}
if let rawSnippet = threadViewModel.lastMessageText {
let snippet = MentionUtilities.highlightMentions(in: rawSnippet, threadID: threadViewModel.threadRecord.uniqueId!)
let font = threadViewModel.hasUnreadMessages ? UIFont.boldSystemFont(ofSize: Values.smallFontSize) : UIFont.systemFont(ofSize: Values.smallFontSize)
result.append(NSAttributedString(string: snippet, attributes: [ .font : font, .foregroundColor : Colors.text ]))
}
return result

View File

@ -155,31 +155,10 @@ NS_ASSUME_NONNULL_BEGIN
if (_read && readTimestamp >= self.expireStartedAt) {
return;
}
BOOL isTrusted = YES;
TSThread* thread = [self threadWithTransaction:transaction];
if ([thread isKindOfClass:[TSContactThread class]]) {
TSContactThread* contactThread = (TSContactThread*)thread;
isTrusted = [[LKStorage shared] getContactWithSessionID:[contactThread contactSessionID] using:transaction].isTrusted;
}
BOOL areAllAttachmentsDownloaded = YES;
if (isTrusted) {
for (NSString *attachmentId in self.attachmentIds) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
// If the attachment download failed, we can mark this message as read.
// Otherwise, this message will never be marked as read.
if ([attachment isKindOfClass:[TSAttachmentPointer class]]
&& ((TSAttachmentPointer *)attachment).state == TSAttachmentPointerStateFailed) {
continue;
}
areAllAttachmentsDownloaded = areAllAttachmentsDownloaded && attachment.isDownloaded;
if (!areAllAttachmentsDownloaded) break;
}
}
if (!areAllAttachmentsDownloaded) {
return;
}
// We just ignore all attachments download state here and mark all messages as read
// This is a workaround for a situation that some large attachments won't be downloaded
// and just stuck in a downloading state. In that case, the corresponding message won't
// be able to be marked as read.
_read = YES;
[self saveWithTransaction:transaction];

View File

@ -114,6 +114,9 @@ public class SignalAttachment: NSObject {
@objc
public var captionText: String?
@objc
public var linkPreviewDraft: OWSLinkPreviewDraft?
@objc
public var data: Data {
@ -292,6 +295,15 @@ public class SignalAttachment: NSObject {
return nil
}
}
@objc
public func text() -> String? {
guard let text = String(data: dataSource.data(), encoding: .utf8) else {
return nil
}
return text
}
// Returns the MIME type for this attachment or nil if no MIME type
// can be identified.

View File

@ -385,10 +385,16 @@ public class OWSLinkPreview: MTLModel {
var urlMatches: [URLMatchResult] = []
let matches = detector.matches(in: body, options: [], range: NSRange(location: 0, length: body.count))
for match in matches {
guard let matchURL = match.url else {
continue
}
let urlString = matchURL.absoluteString
guard let matchURL = match.url else { continue }
// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
// in more previews actually getting loaded without forcing the user to enter 'https://' before
// every URL they enter
let urlString: String = (matchURL.absoluteString == "http://\(body)" ?
"https://\(body)" :
matchURL.absoluteString
)
if isValidLinkUrl(urlString) {
let matchResult = URLMatchResult(urlString: urlString, matchRange: match.range)
urlMatches.append(matchResult)

View File

@ -62,7 +62,15 @@ public final class MentionsManager : NSObject {
if let groupThread = thread as? TSGroupThread, groupThread.groupModel.groupType == .closedGroup {
result = result.union(groupThread.groupModel.groupMemberIds).subtracting([ getUserHexEncodedPublicKey() ])
} else {
guard userPublicKeyCache[threadID] == nil else { return }
let hasOnlyCurrentUser: Bool = (
userPublicKeyCache[threadID]?.count == 1 &&
userPublicKeyCache[threadID]?.first == getUserHexEncodedPublicKey()
)
guard userPublicKeyCache[threadID] == nil || ((thread as? TSGroupThread)?.groupModel.groupType == .openGroup && hasOnlyCurrentUser) else {
return
}
let interactions = transaction.ext(TSMessageDatabaseViewExtensionName) as! YapDatabaseViewTransaction
interactions.enumerateKeysAndObjects(inGroup: threadID) { _, _, object, index, _ in
guard let message = object as? TSIncomingMessage, index < userIDScanLimit else { return }

View File

@ -44,7 +44,7 @@ public final class ClosedGroupPoller : NSObject {
// Might be a race condition that the setUpPolling finishes too soon,
// and the timer is not created, if we mark the group as is polling
// after setUpPolling. So the poller may not work, thus misses messages.
isPolling[groupPublicKey] = true
internalQueue.sync{ isPolling[groupPublicKey] = true }
setUpPolling(for: groupPublicKey)
}
@ -55,7 +55,7 @@ public final class ClosedGroupPoller : NSObject {
}
public func stopPolling(for groupPublicKey: String) {
isPolling[groupPublicKey] = false
internalQueue.sync{ isPolling[groupPublicKey] = false }
timers[groupPublicKey]?.invalidate()
}

View File

@ -69,6 +69,13 @@ NSString *const TSContactThreadPrefix = @"c";
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
}
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
{
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
}
+ (NSString *)threadIDFromContactSessionID:(NSString *)contactSessionID {
return [TSContactThreadPrefix stringByAppendingString:contactSessionID];
}

View File

@ -192,6 +192,11 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
return self.groupModel.groupName ?: self.class.defaultGroupName;
}
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [self name];
}
+ (NSString *)defaultGroupName
{
return @"Group";

View File

@ -38,6 +38,8 @@ BOOL IsNoteToSelfEnabled(void);
*/
- (NSString *)name;
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction;
/**
* Returns if there is any outgoing interations in this thread.
*

View File

@ -148,6 +148,11 @@ BOOL IsNoteToSelfEnabled(void)
return nil;
}
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return nil;
}
- (NSArray<NSString *> *)recipientIdentifiers
{
return @[];

View File

@ -85,18 +85,19 @@ public class FullTextSearchFinder: NSObject {
return query
}
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
public func enumerateObjects(searchText: String, maxSearchResults: Int? = nil, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
guard let ext: YapDatabaseFullTextSearchTransaction = ext(transaction: transaction) else {
return
}
let query = FullTextSearchFinder.query(searchText: searchText)
let maxSearchResults = 500
let maxSearchResults = maxSearchResults ?? 500
var searchResultCount = 0
let snippetOptions = YapDatabaseFullTextSearchSnippetOptions()
snippetOptions.startMatchText = ""
snippetOptions.endMatchText = ""
snippetOptions.numberOfTokens = 5
ext.enumerateKeysAndObjects(matching: query, with: snippetOptions) { (snippet: String, _: String, _: String, object: Any, stop: UnsafeMutablePointer<ObjCBool>) in
guard searchResultCount < maxSearchResults else {
stop.pointee = true
@ -177,8 +178,12 @@ public class FullTextSearchFinder: NSObject {
}
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in
let displayName = Storage.shared.getContact(with: recipientId)?.displayName(for: Contact.Context.regular) ?? recipientId
return "\(recipientId) \(displayName)"
var result = "\(recipientId)"
if let contact = Storage.shared.getContact(with: recipientId) {
if let name = contact.name { result += " \(name)" }
if let nickname = contact.nickname { result += " \(nickname)" }
}
return result
}
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage, transaction: YapDatabaseReadTransaction) in
@ -241,6 +246,6 @@ public class FullTextSearchFinder: NSObject {
options: nil,
handler: handler,
ftsVersion: YapDatabaseFullTextSearchFTS5Version,
versionTag: "1")
versionTag: "2")
}
}

View File

@ -43,6 +43,7 @@ typedef NS_ENUM(NSUInteger, OWSAudioBehavior) {
@property (nonatomic) BOOL isLooping;
@property (nonatomic) BOOL isPlaying;
@property (nonatomic) float playbackRate;
@property (nonatomic) NSTimeInterval duration;
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior;
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior delegate:(id<OWSAudioPlayerDelegate>)delegate;

View File

@ -168,6 +168,11 @@ NS_ASSUME_NONNULL_BEGIN
return self.audioPlayer.rate;
}
- (NSTimeInterval)duration
{
return [self.audioPlayer duration];
}
- (void)setPlaybackRate:(float)rate
{
[self.audioPlayer setRate:rate];

View File

@ -29,7 +29,7 @@ enum ProofOfWork {
value = newValue
}
// Encode as base 64
let base64EncodedNonce = nonce.bigEndianBytes.toBase64()!
let base64EncodedNonce = nonce.bigEndianBytes.toBase64()
// Return
return (timestamp, base64EncodedNonce)
}

View File

@ -9,6 +9,7 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
let appLaunchTime = Date()
let isMainApp = false
let isMainAppAndActive = false
var isShareExtension: Bool = false
var openSystemSettingsAction: UIAlertAction?
var wasWokenUpByPushNotification = true

View File

@ -6,8 +6,6 @@
#import <UIKit/UIKit.h>
// Separate iOS Frameworks from other imports.
#import "SAEScreenLockViewController.h"
#import "ShareAppExtensionContext.h"
#import <SignalCoreKit/NSObject+OWS.h>
#import <SignalCoreKit/OWSAsserts.h>
#import <SignalCoreKit/OWSLogs.h>

View File

@ -1,18 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSViewController.h>
#import <SignalUtilitiesKit/ScreenLockViewController.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ShareViewDelegate;
@interface SAEScreenLockViewController : ScreenLockViewController
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,206 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "SAEScreenLockViewController.h"
#import "UIColor+OWS.h"
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SessionUtilitiesKit/AppContext.h>
NS_ASSUME_NONNULL_BEGIN
@interface SAEScreenLockViewController () <ScreenLockViewDelegate>
@property (nonatomic, readonly, weak) id<ShareViewDelegate> shareViewDelegate;
@property (nonatomic) BOOL hasShownAuthUIOnce;
@property (nonatomic) BOOL isShowingAuthUI;
@end
#pragma mark -
@implementation SAEScreenLockViewController
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate
{
self = [super init];
if (!self) {
return self;
}
_shareViewDelegate = shareViewDelegate;
self.delegate = self;
return self;
}
- (void)loadView
{
[super loadView];
UIView.appearance.tintColor = LKColors.text;
// Gradient background
self.view.backgroundColor = UIColor.clearColor;
CAGradientLayer *layer = [CAGradientLayer new];
layer.frame = UIScreen.mainScreen.bounds;
UIColor *gradientStartColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFCFCFC] : [UIColor colorWithRGBHex:0x171717];
UIColor *gradientEndColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFFFFFF] : [UIColor colorWithRGBHex:0x121212];
layer.colors = @[ (id)gradientStartColor.CGColor, (id)gradientEndColor.CGColor ];
[self.view.layer insertSublayer:layer atIndex:0];
// Navigation bar background color
UINavigationBar *navigationBar = self.navigationController.navigationBar;
[navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
navigationBar.shadowImage = [UIImage new];
[navigationBar setTranslucent:NO];
navigationBar.barTintColor = LKColors.navigationBarBackground;
// Title
UILabel *titleLabel = [UILabel new];
titleLabel.text = NSLocalizedString(@"vc_share_title", @"");
titleLabel.textColor = LKColors.text;
titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize];
self.navigationItem.titleView = titleLabel;
// Close button
UIBarButtonItem *closeButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"X"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissPressed:)];
closeButton.tintColor = LKColors.text;
self.navigationItem.leftBarButtonItem = closeButton;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self ensureUI];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self ensureUI];
// Auto-show the auth UI f
if (!self.hasShownAuthUIOnce) {
self.hasShownAuthUIOnce = YES;
[self tryToPresentAuthUIToUnlockScreenLock];
}
}
- (void)dealloc
{
// Surface memory leaks by logging the deallocation of view controllers.
OWSLogVerbose(@"Dealloc: %@", self.class);
}
- (void)tryToPresentAuthUIToUnlockScreenLock
{
OWSAssertIsOnMainThread();
if (self.isShowingAuthUI) {
// We're already showing the auth UI; abort.
return;
}
OWSLogInfo(@"try to unlock screen lock");
self.isShowingAuthUI = YES;
[OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{
OWSAssertIsOnMainThread();
OWSLogInfo(@"unlock screen lock succeeded.");
self.isShowingAuthUI = NO;
[self.shareViewDelegate shareViewWasUnlocked];
}
failure:^(NSError *error) {
OWSAssertIsOnMainThread();
OWSLogInfo(@"unlock screen lock failed.");
self.isShowingAuthUI = NO;
[self ensureUI];
[self showScreenLockFailureAlertWithMessage:error.localizedDescription];
}
unexpectedFailure:^(NSError *error) {
OWSAssertIsOnMainThread();
OWSLogInfo(@"unlock screen lock unexpectedly failed.");
self.isShowingAuthUI = NO;
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
// it appears to be effective to retry again after waiting a bit.
dispatch_async(dispatch_get_main_queue(), ^{
[self ensureUI];
});
}
cancel:^{
OWSAssertIsOnMainThread();
OWSLogInfo(@"unlock screen lock cancelled.");
self.isShowingAuthUI = NO;
[self ensureUI];
}];
[self ensureUI];
}
- (void)ensureUI
{
[self updateUIWithState:ScreenLockUIStateScreenLock isLogoAtTop:NO animated:NO];
}
- (void)showScreenLockFailureAlertWithMessage:(NSString *)message
{
OWSAssertIsOnMainThread();
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_FAILED",
@"Title for alert indicating that screen lock could not be unlocked.")
message:message
buttonTitle:nil
buttonAction:^(UIAlertAction *action) {
// After the alert, update the UI.
[self ensureUI];
}
fromViewController:self];
}
- (void)dismissPressed:(id)sender
{
OWSLogDebug(@"tapped dismiss share button");
[self cancelShareExperience];
}
- (void)cancelShareExperience
{
[self.shareViewDelegate shareViewWasCancelled];
}
#pragma mark - ScreenLockViewDelegate
- (void)unlockButtonWasTapped
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"unlockButtonWasTapped");
[self tryToPresentAuthUIToUnlockScreenLock];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,206 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import PromiseKit
import SignalCoreKit
import SignalUtilitiesKit
import SessionUIKit
import SessionUtilitiesKit
final class SAEScreenLockViewController: ScreenLockViewController, ScreenLockViewDelegate {
private var hasShownAuthUIOnce: Bool = false
private var isShowingAuthUI: Bool = false
private weak var shareViewDelegate: ShareViewDelegate?
// MARK: - Initialization
init(shareViewDelegate: ShareViewDelegate) {
super.init(nibName: nil, bundle: nil)
self.shareViewDelegate = shareViewDelegate
self.delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
OWSLogger.verbose("Dealloc: \(type(of: self))")
}
// MARK: - UI
private lazy var gradientBackground: CAGradientLayer = {
let layer: CAGradientLayer = CAGradientLayer()
let gradientStartColor: UIColor = (LKAppModeUtilities.isLightMode ?
UIColor(rgbHex: 0xFCFCFC) :
UIColor(rgbHex: 0x171717)
)
let gradientEndColor: UIColor = (LKAppModeUtilities.isLightMode ?
UIColor(rgbHex: 0xFFFFFF) :
UIColor(rgbHex: 0x121212)
)
layer.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
return layer
}()
private lazy var titleLabel: UILabel = {
let titleLabel: UILabel = UILabel()
titleLabel.font = UIFont.boldSystemFont(ofSize: Values.veryLargeFontSize)
titleLabel.text = "vc_share_title".localized()
titleLabel.textColor = Colors.text
return titleLabel
}()
private lazy var closeButton: UIBarButtonItem = {
let closeButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "X"), style: .plain, target: self, action: #selector(dismissPressed))
closeButton.tintColor = Colors.text
return closeButton
}()
// MARK: - Lifecycle
override func loadView() {
super.loadView()
UIView.appearance().tintColor = Colors.text
self.view.backgroundColor = UIColor.clear
self.view.layer.insertSublayer(gradientBackground, at: 0)
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.tintColor = Colors.navigationBarBackground
self.navigationItem.titleView = titleLabel
self.navigationItem.leftBarButtonItem = closeButton
setupLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.ensureUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.ensureUI()
// Auto-show the auth UI f
if !hasShownAuthUIOnce {
hasShownAuthUIOnce = true
self.tryToPresentAuthUIToUnlockScreenLock()
}
}
// MARK: - Layout
private func setupLayout() {
gradientBackground.frame = UIScreen.main.bounds
}
// MARK: - Functions
private func tryToPresentAuthUIToUnlockScreenLock() {
AssertIsOnMainThread()
// If we're already showing the auth UI; abort.
if self.isShowingAuthUI { return }
OWSLogger.info("try to unlock screen lock")
isShowingAuthUI = true
OWSScreenLock.shared.tryToUnlockScreenLock(
success: { [weak self] in
AssertIsOnMainThread()
OWSLogger.info("unlock screen lock succeeded.")
self?.isShowingAuthUI = false
self?.shareViewDelegate?.shareViewWasUnlocked()
},
failure: { [weak self] error in
AssertIsOnMainThread()
OWSLogger.info("unlock screen lock failed.")
self?.isShowingAuthUI = false
self?.ensureUI()
self?.showScreenLockFailureAlert(message: error.localizedDescription)
},
unexpectedFailure: { [weak self] error in
AssertIsOnMainThread()
OWSLogger.info("unlock screen lock unexpectedly failed.")
self?.isShowingAuthUI = false
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
// it appears to be effective to retry again after waiting a bit.
DispatchQueue.main.async {
self?.ensureUI()
}
},
cancel: { [weak self] in
AssertIsOnMainThread()
OWSLogger.info("unlock screen lock cancelled.")
self?.isShowingAuthUI = false
self?.ensureUI()
}
)
self.ensureUI()
}
private func ensureUI() {
self.updateUI(with: .screenLock, isLogoAtTop: false, animated: false)
}
private func showScreenLockFailureAlert(message: String) {
AssertIsOnMainThread()
OWSAlerts.showAlert(
// Title for alert indicating that screen lock could not be unlocked.
title: "SCREEN_LOCK_UNLOCK_FAILED".localized(),
message: message,
buttonTitle: nil,
buttonAction: { [weak self] action in
// After the alert, update the UI
self?.ensureUI()
},
fromViewController: self
)
}
// MARK: - Transitions
@objc private func dismissPressed() {
OWSLogger.debug("unlock screen lock cancelled.")
self.cancelShareExperience()
}
private func cancelShareExperience() {
self.shareViewDelegate?.shareViewWasCancelled()
}
// MARK: - ScreenLockViewDelegate
func unlockButtonWasTapped() {
AssertIsOnMainThread()
OWSLogger.info("unlockButtonWasTapped")
self.tryToPresentAuthUIToUnlockScreenLock()
}
}

View File

@ -1,18 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <SessionUtilitiesKit/AppContext.h>
NS_ASSUME_NONNULL_BEGIN
// This is _NOT_ a singleton and will be instantiated each time that the SAE is used.
@interface ShareAppExtensionContext : NSObject <AppContext>
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,240 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ShareAppExtensionContext.h"
#import <SignalUtilitiesKit/UIViewController+OWS.h>
#import <SessionMessagingKit/OWSStorage.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SignalUtilitiesKit/TSConstants.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@interface ShareAppExtensionContext ()
@property (nonatomic) UIViewController *rootViewController;
@property (atomic) UIApplicationState reportedApplicationState;
@end
#pragma mark -
@implementation ShareAppExtensionContext
@synthesize mainWindow = _mainWindow;
@synthesize appLaunchTime = _appLaunchTime;
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController
{
self = [super init];
if (!self) {
return self;
}
OWSAssertDebug(rootViewController);
_rootViewController = rootViewController;
self.reportedApplicationState = UIApplicationStateActive;
_appLaunchTime = [NSDate new];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(extensionHostDidBecomeActive:)
name:NSExtensionHostDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(extensionHostWillResignActive:)
name:NSExtensionHostWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(extensionHostDidEnterBackground:)
name:NSExtensionHostDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(extensionHostWillEnterForeground:)
name:NSExtensionHostWillEnterForegroundNotification
object:nil];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notifications
- (void)extensionHostDidBecomeActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
self.reportedApplicationState = UIApplicationStateActive;
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil];
}
- (void)extensionHostWillResignActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
self.reportedApplicationState = UIApplicationStateInactive;
OWSLogInfo(@"");
[DDLog flushLog];
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillResignActiveNotification object:nil];
}
- (void)extensionHostDidEnterBackground:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[DDLog flushLog];
self.reportedApplicationState = UIApplicationStateBackground;
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidEnterBackgroundNotification object:nil];
}
- (void)extensionHostWillEnterForeground:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
self.reportedApplicationState = UIApplicationStateInactive;
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillEnterForegroundNotification object:nil];
}
#pragma mark -
- (BOOL)isMainApp
{
return NO;
}
- (BOOL)isMainAppAndActive
{
return NO;
}
- (BOOL)isRTL
{
static BOOL isRTL = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Borrowed from PureLayout's AppExtension compatible RTL support.
// App Extensions may not access -[UIApplication sharedApplication]; fall back to checking the bundle's
// preferred localization character direction
isRTL = [NSLocale characterDirectionForLanguage:[[NSBundle mainBundle] preferredLocalizations][0]]
== NSLocaleLanguageDirectionRightToLeft;
});
return isRTL;
}
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated
{
OWSLogInfo(@"Ignoring request to show/hide status bar since we're in an app extension");
}
- (CGFloat)statusBarHeight
{
return 20;
}
- (BOOL)isInBackground
{
return self.reportedApplicationState == UIApplicationStateBackground;
}
- (BOOL)isAppForegroundAndActive
{
return self.reportedApplicationState == UIApplicationStateActive;
}
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:
(BackgroundTaskExpirationHandler)expirationHandler
{
return UIBackgroundTaskInvalid;
}
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier
{
OWSAssertDebug(backgroundTaskIdentifier == UIBackgroundTaskInvalid);
}
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects
{
OWSLogDebug(@"Ignoring request to block sleep.");
}
- (void)setMainAppBadgeNumber:(NSInteger)value
{
OWSFailDebug(@"");
}
- (nullable UIViewController *)frontmostViewController
{
OWSAssertDebug(self.rootViewController);
return [self.rootViewController findFrontmostViewController:YES];
}
- (nullable UIAlertAction *)openSystemSettingsAction
{
return nil;
}
- (BOOL)isRunningTests
{
// We don't need to distinguish this in the SAE.
return NO;
}
- (void)setNetworkActivityIndicatorVisible:(BOOL)value
{
OWSFailDebug(@"");
}
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block
{
OWSFailDebug(@"cannot run main app active blocks in share extension.");
}
- (id<SSKKeychainStorage>)keychainStorage
{
return [SSKDefaultKeychainStorage shared];
}
- (NSString *)appDocumentDirectoryPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectoryURL =
[[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDirectoryURL path];
}
- (NSString *)appSharedDataDirectoryPath
{
NSURL *groupContainerDirectoryURL =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup];
return [groupContainerDirectoryURL path];
}
- (NSUserDefaults *)appUserDefaults
{
return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,205 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SignalUtilitiesKit
import SessionUtilitiesKit
import SessionMessagingKit
/// This is _NOT_ a singleton and will be instantiated each time that the SAE is used.
final class ShareAppExtensionContext: NSObject, AppContext {
var rootViewController: UIViewController
var reportedApplicationState: UIApplication.State
let appLaunchTime = Date()
let isMainApp = false
let isMainAppAndActive = false
var isShareExtension: Bool = true
var mainWindow: UIWindow?
var wasWokenUpByPushNotification: Bool = false
private static var _isRTL: Bool = {
// Borrowed from PureLayout's AppExtension compatible RTL support.
// App Extensions may not access -[UIApplication sharedApplication]; fall back
// to checking the bundle's preferred localization character direction
return (
Locale.characterDirection(
forLanguage: (Bundle.main.preferredLocalizations.first ?? "")
) == Locale.LanguageDirection.rightToLeft
)
}()
var isRTL: Bool { return ShareAppExtensionContext._isRTL }
var isRunningTests: Bool { return false } // We don't need to distinguish this in the SAE
var statusBarHeight: CGFloat { return 20 }
var openSystemSettingsAction: UIAlertAction?
// MARK: - Initialization
init(rootViewController: UIViewController) {
self.rootViewController = rootViewController
self.reportedApplicationState = .active
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(extensionHostDidBecomeActive(notification:)),
name: .NSExtensionHostDidBecomeActive,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(extensionHostWillResignActive(notification:)),
name: .NSExtensionHostWillResignActive,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(extensionHostDidEnterBackground(notification:)),
name: .NSExtensionHostDidEnterBackground,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(extensionHostWillEnterForeground(notification:)),
name: .NSExtensionHostWillEnterForeground,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Notifications
@objc private func extensionHostDidBecomeActive(notification: NSNotification) {
AssertIsOnMainThread()
OWSLogger.info("")
self.reportedApplicationState = .active
NotificationCenter.default.post(
name: .OWSApplicationDidBecomeActive,
object: nil
)
}
@objc private func extensionHostWillResignActive(notification: NSNotification) {
AssertIsOnMainThread()
self.reportedApplicationState = .inactive
OWSLogger.info("")
DDLog.flushLog()
NotificationCenter.default.post(
name: .OWSApplicationWillResignActive,
object: nil
)
}
@objc private func extensionHostDidEnterBackground(notification: NSNotification) {
AssertIsOnMainThread()
OWSLogger.info("")
DDLog.flushLog()
self.reportedApplicationState = .background
NotificationCenter.default.post(
name: .OWSApplicationDidEnterBackground,
object: nil
)
}
@objc private func extensionHostWillEnterForeground(notification: NSNotification) {
AssertIsOnMainThread()
OWSLogger.info("")
self.reportedApplicationState = .inactive
NotificationCenter.default.post(
name: .OWSApplicationWillEnterForeground,
object: nil
)
}
// MARK: - AppContext Functions
func isAppForegroundAndActive() -> Bool {
return (reportedApplicationState == .active)
}
func isInBackground() -> Bool {
return (reportedApplicationState == .background)
}
func frontmostViewController() -> UIViewController? {
return rootViewController.findFrontmostViewController(true)
}
func keychainStorage() -> SSKKeychainStorage {
return SSKDefaultKeychainStorage.shared
}
func appDocumentDirectoryPath() -> String {
let targetPath: String? = FileManager.default
.urls(
for: .documentDirectory,
in: .userDomainMask
)
.last?
.path
owsAssertDebug(targetPath != nil)
return (targetPath ?? "")
}
func appSharedDataDirectoryPath() -> String {
let targetPath: String? = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup)?
.path
owsAssertDebug(targetPath != nil)
return (targetPath ?? "")
}
func appUserDefaults() -> UserDefaults {
let targetUserDefaults: UserDefaults? = UserDefaults(suiteName: SignalApplicationGroup)
owsAssertDebug(targetUserDefaults != nil)
return (targetUserDefaults ?? UserDefaults.standard)
}
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) {
OWSLogger.info("Ignoring request to show/hide status bar since we're in an app extension")
}
func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier {
return .invalid
}
func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {
owsAssertDebug(backgroundTaskIdentifier == .invalid)
}
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) {
OWSLogger.debug("Ignoring request to block sleep.")
}
func setMainAppBadgeNumber(_ value: Int) {
owsFailDebug("")
}
func setNetworkActivityIndicatorVisible(_ value: Bool) {
owsFailDebug("")
}
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) {
owsFailDebug("cannot run main app active blocks in share extension.")
}
}

View File

@ -1,12 +1,14 @@
import CoreServices
import PromiseKit
import SignalUtilitiesKit
import SessionUIKit
final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate {
private var areVersionMigrationsComplete = false
public static var attachmentPrepPromise: Promise<[SignalAttachment]>?
// MARK: Error
// MARK: - Error
enum ShareViewControllerError: Error {
case assertionError(description: String)
case unsupportedMedia
@ -14,7 +16,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
case obsoleteShare
}
// MARK: Lifecycle
// MARK: - Lifecycle
override func loadView() {
super.loadView()
@ -39,28 +42,35 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
return
}
AppSetup.setupEnvironment(appSpecificSingletonBlock: {
SSKEnvironment.shared.notificationsManager = NoopNotificationsManager()
}, migrationCompletion: { [weak self] in
AssertIsOnMainThread()
AppSetup.setupEnvironment(
appSpecificSingletonBlock: {
SSKEnvironment.shared.notificationsManager = NoopNotificationsManager()
},
migrationCompletion: { [weak self] in
AssertIsOnMainThread()
self?.versionMigrationsDidComplete()
guard let strongSelf = self else { return }
// performUpdateCheck must be invoked after Environment has been initialized because
// upgrade process may depend on Environment.
strongSelf.versionMigrationsDidComplete()
})
// performUpdateCheck must be invoked after Environment has been initialized because
// upgrade process may depend on Environment.
self?.versionMigrationsDidComplete()
}
)
// We don't need to use "screen protection" in the SAE.
NotificationCenter.default.addObserver(self,
selector: #selector(storageIsReady),
name: .StorageIsReady,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidEnterBackground),
name: .OWSApplicationDidEnterBackground,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(storageIsReady),
name: .StorageIsReady,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidEnterBackground),
name: .OWSApplicationDidEnterBackground,
object: nil
)
}
@objc
@ -88,12 +98,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
AssertIsOnMainThread()
// App isn't ready until storage is ready AND all version migrations are complete.
guard areVersionMigrationsComplete else {
return
}
guard OWSStorage.isStorageReady() else {
return
}
guard areVersionMigrationsComplete else { return }
guard OWSStorage.isStorageReady() else { return }
guard !AppReadiness.isAppReady() else {
// Only mark the app as ready once.
return
@ -108,9 +114,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
AppReadiness.setAppIsReady()
// We don't need to use messageFetcherJob in the SAE.
// We don't need to use SyncPushTokensJob in the SAE.
// We don't need to use DeviceSleepManager in the SAE.
AppVersion.sharedInstance().saeLaunchDidComplete()
@ -119,9 +123,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
// We don't need to use OWSMessageReceiver in the SAE.
// We don't need to use OWSBatchMessageProcessor in the SAE.
// We don't need to use OWSOrphanDataCleaner in the SAE.
// We don't need to fetch the local profile in the SAE
OWSReadReceiptManager.shared().prepareCachedValues()
@ -129,10 +131,10 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
override func viewDidLoad() {
super.viewDidLoad()
AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in
AssertIsOnMainThread()
guard let strongSelf = self else { return }
strongSelf.showLockScreenOrMainContent()
self?.showLockScreenOrMainContent()
}
}
@ -143,11 +145,9 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
Logger.info("")
if OWSScreenLock.shared.isScreenLockEnabled() {
self.dismiss(animated: false) { [weak self] in
AssertIsOnMainThread()
guard let strongSelf = self else { return }
strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
}
@ -161,12 +161,15 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
ExitShareExtension()
}
// MARK: App Mode
// MARK: - App Mode
public func getCurrentAppMode() -> AppMode {
guard let window = self.view.window else { return .light }
let userInterfaceStyle = window.traitCollection.userInterfaceStyle
let isLightMode = (userInterfaceStyle == .light || userInterfaceStyle == .unspecified)
return isLightMode ? .light : .dark
return (isLightMode ? .light : .dark)
}
public func setCurrentAppMode(to appMode: AppMode) {
@ -181,7 +184,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
private func showLockScreenOrMainContent() {
if OWSScreenLock.shared.isScreenLockEnabled() {
showLockScreen()
} else {
}
else {
showMainContent()
}
}
@ -192,16 +196,23 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
}
private func showMainContent() {
let threadPickerVC = ThreadPickerVC()
let threadPickerVC: ThreadPickerVC = ThreadPickerVC()
threadPickerVC.shareVC = self
setViewControllers([ threadPickerVC ], animated: false)
let promise = buildAttachments()
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, message: NSLocalizedString("vc_share_loading_message", comment: "")) { activityIndicator in
promise.done { _ in
activityIndicator.dismiss { }
}.catch { _ in
activityIndicator.dismiss { }
}
ModalActivityIndicatorViewController.present(
fromViewController: self,
canCancel: false,
message: "vc_share_loading_message".localized()) { activityIndicator in
promise
.done { _ in
activityIndicator.dismiss { }
}
.catch { _ in
activityIndicator.dismiss { }
}
}
ShareVC.attachmentPrepPromise = promise
}
@ -220,7 +231,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
func shareViewFailed(error: Error) {
let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in
self.extensionContext!.cancelRequest(withError: error)
}))
present(alert, animated: true, completion: nil)
@ -236,22 +247,29 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else {
return false
}
return firstUtiType == utiType
return (firstUtiType == utiType)
}
private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool {
return (itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) ||
itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String))
return (
itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) ||
itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String)
)
}
private class func isUrlItem(itemProvider: NSItemProvider) -> Bool {
return itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeURL as String)
return itemMatchesSpecificUtiType(
itemProvider: itemProvider,
utiType: kUTTypeURL as String
)
}
private class func isContactItem(itemProvider: NSItemProvider) -> Bool {
return itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeContact as String)
return itemMatchesSpecificUtiType(
itemProvider: itemProvider,
utiType: kUTTypeContact as String
)
}
private class func utiType(itemProvider: NSItemProvider) -> String? {
@ -259,7 +277,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
if isUrlItem(itemProvider: itemProvider) {
return kUTTypeURL as String
} else if isContactItem(itemProvider: itemProvider) {
}
else if isContactItem(itemProvider: itemProvider) {
return kUTTypeContact as String
}
@ -278,43 +297,43 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
// and send them as normal text messages if possible.
let urlString = url.absoluteString
return DataSourceValue.dataSource(withOversizeText: urlString)
} else if UTTypeConformsTo(utiType as CFString, kUTTypeText) {
}
else if UTTypeConformsTo(utiType as CFString, kUTTypeText) {
// Share text as oversize text messages.
//
// NOTE: SharingThreadPickerViewController will try to unpack them
// and send them as normal text messages if possible.
return DataSourcePath.dataSource(with: url,
shouldDeleteOnDeallocation: false)
} else {
guard let dataSource = DataSourcePath.dataSource(with: url,
shouldDeleteOnDeallocation: false) else {
return nil
}
if let customFileName = customFileName {
dataSource.sourceFilename = customFileName
} else {
// Ignore the filename for URLs.
dataSource.sourceFilename = url.lastPathComponent
}
return dataSource
return DataSourcePath.dataSource(
with: url,
shouldDeleteOnDeallocation: false
)
}
}
private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? {
guard let attachments = inputItem.attachments else {
guard let dataSource = DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false) else {
return nil
}
// Fallback to the last part of the URL
dataSource.sourceFilename = (customFileName ?? url.lastPathComponent)
return dataSource
}
private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? {
guard let attachments = inputItem.attachments else { return nil }
var visualMediaItemProviders = [NSItemProvider]()
var hasNonVisualMedia = false
for attachment in attachments {
if isVisualMediaItem(itemProvider: attachment) {
visualMediaItemProviders.append(attachment)
} else {
}
else {
hasNonVisualMedia = true
}
}
// Only allow multiple-attachment sends if all attachments
// are visual media.
if visualMediaItemProviders.count > 0 && !hasNonVisualMedia {
@ -334,6 +353,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
guard let itemProvider = attachment as? NSItemProvider else {
return false
}
return isUrlItem(itemProvider: itemProvider)
}) {
return [preferredAttachment]
@ -342,9 +362,11 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
// else return whatever is available
if let itemProvider = inputItem.attachments?.first {
return [itemProvider]
} else {
}
else {
owsFailDebug("Missing attachment.")
}
return []
}
@ -359,6 +381,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
Logger.error("invalid inputItem \(inputItemRaw)")
continue
}
if let itemProviders = ShareVC.preferredItemProviders(inputItem: inputItem) {
return Promise.value(itemProviders)
}
@ -366,6 +389,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error)
}
// MARK: - LoadedItem
private
struct LoadedItem {

View File

@ -4,26 +4,8 @@ import SessionUIKit
final class SimplifiedConversationCell : UITableViewCell {
var threadViewModel: ThreadViewModel! { didSet { update() } }
static let reuseIdentifier = "SimplifiedConversationCell"
// MARK: - Initialization
// MARK: UI Components
private lazy var accentLineView: UIView = {
let result = UIView()
result.backgroundColor = Colors.destructive
return result
}()
private lazy var profilePictureView = ProfilePictureView()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.textColor = Colors.text
result.lineBreakMode = .byTruncatingTail
return result
}()
// MARK: Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
@ -34,41 +16,90 @@ final class SimplifiedConversationCell : UITableViewCell {
setUpViewHierarchy()
}
// MARK: - UI
private lazy var stackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = Values.mediumSpacing
return stackView
}()
private lazy var accentLineView: UIView = {
let result = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.backgroundColor = Colors.destructive
return result
}()
private lazy var profilePictureView: ProfilePictureView = {
let view: ProfilePictureView = ProfilePictureView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.textColor = Colors.text
result.lineBreakMode = .byTruncatingTail
return result
}()
// MARK: - Initialization
private func setUpViewHierarchy() {
// Background color
backgroundColor = Colors.cellBackground
// Highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.backgroundColor = Colors.cellSelected
self.selectedBackgroundView = selectedBackgroundView
// Accent line view
addSubview(stackView)
stackView.addArrangedSubview(accentLineView)
stackView.addArrangedSubview(profilePictureView)
stackView.addArrangedSubview(displayNameLabel)
stackView.addArrangedSubview(UIView.hSpacer(0))
setupLayout()
}
// MARK: - Layout
private func setupLayout() {
accentLineView.set(.width, to: Values.accentLineThickness)
accentLineView.set(.height, to: 68)
// Profile picture view
let profilePictureViewSize = Values.mediumProfilePictureSize
profilePictureView.set(.width, to: profilePictureViewSize)
profilePictureView.set(.height, to: profilePictureViewSize)
profilePictureView.size = profilePictureViewSize
// Main stack view
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, displayNameLabel, UIView.hSpacer(0) ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = Values.mediumSpacing
addSubview(stackView)
stackView.pin(to: self)
}
// MARK: Updating
// MARK: - Content
private func update() {
AssertIsOnMainThread()
guard let thread = threadViewModel?.threadRecord else { return }
let isBlocked: Bool
if let thread = thread as? TSContactThread {
isBlocked = SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(thread.contactSessionID())
} else {
isBlocked = false
}
accentLineView.alpha = isBlocked ? 1 : 0
accentLineView.alpha = (isBlocked ? 1 : 0)
profilePictureView.update(for: thread)
displayNameLabel.text = getDisplayName()
}
@ -76,17 +107,25 @@ final class SimplifiedConversationCell : UITableViewCell {
private func getDisplayName() -> String {
if threadViewModel.isGroupThread {
if threadViewModel.name.isEmpty {
// TODO: Localization
return "Unknown Group"
} else {
return threadViewModel.name
}
} else {
if threadViewModel.threadRecord.isNoteToSelf() {
return NSLocalizedString("NOTE_TO_SELF", comment: "")
} else {
let hexEncodedPublicKey = threadViewModel.contactSessionID!
return Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey
}
return threadViewModel.name
}
if threadViewModel.threadRecord.isNoteToSelf() {
return "NOTE_TO_SELF".localized()
}
guard let hexEncodedPublicKey: String = threadViewModel.contactSessionID else {
// TODO: Localization
return "Unknown"
}
return (
Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ??
hexEncodedPublicKey
)
}
}

View File

@ -1,8 +1,12 @@
import UIKit
import SignalUtilitiesKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
private var threads: YapDatabaseViewMappings!
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var selectedThread: TSThread?
var shareVC: ShareVC?
@ -15,32 +19,50 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie
result.objectCacheLimit = 500
return result
}()
// MARK: - UI
private lazy var titleLabel: UILabel = {
let titleLabel: UILabel = UILabel()
titleLabel.text = "vc_share_title".localized()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
return titleLabel
}()
private lazy var tableView: UITableView = {
let result = UITableView()
result.backgroundColor = .clear
result.separatorStyle = .none
result.register(SimplifiedConversationCell.self, forCellReuseIdentifier: SimplifiedConversationCell.reuseIdentifier)
result.showsVerticalScrollIndicator = false
return result
let tableView: UITableView = UITableView()
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.register(view: SimplifiedConversationCell.self)
tableView.showsVerticalScrollIndicator = false
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
private lazy var fadeView: UIView = {
let result = UIView()
let view = UIView()
let gradient = Gradients.homeVCFade
result.setGradient(gradient)
result.isUserInteractionEnabled = false
return result
view.setGradient(gradient)
view.isUserInteractionEnabled = false
return view
}()
// MARK: Lifecycle
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupNavBar()
// Gradient
view.backgroundColor = .clear
let gradient = Gradients.defaultBackground
view.setGradient(gradient)
view.setGradient(Gradients.defaultBackground)
// Threads
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
@ -48,23 +70,16 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie
dbConnection.read { transaction in
self.threads.update(with: transaction) // Perform the initial update
}
// Title
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("vc_share_title", comment: "")
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
navigationItem.titleView = titleLabel
// Table view
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.pin(to: view)
view.addSubview(fadeView)
fadeView.pin(.leading, to: .leading, of: view)
let topInset = 0.15 * view.height()
fadeView.pin(.top, to: .top, of: view, withInset: topInset)
fadeView.pin(.trailing, to: .trailing, of: view)
fadeView.pin(.bottom, to: .bottom, of: view)
setupLayout()
// Reload
reload()
}
@ -80,18 +95,32 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie
}
}
// MARK: Layout
private func setupLayout() {
let topInset = 0.15 * view.height()
tableView.pin(to: view)
fadeView.pin(.leading, to: .leading, of: view)
fadeView.pin(.top, to: .top, of: view, withInset: topInset)
fadeView.pin(.trailing, to: .trailing, of: view)
fadeView.pin(.bottom, to: .bottom, of: view)
}
// MARK: Table View Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Int(threadCount)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: SimplifiedConversationCell.reuseIdentifier) as! SimplifiedConversationCell
let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath)
cell.threadViewModel = threadViewModel(at: indexPath.row)
return cell
}
// MARK: Updating
// MARK: - Updating
private func reload() {
AssertIsOnMainThread()
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
@ -102,46 +131,89 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie
tableView.reloadData()
}
// MARK: Interaction
// MARK: - Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { return }
self.selectedThread = thread
tableView.deselectRow(at: indexPath, animated: true)
guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else {
return
}
self.selectedThread = thread
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self)
navigationController!.present(approvalVC, animated: true, completion: nil)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
// Sharing a URL or plain text will populate the 'messageText' field so in those
// cases we should ignore the attachments
let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl)
let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText)
let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments)
let message = VisibleMessage()
message.sentTimestamp = NSDate.millisecondTimestamp()
message.text = messageText
message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ?
(
(messageText?.isEmpty == true ?
attachments[0].text() :
"\(attachments[0].text() ?? "")\n\n\(messageText ?? "")"
)
) :
messageText
)
let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!)
Storage.write { transaction in
tsMessage.save(with: transaction)
}
shareVC!.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: NSLocalizedString("vc_share_sending_message", comment: "")) { activityIndicator in
MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!).done { [weak self] _ in
guard let self = self else { return }
activityIndicator.dismiss { }
self.shareVC!.shareViewWasCompleted()
}.catch { [weak self] error in
guard let self = self else { return }
activityIndicator.dismiss { }
self.shareVC!.shareViewFailed(error: error)
Storage.write(
with: { transaction in
if isSharingUrl {
message.linkPreview = VisibleMessage.LinkPreview.from(
attachments[0].linkPreviewDraft,
using: transaction
)
}
else {
tsMessage.save(with: transaction)
}
},
completion: {
if isSharingUrl {
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
Storage.write { transaction in
tsMessage.save(with: transaction)
}
}
}
)
shareVC!.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
MessageSender.sendNonDurably(message, with: finalAttachments, in: self.selectedThread!)
.done { [weak self] _ in
activityIndicator.dismiss { }
self?.shareVC?.shareViewWasCompleted()
}
.catch { [weak self] error in
activityIndicator.dismiss { }
self?.shareVC?.shareViewFailed(error: error)
}
}
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
// Do nothing
dismiss(animated: true, completion: nil)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
// Do nothing
}
// MARK: Convenience
// MARK: - Convenience
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
@ -153,9 +225,11 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie
private func threadViewModel(at index: Int) -> ThreadViewModel? {
guard let thread = thread(at: index) else { return nil }
if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] {
return cachedThreadViewModel
} else {
}
else {
var threadViewModel: ThreadViewModel? = nil
dbConnection.read { transaction in
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)

View File

@ -304,9 +304,10 @@ public final class SnodeAPI : NSObject {
let onsName = onsName.lowercased()
// Hash the ONS name using BLAKE2b
let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!)
guard let nameHash = sodium.genericHash.hash(message: nameAsData),
let base64EncodedNameHash = nameHash.toBase64() else { return Promise(error: Error.hashingFailed) }
guard let nameHash = sodium.genericHash.hash(message: nameAsData) else { return Promise(error: Error.hashingFailed) }
// Ask 3 different snodes for the Session ID associated with the given name hash
let base64EncodedNameHash = nameHash.toBase64()
let parameters: [String:Any] = [
"endpoint" : "ons_resolve",
"params" : [
@ -473,7 +474,7 @@ public final class SnodeAPI : NSObject {
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"messages": serverHashes,
"signature": signature.toBase64()!
"signature": signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in
@ -520,7 +521,7 @@ public final class SnodeAPI : NSObject {
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"timestamp" : timestamp,
"signature" : signature.toBase64()!
"signature" : signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String:Bool] in

View File

@ -29,16 +29,8 @@ public final class SearchBar : UISearchBar {
searchTextField.backgroundColor = Colors.searchBarBackground // The search bar background color
searchTextField.textColor = Colors.text
searchTextField.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("Search", comment: ""), attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
searchTextField.keyboardAppearance = .dark
setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: UISearchBar.Icon.search)
searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: UISearchBar.Icon.clear)
searchTextField.removeConstraints(searchTextField.constraints)
searchTextField.pin(.leading, to: .leading, of: searchTextField.superview!, withInset: Values.mediumSpacing + 3)
searchTextField.pin(.top, to: .top, of: searchTextField.superview!, withInset: 10)
searchTextField.superview!.pin(.trailing, to: .trailing, of: searchTextField, withInset: Values.mediumSpacing + 3)
searchTextField.superview!.pin(.bottom, to: .bottom, of: searchTextField, withInset: 10)
searchTextField.set(.height, to: Values.searchBarHeight)
searchTextField.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
}
}

View File

@ -13,6 +13,7 @@ import UIKit
@objc(LKColors)
public final class Colors : NSObject {
@objc public static var grey: UIColor { UIColor(named: "session_grey")! }
@objc public static var accent: UIColor { UIColor(named: "session_accent")! }
@objc public static var text: UIColor { UIColor(named: "session_text")! }
@objc public static var destructive: UIColor { UIColor(named: "session_destructive")! }
@ -43,4 +44,5 @@ public final class Colors : NSObject {
@objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! }
@objc public static var callMessageBackground: UIColor { UIColor(named: "session_call_message_background")! }
@objc public static var pinIcon: UIColor { UIColor(named: "session_pin_icon")! }
@objc public static var sessionHeading: UIColor { UIColor(named: "session_heading")! }
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFC",
"green" : "0xFC",
"red" : "0xFC"
"blue" : "252",
"green" : "252",
"red" : "252"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1B",
"green" : "0x1B",
"red" : "0x1B"
"blue" : "22",
"green" : "22",
"red" : "22"
}
},
"idiom" : "universal"

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "240",
"green" : "240",
"red" : "240"
"blue" : "247",
"green" : "247",
"red" : "247"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x40",
"green" : "0x40",
"red" : "0x40"
"blue" : "28",
"green" : "28",
"red" : "28"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x30",
"green" : "0x2F",
"red" : "0x31"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x82",
"green" : "0xF7",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFC",
"green" : "0xFC",
"red" : "0xFC"
"blue" : "252",
"green" : "252",
"red" : "252"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x16",
"green" : "0x16",
"red" : "0x16"
"blue" : "22",
"green" : "22",
"red" : "22"
}
},
"idiom" : "universal"

View File

@ -95,4 +95,17 @@ public extension UIView {
constraint.isActive = true
return constraint
}
@discardableResult
func set(_ dimension: Dimension, greaterThanOrEqualTo size: CGFloat) -> NSLayoutConstraint {
translatesAutoresizingMaskIntoConstraints = false
let constraint: NSLayoutConstraint = {
switch dimension {
case .width: return widthAnchor.constraint(greaterThanOrEqualToConstant: size)
case .height: return heightAnchor.constraint(greaterThanOrEqualToConstant: size)
}
}()
constraint.isActive = true
return constraint
}
}

View File

@ -35,6 +35,7 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
@property (nonatomic, readonly) BOOL isMainApp;
@property (nonatomic, readonly) BOOL isMainAppAndActive;
@property (nonatomic, readonly) BOOL isShareExtension;
/// Whether the app was woken up by a silent push notification. This is important for determining whether attachments should be downloaded or not.
@property (nonatomic) BOOL wasWokenUpByPushNotification;

View File

@ -0,0 +1,16 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
public protocol ReusableView: AnyObject {
static var defaultReuseIdentifier: String { get }
}
public extension ReusableView where Self: UIView {
static var defaultReuseIdentifier: String {
return String(describing: self.self)
}
}
extension UITableViewCell: ReusableView {}
extension UITableViewHeaderFooterView: ReusableView {}

View File

@ -0,0 +1,13 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import SignalCoreKit
public extension String {
func localized() -> String {
// If the localized string matches the key provided then the localisation failed
let localizedString = NSLocalizedString(self, comment: "")
owsAssertDebug(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings")
return localizedString
}
}

View File

@ -0,0 +1,23 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
public extension UITableView {
func register<View>(view: View.Type) where View: UITableViewCell {
register(view.self, forCellReuseIdentifier: view.defaultReuseIdentifier)
}
func registerHeaderFooterView<View>(view: View.Type) where View: UITableViewHeaderFooterView {
register(view.self, forHeaderFooterViewReuseIdentifier: view.defaultReuseIdentifier)
}
func dequeue<T>(type: T.Type, for indexPath: IndexPath) -> T where T: UITableViewCell {
let reuseIdentifier = T.defaultReuseIdentifier
return dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T
}
func dequeueHeaderFooterView<T>(type: T.Type) -> T where T: UITableViewHeaderFooterView {
let reuseIdentifier = T.defaultReuseIdentifier
return dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! T
}
}

View File

@ -594,7 +594,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
removeAssetRequestFromQueue(assetRequest: assetRequest)
return
}
guard CurrentAppContext().isMainAppAndActive else {
guard CurrentAppContext().isMainAppAndActive || CurrentAppContext().isShareExtension else {
// If app is not active, fail the asset request.
assetRequest.state = .failed
assetRequestDidFail(assetRequest: assetRequest)

View File

@ -191,6 +191,7 @@ extension AttachmentApprovalInputAccessoryView: AttachmentCaptionToolbarDelegate
return
}
// TODO: Look at refactoring this behaviour to consolidate attachment mutations
currentAttachmentItem.attachment.captionText = attachmentCaptionToolbar.textView.text
delegate?.attachmentApprovalInputUpdateMediaRail()

View File

@ -10,17 +10,25 @@ import SessionUIKit
import CoreServices
@objc
public protocol AttachmentApprovalViewControllerDelegate: class {
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment], messageText: String?)
public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
func attachmentApproval(
_ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
messageText: String?
)
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController)
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController,
didChangeMessageText newMessageText: String?)
func attachmentApproval(
_ attachmentApproval: AttachmentApprovalViewController,
didChangeMessageText newMessageText: String?
)
@objc
optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment)
optional func attachmentApproval(
_ attachmentApproval: AttachmentApprovalViewController,
didRemoveAttachment attachment: SignalAttachment
)
@objc
optional func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController)
@ -38,19 +46,72 @@ public enum AttachmentApprovalViewControllerMode: UInt {
@objc
public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
@objc public enum Mode: UInt {
case modal
case sharedNavigation
}
// MARK: - Properties
private let mode: AttachmentApprovalViewControllerMode
private let mode: Mode
private let isAddMoreVisible: Bool
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
public var isEditingCaptions = false {
didSet {
updateContents()
didSet { updateContents() }
}
let attachmentItemCollection: AttachmentItemCollection
var attachmentItems: [SignalAttachmentItem] {
return attachmentItemCollection.attachmentItems
}
var attachments: [SignalAttachment] {
return attachmentItems.map { (attachmentItem) in
autoreleasepool {
return self.processedAttachment(forAttachmentItem: attachmentItem)
}
}
}
public var pageViewControllers: [AttachmentPrepViewController]? {
return viewControllers?.compactMap { $0 as? AttachmentPrepViewController }
}
public var currentPageViewController: AttachmentPrepViewController? {
return pageViewControllers?.first
}
var currentItem: SignalAttachmentItem? {
get { return currentPageViewController?.attachmentItem }
set { setCurrentItem(newValue, direction: .forward, animated: false) }
}
private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:]
public var shouldHideControls: Bool {
guard let pageViewController: AttachmentPrepViewController = pageViewControllers?.first else {
return false
}
return pageViewController.shouldHideControls
}
override public var inputAccessoryView: UIView? {
bottomToolView.layoutIfNeeded()
return bottomToolView
}
override public var canBecomeFirstResponder: Bool {
return !shouldHideControls
}
public var messageText: String? {
get { return bottomToolView.attachmentTextToolbar.messageText }
set { bottomToolView.attachmentTextToolbar.messageText = newValue }
}
// MARK: - Initializers
@ -59,29 +120,34 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
notImplemented()
}
let kSpacingBetweenItems: CGFloat = 20
@objc
required public init(mode: AttachmentApprovalViewControllerMode,
attachments: [SignalAttachment]) {
required public init(
mode: Mode,
attachments: [SignalAttachment]
) {
assert(attachments.count > 0)
self.mode = mode
let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
self.isAddMoreVisible = mode == .sharedNavigation
self.isAddMoreVisible = (mode == .sharedNavigation)
self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems, isAddMoreVisible: isAddMoreVisible)
let options: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
super.init(transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: options)
super.init(
transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: [
.interPageSpacing: kSpacingBetweenItems
]
)
self.dataSource = self
self.delegate = self
NotificationCenter.default.addObserver(self,
selector: #selector(didBecomeActive),
name: NSNotification.Name.OWSApplicationDidBecomeActive,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(didBecomeActive),
name: .OWSApplicationDidBecomeActive,
object: nil
)
}
deinit {
@ -89,62 +155,78 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
@objc
public class func wrappedInNavController(attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController {
public class func wrappedInNavController(
attachments: [SignalAttachment],
approvalDelegate: AttachmentApprovalViewControllerDelegate
) -> OWSNavigationController {
let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments)
vc.approvalDelegate = approvalDelegate
let navController = OWSNavigationController(rootViewController: vc)
navController.ows_prefersStatusBarHidden = true
return navController
}
// MARK: - Notifications
@objc func didBecomeActive() {
AssertIsOnMainThread()
updateContents()
}
// MARK: - Subviews
var galleryRailView: GalleryRailView {
return bottomToolView.galleryRailView
}
var attachmentTextToolbar: AttachmentTextToolbar {
return bottomToolView.attachmentTextToolbar
}
lazy var bottomToolView: AttachmentApprovalInputAccessoryView = {
// MARK: - UI
private let kSpacingBetweenItems: CGFloat = 20
public override var prefersStatusBarHidden: Bool { return true }
private lazy var bottomToolView: AttachmentApprovalInputAccessoryView = {
let bottomToolView = AttachmentApprovalInputAccessoryView()
bottomToolView.delegate = self
bottomToolView.attachmentTextToolbar.attachmentTextToolbarDelegate = self
bottomToolView.galleryRailView.delegate = self
return bottomToolView
}()
lazy var touchInterceptorView = UIView()
private var galleryRailView: GalleryRailView { return bottomToolView.galleryRailView }
// MARK: - View Lifecycle
private lazy var touchInterceptorView: UIView = {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
let tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(didTapTouchInterceptorView(gesture:))
)
view.addGestureRecognizer(tapGesture)
return view
}()
public override var prefersStatusBarHidden: Bool {
return true
}
private lazy var pagerScrollView: UIScrollView? = {
// This is kind of a hack. Since we don't have first class access to the superview's `scrollView`
// we traverse the view hierarchy until we find it.
let pagerScrollView = view.subviews.first { $0 is UIScrollView } as? UIScrollView
assert(pagerScrollView != nil)
return pagerScrollView
}()
// MARK: - Lifecycle
override public func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = Colors.navigationBarBackground
// avoid an unpleasant "bounce" which doesn't make sense in the context of a single item.
pagerScrollView?.isScrollEnabled = attachmentItems.count > 1
// Bottom Toolbar
galleryRailView.delegate = self
attachmentTextToolbar.attachmentTextToolbarDelegate = self
// Navigation
let backgroundImage: UIImage = UIImage(color: Colors.navigationBarBackground)
self.navigationItem.title = nil
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.barTintColor = Colors.navigationBarBackground
(self.navigationController?.navigationBar as? OWSNavigationBar)?.respectsTheme = true
self.navigationController?.navigationBar.backgroundColor = Colors.navigationBarBackground
self.navigationController?.navigationBar.setBackgroundImage(backgroundImage, for: .default)
// Avoid an unpleasant "bounce" which doesn't make sense in the context of a single item.
pagerScrollView?.isScrollEnabled = (attachmentItems.count > 1)
guard let firstItem = attachmentItems.first else {
owsFailDebug("firstItem was unexpectedly nil")
@ -152,36 +234,27 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
self.setCurrentItem(firstItem, direction: .forward, animated: false)
view.addSubview(touchInterceptorView)
// layout immediately to avoid animating the layout process during the transition
self.currentPageViewController.view.layoutIfNeeded()
UIView.performWithoutAnimation {
self.currentPageViewController?.view.layoutIfNeeded()
}
// If the first item is just text, or is a URL and LinkPreviews are disabled
// then just fill the 'message' box with it
if firstItem.attachment.isText || (firstItem.attachment.isUrl && OWSLinkPreview.previewURL(forRawBodyText: firstItem.attachment.text()) == nil) {
bottomToolView.attachmentTextToolbar.messageText = firstItem.attachment.text()
}
view.addSubview(touchInterceptorView)
touchInterceptorView.autoPinEdgesToSuperviewEdges()
touchInterceptorView.isHidden = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTouchInterceptorView(gesture:)))
touchInterceptorView.addGestureRecognizer(tapGesture)
setupLayout()
}
override public func viewWillAppear(_ animated: Bool) {
Logger.debug("")
super.viewWillAppear(animated)
guard let navigationBar = navigationController?.navigationBar as? OWSNavigationBar else {
owsFailDebug("navigationBar was nil or unexpected class")
return
}
// Loki: Set navigation bar background color
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
navigationBar.respectsTheme = true
navigationBar.backgroundColor = Colors.navigationBarBackground
let backgroundImage = UIImage(color: Colors.navigationBarBackground)
navigationBar.setBackgroundImage(backgroundImage, for: .default)
updateContents()
}
@ -197,7 +270,23 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
Logger.debug("")
super.viewWillDisappear(animated)
}
// MARK: - Layout
private func setupLayout() {
touchInterceptorView.autoPinEdgesToSuperviewEdges()
}
// MARK: - Notifications
@objc func didBecomeActive() {
AssertIsOnMainThread()
updateContents()
}
// MARK: - Contents
private func updateContents() {
updateNavigationBar()
updateInputAccessory()
@ -207,40 +296,26 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
// MARK: - Input Accessory
override public var inputAccessoryView: UIView? {
bottomToolView.layoutIfNeeded()
return bottomToolView
}
override public var canBecomeFirstResponder: Bool {
return !shouldHideControls
}
public func updateInputAccessory() {
var currentPageViewController: AttachmentPrepViewController?
if pageViewControllers.count == 1 {
currentPageViewController = pageViewControllers.first
if pageViewControllers?.count == 1 {
currentPageViewController = pageViewControllers?.first
}
let currentAttachmentItem: SignalAttachmentItem? = currentPageViewController?.attachmentItem
let hasPresentedView = self.presentedViewController != nil
let hasPresentedView = (self.presentedViewController != nil)
let isToolbarFirstResponder = bottomToolView.hasFirstResponder
if !shouldHideControls, !isFirstResponder, !hasPresentedView, !isToolbarFirstResponder {
becomeFirstResponder()
}
bottomToolView.update(isEditingCaptions: isEditingCaptions,
currentAttachmentItem: currentAttachmentItem,
shouldHideControls: shouldHideControls)
}
public var messageText: String? {
get {
return attachmentTextToolbar.messageText
}
set {
attachmentTextToolbar.messageText = newValue
}
bottomToolView.update(
isEditingCaptions: isEditingCaptions,
currentAttachmentItem: currentAttachmentItem,
shouldHideControls: shouldHideControls
)
}
// MARK: - Navigation Bar
@ -254,10 +329,18 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
guard !isEditingCaptions else {
// Hide all navigation bar items while the caption view is open.
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("ATTACHMENT_APPROVAL_CAPTION_TITLE", comment: "Title for 'caption' mode of the attachment approval view."), style: .plain, target: nil, action: nil)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(
//"Title for 'caption' mode of the attachment approval view."
title: "ATTACHMENT_APPROVAL_CAPTION_TITLE".localized(),
style: .plain,
target: nil,
action: nil
)
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapCaptionDone(sender:)))
let doneButton = navigationBarButton(
imageName: "image_editor_checkmark_full",
selector: #selector(didTapCaptionDone(sender:))
)
let navigationBarItems = [doneButton]
updateNavigationBar(navigationBarItems: navigationBarItems)
return
@ -265,29 +348,23 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
var navigationBarItems = [UIView]()
if let viewControllers = viewControllers,
viewControllers.count == 1,
let firstViewController = viewControllers.first as? AttachmentPrepViewController {
if viewControllers?.count == 1, let firstViewController: AttachmentPrepViewController = viewControllers?.first as? AttachmentPrepViewController {
navigationBarItems = firstViewController.navigationBarItems()
// Show the caption UI if there's more than one attachment
// OR if the attachment already has a caption.
let attachmentCount = attachmentItemCollection.count
var shouldShowCaptionUI = attachmentCount > 0
if let captionText = firstViewController.attachmentItem.captionText, captionText.count > 0 {
shouldShowCaptionUI = true
}
if shouldShowCaptionUI {
let captionButton = navigationBarButton(imageName: "image_editor_caption",
selector: #selector(didTapCaption(sender:)))
if attachmentItemCollection.count > 0, (firstViewController.attachmentItem.captionText?.count ?? 0) > 0 {
let captionButton = navigationBarButton(
imageName: "image_editor_caption",
selector: #selector(didTapCaption(sender:))
)
navigationBarItems.append(captionButton)
}
}
updateNavigationBar(navigationBarItems: navigationBarItems)
let hasCancel = (mode != .sharedNavigation)
if hasCancel {
if mode != .sharedNavigation {
// Mimic a UIBarButtonItem of type .cancel, but with a shadow.
let cancelButton = OWSButton(title: CommonStrings.cancelButton) { [weak self] in
self?.cancelPressed()
@ -300,10 +377,11 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
cancelButton.sizeToFit()
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton)
} else {
}
else {
// Mimic a conventional back button, but with a shadow.
let isRTL = CurrentAppContext().isRTL
let imageName = isRTL ? "NavBarBackRTL" : "NavBarBack"
let imageName = (isRTL ? "NavBarBackRTL" : "NavBarBack")
let backButton = OWSButton(imageName: imageName, tintColor: Colors.text) { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
@ -328,40 +406,44 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
// Default back button is 1.5 pixel lower than our extracted image.
let kTopInsetPadding: CGFloat = 1.5
backButton.imageEdgeInsets = UIEdgeInsets(top: kTopInsetPadding, left: kExtraLeftPadding, bottom: 0, right: 0)
backButton.imageEdgeInsets = UIEdgeInsets(
top: kTopInsetPadding,
left: kExtraLeftPadding,
bottom: 0,
right: 0
)
var backImageSize = CGSize.zero
if let backImage = UIImage(named: imageName) {
backImageSize = backImage.size
} else {
}
else {
owsFailDebug("Missing backImage.")
}
backButton.frame = CGRect(origin: .zero, size: CGSize(width: backImageSize.width + kExtraRightPadding,
height: backImageSize.height + kExtraHeightPadding))
backButton.frame = CGRect(
origin: .zero,
size: CGSize(
width: backImageSize.width + kExtraRightPadding,
height: backImageSize.height + kExtraHeightPadding
)
)
// Note: using a custom leftBarButtonItem breaks the interactive pop gesture.
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
}
}
// MARK: - Control Visibility
public var shouldHideControls: Bool {
guard let pageViewController = pageViewControllers.first else {
return false
}
return pageViewController.shouldHideControls
}
// MARK: - View Helpers
func remove(attachmentItem: SignalAttachmentItem) {
if attachmentItem == currentItem {
if let nextItem = attachmentItemCollection.itemAfter(item: attachmentItem) {
setCurrentItem(nextItem, direction: .forward, animated: true)
} else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) {
}
else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) {
setCurrentItem(prevItem, direction: .reverse, animated: true)
} else {
}
else {
owsFailDebug("removing last item shouldn't be possible because rail should not be visible")
return
}
@ -372,30 +454,27 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return
}
UIView.animate(withDuration: 0.2,
animations: {
// shrink stack view item until it disappears
cell.isHidden = true
// simultaneously fade out
cell.alpha = 0
},
completion: { _ in
self.attachmentItemCollection.remove(item: attachmentItem)
self.approvalDelegate?.attachmentApproval?(self, didRemoveAttachment: attachmentItem.attachment)
self.updateMediaRail()
})
UIView.animate(
withDuration: 0.2,
animations: {
// shrink stack view item until it disappears
cell.isHidden = true
// simultaneously fade out
cell.alpha = 0
},
completion: { [weak self] _ in
self?.attachmentItemCollection.remove(item: attachmentItem)
if let strongSelf: AttachmentApprovalViewController = self {
self?.approvalDelegate?.attachmentApproval?(strongSelf, didRemoveAttachment: attachmentItem.attachment)
}
self?.updateMediaRail()
}
)
}
lazy var pagerScrollView: UIScrollView? = {
// This is kind of a hack. Since we don't have first class access to the superview's `scrollView`
// we traverse the view hierarchy until we find it.
let pagerScrollView = view.subviews.first { $0 is UIScrollView } as? UIScrollView
assert(pagerScrollView != nil)
return pagerScrollView
}()
// MARK: - UIPageViewControllerDelegate
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
@ -440,10 +519,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
let currentItem = currentViewController.attachmentItem
guard let previousItem = attachmentItem(before: currentItem) else {
return nil
}
guard let previousItem = attachmentItem(before: currentItem) else { return nil }
guard let previousPage: AttachmentPrepViewController = buildPage(item: previousItem) else {
return nil
}
@ -460,10 +536,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
let currentItem = currentViewController.attachmentItem
guard let nextItem = attachmentItem(after: currentItem) else {
return nil
}
guard let nextItem = attachmentItem(after: currentItem) else { return nil }
guard let nextPage: AttachmentPrepViewController = buildPage(item: nextItem) else {
return nil
}
@ -471,38 +544,19 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return nextPage
}
public var currentPageViewController: AttachmentPrepViewController {
return pageViewControllers.first!
}
public var pageViewControllers: [AttachmentPrepViewController] {
return super.viewControllers!.map { $0 as! AttachmentPrepViewController }
}
@objc
public override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewController.NavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {
super.setViewControllers(viewControllers,
direction: direction,
animated: animated) { [weak self] (finished) in
if let completion = completion {
completion(finished)
}
self?.updateContents()
super.setViewControllers(
viewControllers,
direction: direction,
animated: animated
) { [weak self] finished in
completion?(finished)
self?.updateContents()
}
}
var currentItem: SignalAttachmentItem! {
get {
return currentPageViewController.attachmentItem
}
set {
setCurrentItem(newValue, direction: .forward, animated: false)
}
}
private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:]
private func buildPage(item: SignalAttachmentItem) -> AttachmentPrepViewController? {
if let cachedPage = cachedPages[item] {
Logger.debug("cache hit.")
return cachedPage
@ -516,8 +570,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return viewController
}
private func setCurrentItem(_ item: SignalAttachmentItem, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) {
guard let page = self.buildPage(item: item) else {
private func setCurrentItem(_ item: SignalAttachmentItem?, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) {
guard let item: SignalAttachmentItem = item, let page = self.buildPage(item: item) else {
owsFailDebug("unexpectedly unable to build new page")
return
}
@ -536,42 +590,34 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
let cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView = { [weak self] railItem in
switch railItem {
case is AddMoreRailItem:
return GalleryRailCellView()
case is SignalAttachmentItem:
let cell = ApprovalRailCellView()
cell.approvalRailCellDelegate = self
return cell
default:
owsFailDebug("unexpted rail item type: \(railItem)")
return GalleryRailCellView()
case is AddMoreRailItem:
return GalleryRailCellView()
case is SignalAttachmentItem:
let cell = ApprovalRailCellView()
cell.approvalRailCellDelegate = self
return cell
default:
owsFailDebug("unexpted rail item type: \(railItem)")
return GalleryRailCellView()
}
}
galleryRailView.configureCellViews(itemProvider: attachmentItemCollection,
focusedItem: currentItem,
cellViewBuilder: cellViewBuilder)
galleryRailView.configureCellViews(
itemProvider: attachmentItemCollection,
focusedItem: currentItem,
cellViewBuilder: cellViewBuilder
)
if isAddMoreVisible {
galleryRailView.isHidden = false
} else if attachmentItemCollection.attachmentItems.count > 1 {
galleryRailView.isHidden = false
} else {
galleryRailView.isHidden = true
}
}
let attachmentItemCollection: AttachmentItemCollection
var attachmentItems: [SignalAttachmentItem] {
return attachmentItemCollection.attachmentItems
}
var attachments: [SignalAttachment] {
return attachmentItems.map { (attachmentItem) in
autoreleasepool {
return self.processedAttachment(forAttachmentItem: attachmentItem)
}
else if attachmentItemCollection.attachmentItems.count > 1 {
galleryRailView.isHidden = false
}
else {
galleryRailView.isHidden = true
}
}
@ -596,18 +642,24 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return attachmentItem.attachment
}
var dataUTI = kUTTypeImage as String
guard let dstData: Data = {
let isLossy: Bool = attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame
let maybeDstData: Data? = {
let isLossy: Bool = (
attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame
)
if isLossy {
dataUTI = kUTTypeJPEG as String
return dstImage.jpegData(compressionQuality: 0.9)
} else {
}
else {
dataUTI = kUTTypePNG as String
return dstImage.pngData()
}
}() else {
owsFailDebug("Could not export for output.")
return attachmentItem.attachment
}()
guard let dstData: Data = maybeDstData else {
owsFailDebug("Could not export for output.")
return attachmentItem.attachment
}
guard let dataSource = DataSourceValue.dataSource(with: dstData, utiType: dataUTI) else {
owsFailDebug("Could not prepare data source for output.")
@ -693,18 +745,18 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
func attachmentTextToolbarDidBeginEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {
currentPageViewController.setAttachmentViewScale(.compact, animated: true)
currentPageViewController?.setAttachmentViewScale(.compact, animated: true)
}
func attachmentTextToolbarDidEndEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {
currentPageViewController.setAttachmentViewScale(.fullsize, animated: true)
currentPageViewController?.setAttachmentViewScale(.fullsize, animated: true)
}
func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) {
// Toolbar flickers in and out if there are errors
// and remains visible momentarily after share extension is dismissed.
// It's easiest to just hide it at this point since we're done with it.
currentPageViewController.shouldAllowAttachmentViewResizing = false
currentPageViewController?.shouldAllowAttachmentViewResizing = false
attachmentTextToolbar.isUserInteractionEnabled = false
attachmentTextToolbar.isHidden = true
@ -769,7 +821,7 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate {
return
}
guard let currentIndex = attachmentItems.firstIndex(of: currentItem) else {
guard let currentItem: SignalAttachmentItem = currentItem, let currentIndex = attachmentItems.firstIndex(of: currentItem) else {
owsFailDebug("currentIndex was unexpectedly nil")
return
}
@ -779,7 +831,7 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate {
return
}
let direction: UIPageViewController.NavigationDirection = currentIndex < targetIndex ? .forward : .reverse
let direction: UIPageViewController.NavigationDirection = (currentIndex < targetIndex ? .forward : .reverse)
self.setCurrentItem(targetItem, direction: direction, animated: true)
}
@ -787,12 +839,6 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate {
// MARK: -
enum KeyboardScenario {
case hidden, editingMessage, editingCaption
}
// MARK: -
extension AttachmentApprovalViewController: ApprovalRailCellViewDelegate {
func approvalRailCellView(_ approvalRailCellView: ApprovalRailCellView, didRemoveItem attachmentItem: SignalAttachmentItem) {
remove(attachmentItem: attachmentItem)

View File

@ -7,7 +7,7 @@ import UIKit
import AVFoundation
import SessionUIKit
protocol AttachmentPrepViewControllerDelegate: class {
protocol AttachmentPrepViewControllerDelegate: AnyObject {
func prepViewControllerUpdateNavigationBar()
func prepViewControllerUpdateControls()
@ -15,7 +15,7 @@ protocol AttachmentPrepViewControllerDelegate: class {
// MARK: -
public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate {
public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate, MediaMessageViewAudioDelegate {
// We sometimes shrink the attachment view so that it remains somewhat visible
// when the keyboard is presented.
public enum AttachmentViewScale {
@ -31,13 +31,97 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
return attachmentItem.attachment
}
private var videoPlayer: OWSVideoPlayer?
private lazy var videoPlayer: OWSVideoPlayer? = {
guard let videoURL = attachment.dataUrl else {
owsFailDebug("Missing videoURL")
return nil
}
private(set) var mediaMessageView: MediaMessageView!
private(set) var scrollView: UIScrollView!
private(set) var contentContainer: UIView!
private(set) var playVideoButton: UIView?
private var imageEditorView: ImageEditorView?
let player: OWSVideoPlayer = OWSVideoPlayer(url: videoURL)
player.delegate = self
return player
}()
// MARK: - UI
fileprivate static let verticalCenterOffset: CGFloat = (
AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)
)
private lazy var scrollView: UIScrollView = {
// Scroll View - used to zoom/pan on images and video
let scrollView: UIScrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
// Panning should stop pretty soon after the user stops scrolling
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
return scrollView
}()
private lazy var contentContainerView: UIView = {
// Anything that should be shrunk when user pops keyboard lives in the contentContainer.
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var mediaMessageView: MediaMessageView = {
let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval)
view.translatesAutoresizingMaskIntoConstraints = false
view.audioDelegate = self
view.isHidden = (imageEditorView != nil)
return view
}()
private lazy var imageEditorView: ImageEditorView? = {
guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil }
let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
view.translatesAutoresizingMaskIntoConstraints = false
guard view.configureSubviews() else { return nil }
return view
}()
private lazy var videoPlayerView: VideoPlayerView? = {
guard let videoPlayer: OWSVideoPlayer = videoPlayer else { return nil }
let view: VideoPlayerView = VideoPlayerView()
view.translatesAutoresizingMaskIntoConstraints = false
view.player = videoPlayer.avPlayer
let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
view.addGestureRecognizer(pauseGesture)
return view
}()
private lazy var progressBar: PlayerProgressBar = {
let progressBar: PlayerProgressBar = PlayerProgressBar()
progressBar.translatesAutoresizingMaskIntoConstraints = false
progressBar.player = videoPlayer?.avPlayer
progressBar.delegate = self
return progressBar
}()
private lazy var playVideoButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.contentMode = .scaleAspectFit
button.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
return button
}()
public var shouldHideControls: Bool {
guard let imageEditorView = imageEditorView else {
@ -61,143 +145,141 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
}
// MARK: - View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.navigationBarBackground
override public func loadView() {
self.view = UIView()
self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval)
// Anything that should be shrunk when user pops keyboard lives in the contentContainer.
let contentContainer = UIView()
self.contentContainer = contentContainer
view.addSubview(contentContainer)
contentContainer.autoPinEdgesToSuperviewEdges()
// Scroll View - used to zoom/pan on images and video
scrollView = UIScrollView()
contentContainer.addSubview(scrollView)
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
// Panning should stop pretty soon after the user stops scrolling
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
// We want scroll view content up and behind the system status bar content
// but we want other content (e.g. bar buttons) to respect the top layout guide.
self.automaticallyAdjustsScrollViewInsets = false
scrollView.autoPinEdgesToSuperviewEdges()
let backgroundColor = Colors.navigationBarBackground
self.view.backgroundColor = backgroundColor
// Create full screen container view so the scrollView
// can compute an appropriate content size in which to center
// our media view.
let containerView = UIView.container()
scrollView.addSubview(containerView)
containerView.autoPinEdgesToSuperviewEdges()
containerView.autoMatch(.height, to: .height, of: self.view)
containerView.autoMatch(.width, to: .width, of: self.view)
containerView.addSubview(mediaMessageView)
mediaMessageView.autoPinEdgesToSuperviewEdges()
if let imageEditorModel = attachmentItem.imageEditorModel {
let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
if imageEditorView.configureSubviews() {
self.imageEditorView = imageEditorView
mediaMessageView.isHidden = true
view.addSubview(imageEditorView)
imageEditorView.autoPinEdgesToSuperviewEdges()
imageEditorUpdateNavigationBar()
}
view.addSubview(contentContainerView)
contentContainerView.addSubview(scrollView)
scrollView.addSubview(mediaMessageView)
if attachment.isImage, let editorView: ImageEditorView = imageEditorView {
view.addSubview(editorView)
imageEditorUpdateNavigationBar()
}
// Hide the play button embedded in the MediaView and replace it with our own.
// This allows us to zoom in on the media view without zooming in on the button
if attachment.isVideo {
guard let videoURL = attachment.dataUrl else {
owsFailDebug("Missing videoURL")
return
}
let player = OWSVideoPlayer(url: videoURL)
self.videoPlayer = player
player.delegate = self
let playerView = VideoPlayerView()
playerView.player = player.avPlayer
self.mediaMessageView.addSubview(playerView)
playerView.autoPinEdgesToSuperviewEdges()
let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
playerView.addGestureRecognizer(pauseGesture)
let progressBar = PlayerProgressBar()
progressBar.player = player.avPlayer
progressBar.delegate = self
// we don't want the progress bar to zoom during "pinch-to-zoom"
// TODO: This for both Audio and Video?
if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
mediaMessageView.videoPlayButton.isHidden = true
mediaMessageView.addSubview(playerView)
// We don't want the progress bar to zoom during "pinch-to-zoom"
// but we do want it to shrink with the media content when the user
// pops the keyboard.
contentContainer.addSubview(progressBar)
progressBar.autoPinEdge(.top, to: .top, of: view)
progressBar.autoPinWidthToSuperview()
progressBar.autoSetDimension(.height, toSize: 44)
self.mediaMessageView.videoPlayButton?.isHidden = true
let playButton = UIButton()
self.playVideoButton = playButton
playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "Accessibility label for button to start media playback")
playButton.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
playButton.contentMode = .scaleAspectFit
playButton.autoSetDimension(.width, toSize: 72)
playButton.autoSetDimension(.height, toSize: 72)
let playButtonWidth = ScaleFromIPhone5(70)
playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth))
self.contentContainer.addSubview(playButton)
playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
playButton.autoCenterInSuperview()
contentContainerView.addSubview(progressBar)
contentContainerView.addSubview(playVideoButton)
}
else if attachment.isAudio, mediaMessageView.audioPlayer != nil {
contentContainerView.addSubview(progressBar)
}
setupLayout()
}
override public func viewWillAppear(_ animated: Bool) {
Logger.debug("")
super.viewWillAppear(animated)
prepDelegate?.prepViewControllerUpdateNavigationBar()
prepDelegate?.prepViewControllerUpdateControls()
}
override public func viewDidAppear(_ animated: Bool) {
Logger.debug("")
super.viewDidAppear(animated)
prepDelegate?.prepViewControllerUpdateNavigationBar()
prepDelegate?.prepViewControllerUpdateControls()
}
override public func viewWillLayoutSubviews() {
Logger.debug("")
super.viewWillLayoutSubviews()
// e.g. if flipping to/from landscape
updateMinZoomScaleForSize(view.bounds.size)
setupZoomScale()
ensureAttachmentViewScale(animated: false)
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Note: Need to do this here to ensure it's based on the final sizing
// otherwise the offsets will be slightly off
resetContentInset()
}
// MARK: - Layout
private func setupLayout() {
NSLayoutConstraint.activate([
contentContainerView.topAnchor.constraint(equalTo: view.topAnchor),
contentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor),
contentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor),
contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
scrollView.leftAnchor.constraint(equalTo: contentContainerView.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: contentContainerView.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor),
mediaMessageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
mediaMessageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
mediaMessageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
mediaMessageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
mediaMessageView.widthAnchor.constraint(equalTo: view.widthAnchor),
mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
if attachment.isImage, let editorView: ImageEditorView = imageEditorView {
let size: CGSize = (attachment.image()?.size ?? CGSize.zero)
let isPortrait: Bool = (size.height > size.width)
NSLayoutConstraint.activate([
editorView.topAnchor.constraint(equalTo: view.topAnchor),
editorView.leftAnchor.constraint(equalTo: view.leftAnchor),
editorView.rightAnchor.constraint(equalTo: view.rightAnchor),
editorView.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
// Don't offset portrait images as they look fine vertically aligned, horizontal
// ones need to be pushed up a bit though
constant: (isPortrait ? 0 : -AttachmentPrepViewController.verticalCenterOffset)
)
])
}
if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
let playButtonSize: CGFloat = ScaleFromIPhone5(70)
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: mediaMessageView.topAnchor),
playerView.leftAnchor.constraint(equalTo: mediaMessageView.leftAnchor),
playerView.rightAnchor.constraint(equalTo: mediaMessageView.rightAnchor),
playerView.bottomAnchor.constraint(equalTo: mediaMessageView.bottomAnchor),
progressBar.topAnchor.constraint(equalTo: view.topAnchor),
progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor),
progressBar.heightAnchor.constraint(equalToConstant: 44),
playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
playVideoButton.centerYAnchor.constraint(
equalTo: contentContainerView.centerYAnchor,
constant: -AttachmentPrepViewController.verticalCenterOffset
),
playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize),
playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize),
])
}
else if attachment.isAudio, mediaMessageView.audioPlayer != nil {
NSLayoutConstraint.activate([
progressBar.topAnchor.constraint(equalTo: view.topAnchor),
progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor),
progressBar.heightAnchor.constraint(equalToConstant: 44)
])
}
}
// MARK: - Navigation Bar
@ -205,39 +287,33 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
guard let imageEditorView = imageEditorView else {
return []
}
return imageEditorView.navigationBarItems()
}
// MARK: - Event Handlers
@objc
public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
@objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
assert(self.videoPlayer != nil)
self.pauseVideo()
}
@objc
public func playButtonTapped() {
@objc public func playButtonTapped() {
self.playVideo()
}
// MARK: - Video
private func playVideo() {
Logger.info("")
guard let videoPlayer = self.videoPlayer else {
owsFailDebug("video player was unexpectedly nil")
return
}
guard let playVideoButton = self.playVideoButton else {
owsFailDebug("playVideoButton was unexpectedly nil")
return
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 0.0
UIView.animate(withDuration: 0.1) { [weak self] in
self?.playVideoButton.alpha = 0.0
}
videoPlayer.play()
}
@ -248,55 +324,77 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
}
videoPlayer.pause()
guard let playVideoButton = self.playVideoButton else {
owsFailDebug("playVideoButton was unexpectedly nil")
return
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 1.0
UIView.animate(withDuration: 0.1) { [weak self] in
self?.playVideoButton.alpha = 1.0
}
}
@objc
public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
guard let playVideoButton = self.playVideoButton else {
owsFailDebug("playVideoButton was unexpectedly nil")
return
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 1.0
@objc public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
UIView.animate(withDuration: 0.1) { [weak self] in
self?.playVideoButton.alpha = 1.0
}
}
public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) {
if attachment.isAudio {
mediaMessageView.pauseAudio()
return
}
guard let videoPlayer = self.videoPlayer else {
owsFailDebug("video player was unexpectedly nil")
return
}
videoPlayer.pause()
}
public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) {
if attachment.isAudio {
mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time))
progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds)
return
}
guard let videoPlayer = self.videoPlayer else {
owsFailDebug("video player was unexpectedly nil")
return
}
videoPlayer.seek(to: time)
progressBar.updateState()
}
public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) {
if attachment.isAudio {
mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time))
progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds)
if mediaMessageView.wasPlayingAudio {
mediaMessageView.playAudio()
}
return
}
guard let videoPlayer = self.videoPlayer else {
owsFailDebug("video player was unexpectedly nil")
return
}
videoPlayer.seek(to: time)
progressBar.updateState()
if (shouldResumePlayback) {
videoPlayer.play()
}
}
// MARK: - MediaMessageViewAudioDelegate
public func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) {
progressBar.manuallySetValue(progressSeconds, durationSeconds: durationSeconds)
}
// MARK: - Helpers
@ -315,6 +413,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
var shouldAllowAttachmentViewResizing: Bool = true
var attachmentViewScale: AttachmentViewScale = .fullsize
public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) {
self.attachmentViewScale = attachmentViewScale
ensureAttachmentViewScale(animated: animated)
@ -323,9 +422,9 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
func ensureAttachmentViewScale(animated: Bool) {
let animationDuration = animated ? 0.2 : 0
guard shouldAllowAttachmentViewResizing else {
if self.contentContainer.transform != CGAffineTransform.identity {
if self.contentContainerView.transform != CGAffineTransform.identity {
UIView.animate(withDuration: animationDuration) {
self.contentContainer.transform = CGAffineTransform.identity
self.contentContainerView.transform = CGAffineTransform.identity
}
}
return
@ -333,14 +432,14 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
switch attachmentViewScale {
case .fullsize:
guard self.contentContainer.transform != .identity else {
guard self.contentContainerView.transform != .identity else {
return
}
UIView.animate(withDuration: animationDuration) {
self.contentContainer.transform = CGAffineTransform.identity
self.contentContainerView.transform = CGAffineTransform.identity
}
case .compact:
guard self.contentContainer.transform == .identity else {
guard self.contentContainerView.transform == .identity else {
return
}
UIView.animate(withDuration: animationDuration) {
@ -354,7 +453,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
let heightDelta = originalHeight * (1 - kScaleFactor)
let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2)
self.contentContainer.transform = scale.concatenating(translate)
self.contentContainerView.transform = scale.concatenating(translate)
}
}
}
@ -367,66 +466,61 @@ extension AttachmentPrepViewController: UIScrollViewDelegate {
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if isZoomable {
return mediaMessageView
} else {
// don't zoom for audio or generic attachments.
return nil
}
// Don't zoom for audio or generic attachments.
return nil
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
resetContentInset()
}
fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
Logger.debug("")
fileprivate func setupZoomScale() {
// We only want to setup the zoom scale once (otherwise we get glitchy behaviour
// when anything forces a re-layout)
guard abs(scrollView.maximumZoomScale - 1.0) <= CGFloat.leastNormalMagnitude else {
return
}
// Ensure bounds have been computed
mediaMessageView.layoutIfNeeded()
guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else {
Logger.warn("bad bounds")
return
}
let widthScale = size.width / mediaMessageView.bounds.width
let heightScale = size.height / mediaMessageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.maximumZoomScale = minScale * 5.0
let widthScale: CGFloat = (view.bounds.size.width / mediaMessageView.bounds.width)
let heightScale: CGFloat = (view.bounds.size.height / mediaMessageView.bounds.height)
let minScale: CGFloat = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = (minScale * 5)
scrollView.zoomScale = minScale
}
// Keep the media view centered within the scroll view as you zoom
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize = self.scrollViewVisibleSize
// First assume that mediaMessageView center coincides with the contents center
// This is correct when the mediaMessageView is bigger than scrollView due to zoom
var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2))
let scrollViewCenter = self.scrollViewCenter
// if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly
if self.scrollView.contentSize.width < scrollViewSize.width {
contentCenter.x = scrollViewCenter.x
// Allow the user to zoom out to 100% of the attachment size if it's smaller
// than the screen
fileprivate func resetContentInset() {
// If the content isn't zoomable then inset the content so it appears centered
guard isZoomable else {
scrollView.contentInset = UIEdgeInsets(
top: -AttachmentPrepViewController.verticalCenterOffset,
leading: 0,
bottom: 0,
trailing: 0
)
return
}
if self.scrollView.contentSize.height < scrollViewSize.height {
contentCenter.y = scrollViewCenter.y
}
self.mediaMessageView.center = contentCenter
}
// return the scroll view center
private var scrollViewCenter: CGPoint {
let size = scrollViewVisibleSize
return CGPoint(x: (size.width / 2), y: (size.height / 2))
}
// Return scrollview size without the area overlapping with tab and nav bar.
private var scrollViewVisibleSize: CGSize {
let contentInset = scrollView.contentInset
let scrollViewSize = scrollView.bounds.standardized.size
let width = scrollViewSize.width - (contentInset.left + contentInset.right)
let height = scrollViewSize.height - (contentInset.top + contentInset.bottom)
return CGSize(width: width, height: height)
let offsetX: CGFloat = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
scrollView.contentInset = UIEdgeInsets(
top: offsetY - AttachmentPrepViewController.verticalCenterOffset,
left: offsetX,
bottom: 0,
right: 0
)
}
}

View File

@ -32,8 +32,9 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
}
// Layout Constants
let kMinTextViewHeight: CGFloat = 40
static let kToolbarMargin: CGFloat = 8
static let kMinTextViewHeight: CGFloat = 40
var maxTextViewHeight: CGFloat {
// About ~4 lines in portrait and ~3 lines in landscape.
// Otherwise we risk obscuring too much of the content.
@ -46,7 +47,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
init() {
self.sendButton = UIButton(type: .system)
self.textViewHeight = kMinTextViewHeight
self.textViewHeight = AttachmentTextToolbar.kMinTextViewHeight
super.init(frame: CGRect.zero)
@ -77,15 +78,19 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
contentView.autoPinEdgesToSuperviewEdges()
// Layout
let kToolbarMargin: CGFloat = 8
// We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins
// when resigning first responder (verified by auditing with `layoutMarginsDidChange`).
// The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the
// user dismisses the keyboard, giving the input accessory view a wonky layout.
contentView.layoutMargins = UIEdgeInsets(top: kToolbarMargin, left: kToolbarMargin, bottom: kToolbarMargin, right: kToolbarMargin)
contentView.layoutMargins = UIEdgeInsets(
top: AttachmentTextToolbar.kToolbarMargin,
left: AttachmentTextToolbar.kToolbarMargin,
bottom: AttachmentTextToolbar.kToolbarMargin,
right: AttachmentTextToolbar.kToolbarMargin
)
self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight)
self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: AttachmentTextToolbar.kMinTextViewHeight)
// We pin all three edges explicitly rather than doing something like:
// textView.autoPinEdges(toSuperviewMarginsExcludingEdge: .right)
@ -97,7 +102,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
textContainer.autoPinEdge(toSuperviewMargin: .bottom)
textContainer.autoPinEdge(toSuperviewMargin: .left)
sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: kToolbarMargin)
sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: AttachmentTextToolbar.kToolbarMargin)
sendButton.autoPinEdge(.bottom, to: .bottom, of: textContainer, withOffset: -3)
sendButton.autoPinEdge(toSuperviewMargin: .right)
@ -170,7 +175,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
textContainer.layer.borderColor = UIColor.white.cgColor
textContainer.layer.borderWidth = Values.separatorThickness
textContainer.layer.cornerRadius = kMinTextViewHeight / 2
textContainer.layer.cornerRadius = (AttachmentTextToolbar.kMinTextViewHeight / 2)
textContainer.clipsToBounds = true
textContainer.addSubview(placeholderTextView)
@ -314,6 +319,6 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat {
let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
return CGFloatClamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight)
return CGFloatClamp(contentSize.height, AttachmentTextToolbar.kMinTextViewHeight, maxTextViewHeight)
}
}

View File

@ -770,7 +770,7 @@ class ImageEditorCropViewController: OWSViewController {
}
@objc public func rotate90ButtonPressed() {
rotateButtonPressed(angleRadians: CGFloat.pi * 0.5, rotateCanvas: true)
rotateButtonPressed(angleRadians: -CGFloat.pi * 0.5, rotateCanvas: true)
}
private func rotateButtonPressed(angleRadians: CGFloat, rotateCanvas: Bool) {

View File

@ -5,37 +5,36 @@
import Foundation
import MediaPlayer
import YYImage
import NVActivityIndicatorView
import SessionUIKit
@objc
public enum MediaMessageViewMode: UInt {
case large
case small
case attachmentApproval
public protocol MediaMessageViewAudioDelegate: AnyObject {
func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat)
}
@objc
public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
public enum Mode: UInt {
case large
case small
case attachmentApproval
}
// MARK: Properties
@objc
public let mode: MediaMessageViewMode
@objc
public let mode: Mode
public let attachment: SignalAttachment
@objc
public var audioPlayer: OWSAudioPlayer?
@objc
public var audioPlayButton: UIButton?
@objc
public var videoPlayButton: UIImageView?
@objc
public lazy var audioPlayer: OWSAudioPlayer? = {
guard let dataUrl = attachment.dataUrl else { return nil }
return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self)
}()
public var wasPlayingAudio: Bool = false
public var audioProgressSeconds: CGFloat = 0
public var audioDurationSeconds: CGFloat = 0
public weak var audioDelegate: MediaMessageViewAudioDelegate?
public var playbackState = AudioPlaybackState.stopped {
didSet {
AssertIsOnMainThread()
@ -43,15 +42,50 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
ensureButtonState()
}
}
@objc
public var audioProgressSeconds: CGFloat = 0
@objc
public var audioDurationSeconds: CGFloat = 0
@objc
public var contentView: UIView?
private lazy var validImage: UIImage? = {
if attachment.isImage {
guard
attachment.isValidImage,
let image: UIImage = attachment.image(),
image.size.width > 0,
image.size.height > 0
else {
return nil
}
return image
}
else if attachment.isVideo {
guard
attachment.isValidVideo,
let image: UIImage = attachment.videoPreview(),
image.size.width > 0,
image.size.height > 0
else {
return nil
}
return image
}
return nil
}()
private lazy var validAnimatedImage: YYImage? = {
guard
attachment.isAnimatedImage,
attachment.isValidImage,
let dataUrl: URL = attachment.dataUrl,
let image: YYImage = YYImage(contentsOfFile: dataUrl.path),
image.size.width > 0,
image.size.height > 0
else {
return nil
}
return image
}()
private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
// MARK: Initializers
@ -62,343 +96,520 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
// Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind
// of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future.
@objc
public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) {
if attachment.hasError {
owsFailDebug(attachment.error.debugDescription)
}
public required init(attachment: SignalAttachment, mode: MediaMessageView.Mode) {
if attachment.hasError { owsFailDebug(attachment.error.debugDescription) }
self.attachment = attachment
self.mode = mode
// Set the linkPreviewUrl if it's a url
if attachment.isUrl, let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) {
self.linkPreviewInfo = (url: linkPreviewURL, draft: nil)
}
super.init(frame: CGRect.zero)
createViews()
setupViews()
setupLayout()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Create Views
private func createViews() {
if attachment.isAnimatedImage {
createAnimatedPreview()
} else if attachment.isImage {
createImagePreview()
} else if attachment.isVideo {
createVideoPreview()
} else if attachment.isAudio {
createAudioPreview()
} else {
createGenericPreview()
// MARK: - UI
private lazy var stackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .fill
switch mode {
case .attachmentApproval: stackView.spacing = 2
case .large: stackView.spacing = 10
case .small: stackView.spacing = 5
}
}
private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView {
assert(subviews.count > 0)
let stackView = UIView()
var lastView: UIView?
for subview in subviews {
stackView.addSubview(subview)
subview.autoHCenterInSuperview()
if lastView == nil {
subview.autoPinEdge(toSuperviewEdge: .top)
} else {
subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: stackSpacing())
}
lastView = subview
}
lastView?.autoPinEdge(toSuperviewEdge: .bottom)
return stackView
}
private func stackSpacing() -> CGFloat {
}()
private lazy var loadingView: NVActivityIndicatorView = {
let view: NVActivityIndicatorView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
return view
}()
private lazy var imageView: UIImageView = {
let view: UIImageView = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate)
view.tintColor = Colors.text
view.isHidden = true
// Override the image to the correct one
if attachment.isImage || attachment.isVideo {
if let validImage: UIImage = validImage {
view.layer.minificationFilter = .trilinear
view.layer.magnificationFilter = .trilinear
view.image = validImage
}
}
else if attachment.isUrl {
view.clipsToBounds = true
view.image = UIImage(named: "Link")?.withTint(Colors.text)
view.contentMode = .center
view.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06))
view.layer.cornerRadius = 8
}
return view
}()
private lazy var fileTypeImageView: UIImageView = {
let view: UIImageView = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
return view
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let view: YYAnimatedImageView = YYAnimatedImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
if let image: YYImage = validAnimatedImage {
view.image = image
}
else {
view.contentMode = .scaleAspectFit
view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate)
view.tintColor = Colors.text
}
return view
}()
lazy var videoPlayButton: UIImageView = {
let view: UIImageView = UIImageView(image: UIImage(named: "CirclePlay"))
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.isHidden = true
return view
}()
/// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state
private lazy var audioPlayPauseButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.setBackgroundImage(UIColor.white.toImage(), for: .normal)
button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted)
button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside)
button.isHidden = true
return button
}()
private lazy var titleStackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = (attachment.isUrl && linkPreviewInfo?.url != nil ? .leading : .center)
stackView.distribution = .fill
switch mode {
case .large, .attachmentApproval:
return CGFloat(10)
case .small:
return CGFloat(5)
case .attachmentApproval: stackView.spacing = 2
case .large: stackView.spacing = 10
case .small: stackView.spacing = 5
}
}
private func createAudioPreview() {
guard let dataUrl = attachment.dataUrl else {
createGenericPreview()
return
}
audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self)
var subviews = [UIView]()
let audioPlayButton = UIButton()
self.audioPlayButton = audioPlayButton
setAudioIconToPlay()
audioPlayButton.imageView?.layer.minificationFilter = .trilinear
audioPlayButton.imageView?.layer.magnificationFilter = .trilinear
audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside)
let buttonSize = createHeroViewSize()
audioPlayButton.autoSetDimension(.width, toSize: buttonSize)
audioPlayButton.autoSetDimension(.height, toSize: buttonSize)
subviews.append(audioPlayButton)
let fileNameLabel = createFileNameLabel()
if let fileNameLabel = fileNameLabel {
subviews.append(fileNameLabel)
}
let fileSizeLabel = createFileSizeLabel()
subviews.append(fileSizeLabel)
let stackView = wrapViewsInVerticalStack(subviews: subviews)
self.addSubview(stackView)
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
// We want to center the stackView in it's superview while also ensuring
// it's superview is big enough to contain it.
stackView.autoPinWidthToSuperview()
stackView.autoVCenterInSuperview()
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
stackView.autoPinHeightToSuperview()
}
stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
}
private func createAnimatedPreview() {
guard attachment.isValidImage else {
createGenericPreview()
return
}
guard let dataUrl = attachment.dataUrl else {
createGenericPreview()
return
}
guard let image = YYImage(contentsOfFile: dataUrl.path) else {
createGenericPreview()
return
}
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
let animatedImageView = YYAnimatedImageView()
animatedImageView.image = image
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio)
contentView = animatedImageView
}
private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) {
self.addSubview(view)
// This emulates the behavior of contentMode = .scaleAspectFit using
// iOS auto layout constraints.
//
// This allows ConversationInputToolbar to place the "cancel" button
// in the upper-right hand corner of the preview content.
view.autoCenterInSuperview()
view.autoPin(toAspectRatio: aspectRatio)
view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
}
private func createImagePreview() {
guard attachment.isValidImage else {
createGenericPreview()
return
}
guard let image = attachment.image() else {
createGenericPreview()
return
}
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = .trilinear
imageView.layer.magnificationFilter = .trilinear
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
contentView = imageView
}
private func createVideoPreview() {
guard attachment.isValidVideo else {
createGenericPreview()
return
}
guard let image = attachment.videoPreview() else {
createGenericPreview()
return
}
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = .trilinear
imageView.layer.magnificationFilter = .trilinear
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
contentView = imageView
// attachment approval provides it's own play button to keep it
// at the proper zoom scale.
if mode != .attachmentApproval {
let videoPlayIcon = UIImage(named: "CirclePlay")!
let videoPlayButton = UIImageView(image: videoPlayIcon)
self.videoPlayButton = videoPlayButton
videoPlayButton.contentMode = .scaleAspectFit
self.addSubview(videoPlayButton)
videoPlayButton.autoCenterInSuperview()
videoPlayButton.autoSetDimension(.width, toSize: 72)
videoPlayButton.autoSetDimension(.height, toSize: 72)
}
}
private func createGenericPreview() {
var subviews = [UIView]()
let imageView = createHeroImageView(imageName: "FileLarge")
imageView.contentMode = .center
subviews.append(imageView)
let fileNameLabel = createFileNameLabel()
if let fileNameLabel = fileNameLabel {
subviews.append(fileNameLabel)
}
let fileSizeLabel = createFileSizeLabel()
subviews.append(fileSizeLabel)
let stackView = wrapViewsInVerticalStack(subviews: subviews)
self.addSubview(stackView)
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
// We want to center the stackView in it's superview while also ensuring
// it's superview is big enough to contain it.
stackView.autoPinWidthToSuperview()
stackView.autoVCenterInSuperview()
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
stackView.autoPinHeightToSuperview()
}
stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
}
private func createHeroViewSize() -> CGFloat {
return stackView
}()
private lazy var titleLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
// Styling
switch mode {
case .large:
return ScaleFromIPhone5To7Plus(175, 225)
case .attachmentApproval:
return ScaleFromIPhone5(100)
case .small:
return ScaleFromIPhone5To7Plus(80, 80)
case .attachmentApproval:
label.font = UIFont.ows_boldFont(withSize: ScaleFromIPhone5To7Plus(16, 22))
label.textColor = Colors.text
case .large:
label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
label.textColor = Colors.accent
case .small:
label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
label.textColor = Colors.accent
}
}
private func createHeroImageView(imageName: String) -> UIView {
let imageSize = createHeroViewSize()
let image = UIImage(named: imageName)
assert(image != nil)
let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = .trilinear
imageView.layer.magnificationFilter = .trilinear
imageView.layer.shadowColor = UIColor.black.cgColor
let shadowScaling = 5.0
imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling)
imageView.layer.shadowOpacity = 0.25
imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling)
imageView.autoSetDimension(.width, toSize: imageSize)
imageView.autoSetDimension(.height, toSize: imageSize)
return imageView
}
private func labelFont() -> UIFont {
switch mode {
case .large, .attachmentApproval:
return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
case .small:
return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
// Content
if attachment.isUrl {
// If we have no link preview info at this point then assume link previews are disabled
if let linkPreviewURL: String = linkPreviewInfo?.url {
label.font = .boldSystemFont(ofSize: Values.smallFontSize)
label.text = linkPreviewURL
label.textAlignment = .left
label.lineBreakMode = .byTruncatingTail
label.numberOfLines = 2
}
else {
label.text = "vc_share_link_previews_disabled_title".localized()
}
}
}
private var controlTintColor: UIColor {
switch mode {
case .small, .large:
return Colors.accent
case .attachmentApproval:
return Colors.text
// Title for everything except these types
else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo {
if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 {
label.text = fileName
}
else if let fileExtension: String = attachment.fileExtension {
label.text = String(
format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(),
fileExtension.uppercased()
)
}
label.textAlignment = .center
label.lineBreakMode = .byTruncatingMiddle
}
}
private func formattedFileExtension() -> String? {
guard let fileExtension = attachment.fileExtension else {
return nil
}
return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT",
comment: "Format string for file extension label in call interstitial view"),
fileExtension.uppercased())
}
public func formattedFileName() -> String? {
guard let sourceFilename = attachment.sourceFilename else {
return nil
}
let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard filename.count > 0 else {
return nil
}
return filename
}
private func createFileNameLabel() -> UIView? {
let filename = formattedFileName() ?? formattedFileExtension()
guard filename != nil else {
return nil
}
let label = UILabel()
label.text = filename
label.textColor = controlTintColor
label.font = labelFont()
label.textAlignment = .center
label.lineBreakMode = .byTruncatingMiddle
// Hide the label if it has no content
label.isHidden = ((label.text?.count ?? 0) == 0)
return label
}
private func createFileSizeLabel() -> UIView {
let label = UILabel()
let fileSize = attachment.dataLength
label.text = String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT",
comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."),
OWSFormat.formatFileSize(UInt(fileSize)))
label.textColor = controlTintColor
label.font = labelFont()
label.textAlignment = .center
}()
private lazy var subtitleLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
// Styling
switch mode {
case .attachmentApproval:
label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(12, 18))
label.textColor = Colors.pinIcon
case .large:
label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
label.textColor = Colors.accent
case .small:
label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
label.textColor = Colors.accent
}
// Content
if attachment.isUrl {
// We only load Link Previews for HTTPS urls so append an explanation for not
if let linkPreviewURL: String = linkPreviewInfo?.url {
if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" {
label.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize)
label.text = "vc_share_link_previews_unsecure".localized()
label.textColor = (mode == .attachmentApproval ? Colors.pinIcon : Colors.accent)
}
}
// If we have no link preview info at this point then assume link previews are disabled
else {
label.text = "vc_share_link_previews_disabled_explanation".localized()
label.textColor = Colors.text
label.textAlignment = .center
label.numberOfLines = 0
}
}
// Subtitle for everything else except these types
else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo {
// Format string for file size label in call interstitial view.
// Embeds: {{file size as 'N mb' or 'N kb'}}.
let fileSize: UInt = attachment.dataLength
label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize)))
label.textAlignment = .center
}
// Hide the label if it has no content
label.isHidden = ((label.text?.count ?? 0) == 0)
return label
}()
// MARK: - Layout
private func setupViews() {
// Plain text will just be put in the 'message' input so do nothing
guard !attachment.isText && !attachment.isOversizeText else { return }
// Setup the view hierarchy
addSubview(stackView)
addSubview(loadingView)
addSubview(videoPlayButton)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(animatedImageView)
if !titleLabel.isHidden { stackView.addArrangedSubview(UIView.vhSpacer(10, 10)) }
stackView.addArrangedSubview(titleStackView)
titleStackView.addArrangedSubview(titleLabel)
titleStackView.addArrangedSubview(subtitleLabel)
imageView.addSubview(fileTypeImageView)
// Type-specific configurations
if attachment.isAnimatedImage {
animatedImageView.isHidden = false
}
else if attachment.isImage {
imageView.isHidden = false
}
else if attachment.isVideo {
// Note: The 'attachmentApproval' mode provides it's own play button to keep
// it at the proper scale when zooming
imageView.isHidden = false
videoPlayButton.isHidden = (mode == .attachmentApproval)
}
else if attachment.isAudio {
// Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created
imageView.isHidden = false
audioPlayPauseButton.isHidden = (audioPlayer == nil)
setAudioIconToPlay()
setAudioProgress(0, duration: (audioPlayer?.duration ?? 0))
fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")?
.withRenderingMode(.alwaysTemplate)
fileTypeImageView.tintColor = Colors.text
fileTypeImageView.isHidden = false
// Note: There is an annoying bug where the MediaMessageView will fill the screen if the
// 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with
// the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton)
addSubview(audioPlayPauseButton)
}
else if attachment.isUrl {
imageView.isHidden = false
imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView
loadingView.isHidden = false
if let linkPreviewUrl: String = linkPreviewInfo?.url {
// Don't want to change the axis until we have a URL to start loading, otherwise the
// error message will be broken
stackView.axis = .horizontal
loadLinkPreview(linkPreviewURL: linkPreviewUrl)
}
}
else {
imageView.isHidden = false
}
}
private func setupLayout() {
// Plain text will just be put in the 'message' input so do nothing
guard !attachment.isText && !attachment.isOversizeText else { return }
// Sizing calculations
let clampedRatio: CGFloat = {
if attachment.isUrl {
return 1
}
if attachment.isAnimatedImage {
let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1))
let aspectRatio: CGFloat = (imageSize.width / imageSize.height)
return CGFloatClamp(aspectRatio, 0.05, 95.0)
}
// All other types should maintain the ratio of the image in the 'imageView'
let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1))
let aspectRatio: CGFloat = (imageSize.width / imageSize.height)
return CGFloatClamp(aspectRatio, 0.05, 95.0)
}()
let maybeImageSize: CGFloat? = {
if attachment.isImage || attachment.isVideo {
if validImage != nil { return nil }
// If we don't have a valid image then use the 'generic' case
}
else if attachment.isAnimatedImage {
if validAnimatedImage != nil { return nil }
// If we don't have a valid image then use the 'generic' case
}
else if attachment.isUrl {
return 80
}
// Generic file size
switch mode {
case .large: return 200
case .attachmentApproval: return 120
case .small: return 80
}
}()
let imageSize: CGFloat = (maybeImageSize ?? 0)
let audioButtonSize: CGFloat = (imageSize / 2.5)
audioPlayPauseButton.layer.cornerRadius = (audioButtonSize / 2)
// Actual layout
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
(maybeImageSize != nil ?
stackView.widthAnchor.constraint(
equalTo: widthAnchor,
constant: (attachment.isUrl ? -(32 * 2) : 0) // Inset stackView for urls
) :
stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor)
),
imageView.widthAnchor.constraint(
equalTo: imageView.heightAnchor,
multiplier: clampedRatio
),
animatedImageView.widthAnchor.constraint(
equalTo: animatedImageView.heightAnchor,
multiplier: clampedRatio
),
// Note: AnimatedImage, Image and Video types should allow zooming so be lessThanOrEqualTo
// the view size but some other types should have specific sizes
animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor),
animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
(maybeImageSize != nil ?
imageView.widthAnchor.constraint(equalToConstant: imageSize) :
imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor)
),
(maybeImageSize != nil ?
imageView.heightAnchor.constraint(equalToConstant: imageSize) :
imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor)
),
fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
fileTypeImageView.centerYAnchor.constraint(
equalTo: imageView.centerYAnchor,
constant: ceil(imageSize * 0.15)
),
fileTypeImageView.widthAnchor.constraint(
equalTo: fileTypeImageView.heightAnchor,
multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1))
),
fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5),
videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor),
videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
loadingView.widthAnchor.constraint(equalToConstant: ceil(imageSize / 3)),
loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3))
])
// No inset for the text for URLs but there is for all other layouts
if !attachment.isUrl {
NSLayoutConstraint.activate([
titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
subtitleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2))
])
}
// Note: There is an annoying bug where the MediaMessageView will fill the screen if the
// 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with
// the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton)
if attachment.isAudio {
NSLayoutConstraint.activate([
audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize),
audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize),
])
}
}
// MARK: - Link Loading
private func loadLinkPreview(linkPreviewURL: String) {
loadingView.startAnimating()
OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL)
.done { [weak self] draft in
// TODO: Look at refactoring this behaviour to consolidate attachment mutations
self?.attachment.linkPreviewDraft = draft
self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft)
// Update the UI
self?.titleLabel.text = (draft.title ?? self?.titleLabel.text)
self?.loadingView.alpha = 0
self?.loadingView.stopAnimating()
self?.imageView.alpha = 1
if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) {
self?.imageView.image = loadedImage
self?.imageView.contentMode = .scaleAspectFill
}
}
.catch { [weak self] _ in
self?.loadingView.alpha = 0
self?.loadingView.stopAnimating()
self?.imageView.alpha = 1
self?.titleLabel.numberOfLines = 1 // Truncates the URL at 1 line so the error is more readable
self?.subtitleLabel.isHidden = false
// Set the error text appropriately
if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" {
// This error case is handled already in the 'subtitleLabel' creation
}
else {
self?.subtitleLabel.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize)
self?.subtitleLabel.text = "vc_share_link_previews_error".localized()
self?.subtitleLabel.textColor = (self?.mode == .attachmentApproval ? Colors.pinIcon : Colors.accent )
self?.subtitleLabel.textAlignment = .left
}
}
.retainUntilComplete()
}
// MARK: - Functions
public func playAudio() {
audioPlayer?.play()
ensureButtonState()
}
public func pauseAudio() {
wasPlayingAudio = (audioPlayer?.isPlaying == true)
// If the 'audioPlayer' has a duration of 0 then we probably haven't played previously which
// will result in the audioPlayer having a 'duration' of 0 breaking the progressBar. We play
// the audio to get it to properly load the file right before pausing it so the data is
// loaded correctly
if audioPlayer?.duration == 0 {
audioPlayer?.play()
}
audioPlayer?.pause()
ensureButtonState()
}
public func setAudioTime(currentTime: TimeInterval) {
audioPlayer?.setCurrentTime(currentTime)
}
// MARK: - Event Handlers
@objc
func audioPlayButtonPressed(sender: UIButton) {
@objc func audioPlayPauseButtonPressed(sender: UIButton) {
audioPlayer?.togglePlayState()
}
@ -421,29 +632,27 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
}
private func ensureButtonState() {
if playbackState == .playing {
setAudioIconToPause()
} else {
setAudioIconToPlay()
switch playbackState {
case .playing: setAudioIconToPause()
default: setAudioIconToPlay()
}
}
public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
// Note: When the OWSAudioPlayer stops it sets the duration to 0 (which we want to ignore so
// the UI doesn't look buggy)
let finalDuration: CGFloat = (duration > 0 ? duration : audioDurationSeconds)
audioProgressSeconds = progress
audioDurationSeconds = duration
audioDurationSeconds = finalDuration
audioDelegate?.progressChanged(progress, durationSeconds: finalDuration)
}
private func setAudioIconToPlay() {
let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate)
assert(image != nil)
audioPlayButton?.setImage(image, for: .normal)
audioPlayButton?.imageView?.tintColor = controlTintColor
audioPlayPauseButton.setImage(UIImage(named: "Play"), for: .normal)
}
private func setAudioIconToPause() {
let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate)
assert(image != nil)
audioPlayButton?.setImage(image, for: .normal)
audioPlayButton?.imageView?.tintColor = controlTintColor
audioPlayPauseButton.setImage(UIImage(named: "Pause"), for: .normal)
}
}

View File

@ -58,7 +58,7 @@ public class OWSVideoPlayer: NSObject {
if item.currentTime() == item.duration {
// Rewind for repeated plays, but only if it previously played to end.
avPlayer.seek(to: CMTime.zero)
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
}
avPlayer.play()
@ -67,13 +67,13 @@ public class OWSVideoPlayer: NSObject {
@objc
public func stop() {
avPlayer.pause()
avPlayer.seek(to: CMTime.zero)
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
audioSession.endAudioActivity(self.audioActivity)
}
@objc(seekToTime:)
public func seek(to time: CMTime) {
avPlayer.seek(to: time)
avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
}
// MARK: private

View File

@ -88,12 +88,16 @@ public class PlayerProgressBar: UIView {
let duration: CMTime = item.asset.duration
slider.maximumValue = Float(CMTimeGetSeconds(duration))
// OPTIMIZE We need a high frequency observer for smooth slider updates,
// but could use a much less frequent observer for label updates
progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in
self?.updateState()
}) as AnyObject
updateState()
// OPTIMIZE We need a high frequency observer for smooth slider updates while playing,
// but could use a much less frequent observer for label updates
progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] _ in
// If it is playing update the time
if self?.player?.rate != 0 && self?.player?.error == nil {
self?.updateState()
}
}) as AnyObject
}
}
@ -182,7 +186,7 @@ public class PlayerProgressBar: UIView {
// MARK: Render cycle
private func updateState() {
public func updateState() {
guard let player = player else {
owsFailDebug("player isn't set.")
return
@ -219,4 +223,26 @@ public class PlayerProgressBar: UIView {
let seconds: Double = Double(slider.value)
return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale)
}
// MARK: - Functions
public func manuallySetValue(_ positionSeconds: CGFloat, durationSeconds: CGFloat) {
let remainingSeconds = (durationSeconds - positionSeconds)
slider.minimumValue = 0
slider.maximumValue = Float(durationSeconds)
positionLabel.text = formatter.string(from: positionSeconds)
guard let remainingString = formatter.string(from: remainingSeconds) else {
owsFailDebug("unable to format time remaining")
remainingLabel.text = "0:00"
return
}
// show remaining time as negative
remainingLabel.text = "-\(remainingString)"
slider.setValue(Float(positionSeconds), animated: false)
}
}

View File

@ -460,7 +460,7 @@ typedef void (^BlockAlertCompletionBlock)(UIAlertAction *action);
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OK", nil)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok")
style:UIAlertActionStyleDefault
handler:completionBlock];

View File

@ -65,6 +65,20 @@ public class HomeScreenSearchResultSet: NSObject {
public class var empty: HomeScreenSearchResultSet {
return HomeScreenSearchResultSet(searchText: "", conversations: [], messages: [])
}
public class var noteToSelfOnly: HomeScreenSearchResultSet {
var conversations: [ConversationSearchResult<ConversationSortKey>] = []
Storage.read { transaction in
if let thread = TSContactThread.fetch(for: getUserHexEncodedPublicKey(), using: transaction) {
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
let sortKey = ConversationSortKey(creationDate: thread.creationDate,
lastMessageReceivedAtDate: thread.lastInteractionForInbox(transaction: transaction)?.receivedAtDate())
let searchResult = ConversationSearchResult(thread: threadViewModel, sortKey: sortKey)
conversations.append(searchResult)
}
}
return HomeScreenSearchResultSet(searchText: "", conversations: conversations, messages: [])
}
public var isEmpty: Bool {
return conversations.isEmpty && messages.isEmpty
@ -227,6 +241,7 @@ public class FullTextSearcher: NSObject {
}
public func searchForHomeScreen(searchText: String,
maxSearchResults: Int? = nil,
transaction: YapDatabaseReadTransaction) -> HomeScreenSearchResultSet {
var conversations: [ConversationSearchResult<ConversationSortKey>] = []
@ -234,7 +249,7 @@ public class FullTextSearcher: NSObject {
var existingConversationRecipientIds: Set<String> = Set()
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
self.finder.enumerateObjects(searchText: searchText, maxSearchResults: maxSearchResults, transaction: transaction) { (match: Any, snippet: String?) in
if let thread = match as? TSThread {
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)

View File

@ -30,7 +30,7 @@ public class ThreadViewModel: NSObject {
self.threadRecord = thread
self.isGroupThread = thread.isGroupThread()
self.name = thread.name()
self.name = thread.name(with: transaction)
self.isMuted = thread.isMuted
self.isPinned = thread.isPinned
self.lastMessageText = thread.lastMessageText(transaction: transaction)

View File

@ -57,7 +57,7 @@ import Foundation
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let actionTitle = buttonTitle ?? NSLocalizedString("OK", comment: "")
let actionTitle = buttonTitle ?? NSLocalizedString("BUTTON_OK", comment: "")
let okAction = UIAlertAction(title: actionTitle, style: .default, handler: buttonAction)
okAction.accessibilityIdentifier = "OWSAlerts.\("ok")"
alert.addAction(okAction)
@ -71,7 +71,7 @@ import Foundation
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(self.cancelAction)
let actionTitle = proceedTitle ?? NSLocalizedString("OK", comment: "")
let actionTitle = proceedTitle ?? NSLocalizedString("BUTTON_OK", comment: "")
let okAction = UIAlertAction(title: actionTitle, style: .default, handler: proceedAction)
okAction.accessibilityIdentifier = "OWSAlerts.\("ok")"
alert.addAction(okAction)

View File

@ -5,7 +5,7 @@
import Foundation
// All Observer methods will be invoked from the main thread.
@objc
public protocol ShareViewDelegate: class {
public protocol ShareViewDelegate: AnyObject {
func shareViewWasUnlocked()
func shareViewWasCompleted()
func shareViewWasCancelled()

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