damus

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

DMChatView.swift (7450B)


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