feat: add bottom bar

This commit is contained in:
2024-11-22 14:09:20 +07:00
parent 8bf497bdc7
commit f448508b31
11 changed files with 258 additions and 108 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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" }

View File

@@ -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
View 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())
}
}

View File

@@ -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,42 +20,50 @@ 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()
// Initialize components .with_assets(Assets)
components::init(cx); .with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
.run(move |cx| {
// Initialize components
components::init(cx);
// Set custom theme // Set custom theme
let mut theme = Theme::from(ThemeColor::dark()); let mut theme = Theme::from(ThemeColor::dark());
// TODO: support light mode // TODO: support light mode
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);
cx.set_global(nostr);
cx.set_global(app_state);
// Set nostr client as global state // Set quit action
cx.set_global(nostr); cx.on_action(quit);
cx.set_global(app_state);
// Rerender // Rerender
cx.refresh(); cx.refresh();
// Set window size // Set window size
let bounds = Bounds::centered(None, size(px(860.0), px(650.0)), cx); let bounds = Bounds::centered(None, size(px(860.0), px(650.0)), cx);
cx.open_window( cx.open_window(
WindowOptions { WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)), window_bounds: Some(WindowBounds::Windowed(bounds)),
window_decorations: Some(WindowDecorations::Client), window_decorations: Some(WindowDecorations::Client),
titlebar: Some(TitlebarOptions { titlebar: Some(TitlebarOptions {
title: Some(SharedString::new_static("coop")), title: Some(SharedString::new_static("coop")),
appears_transparent: true, appears_transparent: true,
..Default::default()
}),
..Default::default() ..Default::default()
}), },
..Default::default() |cx| cx.new_view(AppView::new),
}, )
|cx| cx.new_view(AppView::new), .unwrap();
) });
.unwrap(); }
});
fn quit(_: &Quit, cx: &mut AppContext) {
cx.quit();
} }

View File

@@ -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)
} }

View 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())
}
}

View 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())
}
}

View File

@@ -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))
}
}

View File

@@ -1,3 +1,3 @@
pub mod app; pub mod app;
pub mod chatspace; pub mod chat_space;
pub mod setup; pub mod onboarding;

View File

@@ -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()