update
This commit is contained in:
+183
-94
@@ -11,12 +11,12 @@ use gpui::{
|
|||||||
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use instant::Duration;
|
use instant::{Duration, Instant};
|
||||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
|
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -29,7 +29,10 @@ pub use state::FileAttachment;
|
|||||||
|
|
||||||
use crate::search::MessageSearchIndex;
|
use crate::search::MessageSearchIndex;
|
||||||
|
|
||||||
/// A static keypair used only for signing locally-cached rumor events.
|
/// Upper bound on signals applied to the UI in a single update
|
||||||
|
const MAX_SIGNAL_BATCH: usize = 256;
|
||||||
|
|
||||||
|
/// Keypair used only for identifying locally-cached rumor events.
|
||||||
static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
|
static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
@@ -74,6 +77,13 @@ impl Signal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which signer to attempt when unwrapping a gift wrap
|
||||||
|
enum UnwrapRoute {
|
||||||
|
User,
|
||||||
|
Device,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
/// Chat Registry
|
/// Chat Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
@@ -216,32 +226,34 @@ impl ChatRegistry {
|
|||||||
let tx = self.signal_tx.clone();
|
let tx = self.signal_tx.clone();
|
||||||
let rx = self.signal_rx.clone();
|
let rx = self.signal_rx.clone();
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
|
||||||
self.notification_listener = Some(cx.background_spawn(async move {
|
self.notification_listener = Some(cx.background_spawn(async move {
|
||||||
let mut notifications = client.notifications();
|
|
||||||
let mut processed_events = HashSet::new();
|
|
||||||
const MAX_PROCESSED: usize = 10_000;
|
const MAX_PROCESSED: usize = 10_000;
|
||||||
|
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events = HashSet::new();
|
||||||
|
|
||||||
|
let mut user_key = signer.get_public_key_async().await.ok();
|
||||||
|
let device_key = match device_signer.as_ref() {
|
||||||
|
Some(device) => device.get_public_key_async().await.ok(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut last_yield = Instant::now();
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
|
if last_yield.elapsed() >= Duration::from_millis(8) {
|
||||||
|
executor.timer(Duration::from_millis(0)).await;
|
||||||
|
last_yield = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
let ClientNotification::Message { message, relay_url } = notification else {
|
let ClientNotification::Message { message, relay_url } = notification else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
match *message {
|
match *message {
|
||||||
RelayMessage::Event {
|
RelayMessage::Event { event, .. } => {
|
||||||
subscription_id,
|
|
||||||
event,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let chat_sub = subscription_id.as_str() != sub_id1.as_str();
|
|
||||||
let device_sub = subscription_id.as_str() != sub_id2.as_str();
|
|
||||||
|
|
||||||
// Concord wraps are also kind 1059.
|
|
||||||
//
|
|
||||||
// Only the two gift wrap subscriptions carry NIP-59 wraps for this account.
|
|
||||||
if event.kind == Kind::GiftWrap && chat_sub && device_sub {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune the dedup set before it grows unbounded
|
// Prune the dedup set before it grows unbounded
|
||||||
if processed_events.len() >= MAX_PROCESSED {
|
if processed_events.len() >= MAX_PROCESSED {
|
||||||
processed_events.clear();
|
processed_events.clear();
|
||||||
@@ -253,8 +265,10 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Handle msg relays event to determine when the app is ready to subscribe
|
// Handle msg relays event to determine when the app is ready to subscribe
|
||||||
if event.kind == Kind::InboxRelays {
|
if event.kind == Kind::InboxRelays {
|
||||||
let current_user = signer.get_public_key_async().await?;
|
if user_key.is_none() {
|
||||||
if event.pubkey == current_user {
|
user_key = signer.get_public_key_async().await.ok();
|
||||||
|
}
|
||||||
|
if Some(event.pubkey) == user_key {
|
||||||
tx.send_async(Signal::InboxReady).await?;
|
tx.send_async(Signal::InboxReady).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,15 +284,39 @@ impl ChatRegistry {
|
|||||||
seen.entry(event.id).or_default().insert(relay_url);
|
seen.entry(event.id).or_default().insert(relay_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route to the signer matching the recipient tag
|
||||||
|
// so a wrap is never decrypted twice.
|
||||||
|
let recipient = event.tags.public_keys().next();
|
||||||
|
let route = match recipient {
|
||||||
|
Some(key) if Some(key) == device_key => UnwrapRoute::Device,
|
||||||
|
Some(key) if Some(key) == user_key => UnwrapRoute::User,
|
||||||
|
Some(_) if user_key.is_some() && device_key.is_some() => continue,
|
||||||
|
_ => UnwrapRoute::Both,
|
||||||
|
};
|
||||||
|
|
||||||
// Extract the rumor from the gift wrap event
|
// Extract the rumor from the gift wrap event
|
||||||
match extract_rumor(
|
let result = match route {
|
||||||
&client,
|
UnwrapRoute::User => {
|
||||||
&signer,
|
extract_rumor(&client, &signer, None, event.as_ref()).await
|
||||||
device_signer.as_ref(),
|
}
|
||||||
event.as_ref(),
|
UnwrapRoute::Device => match device_signer.as_ref() {
|
||||||
)
|
Some(device) => {
|
||||||
.await
|
extract_rumor(&client, device, None, event.as_ref()).await
|
||||||
{
|
}
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
UnwrapRoute::Both => {
|
||||||
|
extract_rumor(
|
||||||
|
&client,
|
||||||
|
&signer,
|
||||||
|
device_signer.as_ref(),
|
||||||
|
event.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
Ok(rumor) => {
|
Ok(rumor) => {
|
||||||
let Some(rumor_id) = rumor.id else {
|
let Some(rumor_id) = rumor.id else {
|
||||||
log::error!("Rumor missing id after ensure_id");
|
log::error!("Rumor missing id after ensure_id");
|
||||||
@@ -317,35 +355,56 @@ impl ChatRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
let tracking = self.tracking.clone();
|
||||||
while let Ok(message) = rx.recv_async().await {
|
|
||||||
match message {
|
|
||||||
Signal::Message(message) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.new_message(message, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Signal::InboxReady => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.get_messages(cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Signal::Eose => {
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.tracking.store(false, Ordering::Release);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||||
this.get_rooms(cx);
|
while let Ok(first) = rx.recv_async().await {
|
||||||
})?;
|
// Batch whatever has already arrived so the UI updates once per burst
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let mut inbox_ready = false;
|
||||||
|
let mut eose = false;
|
||||||
|
|
||||||
|
let mut pending = Some(first);
|
||||||
|
while let Some(signal) = pending.take() {
|
||||||
|
match signal {
|
||||||
|
Signal::Message(message) => messages.push(message),
|
||||||
|
Signal::InboxReady => inbox_ready = true,
|
||||||
|
Signal::Eose => eose = true,
|
||||||
|
Signal::Error(failed) => errors.push(failed),
|
||||||
}
|
}
|
||||||
Signal::Error(failed) => {
|
|
||||||
trash.update(cx, |this, cx| {
|
if messages.len() + errors.len() >= MAX_SIGNAL_BATCH {
|
||||||
this.insert(failed);
|
break;
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
pending = rx.try_recv().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if inbox_ready {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.get_messages(cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !messages.is_empty() {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.new_messages(messages, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
trash.update(cx, |trash, cx| {
|
||||||
|
trash.extend(errors);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if eose && tracking.swap(false, Ordering::AcqRel) {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.get_rooms(cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -669,6 +728,7 @@ impl ChatRegistry {
|
|||||||
self.rooms.clear();
|
self.rooms.clear();
|
||||||
self.room_index.clear();
|
self.room_index.clear();
|
||||||
self.search.write().unwrap().clear();
|
self.search.write().unwrap().clear();
|
||||||
|
self.tracking.store(true, Ordering::Release);
|
||||||
self.trash.update(cx, |this, cx| {
|
self.trash.update(cx, |this, cx| {
|
||||||
this.clear();
|
this.clear();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -748,6 +808,7 @@ impl ChatRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let search = self.search.clone();
|
let search = self.search.clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
@@ -775,7 +836,15 @@ impl ChatRegistry {
|
|||||||
let events = client.database().query(filter).await?;
|
let events = client.database().query(filter).await?;
|
||||||
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
||||||
|
|
||||||
|
// Keep the single-threaded web event loop responsive during large rescans
|
||||||
|
let mut last_yield = Instant::now();
|
||||||
|
|
||||||
for raw in events.into_iter() {
|
for raw in events.into_iter() {
|
||||||
|
if last_yield.elapsed() >= Duration::from_millis(8) {
|
||||||
|
executor.timer(Duration::from_millis(0)).await;
|
||||||
|
last_yield = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
||||||
&& rumor.tags.public_keys().next().is_some()
|
&& rumor.tags.public_keys().next().is_some()
|
||||||
{
|
{
|
||||||
@@ -811,12 +880,17 @@ impl ChatRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a nostr event into a message and push it to the belonging room
|
/// Parse nostr events into messages and push them to the belonging rooms
|
||||||
///
|
///
|
||||||
/// - If the room doesn't exist, it will be created.
|
/// - If a room doesn't exist, it will be created.
|
||||||
/// - Updates room ordering based on the most recent messages.
|
/// - Room ordering is updated once after the whole batch.
|
||||||
pub(crate) fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
pub(crate) fn new_messages(&mut self, messages: Vec<NewMessage>, cx: &mut Context<Self>) {
|
||||||
self.search.write().unwrap().insert(&message.rumor);
|
{
|
||||||
|
let mut search = self.search.write().unwrap();
|
||||||
|
for message in &messages {
|
||||||
|
search.insert(&message.rumor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
@@ -824,37 +898,42 @@ impl ChatRegistry {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.room_index.get(&message.room).cloned() {
|
for message in messages {
|
||||||
Some(room) => {
|
match self.room_index.get(&message.room).cloned() {
|
||||||
room.update(cx, |this, cx| {
|
Some(room) => {
|
||||||
if this.kind == RoomKind::Request
|
room.update(cx, |this, cx| {
|
||||||
&& (message.rumor.pubkey == public_key || this.auto_trusted(public_key, cx))
|
if this.kind == RoomKind::Request
|
||||||
{
|
&& (message.rumor.pubkey == public_key
|
||||||
this.set_ongoing(cx);
|
|| this.auto_trusted(public_key, cx))
|
||||||
}
|
{
|
||||||
this.push_message(message, cx);
|
this.set_ongoing(cx);
|
||||||
});
|
}
|
||||||
self.sort(cx);
|
this.push_message(message, cx);
|
||||||
}
|
});
|
||||||
None => {
|
}
|
||||||
// Push the new room to the front of the list
|
None => {
|
||||||
self.add_room(message.rumor, cx);
|
// Push the new room to the front of the list
|
||||||
|
self.add_room(message.rumor, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.sort(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a nostr event into a message and push it to the belonging room
|
||||||
|
pub(crate) fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
||||||
|
self.new_messages(vec![message], cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor<S, D>(
|
async fn extract_rumor(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &S,
|
signer: &UniversalSigner,
|
||||||
device_signer: Option<&D>,
|
device_signer: Option<&UniversalSigner>,
|
||||||
gift_wrap: &Event,
|
gift_wrap: &Event,
|
||||||
) -> Result<UnsignedEvent, Error>
|
) -> Result<UnsignedEvent, Error> {
|
||||||
where
|
|
||||||
S: AsyncNip44 + ?Sized,
|
|
||||||
D: AsyncNip44 + ?Sized,
|
|
||||||
{
|
|
||||||
// Try to get cached rumor first
|
// Try to get cached rumor first
|
||||||
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
|
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
|
||||||
return Ok(rumor);
|
return Ok(rumor);
|
||||||
@@ -888,10 +967,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to unwrap a gift wrap event with a given signer.
|
/// Attempts to unwrap a gift wrap event with a given signer.
|
||||||
async fn try_unwrap_with<S>(signer: &S, gift_wrap: &Event) -> Result<UnwrappedGift, Error>
|
async fn try_unwrap_with(
|
||||||
where
|
signer: &UniversalSigner,
|
||||||
S: AsyncNip44 + ?Sized,
|
gift_wrap: &Event,
|
||||||
{
|
) -> Result<UnwrappedGift, Error> {
|
||||||
// Get the sealed event
|
// Get the sealed event
|
||||||
let seal = signer
|
let seal = signer
|
||||||
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
||||||
@@ -926,17 +1005,27 @@ where
|
|||||||
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
||||||
let room_id = rumor.uniq_id().to_string();
|
let room_id = rumor.uniq_id().to_string();
|
||||||
|
|
||||||
let tags = vec![
|
let tags = Tags::from_list(vec![
|
||||||
Tag::identifier(id),
|
Tag::identifier(id),
|
||||||
Tag::public_key(rumor.pubkey),
|
Tag::public_key(rumor.pubkey),
|
||||||
Tag::custom("r", [room_id]),
|
Tag::custom("r", [room_id]),
|
||||||
Tag::custom("k", [rumor.kind.to_string()]),
|
Tag::custom("k", [rumor.kind.to_string()]),
|
||||||
];
|
]);
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, rumor.as_json())
|
let public_key = LOCAL_KEYS.public_key();
|
||||||
.tags(tags)
|
let created_at = Timestamp::now();
|
||||||
.finalize_async(&*LOCAL_KEYS)
|
let kind = Kind::ApplicationSpecificData;
|
||||||
.await?;
|
let content = rumor.as_json();
|
||||||
|
|
||||||
|
let event = Event::new(
|
||||||
|
EventId::compute(&public_key, &created_at, &kind, &tags, &content),
|
||||||
|
public_key,
|
||||||
|
created_at,
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
content,
|
||||||
|
Signature::from_byte_array([0u8; 64]),
|
||||||
|
);
|
||||||
|
|
||||||
client.database().save_event(&event).await?;
|
client.database().save_event(&event).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -477,16 +477,13 @@ impl NostrRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect the browser extension signer (NIP-07), remembering the choice so
|
/// Connect the browser extension signer (NIP-07).
|
||||||
/// reloads reconnect automatically
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn connect_extension(&mut self, cx: &mut Context<Self>) {
|
pub fn connect_extension(&mut self, cx: &mut Context<Self>) {
|
||||||
let signer = match BrowserSigner::new() {
|
let signer = match BrowserSigner::new() {
|
||||||
Ok(signer) => signer,
|
Ok(signer) => signer,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
cx.emit(StateEvent::error(format!(
|
cx.emit(StateEvent::error(format!("No Nostr extension found: {e}.")));
|
||||||
"No Nostr extension found: {e}. Install a NIP-07 extension like Alby or nos2x."
|
|
||||||
)));
|
|
||||||
cx.emit(StateEvent::NoSigner);
|
cx.emit(StateEvent::NoSigner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -495,7 +492,6 @@ impl NostrRegistry {
|
|||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match signer.get_public_key_async().await {
|
match signer.get_public_key_async().await {
|
||||||
Ok(public_key) => {
|
Ok(public_key) => {
|
||||||
// Remember the choice so reloads reconnect automatically
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
|
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
|
||||||
this.signer.swap_inner(signer);
|
this.signer.swap_inner(signer);
|
||||||
@@ -507,8 +503,6 @@ impl NostrRegistry {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Drop the remembered choice so the next visit does not
|
|
||||||
// retry a signer that is no longer answering
|
|
||||||
this.update(cx, |_, cx| {
|
this.update(cx, |_, cx| {
|
||||||
credentials::delete(cx).detach_and_log_err(cx);
|
credentials::delete(cx).detach_and_log_err(cx);
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
|
|||||||
+9
-17
@@ -1,5 +1,8 @@
|
|||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
||||||
@@ -42,36 +45,29 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
// Sets the panic hook and GPUI's console logger
|
// Sets the panic hook and GPUI's console logger
|
||||||
gpui_platform::web_init();
|
gpui_platform::web_init();
|
||||||
|
|
||||||
// A single-threaded platform that draws emoji and CJK graphemes missing
|
|
||||||
// from the bundled fonts with the browser's own fonts, plus a fetch-based
|
|
||||||
// HTTP client so remote images and content work like on desktop
|
|
||||||
let platform = std::rc::Rc::new(gpui_web::WebPlatform::new_with_backend_and_font_fallback(
|
let platform = std::rc::Rc::new(gpui_web::WebPlatform::new_with_backend_and_font_fallback(
|
||||||
false,
|
false,
|
||||||
gpui_web::WebBackendPreference::Auto,
|
gpui_web::WebBackendPreference::Auto,
|
||||||
gpui_web::CanvasFontFallback::EmojiAndCjk,
|
gpui_web::CanvasFontFallback::EmojiAndCjk,
|
||||||
));
|
));
|
||||||
let http_client = std::sync::Arc::new(platform.fetch_http_client());
|
|
||||||
|
let http_client = Arc::new(platform.fetch_http_client());
|
||||||
|
|
||||||
let app = Application::with_platform(platform)
|
let app = Application::with_platform(platform)
|
||||||
.with_http_client(http_client)
|
.with_http_client(http_client)
|
||||||
.with_assets(assets::CombinedAssets);
|
.with_assets(assets::CombinedAssets);
|
||||||
|
|
||||||
APPLICATION.with(|application| {
|
APPLICATION.with(|application| {
|
||||||
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
||||||
// Initialize the framework before anything renders
|
|
||||||
gpui_kit::init(cx);
|
|
||||||
|
|
||||||
// The web text system starts with no fonts, and GPUI panics when
|
|
||||||
// it lays out text in a family that does not exist. Inter is both
|
|
||||||
// bundled and named on the theme (including both mode configs, so
|
|
||||||
// theme reloads keep it) before any text is laid out
|
|
||||||
load_embedded_fonts(cx);
|
load_embedded_fonts(cx);
|
||||||
apply_web_fonts(cx);
|
apply_web_fonts(cx);
|
||||||
|
|
||||||
|
// Initialize the framework before anything renders
|
||||||
|
gpui_kit::init(cx);
|
||||||
|
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
|
|
||||||
// There is no native theme on the web; apply the persisted
|
|
||||||
// appearance directly, then the user's theme tuning
|
|
||||||
apply_appearance(settings::persisted_appearance_mode(), cx);
|
apply_appearance(settings::persisted_appearance_mode(), cx);
|
||||||
settings::AppSettings::apply_theme_style(cx);
|
settings::AppSettings::apply_theme_style(cx);
|
||||||
|
|
||||||
@@ -121,10 +117,6 @@ fn load_embedded_fonts(cx: &mut App) {
|
|||||||
.expect("Failed to load bundled fonts");
|
.expect("Failed to load bundled fonts");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name Inter on the theme and both mode configs so every text run renders in
|
|
||||||
/// it and theme reloads keep it. Without an explicit family the theme falls
|
|
||||||
/// back to `.SystemUIFont`, which the web platform resolves to a family that
|
|
||||||
/// is not bundled
|
|
||||||
fn apply_web_fonts(cx: &mut App) {
|
fn apply_web_fonts(cx: &mut App) {
|
||||||
use gpui_kit::component::Theme;
|
use gpui_kit::component::Theme;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user