227 lines
7.3 KiB
Swift
227 lines
7.3 KiB
Swift
//
|
|
// 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<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
|
|
self
|
|
.onAppear {
|
|
UITextView.appearance().backgroundColor = .clear
|
|
}
|
|
.background(content())
|
|
}
|
|
}
|
|
|