chore: bump gpui-pre to 0.3.8 and clean up unused code

This commit is contained in:
2026-10-06 21:04:20 +07:00
parent 3ada21b728
commit 33785f183d
62 changed files with 162 additions and 3516 deletions
Generated
-31
View File
@@ -271,7 +271,6 @@ version = "1.0.2"
dependencies = [
"anyhow",
"gpui-pre",
"log",
"rust-embed",
]
@@ -1089,7 +1088,6 @@ dependencies = [
"common",
"gpui-pre",
"itertools 0.13.0",
"log",
"nostr-sdk",
"person",
"regex",
@@ -1259,10 +1257,8 @@ dependencies = [
"gpui-pre",
"instant",
"itertools 0.13.0",
"log",
"nostr",
"nostr-sdk",
"qrcode",
"regex",
]
@@ -1403,7 +1399,6 @@ dependencies = [
"assets",
"auto_update",
"chat",
"common",
"gpui-pre",
"gpui-pre-linux",
"gpui-pre-macos",
@@ -1427,9 +1422,7 @@ dependencies = [
name = "coop_web"
version = "1.0.2"
dependencies = [
"assets",
"chat",
"common",
"console_error_panic_hook",
"console_log",
"getrandom 0.2.17",
@@ -4524,12 +4517,9 @@ dependencies = [
"anyhow",
"chat",
"flume 0.11.1",
"futures",
"gpui-pre",
"log",
"nostr",
"nostr-sdk",
"serde_json",
"smallvec",
"state",
]
@@ -4560,10 +4550,8 @@ name = "nip4e"
version = "1.0.2"
dependencies = [
"anyhow",
"common",
"flume 0.11.1",
"gpui-pre",
"instant",
"log",
"nostr-sdk",
"person",
@@ -5863,15 +5851,6 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "qrcode"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
dependencies = [
"image",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -7094,7 +7073,6 @@ dependencies = [
"gpui-pre",
"gpui_tokio",
"instant",
"log",
"mime_guess",
"nostr",
"nostr-blossom",
@@ -7463,7 +7441,6 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"smallvec",
"tempfile",
]
@@ -7977,11 +7954,9 @@ dependencies = [
name = "ui"
version = "1.0.2"
dependencies = [
"common",
"gpui-base",
"gpui-pre",
"instant",
"log",
"serde",
"smallvec",
"theme",
@@ -9431,7 +9406,6 @@ version = "1.0.2"
dependencies = [
"anyhow",
"auto_update",
"browser-signer-proxy",
"chat",
"chat_ui",
"common",
@@ -9955,8 +9929,3 @@ dependencies = [
"syn 3.0.6",
"winnow 1.0.4",
]
[[patch.unused]]
name = "psm"
version = "0.1.32"
source = "git+https://github.com/rust-lang/stacker?branch=master#d3ac2df79a409c681329006e9b31c96ab9c5fcae"
+9 -16
View File
@@ -9,17 +9,16 @@ edition = "2024"
publish = false
[workspace.dependencies]
gpui = { package = "gpui-pre", version = "0.3.5" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" }
gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" }
gpui_web = { package = "gpui-pre-web", version = "0.3.5" }
gpui_util = { package = "gpui-pre-util", version = "0.3.5" }
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
gpui = { package = "gpui-pre", version = "0.3.8" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] }
gpui_linux = { package = "gpui-pre-linux", version = "0.3.8" }
gpui_windows = { package = "gpui-pre-windows", version = "0.3.8" }
gpui_macos = { package = "gpui-pre-macos", version = "0.3.8" }
gpui_web = { package = "gpui-pre-web", version = "0.3.8" }
gpui_util = { package = "gpui-pre-util", version = "0.3.8" }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
gpui_tokio = { path = "crates/gpui_tokio" }
gpui-base = "0.7.0"
gpui-base = "0.7.1"
# Nostr
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
@@ -48,7 +47,6 @@ chrono = { version = "0.4.38", features = ["wasmbind"] }
futures = "0.3"
itertools = "0.13.0"
log = "0.4"
oneshot = "0.1.10"
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
rust-embed = { version = "8.5", features = ["include-exclude"] }
serde = { version = "1.0", features = ["derive"] }
@@ -58,14 +56,9 @@ smallvec = "1.14.0"
smol = "2"
webbrowser = "1.0.4"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
errno = { version = "0.3.14", default-features = false }
instant = "0.1"
ureq = { version = "3", default-features = false, features = ["rustls", "platform-verifier", "json"] }
[patch.crates-io]
# Use stacker's psm version which may have better WASM support
psm = { git = "https://github.com/rust-lang/stacker", branch = "master" }
[profile.release]
strip = true
opt-level = "z"
-1
View File
@@ -7,5 +7,4 @@ publish.workspace = true
[dependencies]
gpui.workspace = true
anyhow.workspace = true
log.workspace = true
rust-embed.workspace = true
+1 -20
View File
@@ -1,5 +1,5 @@
use anyhow::Context;
use gpui::{App, AssetSource, Result, SharedString};
use gpui::{AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
@@ -30,22 +30,3 @@ impl AssetSource for Assets {
.collect())
}
}
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Assets should never return None");
embedded_fonts.push(font_bytes);
}
}
cx.text_system().add_fonts(embedded_fonts)
}
}
-20
View File
@@ -11,8 +11,6 @@ use crate::source::{AssetFilter, GiteaSource, asset_filter_for};
mod source;
pub use gpui_updater_core::UpdateStatus as AutoUpdateStatus;
const GITEA_API_BASE: &str = "https://git.reya.info/api/v1";
const GITEA_REPO_OWNER: &str = "reya";
const GITEA_REPO_NAME: &str = "coop";
@@ -73,29 +71,17 @@ pub struct AutoUpdater {
status: UpdateStatus,
/// The newer release found by the last successful check, if any.
available: Option<Release>,
/// Currently running app version.
pub version: Version,
/// The in-flight check or download, if any.
task: Option<Task<()>>,
}
impl AutoUpdater {
/// Whether auto-update is available for this installation.
pub fn is_available(cx: &App) -> bool {
cx.try_global::<GlobalAutoUpdater>().is_some()
}
/// Retrieve the global auto updater instance, if one was initialized.
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalAutoUpdater>()
.map(|global| global.0.clone())
}
/// Retrieve the global auto updater instance.
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAutoUpdater>().0.clone()
}
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAutoUpdater(state));
}
@@ -119,16 +105,10 @@ impl AutoUpdater {
engine,
status: UpdateStatus::Idle,
available: None,
version,
task: None,
}
}
/// Whether nothing is happening, so the UI can hide the status line.
pub fn idle(&self) -> bool {
matches!(self.status, UpdateStatus::Idle)
}
/// Whether the running version is the newest release, so the status line can be hidden.
pub fn up_to_date(&self) -> bool {
matches!(self.status, UpdateStatus::Idle | UpdateStatus::UpToDate)
+3 -77
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@@ -49,8 +49,6 @@ impl Message {
enum RequestMethod {
GetPublicKey,
SignEvent,
Nip04Encrypt,
Nip04Decrypt,
Nip44Encrypt,
Nip44Decrypt,
}
@@ -60,8 +58,6 @@ impl RequestMethod {
match self {
Self::GetPublicKey => "get_public_key",
Self::SignEvent => "sign_event",
Self::Nip04Encrypt => "nip04_encrypt",
Self::Nip04Decrypt => "nip04_decrypt",
Self::Nip44Encrypt => "nip44_encrypt",
Self::Nip44Decrypt => "nip44_decrypt",
}
@@ -205,36 +201,6 @@ impl Default for BrowserSignerProxyOptions {
}
}
impl BrowserSignerProxyOptions {
/// Sets the timeout duration.
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Sets the IP address.
pub const fn ip_addr(mut self, new_ip: IpAddr) -> Self {
self.addr = SocketAddr::new(new_ip, self.addr.port());
self
}
/// Sets the port number.
pub const fn port(mut self, new_port: u16) -> Self {
self.addr = SocketAddr::new(self.addr.ip(), new_port);
self
}
/// Sets a custom html page.
///
/// The page must include `/proxy.js` script (`<script src="/proxy.js"></script>`)
/// which will handle communication with the server and update the element
/// with id `nip07-proxy-status` with the status.
pub const fn custom_html_page(mut self, custom_html: &'static str) -> Self {
self.custom_html = custom_html;
self
}
}
impl BrowserSignerProxy {
/// Construct a new browser signer proxy
pub fn new(options: BrowserSignerProxyOptions) -> Self {
@@ -255,14 +221,8 @@ impl BrowserSignerProxy {
}
}
/// Indicates whether the server is currently running.
#[inline]
pub fn is_started(&self) -> bool {
self.inner.is_started.load(Ordering::SeqCst)
}
/// Checks if there is an open browser tab ready to respond to requests by
/// verifying the time since the last pending request.
/// Checks if there is an open browser tab ready to respond
/// to requests by verifying the time since the last pending request.
#[inline]
pub fn is_session_active(&self) -> bool {
current_time() - self.inner.state.last_pending_request.load(Ordering::SeqCst) < 2
@@ -419,20 +379,6 @@ impl BrowserSignerProxy {
Ok(event)
}
#[inline]
async fn _nip04_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip04Encrypt, serde_json::to_value(params)?)
.await
}
#[inline]
async fn _nip04_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip04Decrypt, serde_json::to_value(params)?)
.await
}
#[inline]
async fn _nip44_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
@@ -471,26 +417,6 @@ impl AsyncSignEvent for BrowserSignerProxy {
}
}
impl AsyncNip04 for BrowserSignerProxy {
type Error = Error;
fn nip04_encrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
Box::pin(async move { self._nip04_encrypt(public_key, content).await })
}
fn nip04_decrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
encrypted_content: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
Box::pin(async move { self._nip04_decrypt(public_key, encrypted_content).await })
}
}
impl AsyncNip44 for BrowserSignerProxy {
type Error = Error;
+6 -38
View File
@@ -101,7 +101,6 @@ pub struct ChatRegistry {
/// Signal consumer task (cancelled on signer change)
signal_consumer: Option<Task<Result<(), Error>>>,
/// Fuzzy matcher for room search (cached; intentionally excluded from Debug)
#[allow(dead_code)]
matcher: CachedMatcher,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 2]>,
@@ -549,26 +548,6 @@ impl ChatRegistry {
.count()
}
/// Count the number of messages seen by a given relay.
pub fn count_messages(&self, relay_url: &RelayUrl) -> usize {
self.seen
.read()
.unwrap()
.values()
.filter(|s| s.contains(relay_url))
.count()
}
/// Count the number of trash messages.
pub fn count_trash_messages(&self, cx: &App) -> usize {
self.trash.read(cx).len()
}
/// Get the trash messages entity.
pub fn trash(&self) -> Entity<BTreeSet<FailedMessage>> {
self.trash.clone()
}
/// Get the relays that have seen a given rumor id.
pub fn rumor_seen_on(&self, id: &EventId) -> Option<HashSet<RelayUrl>> {
self.event_map
@@ -579,7 +558,7 @@ impl ChatRegistry {
}
/// Get the relays that have seen a given gift wrap id.
pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
pub(crate) fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
self.seen
.read()
.unwrap()
@@ -589,7 +568,7 @@ impl ChatRegistry {
}
/// Add a new room to the start of list.
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
pub(crate) fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
where
I: Into<Room>,
{
@@ -623,7 +602,7 @@ impl ChatRegistry {
}
/// Sort rooms by their created at. Only notifies if order changed.
pub fn sort(&mut self, cx: &mut Context<Self>) {
pub(crate) fn sort(&mut self, cx: &mut Context<Self>) {
let before: Vec<_> = self.rooms.iter().map(|ev| ev.read(cx).id).collect();
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
let after: Vec<_> = self.rooms.iter().map(|ev| ev.read(cx).id).collect();
@@ -659,7 +638,7 @@ impl ChatRegistry {
}
/// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) {
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms.clear();
self.room_index.clear();
self.search.write().unwrap().clear();
@@ -701,7 +680,7 @@ impl ChatRegistry {
}
/// Load all rooms from the database.
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
pub(crate) fn get_rooms(&mut self, cx: &mut Context<Self>) {
let task = self.query_chat_rooms(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
@@ -796,7 +775,7 @@ impl ChatRegistry {
///
/// - If the room doesn't exist, it will be created.
/// - Updates room ordering based on the most recent messages.
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
pub(crate) fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
self.search.write().unwrap().insert(&message.rumor);
let nostr = NostrRegistry::global(cx);
@@ -821,17 +800,6 @@ impl ChatRegistry {
}
}
}
/// Trigger a refresh of the opened chat rooms by their IDs
pub fn refresh_rooms(&mut self, ids: &[u64], cx: &mut Context<Self>) {
for room in self.rooms.iter() {
if ids.contains(&room.read(cx).id) {
room.update(cx, |this, cx| {
this.emit_refresh(cx);
});
}
}
}
}
/// Unwraps a gift-wrapped event and processes its contents.
+6 -11
View File
@@ -6,7 +6,7 @@ use gpui::{SharedString, SharedUri};
use nostr_sdk::prelude::*;
use state::FileAttachment;
pub const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
pub(crate) const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
/// Rendered message.
#[derive(Debug, Clone)]
@@ -152,31 +152,26 @@ impl Message {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage {
pub room: u64,
pub gift_wrap: EventId,
pub rumor: UnsignedEvent,
}
impl NewMessage {
pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
pub fn new(_gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
let room = rumor.uniq_id();
Self {
room,
gift_wrap,
rumor,
}
Self { room, rumor }
}
}
/// Trash message.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FailedMessage {
pub(crate) struct FailedMessage {
pub raw_event: SharedString,
pub reason: SharedString,
}
impl FailedMessage {
pub fn new<T>(event: &Event, reason: T) -> Self
pub(crate) fn new<T>(event: &Event, reason: T) -> Self
where
T: Into<SharedString>,
{
@@ -194,7 +189,7 @@ pub struct Mention {
}
impl Mention {
pub fn new(public_key: PublicKey, range: Range<usize>) -> Self {
pub(crate) fn new(public_key: PublicKey, range: Range<usize>) -> Self {
Self { public_key, range }
}
}
+7 -9
View File
@@ -78,7 +78,7 @@ pub struct Room {
pub created_at: Timestamp,
/// Subject of the room
pub subject: Option<SharedString>,
pub(crate) subject: Option<SharedString>,
/// All members of the room
pub(super) members: Vec<PublicKey>,
@@ -186,13 +186,16 @@ impl Room {
}
/// Sets this room is ongoing conversation
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
pub(crate) fn set_ongoing(&mut self, cx: &mut Context<Self>) {
self.kind = RoomKind::Ongoing;
cx.notify();
}
/// Updates the creation timestamp of the room
pub fn set_created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
pub(crate) fn set_created_at<T>(&mut self, created_at: T, cx: &mut Context<Self>)
where
T: Into<Timestamp>,
{
self.created_at = created_at.into();
cx.notify();
}
@@ -297,7 +300,7 @@ impl Room {
}
/// Push a new message to the current room
pub fn push_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
pub(crate) fn push_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
let created_at = message.rumor.created_at;
let new_message = created_at > self.created_at;
@@ -309,11 +312,6 @@ impl Room {
}
}
/// Emits a signal to reload the current room's messages.
pub fn emit_refresh(&mut self, cx: &mut Context<Self>) {
cx.emit(RoomEvent::Reload);
}
/// Get gossip relays for each member
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
-1
View File
@@ -18,7 +18,6 @@ nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
log.workspace = true
serde.workspace = true
regex = "1"
-2
View File
@@ -243,8 +243,6 @@ impl ChatPanel {
}
}
RoomEvent::Reload => {
// Defer to avoid re-entrant read on Room while
// emit_refresh holds a write lock (via refresh_rooms).
cx.defer_in(window, |this, window, cx| {
this.get_messages(window, cx);
});
-2
View File
@@ -13,8 +13,6 @@ nostr-sdk.workspace = true
itertools.workspace = true
chrono.workspace = true
futures.workspace = true
log.workspace = true
dirs = "5.0"
qrcode = "0.14.1"
regex = "1.10"
+8 -8
View File
@@ -1,9 +1,9 @@
use std::marker::PhantomData;
use instant::Duration;
use futures::channel::oneshot;
use futures::FutureExt;
use futures::channel::oneshot;
use gpui::{Context, Task, Window};
use instant::Duration;
pub struct DebouncedDelay<E: 'static> {
task: Option<Task<()>>,
@@ -11,12 +11,6 @@ pub struct DebouncedDelay<E: 'static> {
_phantom_data: PhantomData<E>,
}
impl<E: 'static> Default for DebouncedDelay<E> {
fn default() -> Self {
Self::new()
}
}
impl<E: 'static> DebouncedDelay<E> {
pub fn new() -> Self {
Self {
@@ -64,3 +58,9 @@ impl<E: 'static> DebouncedDelay<E> {
}));
}
}
impl<E: 'static> Default for DebouncedDelay<E> {
fn default() -> Self {
Self::new()
}
}
+1 -27
View File
@@ -1,10 +1,6 @@
use std::sync::Arc;
use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString};
use gpui::SharedString;
use nostr_sdk::prelude::*;
use qrcode::QrCode;
use qrcode::render::svg;
const NOW: &str = "now";
const SECONDS_IN_MINUTE: i64 = 60;
@@ -59,25 +55,3 @@ impl TimestampExt for Timestamp {
}
}
}
pub trait StringExt {
fn to_qr(&self) -> Option<Arc<Image>>;
}
impl<T: AsRef<str>> StringExt for T {
fn to_qr(&self) -> Option<Arc<Image>> {
let s = self.as_ref();
let code = QrCode::new(s).unwrap();
let svg = code
.render()
.min_dimensions(256, 256)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#FFFFFF"))
.build();
Some(Arc::new(Image::from_bytes(
ImageFormat::Svg,
svg.into_bytes(),
)))
}
}
-2
View File
@@ -4,7 +4,6 @@ pub use event::*;
pub use media_extractor::*;
pub use parser::*;
pub use paths::*;
pub use range::*;
mod debounced_delay;
mod display;
@@ -12,4 +11,3 @@ mod event;
mod media_extractor;
mod parser;
mod paths;
mod range;
+7 -59
View File
@@ -1,30 +1,25 @@
use gpui::SharedUri;
use regex::Regex;
/// Extracts media URLs from a string and returns both the extracted URLs
/// and the string with media URLs removed
pub struct MediaExtractor {
/// Extracts media URLs from a string and returns both
/// the extracted URLs and the string with media URLs removed
struct MediaExtractor {
image_regex: Regex,
video_regex: Regex,
}
impl MediaExtractor {
/// Creates a new MediaExtractor with compiled regex patterns
pub fn new() -> Self {
fn new() -> Self {
MediaExtractor {
// Match common image extensions
image_regex: Regex::new(
r#"(?i)\bhttps?://[^\s<>"']+\.(?:jpg|jpeg|png|gif|bmp|webp|svg|ico)(?:\?[^\s<>"']*)?\b"#,
).unwrap(),
// Match common video extensions
video_regex: Regex::new(
r#"(?i)\bhttps?://[^\s<>"']+\.(?:mp4|mov|avi|mkv|webm|flv|wmv|m4v|3gp)(?:\?[^\s<>"']*)?\b"#,
).unwrap(),
}
}
/// Extracts all media URLs from a string
pub fn extract_media_urls(&self, text: &str) -> Vec<SharedUri> {
fn extract_media_urls(&self, text: &str) -> Vec<SharedUri> {
let mut urls = Vec::new();
// Extract image URLs
@@ -32,30 +27,22 @@ impl MediaExtractor {
urls.push(capture.as_str().to_string().into());
}
// Extract video URLs
// for capture in self.video_regex.find_iter(text) {
// urls.push(capture.as_str().to_string().into());
// }
urls
}
/// Removes all media URLs from a string and returns the cleaned text
pub fn remove_media_urls(&self, text: &str) -> String {
fn remove_media_urls(&self, text: &str) -> String {
let mut result = text.to_string();
// Remove image URLs
result = self.image_regex.replace_all(&result, "").to_string();
// Remove video URLs
// result = self.video_regex.replace_all(&result, "").to_string();
// Clean up extra whitespace that might result from removal
self.cleanup_text(&result)
}
/// Extracts media URLs and removes them from the string, returning both
pub fn extract_and_remove(&self, text: &str) -> (Vec<SharedUri>, String) {
fn extract_and_remove(&self, text: &str) -> (Vec<SharedUri>, String) {
let urls = self.extract_media_urls(text);
let cleaned_text = self.remove_media_urls(text);
(urls, cleaned_text)
@@ -69,33 +56,6 @@ impl MediaExtractor {
let re = Regex::new(r"\s+").unwrap();
re.replace_all(text, " ").trim().to_string()
}
/// Validates if a URL is a valid media URL
pub fn is_media_url(&self, url: &str) -> bool {
self.image_regex.is_match(url) || self.video_regex.is_match(url)
}
/// Categorizes extracted URLs into images and videos
pub fn categorize_urls(&self, urls: &[SharedUri]) -> (Vec<SharedUri>, Vec<SharedUri>) {
let mut images = Vec::new();
let mut videos = Vec::new();
for url in urls {
if self.image_regex.is_match(url) {
images.push(url.clone());
} else if self.video_regex.is_match(url) {
videos.push(url.clone());
}
}
(images, videos)
}
}
impl Default for MediaExtractor {
fn default() -> Self {
Self::new()
}
}
/// Convenience function for one-time extraction and removal
@@ -103,15 +63,3 @@ pub fn extract_and_remove_media_urls(text: &str) -> (Vec<SharedUri>, String) {
let extractor = MediaExtractor::new();
extractor.extract_and_remove(text)
}
/// Convenience function for just extracting media URLs
pub fn extract_media_urls(text: &str) -> Vec<SharedUri> {
let extractor = MediaExtractor::new();
extractor.extract_media_urls(text)
}
/// Convenience function for just removing media URLs
pub fn remove_media_urls(text: &str) -> String {
let extractor = MediaExtractor::new();
extractor.remove_media_urls(text)
}
+6 -6
View File
@@ -26,12 +26,6 @@ struct Match {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NostrParser;
impl Default for NostrParser {
fn default() -> Self {
Self::new()
}
}
impl NostrParser {
/// Create new parser
pub const fn new() -> Self {
@@ -44,6 +38,12 @@ impl NostrParser {
}
}
impl Default for NostrParser {
fn default() -> Self {
Self::new()
}
}
struct FindMatches<'a> {
bytes: &'a [u8],
pos: usize,
+1 -28
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use std::sync::OnceLock;
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
fn home_dir() -> &'static PathBuf {
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
}
@@ -36,30 +36,3 @@ pub fn config_dir() -> &'static PathBuf {
home_dir().join(".config").join("coop")
})
}
/// Returns the path to the support directory used by Coop.
pub fn support_dir() -> &'static PathBuf {
static SUPPORT_DIR: OnceLock<PathBuf> = OnceLock::new();
SUPPORT_DIR.get_or_init(|| {
if cfg!(target_os = "macos") {
return home_dir().join("Library/Application Support/Coop");
}
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
return if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") {
flatpak_xdg_data.into()
} else {
dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory")
}
.join("coop");
}
if cfg!(target_os = "windows") {
return dirs::data_local_dir()
.expect("failed to determine LocalAppData directory")
.join("coop");
}
config_dir().clone()
})
}
-45
View File
@@ -1,45 +0,0 @@
use std::cmp::{self};
use std::ops::{Range, RangeInclusive};
pub trait RangeExt<T> {
fn sorted(&self) -> Self;
fn to_inclusive(&self) -> RangeInclusive<T>;
fn overlaps(&self, other: &Range<T>) -> bool;
fn contains_inclusive(&self, other: &Range<T>) -> bool;
}
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
fn sorted(&self) -> Self {
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
}
fn to_inclusive(&self) -> RangeInclusive<T> {
self.start.clone()..=self.end.clone()
}
fn overlaps(&self, other: &Range<T>) -> bool {
self.start < other.end && other.start < self.end
}
fn contains_inclusive(&self, other: &Range<T>) -> bool {
self.start <= other.start && other.end <= self.end
}
}
impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
fn sorted(&self) -> Self {
cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone()
}
fn to_inclusive(&self) -> RangeInclusive<T> {
self.clone()
}
fn overlaps(&self, other: &Range<T>) -> bool {
self.start() < &other.end && &other.start <= self.end()
}
fn contains_inclusive(&self, other: &Range<T>) -> bool {
self.start() <= &other.start && &other.end <= self.end()
}
}
+2 -20
View File
@@ -1,16 +1,10 @@
//! Vendored from zed's `crates/gpui_tokio` (Apache-2.0) because the `gpui-pre` family
//! does not republish it, and `nostr-sdk`'s reqwest client needs a Tokio runtime.
use std::future::Future;
use gpui::{App, AppContext, Global, ReadGlobal, Task};
use gpui::{App, AppContext, Global, Task};
use gpui_util::defer;
pub use tokio::task::JoinError;
use tokio::task::JoinError;
/// Initializes the Tokio wrapper using a new Tokio runtime with 2 worker threads.
///
/// If you need more threads (or access to the runtime outside of GPUI), you can create the runtime
/// yourself and pass a Handle to `init_from_handle`.
pub fn init(cx: &mut App) {
let runtime = tokio::runtime::Builder::new_multi_thread()
// Since we now have two executors, let's try to keep our footprint small
@@ -26,14 +20,6 @@ pub fn init(cx: &mut App) {
});
}
/// Initializes the Tokio wrapper using a Tokio runtime handle.
pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) {
cx.set_global(GlobalTokio {
owned_runtime: None,
handle,
});
}
struct GlobalTokio {
owned_runtime: Option<tokio::runtime::Runtime>,
handle: tokio::runtime::Handle,
@@ -95,8 +81,4 @@ impl Tokio {
})
})
}
pub fn handle(cx: &App) -> tokio::runtime::Handle {
GlobalTokio::global(cx).handle.clone()
}
}
-4
View File
@@ -5,12 +5,8 @@ edition.workspace = true
publish.workspace = true
[dependencies]
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
futures.workspace = true
serde_json.workspace = true
state = { path = "../state" }
chat = { path = "../chat" }
+2 -95
View File
@@ -3,10 +3,7 @@ use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use chat::Message;
use futures::io::AsyncReadExt;
use gpui::http_client::{AsyncBody, Builder, HttpRequestExt, Method, RedirectPolicy, StatusCode};
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use nostr::nips::nip98::{HttpData, HttpMethod};
use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
@@ -14,14 +11,13 @@ use state::NostrRegistry;
use crate::LOCAL_KEYS;
use crate::protocol::{
ACTIVITY_KINDS, Activity, GroupAdmins, GroupId, GroupKey, GroupMembers, GroupMetadata,
GroupParticipants, GroupPins, GroupRoles, LivekitSession, MEMBERSHIP_KINDS, Membership,
PIN_LIST, Pin, RENDER_KINDS, STATE_KINDS, TimelineWindow,
GroupPins, GroupRoles, MEMBERSHIP_KINDS, Membership, PIN_LIST, Pin, RENDER_KINDS, STATE_KINDS,
TimelineWindow,
};
const REPLAY_LIMIT: usize = 100;
const PAGE_LIMIT: usize = 50;
const PAGE_TIMEOUT: Duration = Duration::from_secs(10);
const LIVEKIT_TIMEOUT: Duration = Duration::from_secs(10);
const LEAVE_WINDOW_SECS: u64 = 5 * 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -75,7 +71,6 @@ pub struct Group {
members: GroupMembers,
admins: GroupAdmins,
roles: GroupRoles,
participants: GroupParticipants,
pins: GroupPins,
membership: Membership,
membership_events: Vec<Event>,
@@ -103,7 +98,6 @@ impl Group {
members: GroupMembers::default(),
admins: GroupAdmins::default(),
roles: GroupRoles::default(),
participants: GroupParticipants::default(),
pins: GroupPins::default(),
membership: Membership::Unknown,
membership_events: Vec::new(),
@@ -163,10 +157,6 @@ impl Group {
&self.roles
}
pub fn participants(&self) -> &GroupParticipants {
&self.participants
}
pub fn pins(&self) -> &GroupPins {
&self.pins
}
@@ -190,10 +180,6 @@ impl Group {
})
}
pub fn loading(&self) -> bool {
self.loading
}
pub fn loading_more(&self) -> bool {
self.loading_more
}
@@ -214,10 +200,6 @@ impl Group {
self.refused.as_deref()
}
pub fn previous_tag(&self, me: PublicKey) -> Option<Tag> {
self.window.previous_tag(me)
}
pub fn send_message(
&mut self,
content: &str,
@@ -236,15 +218,6 @@ impl Group {
self.dispatch(move |id, prev| id.reaction(target, &emoji, prev), cx)
}
pub fn put_user(
&mut self,
public_key: PublicKey,
roles: Vec<String>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.dispatch(move |id, prev| id.put_user(public_key, &roles, prev), cx)
}
pub fn remove_user(
&mut self,
public_key: PublicKey,
@@ -421,68 +394,6 @@ impl Group {
cx.notify();
}
pub fn livekit_session(&self, cx: &App) -> Task<Result<LivekitSession>> {
let url = match self.key.livekit_token_url() {
Ok(url) => url,
Err(error) => return Task::ready(Err(error)),
};
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let client = cx.http_client();
cx.background_spawn(async move {
let authorization = HttpData::new(url.clone(), HttpMethod::GET)
.to_authorization(&signer)
.await?;
let request = Builder::new()
.uri(url.as_str())
.method(Method::GET)
.header("Authorization", authorization)
.follow_redirects(RedirectPolicy::NoFollow)
.timeout(LIVEKIT_TIMEOUT)
.body(AsyncBody::default())?;
let response = client.send(request).await?;
if !response.status().is_success() {
bail!(
"nip29: the AV token endpoint answered {}",
response.status()
);
}
let mut body = Vec::new();
response.into_body().read_to_end(&mut body).await?;
LivekitSession::parse(&body)
})
}
pub fn livekit_supported(&self, cx: &App) -> Task<Result<bool>> {
let url = match self.key.livekit_probe_url() {
Ok(url) => url,
Err(error) => return Task::ready(Err(error)),
};
let client = cx.http_client();
cx.background_spawn(async move {
let request = Builder::new()
.uri(url.as_str())
.method(Method::GET)
.follow_redirects(RedirectPolicy::NoFollow)
.timeout(LIVEKIT_TIMEOUT)
.body(AsyncBody::default())?;
let response = client.send(request).await?;
Ok(response.status() == StatusCode::NO_CONTENT)
})
}
pub(crate) fn save_envelope(&self, event: Event, cx: &App) -> Task<Result<()>> {
let cache_tag = self.key.cache_tag();
let nostr = NostrRegistry::global(cx);
@@ -681,10 +592,6 @@ impl Group {
Ok(roles) => self.roles = roles,
Err(_) => return false,
},
Kind::GroupLivekitParticipants => match GroupParticipants::parse(event) {
Ok(participants) => self.participants = participants,
Err(_) => return false,
},
Kind::Custom(PIN_LIST) => match GroupPins::parse(event) {
Ok(pins) => self.pins = pins,
Err(_) => return false,
+1 -1
View File
@@ -317,7 +317,7 @@ impl GroupsRegistry {
}));
}
pub fn reset(&mut self, cx: &mut Context<Self>) {
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
self.notification_listener = None;
self.signal_consumer = None;
self.tasks.clear();
+13 -147
View File
@@ -6,12 +6,12 @@ use std::str::FromStr;
use anyhow::{Result, anyhow, bail};
use nostr_sdk::prelude::*;
pub const TIMELINE_WINDOW: usize = 50;
pub(crate) const TIMELINE_WINDOW: usize = 50;
const PREVIOUS_REFS: usize = 3;
const PREVIOUS_REF_LEN: usize = 8;
pub const RENDER_KINDS: [Kind; 5] = [
pub(crate) const RENDER_KINDS: [Kind; 5] = [
Kind::TextNote,
Kind::ChatMessage,
Kind::Thread,
@@ -19,21 +19,20 @@ pub const RENDER_KINDS: [Kind; 5] = [
Kind::Reaction,
];
pub const PIN_LIST: u16 = 39_005;
pub const UPDATE_PIN_LIST: u16 = 9_010;
pub(crate) const PIN_LIST: u16 = 39_005;
pub(crate) const UPDATE_PIN_LIST: u16 = 9_010;
pub const STATE_KINDS: [Kind; 6] = [
pub(crate) const STATE_KINDS: [Kind; 5] = [
Kind::GroupMetadata,
Kind::GroupAdmins,
Kind::GroupMembers,
Kind::GroupRoles,
Kind::GroupLivekitParticipants,
Kind::Custom(PIN_LIST),
];
pub const MEMBERSHIP_KINDS: [Kind; 2] = [Kind::GroupPutUser, Kind::GroupRemoveUser];
pub(crate) const MEMBERSHIP_KINDS: [Kind; 2] = [Kind::GroupPutUser, Kind::GroupRemoveUser];
pub const ACTIVITY_KINDS: [Kind; 4] = [
pub(crate) const ACTIVITY_KINDS: [Kind; 4] = [
Kind::GroupPutUser,
Kind::GroupRemoveUser,
Kind::GroupJoinRequest,
@@ -128,22 +127,6 @@ impl GroupId {
EventBuilder::new(Kind::GroupLeaveRequest, reason).tag(self.h_tag())
}
pub fn put_user(
&self,
public_key: PublicKey,
roles: &[String],
previous: Option<Tag>,
) -> EventBuilder {
let mut values = Vec::with_capacity(1 + roles.len());
values.push(public_key.to_hex());
values.extend(roles.iter().cloned());
EventBuilder::new(Kind::GroupPutUser, "")
.tag(self.h_tag())
.tag(Tag::custom("p", values))
.tag_maybe(previous)
}
pub fn remove_user(&self, public_key: PublicKey, previous: Option<Tag>) -> EventBuilder {
EventBuilder::new(Kind::GroupRemoveUser, "")
.tag(self.h_tag())
@@ -263,47 +246,6 @@ impl GroupKey {
self.hash(&mut hasher);
hasher.finish()
}
pub fn livekit_token_url(&self) -> Result<Url> {
self.livekit_url(Some(self.id.as_str()))
}
pub fn livekit_probe_url(&self) -> Result<Url> {
self.livekit_url(None)
}
fn livekit_url(&self, group: Option<&str>) -> Result<Url> {
let mut url: Url = self.relay.clone().into();
let scheme = if self.relay.scheme().is_secure() {
"https"
} else {
"http"
};
url.set_path("/");
url.set_query(None);
url.set_fragment(None);
url.set_scheme(scheme)
.map_err(|_| anyhow!("{scheme} is not available for {}", self.relay))?;
{
let mut segments = url
.path_segments_mut()
.map_err(|_| anyhow!("{} cannot carry a path", self.relay))?;
segments
.pop_if_empty()
.push(".well-known")
.push("nip29")
.push("livekit");
if let Some(group) = group {
segments.push(group);
}
}
Ok(url)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -347,14 +289,6 @@ impl GroupReference {
Ok(Self { id, relay, invite })
}
pub fn id(&self) -> &GroupId {
&self.id
}
pub fn relay(&self) -> &RelayUrl {
&self.relay
}
pub fn invite(&self) -> Option<&str> {
self.invite.as_deref()
}
@@ -376,7 +310,7 @@ impl FromStr for GroupReference {
pub struct GroupMetadata {
pub name: Option<String>,
pub picture: Option<String>,
pub banner: Option<String>,
pub(crate) banner: Option<String>,
pub about: Option<String>,
pub private: bool,
pub restricted: bool,
@@ -478,7 +412,7 @@ impl GroupMetadata {
#[derive(Debug, Clone)]
pub struct GroupCandidate {
pub key: GroupKey,
pub metadata: GroupMetadata,
pub(crate) metadata: GroupMetadata,
}
impl GroupCandidate {
@@ -539,10 +473,6 @@ impl GroupMembers {
Ok(Self(event.tags.public_keys().collect()))
}
pub fn contains(&self, public_key: &PublicKey) -> bool {
self.0.contains(public_key)
}
pub fn iter(&self) -> impl Iterator<Item = &PublicKey> {
self.0.iter()
}
@@ -560,7 +490,6 @@ impl GroupMembers {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Role {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Default)]
@@ -580,10 +509,7 @@ impl GroupRoles {
.next()
.ok_or_else(|| anyhow!("role tag has no name"))?;
roles.push(Role {
name: name.clone(),
description: fields.next().cloned(),
});
roles.push(Role { name: name.clone() });
}
Ok(Self(roles))
@@ -594,37 +520,6 @@ impl GroupRoles {
}
}
#[derive(Debug, Clone, Default)]
pub struct GroupParticipants(Vec<PublicKey>);
impl GroupParticipants {
pub fn parse(event: &Event) -> Result<Self> {
if event.kind != Kind::GroupLivekitParticipants {
bail!("not a group participants event: kind {}", event.kind);
}
let mut participants = Vec::new();
for tag in event.tags.iter().filter(|tag| tag.kind() == "participant") {
let Some(public_key) = tag.content() else {
continue;
};
participants.push(PublicKey::parse(public_key)?);
}
Ok(Self(participants))
}
pub fn contains(&self, public_key: &PublicKey) -> bool {
self.0.contains(public_key)
}
pub fn iter(&self) -> impl Iterator<Item = &PublicKey> {
self.0.iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pin {
Event(EventId),
@@ -680,43 +575,14 @@ impl GroupPins {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LivekitSession {
pub token: String,
pub server_url: String,
}
impl LivekitSession {
pub fn parse(body: &[u8]) -> Result<Self> {
let value: serde_json::Value = serde_json::from_slice(body)?;
let token = value
.get("token")
.or_else(|| value.get("jwt"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| anyhow!("the AV response carries no token"))?;
let server_url = value
.get("url")
.or_else(|| value.get("server_url"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| anyhow!("the AV response carries no server url"))?;
Ok(Self {
token: token.to_owned(),
server_url: server_url.to_owned(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupListEntry {
pub(crate) struct GroupListEntry {
pub id: GroupId,
pub relay: RelayUrl,
pub name: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct GroupList(Vec<GroupListEntry>);
pub(crate) struct GroupList(Vec<GroupListEntry>);
impl GroupList {
pub fn parse(event: &Event) -> Result<Self> {
@@ -897,7 +763,7 @@ impl Activity {
}
#[derive(Debug, Clone, Default)]
pub struct TimelineWindow(Vec<Event>);
pub(crate) struct TimelineWindow(Vec<Event>);
impl TimelineWindow {
pub fn new() -> Self {
+1 -1
View File
@@ -315,7 +315,7 @@ pub struct GroupPanel {
}
impl GroupPanel {
pub fn new(group: Entity<Group>, window: &mut Window, cx: &mut Context<Self>) -> Self {
pub(crate) fn new(group: Entity<Group>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let (id, name, count, head) = {
let group = group.read(cx);
let rows = group.rows();
-2
View File
@@ -5,7 +5,6 @@ edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
state = { path = "../state" }
person = { path = "../person" }
ui = { path = "../ui" }
@@ -14,7 +13,6 @@ settings = { path = "../settings" }
gpui.workspace = true
nostr-sdk.workspace = true
instant.workspace = true
anyhow.workspace = true
smallvec.workspace = true
log.workspace = true
+4 -4
View File
@@ -45,7 +45,7 @@ pub enum DeviceEvent {
}
impl DeviceEvent {
pub fn error<T>(error: T) -> Self
pub(crate) fn error<T>(error: T) -> Self
where
T: Into<SharedString>,
{
@@ -60,9 +60,9 @@ impl DeviceEvent {
pub struct DeviceRegistry {
signer: Entity<Option<UniversalSigner>>,
/// Whether there is a pending request for encryption key approval
pub pending_request: bool,
pub(crate) pending_request: bool,
/// Whether an announcement has been made for this device
pub announcement_existed: Arc<AtomicBool>,
pub(crate) announcement_existed: Arc<AtomicBool>,
/// Relay notification listener
notification_listener: Option<Task<Result<(), Error>>>,
tasks: Vec<Task<Result<(), Error>>>,
@@ -398,7 +398,7 @@ impl DeviceRegistry {
}
/// Request encryption keys from other device
pub fn request(&mut self, cx: &mut Context<Self>) {
pub(crate) fn request(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
+5 -11
View File
@@ -47,12 +47,6 @@ impl Hash for Person {
}
}
impl From<PublicKey> for Person {
fn from(public_key: PublicKey) -> Self {
Self::new(public_key, Metadata::default())
}
}
impl Person {
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
Self {
@@ -64,13 +58,13 @@ impl Person {
}
/// Build profile encryption keys announcement
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
pub(crate) fn with_announcement(mut self, announcement: Announcement) -> Self {
self.announcement = Some(announcement);
self
}
/// Build profile messaging relays
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
pub(crate) fn with_messaging_relays<I>(mut self, relays: I) -> Self
where
I: IntoIterator<Item = RelayUrl>,
{
@@ -135,17 +129,17 @@ impl Person {
}
/// Set profile metadata
pub fn set_metadata(&mut self, metadata: Metadata) {
pub(crate) fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
pub(crate) fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
}
/// Set profile messaging relays
pub fn set_messaging_relays<I>(&mut self, relays: I)
pub(crate) fn set_messaging_relays<I>(&mut self, relays: I)
where
I: IntoIterator<Item = RelayUrl>,
{
+17 -45
View File
@@ -38,14 +38,29 @@ macro_rules! setting_accessors {
};
}
macro_rules! setting_getters {
($(pub $field:ident: $type:ty),* $(,)?) => {
impl AppSettings {
$(
paste::paste! {
pub fn [<get_ $field>](cx: &App) -> $type {
Self::global(cx).read(cx).inner.read(cx).$field.clone()
}
}
)*
}
};
}
setting_accessors! {
pub theme: Option<String>,
pub theme_mode: ThemeMode,
pub hide_avatar: bool,
pub screening: bool,
pub nip4e: bool,
pub trusted_relays: Vec<String>,
pub file_server: Url,
}
setting_getters! {
pub pinned_rooms: Vec<u64>,
pub pinned_groups: Vec<String>,
}
@@ -60,14 +75,6 @@ pub enum SignerKind {
}
impl SignerKind {
pub fn auto(&self) -> bool {
matches!(self, SignerKind::Auto)
}
pub fn user(&self) -> bool {
matches!(self, SignerKind::User)
}
pub fn encryption(&self) -> bool {
matches!(self, SignerKind::Encryption)
}
@@ -158,12 +165,6 @@ impl Default for Settings {
}
}
impl AsRef<Settings> for Settings {
fn as_ref(&self) -> &Settings {
self
}
}
struct GlobalAppSettings(Entity<AppSettings>);
impl Global for GlobalAppSettings {}
@@ -182,11 +183,6 @@ impl AppSettings {
cx.global::<GlobalAppSettings>().0.clone()
}
/// The underlying settings entity, which notifies whenever any field changes.
pub fn entity(&self) -> &Entity<Settings> {
&self.inner
}
/// Set the global settings instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAppSettings(state));
@@ -313,30 +309,6 @@ impl AppSettings {
self.inner.read(cx).nip4e
}
/// Check if the given relay is already authenticated
pub fn trusted_relay(&self, url: &RelayUrl, cx: &App) -> bool {
self.inner
.read(cx)
.trusted_relays
.iter()
.any(|relay| relay == url.as_str_without_trailing_slash())
}
/// Add a relay to the trusted list
pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
self.inner.update(cx, |this, cx| {
if !this
.trusted_relays
.iter()
.any(|relay| relay == url.as_str_without_trailing_slash())
{
this.trusted_relays
.push(url.as_str_without_trailing_slash().to_string());
cx.notify();
}
});
}
/// Check if the chat room is pinned
pub fn pinned_room(&self, id: u64, cx: &App) -> bool {
self.inner.read(cx).pinned_rooms.contains(&id)
-1
View File
@@ -17,7 +17,6 @@ gpui.workspace = true
instant.workspace = true
flume.workspace = true
futures.workspace = true
log.workspace = true
anyhow.workspace = true
webbrowser.workspace = true
serde_json.workspace = true
+5 -16
View File
@@ -1,14 +1,11 @@
/// Client name (Application name)
pub const CLIENT_NAME: &str = "Coop";
/// COOP's public key
pub const COOP_PUBKEY: &str = "npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x";
/// App ID
pub const APP_ID: &str = "su.reya.coop";
/// Keyring name
pub const MASTER_KEYRING: &str = "Coop Master Key";
pub(crate) const MASTER_KEYRING: &str = "Coop Master Key";
pub const USER_KEYRING: &str = "Coop User Credential";
/// Default timeout for subscription
@@ -26,20 +23,12 @@ pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
/// Default subscription id for user gift wrap events
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
/// Default timeout for Nostr Connect
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
/// Default Nostr Connect relay
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
/// Default vertex relays
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
/// Default search relays
pub(crate) const INDEXER_RELAYS: [&str; 2] =
["wss://indexer.coracle.social", "wss://user.kindpag.es"];
/// Default search relays
pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"];
/// Default search relays
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
pub(crate) const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
/// Default bootstrap relays
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
+7 -64
View File
@@ -15,9 +15,9 @@ use nostr::nips::nip94::Sha256Hash;
use nostr_sdk::prelude::*;
use sha2::{Digest, Sha256};
pub const ALGORITHM: &str = "aes-gcm";
pub(crate) const ALGORITHM: &str = "aes-gcm";
pub const MAX_FILE_SIZE: usize = 25 * 1024 * 1024;
pub(crate) const MAX_FILE_SIZE: usize = 25 * 1024 * 1024;
const TAG_SHA256: &str = "x";
const TAG_ORIGINAL_SHA256: &str = "ox";
@@ -30,7 +30,7 @@ const TAG_DIM: &str = "dim";
const TAG_ALT: &str = "alt";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedFile {
pub(crate) struct EncryptedFile {
pub data: Vec<u8>,
pub key: String,
pub nonce: String,
@@ -115,7 +115,7 @@ impl FileAttachment {
}
}
pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
pub(crate) fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
let key = Aes256Gcm::generate_key(OsRng);
let nonce = AesGcm::<Aes256, U16>::generate_nonce(OsRng);
let cipher = AesGcm::<Aes256, U16>::new(&key);
@@ -131,7 +131,7 @@ pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
})
}
pub fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result<Vec<u8>, Error> {
pub(crate) fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result<Vec<u8>, Error> {
let key = decode(key, "decryption key")?;
let nonce = decode(nonce, "decryption nonce")?;
@@ -159,7 +159,7 @@ pub fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result<Vec<u8>, Error> {
}
}
pub fn sha256_hex(data: &[u8]) -> String {
pub(crate) fn sha256_hex(data: &[u8]) -> String {
let hash: [u8; 32] = Sha256::digest(data).into();
Sha256Hash::from_byte_array(hash).to_hex()
@@ -227,7 +227,7 @@ pub async fn upload_encrypted(
Err(anyhow!("File upload not supported on web"))
}
pub async fn download_and_decrypt(
pub(crate) async fn download_and_decrypt(
url: &Url,
key: &str,
nonce: &str,
@@ -318,63 +318,6 @@ pub async fn download_and_decrypt_to_file(
Err(anyhow!("File download not supported on web"))
}
/// The cache file a decrypted blob for `plaintext_sha256` is written to.
#[cfg(not(target_arch = "wasm32"))]
fn blob_cache_path(plaintext_sha256: &str) -> PathBuf {
std::env::temp_dir()
.join("coop-blobs")
.join(plaintext_sha256)
}
/// Download an encrypted blob whose pointer carries the *plaintext* hash
/// and write the decrypted bytes to a content-addressed cache file,
/// so later renders skip the network.
///
/// The cache file carries no extension: `img` sniffs the format from the bytes.
#[cfg(not(target_arch = "wasm32"))]
pub async fn download_and_decrypt_to_cache(
url: &Url,
key: &str,
nonce: &str,
plaintext_sha256: &str,
cx: &AsyncApp,
) -> Result<PathBuf, Error> {
let path = blob_cache_path(plaintext_sha256);
if smol::fs::metadata(&path).await.is_ok() {
return Ok(path);
}
let data = download_and_decrypt(url, key, nonce, None, cx).await?;
if !sha256_hex(&data).eq_ignore_ascii_case(plaintext_sha256) {
bail!("Blob hash mismatch");
}
let Some(parent) = path.parent() else {
bail!("Invalid blob cache path");
};
smol::fs::create_dir_all(parent).await?;
// Write under a temporary name first, so an interrupted download is never reused
let partial = path.with_extension("download");
smol::fs::write(&partial, data).await?;
smol::fs::rename(&partial, &path).await?;
Ok(path)
}
#[cfg(target_arch = "wasm32")]
pub async fn download_and_decrypt_to_cache(
_url: &Url,
_key: &str,
_nonce: &str,
_plaintext_sha256: &str,
_cx: &AsyncApp,
) -> Result<PathBuf, Error> {
Err(anyhow!("Blob download not supported on web"))
}
fn tag_value<'a>(tags: &'a Tags, name: &str) -> Option<&'a str> {
tags.iter()
.find(|tag| tag.kind() == name)
+1 -72
View File
@@ -52,8 +52,6 @@ impl Global for GlobalNostrRegistry {}
/// Signer event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum StateEvent {
/// The state is busy
Busy,
/// User has no signer
NoSigner,
/// The signer has changed
@@ -67,7 +65,7 @@ impl StateEvent {
matches!(self, StateEvent::SignerChanged)
}
pub fn error<T>(error: T) -> Self
pub(crate) fn error<T>(error: T) -> Self
where
T: Into<String>,
{
@@ -180,11 +178,6 @@ impl NostrRegistry {
self.current_user
}
/// Whether the initial credential check has concluded
pub fn ready(&self) -> bool {
self.ready
}
fn mark_ready(&mut self, cx: &mut Context<Self>) {
if self.ready {
return;
@@ -525,68 +518,4 @@ impl NostrRegistry {
Ok(results)
})
}
/// Perform a WoT (via Vertex) search for a given query.
pub fn wot_search(&self, query: &str, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> {
let client = self.client();
let query = query.to_string();
let signer = self.signer.clone();
cx.background_spawn(async move {
// Construct a vertex request event
let event = EventBuilder::new(Kind::Custom(5315), "")
.tags(vec![
Tag::custom("param", vec!["search", &query]),
Tag::custom("param", vec!["limit", "10"]),
])
.finalize_async(&signer)
.await?;
// Send the event to vertex relays
let output = client.send_event(&event).to(WOT_RELAYS).await?;
// Construct a filter to get the response or error from vertex
let filter = Filter::new()
.kinds(vec![Kind::Custom(6315), Kind::Custom(7000)])
.event(output.id().to_owned());
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = WOT_RELAYS
.into_iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect();
// Stream events from the wot relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
if let Ok(event) = res {
match event.kind {
Kind::Custom(6315) => {
let content: serde_json::Value = serde_json::from_str(&event.content)?;
let pubkeys: Vec<PublicKey> = content
.as_array()
.into_iter()
.flatten()
.filter_map(|item| item.as_object())
.filter_map(|obj| obj.get("pubkey").and_then(|v| v.as_str()))
.filter_map(|pubkey_str| PublicKey::parse(pubkey_str).ok())
.collect();
return Ok(pubkeys);
}
Kind::Custom(7000) => {
return Err(anyhow!("Search error"));
}
_ => {}
}
}
}
Err(anyhow!("No results for query: {query}"))
})
}
}
-7
View File
@@ -8,9 +8,6 @@ pub struct Announcement {
}
impl Announcement {
/// Parse an announcement from an event, returning `None` when the `n`
/// tag is missing or malformed so callers never mistake the author's
/// identity key for an encryption key.
pub fn from_event(event: &Event) -> Option<Self> {
let public_key = event
.tags
@@ -22,10 +19,6 @@ impl Announcement {
Some(Self { public_key })
}
pub fn new(public_key: PublicKey) -> Self {
Self { public_key }
}
/// Returns the public key of the device that created this announcement.
pub fn public_key(&self) -> PublicKey {
self.public_key
+1 -9
View File
@@ -50,7 +50,7 @@ impl UniversalSigner {
}
/// Swap the inner signer in-place. All clones see the new signer.
pub fn swap_inner<T>(&self, new_signer: T)
pub(crate) fn swap_inner<T>(&self, new_signer: T)
where
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
@@ -137,14 +137,6 @@ where
}
}
impl UniversalSigner {
#[allow(dead_code)]
fn with_inner<R>(&self, f: impl FnOnce(&dyn InnerSigner) -> R) -> R {
let guard = self.inner.read().expect("RwLock poisoned");
f(&**guard)
}
}
impl AsyncGetPublicKey for UniversalSigner {
type Error = UniversalSignerError;
-1
View File
@@ -12,7 +12,6 @@ log.workspace = true
serde.workspace = true
serde_json.workspace = true
schemars.workspace = true
smallvec.workspace = true
[dev-dependencies]
tempfile = "3.10"
File diff suppressed because it is too large Load Diff
+2 -98
View File
@@ -1,57 +1,9 @@
use std::fmt::{self, Debug, Display, Formatter};
use std::fmt::Debug;
use gpui::{AbsoluteLength, Axis, Length, Pixels};
use gpui::Axis;
use serde::{Deserialize, Serialize};
/// A enum for defining the placement of the element.
///
/// See also: [`Side`] if you need to define the left, right side.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Placement {
#[serde(rename = "top")]
Top,
#[serde(rename = "bottom")]
Bottom,
#[serde(rename = "left")]
Left,
#[serde(rename = "right")]
Right,
}
impl Display for Placement {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Placement::Top => write!(f, "Top"),
Placement::Bottom => write!(f, "Bottom"),
Placement::Left => write!(f, "Left"),
Placement::Right => write!(f, "Right"),
}
}
}
impl Placement {
#[inline]
pub fn is_horizontal(&self) -> bool {
matches!(self, Placement::Left | Placement::Right)
}
#[inline]
pub fn is_vertical(&self) -> bool {
matches!(self, Placement::Top | Placement::Bottom)
}
#[inline]
pub fn axis(&self) -> Axis {
match self {
Placement::Top | Placement::Bottom => Axis::Vertical,
Placement::Left | Placement::Right => Axis::Horizontal,
}
}
}
/// A enum for defining the side of the element.
///
/// See also: [`Placement`] if you need to define the 4 edges.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Side {
#[serde(rename = "left")]
@@ -94,39 +46,6 @@ impl AxisExt for Axis {
}
}
/// A trait for converting [`Pixels`] to `f32` and `f64`.
pub trait PixelsExt {
fn as_f32(&self) -> f32;
#[allow(clippy::wrong_self_convention)]
fn as_f64(self) -> f64;
}
impl PixelsExt for Pixels {
fn as_f32(&self) -> f32 {
f32::from(self)
}
fn as_f64(self) -> f64 {
f64::from(self)
}
}
/// A trait to extend the [`Length`] enum with utility methods.
pub trait LengthExt {
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
///
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
}
impl LengthExt for Length {
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
match self {
Length::Auto => None,
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
}
}
}
/// A struct for defining the edges of an element.
///
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
@@ -142,18 +61,3 @@ pub struct Edges<T: Clone + Debug + Default + PartialEq> {
/// The size of the left edge.
pub left: T,
}
impl<T> Edges<T>
where
T: Clone + Debug + Default + PartialEq,
{
/// Creates a new `Edges` instance with all edges set to the same value.
pub fn all(value: T) -> Self {
Self {
top: value.clone(),
right: value.clone(),
bottom: value.clone(),
left: value,
}
}
}
+2 -6
View File
@@ -31,7 +31,7 @@ pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
/// Defines window titlebar height
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
pub(crate) const TITLEBAR_HEIGHT: Pixels = px(36.0);
/// Defines workspace tabbar height
pub const TABBAR_HEIGHT: Pixels = px(36.0);
@@ -145,9 +145,6 @@ pub struct Theme {
/// Notification settings
pub notification: NotificationSettings,
/// Platform kind
pub platform: PlatformKind,
}
impl Deref for Theme {
@@ -173,7 +170,7 @@ impl Theme {
}
/// Returns the global theme mutable reference
pub fn global_mut(cx: &mut App) -> &mut Theme {
pub(crate) fn global_mut(cx: &mut App) -> &mut Theme {
cx.global_mut::<Theme>()
}
@@ -274,7 +271,6 @@ impl From<ThemeFamily> for Theme {
mode,
colors: *colors,
theme: Rc::new(family),
platform,
}
}
}
-12
View File
@@ -18,16 +18,4 @@ impl PlatformKind {
Self::Web
}
}
pub fn is_linux(&self) -> bool {
matches!(self, Self::Linux)
}
pub fn is_windows(&self) -> bool {
matches!(self, Self::Windows)
}
pub fn is_mac(&self) -> bool {
matches!(self, Self::Mac)
}
}
+6 -110
View File
@@ -1,6 +1,4 @@
#![allow(dead_code)]
use gpui::{Hsla, SharedString};
use gpui::Hsla;
/// A collection of colors that are used to style the UI.
///
@@ -9,21 +7,6 @@ use gpui::{Hsla, SharedString};
pub struct ColorScaleStep(usize);
impl ColorScaleStep {
/// All of the steps in a [`ColorScale`].
pub const ALL: [ColorScaleStep; 12] = [
Self::ONE,
Self::TWO,
Self::THREE,
Self::FOUR,
Self::FIVE,
Self::SIX,
Self::SEVEN,
Self::EIGHT,
Self::NINE,
Self::TEN,
Self::ELEVEN,
Self::TWELVE,
];
pub const EIGHT: Self = Self(8);
pub const ELEVEN: Self = Self(11);
pub const FIVE: Self = Self(5);
@@ -153,89 +136,8 @@ impl ColorScale {
}
}
pub struct ColorScales {
pub gray: ColorScaleSet,
pub mauve: ColorScaleSet,
pub slate: ColorScaleSet,
pub sage: ColorScaleSet,
pub olive: ColorScaleSet,
pub sand: ColorScaleSet,
pub gold: ColorScaleSet,
pub bronze: ColorScaleSet,
pub brown: ColorScaleSet,
pub yellow: ColorScaleSet,
pub amber: ColorScaleSet,
pub orange: ColorScaleSet,
pub tomato: ColorScaleSet,
pub red: ColorScaleSet,
pub ruby: ColorScaleSet,
pub crimson: ColorScaleSet,
pub pink: ColorScaleSet,
pub plum: ColorScaleSet,
pub purple: ColorScaleSet,
pub violet: ColorScaleSet,
pub iris: ColorScaleSet,
pub indigo: ColorScaleSet,
pub blue: ColorScaleSet,
pub cyan: ColorScaleSet,
pub teal: ColorScaleSet,
pub jade: ColorScaleSet,
pub green: ColorScaleSet,
pub grass: ColorScaleSet,
pub lime: ColorScaleSet,
pub mint: ColorScaleSet,
pub sky: ColorScaleSet,
pub black: ColorScaleSet,
pub white: ColorScaleSet,
}
impl IntoIterator for ColorScales {
type IntoIter = std::vec::IntoIter<Self::Item>;
type Item = ColorScaleSet;
fn into_iter(self) -> Self::IntoIter {
vec![
self.gray,
self.mauve,
self.slate,
self.sage,
self.olive,
self.sand,
self.gold,
self.bronze,
self.brown,
self.yellow,
self.amber,
self.orange,
self.tomato,
self.red,
self.ruby,
self.crimson,
self.pink,
self.plum,
self.purple,
self.violet,
self.iris,
self.indigo,
self.blue,
self.cyan,
self.teal,
self.jade,
self.green,
self.grass,
self.lime,
self.mint,
self.sky,
self.black,
self.white,
]
.into_iter()
}
}
/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale.
pub struct ColorScaleSet {
name: SharedString,
light: ColorScale,
dark: ColorScale,
light_alpha: ColorScale,
@@ -243,15 +145,13 @@ pub struct ColorScaleSet {
}
impl ColorScaleSet {
pub fn new(
name: impl Into<SharedString>,
pub(crate) fn new(
light: ColorScale,
light_alpha: ColorScale,
dark: ColorScale,
dark_alpha: ColorScale,
) -> Self {
Self {
name: name.into(),
light,
light_alpha,
dark,
@@ -259,23 +159,19 @@ impl ColorScaleSet {
}
}
pub fn name(&self) -> &SharedString {
&self.name
}
pub fn light(&self) -> &ColorScale {
pub(crate) fn light(&self) -> &ColorScale {
&self.light
}
pub fn light_alpha(&self) -> &ColorScale {
pub(crate) fn light_alpha(&self) -> &ColorScale {
&self.light_alpha
}
pub fn dark(&self) -> &ColorScale {
pub(crate) fn dark(&self) -> &ColorScale {
&self.dark
}
pub fn dark_alpha(&self) -> &ColorScale {
pub(crate) fn dark_alpha(&self) -> &ColorScale {
&self.dark_alpha
}
}
-8
View File
@@ -13,12 +13,4 @@ impl ScrollbarMode {
pub fn is_scrolling(&self) -> bool {
matches!(self, Self::Scrolling)
}
pub fn is_hover(&self) -> bool {
matches!(self, Self::Hover)
}
pub fn is_always(&self) -> bool {
matches!(self, Self::Always)
}
}
+1 -1
View File
@@ -117,7 +117,7 @@ impl ThemeFamily {
/// # Ok(())
/// # }
/// ```
pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let json_data = std::fs::read(path)?;
let theme_family = serde_json::from_slice(&json_data)?;
-2
View File
@@ -5,7 +5,6 @@ edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
theme = { path = "../theme" }
gpui.workspace = true
@@ -13,6 +12,5 @@ gpui-base.workspace = true
instant.workspace = true
serde.workspace = true
smallvec.workspace = true
log.workspace = true
uuid = "1.10"
-12
View File
@@ -391,18 +391,6 @@ impl Avatar {
self.seed = Some(seed.into());
self
}
/// Applies a grayscale filter to the avatar image.
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale;
self
}
/// Sets the border color of the avatar.
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
self.border_color = Some(color.into());
self
}
}
impl Sizable for Avatar {
-20
View File
@@ -208,12 +208,6 @@ impl Button {
self
}
/// Set true to show the indicator.
pub fn indicator(mut self) -> Self {
self.indicator = true;
self
}
/// Add click handler.
pub fn on_click(
mut self,
@@ -223,20 +217,6 @@ impl Button {
self
}
/// Add hover handler, the bool parameter indicates whether the mouse is hovering.
pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Some(Rc::new(handler));
self
}
/// Set the tab index of the button, it will be used to focus the button by tab key.
///
/// Default is 0.
pub fn tab_index(mut self, tab_index: isize) -> Self {
self.tab_index = tab_index;
self
}
/// Set the tab stop of the button, if true, the button will be focusable by tab key.
///
/// Default is true.
-35
View File
@@ -58,12 +58,6 @@ impl DialogButtonProps {
self.cancel_text = Some(cancel_text.into());
self
}
/// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`.
pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
self.cancel_variant = cancel_variant;
self
}
}
#[derive(IntoElement)]
@@ -145,23 +139,12 @@ impl Dialog {
}
/// Set to use confirm dialog, with OK and Cancel buttons.
///
/// See also [`Self::alert`]
pub fn confirm(self) -> Self {
self.footer(|ok, cancel, window, cx| vec![cancel(window, cx), ok(window, cx)])
.overlay_closable(false)
.show_close(false)
}
/// Set to as a alter dialog, with OK button.
///
/// See also [`Self::confirm`]
pub fn alert(self) -> Self {
self.footer(|ok, _, window, cx| vec![ok(window, cx)])
.overlay_closable(false)
.show_close(false)
}
/// Set the button props of the dialog.
pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
self.button_props = button_props;
@@ -207,30 +190,12 @@ impl Dialog {
self
}
/// Shift the dialog down from the window centre, defaults to `None`.
pub fn margin_top(mut self, margin_top: Pixels) -> Self {
self.margin_top = Some(margin_top);
self
}
/// Sets the width of the dialog, defaults to 480px.
pub fn width(mut self, width: Pixels) -> Self {
self.width = width;
self
}
/// Set the maximum width of the dialog, defaults to `None`.
pub fn max_w(mut self, max_width: Pixels) -> Self {
self.max_width = Some(max_width);
self
}
/// Set the overlay of the dialog, defaults to `true`.
pub fn overlay(mut self, overlay: bool) -> Self {
self.overlay = overlay;
self
}
/// Set the overlay closable of the dialog, defaults to `true`.
///
/// When the overlay is clicked, the dialog will be closed.
+3 -40
View File
@@ -1,14 +1,12 @@
use std::str::FromStr;
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement,
RenderOnce, StyleRefinement, Styled, Window,
AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
StyleRefinement, Styled, Window, div, relative,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{v_flex, StyledExt as _};
use crate::{StyledExt as _, v_flex};
/// The variant of the GroupBox.
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
@@ -37,27 +35,6 @@ pub trait GroupBoxVariants: Sized {
}
}
impl GroupBoxVariant {
/// Convert the GroupBoxVariant to a string.
pub const fn as_str(&self) -> &str {
match self {
GroupBoxVariant::Normal => "normal",
GroupBoxVariant::Fill => "fill",
}
}
}
impl FromStr for GroupBoxVariant {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"fill" => Ok(GroupBoxVariant::Fill),
_ => Ok(GroupBoxVariant::Normal),
}
}
}
/// GroupBox is a styled container element that with
/// an optional title to groups related content together.
#[derive(IntoElement)]
@@ -98,20 +75,6 @@ impl GroupBox {
self.title = Some(title.into_any_element());
self
}
/// Set the style of the title of the group box to override the default style, default is None.
#[must_use]
pub fn title_style(mut self, style: StyleRefinement) -> Self {
self.title_style = style;
self
}
/// Set the style of the content of the group box to override the default style, default is None.
#[must_use]
pub fn content_style(mut self, style: StyleRefinement) -> Self {
self.content_style = style;
self
}
}
impl Default for GroupBox {
+5 -30
View File
@@ -1,19 +1,14 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
AnyElement, App, Context, Hsla, IntoElement, Radians, Render, RenderOnce, SharedString,
StyleRefinement, Styled, Svg, Transformation, Window, svg,
};
use theme::ActiveTheme;
use crate::{Sizable, Size};
pub trait IconNamed {
/// Returns the embedded path of the icon.
fn path(self) -> SharedString;
}
impl<T: IconNamed> From<T> for Icon {
fn from(value: T) -> Self {
impl From<IconName> for Icon {
fn from(value: IconName) -> Self {
Icon::build(value)
}
}
@@ -95,13 +90,6 @@ pub enum IconName {
}
impl IconName {
/// Return the icon as a Entity<Icon>
pub fn view(self, cx: &mut App) -> Entity<Icon> {
Icon::build(self).view(cx)
}
}
impl IconNamed for IconName {
fn path(self) -> SharedString {
match self {
Self::ArrowLeft => "icons/arrow-left.svg",
@@ -232,7 +220,7 @@ impl Icon {
icon.into()
}
fn build(name: impl IconNamed) -> Self {
fn build(name: IconName) -> Self {
Self::default().path(name.path())
}
@@ -244,11 +232,6 @@ impl Icon {
self
}
/// Create a new view for the icon
pub fn view(self, cx: &mut App) -> Entity<Icon> {
cx.new(|_| self)
}
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
self.base = self.base.with_transformation(transformation);
self
@@ -257,14 +240,6 @@ impl Icon {
pub fn empty() -> Self {
Self::default()
}
/// Rotate the icon by the given angle
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
self.base = self
.base
.with_transformation(Transformation::rotate(radians));
self
}
}
impl Styled for Icon {
+3 -9
View File
@@ -1,10 +1,9 @@
use instant::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, ease_in_out, percentage, Animation, AnimationExt as _, App, Hsla, IntoElement,
ParentElement, RenderOnce, Styled as _, Transformation, Window,
Animation, AnimationExt as _, App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _,
Transformation, Window, div, ease_in_out, percentage,
};
use instant::Duration;
use crate::{Icon, IconName, Sizable, Size};
@@ -26,11 +25,6 @@ impl Indicator {
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = icon.into();
self
}
pub fn color(mut self, color: Hsla) -> Self {
self.color = Some(color);
self
+1 -25
View File
@@ -2,7 +2,7 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, DefiniteLength, Edges, Entity, Hsla, InteractiveElement as _, IntoElement,
MouseButton, ParentElement as _, Pixels, Rems, RenderOnce, StyleRefinement, Styled, TextAlign,
Window, div, px, relative,
Window, div, px,
};
use gpui_base::InputBase;
use gpui_base::input::{InputBaseState, InputEditorStyle, InputMode, InputModeKind, TextareaMode};
@@ -134,18 +134,6 @@ impl<M: InputModeKind> Input<M> {
self
}
/// Set full height of the input (Multi-line only).
pub fn h_full(mut self) -> Self {
self.height = Some(relative(1.));
self
}
/// Set height of the input (Multi-line only).
pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
self.height = Some(height.into());
self
}
/// Set the appearance of the input field, if false the input field will no border, background.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
@@ -158,24 +146,12 @@ impl<M: InputModeKind> Input<M> {
self
}
/// Set to enable toggle button for password mask state.
pub fn mask_toggle(mut self) -> Self {
self.mask_toggle = true;
self
}
/// Set to disable the input field.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set the tab index for the input, default is 0.
pub fn tab_index(mut self, index: isize) -> Self {
self.tab_index = index;
self
}
fn render_toggle_mask_button(state: &Entity<InputBaseState<M>>) -> impl IntoElement {
Button::new("toggle-mask")
.icon(IconName::Eye)
+1 -2
View File
@@ -7,7 +7,7 @@ use gpui::{
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, Disableable, StyledExt};
use crate::{Disableable, StyledExt, h_flex};
#[derive(IntoElement)]
pub(crate) struct MenuItemElement {
@@ -61,7 +61,6 @@ impl MenuItemElement {
}
/// Set a handler for when the mouse enters the MenuItem.
#[allow(unused)]
pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Some(Box::new(handler));
self
+1 -167
View File
@@ -85,23 +85,6 @@ impl PopupMenuItem {
}
}
/// Create a new menu item with custom element render.
#[inline]
pub fn element<F, E>(builder: F) -> Self
where
F: Fn(&mut Window, &mut App) -> E + 'static,
E: IntoElement,
{
PopupMenuItem::ElementItem {
icon: None,
disabled: false,
checked: false,
action: None,
render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
handler: None,
}
}
/// Create a new submenu item that opens another popup menu.
#[inline]
pub fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
@@ -214,21 +197,6 @@ impl PopupMenuItem {
self
}
/// Create a link menu item.
#[inline]
pub fn link(label: impl Into<SharedString>, href: impl Into<String>) -> Self {
let href = href.into();
PopupMenuItem::Item {
icon: None,
label: label.into(),
disabled: false,
checked: false,
action: None,
is_link: true,
handler: Some(Rc::new(move |_, _, cx| cx.open_url(&href))),
}
}
#[inline]
fn is_clickable(&self) -> bool {
!matches!(self, PopupMenuItem::Separator)
@@ -334,28 +302,12 @@ impl PopupMenu {
cx.new(|cx| f(Self::new(cx), window, cx))
}
/// Set the focus handle of Entity to handle actions.
///
/// When the menu is dismissed or before an action is triggered, the focus will be returned to this handle.
///
/// Then the action will be dispatched to this handle.
pub fn action_context(mut self, handle: FocusHandle) -> Self {
self.action_context = Some(handle);
self
}
/// Set min width of the popup menu, default is 120px
pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
self.min_width = Some(width.into());
self
}
/// Set max width of the popup menu, default is 500px
pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
self.max_width = Some(width.into());
self
}
/// Set max height of the popup menu, default is half of the window height
pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
self.max_height = Some(height.into());
@@ -376,34 +328,11 @@ impl PopupMenu {
self
}
/// Set the side to show check icon, default is `Side::Left`.
pub fn check_side(mut self, side: Side) -> Self {
self.check_side = side;
self
}
/// Set the menu to show external link icon, default is true.
pub fn external_link_icon(mut self, visible: bool) -> Self {
self.external_link_icon = visible;
self
}
/// Add Menu Item
pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.menu_with_disabled(label, action, false)
}
/// Add Menu Item with enable state
pub fn menu_with_enable(
mut self,
label: impl Into<SharedString>,
action: Box<dyn Action>,
enable: bool,
) -> Self {
self.add_menu_item(label, None, action, !enable, false);
self
}
/// Add Menu Item with disabled state
pub fn menu_with_disabled(
mut self,
@@ -421,70 +350,14 @@ impl PopupMenu {
self
}
/// Add Menu to open link
pub fn link(self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
self.link_with_disabled(label, href, false)
}
/// Add Menu to open link with disabled state
pub fn link_with_disabled(
mut self,
label: impl Into<SharedString>,
href: impl Into<String>,
disabled: bool,
) -> Self {
let href = href.into();
self.menu_items
.push(PopupMenuItem::link(label, href).disabled(disabled));
self
}
/// Add Menu to open link
pub fn link_with_icon(
self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
href: impl Into<String>,
) -> Self {
self.link_with_icon_and_disabled(label, icon, href, false)
}
/// Add Menu to open link with icon and disabled state
fn link_with_icon_and_disabled(
mut self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
href: impl Into<String>,
disabled: bool,
) -> Self {
let href = href.into();
self.menu_items.push(
PopupMenuItem::link(label, href)
.icon(icon)
.disabled(disabled),
);
self
}
/// Add Menu Item with Icon.
pub fn menu_with_icon(
self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
action: Box<dyn Action>,
) -> Self {
self.menu_with_icon_and_disabled(label, icon, action, false)
}
/// Add Menu Item with Icon and disabled state
pub fn menu_with_icon_and_disabled(
mut self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
action: Box<dyn Action>,
disabled: bool,
) -> Self {
self.add_menu_item(label, Some(icon.into()), action, disabled, false);
self.add_menu_item(label, Some(icon.into()), action, false, false);
self
}
@@ -510,41 +383,6 @@ impl PopupMenu {
self
}
/// Add Menu Item with custom element render with check state
pub fn menu_element_with_check<F, E>(
self,
checked: bool,
action: Box<dyn Action>,
builder: F,
) -> Self
where
F: Fn(&mut Window, &mut App) -> E + 'static,
E: IntoElement,
{
self.menu_element_with_check_and_disabled(checked, action, false, builder)
}
/// Add Menu Item with custom element render with check state and disabled state
fn menu_element_with_check_and_disabled<F, E>(
mut self,
checked: bool,
action: Box<dyn Action>,
disabled: bool,
builder: F,
) -> Self
where
F: Fn(&mut Window, &mut App) -> E + 'static,
E: IntoElement,
{
self.menu_items.push(
PopupMenuItem::element(builder)
.action(action)
.checked(checked)
.disabled(disabled),
);
self
}
/// Add a separator Menu Item
pub fn separator(mut self) -> Self {
if self.menu_items.is_empty() {
@@ -636,10 +474,6 @@ impl PopupMenu {
None
}
pub fn is_empty(&self) -> bool {
self.menu_items.is_empty()
}
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
self.menu_items
.iter()
-31
View File
@@ -156,13 +156,6 @@ impl Notification {
self
}
/// Create an info notification with the given message.
pub fn info(message: impl Into<SharedString>) -> Self {
Self::new()
.message(message)
.with_kind(NotificationKind::Info)
}
/// Create a success notification with the given message.
pub fn success(message: impl Into<SharedString>) -> Self {
Self::new()
@@ -209,14 +202,6 @@ impl Notification {
self
}
/// Set the icon of the notification.
///
/// If icon is None, the notification will use the default icon of the type.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
/// Set the type of the notification, default is NotificationType::Info.
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
self.kind = Some(kind);
@@ -229,15 +214,6 @@ impl Notification {
self
}
/// Set the click callback of the notification.
pub fn on_click(
mut self,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Rc::new(on_click));
self
}
/// Set the action button of the notification.
///
/// When an action is set, the notification will not autohide.
@@ -611,13 +587,6 @@ impl NotificationList {
}
cx.notify();
}
pub fn notifications(&self) -> Vec<Entity<Notification>> {
self.notifications
.iter()
.map(|(_, note, _)| note.clone())
.collect()
}
}
impl Render for NotificationList {
-17
View File
@@ -31,23 +31,6 @@ pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Eleme
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
}
/// Adds a horizontal scrollbar to the element.
#[track_caller]
fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
#[track_caller]
fn overflow_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Both)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
#[track_caller]
fn overflow_x_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
#[track_caller]
-5
View File
@@ -63,11 +63,6 @@ impl Switch {
self.on_click = Some(Rc::new(handler));
self
}
pub fn label_side(mut self, label_side: Side) -> Self {
self.label_side = label_side;
self
}
}
impl Sizable for Switch {
+5 -45
View File
@@ -2,17 +2,15 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
Anchor, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
Window, div, px,
AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div,
px,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use super::Tab;
use crate::button::{Button, ButtonVariants as _};
use crate::menu::{DropdownMenu as _, PopupMenuItem};
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex};
use crate::{Selectable, StyledExt, h_flex};
/// A TabBar element that contains multiple [`Tab`] items.
#[derive(IntoElement)]
@@ -25,7 +23,6 @@ pub struct TabBar {
children: SmallVec<[Tab; 2]>,
last_empty_space: AnyElement,
selected_index: Option<usize>,
menu: bool,
segmented: bool,
#[allow(clippy::type_complexity)]
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
@@ -44,7 +41,6 @@ impl TabBar {
last_empty_space: div().w_3().into_any_element(),
selected_index: None,
on_click: None,
menu: false,
segmented: false,
}
}
@@ -55,12 +51,6 @@ impl TabBar {
self
}
/// Set whether to show the menu button when tabs overflow, default is false.
pub fn menu(mut self, menu: bool) -> Self {
self.menu = menu;
self
}
/// Track the scroll of the TabBar.
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle.clone());
@@ -123,9 +113,6 @@ impl Styled for TabBar {
impl RenderOnce for TabBar {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let mut item_labels = Vec::new();
let selected_index = self.selected_index;
let on_click = self.on_click.clone();
let segmented = self.segmented;
let has_prefix = self.prefix.is_some();
@@ -155,7 +142,6 @@ impl RenderOnce for TabBar {
})
.gap_1()
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
item_labels.push((child.label.clone(), child.disabled));
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
child
.ix(ix)
@@ -168,36 +154,10 @@ impl RenderOnce for TabBar {
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
})
}))
.when(self.suffix.is_some() || self.menu, |this| {
.when(self.suffix.is_some(), |this| {
this.child(self.last_empty_space)
}),
)
.when(self.menu, |this| {
this.child(
Button::new("more")
.xsmall()
.ghost()
.icon(IconName::ChevronDown)
.dropdown_menu(move |mut this, _, _| {
this = this.scrollable(true);
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
this = this.item(
PopupMenuItem::new(label.clone().unwrap_or_default())
.checked(selected_index == Some(ix))
.disabled(*disabled)
.when_some(on_click.clone(), |this, on_click| {
this.on_click(move |_, window, cx| {
on_click(&ix, window, cx)
})
}),
)
}
this
})
.anchor(Anchor::TopRight),
)
})
.when_some(self.suffix, |this, suffix| this.child(suffix))
}
}
+1 -11
View File
@@ -1,6 +1,4 @@
use std::rc::Rc;
use gpui::{App, ElementId, Entity, Window};
use gpui::{App, ElementId, Window};
use crate::Root;
use crate::dialog::Dialog;
@@ -19,9 +17,6 @@ pub trait WindowExtension: Sized {
/// Closes all active Dialogs.
fn close_all_dialogs(&mut self, cx: &mut App);
/// Returns number of notifications.
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
/// Pushes a notification to the notification list.
fn push_notification<T>(&mut self, note: T, cx: &mut App)
where
@@ -102,9 +97,4 @@ impl WindowExtension for Window {
root.clear_notifications(window, cx);
})
}
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
let entity = Root::read(self, cx).notification.clone();
Rc::new(entity.read(cx).notifications())
}
}
-1
View File
@@ -22,7 +22,6 @@ gpui.workspace = true
nostr-sdk.workspace = true
instant.workspace = true
nostr-connect.workspace = true
browser-signer-proxy = { path = "../browser-signer-proxy" }
anyhow.workspace = true
serde.workspace = true
+2 -7
View File
@@ -50,17 +50,12 @@ impl Sidebar {
.compact()
.transparent()
.dropdown_menu(move |this, _window, _cx| {
this.min_w(px(256.))
.menu_with_icon(
this.menu_with_icon(
"Profile",
IconName::Profile,
Box::new(Command::ShowProfile),
)
.menu_with_icon(
"Relays",
IconName::Relay,
Box::new(Command::ShowRelayList),
)
.menu_with_icon("Relays", IconName::Relay, Box::new(Command::ShowRelayList))
.separator()
.menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme))
.menu_with_icon(
-1
View File
@@ -31,7 +31,6 @@ assets = { path = "../crates/assets" }
workspace = { path = "../crates/workspace" }
ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" }
common = { path = "../crates/common" }
state = { path = "../crates/state" }
nip4e = { path = "../crates/nip4e" }
chat = { path = "../crates/chat" }
-2
View File
@@ -9,10 +9,8 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
workspace = { path = "../crates/workspace" }
assets = { path = "../crates/assets" }
ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" }
common = { path = "../crates/common" }
state = { path = "../crates/state" }
nip4e = { path = "../crates/nip4e" }
chat = { path = "../crates/chat" }