mirror of https://github.com/lukechilds/damus.git
Browse Source
Closes #5 This adds encrypted direct message support to damus Changelog-Added: Implement NIP04: Encrypted Direct Messages Signed-off-by: William Casarin <jb55@jb55.com>profiles-everywhere
William Casarin
3 years ago
24 changed files with 889 additions and 225 deletions
@ -0,0 +1,15 @@ |
|||
// |
|||
// DirectMessagesModel.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-06-29. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
class DirectMessagesModel: ObservableObject { |
|||
@Published var events: [(String, [NostrEvent])] = [] |
|||
@Published var loading: Bool = false |
|||
|
|||
|
|||
} |
@ -0,0 +1,39 @@ |
|||
// |
|||
// InputDismissKeyboard.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-07-02. |
|||
// |
|||
|
|||
import Foundation |
|||
import SwiftUI |
|||
|
|||
public extension View { |
|||
func dismissKeyboardOnTap() -> some View { |
|||
modifier(DismissKeyboardOnTap()) |
|||
} |
|||
} |
|||
|
|||
public struct DismissKeyboardOnTap: ViewModifier { |
|||
public func body(content: Content) -> some View { |
|||
#if os(macOS) |
|||
return content |
|||
#else |
|||
return content.gesture(tapGesture) |
|||
#endif |
|||
} |
|||
|
|||
private var tapGesture: some Gesture { |
|||
TapGesture().onEnded(endEditing) |
|||
} |
|||
|
|||
private func endEditing() { |
|||
UIApplication.shared.connectedScenes |
|||
.filter {$0.activationState == .foregroundActive} |
|||
.map {$0 as? UIWindowScene} |
|||
.compactMap({$0}) |
|||
.first?.windows |
|||
.filter {$0.isKeyWindow} |
|||
.first?.endEditing(true) |
|||
} |
|||
} |
@ -0,0 +1,155 @@ |
|||
// |
|||
// DMChatView.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-06-30. |
|||
// |
|||
|
|||
import SwiftUI |
|||
|
|||
struct DMChatView: View { |
|||
let damus_state: DamusState |
|||
let pubkey: String |
|||
@Binding var events: [NostrEvent] |
|||
@State var message: String = "" |
|||
|
|||
var Messages: some View { |
|||
ScrollViewReader { scroller in |
|||
ScrollView { |
|||
VStack(alignment: .leading) { |
|||
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in |
|||
DMView(event: events[ind], damus_state: damus_state) |
|||
.event_context_menu(ev) |
|||
} |
|||
Color.white.opacity(0) |
|||
.id("endblock") |
|||
.frame(height: 80) |
|||
} |
|||
} |
|||
.onAppear { |
|||
scroller.scrollTo("endblock") |
|||
} |
|||
} |
|||
} |
|||
|
|||
var Header: some View { |
|||
let profile = damus_state.profiles.lookup(id: pubkey) |
|||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) |
|||
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey) |
|||
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel) |
|||
return NavigationLink(destination: profile_page) { |
|||
HStack { |
|||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) |
|||
|
|||
ProfileName(pubkey: pubkey, profile: profile) |
|||
} |
|||
} |
|||
.buttonStyle(PlainButtonStyle()) |
|||
} |
|||
|
|||
var InputField: some View { |
|||
TextField("New Message", text: $message) |
|||
.padding([.leading], 12) |
|||
.padding([.top, .bottom], 8) |
|||
.background { |
|||
InputBackground() |
|||
} |
|||
.foregroundColor(Color.primary) |
|||
.cornerRadius(20) |
|||
.padding([.leading, .top, .bottom], 8) |
|||
} |
|||
|
|||
@Environment(\.colorScheme) var colorScheme |
|||
|
|||
func InputBackground() -> some View { |
|||
if colorScheme == .dark { |
|||
return Color.black.brightness(0.1) |
|||
} else { |
|||
return Color.gray.brightness(0.35) |
|||
} |
|||
} |
|||
|
|||
func BackgroundColor() -> some View { |
|||
if colorScheme == .dark { |
|||
return Color.black.opacity(0.9) |
|||
} else { |
|||
return Color.white.opacity(0.9) |
|||
} |
|||
} |
|||
|
|||
var Footer: some View { |
|||
ZStack { |
|||
BackgroundColor() |
|||
|
|||
HStack { |
|||
InputField |
|||
|
|||
Button(role: .none, action: send_message) { |
|||
Label("", systemImage: "arrow.right.circle") |
|||
.font(.title) |
|||
} |
|||
} |
|||
} |
|||
.frame(height: 70) |
|||
} |
|||
|
|||
func send_message() { |
|||
guard let dm = create_dm(message, to_pk: pubkey, keypair: damus_state.keypair) else { |
|||
print("error creating dm") |
|||
return |
|||
} |
|||
|
|||
message = "" |
|||
|
|||
damus_state.pool.send(.event(dm)) |
|||
} |
|||
|
|||
var body: some View { |
|||
ZStack { |
|||
Messages |
|||
.padding([.top, .leading, .trailing], 10) |
|||
.dismissKeyboardOnTap() |
|||
|
|||
VStack { |
|||
Spacer() |
|||
|
|||
Footer |
|||
} |
|||
} |
|||
.toolbar { Header } |
|||
} |
|||
} |
|||
|
|||
struct DMChatView_Previews: PreviewProvider { |
|||
static var previews: some View { |
|||
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: []) |
|||
let evs = Binding<[NostrEvent]>.init( |
|||
get: { [ev] }, |
|||
set: { _ in }) |
|||
|
|||
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey", events: evs) |
|||
} |
|||
} |
|||
|
|||
|
|||
func create_dm(_ message: String, to_pk: String, keypair: Keypair) -> NostrEvent? |
|||
{ |
|||
guard let privkey = keypair.privkey else { |
|||
return nil |
|||
} |
|||
|
|||
let tags = [["p", to_pk]] |
|||
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 |
|||
} |
|||
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv) |
|||
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags) |
|||
ev.calculate_id() |
|||
ev.sign(privkey: privkey) |
|||
return ev |
|||
} |
@ -0,0 +1,39 @@ |
|||
// |
|||
// DMView.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-07-01. |
|||
// |
|||
|
|||
import SwiftUI |
|||
|
|||
struct DMView: View { |
|||
let event: NostrEvent |
|||
let damus_state: DamusState |
|||
|
|||
var is_ours: Bool { |
|||
event.pubkey == damus_state.pubkey |
|||
} |
|||
|
|||
var body: some View { |
|||
HStack { |
|||
if is_ours { |
|||
Spacer() |
|||
} |
|||
|
|||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.get_content(damus_state.keypair.privkey)) |
|||
.foregroundColor(is_ours ? Color.white : Color.primary) |
|||
.padding(10) |
|||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) |
|||
.cornerRadius(8.0) |
|||
.tint(is_ours ? Color.white : Color.accentColor) |
|||
} |
|||
} |
|||
} |
|||
|
|||
struct DMView_Previews: PreviewProvider { |
|||
static var previews: some View { |
|||
let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", pubkey: "pubkey", kind: 1, tags: []) |
|||
DMView(event: ev, damus_state: test_damus_state()) |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
// |
|||
// DirectMessagesView.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-06-29. |
|||
// |
|||
|
|||
import SwiftUI |
|||
|
|||
struct DirectMessagesView: View { |
|||
let damus_state: DamusState |
|||
@Binding var dms: [(String, [NostrEvent])] |
|||
|
|||
var MainContent: some View { |
|||
ScrollView { |
|||
VStack { |
|||
ForEach(dms, id: \.0) { tup in |
|||
let evs = Binding<[NostrEvent]>.init( |
|||
get: { tup.1 }, |
|||
set: { _ in } |
|||
) |
|||
let chat = DMChatView(damus_state: damus_state, pubkey: tup.0, events: evs) |
|||
NavigationLink(destination: chat) { |
|||
EventView(damus: damus_state, event: tup.1.last!, pubkey: tup.0) |
|||
} |
|||
.buttonStyle(PlainButtonStyle()) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
var body: some View { |
|||
MainContent |
|||
.navigationTitle("Encrypted DMs") |
|||
} |
|||
} |
|||
|
|||
struct DirectMessagesView_Previews: PreviewProvider { |
|||
static var previews: some View { |
|||
let ev = NostrEvent(content: "encrypted stuff", |
|||
pubkey: "pubkey", |
|||
kind: 4, |
|||
tags: []) |
|||
let dms = Binding<[(String, [NostrEvent])]>.init( |
|||
get: { |
|||
return [ ("pubkey", [ ev ]) ] |
|||
}, |
|||
set: { _ in } |
|||
) |
|||
DirectMessagesView(damus_state: test_damus_state(), dms: dms) |
|||
} |
|||
} |
Loading…
Reference in new issue