feat: add bottom bar
This commit is contained in:
@@ -9,6 +9,7 @@ coop = { path = "crates/*" }
|
|||||||
# UI
|
# UI
|
||||||
gpui = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform-window" }
|
gpui = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform-window" }
|
||||||
components = { package = "ui", git = "https://github.com/longbridgeapp/gpui-component" }
|
components = { package = "ui", git = "https://github.com/longbridgeapp/gpui-component" }
|
||||||
|
reqwest_client = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform-window" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ tokio.workspace = true
|
|||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
keyring-search.workspace = true
|
keyring-search.workspace = true
|
||||||
keyring.workspace = true
|
keyring.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
client = { version = "0.1.0", path = "../client" }
|
client = { version = "0.1.0", path = "../client" }
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
|
|||||||
27
crates/ui/src/asset.rs
Normal file
27
crates/ui/src/asset.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use gpui::*;
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "../../assets"]
|
||||||
|
pub struct Assets;
|
||||||
|
|
||||||
|
impl AssetSource for Assets {
|
||||||
|
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||||
|
Self::get(path)
|
||||||
|
.map(|f| Some(f.data))
|
||||||
|
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||||
|
Ok(Self::iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
if p.starts_with(path) {
|
||||||
|
Some(p.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,17 @@
|
|||||||
|
use asset::Assets;
|
||||||
use client::NostrClient;
|
use client::NostrClient;
|
||||||
use components::theme::{Theme, ThemeColor, ThemeMode};
|
use components::theme::{Theme, ThemeColor, ThemeMode};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use http_client::anyhow;
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
use views::app::AppView;
|
use views::app::AppView;
|
||||||
|
|
||||||
|
pub mod asset;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
|
||||||
#[derive(rust_embed::RustEmbed)]
|
actions!(main_menu, [Quit]);
|
||||||
#[folder = "../../assets"]
|
|
||||||
struct Assets;
|
|
||||||
|
|
||||||
impl AssetSource for Assets {
|
|
||||||
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
|
||||||
Self::get(path)
|
|
||||||
.map(|f| Some(f.data))
|
|
||||||
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
|
||||||
Ok(Self::iter()
|
|
||||||
.filter_map(|p| {
|
|
||||||
if p.starts_with(path) {
|
|
||||||
Some(p.into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -40,7 +20,10 @@ async fn main() {
|
|||||||
// Initializ app state
|
// Initializ app state
|
||||||
let app_state = AppState::new();
|
let app_state = AppState::new();
|
||||||
|
|
||||||
App::new().with_assets(Assets).run(move |cx| {
|
App::new()
|
||||||
|
.with_assets(Assets)
|
||||||
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
|
||||||
|
.run(move |cx| {
|
||||||
// Initialize components
|
// Initialize components
|
||||||
components::init(cx);
|
components::init(cx);
|
||||||
|
|
||||||
@@ -50,13 +33,14 @@ async fn main() {
|
|||||||
theme.mode = ThemeMode::Dark;
|
theme.mode = ThemeMode::Dark;
|
||||||
// TODO: adjust color set
|
// TODO: adjust color set
|
||||||
|
|
||||||
// Set global theme
|
// Set app state
|
||||||
cx.set_global(theme);
|
cx.set_global(theme);
|
||||||
|
|
||||||
// Set nostr client as global state
|
|
||||||
cx.set_global(nostr);
|
cx.set_global(nostr);
|
||||||
cx.set_global(app_state);
|
cx.set_global(app_state);
|
||||||
|
|
||||||
|
// Set quit action
|
||||||
|
cx.on_action(quit);
|
||||||
|
|
||||||
// Rerender
|
// Rerender
|
||||||
cx.refresh();
|
cx.refresh();
|
||||||
|
|
||||||
@@ -79,3 +63,7 @@ async fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quit(_: &Quit, cx: &mut AppContext) {
|
||||||
|
cx.quit();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,23 @@
|
|||||||
use components::theme::ActiveTheme;
|
use components::theme::ActiveTheme;
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
|
|
||||||
|
use super::{chat_space::ChatSpace, onboarding::Onboarding};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
use super::{chatspace::ChatSpaceView, setup::SetupView};
|
|
||||||
|
|
||||||
pub struct AppView {
|
pub struct AppView {
|
||||||
onboarding: Model<Option<AnyView>>, // TODO: create onboarding view
|
onboarding: View<Onboarding>,
|
||||||
setup: View<SetupView>,
|
chat_space: View<ChatSpace>,
|
||||||
chat_space: View<ChatSpaceView>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppView {
|
impl AppView {
|
||||||
pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView {
|
pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView {
|
||||||
// Onboarding model
|
// Onboarding model
|
||||||
let onboarding = cx.new_model(|_| None);
|
let onboarding = cx.new_view(Onboarding::new);
|
||||||
// Setup view
|
|
||||||
let setup = cx.new_view(SetupView::new);
|
|
||||||
// Chat Space view
|
// Chat Space view
|
||||||
let chat_space = cx.new_view(ChatSpaceView::new);
|
let chat_space = cx.new_view(ChatSpace::new);
|
||||||
|
|
||||||
cx.foreground_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
// TODO: create onboarding view for the first time open app
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
AppView {
|
AppView {
|
||||||
onboarding,
|
onboarding,
|
||||||
setup,
|
|
||||||
chat_space,
|
chat_space,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,18 +28,14 @@ impl Render for AppView {
|
|||||||
let mut content = div().size_full().flex().items_center().justify_center();
|
let mut content = div().size_full().flex().items_center().justify_center();
|
||||||
|
|
||||||
if cx.global::<AppState>().accounts.is_empty() {
|
if cx.global::<AppState>().accounts.is_empty() {
|
||||||
content = content.child(self.setup.clone())
|
content = content.child(self.onboarding.clone())
|
||||||
} else {
|
} else {
|
||||||
content = content.child(self.chat_space.clone())
|
content = content.child(self.chat_space.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(onboarding) = self.onboarding.read(cx).as_ref() {
|
|
||||||
content = content.child(onboarding.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.text_color(rgb(0xFFFFFF))
|
.text_color(cx.theme().foreground)
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(content)
|
.child(content)
|
||||||
}
|
}
|
||||||
|
|||||||
113
crates/ui/src/views/chat_space/bottom_bar.rs
Normal file
113
crates/ui/src/views/chat_space/bottom_bar.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use client::NostrClient;
|
||||||
|
use components::theme::ActiveTheme;
|
||||||
|
use gpui::*;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use prelude::FluentBuilder;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[derive(Clone, IntoElement)]
|
||||||
|
struct Account {
|
||||||
|
#[allow(dead_code)] // TODO: remove this
|
||||||
|
public_key: PublicKey,
|
||||||
|
metadata: Model<Option<Metadata>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
pub fn new(public_key: PublicKey, cx: &mut WindowContext) -> Self {
|
||||||
|
let client = cx.global::<NostrClient>().client;
|
||||||
|
|
||||||
|
let metadata = cx.new_model(|_| None);
|
||||||
|
let async_metadata = metadata.clone();
|
||||||
|
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
|
||||||
|
cx.foreground_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
match client
|
||||||
|
.fetch_metadata(public_key, Some(Duration::from_secs(2)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(metadata) => {
|
||||||
|
async_metadata
|
||||||
|
.update(&mut async_cx, |a, b| {
|
||||||
|
*a = Some(metadata);
|
||||||
|
b.notify()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
public_key,
|
||||||
|
metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for Account {
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
match self.metadata.read(cx) {
|
||||||
|
Some(metadata) => div()
|
||||||
|
.w_8()
|
||||||
|
.h_12()
|
||||||
|
.px_1()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.border_b_2()
|
||||||
|
.border_color(cx.theme().primary_active)
|
||||||
|
.when_some(metadata.picture.clone(), |parent, picture| {
|
||||||
|
parent.child(
|
||||||
|
img(picture)
|
||||||
|
.size_6()
|
||||||
|
.rounded_full()
|
||||||
|
.object_fit(ObjectFit::Cover),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
None => div(), // TODO: add fallback image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BottomBar {
|
||||||
|
accounts: Vec<Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BottomBar {
|
||||||
|
pub fn new(cx: &mut ViewContext<'_, Self>) -> BottomBar {
|
||||||
|
let state: Vec<PublicKey> = cx
|
||||||
|
.global::<AppState>()
|
||||||
|
.accounts
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let win_cx = cx.window_context();
|
||||||
|
|
||||||
|
let accounts = state
|
||||||
|
.into_iter()
|
||||||
|
.map(|pk| Account::new(pk, win_cx))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
BottomBar { accounts }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BottomBar {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.h_12()
|
||||||
|
.px_3()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_1()
|
||||||
|
.children(self.accounts.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
56
crates/ui/src/views/chat_space/mod.rs
Normal file
56
crates/ui/src/views/chat_space/mod.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use bottom_bar::BottomBar;
|
||||||
|
use components::{
|
||||||
|
resizable::{h_resizable, resizable_panel, ResizablePanelGroup},
|
||||||
|
theme::ActiveTheme,
|
||||||
|
};
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
pub mod bottom_bar;
|
||||||
|
|
||||||
|
pub struct ChatSpace {
|
||||||
|
layout: View<ResizablePanelGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatSpace {
|
||||||
|
pub fn new(cx: &mut ViewContext<'_, Self>) -> Self {
|
||||||
|
let bottom_bar = cx.new_view(BottomBar::new);
|
||||||
|
// TODO: add chat list view
|
||||||
|
|
||||||
|
let layout = cx.new_view(|cx| {
|
||||||
|
h_resizable(cx)
|
||||||
|
.child(
|
||||||
|
resizable_panel().size(px(260.)).content(move |cx| {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.w_full()
|
||||||
|
.child("Chat List"),
|
||||||
|
)
|
||||||
|
.child(bottom_bar.clone())
|
||||||
|
.into_any_element()
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
resizable_panel().content(|_| div().child("Content").into_any_element()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { layout }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ChatSpace {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div().relative().size_full().child(self.layout.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use gpui::*;
|
|
||||||
|
|
||||||
pub struct ChatSpaceView {
|
|
||||||
pub text: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChatSpaceView {
|
|
||||||
pub fn new(_cx: &mut ViewContext<'_, Self>) -> ChatSpaceView {
|
|
||||||
ChatSpaceView {
|
|
||||||
text: "chat".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ChatSpaceView {
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.size_full()
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.child(format!("Hello, {}!", &self.text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod chatspace;
|
pub mod chat_space;
|
||||||
pub mod setup;
|
pub mod onboarding;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ use nostr_sdk::prelude::*;
|
|||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub struct SetupView {
|
pub struct Onboarding {
|
||||||
input: View<TextInput>,
|
input: View<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SetupView {
|
impl Onboarding {
|
||||||
pub fn new(cx: &mut ViewContext<'_, Self>) -> SetupView {
|
pub fn new(cx: &mut ViewContext<'_, Self>) -> Self {
|
||||||
let input = cx.new_view(|cx| {
|
let input = cx.new_view(|cx| {
|
||||||
let mut input = TextInput::new(cx);
|
let mut input = TextInput::new(cx);
|
||||||
input.set_size(components::Size::Medium, cx);
|
input.set_size(components::Size::Medium, cx);
|
||||||
@@ -36,11 +36,11 @@ impl SetupView {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
SetupView { input }
|
Self { input }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for SetupView {
|
impl Render for Onboarding {
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.size_1_3()
|
.size_1_3()
|
||||||
Reference in New Issue
Block a user