update community
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use concord::cord02::ControlFold;
|
use concord::cord02::{ControlFold, ImageRef};
|
||||||
use concord::store::{ChannelKeyRef, CommunityState};
|
use concord::store::{ChannelKeyRef, CommunityState};
|
||||||
use concord::{ChannelId, CommunityId, Epoch};
|
use concord::{ChannelId, CommunityId, Epoch};
|
||||||
use gpui::{AppContext, Context, EventEmitter, Task};
|
use gpui::{AppContext, Context, EventEmitter, Task};
|
||||||
@@ -45,8 +46,11 @@ pub struct Community {
|
|||||||
state: CommunityState,
|
state: CommunityState,
|
||||||
control: ControlFold,
|
control: ControlFold,
|
||||||
members: BTreeSet<PublicKey>,
|
members: BTreeSet<PublicKey>,
|
||||||
|
icon: Option<PathBuf>,
|
||||||
|
icon_ref: Option<ImageRef>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
refresh_task: Option<Task<Result<()>>>,
|
refresh_task: Option<Task<Result<()>>>,
|
||||||
|
icon_task: Option<Task<Result<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<CommunityEvent> for Community {}
|
impl EventEmitter<CommunityEvent> for Community {}
|
||||||
@@ -57,8 +61,11 @@ impl Community {
|
|||||||
state,
|
state,
|
||||||
control: ControlFold::default(),
|
control: ControlFold::default(),
|
||||||
members: BTreeSet::new(),
|
members: BTreeSet::new(),
|
||||||
|
icon: None,
|
||||||
|
icon_ref: None,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
refresh_task: None,
|
refresh_task: None,
|
||||||
|
icon_task: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,16 +78,25 @@ impl Community {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
match &self.control.community {
|
if let Some(metadata) = &self.control.community {
|
||||||
Some(metadata) => metadata.name.clone(),
|
return metadata.name.clone();
|
||||||
None => self.state.id.to_hex(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.state
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.state.id.to_hex())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn control(&self) -> &ControlFold {
|
pub fn control(&self) -> &ControlFold {
|
||||||
&self.control
|
&self.control
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The community's icon, once downloaded and decrypted into a cache file.
|
||||||
|
pub fn icon(&self) -> Option<PathBuf> {
|
||||||
|
self.icon.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn members(&self) -> &BTreeSet<PublicKey> {
|
pub fn members(&self) -> &BTreeSet<PublicKey> {
|
||||||
&self.members
|
&self.members
|
||||||
}
|
}
|
||||||
@@ -121,6 +137,7 @@ impl Community {
|
|||||||
self.state = snapshot.state;
|
self.state = snapshot.state;
|
||||||
self.control = snapshot.control;
|
self.control = snapshot.control;
|
||||||
self.members = snapshot.members;
|
self.members = snapshot.members;
|
||||||
|
self.load_icon(cx);
|
||||||
cx.emit(CommunityEvent::Updated(self.state.id));
|
cx.emit(CommunityEvent::Updated(self.state.id));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -133,4 +150,37 @@ impl Community {
|
|||||||
self.refresh(cx);
|
self.refresh(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the folded icon into a local file.
|
||||||
|
fn load_icon(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let icon = self
|
||||||
|
.control
|
||||||
|
.community
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|metadata| metadata.icon.clone());
|
||||||
|
|
||||||
|
if self.icon_ref == icon {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.icon_ref = icon.clone();
|
||||||
|
self.icon = None;
|
||||||
|
|
||||||
|
let Some(icon) = icon else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.icon_task = Some(cx.spawn(async move |this, cx| {
|
||||||
|
match sync::resolve_icon(&icon, cx).await {
|
||||||
|
Ok(path) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.icon = Some(path);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(error) => log::warn!("community icon: {error}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,7 +357,6 @@ async fn subscribe(
|
|||||||
relays: &[RelayUrl],
|
relays: &[RelayUrl],
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
log::info!("community {id}: subscribing to {relays:?}");
|
|
||||||
client.unsubscribe(id).await?;
|
client.unsubscribe(id).await?;
|
||||||
|
|
||||||
for url in relays {
|
for url in relays {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use concord::cord01::KIND_WRAP;
|
use concord::cord01::KIND_WRAP;
|
||||||
use concord::cord02::list::{CommunityList, KIND_COMMUNITY_LIST};
|
use concord::cord02::list::{CommunityList, KIND_COMMUNITY_LIST};
|
||||||
use concord::cord02::{self, ControlFold};
|
use concord::cord02::{self, ControlFold, ImageRef};
|
||||||
use concord::cord04::AuthorityCitation;
|
use concord::cord04::AuthorityCitation;
|
||||||
use concord::cord04::roles::{Permissions, citation_ok};
|
use concord::cord04::roles::{Permissions, citation_ok};
|
||||||
use concord::derive::{
|
use concord::derive::{
|
||||||
@@ -11,6 +12,7 @@ use concord::derive::{
|
|||||||
};
|
};
|
||||||
use concord::store::{self, CommunityState};
|
use concord::store::{self, CommunityState};
|
||||||
use concord::{ChannelId, CommunityId, Epoch, GroupKey};
|
use concord::{ChannelId, CommunityId, Epoch, GroupKey};
|
||||||
|
use gpui::AsyncApp;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::UniversalSigner;
|
use state::UniversalSigner;
|
||||||
|
|
||||||
@@ -42,6 +44,15 @@ pub fn planes(state: &CommunityState) -> Result<Vec<Plane>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.control_pks.is_empty() {
|
||||||
|
let group = control_group_key(&state.community_root, &state.id, state.root_epoch)?;
|
||||||
|
planes.push(Plane {
|
||||||
|
kind: PlaneKind::Control(state.root_epoch),
|
||||||
|
address: group.pk(),
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let group = guestbook_group_key(&state.community_root, &state.id, state.root_epoch)?;
|
let group = guestbook_group_key(&state.community_root, &state.id, state.root_epoch)?;
|
||||||
planes.push(Plane {
|
planes.push(Plane {
|
||||||
kind: PlaneKind::Guestbook,
|
kind: PlaneKind::Guestbook,
|
||||||
@@ -85,6 +96,12 @@ pub fn community_of(subscription_id: &SubscriptionId) -> Option<CommunityId> {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download and decrypt a community icon into a content-addressed cache file.
|
||||||
|
pub async fn resolve_icon(icon: &ImageRef, cx: &AsyncApp) -> Result<PathBuf> {
|
||||||
|
let url = Url::parse(&icon.url).context("community icon url")?;
|
||||||
|
state::download_and_decrypt_to_cache(&url, &icon.key, &icon.nonce, &icon.hash, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
pub state: CommunityState,
|
pub state: CommunityState,
|
||||||
@@ -317,6 +334,10 @@ fn refresh(mut held: CommunityState, fresh: CommunityState) -> CommunityState {
|
|||||||
held.control_root = fresh.control_root;
|
held.control_root = fresh.control_root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(name) = fresh.name {
|
||||||
|
held.name = Some(name);
|
||||||
|
}
|
||||||
|
|
||||||
for (epoch, address) in fresh.control_pks {
|
for (epoch, address) in fresh.control_pks {
|
||||||
held.control_pks.insert(epoch, address);
|
held.control_pks.insert(epoch, address);
|
||||||
}
|
}
|
||||||
@@ -517,6 +538,7 @@ mod tests {
|
|||||||
|
|
||||||
let state = CommunityState {
|
let state = CommunityState {
|
||||||
id: CommunityId::from_bytes([0x42; 32]),
|
id: CommunityId::from_bytes([0x42; 32]),
|
||||||
|
name: Some("Anime and Manga".to_owned()),
|
||||||
owner,
|
owner,
|
||||||
owner_salt: [0x01; 32],
|
owner_salt: [0x01; 32],
|
||||||
community_root: [0x02; 32],
|
community_root: [0x02; 32],
|
||||||
@@ -548,9 +570,6 @@ mod tests {
|
|||||||
|
|
||||||
let planes = planes(&state).expect("planes");
|
let planes = planes(&state).expect("planes");
|
||||||
|
|
||||||
// Control at the root epoch, the guestbook, and the public channel. The
|
|
||||||
// private channel is skipped: its address derives from the granted key,
|
|
||||||
// not the community_root.
|
|
||||||
assert_eq!(planes.len(), 3);
|
assert_eq!(planes.len(), 3);
|
||||||
assert!(planes.iter().any(|plane| plane.address == control_pk));
|
assert!(planes.iter().any(|plane| plane.address == control_pk));
|
||||||
assert!(
|
assert!(
|
||||||
@@ -580,6 +599,7 @@ mod tests {
|
|||||||
fn held(id: CommunityId, control_pk: PublicKey) -> CommunityState {
|
fn held(id: CommunityId, control_pk: PublicKey) -> CommunityState {
|
||||||
CommunityState {
|
CommunityState {
|
||||||
id,
|
id,
|
||||||
|
name: Some("Anime and Manga".to_owned()),
|
||||||
owner: Keys::generate().public_key(),
|
owner: Keys::generate().public_key(),
|
||||||
owner_salt: [0x01; 32],
|
owner_salt: [0x01; 32],
|
||||||
community_root: [0x02; 32],
|
community_root: [0x02; 32],
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ pub struct ChannelKeyRef {
|
|||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CommunityState {
|
pub struct CommunityState {
|
||||||
pub id: CommunityId,
|
pub id: CommunityId,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
pub owner: PublicKey,
|
pub owner: PublicKey,
|
||||||
pub owner_salt: [u8; 32],
|
pub owner_salt: [u8; 32],
|
||||||
pub community_root: [u8; 32],
|
pub community_root: [u8; 32],
|
||||||
@@ -183,6 +185,7 @@ impl CommunityState {
|
|||||||
let mut channels = Vec::new();
|
let mut channels = Vec::new();
|
||||||
let mut heads = Vec::with_capacity(editions.len());
|
let mut heads = Vec::with_capacity(editions.len());
|
||||||
let mut relays = Vec::new();
|
let mut relays = Vec::new();
|
||||||
|
let mut name = None;
|
||||||
|
|
||||||
for edition in editions {
|
for edition in editions {
|
||||||
heads.push(EntityHead {
|
heads.push(EntityHead {
|
||||||
@@ -201,6 +204,7 @@ impl CommunityState {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|relay| RelayUrl::parse(relay).ok()),
|
.filter_map(|relay| RelayUrl::parse(relay).ok()),
|
||||||
);
|
);
|
||||||
|
name = label(&metadata.name);
|
||||||
}
|
}
|
||||||
vsk::CHANNEL_METADATA => {
|
vsk::CHANNEL_METADATA => {
|
||||||
let metadata: ChannelMetadata = serde_json::from_str(&edition.content)?;
|
let metadata: ChannelMetadata = serde_json::from_str(&edition.content)?;
|
||||||
@@ -228,6 +232,7 @@ impl CommunityState {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: genesis.identity.community_id,
|
id: genesis.identity.community_id,
|
||||||
|
name,
|
||||||
owner: genesis.identity.owner,
|
owner: genesis.identity.owner,
|
||||||
owner_salt: genesis.identity.owner_salt,
|
owner_salt: genesis.identity.owner_salt,
|
||||||
community_root: genesis.community_root,
|
community_root: genesis.community_root,
|
||||||
@@ -267,6 +272,7 @@ impl CommunityState {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: material.community_id,
|
id: material.community_id,
|
||||||
|
name: label(&material.name),
|
||||||
owner: material.owner,
|
owner: material.owner,
|
||||||
owner_salt: decode_hex_32(&material.owner_salt)?,
|
owner_salt: decode_hex_32(&material.owner_salt)?,
|
||||||
community_root: decode_hex_32(&material.community_root)?,
|
community_root: decode_hex_32(&material.community_root)?,
|
||||||
@@ -310,6 +316,10 @@ impl CommunityState {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|relay| RelayUrl::parse(relay).ok())
|
.filter_map(|relay| RelayUrl::parse(relay).ok())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
if let Some(name) = label(&community.name) {
|
||||||
|
self.name = Some(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, metadata) in &fold.channels {
|
for (id, metadata) in &fold.channels {
|
||||||
@@ -373,6 +383,11 @@ pub fn list_entry(state: &CommunityState, name: &str) -> CommunityListEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn label(name: &str) -> Option<String> {
|
||||||
|
let trimmed = name.trim();
|
||||||
|
(!trimmed.is_empty()).then(|| trimmed.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
fn state_identifier(id: &CommunityId) -> String {
|
fn state_identifier(id: &CommunityId) -> String {
|
||||||
format!("{STATE_PREFIX}{}", id.to_hex())
|
format!("{STATE_PREFIX}{}", id.to_hex())
|
||||||
}
|
}
|
||||||
@@ -710,6 +725,7 @@ mod tests {
|
|||||||
|
|
||||||
let state = CommunityState {
|
let state = CommunityState {
|
||||||
id: CommunityId::from_bytes([0x42; 32]),
|
id: CommunityId::from_bytes([0x42; 32]),
|
||||||
|
name: Some("Anime and Manga".to_owned()),
|
||||||
owner: Keys::generate().public_key(),
|
owner: Keys::generate().public_key(),
|
||||||
owner_salt: [0x01; 32],
|
owner_salt: [0x01; 32],
|
||||||
community_root: [0x02; 32],
|
community_root: [0x02; 32],
|
||||||
|
|||||||
@@ -318,6 +318,63 @@ pub async fn download_and_decrypt_to_file(
|
|||||||
Err(anyhow!("File download not supported on web"))
|
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> {
|
fn tag_value<'a>(tags: &'a Tags, name: &str) -> Option<&'a str> {
|
||||||
tags.iter()
|
tags.iter()
|
||||||
.find(|tag| tag.kind() == name)
|
.find(|tag| tag.kind() == name)
|
||||||
|
|||||||
+15
-6
@@ -1,8 +1,8 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AbsoluteLength, AnyElement, App, Bounds, Div, Hsla, InteractiveElement, Interactivity,
|
AbsoluteLength, AnyElement, App, Bounds, Div, Hsla, ImageSource, InteractiveElement,
|
||||||
IntoElement, ObjectFit, ParentElement, PathBuilder, Pixels, Point, RenderOnce, SharedString,
|
Interactivity, IntoElement, ObjectFit, ParentElement, PathBuilder, Pixels, Point, RenderOnce,
|
||||||
StyleRefinement, Styled, StyledImage, Window, canvas, div, img, point, px,
|
SharedString, StyleRefinement, Styled, StyledImage, Window, canvas, div, img, point, px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ fn generated_avatar(seed: Option<&str>, size: Pixels) -> AnyElement {
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
base: Div,
|
base: Div,
|
||||||
picture: Option<SharedString>,
|
picture: Option<ImageSource>,
|
||||||
grayscale: bool,
|
grayscale: bool,
|
||||||
seed: Option<SharedString>,
|
seed: Option<SharedString>,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
@@ -385,9 +385,18 @@ pub struct Avatar {
|
|||||||
impl Avatar {
|
impl Avatar {
|
||||||
/// Creates an avatar for an entity whose profile picture may be missing.
|
/// Creates an avatar for an entity whose profile picture may be missing.
|
||||||
///
|
///
|
||||||
/// Use [`Avatar::seed`] to choose the generated pixel avatar rendered when
|
/// Use [`Avatar::seed`] to choose the generated
|
||||||
/// `picture` is `None`.
|
/// pixel avatar rendered when `picture` is `None`.
|
||||||
pub fn new(picture: Option<SharedString>) -> Self {
|
pub fn new(picture: Option<SharedString>) -> Self {
|
||||||
|
Self::from_picture(picture.map(ImageSource::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an avatar from an already-resolved source.
|
||||||
|
pub fn from_source(picture: impl Into<ImageSource>) -> Self {
|
||||||
|
Self::from_picture(Some(picture.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_picture(picture: Option<ImageSource>) -> Self {
|
||||||
Avatar {
|
Avatar {
|
||||||
base: div(),
|
base: div(),
|
||||||
picture,
|
picture,
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ impl Sidebar {
|
|||||||
)
|
)
|
||||||
.depth(*depth)
|
.depth(*depth)
|
||||||
.avatar(community.id().to_hex())
|
.avatar(community.id().to_hex())
|
||||||
|
.picture(community.icon())
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
SidebarRow::NewCommunity { depth } => TreeRow::new(
|
SidebarRow::NewCommunity { depth } => TreeRow::new(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use chat::Room;
|
use chat::Room;
|
||||||
@@ -8,7 +9,7 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::PixelAvatar;
|
use ui::avatar::{Avatar, PixelAvatar};
|
||||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
@@ -94,6 +95,7 @@ pub struct TreeRow {
|
|||||||
caret: Option<IconName>,
|
caret: Option<IconName>,
|
||||||
icon: Option<IconName>,
|
icon: Option<IconName>,
|
||||||
avatar: Option<SharedString>,
|
avatar: Option<SharedString>,
|
||||||
|
picture: Option<PathBuf>,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
dot: bool,
|
dot: bool,
|
||||||
@@ -114,6 +116,7 @@ impl TreeRow {
|
|||||||
caret: None,
|
caret: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
picture: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
count: None,
|
count: None,
|
||||||
dot: false,
|
dot: false,
|
||||||
@@ -142,6 +145,12 @@ impl TreeRow {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows `picture` instead of the generated avatar.
|
||||||
|
pub fn picture(mut self, picture: Option<PathBuf>) -> Self {
|
||||||
|
self.picture = picture;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn count(mut self, count: usize) -> Self {
|
pub fn count(mut self, count: usize) -> Self {
|
||||||
self.count = Some(count);
|
self.count = Some(count);
|
||||||
self
|
self
|
||||||
@@ -164,11 +173,21 @@ impl TreeRow {
|
|||||||
impl RenderOnce for TreeRow {
|
impl RenderOnce for TreeRow {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let indent = px(6. + self.depth as f32 * 14.);
|
let indent = px(6. + self.depth as f32 * 14.);
|
||||||
let avatar_seed = self.avatar;
|
|
||||||
let is_section = self.kind == TreeRowKind::Section;
|
let is_section = self.kind == TreeRowKind::Section;
|
||||||
let is_community = self.kind == TreeRowKind::Community;
|
let is_community = self.kind == TreeRowKind::Community;
|
||||||
let is_hint = self.kind == TreeRowKind::Hint;
|
let is_hint = self.kind == TreeRowKind::Hint;
|
||||||
|
|
||||||
|
let avatar = match (self.avatar, self.picture) {
|
||||||
|
(seed, Some(picture)) => Some(
|
||||||
|
Avatar::from_source(picture)
|
||||||
|
.when_some(seed, |avatar, seed| avatar.seed(seed))
|
||||||
|
.xsmall()
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
(Some(seed), None) => Some(PixelAvatar::new(seed).xsmall().into_any_element()),
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
.h_8()
|
.h_8()
|
||||||
@@ -189,9 +208,7 @@ impl RenderOnce for TreeRow {
|
|||||||
.when_some(self.icon, |this, icon| {
|
.when_some(self.icon, |this, icon| {
|
||||||
this.child(Icon::new(icon).small().text_color(cx.theme().icon_muted))
|
this.child(Icon::new(icon).small().text_color(cx.theme().icon_muted))
|
||||||
})
|
})
|
||||||
.when_some(avatar_seed, |this, seed| {
|
.when_some(avatar, |this, avatar| this.child(avatar))
|
||||||
this.child(PixelAvatar::new(seed).xsmall())
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
|||||||
Reference in New Issue
Block a user