2021-12-24 23:41:18 -08:00
//
2022-01-01 08:03:46 -08:00
// U s e r M e s s a g e L i s t . s w i f t
2022-06-09 22:11:54 -07:00
// M e s h t a s t i c A p p l e
2022-01-01 08:03:46 -08:00
//
// C r e a t e d b y G a r t h V a n d e r H o u w e n o n 1 2 / 2 4 / 2 1 .
//
import SwiftUI
import CoreData
2022-11-05 08:26:27 -07:00
struct UserMessageList : View {
2022-01-01 08:03:46 -08:00
@ Environment ( \ . managedObjectContext ) var context
@ EnvironmentObject var bleManager : BLEManager
2022-02-22 09:08:06 -10:00
@ EnvironmentObject var userSettings : UserSettings
2022-01-01 08:03:46 -08:00
enum Field : Hashable {
case messageText
}
// K e y b o a r d S t a t e
@ State var typingMessage : String = " "
@ State private var totalBytes = 0
var maxbytes = 228
@ FocusState var focusedField : Field ?
2022-11-11 16:22:50 -08:00
// V i e w S t a t e I t e m s
2022-01-09 08:49:22 -08:00
@ ObservedObject var user : UserEntity
2022-01-01 08:03:46 -08:00
@ State var showDeleteMessageAlert = false
@ State private var deleteMessageId : Int64 = 0
2022-01-01 22:55:25 -08:00
@ State private var replyMessageId : Int64 = 0
2022-02-17 18:07:41 -08:00
@ State private var sendPositionWithMessage : Bool = false
2022-06-06 22:24:35 -07:00
@ State private var refreshId = UUID ( )
2022-01-01 08:03:46 -08:00
var body : some View {
2022-10-15 01:00:13 -07:00
NavigationStack {
2022-01-01 15:45:00 -08:00
ScrollViewReader { scrollView in
ScrollView {
2022-11-07 18:31:12 -08:00
LazyVStack {
2022-06-04 07:41:52 -07:00
ForEach ( user . messageList ) { ( message : MessageEntity ) in
2022-10-14 22:18:28 -07:00
if user . num != userSettings . preferredNodeNum {
2022-10-15 01:00:13 -07:00
let currentUser : Bool = ( userSettings . preferredNodeNum = = message . fromUser ? . num ? true : false )
2022-11-07 18:31:12 -08:00
if message . replyID > 0 {
let messageReply = user . messageList . first ( where : { $0 . messageId = = message . replyID } )
HStack {
Text ( messageReply ? . messagePayload ? ? " EMPTY MESSAGE " ) . foregroundColor ( . blue ) . font ( . caption2 )
. padding ( 10 )
. overlay (
RoundedRectangle ( cornerRadius : 18 )
. stroke ( Color . blue , lineWidth : 0.5 )
)
Image ( systemName : " arrowshape.turn.up.left.fill " )
. symbolRenderingMode ( . hierarchical )
. imageScale ( . large ) . foregroundColor ( . blue )
. padding ( . trailing )
2022-01-05 08:31:30 -08:00
}
2022-11-07 18:31:12 -08:00
}
HStack ( alignment : . top ) {
if currentUser { Spacer ( minLength : 50 ) }
if ! currentUser {
CircleText ( text : message . fromUser ? . shortName ? ? " ???? " , color : currentUser ? . accentColor : Color ( . darkGray ) , circleSize : 44 , fontSize : 14 )
. padding ( . all , 5 )
. offset ( y : - 5 )
}
VStack ( alignment : currentUser ? . trailing : . leading ) {
Text ( message . messagePayload ? ? " EMPTY MESSAGE " )
. padding ( 10 )
. foregroundColor ( . white )
. background ( currentUser ? Color . blue : Color ( . darkGray ) )
. cornerRadius ( 15 )
. contextMenu {
VStack {
Text ( " Channel: \( message . channel ) " )
}
Menu ( " Tapback response " ) {
ForEach ( Tapbacks . allCases ) { tb in
Button ( action : {
2022-11-07 18:51:17 -08:00
if bleManager . sendMessage ( message : tb . emojiString , toUserNum : user . num , channel : 0 , isEmoji : true , replyID : message . messageId ) {
2022-11-07 18:31:12 -08:00
print ( " Sent \( tb . emojiString ) Tapback " )
self . context . refresh ( user , mergeChanges : true )
} else { print ( " \( tb . emojiString ) Tapback Failed " ) }
} ) {
Text ( tb . description )
let image = tb . emojiString . image ( )
Image ( uiImage : image ! )
2022-10-17 18:52:49 -07:00
}
2022-06-12 10:35:23 -07:00
}
2022-11-07 18:31:12 -08:00
}
Button ( action : {
self . replyMessageId = message . messageId
self . focusedField = . messageText
print ( " I want to reply to \( message . messageId ) " )
} ) {
Text ( " Reply " )
Image ( systemName : " arrowshape.turn.up.left.2.fill " )
}
Button ( action : {
UIPasteboard . general . string = message . messagePayload
} ) {
Text ( " Copy " )
Image ( systemName : " doc.on.doc " )
}
Menu ( " Message Details " ) {
VStack {
let messageDate = Date ( timeIntervalSince1970 : TimeInterval ( message . messageTimestamp ) )
Text ( " Date \( messageDate , style : . date ) \( messageDate . formattedDate ( format : " h:mm:ss a " ) ) " ) . font ( . caption2 ) . foregroundColor ( . gray )
2022-02-04 02:26:58 -08:00
}
2022-11-10 23:34:15 -08:00
if ! currentUser {
VStack {
2022-11-11 11:05:55 -08:00
Text ( " SNR \( String ( format : " %.2f " , message . snr ) ) dB " )
2022-11-10 23:34:15 -08:00
}
}
2022-11-07 18:31:12 -08:00
if currentUser && message . receivedACK {
2022-02-23 23:05:47 -10:00
VStack {
2022-11-07 18:31:12 -08:00
Text ( " Received Ack \( message . receivedACK ? " ✔️ " : " " ) " )
2022-10-17 18:52:49 -07:00
}
2022-11-07 18:31:12 -08:00
} else if currentUser && message . ackError = = 0 {
// E m p t y E r r o r
Text ( " Waiting. . . " )
} else if currentUser && message . ackError > 0 {
let ackErrorVal = RoutingError ( rawValue : Int ( message . ackError ) )
Text ( " \( ackErrorVal ? . display ? ? " No Error " ) " ) . fixedSize ( horizontal : false , vertical : true )
}
if currentUser {
VStack {
let ackDate = Date ( timeIntervalSince1970 : TimeInterval ( message . ackTimestamp ) )
let sixMonthsAgo = Calendar . current . date ( byAdding : . month , value : - 6 , to : Date ( ) )
if ackDate >= sixMonthsAgo ! {
Text ( ( ackDate . formattedDate ( format : " h:mm:ss a " ) ) ) . font ( . caption2 ) . foregroundColor ( . gray )
} else {
Text ( " Unknown Age " ) . font ( . caption2 ) . foregroundColor ( . gray )
2022-08-25 14:14:54 -07:00
}
2022-02-23 23:05:47 -10:00
}
}
2022-11-07 18:31:12 -08:00
if message . ackSNR != 0 {
VStack {
2022-11-11 16:22:50 -08:00
Text ( " Ack SNR \( String ( format : " %.2f " , message . ackSNR ) ) dB " )
2022-11-07 18:31:12 -08:00
. font ( . caption2 )
. foregroundColor ( . gray )
2022-01-04 22:57:33 -08:00
}
}
2022-11-07 18:31:12 -08:00
}
Divider ( )
Button ( role : . destructive , action : {
self . showDeleteMessageAlert = true
self . deleteMessageId = message . messageId
print ( deleteMessageId )
} ) {
Text ( " Delete " )
Image ( systemName : " trash " )
2022-08-05 15:58:12 -07:00
}
2022-01-01 08:03:46 -08:00
}
2022-11-07 18:31:12 -08:00
let tapbacks = message . value ( forKey : " tapbacks " ) as ! [ MessageEntity ]
if tapbacks . count > 0 {
VStack ( alignment : . trailing ) {
HStack {
ForEach ( tapbacks ) { ( tapback : MessageEntity ) in
VStack {
let image = tapback . messagePayload ! . image ( fontSize : 20 )
Image ( uiImage : image ! ) . font ( . caption )
Text ( " \( tapback . fromUser ? . shortName ? ? " ???? " ) " )
. font ( . caption2 )
. foregroundColor ( . gray )
. fixedSize ( )
. padding ( . bottom , 1 )
}
}
2022-10-17 18:52:49 -07:00
}
2022-11-07 18:31:12 -08:00
. padding ( 10 )
. overlay (
RoundedRectangle ( cornerRadius : 18 )
. stroke ( Color . gray , lineWidth : 1 )
)
2022-10-14 22:18:28 -07:00
}
2022-01-01 08:03:46 -08:00
}
2022-11-07 18:31:12 -08:00
HStack {
if currentUser && message . receivedACK {
// A c k R e c e i v e d
Text ( " Acknowledged " ) . font ( . caption2 ) . foregroundColor ( . gray )
} else if currentUser && message . ackError = = 0 {
// E m p t y E r r o r
Text ( " Waiting to be acknowledged. . . " ) . font ( . caption2 ) . foregroundColor ( . orange )
} else if currentUser && message . ackError > 0 {
let ackErrorVal = RoutingError ( rawValue : Int ( message . ackError ) )
Text ( " \( ackErrorVal ? . display ? ? " No Error " ) " ) . fixedSize ( horizontal : false , vertical : true )
. font ( . caption2 ) . foregroundColor ( . red )
}
2022-10-17 18:52:49 -07:00
}
2022-01-01 15:45:00 -08:00
}
2022-11-07 18:31:12 -08:00
. padding ( . bottom )
. id ( user . messageList . firstIndex ( of : message ) )
if ! currentUser {
Spacer ( minLength : 50 )
2022-10-15 01:00:13 -07:00
}
2022-01-01 08:03:46 -08:00
}
2022-11-07 18:31:12 -08:00
. padding ( [ . leading , . trailing ] )
. frame ( maxWidth : . infinity )
. id ( message . messageId )
. alert ( isPresented : $ showDeleteMessageAlert ) {
Alert ( title : Text ( " Are you sure you want to delete this message? " ) , message : Text ( " This action is permanent. " ) , primaryButton : . destructive ( Text ( " Delete " ) ) {
print ( " OK button tapped " )
if deleteMessageId > 0 {
let message = user . messageList . first ( where : { $0 . messageId = = deleteMessageId } )
context . delete ( message ! )
do {
try context . save ( )
deleteMessageId = 0
} catch {
print ( " Failed to delete message \( deleteMessageId ) " )
}
}
} , secondaryButton : . cancel ( ) )
}
2022-01-01 21:44:47 -08:00
}
2022-01-01 08:03:46 -08:00
}
}
2022-01-01 15:45:00 -08:00
}
2022-10-15 01:34:50 -07:00
. padding ( [ . top ] )
2022-09-17 12:41:27 -07:00
. scrollDismissesKeyboard ( . immediately )
2022-01-01 15:45:00 -08:00
. onAppear ( perform : {
self . bleManager . context = context
2022-06-06 22:24:35 -07:00
refreshId = UUID ( )
2022-10-17 18:52:49 -07:00
if user . messageList . count > 0 {
2022-10-06 08:56:15 -07:00
scrollView . scrollTo ( user . messageList . last ! . messageId )
}
2022-06-04 09:53:59 -07:00
} )
. onChange ( of : user . messageList , perform : { messages in
2022-06-06 22:24:35 -07:00
refreshId = UUID ( )
2022-10-17 18:52:49 -07:00
if user . messageList . count > 0 {
scrollView . scrollTo ( user . messageList . last ! . messageId )
}
2022-01-02 10:05:13 -08:00
} )
2022-01-01 15:45:00 -08:00
}
HStack ( alignment : . top ) {
2022-01-01 08:03:46 -08:00
2022-01-01 15:45:00 -08:00
ZStack {
let kbType = UIKeyboardType ( rawValue : UserDefaults . standard . object ( forKey : " keyboardType " ) as ? Int ? ? 0 )
2022-10-15 01:34:50 -07:00
TextField ( " Message " , text : $ typingMessage , axis : . vertical )
2022-01-01 15:45:00 -08:00
. onChange ( of : typingMessage , perform : { value in
2022-10-15 01:34:50 -07:00
totalBytes = value . utf8 . count
2022-06-26 20:43:00 -07:00
// O n l y m e s s w i t h t h e v a l u e i f i t i s t o o b i g
2022-06-26 21:05:55 -07:00
if totalBytes > maxbytes {
2022-06-26 20:43:00 -07:00
let firstNBytes = Data ( typingMessage . utf8 . prefix ( maxbytes ) )
if let maxBytesString = String ( data : firstNBytes , encoding : String . Encoding . utf8 ) {
// S e t t h e m e s s a g e b a c k t o t h e l a s t p l a c e w h e r e i t w a s t h e r i g h t s i z e
typingMessage = maxBytesString
} else {
print ( " not a valid UTF-8 sequence " )
}
2022-01-01 08:03:46 -08:00
}
2022-01-01 15:45:00 -08:00
} )
. keyboardType ( kbType ! )
. toolbar {
ToolbarItemGroup ( placement : . keyboard ) {
Button ( " Dismiss Keyboard " ) {
focusedField = nil
}
. font ( . subheadline )
Spacer ( )
2022-02-17 18:07:41 -08:00
Button {
let userLongName = bleManager . connectedPeripheral != nil ? bleManager . connectedPeripheral . longName : " Unknown "
sendPositionWithMessage = true
2022-02-18 06:22:02 -08:00
if user . num = = bleManager . broadcastNodeNum {
2022-02-23 23:05:47 -10:00
if userSettings . meshtasticUsername . count > 0 {
typingMessage = " 📍 " + userSettings . meshtasticUsername + " has shared their position with the mesh from node " + userLongName
} else {
typingMessage = " 📍 " + userLongName + " has shared their position with the mesh. "
}
2022-02-18 06:22:02 -08:00
} else {
2022-02-23 23:05:47 -10:00
if userSettings . meshtasticUsername . count > 0 {
typingMessage = " 📍 " + userSettings . meshtasticUsername + " has shared their position with you from node " + userLongName
} else {
typingMessage = " 📍 " + userLongName + " has shared their position with you. "
}
2022-02-18 06:22:02 -08:00
}
2022-02-17 18:07:41 -08:00
} label : {
2022-02-18 06:22:02 -08:00
Image ( systemName : " mappin.and.ellipse " )
. symbolRenderingMode ( . hierarchical )
. imageScale ( . large ) . foregroundColor ( . accentColor )
2022-02-17 18:07:41 -08:00
}
2022-01-01 15:45:00 -08:00
ProgressView ( " Bytes: \( totalBytes ) / \( maxbytes ) " , value : Double ( totalBytes ) , total : Double ( maxbytes ) )
. frame ( width : 130 )
. padding ( 5 )
. font ( . subheadline )
. accentColor ( . accentColor )
}
2022-01-01 08:03:46 -08:00
}
2022-01-01 15:45:00 -08:00
. padding ( . horizontal , 8 )
. focused ( $ focusedField , equals : . messageText )
. multilineTextAlignment ( . leading )
2022-10-15 01:34:50 -07:00
. frame ( minHeight : 50 )
2022-01-01 15:45:00 -08:00
Text ( typingMessage ) . opacity ( 0 ) . padding ( . all , 0 )
}
. overlay ( RoundedRectangle ( cornerRadius : 20 ) . stroke ( . tertiary , lineWidth : 1 ) )
. padding ( . bottom , 15 )
Button ( action : {
2022-11-07 18:51:17 -08:00
if bleManager . sendMessage ( message : typingMessage , toUserNum : user . num , channel : 0 , isEmoji : false , replyID : replyMessageId ) {
2022-01-01 15:45:00 -08:00
typingMessage = " "
focusedField = nil
2022-01-01 22:55:25 -08:00
replyMessageId = 0
2022-02-17 18:07:41 -08:00
if sendPositionWithMessage {
2022-08-12 08:58:10 -07:00
if bleManager . sendLocation ( destNum : user . num , wantAck : true ) {
print ( " Location Sent " )
2022-02-17 18:07:41 -08:00
}
}
2022-01-01 08:03:46 -08:00
}
2022-01-01 15:45:00 -08:00
} ) {
Image ( systemName : " arrow.up.circle.fill " ) . font ( . largeTitle ) . foregroundColor ( . blue )
2022-01-01 08:03:46 -08:00
}
}
2022-01-01 15:45:00 -08:00
. padding ( . all , 15 )
}
2022-01-01 08:03:46 -08:00
. navigationViewStyle ( . stack )
. navigationBarTitleDisplayMode ( . inline )
. toolbar {
ToolbarItem ( placement : . principal ) {
HStack {
2022-11-03 20:26:38 -07:00
CircleText ( text : user . shortName ? ? " ??? " , color : . blue , circleSize : 44 , fontSize : 14 ) . fixedSize ( )
Text ( user . longName ? ? " Unknown " ) . font ( . headline )
2022-01-01 08:03:46 -08:00
}
}
ToolbarItem ( placement : . navigationBarTrailing ) {
ZStack {
ConnectedDevice (
bluetoothOn : bleManager . isSwitchedOn ,
deviceConnected : bleManager . connectedPeripheral != nil ,
2022-07-01 19:44:25 -07:00
name : ( bleManager . connectedPeripheral != nil ) ? bleManager . connectedPeripheral . shortName : " ???? " )
2022-01-01 08:03:46 -08:00
}
}
}
}
}