From 9c8653aa213bcd90d909a3826b663d1f16e07cfc Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 May 2023 13:09:05 +1000 Subject: [PATCH] Updated the profile picture management UI Updated the UI to use a modal Added the ability to remove the profile picture --- Session/Closed Groups/EditClosedGroupVC.swift | 2 +- Session/Closed Groups/NewClosedGroupVC.swift | 4 +- .../ConversationVC+Interaction.swift | 64 ++- Session/Conversations/ConversationVC.swift | 2 +- .../Settings/ThreadSettingsViewModel.swift | 17 +- Session/Home/HomeVC.swift | 4 +- Session/Home/New Conversation/NewDMVC.swift | 2 +- .../GIFs/GifPickerViewController.swift | 4 +- .../PhotoCaptureViewController.swift | 2 +- .../Contents.json | 23 + .../profile_placeholder.png | Bin 0 -> 2114 bytes .../profile_placeholder@2x.png | Bin 0 -> 4042 bytes .../profile_placeholder@3x.png | Bin 0 -> 5969 bytes .../Translations/de.lproj/Localizable.strings | 3 + .../Translations/en.lproj/Localizable.strings | 3 + .../Translations/es.lproj/Localizable.strings | 3 + .../Translations/fa.lproj/Localizable.strings | 3 + .../Translations/fi.lproj/Localizable.strings | 3 + .../Translations/fr.lproj/Localizable.strings | 3 + .../Translations/hi.lproj/Localizable.strings | 3 + .../Translations/hr.lproj/Localizable.strings | 3 + .../id-ID.lproj/Localizable.strings | 3 + .../Translations/it.lproj/Localizable.strings | 3 + .../Translations/ja.lproj/Localizable.strings | 3 + .../Translations/nl.lproj/Localizable.strings | 3 + .../Translations/pl.lproj/Localizable.strings | 3 + .../pt_BR.lproj/Localizable.strings | 3 + .../Translations/ru.lproj/Localizable.strings | 3 + .../Translations/si.lproj/Localizable.strings | 3 + .../Translations/sk.lproj/Localizable.strings | 3 + .../Translations/sv.lproj/Localizable.strings | 3 + .../Translations/th.lproj/Localizable.strings | 3 + .../vi-VN.lproj/Localizable.strings | 3 + .../zh-Hant.lproj/Localizable.strings | 3 + .../zh_CN.lproj/Localizable.strings | 3 + Session/Onboarding/DisplayNameVC.swift | 2 +- Session/Onboarding/LinkDeviceVC.swift | 4 +- Session/Onboarding/RestoreVC.swift | 2 +- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- Session/Settings/NukeDataModal.swift | 10 +- .../Settings/PrivacySettingsViewModel.swift | 4 +- Session/Settings/QRCodeVC.swift | 2 +- Session/Settings/SeedModal.swift | 4 +- Session/Settings/SettingsViewModel.swift | 161 ++++-- Session/Shared/ScreenLockUI.swift | 3 +- .../Shared/SessionTableViewController.swift | 10 +- Session/Utilities/Permissions.swift | 26 +- .../SAEScreenLockViewController.swift | 2 +- SessionShareExtension/ShareVC.swift | 2 +- .../Components/ConfirmationModal.swift | 536 ++++++++++++------ SessionUIKit/Components/Modal.swift | 30 +- SessionUIKit/Style Guide/Values.swift | 2 +- .../MediaMessageView.swift | 2 +- 53 files changed, 689 insertions(+), 305 deletions(-) create mode 100644 Session/Meta/Images.xcassets/profile_placeholder.imageset/Contents.json create mode 100644 Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder.png create mode 100644 Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@2x.png create mode 100644 Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@3x.png diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 2a04c703f..bc7f80f8e 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -485,7 +485,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat targetView: self.view, info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 1d9f66159..d5cc50a83 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -305,7 +305,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text @@ -350,7 +350,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate targetView: self?.view, info: ConfirmationModal.Info( title: "GROUP_CREATION_ERROR_TITLE".localized(), - explanation: "GROUP_CREATION_ERROR_MESSAGE".localized(), + body: .text("GROUP_CREATION_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2d98029d5..6f8cfb32b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -68,7 +68,7 @@ extension ConversationVC: let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_call_permission_request_title".localized(), - explanation: "modal_call_permission_request_explanation".localized(), + body: .text("modal_call_permission_request_explanation".localized()), confirmTitle: "vc_settings_title".localized(), confirmAccessibilityLabel: "Settings", cancelAccessibilityLabel: "Cancel", @@ -132,11 +132,13 @@ extension ConversationVC: format: "modal_blocked_title".localized(), self.viewModel.threadData.displayName ), - attributedExplanation: NSAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: self.viewModel.threadData.displayName) - ), + body: .attributedText( + NSAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: self.viewModel.threadData.displayName) + ) + ), confirmTitle: "modal_blocked_button_title".localized(), confirmAccessibilityLabel: "Confirm block", cancelAccessibilityLabel: "Cancel block", @@ -205,7 +207,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "GIPHY_PERMISSION_TITLE".localized(), - explanation: "GIPHY_PERMISSION_MESSAGE".localized(), + body: .text("GIPHY_PERMISSION_MESSAGE".localized()), confirmTitle: "continue_2".localized() ) { [weak self] _ in Storage.shared.writeAsync( @@ -295,7 +297,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: "Session", - explanation: "An error occurred.", + body: .text("An error occurred."), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -312,7 +314,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE".localized(), - explanation: "ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized(), + body: .text("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -410,7 +412,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_send_seed_title".localized(), - explanation: "modal_send_seed_explanation".localized(), + body: .text("modal_send_seed_explanation".localized()), confirmTitle: "modal_send_seed_send_button_title".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -540,7 +542,7 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_send_seed_title".localized(), - explanation: "modal_send_seed_explanation".localized(), + body: .text("modal_send_seed_explanation".localized()), confirmTitle: "modal_send_seed_send_button_title".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -646,7 +648,7 @@ extension ConversationVC: let linkPreviewModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_link_previews_title".localized(), - explanation: "modal_link_previews_explanation".localized(), + body: .text("modal_link_previews_explanation".localized()), confirmTitle: "modal_link_previews_button_title".localized() ) { [weak self] _ in Storage.shared.writeAsync { db in @@ -890,11 +892,13 @@ extension ConversationVC: format: "modal_download_attachment_title".localized(), cellViewModel.authorName ), - attributedExplanation: NSAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: cellViewModel.authorName) - ), + body: .attributedText( + NSAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: cellViewModel.authorName) + ) + ), confirmTitle: "modal_download_button_title".localized(), confirmAccessibilityLabel: "Download media", cancelAccessibilityLabel: "Don't download media", @@ -1541,11 +1545,13 @@ extension ConversationVC: let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "Join \(finalName)?", - attributedExplanation: NSMutableAttributedString(string: message) - .adding( - attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], - range: (message as NSString).range(of: finalName) - ), + body: .attributedText( + NSMutableAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: finalName) + ) + ), confirmTitle: "JOIN_COMMUNITY_BUTTON_TITLE".localized(), onConfirm: { modal in guard let presentingViewController: UIViewController = modal.presentingViewController else { @@ -1582,7 +1588,7 @@ extension ConversationVC: let errorModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "COMMUNITY_ERROR_GENERIC".localized(), - explanation: error.localizedDescription, + body: .text(error.localizedDescription), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2048,7 +2054,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "Session", - explanation: "This will ban the selected user from this room. It won't ban them from other rooms.", + body: .text("This will ban the selected user from this room. It won't ban them from other rooms."), confirmTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, onConfirm: { [weak self] _ in @@ -2072,7 +2078,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "context_menu_ban_user_error_alert_message".localized(), + body: .text("context_menu_ban_user_error_alert_message".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2097,7 +2103,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "Session", - explanation: "This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.", + body: .text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there."), confirmTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, onConfirm: { [weak self] _ in @@ -2121,7 +2127,7 @@ extension ConversationVC: targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "context_menu_ban_user_error_alert_message".localized(), + body: .text("context_menu_ban_user_error_alert_message".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2231,7 +2237,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE".localized(), - explanation: "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized(), + body: .text("VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -2302,7 +2308,7 @@ extension ConversationVC: targetView: self.view, info: ConfirmationModal.Info( title: "ATTACHMENT_ERROR_ALERT_TITLE".localized(), - explanation: (attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage), + body: .text(attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, afterClosed: onDismiss diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 10c6922b6..45a7860c0 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1299,7 +1299,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl targetView: self?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(), + body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 447732eeb..fe5722e7d 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -3,6 +3,7 @@ import Foundation import Combine import GRDB +import YYImage import DifferenceKit import SessionUIKit import SessionMessagingKit @@ -395,7 +396,7 @@ class ThreadSettingsViewModel: SessionTableViewModelb{fy5a@?f@x^%`ZE<#+H%A&%R zt;S4>Fd;lDn29`Y3={!X$b;>C|7BM>o$e%^ZtqU7-;B7^0|bA4ul;U!>7WRin3(A0 zL*mn?Pv+;(pTFUAv!kQK#B+S#w7vY4pUd*&r+l8}^S6d!WcYlWIi-l`pb$}lef;?G zHVcq)5UEzOe9LrpcCOu-%P4cvV2?G-Q8U!O=uC2 ztkX`mUpI)cN;W~$y`vU_5N5{v<+vJ0qEl|d~ z1sUranPqnAM^qy<2?&?HxVU)1fq8Ld)dIGFcnVJZkJYaH#@j^B~%2H84uJax@OSbg_ zmuC~~bqkeVSGJIrL$hD0b-6V-ICyq+bo7VY3tVHMjC+4k{WJuk;*{gzw{G#!)rDES z$J7=N^vZF*BaSXCUH2DiqcC~g{MKpQ>}G)#gJ%n};FOh11mvt+&;7h8L1|OcI4MdZ zK^4ryBf~U2JWL<*ALu-PPAA7F;&^s;M!B;bH4sQB*Q^(8U6l=g(?l z_toQ0^!N9d{7frCNGY<_iVGn_pB49xjg2Mt_VzNkW3|A$q#67%{6^yZFVkdA`5|8w0Kv%qIKH262Bro6o`$V(cN?T{SX-Mx%p<3{J=w7SYbw-;|tO= z$G?B6Ntv3OqN(qu3itQ-_Gx2dgM3g(Cc*+`^3Vb-=*yum3oaKWaNUE01KQl&6xKI0 zGb5~VbZnH~Y`*bR0rX1E(#nOkxG!SZ+}vEjW$FQlw6?Y;To)hio#Y+ycu#kauM&#g zIKhXsDx?#n0k}#%i3jleDABBm1SOImI2#M2i^=$Bk_P(v`ot0Ll712Ryq=0AzfX!p zqLPqGWI#g!Z$dk`u(oC?Tm*lV*|r9*bGDKIy+@))MjDhHv?NR z4UO}ZE8Pbzo=~h-N}Hj|Gj!#)4VZg>_wQ{kcVl2mdz^Um0XBE%N&B>ee;K75cT^bEmjhsee zfr^ol5n)AeNnCV7*Th;_L)5#eyRKbm1*~!Y-n?*W#MyG_EZnj5ED{TZFt8|yqskbF zfFfJPM_APS{Cq|66!W$uSLiV`)q1_WS@g`80|!IG^I^J8jmu-1cc zPOPWb#S{-6Ap7p@>|9g#W40%suqD(6mtzB={r)b{= z`xIZyQuv43%NraK-50|sT0>XV(nnx~6;c~VcL8A?-|BA^LhK3KFr zULqi@(C+7LN@JhGb*&s{WGr_`P#cim-QAsadcyUxCRf@&QUIfqMuS}!(|KKY^>AJ8 z`u_d<)4{<(jK1$sYx5J=eHphW%dQv3I$5sfzu-^d7PTN4opItQyI#8D%mi*4E@EF5 zYBBIPvv?IZ6{;wpXc`*EY%7&EvS6OA>a|oQScu4lb1;SKOc&6HT==3}146W>cd7#kZ~mJ9D17L+QeG-}=X*Y^&Va)XM7 z+u;Kv3~t>m?eFg|lMj6FV2Pb9u3o*mCSziw>|%*Mr;|Ls^)@}$0qvX6@+Z4cuL(x( zgkFD}(AweHxX3~IUcCqjcjd|fv2I!SF6TjR#El(55>Z-Ykir$-gF-;=$ADYl)94@? zW?INbK!yS)^nXSmS9+)T5529+G7;O@($@Kyk=wpE*FPh01A<;$TugAhOvp8Rrd)c4 se~(dA7O7^2oX?1@-T%gU*n-*l56;rdG;@S6?f?J)07*qoM6N<$f(;e$9RL6T literal 0 HcmV?d00001 diff --git a/Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@2x.png b/Session/Meta/Images.xcassets/profile_placeholder.imageset/profile_placeholder@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..02649edd922560db938432934f255b15f30713f8 GIT binary patch literal 4042 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 z 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 literal 0 HcmV?d00001 diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 91de95f61..1d590e0d3 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 744b6f641..cf97bdb51 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 0e0b642bf..ee4801866 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index d54a2f83e..4b1b87715 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index b5d2ed518..a75e1070c 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 1ef9e1825..e5dffdb33 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 02573d3b8..ca058e22a 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 0facdab28..e6cd0d2a3 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 20c6c9c0b..c620b6ae6 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index c570b1477..2c1b9996f 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 12a9dcb22..cc28d1898 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 22dc0471d..477156edf 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 918b83ece..69e08221d 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index ee83738af..02587c0a2 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index f38d284ad..d3f3b00b7 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index e7642ad63..c33b738d8 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index db623fbb2..4f924c6fe 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 28bda12ed..2fa76cecb 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 0b000b1c2..6aae143ff 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index eefe6cd65..a146cdbf7 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index ded529a9e..8725b7d5f 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 4627f4fd9..d4f9a737b 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -623,3 +623,6 @@ "group_unable_to_leave" = "Unable to leave the Group, please try again"; "delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; "delete_conversation_confirmation_alert_title" = "Delete Conversation"; +"update_profile_modal_title" = "Set Display Picture"; +"update_profile_modal_upload" = "Upload"; +"update_profile_modal_remove" = "Remove"; diff --git a/Session/Onboarding/DisplayNameVC.swift b/Session/Onboarding/DisplayNameVC.swift index cf0fc7028..aaf6a297b 100644 --- a/Session/Onboarding/DisplayNameVC.swift +++ b/Session/Onboarding/DisplayNameVC.swift @@ -158,7 +158,7 @@ final class DisplayNameVC: BaseVC { targetView: self.view, info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Onboarding/LinkDeviceVC.swift b/Session/Onboarding/LinkDeviceVC.swift index fcab1aa90..da5802576 100644 --- a/Session/Onboarding/LinkDeviceVC.swift +++ b/Session/Onboarding/LinkDeviceVC.swift @@ -143,7 +143,7 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "invalid_recovery_phrase".localized(), - explanation: "INVALID_RECOVERY_PHRASE_MESSAGE".localized(), + body: .text("INVALID_RECOVERY_PHRASE_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, afterClosed: { [weak self] in @@ -321,7 +321,7 @@ private final class RecoveryPhraseVC: UIViewController { targetView: self.view, info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Onboarding/RestoreVC.swift b/Session/Onboarding/RestoreVC.swift index 5fbf82436..92f42d82d 100644 --- a/Session/Onboarding/RestoreVC.swift +++ b/Session/Onboarding/RestoreVC.swift @@ -186,7 +186,7 @@ final class RestoreVC: BaseVC { let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 2b84bb734..7f0e0386b 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -211,7 +211,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: title, - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index d8a791a70..e5dc30044 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -9,8 +9,8 @@ import SignalUtilitiesKit final class NukeDataModal: Modal { // MARK: - Initialization - override init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) { - super.init(targetView: targetView, afterClosed: afterClosed) + override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { + super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve @@ -135,7 +135,7 @@ final class NukeDataModal: Modal { let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "modal_clear_all_data_title".localized(), - explanation: "modal_clear_all_data_explanation_2".localized(), + body: .text("modal_clear_all_data_explanation_2".localized()), confirmTitle: "modal_clear_all_data_confirm".localized(), confirmStyle: .danger, cancelStyle: .alert_text, @@ -184,7 +184,7 @@ final class NukeDataModal: Modal { targetView: self?.view, info: ConfirmationModal.Info( title: "ALERT_ERROR_TITLE".localized(), - explanation: message, + body: .text(message), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) @@ -199,7 +199,7 @@ final class NukeDataModal: Modal { targetView: self?.view, info: ConfirmationModal.Info( title: "ALERT_ERROR_TITLE".localized(), - explanation: error.localizedDescription, + body: .text(error.localizedDescription), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text ) diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index b7a279b5e..e4f2b7690 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -209,8 +209,8 @@ class PrivacySettingsViewModel: SessionTableViewModel ())? = nil) { - super.init(targetView: targetView, afterClosed: afterClosed) + override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { + super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 476277407..509adaee6 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -50,6 +50,10 @@ class SettingsViewModel: SessionTableViewModel { - navState - .map { [weak self] navState -> [NavItem] in - switch navState { - case .standard: - return [ + let userSessionId: String = self.userSessionId + + return navState + .map { [weak self] navState -> [NavItem] in + switch navState { + case .standard: + return [ NavItem( id: .qrCode, image: UIImage(named: "QRCode")? @@ -117,10 +123,10 @@ class SettingsViewModel: SessionTableViewModel ())? = nil ) { - let imageFilePath: String? = ( - profilePictureFilePath ?? - ProfileManager.profileAvatarFilepath(id: self.userSessionId) - ) - let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in ProfileManager.updateLocal( queue: DispatchQueue.global(qos: .default), profileName: name, image: profilePicture, - imageFilePath: imageFilePath, + imageFilePath: profilePictureFilePath, success: { db, updatedProfile in if isUpdatingDisplayName { UserDefaults.standard[.lastDisplayNameUpdate] = Date() @@ -453,7 +522,9 @@ class SettingsViewModel: SessionTableViewModel Bool { - switch self { - case .whenEnabled: return (value == true) - case .whenDisabled: return (value == false) - case .always: return true - } - } - } - - let title: String - let explanation: String? - let attributedExplanation: NSAttributedString? - let accessibilityLabel: String? - let accessibilityIdentifier: String? - public let stateToShow: State - let confirmTitle: String? - let confirmAccessibilityLabel: String? - let confirmStyle: ThemeValue - let cancelTitle: String - let cancelAccessibilityLabel: String? - let cancelStyle: ThemeValue - let dismissOnConfirm: Bool - let onConfirm: ((UIViewController) -> ())? - let afterClosed: (() -> ())? - - // MARK: - Initialization - - public init( - title: String, - explanation: String? = nil, - attributedExplanation: NSAttributedString? = nil, - accessibilityLabel: String? = nil, - accessibilityId: String? = nil, - stateToShow: State = .always, - confirmTitle: String? = nil, - confirmAccessibilityLabel: String? = nil, - confirmStyle: ThemeValue = .alert_text, - cancelTitle: String = "TXT_CANCEL_TITLE".localized(), - cancelAccessibilityLabel: String? = nil, - cancelStyle: ThemeValue = .danger, - dismissOnConfirm: Bool = true, - onConfirm: ((UIViewController) -> ())? = nil, - afterClosed: (() -> ())? = nil - ) { - self.title = title - self.explanation = explanation - self.attributedExplanation = attributedExplanation - self.accessibilityLabel = accessibilityLabel - self.accessibilityIdentifier = accessibilityId - self.stateToShow = stateToShow - self.confirmTitle = confirmTitle - self.confirmAccessibilityLabel = confirmAccessibilityLabel - self.confirmStyle = confirmStyle - self.cancelTitle = cancelTitle - self.cancelAccessibilityLabel = cancelAccessibilityLabel - self.cancelStyle = cancelStyle - self.dismissOnConfirm = dismissOnConfirm - self.onConfirm = onConfirm - self.afterClosed = afterClosed - } - - // MARK: - Mutation - - public func with( - onConfirm: ((UIViewController) -> ())? = nil, - afterClosed: (() -> ())? = nil - ) -> Info { - return Info( - title: self.title, - explanation: self.explanation, - attributedExplanation: self.attributedExplanation, - accessibilityLabel: self.accessibilityLabel, - stateToShow: self.stateToShow, - confirmTitle: self.confirmTitle, - confirmAccessibilityLabel: self.confirmAccessibilityLabel, - confirmStyle: self.confirmStyle, - cancelTitle: self.cancelTitle, - cancelAccessibilityLabel: self.cancelAccessibilityLabel, - cancelStyle: self.cancelStyle, - dismissOnConfirm: self.dismissOnConfirm, - onConfirm: (onConfirm ?? self.onConfirm), - afterClosed: (afterClosed ?? self.afterClosed) - ) - } - - // MARK: - Confirmance - - public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool { - return ( - lhs.title == rhs.title && - lhs.explanation == rhs.explanation && - lhs.attributedExplanation == rhs.attributedExplanation && - lhs.accessibilityLabel == rhs.accessibilityLabel && - lhs.stateToShow == rhs.stateToShow && - lhs.confirmTitle == rhs.confirmTitle && - lhs.confirmAccessibilityLabel == rhs.confirmAccessibilityLabel && - lhs.confirmStyle == rhs.confirmStyle && - lhs.cancelTitle == rhs.cancelTitle && - lhs.cancelAccessibilityLabel == rhs.cancelAccessibilityLabel && - lhs.cancelStyle == rhs.cancelStyle && - lhs.dismissOnConfirm == rhs.dismissOnConfirm - ) - } - - public func hash(into hasher: inout Hasher) { - title.hash(into: &hasher) - explanation.hash(into: &hasher) - attributedExplanation.hash(into: &hasher) - accessibilityLabel.hash(into: &hasher) - stateToShow.hash(into: &hasher) - confirmTitle.hash(into: &hasher) - confirmAccessibilityLabel.hash(into: &hasher) - confirmStyle.hash(into: &hasher) - cancelTitle.hash(into: &hasher) - cancelAccessibilityLabel.hash(into: &hasher) - cancelStyle.hash(into: &hasher) - dismissOnConfirm.hash(into: &hasher) - } - } + private static let imageSize: CGFloat = 80 + private static let closeSize: CGFloat = 24 - private let internalOnConfirm: (UIViewController) -> () + private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil + private var internalOnCancel: ((ConfirmationModal) -> ())? = nil + private var internalOnBodyTap: (() -> ())? = nil // MARK: - Components @@ -151,6 +32,24 @@ public class ConfirmationModal: Modal { result.textAlignment = .center result.lineBreakMode = .byWordWrapping result.numberOfLines = 0 + result.isHidden = true + + return result + }() + + private lazy var imageViewContainer: UIView = { + let result: UIView = UIView() + result.isHidden = true + + return result + }() + + private lazy var imageView: UIImageView = { + let result: UIImageView = UIImageView() + result.clipsToBounds = true + result.contentMode = .scaleAspectFill + result.set(.width, to: ConfirmationModal.imageSize) + result.set(.height, to: ConfirmationModal.imageSize) return result }() @@ -174,7 +73,7 @@ public class ConfirmationModal: Modal { }() private lazy var contentStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ]) + let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ]) result.axis = .vertical result.spacing = Values.smallSpacing result.isLayoutMarginsRelativeArrangement = true @@ -185,13 +84,41 @@ public class ConfirmationModal: Modal { right: Values.largeSpacing ) + let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(bodyTapped) + ) + result.addGestureRecognizer(gestureRecogniser) + return result }() private lazy var mainStackView: UIStackView = { let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ]) result.axis = .vertical - result.spacing = Values.largeSpacing - Values.smallFontSize / 2 + + return result + }() + + private lazy var closeButton: UIButton = { + let result: UIButton = UIButton() + result.setImage( + UIImage(named: "X")? + .withRenderingMode(.alwaysTemplate), + for: .normal + ) + result.imageView?.contentMode = .scaleAspectFit + result.themeTintColor = .textPrimary + result.contentEdgeInsets = UIEdgeInsets( + top: 6, + left: 6, + bottom: 6, + right: 6 + ) + result.set(.width, to: ConfirmationModal.closeSize) + result.set(.height, to: ConfirmationModal.closeSize) + result.addTarget(self, action: #selector(close), for: .touchUpInside) + result.isHidden = true return result }() @@ -199,50 +126,11 @@ public class ConfirmationModal: Modal { // MARK: - Lifecycle public init(targetView: UIView? = nil, info: Info) { - self.internalOnConfirm = { viewController in - if info.dismissOnConfirm { - viewController.dismiss(animated: true) - } - - info.onConfirm?(viewController) - } - - super.init(targetView: targetView, afterClosed: info.afterClosed) + super.init(targetView: targetView, dismissType: info.dismissType, afterClosed: info.afterClosed) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve - - // Set the content based on the provided info - titleLabel.text = info.title - - // Note: We should only set the appropriate explanation/attributedExplanation value (as - // setting both when one is null can result in the other being removed) - if let explanation: String = info.explanation { - explanationLabel.text = explanation - } - - if let attributedExplanation: NSAttributedString = info.attributedExplanation { - explanationLabel.attributedText = attributedExplanation - } - - explanationLabel.isHidden = ( - info.explanation == nil && - info.attributedExplanation == nil - ) - confirmButton.accessibilityLabel = info.confirmAccessibilityLabel - confirmButton.accessibilityIdentifier = info.confirmAccessibilityLabel - confirmButton.isAccessibilityElement = true - confirmButton.setTitle(info.confirmTitle, for: .normal) - confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal) - confirmButton.isHidden = (info.confirmTitle == nil) - cancelButton.accessibilityLabel = info.cancelAccessibilityLabel - cancelButton.accessibilityIdentifier = info.cancelAccessibilityLabel - cancelButton.isAccessibilityElement = true - cancelButton.setTitle(info.cancelTitle, for: .normal) - cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal) - - self.contentView.accessibilityLabel = info.accessibilityLabel - self.contentView.accessibilityIdentifier = info.accessibilityIdentifier + self.updateContent(with: info) } required init?(coder: NSCoder) { @@ -251,13 +139,321 @@ public class ConfirmationModal: Modal { public override func populateContentView() { contentView.addSubview(mainStackView) + contentView.addSubview(closeButton) + + imageViewContainer.addSubview(imageView) + imageView.center(.horizontal, in: imageViewContainer) + imageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15) + imageView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -15) mainStackView.pin(to: contentView) + closeButton.pin(.top, to: .top, of: contentView, withInset: 8) + closeButton.pin(.right, to: .right, of: contentView, withInset: -8) + } + + // MARK: - Content + + public func updateContent(with info: Info) { + internalOnBodyTap = nil + internalOnConfirm = { modal in + if info.dismissOnConfirm { + modal.close() + } + + info.onConfirm?(modal) + } + internalOnCancel = { modal in + guard info.onCancel != nil else { return modal.close() } + + info.onCancel?(modal) + } + + // Set the content based on the provided info + titleLabel.text = info.title + + switch info.body { + case .none: + mainStackView.spacing = Values.smallSpacing + + case .text(let text): + mainStackView.spacing = Values.smallSpacing + explanationLabel.text = text + explanationLabel.isHidden = false + + case .attributedText(let attributedText): + mainStackView.spacing = Values.smallSpacing + explanationLabel.attributedText = attributedText + explanationLabel.isHidden = false + + case .image(let placeholder, let value, let style, let onClick): + mainStackView.spacing = 0 + imageView.image = (value ?? placeholder) + imageView.layer.cornerRadius = (style == .circular ? + (ConfirmationModal.imageSize / 2) : + 0 + ) + imageViewContainer.isHidden = false + internalOnBodyTap = onClick + } + + confirmButton.accessibilityLabel = info.confirmAccessibilityLabel + confirmButton.accessibilityIdentifier = info.confirmAccessibilityLabel + confirmButton.isAccessibilityElement = true + confirmButton.setTitle(info.confirmTitle, for: .normal) + confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal) + confirmButton.setThemeTitleColor(.disabled, for: .disabled) + confirmButton.isHidden = (info.confirmTitle == nil) + confirmButton.isEnabled = info.confirmEnabled + cancelButton.accessibilityLabel = info.cancelAccessibilityLabel + cancelButton.accessibilityIdentifier = info.cancelAccessibilityLabel + cancelButton.isAccessibilityElement = true + cancelButton.setTitle(info.cancelTitle, for: .normal) + cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal) + cancelButton.setThemeTitleColor(.disabled, for: .disabled) + cancelButton.isEnabled = info.cancelEnabled + closeButton.isHidden = !info.hasCloseButton + + contentView.accessibilityLabel = info.accessibilityLabel + contentView.accessibilityIdentifier = info.accessibilityIdentifier } // MARK: - Interaction + @objc private func bodyTapped() { + internalOnBodyTap?() + } + @objc private func confirmationPressed() { - internalOnConfirm(self) + internalOnConfirm?(self) + } + + override public func cancel() { + internalOnCancel?(self) + } +} + +// MARK: - Types + +public extension ConfirmationModal { + struct Info: Equatable, Hashable { + let title: String + let body: Body + let accessibilityLabel: String? + let accessibilityIdentifier: String? + public let showCondition: ShowCondition + let confirmTitle: String? + let confirmAccessibilityLabel: String? + let confirmStyle: ThemeValue + let confirmEnabled: Bool + let cancelTitle: String + let cancelAccessibilityLabel: String? + let cancelStyle: ThemeValue + let cancelEnabled: Bool + let hasCloseButton: Bool + let dismissOnConfirm: Bool + let dismissType: Modal.DismissType + let onConfirm: ((ConfirmationModal) -> ())? + let onCancel: ((ConfirmationModal) -> ())? + let afterClosed: (() -> ())? + + // MARK: - Initialization + + public init( + title: String, + body: Body = .none, + accessibilityLabel: String? = nil, + accessibilityId: String? = nil, + showCondition: ShowCondition = .none, + confirmTitle: String? = nil, + confirmAccessibilityLabel: String? = nil, + confirmStyle: ThemeValue = .alert_text, + confirmEnabled: Bool = true, + cancelTitle: String = "TXT_CANCEL_TITLE".localized(), + cancelAccessibilityLabel: String? = nil, + cancelStyle: ThemeValue = .danger, + cancelEnabled: Bool = true, + hasCloseButton: Bool = false, + dismissOnConfirm: Bool = true, + dismissType: Modal.DismissType = .recursive, + onConfirm: ((ConfirmationModal) -> ())? = nil, + onCancel: ((ConfirmationModal) -> ())? = nil, + afterClosed: (() -> ())? = nil + ) { + self.title = title + self.body = body + self.accessibilityLabel = accessibilityLabel + self.accessibilityIdentifier = accessibilityId + self.showCondition = showCondition + self.confirmTitle = confirmTitle + self.confirmAccessibilityLabel = confirmAccessibilityLabel + self.confirmStyle = confirmStyle + self.confirmEnabled = confirmEnabled + self.cancelTitle = cancelTitle + self.cancelAccessibilityLabel = cancelAccessibilityLabel + self.cancelStyle = cancelStyle + self.cancelEnabled = cancelEnabled + self.hasCloseButton = hasCloseButton + self.dismissOnConfirm = dismissOnConfirm + self.dismissType = dismissType + self.onConfirm = onConfirm + self.onCancel = onCancel + self.afterClosed = afterClosed + } + + // MARK: - Mutation + + public func with( + body: Body? = nil, + confirmEnabled: Bool? = nil, + cancelEnabled: Bool? = nil, + onConfirm: ((ConfirmationModal) -> ())? = nil, + onCancel: ((ConfirmationModal) -> ())? = nil, + afterClosed: (() -> ())? = nil + ) -> Info { + return Info( + title: self.title, + body: (body ?? self.body), + accessibilityLabel: self.accessibilityLabel, + showCondition: self.showCondition, + confirmTitle: self.confirmTitle, + confirmAccessibilityLabel: self.confirmAccessibilityLabel, + confirmStyle: self.confirmStyle, + confirmEnabled: (confirmEnabled ?? self.confirmEnabled), + cancelTitle: self.cancelTitle, + cancelAccessibilityLabel: self.cancelAccessibilityLabel, + cancelStyle: self.cancelStyle, + cancelEnabled: (cancelEnabled ?? self.cancelEnabled), + hasCloseButton: self.hasCloseButton, + dismissOnConfirm: self.dismissOnConfirm, + dismissType: self.dismissType, + onConfirm: (onConfirm ?? self.onConfirm), + onCancel: (onCancel ?? self.onCancel), + afterClosed: (afterClosed ?? self.afterClosed) + ) + } + + // MARK: - Confirmance + + public static func == (lhs: ConfirmationModal.Info, rhs: ConfirmationModal.Info) -> Bool { + return ( + lhs.title == rhs.title && + lhs.body == rhs.body && + lhs.accessibilityLabel == rhs.accessibilityLabel && + lhs.showCondition == rhs.showCondition && + lhs.confirmTitle == rhs.confirmTitle && + lhs.confirmAccessibilityLabel == rhs.confirmAccessibilityLabel && + lhs.confirmStyle == rhs.confirmStyle && + lhs.confirmEnabled == rhs.confirmEnabled && + lhs.cancelTitle == rhs.cancelTitle && + lhs.cancelAccessibilityLabel == rhs.cancelAccessibilityLabel && + lhs.cancelStyle == rhs.cancelStyle && + lhs.cancelEnabled == rhs.cancelEnabled && + lhs.hasCloseButton == rhs.hasCloseButton && + lhs.dismissOnConfirm == rhs.dismissOnConfirm && + lhs.dismissType == rhs.dismissType + ) + } + + public func hash(into hasher: inout Hasher) { + title.hash(into: &hasher) + body.hash(into: &hasher) + accessibilityLabel.hash(into: &hasher) + showCondition.hash(into: &hasher) + confirmTitle.hash(into: &hasher) + confirmAccessibilityLabel.hash(into: &hasher) + confirmStyle.hash(into: &hasher) + confirmEnabled.hash(into: &hasher) + cancelTitle.hash(into: &hasher) + cancelAccessibilityLabel.hash(into: &hasher) + cancelStyle.hash(into: &hasher) + cancelEnabled.hash(into: &hasher) + hasCloseButton.hash(into: &hasher) + dismissOnConfirm.hash(into: &hasher) + dismissType.hash(into: &hasher) + } + } +} + +public extension ConfirmationModal.Info { + // MARK: - ShowCondition + + enum ShowCondition { + case none + case enabled + case disabled + + public func shouldShow(for value: Bool) -> Bool { + switch self { + case .none: return true + case .enabled: return (value == true) + case .disabled: return (value == false) + } + } + } + + // MARK: - Body + + enum Body: Equatable, Hashable { + public enum ImageStyle: Equatable, Hashable { + case inherit + case circular + } + + case none + case text(String) + case attributedText(NSAttributedString) + // FIXME: Implement these + // case input(placeholder: String, value: String?) + // case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)]) + case image( + placeholder: UIImage?, + value: UIImage?, + style: ImageStyle, + onClick: (() -> ()) + ) + + public static func == (lhs: ConfirmationModal.Info.Body, rhs: ConfirmationModal.Info.Body) -> Bool { + switch (lhs, rhs) { + case (.none, .none): return true + case (.text(let lhsText), .text(let rhsText)): return (lhsText == rhsText) + case (.attributedText(let lhsText), .attributedText(let rhsText)): return (lhsText == rhsText) + + // FIXME: Implement these + //case (.input(let lhsPlaceholder, let lhsValue), .input(let rhsPlaceholder, let rhsValue)): + // return ( + // lhsPlaceholder == rhsPlaceholder && + // lhsValue == rhsValue && + // ) + + // FIXME: Implement these + //case (.radio(let lhsExplanation, let lhsOptions), .radio(let rhsExplanation, let rhsOptions)): + // return ( + // lhsExplanation == rhsExplanation && + // lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" } + // ) + + case (.image(let lhsPlaceholder, let lhsValue, let lhsStyle, _), .image(let rhsPlaceholder, let rhsValue, let rhsStyle, _)): + return ( + lhsPlaceholder == rhsPlaceholder && + lhsValue == rhsValue && + lhsStyle == rhsStyle + ) + + default: return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .none: break + case .text(let text): text.hash(into: &hasher) + case .attributedText(let text): text.hash(into: &hasher) + + case .image(let placeholder, let value, let style, _): + placeholder.hash(into: &hasher) + value.hash(into: &hasher) + style.hash(into: &hasher) + } + } } } diff --git a/SessionUIKit/Components/Modal.swift b/SessionUIKit/Components/Modal.swift index f42d9bdd0..9d52c5748 100644 --- a/SessionUIKit/Components/Modal.swift +++ b/SessionUIKit/Components/Modal.swift @@ -6,6 +6,12 @@ import SessionUtilitiesKit open class Modal: UIViewController, UIGestureRecognizerDelegate { private static let cornerRadius: CGFloat = 11 + public enum DismissType: Equatable, Hashable { + case single + case recursive + } + + private let dismissType: DismissType private let afterClosed: (() -> ())? // MARK: - Components @@ -47,14 +53,19 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { public lazy var cancelButton: UIButton = { let result: UIButton = Modal.createButton(title: "cancel".localized(), titleColor: .textPrimary) - result.addTarget(self, action: #selector(close), for: .touchUpInside) + result.addTarget(self, action: #selector(cancel), for: .touchUpInside) return result }() // MARK: - Lifecycle - public init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) { + public init( + targetView: UIView? = nil, + dismissType: DismissType = .recursive, + afterClosed: (() -> ())? = nil + ) { + self.dismissType = dismissType self.afterClosed = afterClosed super.init(nibName: nil, bundle: nil) @@ -129,13 +140,22 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { // MARK: - Interaction - @objc func close() { + @objc public func cancel() { + close() + } + + @objc public final func close() { // Recursively dismiss all modals (ie. find the first modal presented by a non-modal // and get that to dismiss it's presented view controller) var targetViewController: UIViewController? = self - while targetViewController?.presentingViewController is Modal { - targetViewController = targetViewController?.presentingViewController + switch dismissType { + case .single: break + + case .recursive: + while targetViewController?.presentingViewController is Modal { + targetViewController = targetViewController?.presentingViewController + } } targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index fd12c70c9..a66d47801 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -21,7 +21,7 @@ public final class Values : NSObject { @objc public static let smallButtonHeight = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(28) @objc public static let mediumButtonHeight = isIPhone5OrSmaller ? CGFloat(30) : CGFloat(34) @objc public static let largeButtonHeight = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45) - @objc public static let alertButtonHeight: CGFloat = 50 + @objc public static let alertButtonHeight: CGFloat = 51 // 19px tall font with 16px margins @objc public static let accentLineThickness = CGFloat(4) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 99cd2c61b..f59f8357e 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -653,7 +653,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { targetView: CurrentAppContext().frontmostViewController()?.view, info: ConfirmationModal.Info( title: CommonStrings.errorAlertTitle, - explanation: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized(), + body: .text("INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text )