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