2022-04-06 07:43:26 +02:00
// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
2022-02-01 04:55:03 +01:00
import UIKit
2022-11-27 22:32:32 +01:00
import Combine
2021-02-16 06:36:06 +01:00
import CoreServices
2021-02-16 23:40:23 +01:00
import Photos
2021-04-07 08:47:39 +02:00
import PhotosUI
2022-11-27 22:32:32 +01:00
import Sodium
2022-04-06 07:43:26 +02:00
import GRDB
2023-02-14 03:41:24 +01:00
import SessionUIKit
2022-03-02 04:44:56 +01:00
import SessionMessagingKit
2022-01-28 06:24:18 +01:00
import SessionUtilitiesKit
2022-02-01 04:55:03 +01:00
import SignalUtilitiesKit
2023-08-10 06:43:40 +02:00
import SessionSnodeKit
2021-02-16 23:40:23 +01:00
2022-05-08 14:01:39 +02:00
extension ConversationVC :
InputViewDelegate ,
MessageCellDelegate ,
2022-05-10 09:42:15 +02:00
ContextMenuActionDelegate ,
2022-05-08 14:01:39 +02:00
SendMediaNavDelegate ,
UIDocumentPickerDelegate ,
AttachmentApprovalViewControllerDelegate ,
GifPickerViewControllerDelegate
{
@objc func handleTitleViewTapped ( ) {
// D o n ' t t a k e t h e u s e r t o s e t t i n g s f o r u n a p p r o v e d t h r e a d s
2022-05-25 10:48:04 +02:00
guard viewModel . threadData . threadRequiresApproval = = false else { return }
2022-05-08 14:01:39 +02:00
2021-03-01 05:15:37 +01:00
openSettings ( )
}
2022-05-08 14:01:39 +02:00
2021-01-29 01:46:32 +01:00
@objc func openSettings ( ) {
2022-09-28 09:30:31 +02:00
let viewController : SessionTableViewController = SessionTableViewController (
2022-09-07 09:37:01 +02:00
viewModel : ThreadSettingsViewModel (
threadId : self . viewModel . threadData . threadId ,
threadVariant : self . viewModel . threadData . threadVariant ,
didTriggerSearch : { [ weak self ] in
DispatchQueue . main . async {
self ? . showSearchUI ( )
self ? . popAllConversationSettingsViews {
// N o t e : W i t h o u t t h i s d e l a y t h e s e a r c h b a r d o e s n ' t s h o w
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.5 ) {
self ? . searchController . uiSearchController . searchBar . becomeFirstResponder ( )
}
}
}
}
)
2022-05-08 14:01:39 +02:00
)
2022-09-07 09:37:01 +02:00
navigationController ? . pushViewController ( viewController , animated : true )
2021-01-29 01:46:32 +01:00
}
2022-05-08 14:01:39 +02:00
2022-06-09 10:37:44 +02:00
// MARK: - C a l l
2021-10-21 07:28:48 +02:00
@objc func startCall ( _ sender : Any ? ) {
2021-12-08 04:08:01 +01:00
guard SessionCall . isEnabled else { return }
2023-03-17 05:12:35 +01:00
guard viewModel . threadData . threadIsBlocked = = false else { return }
2022-07-01 05:08:45 +02:00
guard Storage . shared [ . areCallsEnabled ] else {
2022-08-24 09:33:10 +02:00
let confirmationModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " modal_call_permission_request_title " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " modal_call_permission_request_explanation " . localized ( ) ) ,
2022-08-31 09:44:44 +02:00
confirmTitle : " vc_settings_title " . localized ( ) ,
2023-04-26 07:13:39 +02:00
confirmAccessibility : Accessibility ( identifier : " Settings " ) ,
2022-08-31 09:44:44 +02:00
dismissOnConfirm : false // C u s t o m d i s m i s s a l l o g i c
) { [ weak self ] _ in
self ? . dismiss ( animated : true ) {
2022-09-30 10:22:28 +02:00
let navController : UINavigationController = StyledNavigationController (
2022-09-28 09:30:31 +02:00
rootViewController : SessionTableViewController (
2022-09-07 09:37:01 +02:00
viewModel : PrivacySettingsViewModel (
shouldShowCloseButton : true
)
2022-08-31 09:44:44 +02:00
)
2022-08-24 09:33:10 +02:00
)
2022-08-31 09:44:44 +02:00
navController . modalPresentationStyle = . fullScreen
self ? . present ( navController , animated : true , completion : nil )
}
2022-08-24 09:33:10 +02:00
}
2022-08-31 09:44:44 +02:00
)
2022-08-24 09:33:10 +02:00
self . navigationController ? . present ( confirmationModal , animated : true , completion : nil )
2022-06-08 06:29:51 +02:00
return
2021-10-25 02:49:54 +02:00
}
2022-06-08 06:29:51 +02:00
2022-09-02 09:32:13 +02:00
Permissions . requestMicrophonePermissionIfNeeded ( )
2022-06-08 06:29:51 +02:00
2022-06-09 10:37:44 +02:00
let threadId : String = self . viewModel . threadData . threadId
2022-06-08 06:29:51 +02:00
guard AVAudioSession . sharedInstance ( ) . recordPermission = = . granted else { return }
guard self . viewModel . threadData . threadVariant = = . contact else { return }
guard AppEnvironment . shared . callManager . currentCall = = nil else { return }
2022-07-01 05:08:45 +02:00
guard let call : SessionCall = Storage . shared . read ( { db in SessionCall ( db , for : threadId , uuid : UUID ( ) . uuidString . lowercased ( ) , mode : . offer , outgoing : true ) } ) else {
2022-06-09 10:37:44 +02:00
return
}
2022-06-08 06:29:51 +02:00
let callVC = CallVC ( for : call )
callVC . conversationVC = self
2022-07-25 07:39:56 +02:00
hideInputAccessoryView ( )
2022-06-08 06:29:51 +02:00
present ( callVC , animated : true , completion : nil )
2021-08-16 08:24:49 +02:00
}
2021-02-16 23:40:23 +01:00
2022-05-08 14:01:39 +02:00
// MARK: - B l o c k i n g
2021-02-16 23:40:23 +01:00
@objc func unblock ( ) {
2022-07-18 01:54:23 +02:00
self . showBlockedModalIfNeeded ( )
2021-02-16 23:40:23 +01:00
}
2022-07-25 07:39:56 +02:00
@ discardableResult func showBlockedModalIfNeeded ( ) -> Bool {
2022-09-02 09:32:13 +02:00
guard
self . viewModel . threadData . threadVariant = = . contact &&
self . viewModel . threadData . threadIsBlocked = = true
else { return false }
2022-03-22 23:59:38 +01:00
2022-09-02 09:32:13 +02:00
let message = String (
format : " modal_blocked_explanation " . localized ( ) ,
self . viewModel . threadData . displayName
)
let confirmationModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : String (
format : " modal_blocked_title " . localized ( ) ,
self . viewModel . threadData . displayName
) ,
2023-05-16 05:09:05 +02:00
body : . attributedText (
NSAttributedString ( string : message )
. adding (
attributes : [ . font : UIFont . boldSystemFont ( ofSize : Values . smallFontSize ) ] ,
range : ( message as NSString ) . range ( of : self . viewModel . threadData . displayName )
)
) ,
2022-09-02 09:32:13 +02:00
confirmTitle : " modal_blocked_button_title " . localized ( ) ,
2023-04-26 07:13:39 +02:00
confirmAccessibility : Accessibility ( identifier : " Confirm block " ) ,
cancelAccessibility : Accessibility ( identifier : " Cancel block " ) ,
2022-09-02 09:32:13 +02:00
dismissOnConfirm : false // C u s t o m d i s m i s s a l l o g i c
) { [ weak self ] _ in
self ? . viewModel . unblockContact ( )
self ? . dismiss ( animated : true , completion : nil )
}
)
present ( confirmationModal , animated : true , completion : nil )
2022-05-08 14:01:39 +02:00
2021-02-16 23:40:23 +01:00
return true
}
2022-05-08 14:01:39 +02:00
// MARK: - S e n d M e d i a N a v D e l e g a t e
2022-09-20 08:06:01 +02:00
func sendMediaNavDidCancel ( _ sendMediaNavigationController : SendMediaNavigationController ? ) {
2021-02-16 23:40:23 +01:00
dismiss ( animated : true , completion : nil )
}
2023-08-01 06:39:00 +02:00
func sendMediaNav (
_ sendMediaNavigationController : SendMediaNavigationController ,
didApproveAttachments attachments : [ SignalAttachment ] ,
forThreadId threadId : String ,
messageText : String ? ,
using dependencies : Dependencies
) {
sendMessage ( text : ( messageText ? ? " " ) , attachments : attachments , using : dependencies )
2022-05-23 09:16:14 +02:00
resetMentions ( )
2023-06-23 09:54:29 +02:00
dismiss ( animated : true ) { [ weak self ] in
if self ? . isFirstResponder = = false {
self ? . becomeFirstResponder ( )
}
else {
self ? . reloadInputViews ( )
}
}
2021-02-16 23:40:23 +01:00
}
func sendMediaNavInitialMessageText ( _ sendMediaNavigationController : SendMediaNavigationController ) -> String ? {
return snInputView . text
}
func sendMediaNav ( _ sendMediaNavigationController : SendMediaNavigationController , didChangeMessageText newMessageText : String ? ) {
2022-05-08 14:01:39 +02:00
snInputView . text = ( newMessageText ? ? " " )
2021-02-16 23:40:23 +01:00
}
2022-05-08 14:01:39 +02:00
// MARK: - A t t a c h m e n t A p p r o v a l V i e w C o n t r o l l e r D e l e g a t e
2023-08-01 06:27:41 +02:00
func attachmentApproval ( _ attachmentApproval : AttachmentApprovalViewController , didApproveAttachments attachments : [ SignalAttachment ] , forThreadId threadId : String , messageText : String ? , using dependencies : Dependencies ) {
sendMessage ( text : ( messageText ? ? " " ) , attachments : attachments , using : dependencies )
2022-05-23 09:16:14 +02:00
resetMentions ( )
2023-06-23 09:54:29 +02:00
dismiss ( animated : true ) { [ weak self ] in
if self ? . isFirstResponder = = false {
self ? . becomeFirstResponder ( )
}
else {
self ? . reloadInputViews ( )
}
}
2021-02-17 00:06:17 +01:00
}
func attachmentApprovalDidCancel ( _ attachmentApproval : AttachmentApprovalViewController ) {
dismiss ( animated : true , completion : nil )
}
func attachmentApproval ( _ attachmentApproval : AttachmentApprovalViewController , didChangeMessageText newMessageText : String ? ) {
2023-06-23 09:54:29 +02:00
snInputView . text = ( newMessageText ? ? " " )
2021-02-17 00:06:17 +01:00
}
2022-05-23 01:49:04 +02:00
func attachmentApproval ( _ attachmentApproval : AttachmentApprovalViewController , didRemoveAttachment attachment : SignalAttachment ) {
}
func attachmentApprovalDidTapAddMore ( _ attachmentApproval : AttachmentApprovalViewController ) {
}
2021-02-17 00:06:17 +01:00
2022-05-08 14:01:39 +02:00
// MARK: - E x p a n d i n g A t t a c h m e n t s B u t t o n D e l e g a t e
func handleGIFButtonTapped ( ) {
2023-02-14 06:02:24 +01:00
guard Storage . shared [ . isGiphyEnabled ] else {
let modal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " GIPHY_PERMISSION_TITLE " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " GIPHY_PERMISSION_MESSAGE " . localized ( ) ) ,
2023-02-14 06:02:24 +01:00
confirmTitle : " continue_2 " . localized ( )
) { [ weak self ] _ in
Storage . shared . writeAsync (
updates : { db in
db [ . isGiphyEnabled ] = true
} ,
completion : { _ , _ in
DispatchQueue . main . async {
self ? . handleGIFButtonTapped ( )
}
}
)
}
)
present ( modal , animated : true , completion : nil )
return
}
2022-05-08 14:01:39 +02:00
let gifVC = GifPickerViewController ( )
gifVC . delegate = self
2022-09-30 10:22:28 +02:00
let navController = StyledNavigationController ( rootViewController : gifVC )
2022-05-08 14:01:39 +02:00
navController . modalPresentationStyle = . fullScreen
present ( navController , animated : true ) { }
}
func handleDocumentButtonTapped ( ) {
// U I D o c u m e n t P i c k e r M o d e I m p o r t c o p i e s t o a t e m p f i l e w i t h i n o u r c o n t a i n e r .
// I t u s e s m o r e m e m o r y t h a n " o p e n " b u t l e t s u s a v o i d w o r k i n g w i t h s e c u r i t y s c o p e d U R L s .
let documentPickerVC = UIDocumentPickerViewController ( documentTypes : [ kUTTypeItem as String ] , in : UIDocumentPickerMode . import )
documentPickerVC . delegate = self
documentPickerVC . modalPresentationStyle = . fullScreen
2022-08-12 05:35:17 +02:00
2022-05-08 14:01:39 +02:00
present ( documentPickerVC , animated : true , completion : nil )
2021-01-29 01:46:32 +01:00
}
func handleLibraryButtonTapped ( ) {
2022-05-25 10:48:04 +02:00
let threadId : String = self . viewModel . threadData . threadId
2022-05-15 06:39:21 +02:00
2022-09-02 09:32:13 +02:00
Permissions . requestLibraryPermissionIfNeeded { [ weak self ] in
2021-04-08 05:36:20 +02:00
DispatchQueue . main . async {
2022-05-15 06:39:21 +02:00
let sendMediaNavController = SendMediaNavigationController . showingMediaLibraryFirst (
threadId : threadId
)
2021-04-08 05:36:20 +02:00
sendMediaNavController . sendMediaNavDelegate = self
sendMediaNavController . modalPresentationStyle = . fullScreen
self ? . present ( sendMediaNavController , animated : true , completion : nil )
}
}
2021-01-29 01:46:32 +01:00
}
2022-05-08 14:01:39 +02:00
func handleCameraButtonTapped ( ) {
2022-09-02 09:32:13 +02:00
guard Permissions . requestCameraPermissionIfNeeded ( presentingViewController : self ) else { return }
2022-05-11 10:20:10 +02:00
2022-09-02 09:32:13 +02:00
Permissions . requestMicrophonePermissionIfNeeded ( )
2022-05-11 10:20:10 +02:00
2022-05-08 14:01:39 +02:00
if AVAudioSession . sharedInstance ( ) . recordPermission != . granted {
SNLog ( " Proceeding without microphone access. Any recorded video will be silent. " )
}
2022-05-11 10:20:10 +02:00
2022-05-25 10:48:04 +02:00
let sendMediaNavController = SendMediaNavigationController . showingCameraFirst ( threadId : self . viewModel . threadData . threadId )
2022-05-08 14:01:39 +02:00
sendMediaNavController . sendMediaNavDelegate = self
sendMediaNavController . modalPresentationStyle = . fullScreen
2022-05-11 10:20:10 +02:00
2022-05-08 14:01:39 +02:00
present ( sendMediaNavController , animated : true , completion : nil )
2021-02-17 00:23:50 +01:00
}
2022-05-08 14:01:39 +02:00
// MARK: - G i f P i c k e r V i e w C o n t r o l l e r D e l e g a t e
2021-02-17 00:23:50 +01:00
func gifPickerDidSelect ( attachment : SignalAttachment ) {
showAttachmentApprovalDialog ( for : [ attachment ] )
2021-01-29 01:46:32 +01:00
}
2022-05-08 14:01:39 +02:00
// MARK: - U I D o c u m e n t P i c k e r D e l e g a t e
2022-08-12 05:35:17 +02:00
2021-02-16 23:40:23 +01:00
func documentPicker ( _ controller : UIDocumentPickerViewController , didPickDocumentsAt urls : [ URL ] ) {
guard let url = urls . first else { return } // TODO: H a n d l e m u l t i p l e ?
2022-05-11 10:20:10 +02:00
2021-02-16 23:40:23 +01:00
let urlResourceValues : URLResourceValues
do {
urlResourceValues = try url . resourceValues ( forKeys : [ . typeIdentifierKey , . isDirectoryKey , . nameKey ] )
}
2022-05-11 10:20:10 +02:00
catch {
DispatchQueue . main . async { [ weak self ] in
2022-09-28 10:26:02 +02:00
let modal : ConfirmationModal = ConfirmationModal (
targetView : self ? . view ,
info : ConfirmationModal . Info (
title : " Session " ,
2023-05-16 05:09:05 +02:00
body : . text ( " An error occurred. " ) ,
2022-09-28 10:26:02 +02:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
self ? . present ( modal , animated : true )
2022-05-11 10:20:10 +02:00
}
return
}
2021-02-16 23:40:23 +01:00
let type = urlResourceValues . typeIdentifier ? ? ( kUTTypeData as String )
guard urlResourceValues . isDirectory != true else {
2022-09-28 10:26:02 +02:00
DispatchQueue . main . async { [ weak self ] in
let modal : ConfirmationModal = ConfirmationModal (
targetView : self ? . view ,
info : ConfirmationModal . Info (
title : " ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY " . localized ( ) ) ,
2022-09-28 10:26:02 +02:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
2022-05-11 10:20:10 +02:00
)
2022-09-28 10:26:02 +02:00
self ? . present ( modal , animated : true )
2021-02-16 23:40:23 +01:00
}
return
}
2022-05-11 10:20:10 +02:00
2021-02-16 23:40:23 +01:00
let fileName = urlResourceValues . name ? ? NSLocalizedString ( " ATTACHMENT_DEFAULT_FILENAME " , comment : " " )
guard let dataSource = DataSourcePath . dataSource ( with : url , shouldDeleteOnDeallocation : false ) else {
2022-09-28 10:26:02 +02:00
DispatchQueue . main . async { [ weak self ] in
let modal : ConfirmationModal = ConfirmationModal (
targetView : self ? . view ,
info : ConfirmationModal . Info (
title : " ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE " . localized ( ) ,
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
self ? . present ( modal , animated : true )
2021-02-16 23:40:23 +01:00
}
return
}
dataSource . sourceFilename = fileName
2022-05-11 10:20:10 +02:00
2021-02-16 23:40:23 +01:00
// A l t h o u g h w e w a n t t o b e a b l e t o s e n d h i g h e r q u a l i t y a t t a c h m e n t s t h r o u g h t h e d o c u m e n t p i c k e r
// i t ' s m o r e i m p o r a n t t h a t w e e n s u r e t h e s e n t f o r m a t i s o n e a l l c l i e n t s c a n a c c e p t ( e . g . * n o t * q u i c k t i m e . m o v )
guard ! SignalAttachment . isInvalidVideo ( dataSource : dataSource , dataUTI : type ) else {
return showAttachmentApprovalDialogAfterProcessingVideo ( at : url , with : fileName )
}
2022-05-11 10:20:10 +02:00
2021-02-16 23:40:23 +01:00
// " D o c u m e n t p i c k e r " a t t a c h m e n t s _ S H O U L D N O T _ b e r e s i z e d
let attachment = SignalAttachment . attachment ( dataSource : dataSource , dataUTI : type , imageQuality : . original )
showAttachmentApprovalDialog ( for : [ attachment ] )
}
2021-02-19 00:50:18 +01:00
func showAttachmentApprovalDialog ( for attachments : [ SignalAttachment ] ) {
2022-05-15 06:39:21 +02:00
let navController = AttachmentApprovalViewController . wrappedInNavController (
2022-05-25 10:48:04 +02:00
threadId : self . viewModel . threadData . threadId ,
2022-05-15 06:39:21 +02:00
attachments : attachments ,
approvalDelegate : self
)
2023-06-23 09:54:29 +02:00
navController . modalPresentationStyle = . fullScreen
2022-05-15 06:39:21 +02:00
2021-02-16 23:40:23 +01:00
present ( navController , animated : true , completion : nil )
}
2021-02-19 00:50:18 +01:00
func showAttachmentApprovalDialogAfterProcessingVideo ( at url : URL , with fileName : String ) {
2021-02-16 23:40:23 +01:00
ModalActivityIndicatorViewController . present ( fromViewController : self , canCancel : true , message : nil ) { [ weak self ] modalActivityIndicator in
let dataSource = DataSourcePath . dataSource ( with : url , shouldDeleteOnDeallocation : false ) !
dataSource . sourceFilename = fileName
2022-05-11 10:20:10 +02:00
SignalAttachment
. compressVideoAsMp4 (
dataSource : dataSource ,
dataUTI : kUTTypeMPEG4 as String
)
2022-12-05 03:52:39 +01:00
. attachmentPublisher
. sinkUntilComplete (
receiveValue : { [ weak self ] attachment in
2022-12-05 07:39:40 +01:00
guard ! modalActivityIndicator . wasCancelled else { return }
2022-05-11 10:20:10 +02:00
2022-12-05 03:52:39 +01:00
modalActivityIndicator . dismiss {
guard ! attachment . hasError else {
2023-06-23 09:54:29 +02:00
self ? . showErrorAlert ( for : attachment )
2022-12-05 03:52:39 +01:00
return
}
self ? . showAttachmentApprovalDialog ( for : [ attachment ] )
}
2021-02-16 23:40:23 +01:00
}
2022-12-05 03:52:39 +01:00
)
2021-02-16 23:40:23 +01:00
}
}
2022-05-08 14:01:39 +02:00
// MARK: - I n p u t V i e w D e l e g a t e
2021-02-16 23:40:23 +01:00
2022-05-08 14:01:39 +02:00
// MARK: - - M e s s a g e S e n d i n g
2022-05-11 10:20:10 +02:00
2021-01-29 01:46:32 +01:00
func handleSendButtonTapped ( ) {
2023-06-23 09:54:29 +02:00
sendMessage (
text : snInputView . text . trimmingCharacters ( in : . whitespacesAndNewlines ) ,
linkPreviewDraft : snInputView . linkPreviewInfo ? . draft ,
quoteModel : snInputView . quoteDraftInfo ? . model
)
2021-02-16 23:40:23 +01:00
}
2023-06-23 09:54:29 +02:00
func sendMessage (
text : String ,
attachments : [ SignalAttachment ] = [ ] ,
linkPreviewDraft : LinkPreviewDraft ? = nil ,
quoteModel : QuotedReplyModel ? = nil ,
2023-08-01 06:27:41 +02:00
hasPermissionToSendSeed : Bool = false ,
using dependencies : Dependencies = Dependencies ( )
2023-06-23 09:54:29 +02:00
) {
2021-02-16 22:01:54 +01:00
guard ! showBlockedModalIfNeeded ( ) else { return }
2022-02-02 06:59:56 +01:00
2023-06-23 09:54:29 +02:00
// H a n d l e a t t a c h m e n t e r r o r s i f a p p l i c a b l e
if let failedAttachment : SignalAttachment = attachments . first ( where : { $0 . hasError } ) {
return showErrorAlert ( for : failedAttachment )
}
let processedText : String = replaceMentions ( in : text . trimmingCharacters ( in : . whitespacesAndNewlines ) )
// I f w e h a v e n o c o n t e n t t h e n d o n o t h i n g
guard ! processedText . isEmpty || ! attachments . isEmpty else { return }
2022-05-08 14:01:39 +02:00
2023-06-23 09:54:29 +02:00
if processedText . contains ( mnemonic ) && ! viewModel . threadData . threadIsNoteToSelf && ! hasPermissionToSendSeed {
2021-07-19 05:15:02 +02:00
// W a r n t h e u s e r i f t h e y ' r e a b o u t t o s e n d t h e i r s e e d t o s o m e o n e
2022-09-14 10:32:23 +02:00
let modal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " modal_send_seed_title " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " modal_send_seed_explanation " . localized ( ) ) ,
2022-09-14 10:32:23 +02:00
confirmTitle : " modal_send_seed_send_button_title " . localized ( ) ,
confirmStyle : . danger ,
2022-09-28 10:26:02 +02:00
cancelStyle : . alert_text ,
2023-06-23 09:54:29 +02:00
onConfirm : { [ weak self ] _ in
self ? . sendMessage (
text : text ,
attachments : attachments ,
linkPreviewDraft : linkPreviewDraft ,
quoteModel : quoteModel ,
hasPermissionToSendSeed : true
)
}
2022-09-14 10:32:23 +02:00
)
)
2021-07-19 05:15:02 +02:00
return present ( modal , animated : true , completion : nil )
}
2022-07-05 08:47:12 +02:00
2022-09-13 02:51:26 +02:00
// C l e a r i n g t h i s o u t i m m e d i a t e l y t o m a k e t h i s a p p e a r m o r e s n a p p y
2022-07-05 08:47:12 +02:00
DispatchQueue . main . async { [ weak self ] in
self ? . snInputView . text = " "
self ? . snInputView . quoteDraftInfo = nil
self ? . resetMentions ( )
2023-07-13 06:47:10 +02:00
self ? . scrollToBottom ( isAnimated : false )
2022-07-05 08:47:12 +02:00
}
2022-05-08 14:01:39 +02:00
2022-02-02 06:59:56 +01:00
// N o t e : ' s h o u l d B e V i s i b l e ' i s s e t t o t r u e t h e f i r s t t i m e a t h r e a d i s s a v e d s o w e c a n
// u s e i t t o d e t e r m i n e i f t h e u s e r i s c r e a t i n g a n e w t h r e a d a n d u p d a t e t h e ' i s A p p r o v e d '
// f l a g s a p p r o p r i a t e l y
2022-05-25 10:48:04 +02:00
let oldThreadShouldBeVisible : Bool = ( self . viewModel . threadData . threadShouldBeVisible = = true )
2022-12-21 06:39:52 +01:00
let sentTimestampMs : Int64 = SnodeAPI . currentOffsetTimestampMs ( )
2023-06-23 09:54:29 +02:00
2022-07-18 04:32:46 +02:00
// I f t h i s w a s a m e s s a g e r e q u e s t t h e n a p p r o v e i t
2022-05-08 14:01:39 +02:00
approveMessageRequestIfNeeded (
2023-07-20 04:29:56 +02:00
for : self . viewModel . threadData . threadId ,
threadVariant : self . viewModel . threadData . threadVariant ,
2022-03-24 05:46:53 +01:00
isNewThread : ! oldThreadShouldBeVisible ,
2022-05-08 14:01:39 +02:00
timestampMs : ( sentTimestampMs - 1 ) // S e t 1 m s e a r l i e r a s t h i s i s u s e d f o r s o r t i n g
2022-03-24 05:46:53 +01:00
)
2022-07-18 04:32:46 +02:00
2023-06-23 09:54:29 +02:00
// O p t i m i s t i c a l l y i n s e r t t h e o u t g o i n g m e s s a g e ( t h i s w i l l t r i g g e r a U I u p d a t e )
self . viewModel . sentMessageBeforeUpdate = true
let optimisticData : ConversationViewModel . OptimisticMessageData = self . viewModel . optimisticallyAppendOutgoingMessage (
text : processedText ,
sentTimestampMs : sentTimestampMs ,
attachments : attachments ,
linkPreviewDraft : linkPreviewDraft ,
quoteModel : quoteModel
)
2023-08-01 06:39:00 +02:00
sendMessage ( optimisticData : optimisticData , using : dependencies )
2023-07-20 04:29:56 +02:00
}
2023-08-01 06:39:00 +02:00
private func sendMessage (
optimisticData : ConversationViewModel . OptimisticMessageData ,
using dependencies : Dependencies
) {
2023-07-20 04:29:56 +02:00
let threadId : String = self . viewModel . threadData . threadId
let threadVariant : SessionThread . Variant = self . viewModel . threadData . threadVariant
2023-08-01 06:39:00 +02:00
DispatchQueue . global ( qos : . userInitiated ) . async ( using : dependencies ) {
2023-07-13 06:47:10 +02:00
// G e n e r a t e t h e q u o t e t h u m b n a i l i f n e e d e d ( w a n t t h i s t o h a p p e n o u t s i d e o f t h e D B W r i t e t h r e a d a s
// t h i s c a n t a k e u p t o 0 . 5 s
2023-07-20 04:29:56 +02:00
let quoteThumbnailAttachment : Attachment ? = optimisticData . quoteModel ? . attachment ? . cloneAsQuoteThumbnail ( )
2023-07-13 06:47:10 +02:00
// A c t u a l l y s e n d t h e m e s s a g e
2023-08-01 06:27:41 +02:00
dependencies . storage
2023-07-13 06:47:10 +02:00
. writePublisher { [ weak self ] db in
// U p d a t e t h e t h r e a d t o b e v i s i b l e ( i f i t i s n ' t a l r e a d y )
if self ? . viewModel . threadData . threadShouldBeVisible = = false {
_ = try SessionThread
. filter ( id : threadId )
. updateAllAndConfig ( db , SessionThread . Columns . shouldBeVisible . set ( to : true ) )
}
// I n s e r t t h e i n t e r a c t i o n a n d a s s o c i a t e d i t w i t h t h e o p t i m i s t i c a l l y i n s e r t e d m e s s a g e s o
// w e c a n r e m o v e i t o n c e t h e d a t a b a s e t r i g g e r s a U I u p d a t e
let insertedInteraction : Interaction = try optimisticData . interaction . inserted ( db )
self ? . viewModel . associate ( optimisticMessageId : optimisticData . id , to : insertedInteraction . id )
// I f t h e r e i s a L i n k P r e v i e w a n d i t d o e s n ' t m a t c h a n e x i s t i n g o n e t h e n a d d i t n o w
if
2023-07-20 04:29:56 +02:00
let linkPreviewDraft : LinkPreviewDraft = optimisticData . linkPreviewDraft ,
2023-07-13 06:47:10 +02:00
( try ? insertedInteraction . linkPreview . isEmpty ( db ) ) = = true
{
try LinkPreview (
url : linkPreviewDraft . urlString ,
title : linkPreviewDraft . title ,
attachmentId : try optimisticData . linkPreviewAttachment ? . inserted ( db ) . id
) . insert ( db )
}
// I f t h e r e i s a Q u o t e t h e i n s e r t i t n o w
2023-07-20 04:29:56 +02:00
if let interactionId : Int64 = insertedInteraction . id , let quoteModel : QuotedReplyModel = optimisticData . quoteModel {
2023-07-13 06:47:10 +02:00
try Quote (
interactionId : interactionId ,
authorId : quoteModel . authorId ,
timestampMs : quoteModel . timestampMs ,
body : quoteModel . body ,
attachmentId : try quoteThumbnailAttachment ? . inserted ( db ) . id
) . insert ( db )
}
// P r o c e s s a n y a t t a c h m e n t s
try Attachment . process (
db ,
data : optimisticData . attachmentData ,
for : insertedInteraction . id
)
try MessageSender . send (
db ,
interaction : insertedInteraction ,
threadId : threadId ,
2023-08-01 06:27:41 +02:00
threadVariant : threadVariant ,
using : dependencies
2023-07-13 06:47:10 +02:00
)
2022-07-18 04:32:46 +02:00
}
2023-07-13 06:47:10 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
. sinkUntilComplete (
2023-07-20 04:29:56 +02:00
receiveCompletion : { [ weak self ] result in
switch result {
case . finished : break
case . failure ( let error ) :
self ? . viewModel . failedToStoreOptimisticOutgoingMessage ( id : optimisticData . id , error : error )
}
2023-07-13 06:47:10 +02:00
self ? . handleMessageSent ( )
}
2023-03-08 07:27:07 +01:00
)
2023-07-13 06:47:10 +02:00
}
2021-02-16 22:01:54 +01:00
}
func handleMessageSent ( ) {
2022-07-01 05:08:45 +02:00
if Storage . shared [ . playNotificationSoundInForeground ] {
2022-05-08 14:01:39 +02:00
let soundID = Preferences . Sound . systemSoundId ( for : . messageSent , quiet : true )
2021-02-16 22:01:54 +01:00
AudioServicesPlaySystemSound ( soundID )
}
2022-05-11 10:20:10 +02:00
2022-05-25 10:48:04 +02:00
let threadId : String = self . viewModel . threadData . threadId
2022-05-11 10:20:10 +02:00
2022-07-01 05:08:45 +02:00
Storage . shared . writeAsync { db in
2022-05-25 10:48:04 +02:00
TypingIndicators . didStopTyping ( db , threadId : threadId , direction : . outgoing )
2022-05-11 10:20:10 +02:00
_ = try SessionThread
2022-05-25 10:48:04 +02:00
. filter ( id : threadId )
2022-05-11 10:20:10 +02:00
. updateAll ( db , SessionThread . Columns . messageDraft . set ( to : " " ) )
2021-03-01 23:33:31 +01:00
}
2021-02-16 22:01:54 +01:00
}
2021-02-17 05:57:07 +01:00
func showLinkPreviewSuggestionModal ( ) {
2022-08-31 09:44:44 +02:00
let linkPreviewModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " modal_link_previews_title " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " modal_link_previews_explanation " . localized ( ) ) ,
2022-08-31 09:44:44 +02:00
confirmTitle : " modal_link_previews_button_title " . localized ( )
) { [ weak self ] _ in
Storage . shared . writeAsync { db in
db [ . areLinkPreviewsEnabled ] = true
}
self ? . snInputView . autoGenerateLinkPreview ( )
}
)
present ( linkPreviewModal , animated : true , completion : nil )
2021-02-17 05:57:07 +01:00
}
2022-05-10 09:42:15 +02:00
func inputTextViewDidChangeContent ( _ inputTextView : InputTextView ) {
2023-04-12 08:50:35 +02:00
// N o t e : I f t h e r e i s a ' d r a f t ' m e s s a g e t h e n w e d o n ' t w a n t i t t o t r i g g e r t h e t y p i n g i n d i c a t o r t o
// a p p e a r ( a s t h a t i s n o t e x p e c t e d / c o r r e c t b e h a v i o u r )
guard ! viewIsAppearing else { return }
2022-05-10 09:42:15 +02:00
let newText : String = ( inputTextView . text ? ? " " )
if ! newText . isEmpty {
2022-05-25 10:48:04 +02:00
let threadId : String = self . viewModel . threadData . threadId
let threadVariant : SessionThread . Variant = self . viewModel . threadData . threadVariant
let threadIsMessageRequest : Bool = ( self . viewModel . threadData . threadIsMessageRequest = = true )
2023-03-17 05:12:35 +01:00
let threadIsBlocked : Bool = ( self . viewModel . threadData . threadIsBlocked = = true )
2022-08-05 09:10:01 +02:00
let needsToStartTypingIndicator : Bool = TypingIndicators . didStartTypingNeedsToStart (
threadId : threadId ,
threadVariant : threadVariant ,
2023-03-17 05:12:35 +01:00
threadIsBlocked : threadIsBlocked ,
2022-08-05 09:10:01 +02:00
threadIsMessageRequest : threadIsMessageRequest ,
direction : . outgoing ,
2022-12-21 06:39:52 +01:00
timestampMs : SnodeAPI . currentOffsetTimestampMs ( )
2022-08-05 09:10:01 +02:00
)
2022-05-11 10:20:10 +02:00
2022-08-05 09:10:01 +02:00
if needsToStartTypingIndicator {
Storage . shared . writeAsync { db in
TypingIndicators . start ( db , threadId : threadId , direction : . outgoing )
}
2022-05-11 10:20:10 +02:00
}
2022-05-10 09:42:15 +02:00
}
updateMentions ( for : newText )
}
// MARK: - - A t t a c h m e n t s
func didPasteImageFromPasteboard ( _ image : UIImage ) {
guard let imageData = image . jpegData ( compressionQuality : 1.0 ) else { return }
2022-05-11 10:20:10 +02:00
2022-05-10 09:42:15 +02:00
let dataSource = DataSourceValue . dataSource ( with : imageData , utiType : kUTTypeJPEG as String )
let attachment = SignalAttachment . attachment ( dataSource : dataSource , dataUTI : kUTTypeJPEG as String , imageQuality : . medium )
2021-02-17 05:57:07 +01:00
2022-05-15 06:39:21 +02:00
let approvalVC = AttachmentApprovalViewController . wrappedInNavController (
2022-05-25 10:48:04 +02:00
threadId : self . viewModel . threadData . threadId ,
2022-05-15 06:39:21 +02:00
attachments : [ attachment ] ,
approvalDelegate : self
)
2022-05-10 09:42:15 +02:00
approvalVC . modalPresentationStyle = . fullScreen
2022-05-11 10:20:10 +02:00
2022-05-10 09:42:15 +02:00
self . present ( approvalVC , animated : true , completion : nil )
}
// MARK: - - M e n t i o n s
2022-09-14 10:32:23 +02:00
func handleMentionSelected ( _ mentionInfo : MentionInfo , from view : MentionSelectionView ) {
2022-05-10 09:42:15 +02:00
guard let currentMentionStartIndex = currentMentionStartIndex else { return }
mentions . append ( mentionInfo )
let newText : String = snInputView . text . replacingCharacters (
in : currentMentionStartIndex . . . ,
2022-05-25 10:48:04 +02:00
with : " @ \( mentionInfo . profile . displayName ( for : self . viewModel . threadData . threadVariant ) ) "
2022-05-10 09:42:15 +02:00
)
snInputView . text = newText
self . currentMentionStartIndex = nil
snInputView . hideMentionsUI ( )
mentions = mentions . filter { mentionInfo -> Bool in
2022-05-25 10:48:04 +02:00
newText . contains ( mentionInfo . profile . displayName ( for : self . viewModel . threadData . threadVariant ) )
2021-02-17 04:26:43 +01:00
}
2022-05-10 09:42:15 +02:00
}
func updateMentions ( for newText : String ) {
2022-05-11 10:20:10 +02:00
guard ! newText . isEmpty else {
if currentMentionStartIndex != nil {
2021-02-17 04:26:43 +01:00
snInputView . hideMentionsUI ( )
2022-05-10 09:42:15 +02:00
}
2022-05-11 10:20:10 +02:00
resetMentions ( )
return
}
let lastCharacterIndex = newText . index ( before : newText . endIndex )
let lastCharacter = newText [ lastCharacterIndex ]
// C h e c k i f t h e r e i s w h i t e s p a c e b e f o r e t h e ' @ ' o r t h e ' @ ' i s t h e f i r s t c h a r a c t e r
let isCharacterBeforeLastWhiteSpaceOrStartOfLine : Bool
if newText . count = = 1 {
isCharacterBeforeLastWhiteSpaceOrStartOfLine = true // S t a r t o f l i n e
}
else {
let characterBeforeLast = newText [ newText . index ( before : lastCharacterIndex ) ]
isCharacterBeforeLastWhiteSpaceOrStartOfLine = characterBeforeLast . isWhitespace
}
if lastCharacter = = " @ " && isCharacterBeforeLastWhiteSpaceOrStartOfLine {
currentMentionStartIndex = lastCharacterIndex
snInputView . showMentionsUI ( for : self . viewModel . mentions ( ) )
}
else if lastCharacter . isWhitespace || lastCharacter = = " @ " { // t h e l a s t C h a r a c t e r = = " @ " i s t o c h e c k f o r @ @
currentMentionStartIndex = nil
snInputView . hideMentionsUI ( )
}
else {
if let currentMentionStartIndex = currentMentionStartIndex {
let query = String ( newText [ newText . index ( after : currentMentionStartIndex ) . . . ] ) // + 1 t o g e t r i d o f t h e @
snInputView . showMentionsUI ( for : self . viewModel . mentions ( for : query ) )
2021-02-17 04:26:43 +01:00
}
}
}
2021-02-19 00:50:18 +01:00
func resetMentions ( ) {
2021-02-17 04:26:43 +01:00
currentMentionStartIndex = nil
mentions = [ ]
}
2021-02-19 00:50:18 +01:00
func replaceMentions ( in text : String ) -> String {
2021-02-17 04:26:43 +01:00
var result = text
for mention in mentions {
2022-05-10 09:42:15 +02:00
guard let range = result . range ( of : " @ \( mention . profile . displayName ( for : mention . threadVariant ) ) " ) else { continue }
result = result . replacingCharacters ( in : range , with : " @ \( mention . profile . id ) " )
2021-02-17 04:26:43 +01:00
}
2022-05-10 09:42:15 +02:00
2021-02-17 04:26:43 +01:00
return result
2021-08-31 02:28:45 +02:00
}
2022-06-14 07:11:17 +02:00
func hideInputAccessoryView ( ) {
self . inputAccessoryView ? . isHidden = true
self . inputAccessoryView ? . alpha = 0
}
2022-05-11 10:20:10 +02:00
func showInputAccessoryView ( ) {
UIView . animate ( withDuration : 0.25 , animations : {
self . inputAccessoryView ? . isHidden = false
self . inputAccessoryView ? . alpha = 1
} )
}
// MARK: M e s s a g e C e l l D e l e g a t e
2022-05-29 11:26:06 +02:00
func handleItemLongPressed ( _ cellViewModel : MessageViewModel ) {
2021-03-02 00:18:08 +01:00
// S h o w t h e c o n t e x t m e n u i f a p p l i c a b l e
2022-05-11 10:20:10 +02:00
guard
2022-07-01 05:33:00 +02:00
// FIXME: N e e d t o u p d a t e t h i s w h e n a n a p p r o p r i a t e r e p l a c e m e n t i s a d d e d ( s e e h t t p s : / / t e n g . p u b / t e c h n i c a l / 2 0 2 1 / 1 1 / 9 / u i a p p l i c a t i o n - k e y - w i n d o w - r e p l a c e m e n t )
2022-05-11 10:20:10 +02:00
let keyWindow : UIWindow = UIApplication . shared . keyWindow ,
2022-05-26 10:13:16 +02:00
let sectionIndex : Int = self . viewModel . interactionData
. firstIndex ( where : { $0 . model = = . messages } ) ,
let index = self . viewModel . interactionData [ sectionIndex ]
. elements
. firstIndex ( of : cellViewModel ) ,
2022-10-05 09:44:25 +02:00
let cell = tableView . cellForRow ( at : IndexPath ( row : index , section : sectionIndex ) ) as ? MessageCell ,
let contextSnapshotView : UIView = cell . contextSnapshotView ,
let snapshot = contextSnapshotView . snapshotView ( afterScreenUpdates : false ) ,
2022-05-11 10:20:10 +02:00
contextMenuWindow = = nil ,
let actions : [ ContextMenuVC . Action ] = ContextMenuVC . actions (
2022-05-25 10:48:04 +02:00
for : cellViewModel ,
2022-08-18 08:13:20 +02:00
recentEmojis : ( self . viewModel . threadData . recentReactionEmoji ? ? [ ] ) . compactMap { EmojiWithSkinTones ( rawValue : $0 ) } ,
2023-02-06 02:51:34 +01:00
currentUserPublicKey : self . viewModel . threadData . currentUserPublicKey ,
2023-07-14 06:36:59 +02:00
currentUserBlinded15PublicKey : self . viewModel . threadData . currentUserBlinded15PublicKey ,
currentUserBlinded25PublicKey : self . viewModel . threadData . currentUserBlinded25PublicKey ,
2022-06-09 10:37:44 +02:00
currentUserIsOpenGroupModerator : OpenGroupManager . isUserModeratorOrAdmin (
2022-05-25 10:48:04 +02:00
self . viewModel . threadData . currentUserPublicKey ,
2022-06-09 10:37:44 +02:00
for : self . viewModel . threadData . openGroupRoomToken ,
2022-05-25 10:48:04 +02:00
on : self . viewModel . threadData . openGroupServer
2022-05-11 10:20:10 +02:00
) ,
2022-08-18 08:13:20 +02:00
currentThreadIsMessageRequest : ( self . viewModel . threadData . threadIsMessageRequest = = true ) ,
2022-05-11 10:20:10 +02:00
delegate : self
)
else { return }
2022-08-31 09:44:44 +02:00
// / L o c k t h e c o n t e n t O f f s e t o f t h e t a b l e V i e w s o t h e t r a n s i t i o n d o e s n ' t l o o k b u g g y
self . tableView . lockContentOffset = true
2021-01-29 01:46:32 +01:00
UIImpactFeedbackGenerator ( style : . heavy ) . impactOccurred ( )
2022-05-11 10:20:10 +02:00
self . contextMenuWindow = ContextMenuWindow ( )
self . contextMenuVC = ContextMenuVC (
snapshot : snapshot ,
2022-10-05 09:44:25 +02:00
frame : contextSnapshotView . convert ( contextSnapshotView . bounds , to : keyWindow ) ,
2022-05-25 10:48:04 +02:00
cellViewModel : cellViewModel ,
2022-05-11 10:20:10 +02:00
actions : actions
) { [ weak self ] in
self ? . contextMenuWindow ? . isHidden = true
self ? . contextMenuVC = nil
self ? . contextMenuWindow = nil
self ? . scrollButton . alpha = 0
2022-08-31 09:44:44 +02:00
UIView . animate (
withDuration : 0.25 ,
2023-06-19 10:19:47 +02:00
animations : { self ? . updateScrollToBottom ( ) } ,
2022-08-31 09:44:44 +02:00
completion : { _ in
guard let contentOffset : CGPoint = self ? . tableView . contentOffset else { return }
// U n l o c k t h e c o n t e n t O f f s e t s o e v e r y t h i n g w i l l b e i n t h e r i g h t
// p l a c e w h e n w e r e t u r n
self ? . tableView . lockContentOffset = false
self ? . tableView . setContentOffset ( contentOffset , animated : false )
}
)
2021-01-29 01:46:32 +01:00
}
2022-05-11 10:20:10 +02:00
2022-09-20 08:06:01 +02:00
self . contextMenuWindow ? . themeBackgroundColor = . clear
2022-05-11 10:20:10 +02:00
self . contextMenuWindow ? . rootViewController = self . contextMenuVC
2022-09-27 07:36:47 +02:00
self . contextMenuWindow ? . overrideUserInterfaceStyle = ThemeManager . currentTheme . interfaceStyle
2022-05-11 10:20:10 +02:00
self . contextMenuWindow ? . makeKeyAndVisible ( )
2021-01-29 01:46:32 +01:00
}
2023-08-01 06:27:41 +02:00
func handleItemTapped (
_ cellViewModel : MessageViewModel ,
gestureRecognizer : UITapGestureRecognizer ,
using dependencies : Dependencies = Dependencies ( )
) {
2023-02-14 03:41:24 +01:00
guard cellViewModel . variant != . standardOutgoing || ( cellViewModel . state != . failed && cellViewModel . state != . failedToSync ) else {
2022-05-11 10:20:10 +02:00
// S h o w t h e f a i l e d m e s s a g e s h e e t
2023-08-01 06:27:41 +02:00
showFailedMessageSheet ( for : cellViewModel , using : dependencies )
2022-05-11 10:20:10 +02:00
return
}
2022-06-09 10:37:44 +02:00
// F o r c a l l i n f o m e s s a g e s s h o w t h e " c a l l m i s s e d " m o d a l
guard cellViewModel . variant != . infoCall else {
let callMissedTipsModal : CallMissedTipsModal = CallMissedTipsModal ( caller : cellViewModel . authorName )
present ( callMissedTipsModal , animated : true , completion : nil )
return
}
2022-05-11 10:20:10 +02:00
// I f i t ' s a n i n c o m i n g m e d i a m e s s a g e a n d t h e t h r e a d i s n ' t t r u s t e d t h e n s h o w t h e p l a c e h o l d e r v i e w
2022-05-25 10:48:04 +02:00
if cellViewModel . cellType != . textOnlyMessage && cellViewModel . variant = = . standardIncoming && ! cellViewModel . threadIsTrusted {
2022-09-02 09:32:13 +02:00
let message : String = String (
format : " modal_download_attachment_explanation " . localized ( ) ,
cellViewModel . authorName
)
let confirmationModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : String (
format : " modal_download_attachment_title " . localized ( ) ,
cellViewModel . authorName
) ,
2023-05-16 05:09:05 +02:00
body : . attributedText (
NSAttributedString ( string : message )
. adding (
attributes : [ . font : UIFont . boldSystemFont ( ofSize : Values . smallFontSize ) ] ,
range : ( message as NSString ) . range ( of : cellViewModel . authorName )
)
) ,
2022-09-02 09:32:13 +02:00
confirmTitle : " modal_download_button_title " . localized ( ) ,
2023-04-26 07:13:39 +02:00
confirmAccessibility : Accessibility ( identifier : " Download media " ) ,
cancelAccessibility : Accessibility ( identifier : " Don't download media " ) ,
2022-09-02 09:32:13 +02:00
dismissOnConfirm : false // C u s t o m d i s m i s s a l l o g i c
) { [ weak self ] _ in
self ? . viewModel . trustContact ( )
self ? . dismiss ( animated : true , completion : nil )
}
)
2022-05-11 10:20:10 +02:00
2022-09-02 09:32:13 +02:00
present ( confirmationModal , animated : true , completion : nil )
2022-05-11 10:20:10 +02:00
return
2021-04-08 07:32:36 +02:00
}
2022-05-11 10:20:10 +02:00
2022-05-25 10:48:04 +02:00
switch cellViewModel . cellType {
case . audio : viewModel . playOrPauseAudio ( for : cellViewModel )
2022-05-11 10:20:10 +02:00
2021-02-15 05:42:16 +01:00
case . mediaMessage :
2022-05-13 10:07:24 +02:00
guard
2022-05-26 10:13:16 +02:00
let sectionIndex : Int = self . viewModel . interactionData
. firstIndex ( where : { $0 . model = = . messages } ) ,
let messageIndex : Int = self . viewModel . interactionData [ sectionIndex ]
. elements
. firstIndex ( where : { $0 . id = = cellViewModel . id } ) ,
let cell = tableView . cellForRow ( at : IndexPath ( row : messageIndex , section : sectionIndex ) ) as ? VisibleMessageCell ,
2022-05-13 10:07:24 +02:00
let albumView : MediaAlbumView = cell . albumView
else { return }
let locationInCell : CGPoint = gestureRecognizer . location ( in : cell )
// F i g u r e o u t w h i c h o f t h e m e d i a v i e w s w a s t a p p e d
let locationInAlbumView : CGPoint = cell . convert ( locationInCell , to : albumView )
guard let mediaView = albumView . mediaView ( forLocation : locationInAlbumView ) else { return }
switch mediaView . attachment . state {
2022-07-01 04:52:41 +02:00
case . pendingDownload , . downloading , . uploading , . invalid : break
2022-06-03 07:47:16 +02:00
// F a i l e d u p l o a d s s h o u l d b e h a n d l e d v i a t h e " r e s e n d " p r o c e s s i n s t e a d
case . failedUpload : break
2022-05-13 10:07:24 +02:00
2022-06-03 07:47:16 +02:00
case . failedDownload :
let threadId : String = self . viewModel . threadData . threadId
// R e t r y d o w n l o a d i n g t h e f a i l e d a t t a c h m e n t
2023-08-01 06:27:41 +02:00
dependencies . storage . writeAsync { db in
dependencies . jobRunner . add (
2022-06-03 07:47:16 +02:00
db ,
job : Job (
variant : . attachmentDownload ,
threadId : threadId ,
interactionId : cellViewModel . id ,
details : AttachmentDownloadJob . Details (
attachmentId : mediaView . attachment . id
)
2023-08-01 06:27:41 +02:00
) ,
canStartJob : true ,
using : dependencies
2022-06-03 07:47:16 +02:00
)
}
2022-05-13 10:07:24 +02:00
break
default :
2022-06-03 07:47:16 +02:00
// I g n o r e i n v a l i d m e d i a
guard mediaView . attachment . isValid else { return }
2022-05-13 10:07:24 +02:00
let viewController : UIViewController ? = MediaGalleryViewModel . createDetailViewController (
2022-05-25 10:48:04 +02:00
for : self . viewModel . threadData . threadId ,
threadVariant : self . viewModel . threadData . threadVariant ,
interactionId : cellViewModel . id ,
2022-05-13 10:07:24 +02:00
selectedAttachmentId : mediaView . attachment . id ,
options : [ . sliderEnabled , . showAllMediaButton ]
)
if let viewController : UIViewController = viewController {
2022-05-20 09:58:39 +02:00
// / D e l a y b e c o m i n g t h e f i r s t r e s p o n d e r t o m a k e t h e r e t u r n t r a n s i t i o n a l i t t l e n i c e r ( a l l o w s
// / f o r t h e f o o t e r o n t h e d e t a i l v i e w t o s l i d e o u t r a t h e r t h a n i n s t a n t l y v a n i s h )
self . delayFirstResponder = true
// / D i s m i s s t h e i n p u t b e f o r e s t a r t i n g t h e p r e s e n t a t i o n t o m a k e e v e r y t h i n g l o o k s m o o t h e r
self . resignFirstResponder ( )
// / D e l a y t h e a c t u a l p r e s e n t a t i o n t o g i v e t h e ' r e s i g n F i r s t R e s p o n d e r ' c a l l t h e c h a n c e t o c o m p l e t e
2022-05-25 10:48:04 +02:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + . milliseconds ( 250 ) ) { [ weak self ] in
2022-05-20 09:58:39 +02:00
// / L o c k t h e c o n t e n t O f f s e t o f t h e t a b l e V i e w s o t h e t r a n s i t i o n d o e s n ' t l o o k b u g g y
self ? . tableView . lockContentOffset = true
self ? . present ( viewController , animated : true ) { [ weak self ] in
// U n l o c k t h e c o n t e n t O f f s e t s o e v e r y t h i n g w i l l b e i n t h e r i g h t
// p l a c e w h e n w e r e t u r n
self ? . tableView . lockContentOffset = false
}
}
2021-03-05 01:48:31 +01:00
}
2021-02-15 05:42:16 +01:00
}
2022-05-13 10:07:24 +02:00
2021-02-15 05:42:16 +01:00
case . genericAttachment :
2022-05-11 10:20:10 +02:00
guard
2022-05-25 10:48:04 +02:00
let attachment : Attachment = cellViewModel . attachments ? . first ,
2022-05-11 10:20:10 +02:00
let originalFilePath : String = attachment . originalFilePath
else { return }
let fileUrl : URL = URL ( fileURLWithPath : originalFilePath )
// O p e n a p r e v i e w o f t h e d o c u m e n t f o r t e x t , p d f o r m i c r o s o f t f i l e s
2022-04-06 07:43:26 +02:00
if
2022-05-11 10:20:10 +02:00
attachment . isText ||
attachment . isMicrosoftDoc ||
attachment . contentType = = OWSMimeTypeApplicationPdf
{
2022-01-28 06:24:18 +01:00
let interactionController : UIDocumentInteractionController = UIDocumentInteractionController ( url : fileUrl )
interactionController . delegate = self
interactionController . presentPreview ( animated : true )
2022-05-11 10:20:10 +02:00
return
2022-01-28 06:24:18 +01:00
}
2022-05-11 10:20:10 +02:00
// O t h e r w i s e s h a r e t h e f i l e
let shareVC = UIActivityViewController ( activityItems : [ fileUrl ] , applicationActivities : nil )
2022-06-08 06:29:51 +02:00
if UIDevice . current . isIPad {
shareVC . excludedActivityTypes = [ ]
shareVC . popoverPresentationController ? . permittedArrowDirections = [ ]
shareVC . popoverPresentationController ? . sourceView = self . view
shareVC . popoverPresentationController ? . sourceRect = self . view . bounds
2021-04-08 07:32:36 +02:00
}
2022-06-08 06:29:51 +02:00
2022-05-11 10:20:10 +02:00
navigationController ? . present ( shareVC , animated : true , completion : nil )
2022-05-23 09:16:14 +02:00
2021-02-15 05:42:16 +01:00
case . textOnlyMessage :
2022-05-28 09:25:38 +02:00
if let quote : Quote = cellViewModel . quote {
// S c r o l l t o t h e o r i g i n a l q u o t e d m e s s a g e
2023-02-01 08:12:36 +01:00
let maybeOriginalInteractionInfo : Interaction . TimestampInfo ? = Storage . shared . read { db in
2022-05-28 09:25:38 +02:00
try quote . originalInteraction
2023-02-01 08:12:36 +01:00
. select ( . id , . timestampMs )
. asRequest ( of : Interaction . TimestampInfo . self )
2022-05-28 09:25:38 +02:00
. fetchOne ( db )
}
2023-02-01 08:12:36 +01:00
guard let interactionInfo : Interaction . TimestampInfo = maybeOriginalInteractionInfo else {
return
}
2022-05-28 09:25:38 +02:00
2023-06-19 10:19:47 +02:00
self . scrollToInteractionIfNeeded ( with : interactionInfo , focusBehaviour : . highlight )
2021-02-19 04:43:49 +01:00
}
2022-05-28 09:25:38 +02:00
else if let linkPreview : LinkPreview = cellViewModel . linkPreview {
switch linkPreview . variant {
case . standard : openUrl ( linkPreview . url )
case . openGroupInvitation : joinOpenGroup ( name : linkPreview . title , url : linkPreview . url )
}
}
2021-02-15 05:42:16 +01:00
default : break
2021-01-29 01:46:32 +01:00
}
}
2021-08-08 09:57:48 +02:00
2022-05-29 11:26:06 +02:00
func handleItemDoubleTapped ( _ cellViewModel : MessageViewModel ) {
2022-05-25 10:48:04 +02:00
switch cellViewModel . cellType {
2022-05-11 10:20:10 +02:00
// T h e u s e r c a n d o u b l e t a p a v o i c e m e s s a g e w h e n i t ' s p l a y i n g t o s p e e d i t u p
2022-05-25 10:48:04 +02:00
case . audio : self . viewModel . speedUpAudio ( for : cellViewModel )
2022-05-11 10:20:10 +02:00
default : break
2021-08-08 09:57:48 +02:00
}
}
2021-01-29 01:46:32 +01:00
2022-05-29 11:26:06 +02:00
func handleItemSwiped ( _ cellViewModel : MessageViewModel , state : SwipeState ) {
2022-05-12 09:28:27 +02:00
switch state {
case . began : tableView . isScrollEnabled = false
case . ended , . cancelled : tableView . isScrollEnabled = true
}
}
func openUrl ( _ urlString : String ) {
guard let url : URL = URL ( string : urlString ) else { return }
// U R L s c a n b e u n s a f e , s o a l w a y s a s k t h e u s e r w h e t h e r t h e y w a n t t o o p e n o n e
2022-09-28 10:26:02 +02:00
let actionSheet : UIAlertController = UIAlertController (
2022-05-12 09:28:27 +02:00
title : " modal_open_url_title " . localized ( ) ,
message : String ( format : " modal_open_url_explanation " . localized ( ) , url . absoluteString ) ,
preferredStyle : . actionSheet
)
2022-09-28 10:26:02 +02:00
actionSheet . addAction ( UIAlertAction ( title : " modal_open_url_button_title " . localized ( ) , style : . default ) { [ weak self ] _ in
2022-05-12 09:28:27 +02:00
UIApplication . shared . open ( url , options : [ : ] , completionHandler : nil )
self ? . showInputAccessoryView ( )
} )
2022-09-28 10:26:02 +02:00
actionSheet . addAction ( UIAlertAction ( title : " modal_copy_url_button_title " . localized ( ) , style : . default ) { [ weak self ] _ in
2022-05-12 09:28:27 +02:00
UIPasteboard . general . string = url . absoluteString
self ? . showInputAccessoryView ( )
} )
2022-09-28 10:26:02 +02:00
actionSheet . addAction ( UIAlertAction ( title : " cancel " . localized ( ) , style : . cancel ) { [ weak self ] _ in
2022-05-12 09:28:27 +02:00
self ? . showInputAccessoryView ( )
} )
2022-09-28 10:26:02 +02:00
Modal . setupForIPadIfNeeded ( actionSheet , targetView : self . view )
self . present ( actionSheet , animated : true )
2022-05-12 09:28:27 +02:00
}
2023-08-01 06:27:41 +02:00
func handleReplyButtonTapped ( for cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
reply ( cellViewModel , using : dependencies )
2022-05-12 09:28:27 +02:00
}
2022-06-09 10:37:44 +02:00
func startThread ( with sessionId : String , openGroupServer : String ? , openGroupPublicKey : String ? ) {
2022-09-15 07:50:15 +02:00
guard viewModel . threadData . canWrite else { return }
2023-07-14 08:48:53 +02:00
// FIXME: A d d i n s u p p o r t f o r s t a r t i n g a t h r e a d w i t h a ' b l i n d e d 2 5 ' i d
guard SessionId . Prefix ( from : sessionId ) != . blinded25 else { return }
guard SessionId . Prefix ( from : sessionId ) = = . blinded15 else {
2022-07-01 05:08:45 +02:00
Storage . shared . write { db in
2023-03-02 07:52:37 +01:00
try SessionThread
. fetchOrCreate ( db , id : sessionId , variant : . contact , shouldBeVisible : nil )
2022-06-09 10:37:44 +02:00
}
2022-06-08 06:29:51 +02:00
2022-06-09 10:37:44 +02:00
let conversationVC : ConversationVC = ConversationVC ( threadId : sessionId , threadVariant : . contact )
2022-06-08 06:29:51 +02:00
self . navigationController ? . pushViewController ( conversationVC , animated : true )
return
}
2022-06-09 10:37:44 +02:00
// I f t h e s e s s i o n I d i s b l i n d e d t h e n c h e c k i f t h e r e i s a n e x i s t i n g u n - b l i n d e d t h r e a d w i t h t h e c o n t a c t
// a n d u s e t h a t , o t h e r w i s e j u s t u s e t h e b l i n d e d i d
guard let openGroupServer : String = openGroupServer , let openGroupPublicKey : String = openGroupPublicKey else {
return
}
2022-06-08 06:29:51 +02:00
2022-07-01 05:08:45 +02:00
let targetThreadId : String ? = Storage . shared . write { db in
2022-06-09 10:37:44 +02:00
let lookup : BlindedIdLookup = try BlindedIdLookup
. fetchOrCreate (
db ,
blindedId : sessionId ,
openGroupServer : openGroupServer ,
2022-06-29 10:10:10 +02:00
openGroupPublicKey : openGroupPublicKey ,
isCheckingForOutbox : false
2022-06-09 10:37:44 +02:00
)
return try SessionThread
2023-03-02 07:52:37 +01:00
. fetchOrCreate (
db ,
id : ( lookup . sessionId ? ? lookup . blindedId ) ,
variant : . contact ,
shouldBeVisible : nil
)
2022-06-09 10:37:44 +02:00
. id
}
guard let threadId : String = targetThreadId else { return }
let conversationVC : ConversationVC = ConversationVC ( threadId : threadId , threadVariant : . contact )
2022-06-08 06:29:51 +02:00
self . navigationController ? . pushViewController ( conversationVC , animated : true )
}
2022-05-12 09:28:27 +02:00
2022-07-25 07:39:56 +02:00
func showReactionList ( _ cellViewModel : MessageViewModel , selectedReaction : EmojiWithSkinTones ? ) {
guard
cellViewModel . reactionInfo ? . isEmpty = = false &&
(
2023-02-20 02:56:48 +01:00
self . viewModel . threadData . threadVariant = = . legacyGroup ||
self . viewModel . threadData . threadVariant = = . group ||
self . viewModel . threadData . threadVariant = = . community
2022-07-25 07:39:56 +02:00
) ,
let allMessages : [ MessageViewModel ] = self . viewModel . interactionData
. first ( where : { $0 . model = = . messages } ) ?
. elements
else { return }
let reactionListSheet : ReactionListSheet = ReactionListSheet ( for : cellViewModel . id ) { [ weak self ] in
self ? . currentReactionListSheet = nil
2021-08-02 03:32:46 +02:00
}
2022-06-03 06:44:31 +02:00
reactionListSheet . delegate = self
2022-07-25 07:39:56 +02:00
reactionListSheet . handleInteractionUpdates (
allMessages ,
selectedReaction : selectedReaction ,
2022-08-18 06:37:33 +02:00
initialLoad : true ,
shouldShowClearAllButton : OpenGroupManager . isUserModeratorOrAdmin (
self . viewModel . threadData . currentUserPublicKey ,
for : self . viewModel . threadData . openGroupRoomToken ,
on : self . viewModel . threadData . openGroupServer
)
2022-07-25 07:39:56 +02:00
)
2022-06-01 09:06:02 +02:00
reactionListSheet . modalPresentationStyle = . overFullScreen
present ( reactionListSheet , animated : true , completion : nil )
2022-07-25 07:39:56 +02:00
// S t o r e s o w e c a n u p d a t e d t h e c o n t e n t b a s e d o n t h e c u r r e n t V C
self . currentReactionListSheet = reactionListSheet
2022-06-01 09:06:02 +02:00
}
2022-07-25 07:39:56 +02:00
func needsLayout ( for cellViewModel : MessageViewModel , expandingReactions : Bool ) {
guard
let messageSectionIndex : Int = self . viewModel . interactionData
. firstIndex ( where : { $0 . model = = . messages } ) ,
let targetMessageIndex = self . viewModel . interactionData [ messageSectionIndex ]
. elements
. firstIndex ( where : { $0 . id = = cellViewModel . id } )
else { return }
if expandingReactions {
self . viewModel . expandReactions ( for : cellViewModel . id )
2022-06-20 07:31:54 +02:00
}
2022-07-25 07:39:56 +02:00
else {
self . viewModel . collapseReactions ( for : cellViewModel . id )
}
UIView . setAnimationsEnabled ( false )
tableView . reloadRows (
at : [ IndexPath ( row : targetMessageIndex , section : messageSectionIndex ) ] ,
with : . none
)
UIView . setAnimationsEnabled ( true )
2022-05-19 09:10:53 +02:00
}
2023-08-01 06:27:41 +02:00
func react ( _ cellViewModel : MessageViewModel , with emoji : EmojiWithSkinTones , using dependencies : Dependencies ) {
react ( cellViewModel , with : emoji . rawValue , remove : false , using : dependencies )
2022-05-19 09:10:53 +02:00
}
2023-08-01 06:27:41 +02:00
func removeReact ( _ cellViewModel : MessageViewModel , for emoji : EmojiWithSkinTones , using dependencies : Dependencies ) {
react ( cellViewModel , with : emoji . rawValue , remove : true , using : dependencies )
2022-05-19 09:10:53 +02:00
}
2023-08-01 06:27:41 +02:00
func removeAllReactions ( _ cellViewModel : MessageViewModel , for emoji : String , using dependencies : Dependencies ) {
2023-02-20 02:56:48 +01:00
guard cellViewModel . threadVariant = = . community else { return }
2022-07-25 07:39:56 +02:00
Storage . shared
2023-06-23 09:54:29 +02:00
. readPublisher { db -> ( OpenGroupAPI . PreparedSendData < OpenGroupAPI . ReactionRemoveAllResponse > , OpenGroupAPI . PendingChange ) in
2022-07-28 05:06:29 +02:00
guard
let openGroup : OpenGroup = try ? OpenGroup
. fetchOne ( db , id : cellViewModel . threadId ) ,
let openGroupServerMessageId : Int64 = try ? Interaction
. select ( . openGroupServerMessageId )
. filter ( id : cellViewModel . id )
. asRequest ( of : Int64 . self )
. fetchOne ( db )
2023-02-20 02:56:48 +01:00
else { throw StorageError . objectNotFound }
2022-07-25 07:39:56 +02:00
2023-06-23 09:54:29 +02:00
let sendData : OpenGroupAPI . PreparedSendData < OpenGroupAPI . ReactionRemoveAllResponse > = try OpenGroupAPI
. preparedReactionDeleteAll (
db ,
emoji : emoji ,
id : openGroupServerMessageId ,
in : openGroup . roomToken ,
on : openGroup . server
)
2022-11-27 22:32:32 +01:00
let pendingChange : OpenGroupAPI . PendingChange = OpenGroupManager
2022-08-29 07:58:49 +02:00
. addPendingReaction (
emoji : emoji ,
id : openGroupServerMessageId ,
in : openGroup . roomToken ,
on : openGroup . server ,
type : . removeAll
)
2023-06-23 09:54:29 +02:00
return ( sendData , pendingChange )
}
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
. flatMap { sendData , pendingChange in
OpenGroupAPI . send ( data : sendData )
. handleEvents (
receiveOutput : { _ , response in
OpenGroupManager
. updatePendingChange (
pendingChange ,
seqNo : response . seqNo
)
}
2022-07-28 05:06:29 +02:00
)
2022-11-27 22:32:32 +01:00
. eraseToAnyPublisher ( )
2022-07-25 07:39:56 +02:00
}
2022-11-27 22:32:32 +01:00
. sinkUntilComplete (
receiveCompletion : { _ in
Storage . shared . writeAsync { db in
_ = try Reaction
. filter ( Reaction . Columns . interactionId = = cellViewModel . id )
. filter ( Reaction . Columns . emoji = = emoji )
. deleteAll ( db )
}
}
)
2022-06-03 06:44:31 +02:00
}
2023-06-28 10:03:40 +02:00
func react (
_ cellViewModel : MessageViewModel ,
with emoji : String ,
remove : Bool ,
using dependencies : Dependencies = Dependencies ( )
) {
2023-06-23 09:54:29 +02:00
guard
self . viewModel . threadData . threadIsMessageRequest != true && (
cellViewModel . variant = = . standardIncoming ||
cellViewModel . variant = = . standardOutgoing
)
else { return }
2022-08-18 03:16:01 +02:00
2022-07-25 07:39:56 +02:00
// P e r f o r m l o c a l r a t e l i m i t i n g ( d o n ' t a l l o w m o r e t h a n 2 0 r e a c t i o n s w i t h i n 6 0 s e c o n d s )
2023-06-23 09:54:29 +02:00
let threadVariant : SessionThread . Variant = self . viewModel . threadData . threadVariant
let openGroupRoom : String ? = self . viewModel . threadData . openGroupRoomToken
2022-12-21 06:39:52 +01:00
let sentTimestamp : Int64 = SnodeAPI . currentOffsetTimestampMs ( )
2023-08-01 06:27:41 +02:00
let recentReactionTimestamps : [ Int64 ] = dependencies . caches [ . general ] . recentReactionTimestamps
2022-07-25 07:39:56 +02:00
guard
recentReactionTimestamps . count < 20 ||
( sentTimestamp - ( recentReactionTimestamps . first ? ? sentTimestamp ) ) > ( 60 * 1000 )
2022-10-05 09:44:25 +02:00
else {
let toastController : ToastController = ToastController (
text : " EMOJI_REACTS_RATE_LIMIT_TOAST " . localized ( ) ,
background : . backgroundSecondary
)
toastController . presentToastView (
fromBottomOfView : self . view ,
inset : ( snInputView . bounds . height + Values . largeSpacing ) ,
duration : . milliseconds ( 2500 )
)
return
}
2022-07-25 07:39:56 +02:00
2023-08-01 06:27:41 +02:00
dependencies . caches . mutate ( cache : . general ) {
2022-07-25 07:39:56 +02:00
$0 . recentReactionTimestamps = Array ( $0 . recentReactionTimestamps
. suffix ( 19 ) )
. appending ( sentTimestamp )
2022-06-22 05:51:36 +02:00
}
2023-03-06 05:20:15 +01:00
2023-06-23 09:54:29 +02:00
typealias OpenGroupInfo = (
pendingReaction : Reaction ? ,
pendingChange : OpenGroupAPI . PendingChange ,
sendData : OpenGroupAPI . PreparedSendData < Int64 ? >
)
// / P e r f o r m t h e s e n d i n g l o g i c , w e g e n e r a t e t h e p e n d i n g r e a c t i o n f i r s t i n a d e f e r r e d f u t u r e c l o s u r e t o p r e v e n t t h e O p e n G r o u p
// / c a c h e f r o m b l o c k i n g e i t h e r t h e m a i n t h r e a d o r t h e d a t a b a s e w r i t e t h r e a d
Deferred {
Future < OpenGroupAPI . PendingChange ? , Error > { resolver in
guard
threadVariant = = . community ,
let serverMessageId : Int64 = cellViewModel . openGroupServerMessageId ,
let openGroupServer : String = cellViewModel . threadOpenGroupServer ,
let openGroupPublicKey : String = cellViewModel . threadOpenGroupPublicKey
else { return resolver ( Result . success ( nil ) ) }
// C r e a t e t h e p e n d i n g c h a n g e i f w e h a v e o p e n g r o u p i n f o
return resolver ( Result . success (
OpenGroupManager . addPendingReaction (
emoji : emoji ,
id : serverMessageId ,
in : openGroupServer ,
on : openGroupPublicKey ,
type : ( remove ? . remove : . add )
)
) )
}
}
2023-08-01 06:27:41 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) , using : dependencies )
2023-06-23 09:54:29 +02:00
. flatMap { pendingChange -> AnyPublisher < ( MessageSender . PreparedSendData ? , OpenGroupInfo ? ) , Error > in
2023-08-01 06:27:41 +02:00
dependencies . storage . writePublisher { [ weak self ] db -> ( MessageSender . PreparedSendData ? , OpenGroupInfo ? ) in
2023-03-06 05:20:15 +01:00
// U p d a t e t h e t h r e a d t o b e v i s i b l e ( i f i t i s n ' t a l r e a d y )
2023-03-08 07:27:07 +01:00
if self ? . viewModel . threadData . threadShouldBeVisible = = false {
2023-03-06 05:20:15 +01:00
_ = try SessionThread
2023-03-08 07:27:07 +01:00
. filter ( id : cellViewModel . threadId )
2023-03-06 05:20:15 +01:00
. updateAllAndConfig ( db , SessionThread . Columns . shouldBeVisible . set ( to : true ) )
}
2022-07-25 07:39:56 +02:00
2022-08-30 06:58:31 +02:00
let pendingReaction : Reaction ? = {
2023-06-23 09:54:29 +02:00
guard ! remove else {
2022-08-30 06:58:31 +02:00
return try ? Reaction
. filter ( Reaction . Columns . interactionId = = cellViewModel . id )
. filter ( Reaction . Columns . authorId = = cellViewModel . currentUserPublicKey )
. filter ( Reaction . Columns . emoji = = emoji )
. fetchOne ( db )
}
2023-06-23 09:54:29 +02:00
let sortId : Int64 = Reaction . getSortId (
db ,
interactionId : cellViewModel . id ,
emoji : emoji
)
return Reaction (
interactionId : cellViewModel . id ,
serverHash : nil ,
timestampMs : sentTimestamp ,
authorId : cellViewModel . currentUserPublicKey ,
emoji : emoji ,
count : 1 ,
sortId : sortId
)
2022-08-30 06:58:31 +02:00
} ( )
2022-07-25 07:39:56 +02:00
// U p d a t e t h e d a t a b a s e
if remove {
2022-08-30 07:26:23 +02:00
try Reaction
2022-07-25 07:39:56 +02:00
. filter ( Reaction . Columns . interactionId = = cellViewModel . id )
. filter ( Reaction . Columns . authorId = = cellViewModel . currentUserPublicKey )
. filter ( Reaction . Columns . emoji = = emoji )
. deleteAll ( db )
}
2022-06-06 07:38:38 +02:00
else {
2022-08-30 06:58:31 +02:00
try pendingReaction ? . insert ( db )
2022-07-25 07:39:56 +02:00
// A d d i t t o t h e r e c e n t l i s t
Emoji . addRecent ( db , emoji : emoji )
2022-06-06 07:38:38 +02:00
}
2022-07-25 07:39:56 +02:00
2023-06-23 09:54:29 +02:00
switch threadVariant {
case . community :
guard
let serverMessageId : Int64 = cellViewModel . openGroupServerMessageId ,
let openGroupServer : String = cellViewModel . threadOpenGroupServer ,
let openGroupRoom : String = openGroupRoom ,
let pendingChange : OpenGroupAPI . PendingChange = pendingChange ,
OpenGroupManager . doesOpenGroupSupport ( db , capability : . reactions , on : openGroupServer )
else { throw MessageSenderError . invalidMessage }
let sendData : OpenGroupAPI . PreparedSendData < Int64 ? > = try {
guard ! remove else {
return try OpenGroupAPI
. preparedReactionDelete (
db ,
emoji : emoji ,
id : serverMessageId ,
in : openGroupRoom ,
on : openGroupServer
)
. map { _ , response in response . seqNo }
}
return try OpenGroupAPI
. preparedReactionAdd (
2022-11-27 22:32:32 +01:00
db ,
emoji : emoji ,
2023-06-23 09:54:29 +02:00
id : serverMessageId ,
in : openGroupRoom ,
on : openGroupServer
2022-11-27 22:32:32 +01:00
)
. map { _ , response in response . seqNo }
2023-06-23 09:54:29 +02:00
} ( )
return ( nil , ( pendingReaction , pendingChange , sendData ) )
default :
let sendData : MessageSender . PreparedSendData = try MessageSender . preparedSendData (
db ,
message : VisibleMessage (
sentTimestamp : UInt64 ( sentTimestamp ) ,
text : nil ,
reaction : VisibleMessage . VMReaction (
timestamp : UInt64 ( cellViewModel . timestampMs ) ,
publicKey : {
guard cellViewModel . variant = = . standardIncoming else {
return cellViewModel . currentUserPublicKey
}
return cellViewModel . authorId
} ( ) ,
2022-11-27 22:32:32 +01:00
emoji : emoji ,
2023-06-23 09:54:29 +02:00
kind : ( remove ? . remove : . react )
2022-11-27 22:32:32 +01:00
)
2023-06-23 09:54:29 +02:00
) ,
to : try Message . Destination
. from ( db , threadId : cellViewModel . threadId , threadVariant : cellViewModel . threadVariant ) ,
namespace : try Message . Destination
. from ( db , threadId : cellViewModel . threadId , threadVariant : cellViewModel . threadVariant )
. defaultNamespace ,
2023-08-01 06:27:41 +02:00
interactionId : cellViewModel . id ,
using : dependencies
2023-06-23 09:54:29 +02:00
)
return ( sendData , nil )
}
}
}
. tryFlatMap { messageSendData , openGroupInfo -> AnyPublisher < Void , Error > in
switch ( messageSendData , openGroupInfo ) {
case ( . some ( let sendData ) , _ ) :
2023-08-01 06:27:41 +02:00
return MessageSender . sendImmediate ( data : sendData , using : dependencies )
2022-11-27 22:32:32 +01:00
2023-06-23 09:54:29 +02:00
case ( _ , . some ( let info ) ) :
return OpenGroupAPI . send ( data : info . sendData )
. handleEvents (
receiveOutput : { _ , seqNo in
OpenGroupManager
. updatePendingChange (
info . pendingChange ,
seqNo : seqNo
2022-11-27 22:32:32 +01:00
)
2023-06-23 09:54:29 +02:00
} ,
receiveCompletion : { [ weak self ] result in
switch result {
case . finished : break
case . failure :
OpenGroupManager . removePendingChange ( info . pendingChange )
self ? . handleReactionSentFailure (
info . pendingReaction ,
remove : remove
)
}
2022-11-27 22:32:32 +01:00
}
2023-06-23 09:54:29 +02:00
)
. map { _ in ( ) }
2022-11-27 22:32:32 +01:00
. eraseToAnyPublisher ( )
2023-06-23 09:54:29 +02:00
default : throw MessageSenderError . invalidMessage
2022-05-19 09:10:53 +02:00
}
2023-06-23 09:54:29 +02:00
}
. sinkUntilComplete ( )
2022-05-16 09:06:06 +02:00
}
2022-08-30 06:58:31 +02:00
func handleReactionSentFailure ( _ pendingReaction : Reaction ? , remove : Bool ) {
2022-08-30 07:26:23 +02:00
guard let pendingReaction = pendingReaction else { return }
2022-08-30 03:59:11 +02:00
Storage . shared . writeAsync { db in
2022-08-30 06:58:31 +02:00
// R e v e r s e t h e d a t a b a s e
2022-08-30 03:59:11 +02:00
if remove {
2022-08-30 07:26:23 +02:00
try pendingReaction . insert ( db )
2022-08-30 03:59:11 +02:00
}
else {
2022-08-30 07:26:23 +02:00
try Reaction
. filter ( Reaction . Columns . interactionId = = pendingReaction . interactionId )
. filter ( Reaction . Columns . authorId = = pendingReaction . authorId )
. filter ( Reaction . Columns . emoji = = pendingReaction . emoji )
. deleteAll ( db )
2022-08-30 03:59:11 +02:00
}
}
}
2023-08-01 06:27:41 +02:00
func showFullEmojiKeyboard ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2022-06-14 07:11:17 +02:00
hideInputAccessoryView ( )
2022-07-25 07:39:56 +02:00
2022-06-14 07:11:17 +02:00
let emojiPicker = EmojiPickerSheet (
2022-07-25 07:39:56 +02:00
completionHandler : { [ weak self ] emoji in
guard let emoji : EmojiWithSkinTones = emoji else { return }
2023-08-01 06:27:41 +02:00
self ? . react ( cellViewModel , with : emoji , using : dependencies )
2022-06-14 07:11:17 +02:00
} ,
2022-07-25 07:39:56 +02:00
dismissHandler : { [ weak self ] in
self ? . showInputAccessoryView ( )
}
)
2022-09-15 05:56:32 +02:00
2022-06-14 07:11:17 +02:00
present ( emojiPicker , animated : true , completion : nil )
2022-05-16 09:06:06 +02:00
}
2022-06-08 06:29:51 +02:00
func contextMenuDismissed ( ) {
recoverInputView ( )
}
// MARK: - - a c t i o n h a n d l i n g
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
private func showFailedMessageSheet ( for cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2023-02-14 03:41:24 +01:00
let sheet = UIAlertController (
title : ( cellViewModel . state = = . failedToSync ?
" MESSAGE_DELIVERY_FAILED_SYNC_TITLE " . localized ( ) :
" MESSAGE_DELIVERY_FAILED_TITLE " . localized ( )
) ,
message : cellViewModel . mostRecentFailureText ,
preferredStyle : . actionSheet
)
sheet . addAction ( UIAlertAction ( title : " TXT_CANCEL_TITLE " . localized ( ) , style : . cancel , handler : nil ) )
if cellViewModel . state != . failedToSync {
sheet . addAction ( UIAlertAction ( title : " TXT_DELETE_TITLE " . localized ( ) , style : . destructive , handler : { _ in
Storage . shared . writeAsync { db in
try Interaction
. filter ( id : cellViewModel . id )
. deleteAll ( db )
}
} ) )
}
sheet . addAction ( UIAlertAction (
title : ( cellViewModel . state = = . failedToSync ?
" context_menu_resync " . localized ( ) :
" context_menu_resend " . localized ( )
) ,
style : . default ,
2023-08-01 06:27:41 +02:00
handler : { [ weak self ] _ in self ? . retry ( cellViewModel , using : dependencies ) }
2023-02-14 03:41:24 +01:00
) )
2022-05-12 09:28:27 +02:00
2021-07-15 01:47:03 +02:00
// H A C K : E x t r a c t i n g t h i s i n f o f r o m t h e e r r o r s t r i n g i s p r e t t y d o d g y
2022-05-12 09:28:27 +02:00
let prefix : String = " HTTP request failed at destination (Service node "
2022-05-25 10:48:04 +02:00
if let mostRecentFailureText : String = cellViewModel . mostRecentFailureText , mostRecentFailureText . hasPrefix ( prefix ) {
2022-05-12 09:28:27 +02:00
let rest = mostRecentFailureText . substring ( from : prefix . count )
2021-07-15 01:47:03 +02:00
if let index = rest . firstIndex ( of : " ) " ) {
let snodeAddress = String ( rest [ rest . startIndex . . < index ] )
2022-05-12 09:28:27 +02:00
sheet . addAction ( UIAlertAction ( title : " Copy Service Node Info " , style : . default ) { _ in
2021-07-15 01:47:03 +02:00
UIPasteboard . general . string = snodeAddress
2022-05-12 09:28:27 +02:00
} )
2021-07-15 01:47:03 +02:00
}
}
2022-05-12 09:28:27 +02:00
2023-02-24 01:12:41 +01:00
Modal . setupForIPadIfNeeded ( sheet , targetView : self . view )
2022-05-12 09:28:27 +02:00
present ( sheet , animated : true , completion : nil )
2021-02-17 05:57:07 +01:00
}
2021-01-29 01:46:32 +01:00
2022-05-12 09:28:27 +02:00
func joinOpenGroup ( name : String ? , url : String ) {
// O p e n g r o u p s c a n b e u n s a f e , s o a l w a y s a s k t h e u s e r w h e t h e r t h e y w a n t t o j o i n o n e
2022-09-14 10:32:23 +02:00
let finalName : String = ( name ? ? " Open Group " )
let message : String = " Are you sure you want to join the \( finalName ) open group? " ;
let modal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " Join \( finalName ) ? " ,
2023-05-16 05:09:05 +02:00
body : . attributedText (
NSMutableAttributedString ( string : message )
. adding (
attributes : [ . font : UIFont . boldSystemFont ( ofSize : Values . smallFontSize ) ] ,
range : ( message as NSString ) . range ( of : finalName )
)
) ,
2022-09-26 03:16:47 +02:00
confirmTitle : " JOIN_COMMUNITY_BUTTON_TITLE " . localized ( ) ,
2022-09-14 10:32:23 +02:00
onConfirm : { modal in
guard let presentingViewController : UIViewController = modal . presentingViewController else {
return
}
2023-02-28 07:23:56 +01:00
guard let ( room , server , publicKey ) = SessionUtil . parseCommunity ( url : url ) else {
2022-09-14 10:32:23 +02:00
let errorModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
2022-10-07 09:46:05 +02:00
title : " COMMUNITY_ERROR_GENERIC " . localized ( ) ,
2022-09-14 10:32:23 +02:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
2022-09-28 10:26:02 +02:00
cancelStyle : . alert_text
2022-09-14 10:32:23 +02:00
)
)
return presentingViewController . present ( errorModal , animated : true , completion : nil )
}
Storage . shared
2023-06-28 10:03:40 +02:00
. writePublisher { db in
2022-09-14 10:32:23 +02:00
OpenGroupManager . shared . add (
db ,
roomToken : room ,
server : server ,
2023-06-28 10:03:40 +02:00
publicKey : publicKey ,
calledFromConfigHandling : false
)
}
. flatMap { successfullyAddedGroup in
OpenGroupManager . shared . performInitialRequestsAfterAdd (
successfullyAddedGroup : successfullyAddedGroup ,
roomToken : room ,
server : server ,
2023-02-28 07:23:56 +01:00
publicKey : publicKey ,
calledFromConfigHandling : false
2022-09-14 10:32:23 +02:00
)
}
2023-04-14 04:39:18 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
2022-11-27 22:32:32 +01:00
. receive ( on : DispatchQueue . main )
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
2022-12-16 07:02:40 +01:00
case . finished : break
2022-11-27 22:32:32 +01:00
case . failure ( let error ) :
2023-06-29 04:11:54 +02:00
// I f t h e r e w a s a f a i l u r e t h e n t h e g r o u p w i l l b e i n i n v a l i d s t a t e u n t i l
// t h e n e x t l a u n c h s o r e m o v e i t ( t h e u s e r w i l l b e l e f t o n t h e p r e v i o u s
// s c r e e n s o c a n r e - t r i g g e r t h e j o i n )
Storage . shared . writeAsync { db in
OpenGroupManager . shared . delete (
db ,
openGroupId : OpenGroup . idFor ( roomToken : room , server : server ) ,
calledFromConfigHandling : false
)
}
// S h o w t h e u s e r a n e r r o r i n d i c a t i n g t h e y f a i l e d t o p r o p e r l y j o i n t h e g r o u p
2022-11-27 22:32:32 +01:00
let errorModal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " COMMUNITY_ERROR_GENERIC " . localized ( ) ,
2023-05-18 09:34:25 +02:00
body : . text ( error . localizedDescription ) ,
2022-11-27 22:32:32 +01:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
presentingViewController . present ( errorModal , animated : true , completion : nil )
}
2022-09-14 10:32:23 +02:00
}
2022-11-27 22:32:32 +01:00
)
2022-09-14 10:32:23 +02:00
}
)
)
2022-05-12 09:28:27 +02:00
2022-09-14 10:32:23 +02:00
present ( modal , animated : true , completion : nil )
2021-01-29 01:46:32 +01:00
}
2022-05-12 09:28:27 +02:00
// MARK: - C o n t e x t M e n u A c t i o n D e l e g a t e
2023-02-14 03:41:24 +01:00
2023-08-01 06:27:41 +02:00
func info ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2023-01-23 06:53:23 +01:00
let mediaInfoVC = MediaInfoVC (
attachments : ( cellViewModel . attachments ? ? [ ] ) ,
2023-02-09 04:14:35 +01:00
isOutgoing : ( cellViewModel . variant = = . standardOutgoing ) ,
threadId : self . viewModel . threadData . threadId ,
threadVariant : self . viewModel . threadData . threadVariant ,
interactionId : cellViewModel . id
2023-01-23 06:53:23 +01:00
)
2023-01-19 01:37:49 +01:00
navigationController ? . pushViewController ( mediaInfoVC , animated : true )
2023-01-12 04:54:38 +01:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func retry ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2023-07-20 04:29:56 +02:00
guard cellViewModel . id != MessageViewModel . optimisticUpdateId else {
guard
let optimisticMessageId : UUID = cellViewModel . optimisticMessageId ,
let optimisticMessageData : ConversationViewModel . OptimisticMessageData = self . viewModel . optimisticMessageData ( for : optimisticMessageId )
else {
// S h o w a n e r r o r f o r t h e r e t r y
let modal : ConfirmationModal = ConfirmationModal (
info : ConfirmationModal . Info (
title : " ALERT_ERROR_TITLE " . localized ( ) ,
body : . text ( " FAILED_TO_STORE_OUTGOING_MESSAGE " . localized ( ) ) ,
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
self . present ( modal , animated : true , completion : nil )
return
}
// T r y t o s e n d t h e o p t i m i s t i c m e s s a g e a g a i n
2023-08-01 06:39:00 +02:00
sendMessage ( optimisticData : optimisticMessageData , using : dependencies )
2023-07-20 04:29:56 +02:00
return
}
2023-08-01 06:27:41 +02:00
dependencies . storage . writeAsync { [ weak self ] db in
2023-02-14 03:41:24 +01:00
guard
let threadId : String = self ? . viewModel . threadData . threadId ,
2023-03-08 07:27:07 +01:00
let threadVariant : SessionThread . Variant = self ? . viewModel . threadData . threadVariant ,
let interaction : Interaction = try ? Interaction . fetchOne ( db , id : cellViewModel . id )
2023-02-14 03:41:24 +01:00
else { return }
2023-03-02 07:05:26 +01:00
if
let quote = try ? interaction . quote . fetchOne ( db ) ,
2023-03-06 04:33:24 +01:00
let quotedAttachment = try ? quote . attachment . fetchOne ( db ) ,
quotedAttachment . isVisualMedia ,
quotedAttachment . downloadUrl = = Attachment . nonMediaQuoteFileId ,
let quotedInteraction = try ? quote . originalInteraction . fetchOne ( db )
2023-03-02 07:05:26 +01:00
{
2023-03-07 05:48:27 +01:00
let attachment : Attachment ? = {
2023-03-07 06:02:46 +01:00
if let attachment = try ? quotedInteraction . attachments . fetchOne ( db ) {
2023-03-07 05:48:27 +01:00
return attachment
}
if
let linkPreview = try ? quotedInteraction . linkPreview . fetchOne ( db ) ,
let linkPreviewAttachment = try ? linkPreview . attachment . fetchOne ( db )
{
return linkPreviewAttachment
}
return nil
} ( )
2023-03-06 04:33:24 +01:00
try quote . with (
attachmentId : attachment ? . cloneAsQuoteThumbnail ( ) ? . inserted ( db ) . id
) . update ( db )
2023-03-02 07:05:26 +01:00
}
2023-03-06 05:31:04 +01:00
// R e m o v e m e s s a g e s e n d i n g j o b s f o r t h e s a m e i n t e r a c t i o n i n d a t a b a s e
// P r e v e n t t h e s a m e m e s s a g e b e i n g s e n t t w i c e
try Job . filter ( Job . Columns . interactionId = = interaction . id ) . deleteAll ( db )
2023-02-14 03:41:24 +01:00
try MessageSender . send (
db ,
interaction : interaction ,
2023-03-08 07:27:07 +01:00
threadId : threadId ,
threadVariant : threadVariant ,
2023-08-01 06:27:41 +02:00
isSyncMessage : ( cellViewModel . state = = . failedToSync ) ,
using : dependencies
2023-02-14 03:41:24 +01:00
)
}
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func reply ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2022-05-12 09:28:27 +02:00
let maybeQuoteDraft : QuotedReplyModel ? = QuotedReplyModel . quotedReplyForSending (
2022-05-25 10:48:04 +02:00
threadId : self . viewModel . threadData . threadId ,
authorId : cellViewModel . authorId ,
variant : cellViewModel . variant ,
body : cellViewModel . body ,
timestampMs : cellViewModel . timestampMs ,
attachments : cellViewModel . attachments ,
2022-10-10 02:48:48 +02:00
linkPreviewAttachment : cellViewModel . linkPreviewAttachment ,
currentUserPublicKey : cellViewModel . currentUserPublicKey ,
2023-07-14 06:36:59 +02:00
currentUserBlinded15PublicKey : cellViewModel . currentUserBlinded15PublicKey ,
currentUserBlinded25PublicKey : cellViewModel . currentUserBlinded25PublicKey
2022-05-12 09:28:27 +02:00
)
guard let quoteDraft : QuotedReplyModel = maybeQuoteDraft else { return }
snInputView . quoteDraftInfo = (
model : quoteDraft ,
2022-05-25 10:48:04 +02:00
isOutgoing : ( cellViewModel . variant = = . standardOutgoing )
2022-05-12 09:28:27 +02:00
)
2021-02-10 04:43:57 +01:00
snInputView . becomeFirstResponder ( )
2021-01-29 01:46:32 +01:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func copy ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2022-05-25 10:48:04 +02:00
switch cellViewModel . cellType {
2023-06-19 10:19:47 +02:00
case . typingIndicator , . dateHeader , . unreadMarker : break
2021-08-05 08:05:52 +02:00
2022-05-12 09:28:27 +02:00
case . textOnlyMessage :
2022-11-09 01:07:13 +01:00
if cellViewModel . body = = nil , let linkPreview : LinkPreview = cellViewModel . linkPreview {
UIPasteboard . general . string = linkPreview . url
return
}
2022-05-25 10:48:04 +02:00
UIPasteboard . general . string = cellViewModel . body
2021-08-05 08:05:52 +02:00
2022-05-12 09:28:27 +02:00
case . audio , . genericAttachment , . mediaMessage :
guard
2022-05-25 10:48:04 +02:00
cellViewModel . attachments ? . count = = 1 ,
let attachment : Attachment = cellViewModel . attachments ? . first ,
2022-05-12 09:28:27 +02:00
attachment . isValid ,
(
attachment . state = = . downloaded ||
attachment . state = = . uploaded
) ,
let utiType : String = MIMETypeUtil . utiType ( forMIMEType : attachment . contentType ) ,
let originalFilePath : String = attachment . originalFilePath ,
let data : Data = try ? Data ( contentsOf : URL ( fileURLWithPath : originalFilePath ) )
else { return }
2021-08-05 08:55:49 +02:00
2022-05-12 09:28:27 +02:00
UIPasteboard . general . setData ( data , forPasteboardType : utiType )
2021-08-05 07:59:23 +02:00
}
2021-07-30 07:26:58 +02:00
}
2022-05-12 09:28:27 +02:00
2022-05-29 11:26:06 +02:00
func copySessionID ( _ cellViewModel : MessageViewModel ) {
2022-05-25 10:48:04 +02:00
guard cellViewModel . variant = = . standardIncoming || cellViewModel . variant = = . standardIncomingDeleted else {
2022-05-12 09:28:27 +02:00
return
2021-07-30 08:51:43 +02:00
}
2022-05-12 09:28:27 +02:00
2022-05-25 10:48:04 +02:00
UIPasteboard . general . string = cellViewModel . authorId
2021-08-02 03:32:46 +02:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func delete ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2022-10-05 09:44:25 +02:00
switch cellViewModel . variant {
case . standardIncomingDeleted , . infoCall ,
. infoScreenshotNotification , . infoMediaSavedNotification ,
2023-03-08 07:00:17 +01:00
. infoClosedGroupCreated , . infoClosedGroupUpdated ,
. infoClosedGroupCurrentUserLeft , . infoClosedGroupCurrentUserLeaving , . infoClosedGroupCurrentUserErrorLeaving ,
2022-10-05 09:44:25 +02:00
. infoMessageRequestAccepted , . infoDisappearingMessagesUpdate :
// I n f o m e s s a g e s a n d u n s e n t m e s s a g e s s h o u l d j u s t t r i g g e r a l o c a l
// d e l e t i o n ( t h e y a r e c r e a t e d a s s i d e e f f e c t s s o w e w o u l d n ' t b e
// a b l e t o d e l e t e t h e m f o r a l l p a r t i c i p a n t s a n y w a y )
Storage . shared . writeAsync { db in
_ = try Interaction
. filter ( id : cellViewModel . id )
. deleteAll ( db )
}
return
case . standardOutgoing , . standardIncoming : break
2021-07-30 08:51:43 +02:00
}
2022-05-12 09:28:27 +02:00
2022-05-25 10:48:04 +02:00
let threadName : String = self . viewModel . threadData . displayName
2022-05-12 09:28:27 +02:00
let userPublicKey : String = getUserHexEncodedPublicKey ( )
// R e m o t e d e l e t i o n l o g i c
2022-11-27 22:32:32 +01:00
func deleteRemotely ( from viewController : UIViewController ? , request : AnyPublisher < Void , Error > , onComplete : ( ( ) -> ( ) ) ? ) {
2022-05-12 09:28:27 +02:00
// S h o w a l o a d i n g i n d i c a t o r
2023-02-20 02:56:48 +01:00
Deferred {
Future < Void , Error > { resolver in
2023-05-19 06:55:35 +02:00
DispatchQueue . main . async {
ModalActivityIndicatorViewController . present ( fromViewController : viewController , canCancel : false ) { _ in
resolver ( Result . success ( ( ) ) )
}
2023-02-20 02:56:48 +01:00
}
2022-11-27 22:32:32 +01:00
}
2021-08-02 03:32:46 +02:00
}
2022-11-27 22:32:32 +01:00
. flatMap { _ in request }
2023-04-14 04:39:18 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
2022-11-27 22:32:32 +01:00
. receive ( on : DispatchQueue . main )
. sinkUntilComplete (
receiveCompletion : { [ weak self ] result in
switch result {
case . failure : break
case . finished :
// D e l e t e t h e i n t e r a c t i o n ( a n d a s s o c i a t e d d a t a ) f r o m t h e d a t a b a s e
Storage . shared . writeAsync { db in
_ = try Interaction
. filter ( id : cellViewModel . id )
. deleteAll ( db )
}
2022-05-12 09:28:27 +02:00
}
2022-11-27 22:32:32 +01:00
// R e g a r d l e s s o f s u c c e s s w e s h o u l d d i s m i s s a n d c a l l b a c k
if self ? . presentedViewController is ModalActivityIndicatorViewController {
self ? . dismiss ( animated : true , completion : nil ) // D i s m i s s t h e l o a d e r
2022-05-12 09:28:27 +02:00
}
2022-11-27 22:32:32 +01:00
onComplete ? ( )
2022-05-12 09:28:27 +02:00
}
2022-11-27 22:32:32 +01:00
)
2021-08-02 03:32:46 +02:00
}
2022-05-12 09:28:27 +02:00
// H o w w e d e l e t e t h e m e s s a g e d i f f e r s d e p e n d i n g o n t h e t y p e o f t h r e a d
2022-05-25 10:48:04 +02:00
switch cellViewModel . threadVariant {
2022-05-12 09:28:27 +02:00
// H a n d l e o p e n g r o u p m e s s a g e s t h e o l d w a y
2023-02-20 02:56:48 +01:00
case . community :
2022-05-12 09:28:27 +02:00
// I f i t ' s a n i n c o m i n g m e s s a g e t h e u s e r m u s t h a v e m o d e r a t o r s t a t u s
2022-07-01 05:08:45 +02:00
let result : ( openGroupServerMessageId : Int64 ? , openGroup : OpenGroup ? ) ? = Storage . shared . read { db -> ( Int64 ? , OpenGroup ? ) in
2022-05-12 09:28:27 +02:00
(
try Interaction
. select ( . openGroupServerMessageId )
2022-05-25 10:48:04 +02:00
. filter ( id : cellViewModel . id )
2022-05-12 09:28:27 +02:00
. asRequest ( of : Int64 . self )
. fetchOne ( db ) ,
2023-03-08 07:27:07 +01:00
try OpenGroup . fetchOne ( db , id : cellViewModel . threadId )
2022-05-12 09:28:27 +02:00
)
}
guard
let openGroup : OpenGroup = result ? . openGroup ,
let openGroupServerMessageId : Int64 = result ? . openGroupServerMessageId , (
2022-05-25 10:48:04 +02:00
cellViewModel . variant != . standardIncoming ||
2022-06-09 10:37:44 +02:00
OpenGroupManager . isUserModeratorOrAdmin (
userPublicKey ,
for : openGroup . roomToken ,
on : openGroup . server
)
2022-05-12 09:28:27 +02:00
)
2022-08-26 06:09:41 +02:00
else {
// I f t h e m e s s a g e h a s n ' t b e e n s e n t y e t t h e n j u s t d e l e t e l o c a l l y
guard cellViewModel . state = = . sending || cellViewModel . state = = . failed else { return }
// R e t r i e v e a n y m e s s a g e s e n d j o b s f o r t h i s i n t e r a c t i o n
let jobs : [ Job ] = Storage . shared
. read { db in
try ? Job
. filter ( Job . Columns . variant = = Job . Variant . messageSend )
. filter ( Job . Columns . interactionId = = cellViewModel . id )
. fetchAll ( db )
}
. defaulting ( to : [ ] )
// I f t h e j o b i s c u r r e n t l y r u n n i n g t h e n w a i t u n t i l i t ' s d o n e b e f o r e t r i g g e r i n g
// t h e d e l e t i o n
let targetJob : Job ? = jobs . first ( where : { JobRunner . isCurrentlyRunning ( $0 ) } )
guard targetJob = = nil else {
JobRunner . afterCurrentlyRunningJob ( targetJob ) { [ weak self ] result in
switch result {
// I f i t s u c c e e d e d t h e n w e ' l l n e e d t o d e l e t e f r o m t h e s e r v e r s o r e - r u n
// t h i s f u n c t i o n ( i f w e s t i l l d o n ' t h a v e t h e s e r v e r i d f o r s o m e r e a s o n
// t h e n t h i s w o u l d r e s u l t i n a l o c a l - o n l y d e l e t i o n w h i c h s h o u l d b e f i n e
case . succeeded : self ? . delete ( cellViewModel )
// O t h e r w i s e w e j u s t n e e d t o c a n c e l t h e p e n d i n g j o b ( i n c a s e i t r e t r i e s )
// a n d d e l e t e t h e i n t e r a c t i o n
default :
JobRunner . removePendingJob ( targetJob )
Storage . shared . writeAsync { db in
_ = try Interaction
. filter ( id : cellViewModel . id )
. deleteAll ( db )
}
}
}
return
}
// I f i t ' s n o t c u r r e n t l y r u n n i n g t h e n r e m o v e a n y p e n d i n g j o b s ( j u s t t o b e s a f e ) a n d
// d e l e t e t h e i n t e r a c t i o n l o c a l l y
jobs . forEach { JobRunner . removePendingJob ( $0 ) }
Storage . shared . writeAsync { db in
_ = try Interaction
. filter ( id : cellViewModel . id )
. deleteAll ( db )
}
return
}
2022-05-12 09:28:27 +02:00
// D e l e t e t h e m e s s a g e f r o m t h e o p e n g r o u p
deleteRemotely (
from : self ,
2023-06-23 09:54:29 +02:00
request : Storage . shared
. readPublisher { db in
try OpenGroupAPI . preparedMessageDelete (
db ,
id : openGroupServerMessageId ,
in : openGroup . roomToken ,
on : openGroup . server
)
}
. flatMap { OpenGroupAPI . send ( data : $0 ) }
2022-06-09 10:37:44 +02:00
. map { _ in ( ) }
2022-11-27 22:32:32 +01:00
. eraseToAnyPublisher ( )
2022-05-12 09:28:27 +02:00
) { [ weak self ] in
self ? . showInputAccessoryView ( )
}
2023-02-20 02:56:48 +01:00
case . contact , . legacyGroup , . group :
2023-05-23 01:42:10 +02:00
let targetPublicKey : String = ( cellViewModel . threadVariant = = . contact ?
userPublicKey :
cellViewModel . threadId
)
2022-07-01 05:08:45 +02:00
let serverHash : String ? = Storage . shared . read { db -> String ? in
2022-05-12 09:28:27 +02:00
try Interaction
. select ( . serverHash )
2022-05-25 10:48:04 +02:00
. filter ( id : cellViewModel . id )
2022-05-12 09:28:27 +02:00
. asRequest ( of : String . self )
. fetchOne ( db )
}
let unsendRequest : UnsendRequest = UnsendRequest (
2022-05-25 10:48:04 +02:00
timestamp : UInt64 ( cellViewModel . timestampMs ) ,
author : ( cellViewModel . variant = = . standardOutgoing ?
2022-05-12 09:28:27 +02:00
userPublicKey :
2022-05-25 10:48:04 +02:00
cellViewModel . authorId
2022-05-12 09:28:27 +02:00
)
)
// F o r i n c o m i n g i n t e r a c t i o n s o r i n t e r a c t i o n s w i t h n o s e r v e r H a s h j u s t d e l e t e t h e m l o c a l l y
2022-05-25 10:48:04 +02:00
guard cellViewModel . variant = = . standardOutgoing , let serverHash : String = serverHash else {
2022-07-01 05:08:45 +02:00
Storage . shared . writeAsync { db in
2022-05-12 09:28:27 +02:00
_ = try Interaction
2022-05-25 10:48:04 +02:00
. filter ( id : cellViewModel . id )
2022-05-12 09:28:27 +02:00
. deleteAll ( db )
// N o n e e d t o s e n d t h e u n s e n d R e q u e s t i f t h e r e i s n o s e r v e r H a s h ( i e . t h e m e s s a g e
// w a s o u t g o i n g b u t n e v e r g o t t o t h e s e r v e r )
guard serverHash != nil else { return }
MessageSender
. send (
db ,
message : unsendRequest ,
2023-03-08 07:27:07 +01:00
threadId : cellViewModel . threadId ,
2022-05-12 09:28:27 +02:00
interactionId : nil ,
2023-08-01 06:27:41 +02:00
to : . contact ( publicKey : userPublicKey ) ,
using : dependencies
2022-05-12 09:28:27 +02:00
)
}
return
}
2022-09-28 10:26:02 +02:00
let actionSheet : UIAlertController = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
2023-01-06 00:38:34 +01:00
actionSheet . addAction ( UIAlertAction (
title : " delete_message_for_me " . localized ( ) ,
accessibilityIdentifier : " Delete for me " ,
style : . destructive
) { [ weak self ] _ in
2022-07-01 05:08:45 +02:00
Storage . shared . writeAsync { db in
2022-05-12 09:28:27 +02:00
_ = try Interaction
2022-05-25 10:48:04 +02:00
. filter ( id : cellViewModel . id )
2022-05-12 09:28:27 +02:00
. deleteAll ( db )
MessageSender
. send (
db ,
message : unsendRequest ,
2023-03-08 07:27:07 +01:00
threadId : cellViewModel . threadId ,
2022-05-12 09:28:27 +02:00
interactionId : nil ,
2023-08-01 06:27:41 +02:00
to : . contact ( publicKey : userPublicKey ) ,
using : dependencies
2022-05-12 09:28:27 +02:00
)
}
self ? . showInputAccessoryView ( )
} )
2022-09-28 10:26:02 +02:00
actionSheet . addAction ( UIAlertAction (
2023-02-10 07:36:40 +01:00
title : {
switch cellViewModel . threadVariant {
2023-02-21 01:11:06 +01:00
case . legacyGroup , . group : return " delete_message_for_everyone " . localized ( )
2023-02-10 07:36:40 +01:00
default :
return ( cellViewModel . threadId = = userPublicKey ?
" delete_message_for_me_and_my_devices " . localized ( ) :
String ( format : " delete_message_for_me_and_recipient " . localized ( ) , threadName )
)
}
} ( ) ,
2023-01-06 00:38:34 +01:00
accessibilityIdentifier : " Delete for everyone " ,
2022-05-12 09:28:27 +02:00
style : . destructive
) { [ weak self ] _ in
2023-05-23 01:42:10 +02:00
let completeServerDeletion = { [ weak self ] in
2022-07-01 05:08:45 +02:00
Storage . shared . writeAsync { db in
2022-05-12 09:28:27 +02:00
try MessageSender
. send (
db ,
message : unsendRequest ,
interactionId : nil ,
2023-03-08 07:27:07 +01:00
threadId : cellViewModel . threadId ,
2023-08-01 06:27:41 +02:00
threadVariant : cellViewModel . threadVariant ,
using : dependencies
2022-05-12 09:28:27 +02:00
)
}
self ? . showInputAccessoryView ( )
}
2023-05-23 01:42:10 +02:00
// W e c a n o n l y d e l e t e m e s s a g e s o n t h e s e r v e r f o r ` c o n t a c t ` a n d ` g r o u p ` c o n v e r s a t i o n s
guard cellViewModel . threadVariant = = . contact || cellViewModel . threadVariant = = . group else {
return completeServerDeletion ( )
}
deleteRemotely (
from : self ,
request : SnodeAPI
. deleteMessages (
publicKey : targetPublicKey ,
serverHashes : [ serverHash ]
)
. map { _ in ( ) }
. eraseToAnyPublisher ( )
) { completeServerDeletion ( ) }
2022-05-12 09:28:27 +02:00
} )
2021-02-10 07:04:26 +01:00
2022-09-28 10:26:02 +02:00
actionSheet . addAction ( UIAlertAction . init ( title : " TXT_CANCEL_TITLE " . localized ( ) , style : . cancel ) { [ weak self ] _ in
2022-05-12 09:28:27 +02:00
self ? . showInputAccessoryView ( )
} )
2023-07-04 09:09:50 +02:00
self . hideInputAccessoryView ( )
2022-09-28 10:26:02 +02:00
Modal . setupForIPadIfNeeded ( actionSheet , targetView : self . view )
self . present ( actionSheet , animated : true )
2022-05-12 09:28:27 +02:00
}
2021-02-10 07:04:26 +01:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func save ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2022-05-25 10:48:04 +02:00
guard cellViewModel . cellType = = . mediaMessage else { return }
2022-05-12 09:28:27 +02:00
2022-05-25 10:48:04 +02:00
let mediaAttachments : [ ( Attachment , String ) ] = ( cellViewModel . attachments ? ? [ ] )
2022-05-12 09:28:27 +02:00
. filter { attachment in
attachment . isValid &&
attachment . isVisualMedia && (
attachment . state = = . downloaded ||
attachment . state = = . uploaded
)
}
. compactMap { attachment in
guard let originalFilePath : String = attachment . originalFilePath else { return nil }
return ( attachment , originalFilePath )
}
guard ! mediaAttachments . isEmpty else { return }
2021-02-11 04:24:38 +01:00
2022-05-12 09:28:27 +02:00
mediaAttachments . forEach { attachment , originalFilePath in
PHPhotoLibrary . shared ( ) . performChanges (
{
if attachment . isImage || attachment . isAnimated {
PHAssetChangeRequest . creationRequestForAssetFromImage (
atFileURL : URL ( fileURLWithPath : originalFilePath )
)
}
else if attachment . isVideo {
PHAssetChangeRequest . creationRequestForAssetFromVideo (
atFileURL : URL ( fileURLWithPath : originalFilePath )
)
}
} ,
completionHandler : { _ , _ in }
)
2021-08-31 02:28:45 +02:00
}
2022-05-12 09:28:27 +02:00
// S e n d a ' m e d i a s a v e d ' n o t i f i c a t i o n i f n e e d e d
2022-05-25 10:48:04 +02:00
guard self . viewModel . threadData . threadVariant = = . contact , cellViewModel . variant = = . standardIncoming else {
2022-05-12 09:28:27 +02:00
return
2021-08-31 02:28:45 +02:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
sendDataExtraction ( kind : . mediaSaved ( timestamp : UInt64 ( cellViewModel . timestampMs ) ) )
2021-02-11 04:24:38 +01:00
}
2022-05-12 09:28:27 +02:00
2023-08-01 06:27:41 +02:00
func ban ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2023-02-20 02:56:48 +01:00
guard cellViewModel . threadVariant = = . community else { return }
2022-05-10 09:42:15 +02:00
2022-05-25 10:48:04 +02:00
let threadId : String = self . viewModel . threadData . threadId
2022-09-28 10:26:02 +02:00
let modal : ConfirmationModal = ConfirmationModal (
targetView : self . view ,
info : ConfirmationModal . Info (
title : " Session " ,
2023-05-16 05:09:05 +02:00
body : . text ( " This will ban the selected user from this room. It won't ban them from other rooms. " ) ,
2022-09-28 10:26:02 +02:00
confirmTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text ,
onConfirm : { [ weak self ] _ in
Storage . shared
2023-06-23 09:54:29 +02:00
. readPublisher { db -> OpenGroupAPI . PreparedSendData < NoResponse > in
2022-09-28 10:26:02 +02:00
guard let openGroup : OpenGroup = try OpenGroup . fetchOne ( db , id : threadId ) else {
2023-02-20 02:56:48 +01:00
throw StorageError . objectNotFound
2022-09-28 10:26:02 +02:00
}
2023-06-23 09:54:29 +02:00
return try OpenGroupAPI
. preparedUserBan (
2022-09-28 10:26:02 +02:00
db ,
sessionId : cellViewModel . authorId ,
from : [ openGroup . roomToken ] ,
on : openGroup . server
)
}
2023-06-23 09:54:29 +02:00
. flatMap { OpenGroupAPI . send ( data : $0 ) }
2023-04-14 04:39:18 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
2022-11-27 22:32:32 +01:00
. receive ( on : DispatchQueue . main )
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
case . finished : break
case . failure :
let modal : ConfirmationModal = ConfirmationModal (
targetView : self ? . view ,
info : ConfirmationModal . Info (
title : CommonStrings . errorAlertTitle ,
2023-05-18 09:34:25 +02:00
body : . text ( " context_menu_ban_user_error_alert_message " . localized ( ) ) ,
2022-11-27 22:32:32 +01:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
self ? . present ( modal , animated : true )
}
}
)
2022-06-09 10:37:44 +02:00
2022-09-28 10:26:02 +02:00
self ? . becomeFirstResponder ( )
} ,
afterClosed : { [ weak self ] in self ? . becomeFirstResponder ( ) }
)
)
self . present ( modal , animated : true )
2021-07-14 07:56:56 +02:00
}
2021-02-16 06:36:06 +01:00
2023-08-01 06:27:41 +02:00
func banAndDeleteAllMessages ( _ cellViewModel : MessageViewModel , using dependencies : Dependencies ) {
2023-02-20 02:56:48 +01:00
guard cellViewModel . threadVariant = = . community else { return }
2022-05-12 09:28:27 +02:00
2022-05-25 10:48:04 +02:00
let threadId : String = self . viewModel . threadData . threadId
2022-09-28 10:26:02 +02:00
let modal : ConfirmationModal = ConfirmationModal (
targetView : self . view ,
info : ConfirmationModal . Info (
title : " Session " ,
2023-05-16 05:09:05 +02:00
body : . text ( " This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there. " ) ,
2022-09-28 10:26:02 +02:00
confirmTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text ,
onConfirm : { [ weak self ] _ in
Storage . shared
2023-06-26 10:03:40 +02:00
. readPublisher { db in
2022-09-28 10:26:02 +02:00
guard let openGroup : OpenGroup = try OpenGroup . fetchOne ( db , id : threadId ) else {
2023-02-20 02:56:48 +01:00
throw StorageError . objectNotFound
2022-09-28 10:26:02 +02:00
}
2023-06-26 10:03:40 +02:00
return try OpenGroupAPI
. preparedUserBanAndDeleteAllMessages (
2022-09-28 10:26:02 +02:00
db ,
sessionId : cellViewModel . authorId ,
in : openGroup . roomToken ,
on : openGroup . server
)
}
2023-06-26 10:03:40 +02:00
. flatMap { OpenGroupAPI . send ( data : $0 ) }
2023-04-14 04:39:18 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
2022-11-27 22:32:32 +01:00
. receive ( on : DispatchQueue . main )
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
case . finished : break
case . failure :
let modal : ConfirmationModal = ConfirmationModal (
targetView : self ? . view ,
info : ConfirmationModal . Info (
title : CommonStrings . errorAlertTitle ,
2023-05-18 09:34:25 +02:00
body : . text ( " context_menu_ban_user_error_alert_message " . localized ( ) ) ,
2022-11-27 22:32:32 +01:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
)
self ? . present ( modal , animated : true )
}
}
)
2022-09-28 10:26:02 +02:00
self ? . becomeFirstResponder ( )
} ,
afterClosed : { [ weak self ] in self ? . becomeFirstResponder ( ) }
)
2022-05-12 09:28:27 +02:00
)
2022-09-28 10:26:02 +02:00
self . present ( modal , animated : true )
2021-02-16 23:40:23 +01:00
}
2022-05-12 09:28:27 +02:00
// MARK: - V o i c e M e s s a g e R e c o r d i n g V i e w D e l e g a t e
2021-02-16 06:36:06 +01:00
2023-08-01 06:39:00 +02:00
func startVoiceMessageRecording ( using dependencies : Dependencies ) {
2021-02-16 06:36:06 +01:00
// R e q u e s t p e r m i s s i o n i f n e e d e d
2022-09-02 09:32:13 +02:00
Permissions . requestMicrophonePermissionIfNeeded ( ) { [ weak self ] in
2022-10-24 05:52:28 +02:00
DispatchQueue . main . async {
self ? . cancelVoiceMessageRecording ( )
}
2021-02-16 23:40:23 +01:00
}
2022-05-12 09:28:27 +02:00
2021-11-16 00:36:31 +01:00
// K e e p s c r e e n o n
UIApplication . shared . isIdleTimerDisabled = false
2021-02-16 09:28:32 +01:00
guard AVAudioSession . sharedInstance ( ) . recordPermission = = . granted else { return }
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// C a n c e l a n y c u r r e n t a u d i o p l a y b a c k
2022-05-12 09:28:27 +02:00
self . viewModel . stopAudio ( )
2021-02-16 06:36:06 +01:00
// C r e a t e U R L
2022-05-12 09:28:27 +02:00
let directory : String = OWSTemporaryDirectory ( )
2022-12-21 06:39:52 +01:00
let fileName : String = " \( SnodeAPI . currentOffsetTimestampMs ( ) ) .m4a "
2022-05-12 09:28:27 +02:00
let url : URL = URL ( fileURLWithPath : directory ) . appendingPathComponent ( fileName )
2021-02-16 06:36:06 +01:00
// S e t u p a u d i o s e s s i o n
2022-06-22 06:27:34 +02:00
let isConfigured = ( Environment . shared ? . audioSession . startAudioActivity ( recordVoiceMessageActivity ) = = true )
2021-02-16 06:36:06 +01:00
guard isConfigured else {
return cancelVoiceMessageRecording ( )
}
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// S e t u p a u d i o r e c o r d e r
let audioRecorder : AVAudioRecorder
do {
2022-05-12 09:28:27 +02:00
audioRecorder = try AVAudioRecorder (
url : url ,
settings : [
AVFormatIDKey : NSNumber ( value : kAudioFormatMPEG4AAC ) ,
AVSampleRateKey : NSNumber ( value : 44100 ) ,
AVNumberOfChannelsKey : NSNumber ( value : 2 ) ,
AVEncoderBitRateKey : NSNumber ( value : 128 * 1024 )
]
)
2021-02-16 06:36:06 +01:00
audioRecorder . isMeteringEnabled = true
self . audioRecorder = audioRecorder
2022-05-12 09:28:27 +02:00
}
catch {
2021-02-16 06:36:06 +01:00
SNLog ( " Couldn't start audio recording due to error: \( error ) . " )
return cancelVoiceMessageRecording ( )
}
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// L i m i t v o i c e m e s s a g e s t o a m i n u t e
2021-08-03 01:12:01 +02:00
audioTimer = Timer . scheduledTimer ( withTimeInterval : 180 , repeats : false , block : { [ weak self ] _ in
2021-02-16 06:36:06 +01:00
self ? . snInputView . hideVoiceMessageUI ( )
2023-08-01 06:39:00 +02:00
self ? . endVoiceMessageRecording ( using : dependencies )
2021-02-16 06:36:06 +01:00
} )
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// P r e p a r e a u d i o r e c o r d e r
guard audioRecorder . prepareToRecord ( ) else {
SNLog ( " Couldn't prepare audio recorder. " )
return cancelVoiceMessageRecording ( )
}
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// S t a r t r e c o r d i n g
guard audioRecorder . record ( ) else {
SNLog ( " Couldn't record audio. " )
return cancelVoiceMessageRecording ( )
}
}
2023-08-01 06:39:00 +02:00
func endVoiceMessageRecording ( using dependencies : Dependencies ) {
2021-11-16 00:36:31 +01:00
UIApplication . shared . isIdleTimerDisabled = true
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// H i d e t h e U I
snInputView . hideVoiceMessageUI ( )
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// C a n c e l t h e t i m e r
audioTimer ? . invalidate ( )
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// C h e c k p r e c o n d i t i o n s
guard let audioRecorder = audioRecorder else { return }
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// G e t d u r a t i o n
let duration = audioRecorder . currentTime
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// S t o p t h e r e c o r d i n g
stopVoiceMessageRecording ( )
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// C h e c k f o r u s e r m i s u n d e r s t a n d i n g
guard duration > 1 else {
self . audioRecorder = nil
2022-05-12 09:28:27 +02:00
2022-09-28 10:26:02 +02:00
let modal : ConfirmationModal = ConfirmationModal (
targetView : self . view ,
info : ConfirmationModal . Info (
title : " VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( " VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE " . localized ( ) ) ,
2022-09-28 10:26:02 +02:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
cancelStyle : . alert_text
)
2022-05-12 09:28:27 +02:00
)
2022-09-28 10:26:02 +02:00
self . present ( modal , animated : true )
2022-05-12 09:28:27 +02:00
return
2021-02-16 06:36:06 +01:00
}
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// G e t d a t a
let dataSourceOrNil = DataSourcePath . dataSource ( with : audioRecorder . url , shouldDeleteOnDeallocation : true )
self . audioRecorder = nil
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
guard let dataSource = dataSourceOrNil else { return SNLog ( " Couldn't load recorded data. " ) }
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// C r e a t e a t t a c h m e n t
2022-05-12 09:28:27 +02:00
let fileName = ( " VOICE_MESSAGE_FILE_NAME " . localized ( ) as NSString ) . appendingPathExtension ( " m4a " )
2021-02-16 06:36:06 +01:00
dataSource . sourceFilename = fileName
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
let attachment = SignalAttachment . voiceMessageAttachment ( dataSource : dataSource , dataUTI : kUTTypeMPEG4Audio as String )
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
guard ! attachment . hasError else {
2023-06-23 09:54:29 +02:00
return showErrorAlert ( for : attachment )
2021-02-16 06:36:06 +01:00
}
2022-05-12 09:28:27 +02:00
2021-02-16 06:36:06 +01:00
// S e n d a t t a c h m e n t
2023-08-01 06:39:00 +02:00
sendMessage ( text : " " , attachments : [ attachment ] , using : dependencies )
2021-02-16 06:36:06 +01:00
}
func cancelVoiceMessageRecording ( ) {
snInputView . hideVoiceMessageUI ( )
audioTimer ? . invalidate ( )
stopVoiceMessageRecording ( )
audioRecorder = nil
}
func stopVoiceMessageRecording ( ) {
audioRecorder ? . stop ( )
2022-06-22 06:27:34 +02:00
Environment . shared ? . audioSession . endAudioActivity ( recordVoiceMessageActivity )
2021-02-16 06:36:06 +01:00
}
2021-03-02 04:59:07 +01:00
2022-08-26 06:09:41 +02:00
// MARK: - D a t a E x t r a c t i o n N o t i f i c a t i o n s
2023-08-01 06:27:41 +02:00
@objc func sendScreenshotNotification ( ) { sendDataExtraction ( kind : . screenshot ) }
func sendDataExtraction (
kind : DataExtractionNotification . Kind ,
using dependencies : Dependencies = Dependencies ( )
) {
2022-08-26 06:09:41 +02:00
// O n l y s e n d s c r e e n s h o t n o t i f i c a t i o n s t o o n e - t o - o n e c o n v e r s a t i o n s
guard self . viewModel . threadData . threadVariant = = . contact else { return }
let threadId : String = self . viewModel . threadData . threadId
2023-03-08 07:27:07 +01:00
let threadVariant : SessionThread . Variant = self . viewModel . threadData . threadVariant
2022-08-26 06:09:41 +02:00
2023-08-01 06:27:41 +02:00
dependencies . storage . writeAsync { db in
2022-08-26 06:09:41 +02:00
try MessageSender . send (
db ,
message : DataExtractionNotification (
2023-08-01 06:27:41 +02:00
kind : kind ,
2023-04-05 09:19:21 +02:00
sentTimestamp : UInt64 ( SnodeAPI . currentOffsetTimestampMs ( ) )
2022-08-26 06:09:41 +02:00
) ,
interactionId : nil ,
2023-03-08 07:27:07 +01:00
threadId : threadId ,
2023-08-01 06:27:41 +02:00
threadVariant : threadVariant ,
using : dependencies
2022-08-26 06:09:41 +02:00
)
}
}
2021-02-16 23:40:23 +01:00
2022-02-01 04:55:03 +01:00
// MARK: - C o n v e n i e n c e
2022-06-08 06:29:51 +02:00
2023-06-23 09:54:29 +02:00
func showErrorAlert ( for attachment : SignalAttachment ) {
2022-09-28 10:26:02 +02:00
let modal : ConfirmationModal = ConfirmationModal (
targetView : self . view ,
info : ConfirmationModal . Info (
title : " ATTACHMENT_ERROR_ALERT_TITLE " . localized ( ) ,
2023-05-16 05:09:05 +02:00
body : . text ( attachment . localizedErrorDescription ? ? SignalAttachment . missingDataErrorMessage ) ,
2022-09-28 10:26:02 +02:00
cancelTitle : " BUTTON_OK " . localized ( ) ,
2023-06-23 09:54:29 +02:00
cancelStyle : . alert_text
2022-09-28 10:26:02 +02:00
)
)
self . present ( modal , animated : true )
2021-02-16 23:40:23 +01:00
}
2021-01-29 01:46:32 +01:00
}
2022-01-28 06:24:18 +01:00
// MARK: - U I D o c u m e n t I n t e r a c t i o n C o n t r o l l e r D e l e g a t e
extension ConversationVC : UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview ( _ controller : UIDocumentInteractionController ) -> UIViewController {
return self
}
}
2022-02-17 01:52:23 +01:00
2022-02-02 06:59:56 +01:00
// MARK: - M e s s a g e R e q u e s t A c t i o n s
extension ConversationVC {
2022-05-12 09:28:27 +02:00
fileprivate func approveMessageRequestIfNeeded (
2022-05-25 10:48:04 +02:00
for threadId : String ,
threadVariant : SessionThread . Variant ,
2022-05-12 09:28:27 +02:00
isNewThread : Bool ,
2023-08-01 06:27:41 +02:00
timestampMs : Int64 ,
using dependencies : Dependencies = Dependencies ( )
2022-07-18 04:32:46 +02:00
) {
guard threadVariant = = . contact else { return }
2022-11-27 22:32:32 +01:00
let updateNavigationBackStack : ( ) -> Void = {
// R e m o v e t h e ' M e s s a g e R e q u e s t s V i e w C o n t r o l l e r ' f r o m t h e n a v h i e r a r c h y i f p r e s e n t
DispatchQueue . main . async { [ weak self ] in
if
let viewControllers : [ UIViewController ] = self ? . navigationController ? . viewControllers ,
let messageRequestsIndex = viewControllers . firstIndex ( where : { $0 is MessageRequestsViewController } ) ,
messageRequestsIndex > 0
{
var newViewControllers = viewControllers
newViewControllers . remove ( at : messageRequestsIndex )
self ? . navigationController ? . viewControllers = newViewControllers
}
}
}
2022-05-12 09:28:27 +02:00
2022-02-02 06:59:56 +01:00
// I f t h e c o n t a c t d o e s n ' t e x i s t t h e n w e s h o u l d c r e a t e i t s o w e c a n s t o r e t h e ' i s A p p r o v e d ' s t a t e
// ( i t ' l l b e u p d a t e d w i t h c o r r e c t p r o f i l e i n f o i f t h e y a c c e p t t h e m e s s a g e r e q u e s t s o t h i s
// s h o u l d n ' t c a u s e w e i r d b e h a v i o u r s )
2022-04-06 07:43:26 +02:00
guard
2023-03-08 07:27:07 +01:00
let contact : Contact = Storage . shared . read ( { db in Contact . fetchOrCreate ( db , id : threadId ) } ) ,
! contact . isApproved
else { return }
2022-07-18 04:32:46 +02:00
2022-12-16 06:51:08 +01:00
Storage . shared
2023-04-14 04:39:18 +02:00
. writePublisher { db in
2022-03-01 05:26:14 +01:00
// I f w e a r e n ' t c r e a t i n g a n e w t h r e a d ( i e . s e n d i n g a m e s s a g e r e q u e s t ) t h e n s e n d a
// m e s s a g e R e q u e s t R e s p o n s e b a c k t o t h e s e n d e r ( t h i s a l l o w s t h e s e n d e r t o k n o w t h a t
// t h e y h a v e b e e n a p p r o v e d a n d c a n n o w u s e t h i s c o n t a c t i n c l o s e d g r o u p s )
2022-07-18 04:32:46 +02:00
if ! isNewThread {
try MessageSender . send (
db ,
message : MessageRequestResponse (
isApproved : true ,
sentTimestampMs : UInt64 ( timestampMs )
) ,
interactionId : nil ,
2023-03-08 07:27:07 +01:00
threadId : threadId ,
2023-08-01 06:27:41 +02:00
threadVariant : threadVariant ,
using : dependencies
2022-07-18 04:32:46 +02:00
)
2022-03-01 06:48:47 +01:00
}
2022-07-18 04:32:46 +02:00
2022-03-01 05:26:14 +01:00
// D e f a u l t ' d i d A p p r o v e M e ' t o t r u e f o r t h e p e r s o n a p p r o v i n g t h e m e s s a g e r e q u e s t
2023-03-08 07:27:07 +01:00
try contact . save ( db )
2022-12-16 06:51:08 +01:00
try Contact
2023-03-08 07:27:07 +01:00
. filter ( id : contact . id )
2022-12-16 06:51:08 +01:00
. updateAllAndConfig (
db ,
Contact . Columns . isApproved . set ( to : true ) ,
Contact . Columns . didApproveMe
2023-03-08 07:27:07 +01:00
. set ( to : contact . didApproveMe || ! isNewThread )
2022-07-18 04:32:46 +02:00
)
2022-12-16 06:51:08 +01:00
}
2023-04-14 04:39:18 +02:00
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) )
. receive ( on : DispatchQueue . main )
2022-12-16 06:51:08 +01:00
. sinkUntilComplete (
receiveCompletion : { _ in
// U p d a t e t h e U I
updateNavigationBackStack ( )
}
)
2022-02-02 06:59:56 +01:00
}
2022-05-12 09:28:27 +02:00
2022-02-02 06:59:56 +01:00
@objc func acceptMessageRequest ( ) {
2022-05-12 09:28:27 +02:00
self . approveMessageRequestIfNeeded (
2022-05-25 10:48:04 +02:00
for : self . viewModel . threadData . threadId ,
threadVariant : self . viewModel . threadData . threadVariant ,
2022-03-24 05:46:53 +01:00
isNewThread : false ,
2022-12-21 06:39:52 +01:00
timestampMs : SnodeAPI . currentOffsetTimestampMs ( )
2022-03-24 05:46:53 +01:00
)
2022-02-02 06:59:56 +01:00
}
2022-05-12 09:28:27 +02:00
2022-02-02 06:59:56 +01:00
@objc func deleteMessageRequest ( ) {
2023-04-06 10:09:26 +02:00
let actions : [ UIContextualAction ] ? = UIContextualAction . generateSwipeActions (
[ . delete ] ,
for : . trailing ,
indexPath : IndexPath ( row : 0 , section : 0 ) ,
tableView : self . tableView ,
threadViewModel : self . viewModel . threadData ,
2022-12-08 04:21:38 +01:00
viewController : self
2022-05-12 09:28:27 +02:00
)
2022-06-08 06:29:51 +02:00
2023-04-06 10:09:26 +02:00
guard let action : UIContextualAction = actions ? . first else { return }
action . handler ( action , self . view , { [ weak self ] didConfirm in
guard didConfirm else { return }
2022-12-08 04:21:38 +01:00
self ? . stopObservingChanges ( )
DispatchQueue . main . async {
self ? . navigationController ? . popViewController ( animated : true )
}
2023-04-06 10:09:26 +02:00
} )
2022-02-02 06:59:56 +01:00
}
2022-08-12 08:16:37 +02:00
2022-12-08 04:21:38 +01:00
@objc func blockMessageRequest ( ) {
2023-04-06 10:09:26 +02:00
let actions : [ UIContextualAction ] ? = UIContextualAction . generateSwipeActions (
[ . block ] ,
for : . trailing ,
indexPath : IndexPath ( row : 0 , section : 0 ) ,
tableView : self . tableView ,
threadViewModel : self . viewModel . threadData ,
2022-12-08 04:21:38 +01:00
viewController : self
2022-08-12 08:16:37 +02:00
)
2022-06-08 06:29:51 +02:00
2023-04-06 10:09:26 +02:00
guard let action : UIContextualAction = actions ? . first else { return }
action . handler ( action , self . view , { [ weak self ] didConfirm in
guard didConfirm else { return }
2022-12-08 04:21:38 +01:00
self ? . stopObservingChanges ( )
DispatchQueue . main . async {
self ? . navigationController ? . popViewController ( animated : true )
}
2023-04-06 10:09:26 +02:00
} )
2022-02-02 06:59:56 +01:00
}
}
2022-05-13 10:07:24 +02:00
// MARK: - M e d i a P r e s e n t a t i o n C o n t e x t P r o v i d e r
extension ConversationVC : MediaPresentationContextProvider {
func mediaPresentationContext ( mediaItem : Media , in coordinateSpace : UICoordinateSpace ) -> MediaPresentationContext ? {
guard case let . gallery ( galleryItem ) = mediaItem else { return nil }
2022-05-20 09:58:39 +02:00
2022-05-13 10:07:24 +02:00
// N o t e : A c c o r d i n g t o A p p l e ' s d o c s t h e ' i n d e x P a t h s F o r V i s i b l e R o w s ' m e t h o d r e t u r n s a n
// u n s o r t e d a r r a y w h i c h m e a n s w e c a n ' t u s e i t t o d e t e r m i n e t h e d e s i r e d ' v i s i b l e C e l l '
// w e a r e a f t e r , d u e t o t h i s w e w i l l n e e d t o i t e r a t e a l l o f t h e v i s i b l e c e l l s t o f i n d
// t h e o n e w e w a n t
let maybeMessageCell : VisibleMessageCell ? = tableView . visibleCells
. first { cell -> Bool in
( ( cell as ? VisibleMessageCell ) ?
. albumView ?
. itemViews
. contains ( where : { mediaView in
mediaView . attachment . id = = galleryItem . attachment . id
} ) )
. defaulting ( to : false )
}
. map { $0 as ? VisibleMessageCell }
let maybeTargetView : MediaView ? = maybeMessageCell ?
. albumView ?
. itemViews
. first ( where : { $0 . attachment . id = = galleryItem . attachment . id } )
guard
let messageCell : VisibleMessageCell = maybeMessageCell ,
let targetView : MediaView = maybeTargetView ,
let mediaSuperview : UIView = targetView . superview
else { return nil }
let cornerRadius : CGFloat
let cornerMask : CACornerMask
2022-05-20 09:58:39 +02:00
let presentationFrame : CGRect = coordinateSpace . convert ( targetView . frame , from : mediaSuperview )
let frameInBubble : CGRect = messageCell . bubbleView . convert ( targetView . frame , from : mediaSuperview )
2022-05-13 10:07:24 +02:00
if messageCell . bubbleView . bounds = = targetView . bounds {
cornerRadius = messageCell . bubbleView . layer . cornerRadius
cornerMask = messageCell . bubbleView . layer . maskedCorners
}
else {
// I f t h e f r a m e s d o n ' t m a t c h t h e n a s s u m e i t ' s e i t h e r m u l t i p l e i m a g e s o r t h e r e i s a c a p t i o n
// a n d d e t e r m i n e w h i c h c o r n e r s n e e d t o b e r o u n d e d
cornerRadius = messageCell . bubbleView . layer . cornerRadius
var newCornerMask = CACornerMask ( )
let cellMaskedCorners : CACornerMask = messageCell . bubbleView . layer . maskedCorners
if
cellMaskedCorners . contains ( . layerMinXMinYCorner ) &&
2022-05-20 09:58:39 +02:00
frameInBubble . minX < CGFloat . leastNonzeroMagnitude &&
frameInBubble . minY < CGFloat . leastNonzeroMagnitude
2022-05-13 10:07:24 +02:00
{
newCornerMask . insert ( . layerMinXMinYCorner )
}
if
cellMaskedCorners . contains ( . layerMaxXMinYCorner ) &&
2022-05-20 09:58:39 +02:00
abs ( frameInBubble . maxX - messageCell . bubbleView . bounds . width ) < CGFloat . leastNonzeroMagnitude &&
frameInBubble . minY < CGFloat . leastNonzeroMagnitude
2022-05-13 10:07:24 +02:00
{
newCornerMask . insert ( . layerMaxXMinYCorner )
}
if
cellMaskedCorners . contains ( . layerMinXMaxYCorner ) &&
2022-05-20 09:58:39 +02:00
frameInBubble . minX < CGFloat . leastNonzeroMagnitude &&
abs ( frameInBubble . maxY - messageCell . bubbleView . bounds . height ) < CGFloat . leastNonzeroMagnitude
2022-05-13 10:07:24 +02:00
{
newCornerMask . insert ( . layerMinXMaxYCorner )
}
if
cellMaskedCorners . contains ( . layerMaxXMaxYCorner ) &&
2022-05-20 09:58:39 +02:00
abs ( frameInBubble . maxX - messageCell . bubbleView . bounds . width ) < CGFloat . leastNonzeroMagnitude &&
abs ( frameInBubble . maxY - messageCell . bubbleView . bounds . height ) < CGFloat . leastNonzeroMagnitude
2022-05-13 10:07:24 +02:00
{
newCornerMask . insert ( . layerMaxXMaxYCorner )
}
cornerMask = newCornerMask
}
2022-05-20 09:58:39 +02:00
2022-05-13 10:07:24 +02:00
return MediaPresentationContext (
mediaView : targetView ,
presentationFrame : presentationFrame ,
cornerRadius : cornerRadius ,
cornerMask : cornerMask
)
}
func snapshotOverlayView ( in coordinateSpace : UICoordinateSpace ) -> ( UIView , CGRect ) ? {
return self . navigationController ? . navigationBar . generateSnapshot ( in : coordinateSpace )
}
}