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