325 lines
11 KiB
Swift
325 lines
11 KiB
Swift
//
|
|
// Post.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2022-04-03.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
enum NostrPostResult {
|
|
case post(NostrPost)
|
|
case cancel
|
|
}
|
|
|
|
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
|
|
|
|
struct PostView: View {
|
|
@State var post: NSMutableAttributedString = NSMutableAttributedString()
|
|
@FocusState var focus: Bool
|
|
@State var showPrivateKeyWarning: Bool = false
|
|
@State var attach_media: Bool = false
|
|
@State var attach_camera: Bool = false
|
|
@State var error: String? = nil
|
|
|
|
@State var originalReferences: [ReferencedId] = []
|
|
@State var references: [ReferencedId] = []
|
|
|
|
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
|
|
|
let replying_to: NostrEvent?
|
|
let damus_state: DamusState
|
|
|
|
@Environment(\.presentationMode) var presentationMode
|
|
|
|
enum FocusField: Hashable {
|
|
case post
|
|
}
|
|
|
|
func cancel() {
|
|
NotificationCenter.default.post(name: .post, object: NostrPostResult.cancel)
|
|
dismiss()
|
|
}
|
|
|
|
func dismiss() {
|
|
self.presentationMode.wrappedValue.dismiss()
|
|
}
|
|
|
|
func send_post() {
|
|
var kind: NostrKind = .text
|
|
if replying_to?.known_kind == .chat {
|
|
kind = .chat
|
|
}
|
|
|
|
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
|
if let link = attributes[.link] as? String {
|
|
post.replaceCharacters(in: range, with: link)
|
|
}
|
|
}
|
|
|
|
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
let new_post = NostrPost(content: content, references: references, kind: kind)
|
|
|
|
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
|
|
|
|
if let replying_to {
|
|
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
|
} else {
|
|
damus_state.drafts.post = NSMutableAttributedString(string: "")
|
|
}
|
|
|
|
dismiss()
|
|
}
|
|
|
|
var is_post_empty: Bool {
|
|
return post.string.allSatisfy { $0.isWhitespace }
|
|
}
|
|
|
|
var ImageButton: some View {
|
|
Button(action: {
|
|
attach_media = true
|
|
}, label: {
|
|
Image(systemName: "photo")
|
|
})
|
|
}
|
|
|
|
var CameraButton: some View {
|
|
Button(action: {
|
|
attach_camera = true
|
|
}, label: {
|
|
Image(systemName: "camera")
|
|
})
|
|
}
|
|
|
|
var AttachmentBar: some View {
|
|
HStack(alignment: .center) {
|
|
ImageButton
|
|
CameraButton
|
|
}
|
|
.disabled(image_upload.progress != nil)
|
|
}
|
|
|
|
var PostButton: some View {
|
|
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
|
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
|
|
|
|
if !showPrivateKeyWarning {
|
|
self.send_post()
|
|
}
|
|
}
|
|
.disabled(is_post_empty)
|
|
.font(.system(size: 14, weight: .bold))
|
|
.frame(width: 80, height: 30)
|
|
.foregroundColor(.white)
|
|
.background(LINEAR_GRADIENT)
|
|
.opacity(is_post_empty ? 0.5 : 1.0)
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
var TextEntry: some View {
|
|
ZStack(alignment: .topLeading) {
|
|
TextViewWrapper(attributedText: $post)
|
|
.focused($focus)
|
|
.textInputAutocapitalization(.sentences)
|
|
.onChange(of: post) { _ in
|
|
if let replying_to {
|
|
damus_state.drafts.replies[replying_to] = post
|
|
} else {
|
|
damus_state.drafts.post = post
|
|
}
|
|
}
|
|
|
|
if post.string.isEmpty {
|
|
Text(POST_PLACEHOLDER)
|
|
.padding(.top, 8)
|
|
.padding(.leading, 4)
|
|
.foregroundColor(Color(uiColor: .placeholderText))
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
var TopBar: some View {
|
|
VStack {
|
|
HStack(spacing: 5.0) {
|
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) {
|
|
self.cancel()
|
|
}
|
|
.foregroundColor(.primary)
|
|
|
|
if let error {
|
|
Text(error)
|
|
.foregroundColor(.red)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
PostButton
|
|
}
|
|
|
|
if let progress = image_upload.progress {
|
|
ProgressView(value: progress, total: 1.0)
|
|
.progressViewStyle(.linear)
|
|
}
|
|
}
|
|
.frame(height: 30)
|
|
.padding([.bottom], 10)
|
|
}
|
|
|
|
func append_url(_ url: String) {
|
|
let uploadedImageURL = NSMutableAttributedString(string: url)
|
|
let combinedAttributedString = NSMutableAttributedString()
|
|
combinedAttributedString.append(post)
|
|
if !post.string.hasSuffix(" ") {
|
|
combinedAttributedString.append(NSAttributedString(string: " "))
|
|
}
|
|
combinedAttributedString.append(uploadedImageURL)
|
|
|
|
// make sure we have a space at the end
|
|
combinedAttributedString.append(NSAttributedString(string: " "))
|
|
post = combinedAttributedString
|
|
}
|
|
|
|
func handle_upload(media: MediaUpload) {
|
|
let uploader = get_media_uploader(damus_state.pubkey)
|
|
|
|
Task.init {
|
|
let res = await image_upload.start(media: media, uploader: uploader)
|
|
|
|
switch res {
|
|
case .success(let url):
|
|
append_url(url)
|
|
|
|
case .failed(let error):
|
|
if let error {
|
|
self.error = error.localizedDescription
|
|
} else {
|
|
self.error = "Error uploading image :("
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { (deviceSize: GeometryProxy) in
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
let searching = get_searching_string(post.string)
|
|
|
|
TopBar
|
|
|
|
ScrollViewReader { scroller in
|
|
ScrollView {
|
|
if let replying_to = replying_to {
|
|
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
|
|
}
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack(alignment: .top) {
|
|
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
|
.padding(.leading, replying_to != nil ? 15 : 0)
|
|
|
|
TextEntry
|
|
}
|
|
.frame(height: deviceSize.size.height*0.78)
|
|
.id("post")
|
|
}
|
|
}
|
|
.frame(maxHeight: searching == nil ? .infinity : 70)
|
|
.onAppear {
|
|
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
|
|
}
|
|
}
|
|
|
|
// This if-block observes @ for tagging
|
|
if let searching {
|
|
UserSearch(damus_state: damus_state, search: searching, post: $post)
|
|
.padding(.leading, replying_to != nil ? 15 : 0)
|
|
.frame(maxHeight: .infinity)
|
|
} else {
|
|
Divider()
|
|
.padding([.top, .bottom], 10)
|
|
VStack(alignment: .leading) {
|
|
AttachmentBar
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.sheet(isPresented: $attach_media) {
|
|
ImagePicker(sourceType: .photoLibrary, damusState: damus_state) { img in
|
|
handle_upload(media: .image(img))
|
|
} onVideoPicked: { url in
|
|
handle_upload(media: .video(url))
|
|
}
|
|
}
|
|
.sheet(isPresented: $attach_camera) {
|
|
ImagePicker(sourceType: .camera, damusState: damus_state) { img in
|
|
handle_upload(media: .image(img))
|
|
} onVideoPicked: { url in
|
|
handle_upload(media: .video(url))
|
|
}
|
|
}
|
|
.onAppear() {
|
|
if let replying_to {
|
|
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
|
|
originalReferences = references
|
|
if damus_state.drafts.replies[replying_to] == nil {
|
|
damus_state.drafts.post = NSMutableAttributedString(string: "")
|
|
}
|
|
if let p = damus_state.drafts.replies[replying_to] {
|
|
post = p
|
|
}
|
|
} else {
|
|
post = damus_state.drafts.post
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self.focus = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
|
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
damus_state.drafts.post = NSMutableAttributedString(string : "")
|
|
}
|
|
}
|
|
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
|
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
|
showPrivateKeyWarning = false
|
|
}
|
|
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
|
|
self.send_post()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func get_searching_string(_ post: String) -> String? {
|
|
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else {
|
|
return nil
|
|
}
|
|
|
|
guard last_word.count >= 2 else {
|
|
return nil
|
|
}
|
|
|
|
guard last_word.first! == "@" else {
|
|
return nil
|
|
}
|
|
|
|
// don't include @npub... strings
|
|
guard last_word.count != 64 else {
|
|
return nil
|
|
}
|
|
|
|
return String(last_word.dropFirst())
|
|
}
|
|
|
|
struct PostView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
PostView(replying_to: nil, damus_state: test_damus_state())
|
|
}
|
|
}
|