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