damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

DMChatView.swift (7577B)


      1 //
      2 //  DMChatView.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2022-06-30.
      6 //
      7 
      8 import SwiftUI
      9 import Combine
     10 
     11 struct DMChatView: View, KeyboardReadable {
     12     let damus_state: DamusState
     13     @FocusState private var isTextFieldFocused: Bool
     14     @ObservedObject var dms: DirectMessageModel
     15     
     16     var pubkey: Pubkey {
     17         dms.pubkey
     18     }
     19     
     20     var Messages: some View {
     21         ScrollViewReader { scroller in
     22             ScrollView {
     23                 LazyVStack(alignment: .leading) {
     24                     ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0)}, id: \.0.id) { (ev, ind) in
     25                         DMView(event: dms.events[ind], damus_state: damus_state)
     26                             .contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
     27                     }
     28                     EndBlock(height: 1)
     29                 }
     30                 .padding(.horizontal)
     31 
     32             }
     33             .dismissKeyboardOnTap()
     34             .onAppear {
     35                 scroll_to_end(scroller)
     36             }.onChange(of: dms.events.count) { _ in
     37                 scroll_to_end(scroller, animated: true)
     38             }
     39             
     40             Footer
     41                 .onReceive(keyboardPublisher) { visible in
     42                     guard visible else {
     43                         return
     44                     }
     45                     DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
     46                         scroll_to_end(scroller, animated: true)
     47                     }
     48                 }
     49         }
     50         .padding(.bottom, isTextFieldFocused ? 0 : tabHeight)
     51     }
     52     
     53     func scroll_to_end(_ scroller: ScrollViewProxy, animated: Bool = false) {
     54         if animated {
     55             withAnimation {
     56                 scroller.scrollTo("endblock")
     57             }
     58         } else {
     59             scroller.scrollTo("endblock")
     60         }
     61     }
     62 
     63     var Header: some View {
     64         return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
     65             HStack {
     66                 ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
     67 
     68                 ProfileName(pubkey: pubkey, damus: damus_state)
     69             }
     70         }
     71         .buttonStyle(PlainButtonStyle())
     72     }
     73 
     74     var InputField: some View {
     75         TextEditor(text: $dms.draft)
     76             .textEditorBackground {
     77                 InputBackground()
     78             }
     79             .focused($isTextFieldFocused)
     80             .cornerRadius(8)
     81             .background(
     82                 RoundedRectangle(cornerRadius: 8)
     83                     .stroke(style: .init(lineWidth: 2))
     84                     .foregroundColor(.secondary.opacity(0.2))
     85             )
     86             .padding(16)
     87             .foregroundColor(Color.primary)
     88             .frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
     89             .fixedSize(horizontal: false, vertical: true)
     90     }
     91 
     92     @Environment(\.colorScheme) var colorScheme
     93 
     94     func InputBackground() -> Color {
     95         if colorScheme == .light {
     96             return Color.init(.sRGB, red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0)
     97         } else {
     98             return Color.init(.sRGB, red: 0.1, green: 0.1, blue: 0.1, opacity: 1.0)
     99         }
    100     }
    101 
    102     var Footer: some View {
    103     
    104         HStack(spacing: 0) {
    105             InputField
    106 
    107             if !dms.draft.isEmpty {
    108                 Button(
    109                     role: .none,
    110                     action: {
    111                         send_message()
    112                     }
    113                 ) {
    114                     Label("", image: "send")
    115                         .font(.title)
    116                 }
    117             }
    118         }
    119 
    120         /*
    121         Text(dms.draft).opacity(0).padding(.all, 8)
    122             .fixedSize(horizontal: false, vertical: true)
    123             .frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
    124          */
    125     }
    126 
    127     func send_message() {
    128         let tags = [["p", pubkey.hex()]]
    129         let post_blocks = parse_post_blocks(content: dms.draft)
    130         let content = post_blocks
    131             .map(\.asString)
    132             .joined(separator: "")
    133 
    134         guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
    135             print("error creating dm")
    136             return
    137         }
    138 
    139         dms.draft = ""
    140 
    141         damus_state.postbox.send(dm)
    142         
    143         handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
    144 
    145         end_editing()
    146     }
    147 
    148     var body: some View {
    149         ZStack {
    150             Messages
    151 
    152             Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
    153                 .lineLimit(nil)
    154                 .multilineTextAlignment(.center)
    155                 .padding(.horizontal, 40)
    156                 .opacity(((dms.events.count == 0) ? 1.0 : 0.0))
    157                 .foregroundColor(.gray)
    158         }
    159         .navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
    160         .toolbar { Header }
    161         .onDisappear {
    162             if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
    163                 dms.draft = ""
    164             }
    165         }
    166     }
    167 }
    168 
    169 struct DMChatView_Previews: PreviewProvider {
    170     static var previews: some View {
    171         let ev = NostrEvent(content: "hi", keypair: test_keypair, kind: 1, tags: [])!
    172 
    173         let model = DirectMessageModel(events: [ev], our_pubkey: test_pubkey, pubkey: test_pubkey)
    174 
    175         DMChatView(damus_state: test_damus_state, dms: model)
    176     }
    177 }
    178 
    179 func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
    180     let iv = random_bytes(count: 16).bytes
    181     guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
    182         return nil
    183     }
    184     let utf8_message = Data(message.utf8).bytes
    185     guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
    186         return nil
    187     }
    188     
    189     switch encoding {
    190     case .base64:
    191         return encode_dm_base64(content: enc_message.bytes, iv: iv)
    192     case .bech32:
    193         return encode_dm_bech32(content: enc_message.bytes, iv: iv)
    194     }
    195     
    196 }
    197 
    198 func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
    199     let privkey = keypair.privkey
    200     
    201     guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
    202         return nil
    203     }
    204     
    205     return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
    206 }
    207 
    208 func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
    209 {
    210     let created = created_at ?? UInt32(Date().timeIntervalSince1970)
    211 
    212     guard let keypair = keypair.to_full() else {
    213         return nil
    214     }
    215     
    216     return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
    217 }
    218 
    219 extension View {
    220 /// Layers the given views behind this ``TextEditor``.
    221     func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
    222         self
    223             .onAppear {
    224                 UITextView.appearance().backgroundColor = .clear
    225             }
    226             .background(content())
    227     }
    228 }
    229