import Combine import Foundation import Martin import SwiftUI struct ConversationScreen: View { @EnvironmentObject var store: AppStore var body: some View { ZStack { // Background color Color.Main.backgroundLight .ignoresSafeArea() // Content VStack(spacing: 0) { // Header ConversationScreenHeader() // Msg list let messages = store.state.conversationsState.currentMessages if !messages.isEmpty { List { ForEach(messages) { message in ConversationMessageRow(message: message) } } .listStyle(.plain) .background(Color.Main.backgroundLight) .scrollDismissesKeyboard(.immediately) } else { Spacer() } } .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } .safeAreaInset(edge: .bottom, spacing: 0) { ConversationScreenTextInput() } } } struct ConversationScreenTextInput: View { @EnvironmentObject var store: AppStore @State private var messageStr = "" var body: some View { HStack { Image(systemName: "paperclip") .font(.title2) .foregroundColor(.Tango.blueLight) .padding(.leading, 8) .tappablePadding(.symmetric(8)) { print("Attach file") } TextField(L10n.Chat.textfieldPrompt, text: $messageStr) .font(.body1) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.Main.white) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding(.vertical, 4) let img = messageStr.isEmpty ? "paperplane" : "paperplane.fill" Image(systemName: img) .font(.title2) .foregroundColor(messageStr.isEmpty ? .Main.separator : .Tango.blueLight) .padding(.trailing, 8) .tappablePadding(.symmetric(8)) { if !messageStr.isEmpty { guard let acc = store.state.conversationsState.currentChat?.account else { return } guard let contact = store.state.conversationsState.currentChat?.participant else { return } store.dispatch(.conversationAction(.sendMessage( from: acc, to: contact, body: messageStr ))) messageStr = "" UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } } .padding(.vertical, 8) .background(Color.Main.backgroundDark) } } private struct ConversationScreenHeader: View { @EnvironmentObject var store: AppStore var body: some View { ZStack { // bg Color.Main.backgroundDark .ignoresSafeArea() // title let name = ( store.state.conversationsState.currentRoster?.name ?? store.state.conversationsState.currentRoster?.contactBareJid ) ?? L10n.Chat.title Text(name) .font(.head2) .foregroundColor(Color.Main.black) HStack { Image(systemName: "chevron.left") .foregroundColor(Color.Tango.orangeMedium) .tappablePadding(.symmetric(12)) { store.dispatch(.changeFlow(store.state.previousFlow)) } Spacer() } .padding(.horizontal, 16) } .frame(height: 44) } } private struct ConversationMessageRow: View { @EnvironmentObject var store: AppStore let message: Message var body: some View { VStack(spacing: 0) { HStack(spacing: 0) { if isOutgoing() { Spacer() VStack(spacing: 0) { MessageTime(message: message) Spacer() } .padding(.trailing, 4) } MessageContainer(message: message, isOutgoing: isOutgoing()) .background(isOutgoing() ? Color.Material.greenDark100 : Color.Main.white) .clipShape(MessageBubble(isOutgoing: isOutgoing())) if !isOutgoing() { VStack(spacing: 0) { MessageTime(message: message) Spacer() } .padding(.leading, 4) Spacer() } } .padding(.vertical, 10) .padding(.horizontal, 16) } .sharedListRow() } private func isOutgoing() -> Bool { message.from == store.state.conversationsState.currentChat?.account } } struct MessageContainer: View { let message: Message let isOutgoing: Bool var body: some View { Text(message.body ?? "...") .font(.body2) .foregroundColor(Color.Main.black) .multilineTextAlignment(.leading) .padding(10) } } struct MessageBubble: Shape { let isOutgoing: Bool func path(in rect: CGRect) -> Path { let path = UIBezierPath( roundedRect: rect, byRoundingCorners: isOutgoing ? [.topLeft, .bottomLeft, .bottomRight] : [.topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 8, height: 10) ) return Path(path.cgPath) } } struct MessageTime: View { let message: Message var body: some View { Text(message.date, style: .time) .font(.sub2) .foregroundColor(Color.Main.gray) } } // Preview #if DEBUG struct ConversationScreen_Previews: PreviewProvider { static var previews: some View { ConversationScreen() .environmentObject(pStore) } static var pStore: AppStore { let state = pState return AppStore(initialState: state, reducer: AppState.reducer, middlewares: []) } static var pState: AppState { var state = AppState() let acc = "user@test.com" let contact = "some@test.com" state.conversationsState.currentChat = Chat(id: "1", account: acc, participant: contact, type: .chat) state.conversationsState.currentMessages = [ Message( id: "1", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test sdgdsfg dsfg dsfgdg dsfgdfgsdgsdfgdfg sdfgdsfgdfsg dsfgdsfgsdfg dsfgdfgsdg fgf fgfg sdfsdf sdfsdf sdf sdfsdf sdf sdfsdf sdfsdfsdf sdfsdf ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false ), Message(id: "2", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for testsdfsdf sdfsdf sdfs sdf sdffsdf sdf sdf sdf sdf sdf sdff sdfffwwe ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "3", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "4", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for test sdfkjwek jwkjfh jwerf jdfhskjdhf jsdhfjhwefh sjdhfh fsdjhfh sd ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "5", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test sdfjkkeke kekkddjw;; w;edkdjfj l kjwekrjfk wef", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "6", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for testsdf dsdkkekkddn wejkjfj ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message( id: "7", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for test sdgdsfg dsfg dsfgdg dsfgdfgsdgsdfgdfg sdfgdsfgdfsg dsfgdsfgsdfg dsfgdfgsdg fgf fgfg sdfsdf sdfsdf sdf sdfsdf sdf sdfsdf sdfsdfsdf sdfsdf ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false ), Message(id: "8", type: .chat, contentType: .text, from: acc, to: contact, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "9", type: .chat, contentType: .text, from: contact, to: acc, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "10", type: .chat, contentType: .text, from: acc, to: contact, body: "so test so test so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), Message(id: "11", type: .chat, contentType: .text, from: contact, to: acc, body: "xD", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false) ] return state } } #endif