// // DMChatView.swift // damus // // Created by William Casarin on 2022-06-30. // import SwiftUI import Combine struct DMChatView: View, KeyboardReadable { let damus_state: DamusState @ObservedObject var dms: DirectMessageModel var pubkey: Pubkey { dms.pubkey } var Messages: some View { ScrollViewReader { scroller in ScrollView { LazyVStack(alignment: .leading) { ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in DMView(event: dms.events[ind], damus_state: damus_state) .contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))} } EndBlock(height: 1) } .padding(.horizontal) } .dismissKeyboardOnTap() .onAppear { scroll_to_end(scroller) }.onChange(of: dms.events.count) { _ in scroll_to_end(scroller, animated: true) } Footer .onReceive(keyboardPublisher) { visible in guard visible else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { scroll_to_end(scroller, animated: true) } } } } func scroll_to_end(_ scroller: ScrollViewProxy, animated: Bool = false) { if animated { withAnimation { scroller.scrollTo("endblock") } } else { scroller.scrollTo("endblock") } } var Header: some View { return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) ProfileName(pubkey: pubkey, damus: damus_state) } } .buttonStyle(PlainButtonStyle()) } var InputField: some View { TextEditor(text: $dms.draft) .textEditorBackground { InputBackground() } .cornerRadius(8) .background( RoundedRectangle(cornerRadius: 8) .stroke(style: .init(lineWidth: 2)) .foregroundColor(.secondary.opacity(0.2)) ) .padding(16) .foregroundColor(Color.primary) .frame(minHeight: 70, maxHeight: 150, alignment: .bottom) .fixedSize(horizontal: false, vertical: true) } @Environment(\.colorScheme) var colorScheme func InputBackground() -> Color { if colorScheme == .light { return Color.init(.sRGB, red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0) } else { return Color.init(.sRGB, red: 0.1, green: 0.1, blue: 0.1, opacity: 1.0) } } var Footer: some View { HStack(spacing: 0) { InputField if !dms.draft.isEmpty { Button( role: .none, action: { send_message() } ) { Label("", image: "send") .font(.title) } } } /* Text(dms.draft).opacity(0).padding(.all, 8) .fixedSize(horizontal: false, vertical: true) .frame(minHeight: 70, maxHeight: 150, alignment: .bottom) */ } func send_message() { let tags = [["p", pubkey.hex()]] let post_blocks = parse_post_blocks(content: dms.draft) let content = post_blocks .map(\.asString) .joined(separator: "") guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else { print("error creating dm") return } dms.draft = "" damus_state.postbox.send(dm) handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits()) end_editing() } var body: some View { ZStack { Messages Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.") .lineLimit(nil) .multilineTextAlignment(.center) .padding(.horizontal, 40) .opacity(((dms.events.count == 0) ? 1.0 : 0.0)) .foregroundColor(.gray) } .navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.")) .toolbar { Header } .onDisappear { if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { dms.draft = "" } } } } struct DMChatView_Previews: PreviewProvider { static var previews: some View { let ev = NostrEvent(content: "hi", keypair: test_keypair, kind: 1, tags: [])! let model = DirectMessageModel(events: [ev], our_pubkey: test_pubkey, pubkey: test_pubkey) DMChatView(damus_state: test_damus_state, dms: model) } } func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? { let iv = random_bytes(count: 16).bytes guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else { return nil } let utf8_message = Data(message.utf8).bytes guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else { return nil } switch encoding { case .base64: return encode_dm_base64(content: enc_message.bytes, iv: iv) case .bech32: return encode_dm_bech32(content: enc_message.bytes, iv: iv) } } func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? { let privkey = keypair.privkey guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else { return nil } return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at) } func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent? { let created = created_at ?? UInt32(Date().timeIntervalSince1970) guard let keypair = keypair.to_full() else { return nil } return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4) } extension View { /// Layers the given views behind this ``TextEditor``. func textEditorBackground(@ViewBuilder _ content: () -> V) -> some View where V : View { self .onAppear { UITextView.appearance().backgroundColor = .clear } .background(content()) } }