add spam badge for requests
This commit is contained in:
+24
-2
@@ -192,16 +192,38 @@ impl Room {
|
||||
|
||||
let wot = WebOfTrust::global(cx).read(cx);
|
||||
|
||||
Self::trusted_by(&self.members, me, |key| wot.is_in_network(key))
|
||||
}
|
||||
|
||||
/// Whether the room looks like spam: everyone other than the current user
|
||||
/// is outside the web of trust
|
||||
pub fn is_spam(&self, cx: &App) -> bool {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let Some(me) = nostr.read(cx).current_user() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let wot = WebOfTrust::global(cx).read(cx);
|
||||
|
||||
Self::trusted_by(&self.members, me, |key| !wot.is_outside(key))
|
||||
}
|
||||
|
||||
/// Whether every member other than `me` passes the check
|
||||
fn trusted_by(
|
||||
members: &[PublicKey],
|
||||
me: PublicKey,
|
||||
check: impl Fn(&PublicKey) -> bool,
|
||||
) -> bool {
|
||||
let mut others = false;
|
||||
|
||||
for key in &self.members {
|
||||
for key in members {
|
||||
if *key == me {
|
||||
continue;
|
||||
}
|
||||
|
||||
others = true;
|
||||
|
||||
if !wot.is_in_network(key) {
|
||||
if !check(key) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +434,11 @@ impl WebOfTrust {
|
||||
self.follows.contains(public_key) || self.score(public_key) > 0
|
||||
}
|
||||
|
||||
/// Whether the graph is loaded and the public key is outside the network
|
||||
pub fn is_outside(&self, public_key: &PublicKey) -> bool {
|
||||
self.ready && !self.disabled && !self.is_in_network(public_key)
|
||||
}
|
||||
|
||||
/// The current user's contacts who follow the public key
|
||||
pub fn mutuals(&self, public_key: &PublicKey) -> Vec<PublicKey> {
|
||||
self.reverse
|
||||
@@ -572,6 +577,28 @@ mod tests {
|
||||
assert!(wot.reverse.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outside_requires_a_loaded_graph() {
|
||||
let mut wot = registry();
|
||||
let (me, alice, carol) = (key(1), key(2), key(3));
|
||||
|
||||
wot.self_key = Some(me);
|
||||
wot.store_follows(Some(Timestamp::from_secs(10)), HashSet::from([alice]));
|
||||
|
||||
// The graph is not loaded yet
|
||||
assert!(!wot.is_outside(&carol));
|
||||
|
||||
wot.ready = true;
|
||||
|
||||
assert!(wot.is_outside(&carol));
|
||||
assert!(!wot.is_outside(&alice));
|
||||
|
||||
// A disabled graph marks nothing as outside
|
||||
wot.disabled = true;
|
||||
|
||||
assert!(!wot.is_outside(&carol));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_ingested_contact_lists() {
|
||||
let mut wot = registry();
|
||||
|
||||
@@ -14,6 +14,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
||||
|
||||
/// Built-in image shown behind chat and group panels
|
||||
pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg";
|
||||
|
||||
@@ -126,42 +127,30 @@ impl RoomConfig {
|
||||
pub struct Settings {
|
||||
#[serde(default, alias = "theme_mode")]
|
||||
pub appearance_mode: AppearanceMode,
|
||||
|
||||
/// Hide user avatars
|
||||
pub hide_avatar: bool,
|
||||
|
||||
/// Enable screening for unknown chat requests
|
||||
pub screening: bool,
|
||||
|
||||
/// Promote chat requests from people followed by the user's contacts
|
||||
#[serde(default)]
|
||||
pub wot_auto_trust: bool,
|
||||
|
||||
/// Enable decoupling encryption key
|
||||
pub nip4e: bool,
|
||||
|
||||
/// Hide the notice that messages in public groups are not encrypted
|
||||
#[serde(default)]
|
||||
pub hide_group_notice: bool,
|
||||
|
||||
/// Show an image behind chat and group panels
|
||||
#[serde(default)]
|
||||
pub panel_background: bool,
|
||||
|
||||
/// Custom image for the panel background; the built-in one when unset
|
||||
#[serde(default)]
|
||||
pub panel_background_path: Option<PathBuf>,
|
||||
|
||||
/// Trusted relays; Coop will automatically authenticate with these relays
|
||||
pub trusted_relays: Vec<String>,
|
||||
|
||||
/// Server for blossom media attachments
|
||||
pub file_server: Url,
|
||||
|
||||
/// Pinned chat rooms, by conversation ID
|
||||
#[serde(default)]
|
||||
pub pinned_rooms: Vec<u64>,
|
||||
|
||||
/// Pinned groups, by cache tag
|
||||
#[serde(default)]
|
||||
pub pinned_groups: Vec<String>,
|
||||
@@ -173,7 +162,7 @@ impl Default for Settings {
|
||||
appearance_mode: AppearanceMode::default(),
|
||||
hide_avatar: false,
|
||||
screening: true,
|
||||
wot_auto_trust: false,
|
||||
wot_auto_trust: true,
|
||||
nip4e: false,
|
||||
hide_group_notice: false,
|
||||
panel_background: true,
|
||||
@@ -345,30 +334,3 @@ impl AppSettings {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn json_with(partial: &str) -> Result<Settings, serde_json::Error> {
|
||||
// Every field the persisted format has always required; `partial`
|
||||
// contributes the fields under test.
|
||||
serde_json::from_str(&format!(
|
||||
r#"{{ {} "hide_avatar": false, "screening": true, "nip4e": false,
|
||||
"trusted_relays": [], "file_server": "https://nostr.download/" }}"#,
|
||||
partial
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_theme_mode_key_keeps_its_pinned_choice() {
|
||||
let settings = json_with(r#""theme_mode": "Dark", "#).unwrap();
|
||||
assert_eq!(settings.appearance_mode, AppearanceMode::Dark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_appearance_mode_falls_back_to_dark() {
|
||||
let settings = json_with("").unwrap();
|
||||
assert_eq!(settings.appearance_mode, AppearanceMode::Dark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ impl Render for Screening {
|
||||
Button::new("njump")
|
||||
.icon(IconName::Link)
|
||||
.label("njump.me")
|
||||
.secondary()
|
||||
.ghost_alt()
|
||||
.small()
|
||||
.rounded()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
@@ -405,7 +405,6 @@ impl Render for Screening {
|
||||
Button::new("report")
|
||||
.tooltip("Report as a scam or impostor")
|
||||
.icon(IconName::Warning)
|
||||
.small()
|
||||
.warning()
|
||||
.rounded()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
|
||||
@@ -120,7 +120,7 @@ impl Sidebar {
|
||||
.px_4()
|
||||
.text_style(TextStyle::Caption)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(list_title),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Sidebar {
|
||||
.label(label)
|
||||
.text_style(TextStyle::Caption)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Empty(text) => v_flex()
|
||||
@@ -136,17 +136,20 @@ impl Sidebar {
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(px(button_radius()))
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(*text),
|
||||
)
|
||||
.into_any_element(),
|
||||
SidebarRow::Room { room } => {
|
||||
let id = room.read(cx).id;
|
||||
let name = room.read(cx).display_name(cx);
|
||||
let picture = room.read(cx).display_image(cx);
|
||||
let seed = room.read(cx).display_image_seed(cx);
|
||||
let created_at = room.read(cx).created_at.to_ago();
|
||||
let id = room.read(cx).id;
|
||||
let member = room.read(cx).members().first().copied();
|
||||
let spam =
|
||||
room.read(cx).is_spam(cx) && room.read(cx).kind == RoomKind::Request;
|
||||
|
||||
let pinned = AppSettings::global(cx).read(cx).pinned_room(id, cx);
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
@@ -163,12 +166,29 @@ impl Sidebar {
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded(px(button_radius()))
|
||||
.bg(cx.theme().danger.opacity(0.15))
|
||||
.text_size(px(8.0))
|
||||
.text_color(cx.theme().danger)
|
||||
.child("SPAM"),
|
||||
)
|
||||
})
|
||||
.when(!spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(created_at),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
});
|
||||
|
||||
+10
-1
@@ -146,6 +146,13 @@ to be cached, so "mutual contacts" is usually empty) with the registry:
|
||||
|
||||
### Phase 2 — requests list badge
|
||||
|
||||
Status: **partially implemented**. Request rooms whose members are all outside
|
||||
the web of trust show a `SPAM` badge in the sidebar
|
||||
(`Room::is_spam` + `WebOfTrust::is_outside`, rendered in
|
||||
`crates/workspace/src/sidebar/rows.rs`; the sidebar subscribes to `WoTEvent` so
|
||||
badges update when the graph loads). The mutual-count caption below is still
|
||||
open and will follow a different flow.
|
||||
|
||||
Where request rooms are rendered, show the mutual count for the sender:
|
||||
|
||||
- `crates/workspace/src/sidebar/inbox.rs` (`requests_rows`) and
|
||||
@@ -162,7 +169,9 @@ flow).
|
||||
The payoff: strangers vouched for by the user's contacts can skip screening.
|
||||
|
||||
- `pub wot_auto_trust: bool` in `setting_accessors!` in
|
||||
`crates/settings/src/lib.rs` (default `false`) and a "Web of trust" toggle in
|
||||
`crates/settings/src/lib.rs` (enabled out of the box: `Default` is `true` and
|
||||
a `#[serde(default = "default_true")]` fallback covers settings files saved
|
||||
before the field existed) and a "Web of trust" toggle in
|
||||
the preferences screen (`crates/workspace/src/dialogs/settings.rs`).
|
||||
- The rule lives in one predicate — `Room::auto_trusted(me, cx)` in
|
||||
`crates/chat/src/room.rs`: the setting is on and every member other than the
|
||||
|
||||
Reference in New Issue
Block a user