add spam badge for requests

This commit is contained in:
2026-10-20 10:53:45 +07:00
parent 10d74c5396
commit 9143de6cfe
7 changed files with 93 additions and 54 deletions
+24 -2
View File
@@ -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;
}
}
+27
View File
@@ -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();
+2 -40
View File
@@ -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);
}
}
+1 -2
View File
@@ -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| {
+1 -1
View File
@@ -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),
)
})
+28 -8
View File
@@ -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,11 +166,28 @@ impl Sidebar {
)
})
.suffix(
div()
.font_normal()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.child(created_at),
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);