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
2022-05-31 07:55:13 +01:00
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 )
}
}
@ ViewBuilder func smpServers ( _ title : LocalizedStringKey , _ servers : [ String ] ? ) -> some View {
if let servers = servers ,
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 : " ; " )
}
. foregroundColor ( . secondary )
. lineLimit ( 1 )
}
2022-07-27 13:40:26 +04:00
}
}
private func serverHost ( _ s : String ) -> String {
if let i = s . range ( of : " @ " ) ? . lowerBound {
return String ( s [ i . . . ] . dropFirst ( ) )
} else {
return s
}
}
2022-02-05 20:10:47 +00:00
struct ChatInfoView : View {
2022-02-07 10:36:11 +00:00
@ EnvironmentObject var chatModel : ChatModel
2022-07-30 13:03:44 +01:00
@ Environment ( \ . dismiss ) var dismiss : DismissAction
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-11-01 20:30:53 +00:00
@ Binding var connectionStats : ConnectionStats ?
2022-12-12 08:59:35 +00:00
@ Binding var customUserProfile : Profile ?
2022-08-25 17:36:26 +04:00
@ State var localAlias : String
2022-12-12 08:59:35 +00:00
@ Binding var connectionCode : String ?
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
2022-08-02 17:00:12 +04:00
@ AppStorage ( DEFAULT_DEVELOPER_TOOLS ) private var developerTools = false
2022-02-05 20:10:47 +00:00
2022-05-19 16:56:34 +04:00
enum ChatInfoViewAlert : Identifiable {
case deleteContactAlert
case clearChatAlert
2022-07-27 13:40:26 +04:00
case networkStatusAlert
2022-11-02 09:48:20 +00:00
case switchAddressAlert
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 . deleteContactAlert : return " deleteContactAlert "
case . clearChatAlert : return " clearChatAlert "
case . networkStatusAlert : return " networkStatusAlert "
2022-11-02 09:48:20 +00:00
case . switchAddressAlert : return " switchAddressAlert "
case let . error ( title , _ ) : return " error \( title ) "
2022-09-21 17:18:48 +04:00
}
}
2022-05-19 16:56:34 +04:00
}
2022-02-05 20:10:47 +00:00
var body : some View {
2022-07-27 13:40:26 +04:00
NavigationView {
List {
contactInfoHeader ( )
. listRowBackground ( Color . clear )
2022-08-25 17:36:26 +04:00
. contentShape ( Rectangle ( ) )
. onTapGesture {
aliasTextFieldFocused = false
}
2022-12-12 08:59:35 +00:00
Group {
localAliasTextEdit ( )
}
2022-08-25 17:36:26 +04:00
. listRowBackground ( Color . clear )
. listRowSeparator ( . hidden )
2022-02-05 20:10:47 +00:00
2022-08-23 18:18:12 +04:00
if let customUserProfile = customUserProfile {
Section ( " Incognito " ) {
infoRow ( " Your random profile " , customUserProfile . chatViewName )
}
}
2022-11-17 12:59:13 +04:00
Section {
2022-12-12 08:59:35 +00:00
if let code = connectionCode { verifyCodeButton ( code ) }
2022-11-17 12:59:13 +04:00
contactPreferencesButton ( )
2022-11-14 10:12:17 +00:00
}
2023-04-27 17:19:21 +04:00
if let contactLink = contact . contactLink {
Section {
QRCode ( uri : contactLink )
Button {
showShareSheet ( items : [ contactLink ] )
} label : {
Label ( " Share address " , systemImage : " square.and.arrow.up " )
}
} header : {
Text ( " Address " )
} footer : {
Text ( " You can share this address with your contacts to let them connect with ** \( contact . displayName ) **. " )
}
}
2022-11-01 20:30:53 +00:00
Section ( " Servers " ) {
networkStatusRow ( )
. onTapGesture {
alert = . networkStatusAlert
}
2022-11-26 09:45:10 +00:00
Button ( " Change receiving address " ) {
alert = . switchAddressAlert
2022-11-01 20:30:53 +00:00
}
if let connStats = connectionStats {
2022-07-27 13:40:26 +04:00
smpServers ( " Receiving via " , connStats . rcvServers )
smpServers ( " Sending via " , connStats . sndServers )
}
}
2022-02-07 10:36:11 +00:00
2022-07-27 13:40:26 +04:00
Section {
clearChatButton ( )
deleteContactButton ( )
}
2022-08-02 17:00:12 +04:00
if developerTools {
Section ( header : Text ( " For console " ) ) {
infoRow ( " Local name " , chat . chatInfo . localDisplayName )
infoRow ( " Database ID " , " \( chat . chatInfo . apiId ) " )
}
2022-07-27 13:40:26 +04:00
}
2022-07-14 16:40:32 +04:00
}
2022-07-27 13:40:26 +04:00
. navigationBarHidden ( true )
2022-07-14 16:40:32 +04:00
}
2022-07-27 13:40:26 +04:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . top )
2022-07-14 16:40:32 +04:00
. alert ( item : $ alert ) { alertItem in
switch ( alertItem ) {
2022-07-26 12:33:10 +04:00
case . deleteContactAlert : return deleteContactAlert ( )
2022-07-14 16:40:32 +04:00
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 )
case let . error ( title , error ) : return mkAlert ( title : title , message : error )
2022-07-27 13:40:26 +04:00
}
}
}
2022-12-12 08:59:35 +00:00
private func contactInfoHeader ( ) -> some View {
2022-07-27 13:40:26 +04:00
VStack {
let cInfo = chat . chatInfo
ChatInfoImage ( chat : chat , color : Color ( uiColor : . tertiarySystemFill ) )
. frame ( width : 192 , height : 192 )
. padding ( . top , 12 )
. padding ( )
2022-12-12 08:59:35 +00:00
HStack {
if contact . verified {
Image ( systemName : " checkmark.shield " )
. foregroundColor ( . secondary )
}
Text ( contact . profile . displayName )
. font ( . largeTitle )
. lineLimit ( 1 )
. 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 )
. lineLimit ( 2 )
}
}
. frame ( maxWidth : . infinity , alignment : . center )
}
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 )
. foregroundColor ( . secondary )
}
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 ) ) " )
}
}
}
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
contact . activeConn . connectionCode = verified ? SecurityCode ( securityCode : existingCode , verifiedAt : . now ) : nil
connectionCode = existingCode
DispatchQueue . main . async {
chat . chatInfo = . direct ( contact : contact )
}
return r
}
return nil
}
)
. navigationBarTitleDisplayMode ( . inline )
. navigationTitle ( " Security code " )
} label : {
Label (
contact . verified ? " View security code " : " Verify security code " ,
systemImage : contact . verified ? " checkmark.shield " : " shield "
)
}
}
private func contactPreferencesButton ( ) -> some View {
2022-11-17 12:59:13 +04:00
NavigationLink {
ContactPreferencesView (
contact : $ contact ,
featuresAllowed : contactUserPrefsToFeaturesAllowed ( contact . mergedPreferences ) ,
currentFeaturesAllowed : contactUserPrefsToFeaturesAllowed ( contact . mergedPreferences )
)
. navigationBarTitle ( " Contact preferences " )
. navigationBarTitleDisplayMode ( . large )
} label : {
Label ( " Contact preferences " , systemImage : " switch.2 " )
}
}
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 " )
. foregroundColor ( . accentColor )
. font ( . system ( size : 14 ) )
Spacer ( )
2023-01-20 14:56:05 +04:00
Text ( chatModel . contactNetworkStatus ( contact ) . statusString )
2022-07-27 13:40:26 +04:00
. foregroundColor ( . secondary )
serverImage ( )
}
2022-02-05 20:10:47 +00:00
}
2022-12-12 08:59:35 +00:00
private func serverImage ( ) -> some View {
2023-01-20 14:56:05 +04:00
let status = chatModel . contactNetworkStatus ( contact )
2022-02-05 20:10:47 +00:00
return Image ( systemName : status . imageName )
. foregroundColor ( status = = . connected ? . green : . secondary )
2022-07-27 13:40:26 +04:00
. font ( . system ( size : 12 ) )
}
2022-12-12 08:59:35 +00:00
private func deleteContactButton ( ) -> some View {
2022-07-27 13:40:26 +04:00
Button ( role : . destructive ) {
alert = . deleteContactAlert
} label : {
Label ( " Delete contact " , systemImage : " trash " )
. foregroundColor ( Color . red )
}
}
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
}
2022-02-07 10:36:11 +00:00
2022-07-26 12:33:10 +04:00
private func deleteContactAlert ( ) -> Alert {
2022-02-07 10:36:11 +00:00
Alert (
title : Text ( " Delete contact? " ) ,
2022-04-16 09:37:01 +01:00
message : Text ( " Contact and all messages will be deleted - this cannot be undone! " ) ,
2022-02-07 10:36:11 +00:00
primaryButton : . destructive ( Text ( " Delete " ) ) {
2022-02-24 17:16:41 +00:00
Task {
do {
2022-07-26 12:33:10 +04:00
try await apiDeleteChat ( type : chat . chatInfo . chatType , id : chat . chatInfo . apiId )
await MainActor . run {
chatModel . removeChat ( chat . chatInfo . id )
2022-08-29 14:08:46 +01:00
chatModel . chatId = nil
2022-07-30 13:03:44 +01:00
dismiss ( )
2022-02-24 17:16:41 +00:00
}
} catch let error {
2022-11-02 09:48:20 +00:00
logger . error ( " deleteContactAlert apiDeleteChat error: \( responseError ( error ) ) " )
let a = getErrorAlert ( error , " Error deleting contact " )
await MainActor . run {
alert = . error ( title : a . title , error : a . message )
}
2022-02-24 17:16:41 +00:00
}
2022-02-07 10:36:11 +00:00
}
2022-02-12 15:59:43 +00:00
} ,
secondaryButton : . cancel ( )
2022-02-07 10:36:11 +00: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 ( )
)
}
2022-07-27 13:40:26 +04:00
private func networkStatusAlert ( ) -> Alert {
Alert (
title : Text ( " Network status " ) ,
2023-01-20 14:56:05 +04:00
message : Text ( chatModel . contactNetworkStatus ( contact ) . statusExplanation )
2022-07-27 13:40:26 +04:00
)
}
2022-11-02 09:48:20 +00:00
private func switchContactAddress ( ) {
Task {
do {
try await apiSwitchContact ( contactId : contact . apiId )
} 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 )
}
}
}
}
}
func switchAddressAlert ( _ switchAddress : @ escaping ( ) -> Void ) -> Alert {
Alert (
title : Text ( " Change receiving address? " ) ,
message : Text ( " This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member). " ) ,
primaryButton : . destructive ( Text ( " Change " ) , action : switchAddress ) ,
secondaryButton : . cancel ( )
)
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 ,
connectionStats : Binding . constant ( nil ) ,
2022-12-12 08:59:35 +00:00
customUserProfile : Binding . constant ( nil ) ,
localAlias : " " ,
connectionCode : Binding . constant ( nil )
2022-11-01 20:30:53 +00:00
)
2022-02-05 20:10:47 +00:00
}
}