feat: add support for basic web of trust #65
Generated
+29
-30
@@ -933,9 +933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.8.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -1066,7 +1066,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.1",
|
"block-buffer 0.12.1",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
"inout 0.2.4",
|
"inout 0.2.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2013,9 +2013,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flatbuffers"
|
name = "flatbuffers"
|
||||||
@@ -2696,7 +2696,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3392,7 +3392,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3616,9 +3616,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ce983bb6fb399067d101fbfc9e74174e07e9f29a2bc679efefeb8c645819c3f"
|
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
@@ -4352,7 +4352,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.5"
|
version = "0.45.5"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -4378,7 +4378,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitcoin_hashes 1.2.0",
|
"bitcoin_hashes 1.2.0",
|
||||||
@@ -4391,10 +4391,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4405,7 +4404,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4414,7 +4413,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4423,7 +4422,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4435,7 +4434,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.3"
|
version = "0.45.3"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4450,7 +4449,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4461,7 +4460,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.4"
|
version = "0.45.4"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -5943,7 +5942,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7164,9 +7163,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.3"
|
version = "1.54.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -7656,14 +7655,14 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq-proto",
|
"ureq-proto",
|
||||||
"utf8-zero",
|
"utf8-zero",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq-proto"
|
name = "ureq-proto"
|
||||||
version = "0.6.4"
|
version = "0.6.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
|
checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"http",
|
"http",
|
||||||
@@ -8043,9 +8042,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -8056,14 +8055,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|||||||
+45
-5
@@ -14,6 +14,7 @@ use gpui::{
|
|||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nip4e::{DeviceEvent, DeviceRegistry};
|
use nip4e::{DeviceEvent, DeviceRegistry};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
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};
|
||||||
|
|
||||||
@@ -139,13 +140,13 @@ impl ChatRegistry {
|
|||||||
/// Create a new chat registry instance
|
/// Create a new chat registry instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let device = DeviceRegistry::global(cx);
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||||
let entity = cx.entity().downgrade();
|
let entity = cx.entity().downgrade();
|
||||||
|
|
||||||
let (tx, rx) = flume::unbounded::<Signal>();
|
let (tx, rx) = flume::unbounded::<Signal>();
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
let device = DeviceRegistry::global(cx);
|
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
subscriptions.push(cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||||
if event.signer_changed() {
|
if event.signer_changed() {
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
@@ -163,6 +164,10 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
subscriptions.push(cx.subscribe(&wot, |this, _wot, _event, cx| {
|
||||||
|
this.promote_trusted(cx);
|
||||||
|
}));
|
||||||
|
|
||||||
cx.defer(move |cx| {
|
cx.defer(move |cx| {
|
||||||
let _ = entity.update(cx, |this, cx| {
|
let _ = entity.update(cx, |this, cx| {
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
@@ -567,7 +572,23 @@ impl ChatRegistry {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new room to the start of list.
|
/// Promote request rooms whose senders are inside the web of trust
|
||||||
|
fn promote_trusted(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let Some(me) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for room in &self.rooms {
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
if room.kind == RoomKind::Request && room.auto_trusted(me, cx) {
|
||||||
|
room.set_ongoing(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Promote a room to ongoing when its latest message is from the current user
|
||||||
pub(crate) 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
|
where
|
||||||
I: Into<Room>,
|
I: Into<Room>,
|
||||||
@@ -577,7 +598,12 @@ impl ChatRegistry {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let room: Room = room.into().organize(&public_key);
|
let mut room: Room = room.into().organize(&public_key);
|
||||||
|
|
||||||
|
if room.auto_trusted(public_key, cx) {
|
||||||
|
room.kind = RoomKind::Ongoing;
|
||||||
|
}
|
||||||
|
|
||||||
let room_id = room.id;
|
let room_id = room.id;
|
||||||
let entity = cx.new(|_| room);
|
let entity = cx.new(|_| room);
|
||||||
entity.update(cx, |room, cx| room.ensure_directory(cx));
|
entity.update(cx, |room, cx| room.ensure_directory(cx));
|
||||||
@@ -652,6 +678,11 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Extend the registry with new rooms.
|
/// Extend the registry with new rooms.
|
||||||
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
|
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let Some(me) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let mut room_map: HashMap<u64, usize> = self
|
let mut room_map: HashMap<u64, usize> = self
|
||||||
.rooms
|
.rooms
|
||||||
.iter()
|
.iter()
|
||||||
@@ -660,6 +691,13 @@ impl ChatRegistry {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for new_room in rooms.into_iter() {
|
for new_room in rooms.into_iter() {
|
||||||
|
// Auto-trust also applies to rooms loaded from the database
|
||||||
|
let new_room = if new_room.kind == RoomKind::Request && new_room.auto_trusted(me, cx) {
|
||||||
|
new_room.kind(RoomKind::Ongoing)
|
||||||
|
} else {
|
||||||
|
new_room
|
||||||
|
};
|
||||||
|
|
||||||
// Check if we already have a room with this ID
|
// Check if we already have a room with this ID
|
||||||
if let Some(&index) = room_map.get(&new_room.id) {
|
if let Some(&index) = room_map.get(&new_room.id) {
|
||||||
self.rooms[index].update(cx, |this, cx| {
|
self.rooms[index].update(cx, |this, cx| {
|
||||||
@@ -789,7 +827,9 @@ impl ChatRegistry {
|
|||||||
match self.room_index.get(&message.room).cloned() {
|
match self.room_index.get(&message.room).cloned() {
|
||||||
Some(room) => {
|
Some(room) => {
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
if this.kind == RoomKind::Request && message.rumor.pubkey == public_key {
|
if this.kind == RoomKind::Request
|
||||||
|
&& (message.rumor.pubkey == public_key || this.auto_trusted(public_key, cx))
|
||||||
|
{
|
||||||
this.set_ongoing(cx);
|
this.set_ongoing(cx);
|
||||||
}
|
}
|
||||||
this.push_message(message, cx);
|
this.push_message(message, cx);
|
||||||
|
|||||||
+57
-2
@@ -8,8 +8,8 @@ use instant::Duration;
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nip4e::DeviceRegistry;
|
use nip4e::DeviceRegistry;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Directory, Person};
|
use person::{Directory, Person, PersonRegistry};
|
||||||
use settings::{RoomConfig, SignerKind};
|
use settings::{AppSettings, RoomConfig, SignerKind};
|
||||||
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
||||||
|
|
||||||
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
|
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
|
||||||
@@ -183,6 +183,61 @@ impl Room {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the auto-trust setting promotes this room out of the requests:
|
||||||
|
/// everyone other than the current user is inside the web of trust
|
||||||
|
pub fn auto_trusted(&self, me: PublicKey, cx: &App) -> bool {
|
||||||
|
if !AppSettings::get_wot_auto_trust(cx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot().read(cx);
|
||||||
|
|
||||||
|
Self::trusted_by(&self.members, me, |key| wot.is_in_network(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the room looks like spam: the web of trust is loaded and
|
||||||
|
/// everyone other than the current user is outside of it
|
||||||
|
pub fn is_spam(&self, cx: &App) -> bool {
|
||||||
|
let person = PersonRegistry::global(cx);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
let Some(me) = nostr.read(cx).current_user() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let wot = person.read(cx).wot().read(cx);
|
||||||
|
|
||||||
|
// An unloaded graph cannot classify anything as spam
|
||||||
|
if !wot.is_loaded() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 members {
|
||||||
|
if *key == me {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
others = true;
|
||||||
|
|
||||||
|
if !check(key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
others
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets this room is ongoing conversation
|
/// Sets this room is ongoing conversation
|
||||||
pub(crate) fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
pub(crate) fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||||
self.kind = RoomKind::Ongoing;
|
self.kind = RoomKind::Ongoing;
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
|||||||
|
|
||||||
mod directory;
|
mod directory;
|
||||||
mod person;
|
mod person;
|
||||||
|
mod wot;
|
||||||
|
|
||||||
pub use directory::*;
|
pub use directory::*;
|
||||||
pub use person::*;
|
pub use person::*;
|
||||||
|
pub use wot::*;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
||||||
@@ -60,6 +62,8 @@ enum Dispatch {
|
|||||||
pub struct PersonRegistry {
|
pub struct PersonRegistry {
|
||||||
/// Public keys with a metadata request in flight
|
/// Public keys with a metadata request in flight
|
||||||
in_flight: RwLock<HashSet<PublicKey>>,
|
in_flight: RwLock<HashSet<PublicKey>>,
|
||||||
|
/// Web of trust graph of the signed-in user
|
||||||
|
wot: Entity<WebOfTrust>,
|
||||||
sender: flume::Sender<PublicKey>,
|
sender: flume::Sender<PublicKey>,
|
||||||
tasks: SmallVec<[Task<()>; 4]>,
|
tasks: SmallVec<[Task<()>; 4]>,
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
@@ -80,6 +84,7 @@ impl PersonRegistry {
|
|||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let wot = cx.new(WebOfTrust::new);
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||||
@@ -122,17 +127,28 @@ impl PersonRegistry {
|
|||||||
let subscription = cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
let subscription = cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||||
if event.signer_changed() {
|
if event.signer_changed() {
|
||||||
this.get_emojis(cx);
|
this.get_emojis(cx);
|
||||||
|
|
||||||
|
this.wot.update(cx, |wot, cx| {
|
||||||
|
wot.reset(cx);
|
||||||
|
wot.hydrate(cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
in_flight: RwLock::new(HashSet::new()),
|
in_flight: RwLock::new(HashSet::new()),
|
||||||
|
wot,
|
||||||
sender: metadata_tx,
|
sender: metadata_tx,
|
||||||
tasks,
|
tasks,
|
||||||
_subscriptions: smallvec![subscription],
|
_subscriptions: smallvec![subscription],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The web of trust graph of the signed-in user
|
||||||
|
pub fn wot(&self) -> Entity<WebOfTrust> {
|
||||||
|
self.wot.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle nostr notifications
|
/// Handle nostr notifications
|
||||||
async fn handle_notifications(client: &Client, tx: &flume::Sender<Dispatch>) {
|
async fn handle_notifications(client: &Client, tx: &flume::Sender<Dispatch>) {
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use gpui::{Context, EventEmitter, Task};
|
||||||
|
use instant::Duration;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
use state::NostrRegistry;
|
||||||
|
|
||||||
|
/// Disable scoring when the follow set exceeds this size
|
||||||
|
const MAX_FOLLOWS: usize = 2000;
|
||||||
|
/// Cap a followed author's contact list to bound hostile publishers
|
||||||
|
const MAX_TAGS_PER_LIST: usize = 5000;
|
||||||
|
/// Time between reconciliations with the relays
|
||||||
|
const SYNC_INTERVAL: u64 = 15 * 60;
|
||||||
|
/// Longest wait between retries while the graph has no data yet
|
||||||
|
const MAX_RETRY_WAIT: u64 = 60;
|
||||||
|
|
||||||
|
/// The web of trust graph changed
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WoTEvent;
|
||||||
|
|
||||||
|
/// A built graph: the follow set and, per public key, who follows it
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Snapshot {
|
||||||
|
follows: HashSet<PublicKey>,
|
||||||
|
scores: HashMap<PublicKey, usize>,
|
||||||
|
mutuals: HashMap<PublicKey, Vec<PublicKey>>,
|
||||||
|
disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Snapshot {
|
||||||
|
/// Build the snapshot from the newest stored contact list of every author
|
||||||
|
fn build(self_key: PublicKey, follows: HashSet<PublicKey>, events: Vec<Event>) -> Self {
|
||||||
|
let disabled = follows.len() > MAX_FOLLOWS;
|
||||||
|
|
||||||
|
let mut snapshot = Self {
|
||||||
|
follows,
|
||||||
|
scores: HashMap::new(),
|
||||||
|
mutuals: HashMap::new(),
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
if snapshot.disabled {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the newest contact list per author
|
||||||
|
let mut latest: HashMap<PublicKey, Event> = HashMap::new();
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
if event.pubkey == self_key || !snapshot.follows.contains(&event.pubkey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match latest.get(&event.pubkey) {
|
||||||
|
Some(current) if current.created_at > event.created_at => continue,
|
||||||
|
_ => {
|
||||||
|
latest.insert(event.pubkey, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (author, event) in latest {
|
||||||
|
for key in event.tags.public_keys().take(MAX_TAGS_PER_LIST) {
|
||||||
|
if key == author || key == self_key {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*snapshot.scores.entry(key).or_default() += 1;
|
||||||
|
snapshot.mutuals.entry(key).or_default().push(author);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for contacts in snapshot.mutuals.values_mut() {
|
||||||
|
contacts.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<WoTEvent> for WebOfTrust {}
|
||||||
|
|
||||||
|
/// Web of trust
|
||||||
|
///
|
||||||
|
/// Scores public keys by how many of the current user's contacts follow them.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WebOfTrust {
|
||||||
|
/// Current user's public key
|
||||||
|
self_key: Option<PublicKey>,
|
||||||
|
/// Latest built snapshot, none until the first refresh completes
|
||||||
|
snapshot: Option<Snapshot>,
|
||||||
|
/// In-flight refresh, replaced by newer ones
|
||||||
|
refresh_task: Option<Task<()>>,
|
||||||
|
/// Keeps the periodic reconciliation loop alive
|
||||||
|
_tasks: SmallVec<[Task<()>; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebOfTrust {
|
||||||
|
/// Create a new web of trust instance
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let entity = cx.entity().downgrade();
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
let mut delay = Duration::from_secs(5);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
cx.background_executor().timer(delay).await;
|
||||||
|
|
||||||
|
let mut has_data = false;
|
||||||
|
|
||||||
|
// Ignore errors, the next refresh will retry
|
||||||
|
let _ = this.update(cx, |this, cx| {
|
||||||
|
this.refresh(cx);
|
||||||
|
has_data = this.has_data();
|
||||||
|
});
|
||||||
|
|
||||||
|
delay = if has_data {
|
||||||
|
Duration::from_secs(SYNC_INTERVAL)
|
||||||
|
} else {
|
||||||
|
(delay * 2).min(Duration::from_secs(MAX_RETRY_WAIT))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = entity.update(cx, |this, cx| {
|
||||||
|
this.hydrate(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
self_key: None,
|
||||||
|
snapshot: None,
|
||||||
|
refresh_task: None,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the graph for the current account
|
||||||
|
pub(crate) fn hydrate(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let Some(self_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.self_key = Some(self_key);
|
||||||
|
self.refresh(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the graph on sign out or account switch
|
||||||
|
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.self_key = None;
|
||||||
|
self.snapshot = None;
|
||||||
|
self.refresh_task = None;
|
||||||
|
|
||||||
|
cx.emit(WoTEvent);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconcile the follow set's contact lists with the relays and rebuild.
|
||||||
|
fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(self_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.refresh_task = Some(cx.spawn(async move |this, cx| {
|
||||||
|
// Scope the reconcile to the follow set known so far
|
||||||
|
let known: HashSet<PublicKey> = stored_contact_list(&client, self_key)
|
||||||
|
.await
|
||||||
|
.map(|event| event.tags.public_keys().take(MAX_FOLLOWS + 1).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut authors: Vec<PublicKey> = known.iter().copied().collect();
|
||||||
|
authors.push(self_key);
|
||||||
|
|
||||||
|
// Negentropy transfers only the lists the relays hold
|
||||||
|
// and the database is missing
|
||||||
|
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||||
|
if let Err(error) = client.sync(filter).await {
|
||||||
|
log::warn!("Failed to reconcile contact lists: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The follow set after the reconcile
|
||||||
|
let Some(own) = stored_contact_list(&client, self_key).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let follows: HashSet<PublicKey> =
|
||||||
|
own.tags.public_keys().take(MAX_FOLLOWS + 1).collect();
|
||||||
|
|
||||||
|
let mut authors: Vec<PublicKey> = follows.iter().copied().collect();
|
||||||
|
authors.push(self_key);
|
||||||
|
|
||||||
|
let events: Vec<Event> = if follows.is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||||
|
client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let snapshot = Snapshot::build(self_key, follows.clone(), events);
|
||||||
|
log::debug!(
|
||||||
|
"Web of trust rebuilt: {} follows, {} scored keys",
|
||||||
|
snapshot.follows.len(),
|
||||||
|
snapshot.scores.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Newly followed authors were not part of the reconcile
|
||||||
|
let grew = !known.is_superset(&follows);
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.snapshot = Some(snapshot);
|
||||||
|
cx.emit(WoTEvent);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if grew {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.refresh(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many of the current user's contacts follow the public key
|
||||||
|
pub fn score(&self, public_key: &PublicKey) -> usize {
|
||||||
|
self.snapshot.as_ref().map_or(0, |snapshot| {
|
||||||
|
snapshot.scores.get(public_key).copied().unwrap_or(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the public key is followed by the user or by any of their contacts
|
||||||
|
pub fn is_in_network(&self, public_key: &PublicKey) -> bool {
|
||||||
|
self.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|snapshot| snapshot.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.is_loaded() && !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.snapshot.as_ref().map_or_else(Vec::new, |snapshot| {
|
||||||
|
snapshot
|
||||||
|
.mutuals
|
||||||
|
.get(public_key)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the graph is loaded and score queries are meaningful
|
||||||
|
pub fn is_loaded(&self) -> bool {
|
||||||
|
self.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|snapshot| !snapshot.disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the snapshot covers a follow set
|
||||||
|
fn has_data(&self) -> bool {
|
||||||
|
self.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|snapshot| !snapshot.follows.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The newest stored contact list of the public key
|
||||||
|
async fn stored_contact_list(client: &Client, self_key: PublicKey) -> Option<Event> {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(self_key)
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ pub fn init(cx: &mut App) {
|
|||||||
|
|
||||||
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||||
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
||||||
|
|
||||||
/// Built-in image shown behind chat and group panels
|
/// Built-in image shown behind chat and group panels
|
||||||
pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg";
|
pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg";
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ setting_accessors! {
|
|||||||
pub appearance_mode: AppearanceMode,
|
pub appearance_mode: AppearanceMode,
|
||||||
pub hide_avatar: bool,
|
pub hide_avatar: bool,
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
|
pub wot_auto_trust: bool,
|
||||||
pub nip4e: bool,
|
pub nip4e: bool,
|
||||||
pub hide_group_notice: bool,
|
pub hide_group_notice: bool,
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
@@ -125,38 +127,30 @@ impl RoomConfig {
|
|||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
#[serde(default, alias = "theme_mode")]
|
#[serde(default, alias = "theme_mode")]
|
||||||
pub appearance_mode: AppearanceMode,
|
pub appearance_mode: AppearanceMode,
|
||||||
|
|
||||||
/// Hide user avatars
|
/// Hide user avatars
|
||||||
pub hide_avatar: bool,
|
pub hide_avatar: bool,
|
||||||
|
|
||||||
/// Enable screening for unknown chat requests
|
/// Enable screening for unknown chat requests
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
|
/// Promote chat requests from people followed by the user's contacts
|
||||||
|
pub wot_auto_trust: bool,
|
||||||
/// Enable decoupling encryption key
|
/// Enable decoupling encryption key
|
||||||
pub nip4e: bool,
|
pub nip4e: bool,
|
||||||
|
|
||||||
/// Hide the notice that messages in public groups are not encrypted
|
/// Hide the notice that messages in public groups are not encrypted
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hide_group_notice: bool,
|
pub hide_group_notice: bool,
|
||||||
|
|
||||||
/// Show an image behind chat and group panels
|
/// Show an image behind chat and group panels
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub panel_background: bool,
|
pub panel_background: bool,
|
||||||
|
|
||||||
/// Custom image for the panel background; the built-in one when unset
|
/// Custom image for the panel background; the built-in one when unset
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub panel_background_path: Option<PathBuf>,
|
pub panel_background_path: Option<PathBuf>,
|
||||||
|
|
||||||
/// Trusted relays; Coop will automatically authenticate with these relays
|
/// Trusted relays; Coop will automatically authenticate with these relays
|
||||||
pub trusted_relays: Vec<String>,
|
pub trusted_relays: Vec<String>,
|
||||||
|
|
||||||
/// Server for blossom media attachments
|
/// Server for blossom media attachments
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
|
|
||||||
/// Pinned chat rooms, by conversation ID
|
/// Pinned chat rooms, by conversation ID
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub pinned_rooms: Vec<u64>,
|
pub pinned_rooms: Vec<u64>,
|
||||||
|
|
||||||
/// Pinned groups, by cache tag
|
/// Pinned groups, by cache tag
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub pinned_groups: Vec<String>,
|
pub pinned_groups: Vec<String>,
|
||||||
@@ -168,6 +162,7 @@ impl Default for Settings {
|
|||||||
appearance_mode: AppearanceMode::default(),
|
appearance_mode: AppearanceMode::default(),
|
||||||
hide_avatar: false,
|
hide_avatar: false,
|
||||||
screening: true,
|
screening: true,
|
||||||
|
wot_auto_trust: true,
|
||||||
nip4e: false,
|
nip4e: false,
|
||||||
hide_group_notice: false,
|
hide_group_notice: false,
|
||||||
panel_background: true,
|
panel_background: true,
|
||||||
@@ -339,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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-19
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -260,15 +261,9 @@ impl NostrRegistry {
|
|||||||
let uri = NostrConnectUri::parse(base)?;
|
let uri = NostrConnectUri::parse(base)?;
|
||||||
|
|
||||||
// Construct the nostr connect signer
|
// Construct the nostr connect signer
|
||||||
let mut signer = NostrConnect::new(
|
let signer = NostrConnect::builder(uri, keys)
|
||||||
uri,
|
.auth_url_handler(Arc::new(CoopAuthUrlHandler))
|
||||||
keys,
|
.build()?;
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(signer, cx);
|
this.set_signer(signer, cx);
|
||||||
@@ -447,16 +442,9 @@ impl NostrRegistry {
|
|||||||
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// Construct the nostr connect signer
|
let signer = NostrConnect::builder(connect_uri, keys)
|
||||||
let mut signer = NostrConnect::new(
|
.auth_url_handler(Arc::new(CoopAuthUrlHandler))
|
||||||
connect_uri,
|
.build()?;
|
||||||
keys,
|
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Verify the bunker responds before persisting the credential
|
// Verify the bunker responds before persisting the credential
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|||||||
@@ -459,9 +459,24 @@ impl Render for ProfileDialog {
|
|||||||
.filter(|website| !website.trim().is_empty())
|
.filter(|website| !website.trim().is_empty())
|
||||||
.map(SharedString::from);
|
.map(SharedString::from);
|
||||||
|
|
||||||
|
let score = {
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot().read(cx);
|
||||||
|
wot.is_loaded().then(|| wot.score(&self.public_key))
|
||||||
|
}
|
||||||
|
.map(|score| {
|
||||||
|
SharedString::from(format!(
|
||||||
|
"{} mutual {}",
|
||||||
|
score,
|
||||||
|
if score == 1 { "contact" } else { "contacts" }
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
this.child(field("Name", name, "Unknown", cx))
|
this.child(field("Name", name, "Unknown", cx))
|
||||||
.child(field("About", about, "No bio", cx))
|
.child(field("About", about, "No bio", cx))
|
||||||
.child(field("Website", website, "No website", cx))
|
.child(field("Website", website, "No website", cx))
|
||||||
|
.when(!self.is_current_user, |this| {
|
||||||
|
this.child(field("Web of Trust", score, "Not loaded yet", cx))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Directory, Person, shorten_pubkey};
|
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT};
|
use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT};
|
||||||
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
|
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
|
||||||
@@ -33,14 +33,19 @@ pub struct Screening {
|
|||||||
last_active: Option<Timestamp>,
|
last_active: Option<Timestamp>,
|
||||||
/// All mutual contacts of the person being screened.
|
/// All mutual contacts of the person being screened.
|
||||||
mutual_contacts: Vec<PublicKey>,
|
mutual_contacts: Vec<PublicKey>,
|
||||||
|
/// Whether the fallback lookup for mutual contacts has run.
|
||||||
|
mutuals_fallback: bool,
|
||||||
/// Person directory for the person and their mutual contacts
|
/// Person directory for the person and their mutual contacts
|
||||||
directory: Entity<Directory>,
|
directory: Entity<Directory>,
|
||||||
tasks: SmallVec<[Task<()>; 3]>,
|
tasks: SmallVec<[Task<()>; 3]>,
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screening {
|
impl Screening {
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||||
|
let directory = Directory::new(cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(cx.on_release_in(window, move |this, window, cx| {
|
subscriptions.push(cx.on_release_in(window, move |this, window, cx| {
|
||||||
@@ -48,22 +53,28 @@ impl Screening {
|
|||||||
window.close_all_dialogs(cx);
|
window.close_all_dialogs(cx);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
subscriptions.push(cx.subscribe(&wot, |_this, _wot, _event, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
|
directory.update(cx, |directory, cx| {
|
||||||
|
directory.track([public_key], cx);
|
||||||
|
});
|
||||||
|
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
this.check_contact(cx);
|
this.check_contact(cx);
|
||||||
this.check_wot(cx);
|
this.fetch_fallback(cx);
|
||||||
this.check_last_activity(cx);
|
this.check_last_activity(cx);
|
||||||
this.verify_identifier(cx);
|
this.verify_identifier(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let directory = Directory::new(cx);
|
|
||||||
directory.update(cx, |directory, cx| directory.track([public_key], cx));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
verified: false,
|
verified: false,
|
||||||
followed: false,
|
followed: false,
|
||||||
last_active: None,
|
last_active: None,
|
||||||
mutual_contacts: vec![],
|
mutual_contacts: vec![],
|
||||||
|
mutuals_fallback: false,
|
||||||
directory,
|
directory,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
@@ -110,42 +121,48 @@ impl Screening {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_wot(&mut self, cx: &mut Context<Self>) {
|
/// Fetch mutual contacts from the database while the web of trust loads
|
||||||
|
fn fetch_fallback(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
if self.mutuals_fallback || persons.read(cx).wot().read(cx).is_loaded() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.mutuals_fallback = true;
|
||||||
|
|
||||||
let public_key = self.public_key;
|
let public_key = self.public_key;
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).current_user() else {
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
let task: Task<Vec<PublicKey>> = cx.background_spawn(async move {
|
||||||
// Check mutual contacts
|
// Check mutual contacts among the locally stored contact lists
|
||||||
let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key);
|
let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key);
|
||||||
let mut mutual_contacts = vec![];
|
let mut contacts = vec![];
|
||||||
|
|
||||||
if let Ok(events) = client.database().query(filter).await {
|
if let Ok(events) = client.database().query(filter).await {
|
||||||
for event in events.into_iter().filter(|ev| ev.pubkey != current_user) {
|
for event in events.into_iter().filter(|ev| ev.pubkey != current_user) {
|
||||||
mutual_contacts.push(event.pubkey);
|
contacts.push(event.pubkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(mutual_contacts)
|
contacts
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
let contacts = task.await;
|
||||||
Ok(contacts) => {
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
if persons.read(cx).wot().read(cx).is_loaded() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.mutual_contacts = contacts;
|
this.mutual_contacts = contacts;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to fetch mutual contacts: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +280,12 @@ impl Screening {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn mutual_contacts(
|
||||||
let contacts = self.mutual_contacts.clone();
|
&mut self,
|
||||||
|
contacts: Vec<PublicKey>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let directory = self.directory.clone();
|
let directory = self.directory.clone();
|
||||||
|
|
||||||
window.open_dialog(cx, move |this, _window, _cx| {
|
window.open_dialog(cx, move |this, _window, _cx| {
|
||||||
@@ -317,11 +338,23 @@ impl Render for Screening {
|
|||||||
const NIP05_NOT_MATCH: &str = "The address does not match the user's public key.";
|
const NIP05_NOT_MATCH: &str = "The address does not match the user's public key.";
|
||||||
const NO_NIP05: &str = "This person has not set up their friendly address";
|
const NO_NIP05: &str = "This person has not set up their friendly address";
|
||||||
|
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = self.profile(cx);
|
let profile = self.profile(cx);
|
||||||
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
|
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
|
||||||
|
|
||||||
let last_active = self.last_active.map(|_| true);
|
let last_active = self.last_active.map(|_| true);
|
||||||
let mutuals = self.mutual_contacts.len();
|
|
||||||
|
// Pull from the web of trust once loaded, else use the database fallback
|
||||||
|
let mutual_contacts = {
|
||||||
|
let wot = persons.read(cx).wot().read(cx);
|
||||||
|
if wot.is_loaded() {
|
||||||
|
wot.mutuals(&self.public_key)
|
||||||
|
} else {
|
||||||
|
self.mutual_contacts.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mutuals = mutual_contacts.len();
|
||||||
let mutuals_str = format!("You have {} mutual contacts with this person.", mutuals);
|
let mutuals_str = format!("You have {} mutual contacts with this person.", mutuals);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -365,7 +398,7 @@ impl Render for Screening {
|
|||||||
Button::new("njump")
|
Button::new("njump")
|
||||||
.icon(IconName::Link)
|
.icon(IconName::Link)
|
||||||
.label("njump.me")
|
.label("njump.me")
|
||||||
.secondary()
|
.ghost_alt()
|
||||||
.small()
|
.small()
|
||||||
.rounded()
|
.rounded()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
@@ -376,7 +409,6 @@ impl Render for Screening {
|
|||||||
Button::new("report")
|
Button::new("report")
|
||||||
.tooltip("Report as a scam or impostor")
|
.tooltip("Report as a scam or impostor")
|
||||||
.icon(IconName::Warning)
|
.icon(IconName::Warning)
|
||||||
.small()
|
|
||||||
.warning()
|
.warning()
|
||||||
.rounded()
|
.rounded()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
@@ -515,7 +547,11 @@ impl Render for Screening {
|
|||||||
.rounded()
|
.rounded()
|
||||||
.disabled(mutuals == 0)
|
.disabled(mutuals == 0)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.mutual_contacts(window, cx);
|
this.mutual_contacts(
|
||||||
|
mutual_contacts.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -83,12 +83,14 @@ impl Preferences {
|
|||||||
impl Render for Preferences {
|
impl Render for Preferences {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
const SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
|
const SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
|
||||||
|
const WOT: &str = "Treat people followed by your contacts as known senders.";
|
||||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||||
const MODE: &str = "Follow the OS, or always use light or dark.";
|
const MODE: &str = "Follow the OS, or always use light or dark.";
|
||||||
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
||||||
const BACKGROUND: &str = "Show an image behind chat and group panels.";
|
const BACKGROUND: &str = "Show an image behind chat and group panels.";
|
||||||
|
|
||||||
let screening = AppSettings::get_screening(cx);
|
let screening = AppSettings::get_screening(cx);
|
||||||
|
let wot_auto_trust = AppSettings::get_wot_auto_trust(cx);
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
let nip4e = AppSettings::get_nip4e(cx);
|
let nip4e = AppSettings::get_nip4e(cx);
|
||||||
let appearance_mode = AppSettings::get_appearance_mode(cx);
|
let appearance_mode = AppSettings::get_appearance_mode(cx);
|
||||||
@@ -116,6 +118,15 @@ impl Render for Preferences {
|
|||||||
AppSettings::update_screening(!screening, cx);
|
AppSettings::update_screening(!screening, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Switch::new("wot-auto-trust")
|
||||||
|
.label("Web of Trust")
|
||||||
|
.description(WOT)
|
||||||
|
.checked(wot_auto_trust)
|
||||||
|
.on_click(move |_, _window, cx| {
|
||||||
|
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Switch::new("avatar")
|
Switch::new("avatar")
|
||||||
.label("Hide user avatar")
|
.label("Hide user avatar")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use gpui::{
|
|||||||
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, svg,
|
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, svg,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, StateEvent};
|
use state::{NostrRegistry, StateEvent};
|
||||||
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
|
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
|
||||||
@@ -57,13 +58,14 @@ pub struct GreeterPanel {
|
|||||||
/// The coalesced reload scheduled after registry updates
|
/// The coalesced reload scheduled after registry updates
|
||||||
refresh_task: Option<Task<Result<(), Error>>>,
|
refresh_task: Option<Task<Result<(), Error>>>,
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GreeterPanel {
|
impl GreeterPanel {
|
||||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -76,6 +78,13 @@ impl GreeterPanel {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Redraw when the web of trust loads or changes
|
||||||
|
cx.subscribe(&wot, |_, _, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Reload soon after rooms load, messages arrive, or requests change
|
// Reload soon after rooms load, messages arrive, or requests change
|
||||||
cx.observe(&chat, |this, _, cx| {
|
cx.observe(&chat, |this, _, cx| {
|
||||||
@@ -176,17 +185,35 @@ impl GreeterPanel {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_requests(&mut self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
fn render_requests(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
if self.requests.is_empty() {
|
// Spam requests stay out of the greeter entirely
|
||||||
return None;
|
let requests: Vec<_> = self
|
||||||
}
|
.requests
|
||||||
|
.iter()
|
||||||
|
.filter(|(room, _)| !room.read(cx).is_spam(cx))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
|
|
||||||
let rows: Vec<AnyElement> = self
|
let rows: Vec<AnyElement> = if requests.is_empty() {
|
||||||
.requests
|
vec![
|
||||||
|
h_flex()
|
||||||
|
.h_16()
|
||||||
|
.w_full()
|
||||||
|
.justify_center()
|
||||||
|
.border_1()
|
||||||
|
.border_dashed()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.rounded(px(button_radius()))
|
||||||
|
.text_style(TextStyle::Caption)
|
||||||
|
.text_color(cx.theme().text_faint)
|
||||||
|
.child("No chat requests")
|
||||||
|
.into_any_element(),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
requests
|
||||||
.iter()
|
.iter()
|
||||||
.take(SECTION_SIZE)
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, (room, preview))| {
|
.map(|(index, (room, preview))| {
|
||||||
let room = room.clone();
|
let room = room.clone();
|
||||||
@@ -280,16 +307,15 @@ impl GreeterPanel {
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
Some(
|
|
||||||
GroupBox::new()
|
GroupBox::new()
|
||||||
.id("requests")
|
.id("requests")
|
||||||
.title("Requests")
|
.title("Requests")
|
||||||
.fill()
|
.fill()
|
||||||
.child(v_flex().gap_0().w_full().children(rows))
|
.child(v_flex().gap_0().w_full().children(rows))
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_follow_ups(&mut self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
fn render_follow_ups(&mut self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||||
@@ -503,7 +529,7 @@ impl Render for GreeterPanel {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.children(self.render_requests(cx))
|
.child(self.render_requests(cx))
|
||||||
.children(self.render_follow_ups(cx))
|
.children(self.render_follow_ups(cx))
|
||||||
.child(self.render_find_friends(cx)),
|
.child(self.render_find_friends(cx)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl Sidebar {
|
|||||||
.px_4()
|
.px_4()
|
||||||
.text_style(TextStyle::Caption)
|
.text_style(TextStyle::Caption)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_faint)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(list_title),
|
.child(list_title),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use gpui::{
|
|||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::Directory;
|
use person::{Directory, PersonRegistry};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{FIND_DELAY, NostrRegistry, StateEvent};
|
use state::{FIND_DELAY, NostrRegistry, StateEvent};
|
||||||
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _};
|
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _};
|
||||||
@@ -103,6 +103,7 @@ impl Sidebar {
|
|||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let groups = GroupsRegistry::global(cx);
|
let groups = GroupsRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||||
|
|
||||||
let find_input = cx.new(|cx| {
|
let find_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
@@ -121,6 +122,12 @@ impl Sidebar {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ impl Sidebar {
|
|||||||
.label(label)
|
.label(label)
|
||||||
.text_style(TextStyle::Caption)
|
.text_style(TextStyle::Caption)
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_faint)
|
.text_color(cx.theme().text_muted)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
SidebarRow::Empty(text) => v_flex()
|
SidebarRow::Empty(text) => v_flex()
|
||||||
@@ -136,17 +136,20 @@ impl Sidebar {
|
|||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.rounded(px(button_radius()))
|
.rounded(px(button_radius()))
|
||||||
.text_style(TextStyle::Caption)
|
.text_style(TextStyle::Caption)
|
||||||
.text_color(cx.theme().text_faint)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(*text),
|
.child(*text),
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
SidebarRow::Room { room } => {
|
SidebarRow::Room { room } => {
|
||||||
|
let id = room.read(cx).id;
|
||||||
let name = room.read(cx).display_name(cx);
|
let name = room.read(cx).display_name(cx);
|
||||||
let picture = room.read(cx).display_image(cx);
|
let picture = room.read(cx).display_image(cx);
|
||||||
let seed = room.read(cx).display_image_seed(cx);
|
let seed = room.read(cx).display_image_seed(cx);
|
||||||
let created_at = room.read(cx).created_at.to_ago();
|
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 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 pinned = AppSettings::global(cx).read(cx).pinned_room(id, cx);
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let room = room.clone();
|
let room = room.clone();
|
||||||
@@ -163,12 +166,29 @@ impl Sidebar {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.suffix(
|
.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()
|
div()
|
||||||
.font_normal()
|
.font_normal()
|
||||||
.text_style(TextStyle::Caption)
|
.text_style(TextStyle::Caption)
|
||||||
.text_color(cx.theme().text_faint)
|
.text_color(cx.theme().text_faint)
|
||||||
.child(created_at),
|
.child(created_at),
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.on_click(move |_event, window, cx| {
|
.on_click(move |_event, window, cx| {
|
||||||
open_room(&room, &dock, window, cx);
|
open_room(&room, &dock, window, cx);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user