feat: add support for multi languages (#79)
* update backup settings description * add rust-i18n * translate * . * update translations * fix * update translate * .
This commit is contained in:
@@ -1,41 +1,43 @@
|
||||
[package]
|
||||
name = "coop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "coop"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ui = { path = "../ui" }
|
||||
identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
gpui.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
nostr-connect.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
nostr.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
itertools.workspace = true
|
||||
dirs.workspace = true
|
||||
rust-embed.workspace = true
|
||||
log.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
oneshot.workspace = true
|
||||
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
[package]
|
||||
name = "coop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "coop"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ui = { path = "../ui" }
|
||||
identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
rust-i18n.workspace = true
|
||||
i18n.workspace = true
|
||||
gpui.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
nostr-connect.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
nostr.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
itertools.workspace = true
|
||||
dirs.workspace = true
|
||||
rust-embed.workspace = true
|
||||
log.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
oneshot.workspace = true
|
||||
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
|
||||
@@ -8,8 +8,9 @@ use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
||||
Render, Styled, Subscription, Task, Window,
|
||||
Render, SharedString, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -54,6 +55,10 @@ pub enum ModalKind {
|
||||
SetupRelay,
|
||||
}
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = story, no_json)]
|
||||
pub struct SelectLocale(SharedString);
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = modal, no_json)]
|
||||
pub struct ToggleModal {
|
||||
@@ -91,17 +96,14 @@ impl ChatSpace {
|
||||
|_this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_keys() {
|
||||
window.open_modal(cx, |this, _window, cx| {
|
||||
const DESCRIPTION: &str =
|
||||
"Allow Coop to read the client keys stored in Keychain to continue";
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text("Create New Keys")
|
||||
.ok_text("Allow"),
|
||||
.cancel_text(t!("chatspace.create_new_keys"))
|
||||
.ok_text(t!("common.allow")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -119,9 +121,13 @@ impl ChatSpace {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Warning"),
|
||||
.child(SharedString::new(t!("chatspace.warning"))),
|
||||
)
|
||||
.child(div().line_height(relative(1.4)).child(DESCRIPTION)),
|
||||
.child(div().line_height(relative(1.4)).child(
|
||||
SharedString::new(t!(
|
||||
"chatspace.allow_keychain_access"
|
||||
)),
|
||||
)),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
@@ -182,7 +188,7 @@ impl ChatSpace {
|
||||
});
|
||||
} else {
|
||||
window.push_notification(
|
||||
"Failed to open room. Please try again later.",
|
||||
SharedString::new(t!("chatspace.failed_to_open_room")),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -264,7 +270,7 @@ impl ChatSpace {
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.title("Preferences")
|
||||
.title(SharedString::new(t!("chatspace.preferences_title")))
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(settings.clone())
|
||||
});
|
||||
@@ -349,7 +355,7 @@ impl Render for ChatSpace {
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("appearance")
|
||||
.tooltip("Change the app's appearance")
|
||||
.tooltip(t!("chatspace.appearance_tooltip"))
|
||||
.small()
|
||||
.ghost()
|
||||
.map(|this| {
|
||||
@@ -365,7 +371,7 @@ impl Render for ChatSpace {
|
||||
)
|
||||
.child(
|
||||
Button::new("preferences")
|
||||
.tooltip("Open Preferences")
|
||||
.tooltip(t!("chatspace.preferences_tooltip"))
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Settings)
|
||||
@@ -375,7 +381,7 @@ impl Render for ChatSpace {
|
||||
)
|
||||
.child(
|
||||
Button::new("logout")
|
||||
.tooltip("Log Out")
|
||||
.tooltip(t!("common.logout"))
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Logout)
|
||||
|
||||
@@ -23,6 +23,8 @@ pub(crate) mod asset;
|
||||
pub(crate) mod chatspace;
|
||||
pub(crate) mod views;
|
||||
|
||||
i18n::init!();
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui::{
|
||||
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
||||
Styled, StyledImage, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -73,10 +74,7 @@ impl Chat {
|
||||
|
||||
let messages = cx.new(|_| {
|
||||
let message = Message::builder()
|
||||
.content(
|
||||
"This conversation is private. Only members can see each other's messages."
|
||||
.into(),
|
||||
)
|
||||
.content(t!("chat.private_conversation_notice").into())
|
||||
.build_rc()
|
||||
.unwrap();
|
||||
|
||||
@@ -85,7 +83,7 @@ impl Chat {
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Message...")
|
||||
.placeholder(t!("chat.placeholder"))
|
||||
.multi_line()
|
||||
.prevent_new_line_on_enter()
|
||||
.rows(1)
|
||||
@@ -103,7 +101,10 @@ impl Chat {
|
||||
move |this: &mut Self, input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
if input.read(cx).value().trim().is_empty() {
|
||||
window.push_notification("Cannot send an empty message", cx);
|
||||
window.push_notification(
|
||||
Notification::new(t!("chat.empty_message_error")),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.send_message(window, cx);
|
||||
}
|
||||
@@ -498,7 +499,7 @@ impl Chat {
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Replying to:")
|
||||
.child(SharedString::new(t!("chat.replying_to_label")))
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
@@ -687,12 +688,12 @@ impl Chat {
|
||||
.text_xs()
|
||||
.italic()
|
||||
.child(Icon::new(IconName::Info).small())
|
||||
.child("Failed to send message. Click to see details.")
|
||||
.child(SharedString::new(t!("chat.send_fail")))
|
||||
.on_click(move |_, window, cx| {
|
||||
let errors = errors.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.title("Error Logs")
|
||||
this.title(SharedString::new(t!("chat.logs_title")))
|
||||
.child(message_errors(errors.clone(), cx))
|
||||
});
|
||||
}),
|
||||
@@ -705,7 +706,7 @@ impl Chat {
|
||||
vec![
|
||||
Button::new("reply")
|
||||
.icon(IconName::Reply)
|
||||
.tooltip("Reply")
|
||||
.tooltip(t!("chat.reply_button"))
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
@@ -716,7 +717,7 @@ impl Chat {
|
||||
}),
|
||||
Button::new("copy")
|
||||
.icon(IconName::Copy)
|
||||
.tooltip("Copy Message")
|
||||
.tooltip(t!("chat.copy_message_button"))
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
@@ -779,12 +780,12 @@ impl Panel for Chat {
|
||||
|
||||
let button = Button::new("subject")
|
||||
.icon(IconName::EditFill)
|
||||
.tooltip("Change Subject")
|
||||
.tooltip(t!("chat.change_subject_button"))
|
||||
.on_click(move |_, window, cx| {
|
||||
let subject = subject::init(id, subject.clone(), window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
this.title("Change the subject of the conversation")
|
||||
this.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||
.child(subject.clone())
|
||||
});
|
||||
});
|
||||
@@ -896,7 +897,7 @@ fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
||||
.items_baseline()
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Send to:")
|
||||
.child(SharedString::new(t!("chat.send_to_label")))
|
||||
.child(error.profile.render_name()),
|
||||
)
|
||||
.child(error.message)
|
||||
|
||||
@@ -13,16 +13,19 @@ use gpui::{
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
notification::Notification,
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
@@ -72,10 +75,10 @@ pub struct Compose {
|
||||
impl Compose {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
||||
let user_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("npub or nprofile..."));
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_npub")));
|
||||
|
||||
let title_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("Family...(Optional)"));
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_title")));
|
||||
|
||||
let error_message = cx.new(|_| None);
|
||||
let mut subscriptions = smallvec![];
|
||||
@@ -113,10 +116,7 @@ impl Compose {
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).title("Contacts"),
|
||||
cx,
|
||||
);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -139,7 +139,7 @@ impl Compose {
|
||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||
|
||||
if public_keys.is_empty() {
|
||||
self.set_error(Some("You need to add at least 1 receiver".into()), cx);
|
||||
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,28 +171,30 @@ impl Compose {
|
||||
Ok(room)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| match event.await {
|
||||
Ok(room) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match event.await {
|
||||
Ok(room) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| room), cx);
|
||||
});
|
||||
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| room), cx);
|
||||
});
|
||||
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(Some(e.to_string().into()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(Some(e.to_string().into()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -211,6 +213,11 @@ impl Compose {
|
||||
{
|
||||
self.contacts.insert(0, cx.new(|_| contact));
|
||||
cx.notify();
|
||||
} else {
|
||||
self.set_error(
|
||||
Some(t!("compose.contact_existed", name = contact.profile.name()).into()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +266,7 @@ impl Compose {
|
||||
|
||||
Ok(contact)
|
||||
} else {
|
||||
Err(anyhow!("Profile not found"))
|
||||
Err(anyhow!(t!("common.not_found")))
|
||||
}
|
||||
})
|
||||
} else if content.starts_with("nprofile1") {
|
||||
@@ -267,7 +274,7 @@ impl Compose {
|
||||
.map(|nip19| nip19.public_key)
|
||||
.ok()
|
||||
else {
|
||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -285,7 +292,7 @@ impl Compose {
|
||||
})
|
||||
} else {
|
||||
let Ok(public_key) = PublicKey::parse(&content) else {
|
||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -328,7 +335,7 @@ impl Compose {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: Option<SharedString>, cx: &mut Context<Self>) {
|
||||
fn set_error(&mut self, error: impl Into<Option<SharedString>>, cx: &mut Context<Self>) {
|
||||
if self.adding {
|
||||
self.set_adding(false, cx);
|
||||
}
|
||||
@@ -340,7 +347,7 @@ impl Compose {
|
||||
|
||||
// Update error message
|
||||
self.error_message.update(cx, |this, cx| {
|
||||
*this = error;
|
||||
*this = error.into();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
@@ -418,13 +425,12 @@ impl Compose {
|
||||
|
||||
impl Render for Compose {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const DESCRIPTION: &str =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
|
||||
let label: SharedString = if self.contacts.len() > 1 {
|
||||
"Create Group DM".into()
|
||||
let label = if self.submitting {
|
||||
t!("compose.creating_dm_button")
|
||||
} else if self.contacts.len() > 1 {
|
||||
t!("compose.create_group_dm_button")
|
||||
} else {
|
||||
"Create DM".into()
|
||||
t!("compose.create_dm_button")
|
||||
};
|
||||
|
||||
div()
|
||||
@@ -436,7 +442,7 @@ impl Render for Compose {
|
||||
.px_3()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(DESCRIPTION),
|
||||
.child(SharedString::new(t!("compose.description"))),
|
||||
)
|
||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||
this.child(div().px_3().text_xs().text_color(red()).child(msg.clone()))
|
||||
@@ -450,7 +456,12 @@ impl Render for Compose {
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().text_sm().font_semibold().child("Subject:"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("compose.subject_label"))),
|
||||
)
|
||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||
),
|
||||
)
|
||||
@@ -466,7 +477,12 @@ impl Render for Compose {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(div().text_sm().font_semibold().child("To:"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("compose.to_label"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -505,13 +521,16 @@ impl Render for Compose {
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.child("No contacts"),
|
||||
.child(SharedString::new(t!(
|
||||
"compose.no_contacts_message"
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Your recently contacts will appear here."),
|
||||
div().text_xs().text_color(cx.theme().text_muted).child(
|
||||
SharedString::new(t!(
|
||||
"compose.no_contacts_description"
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ use gpui::{
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
@@ -40,17 +41,20 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||
let name_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
||||
});
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.placeholder("A short introduce about you.")
|
||||
.placeholder(SharedString::new(t!("profile.placeholder_bio")))
|
||||
});
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||
|
||||
Self {
|
||||
name_input,
|
||||
avatar_input,
|
||||
@@ -93,7 +97,7 @@ impl NewAccount {
|
||||
.on_cancel(move |_, window, cx| {
|
||||
view_cancel
|
||||
.update(cx, |_this, cx| {
|
||||
window.push_notification("Password is invalid", cx)
|
||||
window.push_notification(t!("new_account.password_invalid"), cx)
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
@@ -121,7 +125,7 @@ impl NewAccount {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Set password to encrypt your key *")
|
||||
.child(SharedString::new(t!("new_account.set_password_prompt")))
|
||||
.child(TextInput::new(&pwd_input).small()),
|
||||
)
|
||||
});
|
||||
@@ -258,7 +262,7 @@ impl Render for NewAccount {
|
||||
.text_lg()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child("Create New Account"),
|
||||
.child(SharedString::new(t!("new_account.title"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -294,7 +298,7 @@ impl Render for NewAccount {
|
||||
})
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.label("Set Profile Picture")
|
||||
.label(t!("profile.set_profile_picture"))
|
||||
.icon(Icon::new(IconName::Plus))
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -311,7 +315,7 @@ impl Render for NewAccount {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Name *:")
|
||||
.child(SharedString::new(t!("profile.label_name")))
|
||||
.child(TextInput::new(&self.name_input).small()),
|
||||
)
|
||||
.child(
|
||||
@@ -320,7 +324,7 @@ impl Render for NewAccount {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Bio:")
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.child(TextInput::new(&self.bio_input).small()),
|
||||
)
|
||||
.child(
|
||||
@@ -332,7 +336,7 @@ impl Render for NewAccount {
|
||||
)
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label("Continue")
|
||||
.label(SharedString::new(t!("common.continue")))
|
||||
.primary()
|
||||
.loading(self.is_submitting)
|
||||
.disabled(self.is_submitting || self.is_uploading)
|
||||
|
||||
@@ -1,290 +1,294 @@
|
||||
use anyhow::anyhow;
|
||||
use common::profile::RenderProfile;
|
||||
use global::constants::ACCOUNT_D;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Onboarding {
|
||||
name: SharedString,
|
||||
local_account: Entity<Option<Profile>>,
|
||||
loading: bool,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self::view(window, cx))
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let local_account = cx.new(|_| None);
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let database = shared_state().client().database();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_D)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
||||
let public_key = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.copied()
|
||||
.collect_vec()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
|
||||
Ok(profile)
|
||||
} else {
|
||||
Err(anyhow!("Not found"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(profile) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.local_account.update(cx, |this, cx| {
|
||||
*this = Some(profile);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
local_account,
|
||||
name: "Onboarding".into(),
|
||||
loading: false,
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Onboarding {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Onboarding {}
|
||||
|
||||
impl Focusable for Onboarding {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const SUBTITLE: &str = "Secure Communication on Nostr.";
|
||||
|
||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
|
||||
div()
|
||||
.py_4()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.mb_10()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_16()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(TITLE),
|
||||
)
|
||||
.child(div().text_color(cx.theme().text_muted).child(SUBTITLE)),
|
||||
),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(profile) = self.local_account.read(cx).as_ref() {
|
||||
this.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("account")
|
||||
.mb_3()
|
||||
.h_10()
|
||||
.w_72()
|
||||
.bg(cx.theme().element_background)
|
||||
.text_color(cx.theme().element_foreground)
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.map(|this| {
|
||||
if self.loading {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Indicator::new().small()),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child("Continue as")
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(
|
||||
Avatar::new(
|
||||
profile.render_avatar(proxy),
|
||||
)
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.child(profile.render_name()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_loading(true, cx);
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("auto_login")
|
||||
.label("Automatically log in next time")
|
||||
.checked(auto_login)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.auto_login = !this.settings.auto_login;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_4().right_4().child(
|
||||
Button::new("unload")
|
||||
.icon(IconName::Logout)
|
||||
.label("Logout")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("continue_btn")
|
||||
.icon(Icon::new(IconName::ArrowRight))
|
||||
.label("Start Messaging")
|
||||
.primary()
|
||||
.reverse()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::new_account(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label("Already have an account? Log in.")
|
||||
.ghost()
|
||||
.underline()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
use anyhow::anyhow;
|
||||
use common::profile::RenderProfile;
|
||||
use global::constants::ACCOUNT_D;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Onboarding {
|
||||
name: SharedString,
|
||||
local_account: Entity<Option<Profile>>,
|
||||
loading: bool,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self::view(window, cx))
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let local_account = cx.new(|_| None);
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let database = shared_state().client().database();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_D)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
||||
let public_key = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.copied()
|
||||
.collect_vec()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
|
||||
Ok(profile)
|
||||
} else {
|
||||
Err(anyhow!("Not found"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(profile) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.local_account.update(cx, |this, cx| {
|
||||
*this = Some(profile);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
local_account,
|
||||
name: "Onboarding".into(),
|
||||
loading: false,
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Onboarding {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Onboarding {}
|
||||
|
||||
impl Focusable for Onboarding {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
|
||||
div()
|
||||
.py_4()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.mb_10()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_16()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("welcome.title"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("welcome.subtitle"))),
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(profile) = self.local_account.read(cx).as_ref() {
|
||||
this.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("account")
|
||||
.mb_3()
|
||||
.h_10()
|
||||
.w_72()
|
||||
.bg(cx.theme().element_background)
|
||||
.text_color(cx.theme().element_foreground)
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.map(|this| {
|
||||
if self.loading {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Indicator::new().small()),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(SharedString::new(t!(
|
||||
"onboarding.choose_account"
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(
|
||||
Avatar::new(
|
||||
profile.render_avatar(proxy),
|
||||
)
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.child(profile.render_name()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_loading(true, cx);
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("auto_login")
|
||||
.label(SharedString::new(t!("onboarding.auto_login")))
|
||||
.checked(auto_login)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.auto_login = !this.settings.auto_login;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_4().right_4().child(
|
||||
Button::new("unload")
|
||||
.icon(IconName::Logout)
|
||||
.label(SharedString::new(t!("common.logout")))
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("continue_btn")
|
||||
.icon(Icon::new(IconName::ArrowRight))
|
||||
.label(SharedString::new(t!("onboarding.start_messaging")))
|
||||
.primary()
|
||||
.reverse()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::new_account(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label(SharedString::new(t!("onboarding.already_have_account")))
|
||||
.ghost()
|
||||
.underline()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use gpui::http_client::Url;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, rems, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, StatefulInteractiveElement, Styled, Window,
|
||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
@@ -50,9 +51,10 @@ impl Preferences {
|
||||
fn open_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let profile = profile::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let title = SharedString::new(t!("preferences.modal_profile_title"));
|
||||
modal
|
||||
.title("Profile")
|
||||
.title(title)
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(profile.clone())
|
||||
});
|
||||
@@ -61,9 +63,10 @@ impl Preferences {
|
||||
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let relays = relays::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _, _| {
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
let title = SharedString::new(t!("preferences.modal_relays_title"));
|
||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.title("Edit your Messaging Relays")
|
||||
.title(title)
|
||||
.child(relays.clone())
|
||||
});
|
||||
}
|
||||
@@ -71,15 +74,6 @@ impl Preferences {
|
||||
|
||||
impl Render for Preferences {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const MEDIA_DESCRIPTION: &str = "Coop only supports NIP-96 media servers for now. If you're not sure about it, please keep the default value.";
|
||||
const BACKUP_DESCRIPTION: &str =
|
||||
"When a user sends a message, Coop won't back it up to the user's messaging relays";
|
||||
const TRUSTED_DESCRIPTION: &str = "Show trusted requests by default";
|
||||
const HIDE_AVATAR_DESCRIPTION: &str =
|
||||
"Unload all avatar pictures to improve performance and reduce memory usage";
|
||||
const PROXY_DESCRIPTION: &str =
|
||||
"Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)";
|
||||
|
||||
let input_state = self.media_input.downgrade();
|
||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
||||
|
||||
@@ -101,7 +95,7 @@ impl Render for Preferences {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
.child("Account"),
|
||||
.child(SharedString::new(t!("preferences.account_header"))),
|
||||
)
|
||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||
this.child(
|
||||
@@ -137,7 +131,9 @@ impl Render for Preferences {
|
||||
.line_height(relative(1.3))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("See your profile"),
|
||||
.child(SharedString::new(t!(
|
||||
"preferences.see_your_profile"
|
||||
))),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -169,7 +165,7 @@ impl Render for Preferences {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
.child("Media Server"),
|
||||
.child(SharedString::new(t!("preferences.media_server_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -186,7 +182,10 @@ impl Render for Preferences {
|
||||
if let Some(input) = input_state.upgrade() {
|
||||
let value = input.read(cx).value();
|
||||
let Ok(url) = Url::parse(value) else {
|
||||
window.push_notification("URL is not valid", cx);
|
||||
window.push_notification(
|
||||
t!("preferences.url_not_valid"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -202,7 +201,7 @@ impl Render for Preferences {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(MEDIA_DESCRIPTION),
|
||||
.child(SharedString::new(t!("preferences.media_description"))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -218,39 +217,22 @@ impl Render for Preferences {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
.child("Messages"),
|
||||
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("backup_messages")
|
||||
.label("Backup messages")
|
||||
.description(BACKUP_DESCRIPTION)
|
||||
.checked(settings.backup_messages)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.backup_messages =
|
||||
!this.settings.backup_messages;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Switch::new("only_show_trusted")
|
||||
.label("Only trusted")
|
||||
.description(TRUSTED_DESCRIPTION)
|
||||
.checked(settings.only_show_trusted)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.only_show_trusted =
|
||||
!this.settings.only_show_trusted;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
),
|
||||
div().flex().flex_col().gap_2().child(
|
||||
Switch::new("backup_messages")
|
||||
.label(t!("preferences.backup_messages_label"))
|
||||
.description(t!("preferences.backup_description"))
|
||||
.checked(settings.backup_messages)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.backup_messages =
|
||||
!this.settings.backup_messages;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -266,7 +248,7 @@ impl Render for Preferences {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
.child("Display"),
|
||||
.child(SharedString::new(t!("preferences.display_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -275,8 +257,8 @@ impl Render for Preferences {
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("hide_user_avatars")
|
||||
.label("Hide user avatars")
|
||||
.description(HIDE_AVATAR_DESCRIPTION)
|
||||
.label(t!("preferences.hide_avatars_label"))
|
||||
.description(t!("preferences.hide_avatar_description"))
|
||||
.checked(settings.hide_user_avatars)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
@@ -288,8 +270,8 @@ impl Render for Preferences {
|
||||
)
|
||||
.child(
|
||||
Switch::new("proxy_user_avatars")
|
||||
.label("Proxy user avatars")
|
||||
.description(PROXY_DESCRIPTION)
|
||||
.label(t!("preferences.proxy_avatars_label"))
|
||||
.description(t!("preferences.proxy_description"))
|
||||
.checked(settings.proxy_user_avatars)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
|
||||
@@ -6,8 +6,9 @@ use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||
PathPromptOptions, Render, Styled, Task, Window,
|
||||
PathPromptOptions, Render, SharedString, Styled, Task, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
@@ -32,7 +33,8 @@ pub struct Profile {
|
||||
|
||||
impl Profile {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||
let name_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||
let website_input =
|
||||
@@ -40,7 +42,7 @@ impl Profile {
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.placeholder("A short introduce about you.")
|
||||
.placeholder(t!("profile.placeholder_bio"))
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
@@ -124,10 +126,8 @@ impl Profile {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
if let Ok(url) =
|
||||
nip96_upload(shared_state().client(), &nip96_server, file_data)
|
||||
.await
|
||||
{
|
||||
let client = shared_state().client();
|
||||
if let Ok(url) = nip96_upload(client, &nip96_server, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
@@ -197,17 +197,23 @@ impl Profile {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
window.push_notification("Your profile has been updated successfully", cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -263,7 +269,7 @@ impl Render for Profile {
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.icon(IconName::Upload)
|
||||
.label("Change")
|
||||
.label(t!("common.change"))
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.is_loading || self.is_submitting)
|
||||
@@ -279,7 +285,7 @@ impl Render for Profile {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Name:")
|
||||
.child(SharedString::new(t!("profile.label_name")))
|
||||
.child(TextInput::new(&self.name_input).small()),
|
||||
)
|
||||
.child(
|
||||
@@ -288,7 +294,7 @@ impl Render for Profile {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Website:")
|
||||
.child(SharedString::new(t!("profile.label_website")))
|
||||
.child(TextInput::new(&self.website_input).small()),
|
||||
)
|
||||
.child(
|
||||
@@ -297,13 +303,13 @@ impl Render for Profile {
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Bio:")
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.child(TextInput::new(&self.bio_input).small()),
|
||||
)
|
||||
.child(
|
||||
div().py_3().child(
|
||||
Button::new("submit")
|
||||
.label("Update")
|
||||
.label(SharedString::new(t!("common.update")))
|
||||
.primary()
|
||||
.disabled(self.is_loading || self.is_submitting)
|
||||
.loading(self.is_submitting)
|
||||
|
||||
@@ -4,8 +4,10 @@ use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
|
||||
UniformList, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
@@ -14,8 +16,6 @@ use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
const MIN_HEIGHT: f32 = 200.0;
|
||||
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
|
||||
const HELP_TEXT: &str = "Please add some relays.";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
||||
Relays::new(window, cx)
|
||||
@@ -270,7 +270,7 @@ impl Relays {
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.text_align(TextAlign::Center)
|
||||
.child(HELP_TEXT)
|
||||
.child(SharedString::new(t!("relays.add_some_relays")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ impl Render for Relays {
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(MESSAGE),
|
||||
.child(SharedString::new(t!("relays.description"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -312,7 +312,7 @@ impl Render for Relays {
|
||||
.child(
|
||||
Button::new("add_relay_btn")
|
||||
.icon(IconName::Plus)
|
||||
.label("Add")
|
||||
.label(t!("common.add"))
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded_md()
|
||||
@@ -334,7 +334,7 @@ impl Render for Relays {
|
||||
)
|
||||
.child(
|
||||
Button::new("submti")
|
||||
.label("Update")
|
||||
.label(t!("common.update"))
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_loading)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,131 +1,131 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||
Startup::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Startup {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
name: "Welcome".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Startup {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
"Startup".into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Startup {}
|
||||
|
||||
impl Focusable for Startup {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Startup {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let identity = Identity::global(cx);
|
||||
let logging_in = identity.read(cx).logging_in();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_12()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.when(logging_in, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text)
|
||||
.child("Auto login in progress"),
|
||||
)
|
||||
})
|
||||
.child(Indicator::new().small()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().bottom_3().right_3().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Stuck?"),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset")
|
||||
.label("Reset")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
// Restart application
|
||||
cx.restart(None);
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||
Startup::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Startup {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
name: "Startup".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Startup {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Startup {}
|
||||
|
||||
impl Focusable for Startup {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Startup {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let identity = Identity::global(cx);
|
||||
let logging_in = identity.read(cx).logging_in();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_12()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.when(logging_in, |this| {
|
||||
this.child(
|
||||
div().text_sm().text_color(cx.theme().text).child(
|
||||
SharedString::new(t!("startup.auto_login_in_progress")),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(Indicator::new().small()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().bottom_3().right_3().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("startup.stuck"))),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset")
|
||||
.label(SharedString::new(t!("startup.reset")))
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
// Restart application
|
||||
cx.restart(None);
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,103 @@
|
||||
use chats::ChatRegistry;
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Subject> {
|
||||
Subject::new(id, subject, window, cx)
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
id: u64,
|
||||
input: Entity<InputState>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let input = cx.new(|cx| {
|
||||
let mut this = InputState::new(window, cx).placeholder("Exciting Project...");
|
||||
if let Some(text) = subject.clone() {
|
||||
this.set_value(text, window, cx);
|
||||
}
|
||||
this
|
||||
});
|
||||
|
||||
cx.new(|cx| Self {
|
||||
id,
|
||||
input,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
let subject = self.input.read(cx).value().clone();
|
||||
|
||||
if let Some(room) = registry.room(&self.id, cx) {
|
||||
room.update(cx, |this, cx| {
|
||||
this.subject = Some(subject);
|
||||
cx.notify();
|
||||
});
|
||||
window.close_modal(cx);
|
||||
} else {
|
||||
window.push_notification("Room not found", cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const HELP_TEXT: &str = "Subject will be updated when you send a message.";
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Subject:"),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(HELP_TEXT),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label("Change")
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
||||
)
|
||||
}
|
||||
}
|
||||
use chats::ChatRegistry;
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Subject> {
|
||||
Subject::new(id, subject, window, cx)
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
id: u64,
|
||||
input: Entity<InputState>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let input = cx.new(|cx| {
|
||||
let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder"));
|
||||
if let Some(text) = subject.clone() {
|
||||
this.set_value(text, window, cx);
|
||||
}
|
||||
this
|
||||
});
|
||||
|
||||
cx.new(|cx| Self {
|
||||
id,
|
||||
input,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
let subject = self.input.read(cx).value().clone();
|
||||
|
||||
if let Some(room) = registry.room(&self.id, cx) {
|
||||
room.update(cx, |this, cx| {
|
||||
this.subject = Some(subject);
|
||||
cx.notify();
|
||||
});
|
||||
window.close_modal(cx);
|
||||
} else {
|
||||
window.push_notification(SharedString::new(t!("subject.room_not_found")), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("subject.title"))),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::new(t!("subject.help_text"))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label(t!("common.change"))
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ impl Render for Welcome {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child("coop on nostr.")
|
||||
.child("coop on nostr")
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
.text_sm(),
|
||||
|
||||
Reference in New Issue
Block a user