2022-02-05 20:10:47 +00:00
//
// C h a t I n f o V i e w . s w i f t
// S i m p l e X
//
// C r e a t e d b y E v g e n y P o b e r e z k i n o n 0 5 / 0 2 / 2 0 2 2 .
// C o p y r i g h t © 2 0 2 2 S i m p l e X C h a t . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
2025-04-14 21:25:32 +01:00
@ preconcurrency import SimpleXChat
2022-02-05 20:10:47 +00:00
2022-12-23 19:55:45 +00:00
func infoRow ( _ title : LocalizedStringKey , _ value : String ) -> some View {
2022-07-27 13:40:26 +04:00
HStack {
Text ( title )
Spacer ( )
Text ( value )
. foregroundStyle ( . secondary )
}
}
2022-12-23 19:55:45 +00:00
func infoRow ( _ title : Text , _ value : String ) -> some View {
HStack {
title
Spacer ( )
Text ( value )
. foregroundStyle ( . secondary )
}
}
2022-07-27 13:40:26 +04:00
func localizedInfoRow ( _ title : LocalizedStringKey , _ value : LocalizedStringKey ) -> some View {
HStack {
Text ( title )
Spacer ( )
Text ( value )
. foregroundStyle ( . secondary )
}
}
2024-07-03 22:42:13 +01:00
@ ViewBuilder func smpServers ( _ title : LocalizedStringKey , _ servers : [ String ] , _ secondaryColor : Color ) -> some View {
2023-06-19 14:46:08 +04:00
if servers . count > 0 {
2022-11-01 20:30:53 +00:00
HStack {
Text ( title ) . frame ( width : 120 , alignment : . leading )
Button ( serverHost ( servers [ 0 ] ) ) {
UIPasteboard . general . string = servers . joined ( separator : " ; " )
}
2024-07-03 22:42:13 +01:00
. foregroundColor ( secondaryColor )
2022-11-01 20:30:53 +00:00
. lineLimit ( 1 )
}
2022-07-27 13:40:26 +04:00
}
}
2024-05-20 17:49:19 +04:00
func serverHost ( _ s : String ) -> String {
2022-07-27 13:40:26 +04:00
if let i = s . range ( of : " @ " ) ? . lowerBound {
return String ( s [ i . . . ] . dropFirst ( ) )
} else {
return s
}
}
2023-07-13 23:48:25 +01:00
enum SendReceipts : Identifiable , Hashable {
case yes
case no
case userDefault ( Bool )
var id : Self { self }
var text : LocalizedStringKey {
switch self {
2023-07-14 13:15:27 +01:00
case . yes : return " yes "
case . no : return " no "
case let . userDefault ( on ) : return on ? " default (yes) " : " default (no) "
2023-07-13 23:48:25 +01:00
}
}
2023-07-16 14:55:31 +04:00
func bool ( ) -> Bool ? {
switch self {
case . yes : return true
case . no : return false
case . userDefault : return nil
}
}
static func fromBool ( _ enable : Bool ? , userDefault def : Bool ) -> SendReceipts {
if let enable = enable {
return enable ? . yes : . no
}
return . userDefault ( def )
}
2023-07-13 23:48:25 +01:00
}
2022-02-05 20:10:47 +00:00
struct ChatInfoView : View {
2022-02-07 10:36:11 +00:00
@ EnvironmentObject var chatModel : ChatModel
2024-07-03 22:42:13 +01:00
@ EnvironmentObject var theme : AppTheme
2022-07-30 13:03:44 +01:00
@ Environment ( \ . dismiss ) var dismiss : DismissAction
2024-08-13 23:08:04 +03:00
@ ObservedObject var networkModel = NetworkModel . shared
2022-02-05 20:10:47 +00:00
@ ObservedObject var chat : Chat
2022-11-16 20:26:43 +04:00
@ State var contact : Contact
2022-08-25 17:36:26 +04:00
@ State var localAlias : String
2024-12-06 14:44:56 +00:00
@ State var featuresAllowed : ContactFeaturesAllowed
@ State var currentFeaturesAllowed : ContactFeaturesAllowed
2024-08-05 12:58:24 +01:00
var onSearch : ( ) -> Void
@ State private var connectionStats : ConnectionStats ? = nil
@ State private var customUserProfile : Profile ? = nil
@ State private var connectionCode : String ? = nil
2022-08-25 17:36:26 +04:00
@ FocusState private var aliasTextFieldFocused : Bool
2022-07-27 13:40:26 +04:00
@ State private var alert : ChatInfoViewAlert ? = nil
2024-08-05 12:58:24 +01:00
@ State private var actionSheet : SomeActionSheet ? = nil
@ State private var sheet : SomeSheet < AnyView > ? = nil
@ State private var showConnectContactViaAddressDialog = false
2023-07-16 14:55:31 +04:00
@ State private var sendReceipts = SendReceipts . userDefault ( true )
@ State private var sendReceiptsUserDefault = true
2025-01-20 18:06:00 +00:00
@ State private var progressIndicator = false
2022-08-02 17:00:12 +04:00
@ AppStorage ( DEFAULT_DEVELOPER_TOOLS ) private var developerTools = false
2024-08-05 12:58:24 +01:00
2022-05-19 16:56:34 +04:00
enum ChatInfoViewAlert : Identifiable {
case clearChatAlert
2022-07-27 13:40:26 +04:00
case networkStatusAlert
2022-11-02 09:48:20 +00:00
case switchAddressAlert
2023-06-19 14:46:08 +04:00
case abortSwitchAddressAlert
2023-07-10 19:01:22 +04:00
case syncConnectionForceAlert
2024-05-31 12:45:58 +01:00
case queueInfo ( info : String )
2024-08-05 12:58:24 +01:00
case someAlert ( alert : SomeAlert )
2024-07-28 17:54:58 +01:00
case error ( title : LocalizedStringKey , error : LocalizedStringKey ? )
2022-05-19 16:56:34 +04:00
2022-09-21 17:18:48 +04:00
var id : String {
switch self {
case . clearChatAlert : return " clearChatAlert "
case . networkStatusAlert : return " networkStatusAlert "
2022-11-02 09:48:20 +00:00
case . switchAddressAlert : return " switchAddressAlert "
2023-06-19 14:46:08 +04:00
case . abortSwitchAddressAlert : return " abortSwitchAddressAlert "
2023-07-10 19:01:22 +04:00
case . syncConnectionForceAlert : return " syncConnectionForceAlert "
2024-05-31 12:45:58 +01:00
case let . queueInfo ( info ) : return " queueInfo \( info ) "
2024-08-05 12:58:24 +01:00
case let . someAlert ( alert ) : return " chatInfoSomeAlert \( alert . id ) "
2022-11-02 09:48:20 +00:00
case let . error ( title , _ ) : return " error \( title ) "
2022-09-21 17:18:48 +04:00
}
}
2022-05-19 16:56:34 +04:00
}
2024-08-05 12:58:24 +01:00
2022-02-05 20:10:47 +00:00
var body : some View {
2022-07-27 13:40:26 +04:00
NavigationView {
2025-01-20 18:06:00 +00:00
ZStack {
List {
contactInfoHeader ( )
. listRowBackground ( Color . clear )
. contentShape ( Rectangle ( ) )
. onTapGesture {
aliasTextFieldFocused = false
}
2022-12-12 08:59:35 +00:00
localAliasTextEdit ( )
2025-01-20 18:06:00 +00:00
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
. padding ( . bottom , 18 )
GeometryReader { g in
HStack ( alignment : . center , spacing : 8 ) {
let buttonWidth = g . size . width / 4
searchButton ( width : buttonWidth )
AudioCallButton ( chat : chat , contact : contact , connectionStats : $ connectionStats , width : buttonWidth ) { alert = . someAlert ( alert : $0 ) }
VideoButton ( chat : chat , contact : contact , connectionStats : $ connectionStats , width : buttonWidth ) { alert = . someAlert ( alert : $0 ) }
2025-02-03 20:47:32 +00:00
if let nextNtfMode = chat . chatInfo . nextNtfMode {
muteButton ( width : buttonWidth , nextNtfMode : nextNtfMode )
}
2025-01-20 18:06:00 +00:00
}
2024-08-05 21:22:09 +04:00
}
2025-01-20 18:06:00 +00:00
. padding ( . trailing )
. frame ( maxWidth : . infinity )
. frame ( height : infoViewActionButtonHeight )
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
. listRowInsets ( EdgeInsets ( top : 0 , leading : 0 , bottom : 0 , trailing : 8 ) )
if let customUserProfile = customUserProfile {
Section ( header : Text ( " Incognito " ) . foregroundColor ( theme . colors . secondary ) ) {
HStack {
Text ( " Your random profile " )
Spacer ( )
Text ( customUserProfile . chatViewName )
. foregroundStyle ( . indigo )
}
2023-08-08 17:26:56 +04:00
}
2022-08-23 18:18:12 +04:00
}
2025-01-20 18:06:00 +00:00
Section {
2024-07-19 11:31:43 +04:00
if let code = connectionCode { verifyCodeButton ( code ) }
contactPreferencesButton ( )
sendReceiptsOption ( )
if let connStats = connectionStats ,
connStats . ratchetSyncAllowed {
synchronizeConnectionButton ( )
}
// } e l s e i f d e v e l o p e r T o o l s {
// s y n c h r o n i z e C o n n e c t i o n B u t t o n F o r c e ( )
// }
2025-01-20 18:06:00 +00:00
NavigationLink {
ChatWallpaperEditorSheet ( chat : chat )
} label : {
Label ( " Chat theme " , systemImage : " photo " )
}
// } e l s e i f d e v e l o p e r T o o l s {
// s y n c h r o n i z e C o n n e c t i o n B u t t o n F o r c e ( )
// }
2023-07-10 19:01:22 +04:00
}
2024-07-19 11:31:43 +04:00
. disabled ( ! contact . ready || ! contact . active )
2025-01-20 18:06:00 +00:00
2024-03-07 16:43:10 +04:00
Section {
2025-01-20 18:06:00 +00:00
ChatTTLOption ( chat : chat , progressIndicator : $ progressIndicator )
} footer : {
Text ( " Delete chat messages from your device. " )
2024-03-07 16:43:10 +04:00
}
2025-01-20 18:06:00 +00:00
if let conn = contact . activeConn {
Section {
infoRow ( Text ( String ( " E2E encryption " ) ) , conn . connPQEnabled ? " Quantum resistant " : " Standard " )
2023-04-27 17:19:21 +04:00
}
}
2025-01-20 18:06:00 +00:00
if let contactLink = contact . contactLink {
Section {
SimpleXLinkQRCode ( uri : contactLink )
Button {
showShareSheet ( items : [ simplexChatLink ( contactLink ) ] )
} label : {
Label ( " Share address " , systemImage : " square.and.arrow.up " )
2023-06-19 14:46:08 +04:00
}
2025-01-20 18:06:00 +00:00
} header : {
Text ( " Address " )
. foregroundColor ( theme . colors . secondary )
} footer : {
Text ( " You can share this address with your contacts to let them connect with ** \( contact . displayName ) **. " )
. foregroundColor ( theme . colors . secondary )
}
}
if contact . ready && contact . active {
Section ( header : Text ( " Servers " ) . foregroundColor ( theme . colors . secondary ) ) {
networkStatusRow ( )
. onTapGesture {
alert = . networkStatusAlert
}
if let connStats = connectionStats {
Button ( " Change receiving address " ) {
alert = . switchAddressAlert
2023-09-20 12:26:16 +04:00
}
. disabled (
2025-01-20 18:06:00 +00:00
connStats . rcvQueuesInfo . contains { $0 . rcvSwitchStatus != nil }
2023-09-20 12:26:16 +04:00
|| connStats . ratchetSyncSendProhibited
)
2025-01-20 18:06:00 +00:00
if connStats . rcvQueuesInfo . contains ( where : { $0 . rcvSwitchStatus != nil } ) {
Button ( " Abort changing address " ) {
alert = . abortSwitchAddressAlert
}
. disabled (
connStats . rcvQueuesInfo . contains { $0 . rcvSwitchStatus != nil && ! $0 . canAbortSwitch }
|| connStats . ratchetSyncSendProhibited
)
}
smpServers ( " Receiving via " , connStats . rcvQueuesInfo . map { $0 . rcvServer } , theme . colors . secondary )
smpServers ( " Sending via " , connStats . sndQueuesInfo . map { $0 . sndServer } , theme . colors . secondary )
2023-09-20 12:26:16 +04:00
}
2023-06-19 14:46:08 +04:00
}
2022-07-27 13:40:26 +04:00
}
2025-01-20 18:06:00 +00:00
Section {
clearChatButton ( )
deleteContactButton ( )
}
if developerTools {
Section ( header : Text ( " For console " ) . foregroundColor ( theme . colors . secondary ) ) {
infoRow ( " Local name " , chat . chatInfo . localDisplayName )
infoRow ( " Database ID " , " \( chat . chatInfo . apiId ) " )
Button ( " Debug delivery " ) {
Task {
do {
let info = queueInfoText ( try await apiContactQueueInfo ( chat . chatInfo . apiId ) )
await MainActor . run { alert = . queueInfo ( info : info ) }
} catch let e {
logger . error ( " apiContactQueueInfo error: \( responseError ( e ) ) " )
let a = getErrorAlert ( e , " Error " )
await MainActor . run { alert = . error ( title : a . title , error : a . message ) }
}
2024-05-31 12:45:58 +01:00
}
}
}
2022-08-02 17:00:12 +04:00
}
2022-07-27 13:40:26 +04:00
}
2025-01-20 18:06:00 +00:00
. modifier ( ThemedBackground ( grouped : true ) )
. navigationBarHidden ( true )
. disabled ( progressIndicator )
. opacity ( progressIndicator ? 0.6 : 1 )
if progressIndicator {
ProgressView ( ) . scaleEffect ( 2 )
}
2022-07-14 16:40:32 +04:00
}
}
2022-07-27 13:40:26 +04:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . top )
2023-07-16 14:55:31 +04:00
. onAppear {
if let currentUser = chatModel . currentUser {
sendReceiptsUserDefault = currentUser . sendRcptsContacts
}
sendReceipts = SendReceipts . fromBool ( contact . chatSettings . sendRcpts , userDefault : sendReceiptsUserDefault )
2024-08-05 12:58:24 +01:00
Task {
do {
let ( stats , profile ) = try await apiContactInfo ( chat . chatInfo . apiId )
let ( ct , code ) = try await apiGetContactCode ( chat . chatInfo . apiId )
await MainActor . run {
connectionStats = stats
customUserProfile = profile
connectionCode = code
if contact . activeConn ? . connectionCode != ct . activeConn ? . connectionCode {
chat . chatInfo = . direct ( contact : ct )
}
}
} catch let error {
logger . error ( " apiContactInfo or apiGetContactCode error: \( responseError ( error ) ) " )
}
}
2023-07-16 14:55:31 +04:00
}
2022-07-14 16:40:32 +04:00
. alert ( item : $ alert ) { alertItem in
switch ( alertItem ) {
case . clearChatAlert : return clearChatAlert ( )
2022-07-27 13:40:26 +04:00
case . networkStatusAlert : return networkStatusAlert ( )
2022-11-02 09:48:20 +00:00
case . switchAddressAlert : return switchAddressAlert ( switchContactAddress )
2023-06-19 14:46:08 +04:00
case . abortSwitchAddressAlert : return abortSwitchAddressAlert ( abortSwitchContactAddress )
2024-12-09 21:03:56 +04:00
case . syncConnectionForceAlert :
return syncConnectionForceAlert ( {
Task {
if let stats = await syncContactConnection ( contact , force : true , showAlert : { alert = . someAlert ( alert : $0 ) } ) {
connectionStats = stats
dismiss ( )
}
}
} )
2024-05-31 12:45:58 +01:00
case let . queueInfo ( info ) : return queueInfoAlert ( info )
2024-08-05 12:58:24 +01:00
case let . someAlert ( a ) : return a . alert
2022-11-02 09:48:20 +00:00
case let . error ( title , error ) : return mkAlert ( title : title , message : error )
2022-07-27 13:40:26 +04:00
}
}
2024-08-05 12:58:24 +01:00
. actionSheet ( item : $ actionSheet ) { $0 . actionSheet }
. sheet ( item : $ sheet ) {
if #available ( iOS 16.0 , * ) {
$0 . content
2024-12-19 10:48:26 +00:00
. presentationDetents ( [ . fraction ( $0 . fraction ) ] )
2023-10-19 19:52:59 +04:00
} else {
2024-08-05 12:58:24 +01:00
$0 . content
2023-10-19 19:52:59 +04:00
}
}
2024-12-06 14:44:56 +00:00
. onDisappear {
if currentFeaturesAllowed != featuresAllowed {
showAlert (
title : NSLocalizedString ( " Save preferences? " , comment : " alert title " ) ,
buttonTitle : NSLocalizedString ( " Save and notify contact " , comment : " alert button " ) ,
buttonAction : { savePreferences ( ) } ,
cancelButton : true
)
}
}
2022-07-27 13:40:26 +04:00
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func contactInfoHeader ( ) -> some View {
2024-08-06 08:12:42 +01:00
VStack ( spacing : 8 ) {
2022-07-27 13:40:26 +04:00
let cInfo = chat . chatInfo
2024-04-24 21:20:26 +01:00
ChatInfoImage ( chat : chat , size : 192 , color : Color ( uiColor : . tertiarySystemFill ) )
2024-08-06 08:12:42 +01:00
. padding ( . vertical , 12 )
2023-07-19 15:16:50 +04:00
if contact . verified {
(
Text ( Image ( systemName : " checkmark.shield " ) )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2023-07-19 15:16:50 +04:00
. font ( . title2 )
2024-11-27 19:01:16 +00:00
+ textSpace
2023-07-19 15:16:50 +04:00
+ Text ( contact . profile . displayName )
. font ( . largeTitle )
)
. multilineTextAlignment ( . center )
. lineLimit ( 2 )
. padding ( . bottom , 2 )
} else {
2022-12-12 08:59:35 +00:00
Text ( contact . profile . displayName )
. font ( . largeTitle )
2023-07-19 15:16:50 +04:00
. multilineTextAlignment ( . center )
. lineLimit ( 2 )
2022-12-12 08:59:35 +00:00
. padding ( . bottom , 2 )
}
2022-08-25 17:36:26 +04:00
if cInfo . fullName != " " && cInfo . fullName != cInfo . displayName && cInfo . fullName != contact . profile . displayName {
2022-07-27 13:40:26 +04:00
Text ( cInfo . fullName )
. font ( . title2 )
2023-07-19 15:16:50 +04:00
. multilineTextAlignment ( . center )
. lineLimit ( 4 )
2022-07-27 13:40:26 +04:00
}
}
. frame ( maxWidth : . infinity , alignment : . center )
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func localAliasTextEdit ( ) -> some View {
2022-08-25 17:36:26 +04:00
TextField ( " Set contact name… " , text : $ localAlias )
. disableAutocorrection ( true )
. focused ( $ aliasTextFieldFocused )
. submitLabel ( . done )
. onChange ( of : aliasTextFieldFocused ) { focused in
if ! focused {
setContactAlias ( )
}
}
. onSubmit {
setContactAlias ( )
}
. multilineTextAlignment ( . center )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2022-08-25 17:36:26 +04:00
}
2024-08-05 12:58:24 +01:00
2022-08-25 17:36:26 +04:00
private func setContactAlias ( ) {
Task {
do {
if let contact = try await apiSetContactAlias ( contactId : chat . chatInfo . apiId , localAlias : localAlias ) {
await MainActor . run {
chatModel . updateContact ( contact )
}
}
} catch {
logger . error ( " setContactAlias error: \( responseError ( error ) ) " )
}
}
}
2024-08-05 21:22:09 +04:00
private func searchButton ( width : CGFloat ) -> some View {
2024-08-06 08:12:42 +01:00
InfoViewButton ( image : " magnifyingglass " , title : " search " , width : width ) {
dismiss ( )
onSearch ( )
}
. disabled ( ! contact . ready || chat . chatItems . isEmpty )
2024-08-05 12:58:24 +01:00
}
2025-02-03 20:47:32 +00:00
private func muteButton ( width : CGFloat , nextNtfMode : MsgFilter ) -> some View {
return InfoViewButton (
image : nextNtfMode . iconFilled ,
title : " \( nextNtfMode . text ( mentions : false ) ) " ,
2024-08-05 21:22:09 +04:00
width : width
2024-08-06 08:12:42 +01:00
) {
2025-02-03 20:47:32 +00:00
toggleNotifications ( chat , enableNtfs : nextNtfMode )
2024-08-05 12:58:24 +01:00
}
. disabled ( ! contact . ready || ! contact . active )
}
2022-12-12 08:59:35 +00:00
private func verifyCodeButton ( _ code : String ) -> some View {
NavigationLink {
VerifyCodeView (
displayName : contact . displayName ,
connectionCode : code ,
connectionVerified : contact . verified ,
verify : { code in
if let r = apiVerifyContact ( chat . chatInfo . apiId , connectionCode : code ) {
let ( verified , existingCode ) = r
2023-11-10 10:16:06 +04:00
contact . activeConn ? . connectionCode = verified ? SecurityCode ( securityCode : existingCode , verifiedAt : . now ) : nil
2022-12-12 08:59:35 +00:00
connectionCode = existingCode
DispatchQueue . main . async {
chat . chatInfo = . direct ( contact : contact )
}
return r
}
return nil
}
)
. navigationBarTitleDisplayMode ( . inline )
. navigationTitle ( " Security code " )
2024-07-03 22:42:13 +01:00
. modifier ( ThemedBackground ( grouped : true ) )
2022-12-12 08:59:35 +00:00
} label : {
Label (
contact . verified ? " View security code " : " Verify security code " ,
systemImage : contact . verified ? " checkmark.shield " : " shield "
)
}
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func contactPreferencesButton ( ) -> some View {
2022-11-17 12:59:13 +04:00
NavigationLink {
ContactPreferencesView (
contact : $ contact ,
2024-12-06 14:44:56 +00:00
featuresAllowed : $ featuresAllowed ,
currentFeaturesAllowed : $ currentFeaturesAllowed ,
savePreferences : savePreferences
2022-11-17 12:59:13 +04:00
)
. navigationBarTitle ( " Contact preferences " )
2024-07-03 22:42:13 +01:00
. modifier ( ThemedBackground ( grouped : true ) )
2022-11-17 12:59:13 +04:00
. navigationBarTitleDisplayMode ( . large )
} label : {
Label ( " Contact preferences " , systemImage : " switch.2 " )
}
}
2024-08-05 12:58:24 +01:00
2023-07-13 23:48:25 +01:00
private func sendReceiptsOption ( ) -> some View {
Picker ( selection : $ sendReceipts ) {
2023-07-16 14:55:31 +04:00
ForEach ( [ . yes , . no , . userDefault ( sendReceiptsUserDefault ) ] ) { ( opt : SendReceipts ) in
2023-07-13 23:48:25 +01:00
Text ( opt . text )
}
} label : {
Label ( " Send receipts " , systemImage : " checkmark.message " )
}
. frame ( height : 36 )
2023-07-16 14:55:31 +04:00
. onChange ( of : sendReceipts ) { _ in
setSendReceipts ( )
}
}
2024-08-05 12:58:24 +01:00
2023-07-16 14:55:31 +04:00
private func setSendReceipts ( ) {
var chatSettings = chat . chatInfo . chatSettings ? ? ChatSettings . defaults
chatSettings . sendRcpts = sendReceipts . bool ( )
updateChatSettings ( chat , chatSettings : chatSettings )
2023-07-13 23:48:25 +01:00
}
2025-01-20 18:06:00 +00:00
2023-07-10 19:01:22 +04:00
private func synchronizeConnectionButton ( ) -> some View {
Button {
2024-12-09 21:03:56 +04:00
Task {
if let stats = await syncContactConnection ( contact , force : false , showAlert : { alert = . someAlert ( alert : $0 ) } ) {
connectionStats = stats
dismiss ( )
}
}
2023-07-10 19:01:22 +04:00
} label : {
Label ( " Fix connection " , systemImage : " exclamationmark.arrow.triangle.2.circlepath " )
. foregroundColor ( . orange )
}
}
2024-08-05 12:58:24 +01:00
2023-07-10 19:01:22 +04:00
private func synchronizeConnectionButtonForce ( ) -> some View {
Button {
alert = . syncConnectionForceAlert
} label : {
Label ( " Renegotiate encryption " , systemImage : " exclamationmark.triangle " )
. foregroundColor ( . red )
}
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func networkStatusRow ( ) -> some View {
2022-07-27 13:40:26 +04:00
HStack {
Text ( " Network status " )
Image ( systemName : " info.circle " )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . primary )
2022-07-27 13:40:26 +04:00
. font ( . system ( size : 14 ) )
Spacer ( )
2024-08-13 23:08:04 +03:00
Text ( networkModel . contactNetworkStatus ( contact ) . statusString )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2022-07-27 13:40:26 +04:00
serverImage ( )
}
2022-02-05 20:10:47 +00:00
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func serverImage ( ) -> some View {
2024-08-13 23:08:04 +03:00
let status = networkModel . contactNetworkStatus ( contact )
2022-02-05 20:10:47 +00:00
return Image ( systemName : status . imageName )
2024-07-03 22:42:13 +01:00
. foregroundColor ( status = = . connected ? . green : theme . colors . secondary )
2022-07-27 13:40:26 +04:00
. font ( . system ( size : 12 ) )
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func deleteContactButton ( ) -> some View {
2022-07-27 13:40:26 +04:00
Button ( role : . destructive ) {
2024-08-05 12:58:24 +01:00
deleteContactDialog (
chat ,
contact ,
dismissToChatList : true ,
showAlert : { alert = . someAlert ( alert : $0 ) } ,
showActionSheet : { actionSheet = $0 } ,
showSheetContent : { sheet = $0 }
)
2022-07-27 13:40:26 +04:00
} label : {
2024-08-05 12:58:24 +01:00
Label ( " Delete contact " , systemImage : " person.badge.minus " )
2022-07-27 13:40:26 +04:00
. foregroundColor ( Color . red )
}
}
2024-08-05 12:58:24 +01:00
2022-12-12 08:59:35 +00:00
private func clearChatButton ( ) -> some View {
2022-07-27 13:40:26 +04:00
Button ( ) {
alert = . clearChatAlert
} label : {
Label ( " Clear conversation " , systemImage : " gobackward " )
. foregroundColor ( Color . orange )
}
2022-02-05 20:10:47 +00:00
}
2024-08-05 12:58:24 +01:00
2022-05-19 16:56:34 +04:00
private func clearChatAlert ( ) -> Alert {
Alert (
title : Text ( " Clear conversation? " ) ,
message : Text ( " All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. " ) ,
primaryButton : . destructive ( Text ( " Clear " ) ) {
Task {
await clearChat ( chat )
2022-07-30 13:03:44 +01:00
await MainActor . run { dismiss ( ) }
2022-05-19 16:56:34 +04:00
}
} ,
secondaryButton : . cancel ( )
)
}
2024-08-05 12:58:24 +01:00
2022-07-27 13:40:26 +04:00
private func networkStatusAlert ( ) -> Alert {
Alert (
title : Text ( " Network status " ) ,
2024-08-13 23:08:04 +03:00
message : Text ( networkModel . contactNetworkStatus ( contact ) . statusExplanation )
2022-07-27 13:40:26 +04:00
)
}
2024-08-05 12:58:24 +01:00
2022-11-02 09:48:20 +00:00
private func switchContactAddress ( ) {
Task {
do {
2023-06-20 10:09:04 +04:00
let stats = try apiSwitchContact ( contactId : contact . apiId )
connectionStats = stats
2023-07-10 19:01:22 +04:00
await MainActor . run {
chatModel . updateContactConnectionStats ( contact , stats )
dismiss ( )
}
2022-11-02 09:48:20 +00:00
} catch let error {
logger . error ( " switchContactAddress apiSwitchContact error: \( responseError ( error ) ) " )
let a = getErrorAlert ( error , " Error changing address " )
await MainActor . run {
alert = . error ( title : a . title , error : a . message )
}
}
}
}
2024-08-05 12:58:24 +01:00
2023-06-19 14:46:08 +04:00
private func abortSwitchContactAddress ( ) {
Task {
do {
let stats = try apiAbortSwitchContact ( contact . apiId )
connectionStats = stats
2023-07-10 19:01:22 +04:00
await MainActor . run {
chatModel . updateContactConnectionStats ( contact , stats )
}
2023-06-19 14:46:08 +04:00
} catch let error {
logger . error ( " abortSwitchContactAddress apiAbortSwitchContact error: \( responseError ( error ) ) " )
let a = getErrorAlert ( error , " Error aborting address change " )
await MainActor . run {
alert = . error ( title : a . title , error : a . message )
}
}
}
}
2024-08-05 12:58:24 +01:00
2024-12-06 14:44:56 +00:00
private func savePreferences ( ) {
Task {
do {
let prefs = contactFeaturesAllowedToPrefs ( featuresAllowed )
if let toContact = try await apiSetContactPrefs ( contactId : contact . contactId , preferences : prefs ) {
await MainActor . run {
contact = toContact
chatModel . updateContact ( toContact )
currentFeaturesAllowed = featuresAllowed
}
}
} catch {
logger . error ( " ContactPreferencesView apiSetContactPrefs error: \( responseError ( error ) ) " )
}
}
}
2022-11-02 09:48:20 +00:00
}
2025-01-20 18:06:00 +00:00
struct ChatTTLOption : View {
@ ObservedObject var chat : Chat
@ Binding var progressIndicator : Bool
@ State private var currentChatItemTTL : ChatTTL = ChatTTL . userDefault ( . seconds ( 0 ) )
@ State private var chatItemTTL : ChatTTL = ChatTTL . chat ( . seconds ( 0 ) )
var body : some View {
Picker ( " Delete messages after " , selection : $ chatItemTTL ) {
ForEach ( ChatItemTTL . values ) { ttl in
Text ( ttl . deleteAfterText ) . tag ( ChatTTL . chat ( ttl ) )
}
let defaultTTL = ChatTTL . userDefault ( ChatModel . shared . chatItemTTL )
Text ( defaultTTL . text ) . tag ( defaultTTL )
if case . chat ( let ttl ) = chatItemTTL , case . seconds = ttl {
Text ( ttl . deleteAfterText ) . tag ( chatItemTTL )
}
}
. disabled ( progressIndicator )
. frame ( height : 36 )
. onChange ( of : chatItemTTL ) { ttl in
if ttl = = currentChatItemTTL { return }
setChatTTL (
ttl ,
hasPreviousTTL : ! currentChatItemTTL . neverExpires ,
onCancel : { chatItemTTL = currentChatItemTTL }
) {
progressIndicator = true
Task {
2025-02-18 01:21:40 +07:00
let m = ChatModel . shared
2025-01-20 18:06:00 +00:00
do {
try await setChatTTL ( chatType : chat . chatInfo . chatType , id : chat . chatInfo . apiId , ttl )
2025-02-18 01:21:40 +07:00
await loadChat ( chat : chat , clearItems : true )
2025-01-20 18:06:00 +00:00
await MainActor . run {
progressIndicator = false
currentChatItemTTL = chatItemTTL
2025-02-18 01:21:40 +07:00
if ItemsModel . shared . reversedChatItems . isEmpty && m . chatId = = chat . id ,
let chat = m . getChat ( chat . id ) {
chat . chatItems = [ ]
m . replaceChat ( chat . id , chat )
}
2025-01-20 18:06:00 +00:00
}
}
catch let error {
logger . error ( " setChatTTL error \( responseError ( error ) ) " )
2025-02-18 01:21:40 +07:00
await loadChat ( chat : chat , clearItems : true )
2025-01-20 18:06:00 +00:00
await MainActor . run {
chatItemTTL = currentChatItemTTL
progressIndicator = false
}
}
}
}
}
. onAppear {
let sm = ChatModel . shared
let ttl = chat . chatInfo . ttl ( sm . chatItemTTL )
chatItemTTL = ttl
currentChatItemTTL = ttl
}
}
}
2024-12-09 21:03:56 +04:00
func syncContactConnection ( _ contact : Contact , force : Bool , showAlert : ( SomeAlert ) -> Void ) async -> ConnectionStats ? {
do {
let stats = try apiSyncContactRatchet ( contact . apiId , force )
await MainActor . run {
ChatModel . shared . updateContactConnectionStats ( contact , stats )
}
return stats
} catch let error {
logger . error ( " syncContactConnection apiSyncContactRatchet error: \( responseError ( error ) ) " )
let a = getErrorAlert ( error , " Error synchronizing connection " )
await MainActor . run {
showAlert (
SomeAlert (
alert : mkAlert ( title : a . title , message : a . message ) ,
id : " syncContactConnection error "
)
)
}
return nil
}
}
2024-08-05 12:58:24 +01:00
struct AudioCallButton : View {
var chat : Chat
var contact : Contact
2024-12-09 21:03:56 +04:00
@ Binding var connectionStats : ConnectionStats ?
2024-08-06 08:12:42 +01:00
var width : CGFloat
2024-08-05 12:58:24 +01:00
var showAlert : ( SomeAlert ) -> Void
var body : some View {
CallButton (
chat : chat ,
contact : contact ,
2024-12-09 21:03:56 +04:00
connectionStats : $ connectionStats ,
2024-08-05 21:22:09 +04:00
image : " phone.fill " ,
2024-08-05 12:58:24 +01:00
title : " call " ,
mediaType : . audio ,
2024-08-06 08:12:42 +01:00
width : width ,
showAlert : showAlert
2024-08-05 12:58:24 +01:00
)
}
}
struct VideoButton : View {
var chat : Chat
var contact : Contact
2024-12-09 21:03:56 +04:00
@ Binding var connectionStats : ConnectionStats ?
2024-08-06 08:12:42 +01:00
var width : CGFloat
2024-08-05 12:58:24 +01:00
var showAlert : ( SomeAlert ) -> Void
var body : some View {
CallButton (
chat : chat ,
contact : contact ,
2024-12-09 21:03:56 +04:00
connectionStats : $ connectionStats ,
2024-08-05 21:22:09 +04:00
image : " video.fill " ,
2024-08-05 12:58:24 +01:00
title : " video " ,
mediaType : . video ,
2024-08-06 08:12:42 +01:00
width : width ,
showAlert : showAlert
2024-08-05 12:58:24 +01:00
)
}
}
private struct CallButton : View {
var chat : Chat
var contact : Contact
2024-12-09 21:03:56 +04:00
@ Binding var connectionStats : ConnectionStats ?
2024-08-05 12:58:24 +01:00
var image : String
var title : LocalizedStringKey
var mediaType : CallMediaType
2024-08-06 08:12:42 +01:00
var width : CGFloat
2024-08-05 12:58:24 +01:00
var showAlert : ( SomeAlert ) -> Void
var body : some View {
let canCall = contact . ready && contact . active && chat . chatInfo . featureEnabled ( . calls ) && ChatModel . shared . activeCall = = nil
2024-08-06 08:12:42 +01:00
InfoViewButton ( image : image , title : title , disabledLook : ! canCall , width : width ) {
if canCall {
2024-12-09 21:03:56 +04:00
if let connStats = connectionStats {
if connStats . ratchetSyncState = = . ok {
if CallController . useCallKit ( ) {
CallController . shared . startCall ( contact , mediaType )
} else {
// W h e n C a l l K i t i s n o t u s e d , c o l o r s c h e m e w i l l b e c h a n g e d a n d i t w i l l b e v i s i b l e i f n o t h i d i n g s h e e t s f i r s t
dismissAllSheets ( animated : true ) {
CallController . shared . startCall ( contact , mediaType )
}
}
} else if connStats . ratchetSyncAllowed {
showAlert ( SomeAlert (
alert : Alert (
title : Text ( " Fix connection? " ) ,
message : Text ( " Connection requires encryption renegotiation. " ) ,
primaryButton : . default ( Text ( " Fix " ) ) {
Task {
if let stats = await syncContactConnection ( contact , force : false , showAlert : showAlert ) {
connectionStats = stats
}
}
} ,
secondaryButton : . cancel ( )
) ,
id : " can't call contact, fix connection "
) )
} else {
showAlert ( SomeAlert (
alert : mkAlert (
title : " Can't call contact " ,
message : " Encryption renegotiation in progress. "
) ,
id : " can't call contact, encryption renegotiation in progress "
) )
2024-08-20 16:04:00 +00:00
}
}
2024-08-06 08:12:42 +01:00
} else if contact . nextSendGrpInv {
showAlert ( SomeAlert (
alert : mkAlert (
title : " Can't call contact " ,
message : " Send message to enable calls. "
) ,
id : " can't call contact, send message "
) )
} else if ! contact . active {
showAlert ( SomeAlert (
alert : mkAlert (
title : " Can't call contact " ,
message : " Contact is deleted. "
) ,
id : " can't call contact, contact deleted "
) )
} else if ! contact . ready {
showAlert ( SomeAlert (
alert : mkAlert (
title : " Can't call contact " ,
message : " Connecting to contact, please wait or check later! "
) ,
id : " can't call contact, contact not ready "
) )
} else if ! chat . chatInfo . featureEnabled ( . calls ) {
switch chat . chatInfo . showEnableCallsAlert {
case . userEnable :
2024-08-05 12:58:24 +01:00
showAlert ( SomeAlert (
2024-08-06 08:12:42 +01:00
alert : Alert (
title : Text ( " Allow calls? " ) ,
message : Text ( " You need to allow your contact to call to be able to call them. " ) ,
primaryButton : . default ( Text ( " Allow " ) ) {
allowFeatureToContact ( contact , . calls )
} ,
secondaryButton : . cancel ( )
2024-08-05 12:58:24 +01:00
) ,
2024-08-06 08:12:42 +01:00
id : " allow calls "
2024-08-05 12:58:24 +01:00
) )
2024-08-06 08:12:42 +01:00
case . askContact :
2024-08-05 12:58:24 +01:00
showAlert ( SomeAlert (
alert : mkAlert (
2024-08-06 08:12:42 +01:00
title : " Calls prohibited! " ,
message : " Please ask your contact to enable calls. "
2024-08-05 12:58:24 +01:00
) ,
2024-08-06 08:12:42 +01:00
id : " calls prohibited, ask contact "
2024-08-05 12:58:24 +01:00
) )
2024-08-06 08:12:42 +01:00
case . other :
2024-08-05 12:58:24 +01:00
showAlert ( SomeAlert (
alert : mkAlert (
2024-08-06 08:12:42 +01:00
title : " Calls prohibited! " ,
message : " Please check yours and your contact preferences. "
)
, id : " calls prohibited, other "
2024-08-05 12:58:24 +01:00
) )
}
2024-08-06 08:12:42 +01:00
} else {
showAlert ( SomeAlert (
alert : mkAlert ( title : " Can't call contact " ) ,
id : " can't call contact "
) )
2024-08-05 12:58:24 +01:00
}
2024-08-06 08:12:42 +01:00
}
. disabled ( ChatModel . shared . activeCall != nil )
2024-08-05 12:58:24 +01:00
}
}
2024-08-05 21:22:09 +04:00
let infoViewActionButtonHeight : CGFloat = 60
2024-08-06 08:12:42 +01:00
struct InfoViewButton : View {
2024-08-05 12:58:24 +01:00
var image : String
var title : LocalizedStringKey
var disabledLook : Bool = false
2024-08-06 08:12:42 +01:00
var width : CGFloat
var action : ( ) -> Void
2024-08-05 12:58:24 +01:00
var body : some View {
VStack ( spacing : 4 ) {
Image ( systemName : image )
. resizable ( )
. scaledToFit ( )
. frame ( width : 20 , height : 20 )
Text ( title )
. font ( . caption )
}
. frame ( maxWidth : . infinity , maxHeight : . infinity )
. foregroundColor ( . accentColor )
. background ( Color ( . secondarySystemGroupedBackground ) )
2024-08-05 21:22:09 +04:00
. cornerRadius ( 10.0 )
. frame ( width : width , height : infoViewActionButtonHeight )
2024-08-05 12:58:24 +01:00
. disabled ( disabledLook )
2024-08-06 08:12:42 +01:00
. onTapGesture ( perform : action )
2024-08-05 12:58:24 +01:00
}
}
2024-07-03 22:42:13 +01:00
struct ChatWallpaperEditorSheet : View {
@ Environment ( \ . dismiss ) var dismiss
@ EnvironmentObject var theme : AppTheme
@ State private var globalThemeUsed : Bool = false
@ State var chat : Chat
@ State private var themes : ThemeModeOverrides
init ( chat : Chat ) {
self . chat = chat
self . themes = if case let ChatInfo . direct ( contact ) = chat . chatInfo , let uiThemes = contact . uiThemes {
uiThemes
} else if case let ChatInfo . group ( groupInfo ) = chat . chatInfo , let uiThemes = groupInfo . uiThemes {
uiThemes
} else {
ThemeModeOverrides ( )
}
}
var body : some View {
let preferred = themes . preferredMode ( ! theme . colors . isLight )
let initialTheme = preferred ? ? ThemeManager . defaultActiveTheme ( ChatModel . shared . currentUser ? . uiThemes , themeOverridesDefault . get ( ) )
ChatWallpaperEditor (
initialTheme : initialTheme ,
themeModeOverride : initialTheme ,
applyToMode : themes . light = = themes . dark ? nil : initialTheme . mode ,
globalThemeUsed : $ globalThemeUsed ,
save : { applyToMode , newTheme in
await save ( applyToMode , newTheme , $ chat )
}
)
. navigationTitle ( " Chat theme " )
. modifier ( ThemedBackground ( grouped : true ) )
. navigationBarTitleDisplayMode ( . inline )
. onAppear {
globalThemeUsed = preferred = = nil
}
. onChange ( of : theme . base . mode ) { _ in
globalThemeUsed = themesFromChat ( chat ) . preferredMode ( ! theme . colors . isLight ) = = nil
}
. onChange ( of : ChatModel . shared . chatId ) { _ in
dismiss ( )
}
}
private func themesFromChat ( _ chat : Chat ) -> ThemeModeOverrides {
if case let ChatInfo . direct ( contact ) = chat . chatInfo , let uiThemes = contact . uiThemes {
uiThemes
} else if case let ChatInfo . group ( groupInfo ) = chat . chatInfo , let uiThemes = groupInfo . uiThemes {
uiThemes
} else {
ThemeModeOverrides ( )
}
}
private static var updateBackendTask : Task = Task { }
private func save (
_ applyToMode : DefaultThemeMode ? ,
_ newTheme : ThemeModeOverride ? ,
_ chat : Binding < Chat >
) async {
let unchangedThemes : ThemeModeOverrides = themesFromChat ( chat . wrappedValue )
var wallpaperFiles = Set ( [ unchangedThemes . light ? . wallpaper ? . imageFile , unchangedThemes . dark ? . wallpaper ? . imageFile ] )
var changedThemes : ThemeModeOverrides ? = unchangedThemes
let light : ThemeModeOverride ? = if let newTheme {
ThemeModeOverride ( mode : DefaultThemeMode . light , colors : newTheme . colors , wallpaper : newTheme . wallpaper ? . withFilledWallpaperPath ( ) )
} else {
nil
}
let dark : ThemeModeOverride ? = if let newTheme {
ThemeModeOverride ( mode : DefaultThemeMode . dark , colors : newTheme . colors , wallpaper : newTheme . wallpaper ? . withFilledWallpaperPath ( ) )
} else {
nil
}
if let applyToMode {
switch applyToMode {
case DefaultThemeMode . light :
changedThemes ? . light = light
case DefaultThemeMode . dark :
changedThemes ? . dark = dark
}
} else {
changedThemes ? . light = light
changedThemes ? . dark = dark
}
if changedThemes ? . light != nil || changedThemes ? . dark != nil {
let light = changedThemes ? . light
let dark = changedThemes ? . dark
let currentMode = CurrentColors . base . mode
// s a m e i m a g e f i l e f o r b o t h m o d e s , c o p y i m a g e t o m a k e t h e m a s d i f f e r e n t f i l e s
if var light , var dark , let lightWallpaper = light . wallpaper , let darkWallpaper = dark . wallpaper , let lightImageFile = lightWallpaper . imageFile , let darkImageFile = darkWallpaper . imageFile , lightWallpaper . imageFile = = darkWallpaper . imageFile {
let imageFile = if currentMode = = DefaultThemeMode . light {
darkImageFile
} else {
lightImageFile
}
let filePath = saveWallpaperFile ( url : getWallpaperFilePath ( imageFile ) )
if currentMode = = DefaultThemeMode . light {
dark . wallpaper ? . imageFile = filePath
changedThemes = ThemeModeOverrides ( light : changedThemes ? . light , dark : dark )
} else {
light . wallpaper ? . imageFile = filePath
changedThemes = ThemeModeOverrides ( light : light , dark : changedThemes ? . dark )
}
}
} else {
changedThemes = nil
}
wallpaperFiles . remove ( changedThemes ? . light ? . wallpaper ? . imageFile )
wallpaperFiles . remove ( changedThemes ? . dark ? . wallpaper ? . imageFile )
wallpaperFiles . forEach ( removeWallpaperFile )
let changedThemesConstant = changedThemes
ChatWallpaperEditorSheet . updateBackendTask . cancel ( )
ChatWallpaperEditorSheet . updateBackendTask = Task {
do {
try await Task . sleep ( nanoseconds : 300_000000 )
if await apiSetChatUIThemes ( chatId : chat . id , themes : changedThemesConstant ) {
if case var ChatInfo . direct ( contact ) = chat . wrappedValue . chatInfo {
contact . uiThemes = changedThemesConstant
await MainActor . run {
ChatModel . shared . updateChatInfo ( ChatInfo . direct ( contact : contact ) )
chat . wrappedValue = Chat . init ( chatInfo : ChatInfo . direct ( contact : contact ) )
themes = themesFromChat ( chat . wrappedValue )
}
} else if case var ChatInfo . group ( groupInfo ) = chat . wrappedValue . chatInfo {
groupInfo . uiThemes = changedThemesConstant
await MainActor . run {
ChatModel . shared . updateChatInfo ( ChatInfo . group ( groupInfo : groupInfo ) )
chat . wrappedValue = Chat . init ( chatInfo : ChatInfo . group ( groupInfo : groupInfo ) )
themes = themesFromChat ( chat . wrappedValue )
}
}
}
} catch {
// c a n c e l e d t a s k
}
}
}
}
2022-11-02 09:48:20 +00:00
func switchAddressAlert ( _ switchAddress : @ escaping ( ) -> Void ) -> Alert {
Alert (
title : Text ( " Change receiving address? " ) ,
2023-06-19 14:46:08 +04:00
message : Text ( " Receiving address will be changed to a different server. Address change will complete after sender comes online. " ) ,
primaryButton : . default ( Text ( " Change " ) , action : switchAddress ) ,
secondaryButton : . cancel ( )
)
}
func abortSwitchAddressAlert ( _ abortSwitchAddress : @ escaping ( ) -> Void ) -> Alert {
Alert (
title : Text ( " Abort changing address? " ) ,
message : Text ( " Address change will be aborted. Old receiving address will be used. " ) ,
primaryButton : . destructive ( Text ( " Abort " ) , action : abortSwitchAddress ) ,
2022-11-02 09:48:20 +00:00
secondaryButton : . cancel ( )
)
2022-02-05 20:10:47 +00:00
}
2023-07-10 19:01:22 +04:00
func syncConnectionForceAlert ( _ syncConnectionForce : @ escaping ( ) -> Void ) -> Alert {
Alert (
title : Text ( " Renegotiate encryption? " ) ,
message : Text ( " The encryption is working and the new encryption agreement is not required. It may result in connection errors! " ) ,
primaryButton : . destructive ( Text ( " Renegotiate " ) , action : syncConnectionForce ) ,
secondaryButton : . cancel ( )
)
}
2024-08-24 19:10:30 +01:00
func queueInfoText ( _ info : ( RcvMsgInfo ? , ServerQueueInfo ) ) -> String {
2024-05-31 12:45:58 +01:00
let ( rcvMsgInfo , qInfo ) = info
var msgInfo : String
if let rcvMsgInfo { msgInfo = encodeJSON ( rcvMsgInfo ) } else { msgInfo = " none " }
return String . localizedStringWithFormat ( NSLocalizedString ( " server queue info: %@ \n \n last received msg: %@ " , comment : " queue info " ) , encodeJSON ( qInfo ) , msgInfo )
}
func queueInfoAlert ( _ info : String ) -> Alert {
Alert (
title : Text ( " Message queue info " ) ,
message : Text ( info ) ,
primaryButton : . default ( Text ( " Ok " ) ) ,
secondaryButton : . default ( Text ( " Copy " ) ) { UIPasteboard . general . string = info }
)
}
2024-08-05 12:58:24 +01:00
func deleteContactDialog (
_ chat : Chat ,
_ contact : Contact ,
dismissToChatList : Bool ,
showAlert : @ escaping ( SomeAlert ) -> Void ,
showActionSheet : @ escaping ( SomeActionSheet ) -> Void ,
showSheetContent : @ escaping ( SomeSheet < AnyView > ) -> Void
) {
if contact . sndReady && contact . active && ! contact . chatDeleted {
deleteContactOrConversationDialog ( chat , contact , dismissToChatList , showAlert , showActionSheet , showSheetContent )
} else if contact . sndReady && contact . active && contact . chatDeleted {
deleteContactWithoutConversation ( chat , contact , dismissToChatList , showAlert , showActionSheet )
} else { // ! ( c o n t a c t . s n d R e a d y & & c o n t a c t . a c t i v e )
deleteNotReadyContact ( chat , contact , dismissToChatList , showAlert , showActionSheet )
}
}
2025-01-20 18:06:00 +00:00
func setChatTTL ( _ ttl : ChatTTL , hasPreviousTTL : Bool , onCancel : @ escaping ( ) -> Void , onConfirm : @ escaping ( ) -> Void ) {
let title = if ttl . neverExpires {
NSLocalizedString ( " Disable automatic message deletion? " , comment : " alert title " )
} else if ttl . usingDefault || hasPreviousTTL {
NSLocalizedString ( " Change automatic message deletion? " , comment : " alert title " )
} else {
NSLocalizedString ( " Enable automatic message deletion? " , comment : " alert title " )
}
let message = if ttl . neverExpires {
NSLocalizedString ( " Messages in this chat will never be deleted. " , comment : " alert message " )
} else {
NSLocalizedString ( " This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. " , comment : " alert message " )
}
showAlert ( title , message : message ) {
[
UIAlertAction (
title : ttl . neverExpires ? NSLocalizedString ( " Disable delete messages " , comment : " alert button " ) : NSLocalizedString ( " Delete messages " , comment : " alert button " ) ,
style : . destructive ,
handler : { _ in onConfirm ( ) }
) ,
UIAlertAction ( title : NSLocalizedString ( " Cancel " , comment : " alert button " ) , style : . cancel , handler : { _ in onCancel ( ) } )
]
}
}
2024-08-05 12:58:24 +01:00
private func deleteContactOrConversationDialog (
_ chat : Chat ,
_ contact : Contact ,
_ dismissToChatList : Bool ,
_ showAlert : @ escaping ( SomeAlert ) -> Void ,
_ showActionSheet : @ escaping ( SomeActionSheet ) -> Void ,
_ showSheetContent : @ escaping ( SomeSheet < AnyView > ) -> Void
) {
showActionSheet ( SomeActionSheet (
actionSheet : ActionSheet (
title : Text ( " Delete contact? " ) ,
buttons : [
. destructive ( Text ( " Only delete conversation " ) ) {
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : . messages , dismissToChatList , showAlert )
} ,
. destructive ( Text ( " Delete contact " ) ) {
showSheetContent ( SomeSheet (
content : { AnyView (
DeleteActiveContactDialog (
chat : chat ,
contact : contact ,
dismissToChatList : dismissToChatList ,
showAlert : showAlert
)
) } ,
id : " DeleteActiveContactDialog "
) )
} ,
. cancel ( )
]
) ,
id : " deleteContactOrConversationDialog "
) )
}
private func deleteContactMaybeErrorAlert (
_ chat : Chat ,
_ contact : Contact ,
chatDeleteMode : ChatDeleteMode ,
_ dismissToChatList : Bool ,
_ showAlert : @ escaping ( SomeAlert ) -> Void
) {
Task {
let alert_ = await deleteContactChat ( chat , chatDeleteMode : chatDeleteMode )
if let alert = alert_ {
showAlert ( SomeAlert ( alert : alert , id : " deleteContactMaybeErrorAlert, error " ) )
} else {
if dismissToChatList {
await MainActor . run {
ChatModel . shared . chatId = nil
}
DispatchQueue . main . async {
dismissAllSheets ( animated : true ) {
if case . messages = chatDeleteMode , showDeleteConversationNoticeDefault . get ( ) {
AlertManager . shared . showAlert ( deleteConversationNotice ( contact ) )
} else if chatDeleteMode . isEntity , showDeleteContactNoticeDefault . get ( ) {
AlertManager . shared . showAlert ( deleteContactNotice ( contact ) )
}
}
}
} else {
if case . messages = chatDeleteMode , showDeleteConversationNoticeDefault . get ( ) {
showAlert ( SomeAlert ( alert : deleteConversationNotice ( contact ) , id : " deleteContactMaybeErrorAlert, deleteConversationNotice " ) )
} else if chatDeleteMode . isEntity , showDeleteContactNoticeDefault . get ( ) {
showAlert ( SomeAlert ( alert : deleteContactNotice ( contact ) , id : " deleteContactMaybeErrorAlert, deleteContactNotice " ) )
}
}
}
}
}
private func deleteConversationNotice ( _ contact : Contact ) -> Alert {
return Alert (
title : Text ( " Conversation deleted! " ) ,
2024-08-06 19:20:54 +01:00
message : Text ( " You can send messages to \( contact . displayName ) from Archived contacts. " ) ,
2024-08-05 12:58:24 +01:00
primaryButton : . default ( Text ( " Don't show again " ) ) {
showDeleteConversationNoticeDefault . set ( false )
} ,
secondaryButton : . default ( Text ( " Ok " ) )
)
}
private func deleteContactNotice ( _ contact : Contact ) -> Alert {
return Alert (
title : Text ( " Contact deleted! " ) ,
message : Text ( " You can still view conversation with \( contact . displayName ) in the list of chats. " ) ,
primaryButton : . default ( Text ( " Don't show again " ) ) {
showDeleteContactNoticeDefault . set ( false )
} ,
secondaryButton : . default ( Text ( " Ok " ) )
)
}
enum ContactDeleteMode {
case full
case entity
public func toChatDeleteMode ( notify : Bool ) -> ChatDeleteMode {
switch self {
case . full : . full ( notify : notify )
case . entity : . entity ( notify : notify )
}
}
}
struct DeleteActiveContactDialog : View {
@ Environment ( \ . dismiss ) var dismiss
@ EnvironmentObject var theme : AppTheme
var chat : Chat
var contact : Contact
var dismissToChatList : Bool
var showAlert : ( SomeAlert ) -> Void
@ State private var keepConversation = false
var body : some View {
NavigationView {
List {
Section {
Toggle ( " Keep conversation " , isOn : $ keepConversation )
Button ( role : . destructive ) {
dismiss ( )
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : contactDeleteMode . toChatDeleteMode ( notify : false ) , dismissToChatList , showAlert )
} label : {
Text ( " Delete without notification " )
}
Button ( role : . destructive ) {
dismiss ( )
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : contactDeleteMode . toChatDeleteMode ( notify : true ) , dismissToChatList , showAlert )
} label : {
Text ( " Delete and notify contact " )
}
} footer : {
Text ( " Contact will be deleted - this cannot be undone! " )
. foregroundColor ( theme . colors . secondary )
}
}
. modifier ( ThemedBackground ( grouped : true ) )
}
}
var contactDeleteMode : ContactDeleteMode {
keepConversation ? . entity : . full
}
}
private func deleteContactWithoutConversation (
_ chat : Chat ,
_ contact : Contact ,
_ dismissToChatList : Bool ,
_ showAlert : @ escaping ( SomeAlert ) -> Void ,
_ showActionSheet : @ escaping ( SomeActionSheet ) -> Void
) {
showActionSheet ( SomeActionSheet (
actionSheet : ActionSheet (
title : Text ( " Confirm contact deletion? " ) ,
buttons : [
. destructive ( Text ( " Delete and notify contact " ) ) {
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : . full ( notify : true ) , dismissToChatList , showAlert )
} ,
. destructive ( Text ( " Delete without notification " ) ) {
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : . full ( notify : false ) , dismissToChatList , showAlert )
} ,
. cancel ( )
]
) ,
id : " deleteContactWithoutConversation "
) )
}
private func deleteNotReadyContact (
_ chat : Chat ,
_ contact : Contact ,
_ dismissToChatList : Bool ,
_ showAlert : @ escaping ( SomeAlert ) -> Void ,
_ showActionSheet : @ escaping ( SomeActionSheet ) -> Void
) {
showActionSheet ( SomeActionSheet (
actionSheet : ActionSheet (
title : Text ( " Confirm contact deletion? " ) ,
buttons : [
. destructive ( Text ( " Confirm " ) ) {
deleteContactMaybeErrorAlert ( chat , contact , chatDeleteMode : . full ( notify : false ) , dismissToChatList , showAlert )
} ,
. cancel ( )
]
) ,
id : " deleteNotReadyContact "
) )
}
2022-02-05 20:10:47 +00:00
struct ChatInfoView_Previews : PreviewProvider {
static var previews : some View {
2022-11-01 20:30:53 +00:00
ChatInfoView (
chat : Chat ( chatInfo : ChatInfo . sampleData . direct , chatItems : [ ] ) ,
contact : Contact . sampleData ,
2022-12-12 08:59:35 +00:00
localAlias : " " ,
2024-12-06 14:44:56 +00:00
featuresAllowed : contactUserPrefsToFeaturesAllowed ( Contact . sampleData . mergedPreferences ) ,
currentFeaturesAllowed : contactUserPrefsToFeaturesAllowed ( Contact . sampleData . mergedPreferences ) ,
2025-01-20 18:06:00 +00:00
onSearch : { }
2022-11-01 20:30:53 +00:00
)
2022-02-05 20:10:47 +00:00
}
}