From de20b4b2193e1afd182f974a1385a9b616da0ad3 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 15 Jan 2025 14:33:15 +0700 Subject: [PATCH] wip: nip96 upload --- crates/app/src/constants.rs | 1 + crates/app/src/utils.rs | 13 +++ crates/app/src/views/chat/mod.rs | 145 +++++++++++++++++++++++-------- 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs index 185b09f..6c5abce 100644 --- a/crates/app/src/constants.rs +++ b/crates/app/src/constants.rs @@ -5,3 +5,4 @@ pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwrap"; pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps"; pub const METADATA_DELAY: u64 = 200; pub const IMAGE_SERVICE: &str = "https://wsrv.nl"; +pub const NIP96_SERVER: &str = "https://nostrcheck.me"; diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 898f7a6..6adf7ef 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -5,6 +5,19 @@ use std::{ hash::{DefaultHasher, Hash, Hasher}, }; +use crate::{constants::NIP96_SERVER, get_client}; + +pub async fn nip96_upload(file: Vec) -> anyhow::Result { + let client = get_client(); + let signer = client.signer().await?; + let server_url = Url::parse(NIP96_SERVER)?; + + let config: ServerConfig = nip96::get_server_config(server_url, None).await?; + let url = nip96::upload_data(&signer, &config, file, None, None).await?; + + Ok(url) +} + pub fn room_hash(tags: &Tags) -> u64 { let pubkeys: Vec = tags.public_keys().copied().collect(); let mut hasher = DefaultHasher::new(); diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index 229a7a6..cd756ae 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -1,18 +1,21 @@ use crate::{ get_client, states::chat::room::Room, - utils::{ago, compare}, + utils::{ago, compare, nip96_upload}, }; +use async_utility::task::spawn; use gpui::{ - div, list, px, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, - FocusableView, IntoElement, ListAlignment, ListState, Model, ParentElement, PathPromptOptions, - Pixels, Render, SharedString, Styled, View, ViewContext, VisualContext, WeakModel, WeakView, - WindowContext, + div, img, list, px, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, + FocusableView, InteractiveElement, IntoElement, ListAlignment, ListState, Model, ObjectFit, + ParentElement, PathPromptOptions, Pixels, Render, SharedString, StatefulInteractiveElement, + Styled, StyledImage, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, }; use itertools::Itertools; use message::Message; use nostr_sdk::prelude::*; +use smol::fs; use std::sync::Arc; +use tokio::sync::oneshot; use ui::{ button::{Button, ButtonVariants}, dock_area::{ @@ -21,6 +24,7 @@ use ui::{ }, input::{InputEvent, TextInput}, popup_menu::PopupMenu, + prelude::FluentBuilder, theme::{scale::ColorScaleStep, ActiveTheme}, v_flex, Icon, IconName, }; @@ -44,7 +48,11 @@ pub struct ChatPanel { room: Model, state: Model, list: ListState, + // New Message input: View, + // Media + attaches: Model>>, + is_uploading: bool, } impl ChatPanel { @@ -68,6 +76,7 @@ impl ChatPanel { .cleanable() }); + // List let state = cx.new_model(|_| State { count: 0, items: vec![], @@ -107,6 +116,8 @@ impl ChatPanel { }) .detach(); + let attaches = cx.new_model(|_| None); + Self { closeable: true, zoomable: true, @@ -115,10 +126,12 @@ impl ChatPanel { list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _| { div().into_any_element() }), + is_uploading: false, id, name, input, state, + attaches, } }) } @@ -291,6 +304,52 @@ impl ChatPanel { }) .detach(); } + + fn upload(&mut self, cx: &mut ViewContext) { + let attaches = self.attaches.clone(); + + let paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: false, + multiple: false, + }); + + cx.spawn(move |_, mut async_cx| async move { + match Flatten::flatten(paths.await.map_err(|e| e.into())) { + Ok(Some(mut paths)) => { + let path = paths.pop().unwrap(); + + if let Ok(file_data) = fs::read(path).await { + let (tx, rx) = oneshot::channel::(); + + spawn(async move { + if let Ok(url) = nip96_upload(file_data).await { + _ = tx.send(url); + } + }); + + if let Ok(url) = rx.await { + _ = async_cx.update_model(&attaches, |model, cx| { + if let Some(model) = model.as_mut() { + model.push(url); + } else { + *model = Some(vec![url]); + } + cx.notify(); + }); + } + } + } + Ok(None) => {} + Err(_) => {} + } + }) + .detach(); + } + + fn remove(&mut self, cx: &mut ViewContext) { + // TODO + } } impl Panel for ChatPanel { @@ -343,44 +402,56 @@ impl Render for ChatPanel { .child( div() .flex_shrink_0() - .w_full() - .h_12() .flex() - .items_center() - .gap_2() - .px_2() - .child( - Button::new("upload") - .icon(Icon::new(IconName::Upload)) - .ghost() - .on_click(|_, cx| { - let paths = cx.prompt_for_paths(PathPromptOptions { - files: true, - directories: false, - multiple: false, - }); + .flex_col() + .gap_1() + .when_some(self.attaches.read(cx).as_ref(), |this, attaches| { + this.flex() + .items_center() + .gap_1p5() + .px_2() + .children(attaches.iter().map(|url| { + let path: SharedString = url.to_string().into(); - cx.spawn(move |_async_cx| async move { - match Flatten::flatten(paths.await.map_err(|e| e.into())) { - Ok(Some(paths)) => { - // TODO: upload file to blossom server - println!("Paths: {:?}", paths) - } - Ok(None) => {} - Err(_) => {} - } - }) - .detach(); - }), - ) + div() + .id(path.clone()) + .child( + img(path) + .h_16() + .rounded(px(cx.theme().radius)) + .object_fit(ObjectFit::ScaleDown), + ) + .on_click(cx.listener(move |this, _, cx| { + this.remove(cx); + })) + })) + }) .child( div() - .flex_1() + .w_full() + .h_12() .flex() - .bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) - .rounded(px(cx.theme().radius)) + .items_center() + .gap_2() .px_2() - .child(self.input.clone()), + .child( + Button::new("upload") + .icon(Icon::new(IconName::Upload)) + .ghost() + .on_click(cx.listener(move |this, _, cx| { + this.upload(cx); + })) + .loading(self.is_uploading), + ) + .child( + div() + .flex_1() + .flex() + .bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) + .rounded(px(cx.theme().radius)) + .px_2() + .child(self.input.clone()), + ), ), ) }