clean up nip29 logging and rework browse panel header
This commit is contained in:
+61
-78
@@ -90,7 +90,6 @@ fn is_own_subscription(id: &SubscriptionId) -> bool {
|
||||
|| id.starts_with(PAGE_PREFIX)
|
||||
}
|
||||
|
||||
/// The group id, member count and timestamp of a group members event.
|
||||
fn parse_members(event: &Event) -> Option<(GroupId, Timestamp, usize)> {
|
||||
if event.kind != Kind::GroupMembers {
|
||||
return None;
|
||||
@@ -102,8 +101,6 @@ fn parse_members(event: &Event) -> Option<(GroupId, Timestamp, usize)> {
|
||||
Some((id, event.created_at, members))
|
||||
}
|
||||
|
||||
/// Reconcile a filter with the relays via negentropy, returning the events
|
||||
/// that were downloaded to the local database.
|
||||
async fn reconcile(
|
||||
client: &Client,
|
||||
filter: Filter,
|
||||
@@ -112,22 +109,19 @@ async fn reconcile(
|
||||
let output = match client.sync(filter).with(urls).await {
|
||||
Ok(output) => output,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: the reconciliation failed: {error}");
|
||||
log::warn!("the reconciliation failed: {error}");
|
||||
return HashMap::new();
|
||||
}
|
||||
};
|
||||
|
||||
for (relay, error) in &output.failed {
|
||||
log::debug!("nip29: syncing with {relay} failed: {error}");
|
||||
log::debug!("syncing with {relay} failed: {error}");
|
||||
}
|
||||
|
||||
output.value.received
|
||||
}
|
||||
|
||||
/// Pair the downloaded events with the relay that served them.
|
||||
///
|
||||
/// The database does not keep relay associations, so the pairing has to
|
||||
/// happen while the reconciliation still knows them.
|
||||
async fn pull_received(
|
||||
client: &Client,
|
||||
received: HashMap<EventId, HashSet<RelayUrl>>,
|
||||
@@ -202,16 +196,15 @@ impl GroupsRegistry {
|
||||
}));
|
||||
|
||||
cx.defer(move |cx| {
|
||||
if let Err(error) = this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.handle_notifications(cx);
|
||||
this.load_cached(cx);
|
||||
|
||||
if nostr.read(cx).current_user().is_some() {
|
||||
this.load(cx);
|
||||
}
|
||||
}) {
|
||||
log::warn!("nip29: initializing the registry failed: {error}");
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -278,7 +271,7 @@ impl GroupsRegistry {
|
||||
});
|
||||
}
|
||||
}) {
|
||||
log::warn!("nip29: marking the refused join failed: {error}");
|
||||
log::warn!("marking the refused join failed: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -337,11 +330,11 @@ impl GroupsRegistry {
|
||||
.close_on(opts)
|
||||
.await
|
||||
{
|
||||
log::warn!("nip29: browsing {relay} failed: {error}");
|
||||
log::warn!("browsing {relay} failed: {error}");
|
||||
}
|
||||
|
||||
if let Err(error) = tx.send_async(Signal::BrowseSettled(id)).await {
|
||||
log::warn!("nip29: the browse settle signal was dropped: {error}");
|
||||
log::warn!("the browse settle signal was dropped: {error}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -405,7 +398,7 @@ impl GroupsRegistry {
|
||||
let (authors, lists) = match task.await {
|
||||
Ok(ok) => ok,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: loading the cached groups failed: {error}");
|
||||
log::warn!("loading the cached groups failed: {error}");
|
||||
(HashSet::new(), Vec::new())
|
||||
}
|
||||
};
|
||||
@@ -413,21 +406,20 @@ impl GroupsRegistry {
|
||||
if let Err(error) = this.update(cx, |this, cx| {
|
||||
this.absorb_lists(lists, cx);
|
||||
}) {
|
||||
log::warn!("nip29: applying the cached group lists failed: {error}");
|
||||
log::warn!("applying the cached group lists failed: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the cached metadata before the relays answer.
|
||||
let previews = match this.update(cx, |this, cx| this.load_cached_previews(cx)) {
|
||||
Ok(task) => match task.await {
|
||||
Ok(previews) => previews,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: loading the cached group metadata failed: {error}");
|
||||
log::warn!("loading the cached group metadata failed: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -437,12 +429,12 @@ impl GroupsRegistry {
|
||||
Ok(task) => match task.await {
|
||||
Ok(members) => members,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: loading the cached group members failed: {error}");
|
||||
log::warn!("loading the cached group members failed: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -451,16 +443,14 @@ impl GroupsRegistry {
|
||||
this.absorb_cached_previews(previews, cx);
|
||||
this.absorb_cached_members(members, cx);
|
||||
}) {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconcile the contacts' group lists with the pool relays: only
|
||||
// the events missing locally are downloaded and stored.
|
||||
let lists = match this.update(cx, |this, cx| this.sync_lists(authors, cx)) {
|
||||
Ok(task) => task,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -469,7 +459,7 @@ impl GroupsRegistry {
|
||||
let sweep = match this.update(cx, |this, cx| this.sync_sweep(cx)) {
|
||||
Ok(task) => task,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -477,7 +467,7 @@ impl GroupsRegistry {
|
||||
let lists = match lists.await {
|
||||
Ok(lists) => lists,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: syncing the contacts group lists failed: {error}");
|
||||
log::warn!("syncing the contacts group lists failed: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
@@ -485,13 +475,13 @@ impl GroupsRegistry {
|
||||
if let Err(error) = this.update(cx, |this, cx| {
|
||||
this.absorb_lists(lists, cx);
|
||||
}) {
|
||||
log::warn!("nip29: applying the contacts group lists failed: {error}");
|
||||
log::warn!("applying the contacts group lists failed: {error}");
|
||||
}
|
||||
|
||||
let sweep = match sweep.await {
|
||||
Ok(events) => events,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: syncing the discovered groups failed: {error}");
|
||||
log::warn!("syncing the discovered groups failed: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
@@ -501,7 +491,7 @@ impl GroupsRegistry {
|
||||
this.absorb_discovered(relay, &event, cx);
|
||||
}
|
||||
}) {
|
||||
log::warn!("nip29: applying the discovered groups failed: {error}");
|
||||
log::warn!("applying the discovered groups failed: {error}");
|
||||
}
|
||||
|
||||
// The previews cover the relays the lists just referenced.
|
||||
@@ -509,12 +499,12 @@ impl GroupsRegistry {
|
||||
Ok(task) => match task.await {
|
||||
Ok(previews) => previews,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: syncing the group previews failed: {error}");
|
||||
log::warn!("syncing the group previews failed: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -527,7 +517,7 @@ impl GroupsRegistry {
|
||||
this.discovery
|
||||
.update(cx, |discovery, cx| discovery.settle(cx));
|
||||
}) {
|
||||
log::warn!("nip29: discovering the groups failed: {error}");
|
||||
log::warn!("discovering the groups failed: {error}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -560,7 +550,7 @@ impl GroupsRegistry {
|
||||
let metadata = match GroupMetadata::parse(&event) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: ignoring malformed cached group metadata: {error}");
|
||||
log::warn!("ignoring malformed cached group metadata: {error}");
|
||||
return newest;
|
||||
}
|
||||
};
|
||||
@@ -717,7 +707,7 @@ impl GroupsRegistry {
|
||||
let metadata = match GroupMetadata::parse(event) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: ignoring malformed group metadata from {relay}: {error}");
|
||||
log::warn!("ignoring malformed group metadata from {relay}: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -762,7 +752,7 @@ impl GroupsRegistry {
|
||||
|
||||
async move {
|
||||
if let Err(error) = client.add_relay(relay.clone()).and_connect().await {
|
||||
log::warn!("nip29: connecting to {relay} failed: {error}");
|
||||
log::warn!("connecting to {relay} failed: {error}");
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -811,10 +801,7 @@ impl GroupsRegistry {
|
||||
|
||||
fn absorb_group_list(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
let Ok(list) = GroupList::parse(event) else {
|
||||
log::warn!(
|
||||
"nip29: ignoring a malformed group list from {}",
|
||||
event.pubkey
|
||||
);
|
||||
log::warn!("ignoring a malformed group list from {}", event.pubkey);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -861,7 +848,7 @@ impl GroupsRegistry {
|
||||
let metadata = match GroupMetadata::parse(event) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: ignoring malformed group metadata from {relay}: {error}");
|
||||
log::warn!("ignoring malformed group metadata from {relay}: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -925,7 +912,7 @@ impl GroupsRegistry {
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(error) = sent.await {
|
||||
log::warn!("nip29: leaving {} failed: {error}", key.id());
|
||||
log::warn!("leaving {} failed: {error}", key.id());
|
||||
return;
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -963,7 +950,7 @@ impl GroupsRegistry {
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
for id in ids {
|
||||
if let Err(error) = client.unsubscribe(&id).await {
|
||||
log::warn!("nip29: unsubscribing {id} failed: {error}");
|
||||
log::warn!("unsubscribing {id} failed: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -995,11 +982,14 @@ impl GroupsRegistry {
|
||||
self.tasks
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(list) => {
|
||||
if let Err(error) = this.update(cx, |this, cx| this.merge(list, cx)) {
|
||||
log::warn!("nip29: applying the cached group list failed: {error}");
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
this.merge(list, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("loading the cached group list failed: {error}");
|
||||
}
|
||||
Err(error) => log::warn!("nip29: loading the cached group list failed: {error}"),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1008,7 +998,7 @@ impl GroupsRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let updates: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
cx.background_spawn(async move {
|
||||
let me = signer.get_public_key_async().await?;
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
|
||||
@@ -1019,14 +1009,9 @@ impl GroupsRegistry {
|
||||
.close_on(opts)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
if let Err(error) = updates.await {
|
||||
log::warn!("nip29: subscribing to the group list failed: {error}");
|
||||
}
|
||||
}));
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.watch_forks(cx);
|
||||
}
|
||||
@@ -1050,7 +1035,6 @@ impl GroupsRegistry {
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
loop {
|
||||
executor.timer(FORK_INTERVAL).await;
|
||||
|
||||
if tx.send_async(Signal::Forks).await.is_err() {
|
||||
break;
|
||||
}
|
||||
@@ -1091,14 +1075,14 @@ impl GroupsRegistry {
|
||||
.with_id(fork_subscription_id())
|
||||
.await
|
||||
{
|
||||
log::warn!("nip29: subscribing to the admins' group lists failed: {error}");
|
||||
log::warn!("subscribing to the admins' group lists failed: {error}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn absorb_fork_list(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
let Ok(list) = GroupList::parse(event) else {
|
||||
log::warn!("nip29: ignoring a malformed admin group list");
|
||||
log::warn!("ignoring a malformed admin group list");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1201,7 +1185,7 @@ impl GroupsRegistry {
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
for id in [id, page] {
|
||||
if let Err(error) = client.unsubscribe(&id).await {
|
||||
log::warn!("nip29: unsubscribing {id} failed: {error}");
|
||||
log::warn!("unsubscribing {id} failed: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1194,7 @@ impl GroupsRegistry {
|
||||
.custom_tag(SingleLetterTag::LOWERCASE_R, cache_tag);
|
||||
|
||||
if let Err(error) = client.database().delete(filter).await {
|
||||
log::warn!("nip29: clearing the group cache failed: {error}");
|
||||
log::warn!("clearing the group cache failed: {error}");
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1271,18 +1255,17 @@ impl GroupsRegistry {
|
||||
let event = match builder.finalize_async(&signer).await {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: signing the group list failed: {error}");
|
||||
log::warn!("signing the group list failed: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match client.send_event(&event).to_nip65().await {
|
||||
Ok(output) if output.failed.is_empty() => {}
|
||||
Ok(output) => log::warn!(
|
||||
"nip29: {} relay(s) rejected the group list",
|
||||
output.failed.len()
|
||||
),
|
||||
Err(error) => log::warn!("nip29: publishing the group list failed: {error}"),
|
||||
Ok(output) => {
|
||||
log::warn!("{} relay(s) rejected the group list", output.failed.len())
|
||||
}
|
||||
Err(error) => log::warn!("publishing the group list failed: {error}"),
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -1307,7 +1290,7 @@ impl GroupsRegistry {
|
||||
.read_with(cx, |group, cx| group.save_envelope(event, cx))
|
||||
.await
|
||||
{
|
||||
log::warn!("nip29: caching failed: {error}");
|
||||
log::warn!("caching failed: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1318,7 +1301,7 @@ impl GroupsRegistry {
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("nip29: reloading {} failed: {error}", logged.id());
|
||||
log::warn!("reloading {} failed: {error}", logged.id());
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -1345,7 +1328,7 @@ impl GroupsRegistry {
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("nip29: loading {} state failed: {error}", logged.id());
|
||||
log::warn!("loading {} state failed: {error}", logged.id());
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -1365,7 +1348,7 @@ impl GroupsRegistry {
|
||||
.read_with(cx, |group, cx| group.save_envelope(event.clone(), cx))
|
||||
.await
|
||||
{
|
||||
log::warn!("nip29: caching failed: {error}");
|
||||
log::warn!("caching failed: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1411,7 +1394,7 @@ impl GroupsRegistry {
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(error) = purge.await {
|
||||
log::warn!("nip29: dropping the deleted envelope failed: {error}");
|
||||
log::warn!("dropping the deleted envelope failed: {error}");
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
group.update(cx, |group, cx| {
|
||||
@@ -1427,7 +1410,7 @@ impl GroupsRegistry {
|
||||
if id == list_subscription_id() {
|
||||
match GroupList::parse(&event) {
|
||||
Ok(list) => self.merge(list, cx),
|
||||
Err(error) => log::warn!("nip29: ignoring a malformed group list: {error}"),
|
||||
Err(error) => log::warn!("ignoring a malformed group list: {error}"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1470,7 +1453,7 @@ impl GroupsRegistry {
|
||||
}
|
||||
|
||||
let Ok(metadata) = GroupMetadata::parse(event) else {
|
||||
log::warn!("nip29: ignoring malformed group metadata from {relay}");
|
||||
log::warn!("ignoring malformed group metadata from {relay}");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1515,12 +1498,12 @@ impl GroupsRegistry {
|
||||
|
||||
fn handle_closed(&mut self, id: SubscriptionId, reason: String, cx: &mut Context<Self>) {
|
||||
if let Some(relay) = self.browse.get(&id) {
|
||||
log::warn!("nip29: browsing {relay} was refused: {reason}");
|
||||
log::warn!("browsing {relay} was refused: {reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
if id == fork_subscription_id() {
|
||||
log::debug!("nip29: the admins' group lists were refused: {reason}");
|
||||
log::debug!("the admins' group lists were refused: {reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1531,7 +1514,7 @@ impl GroupsRegistry {
|
||||
if is_page(&id) {
|
||||
self.routes.remove(&id);
|
||||
log::warn!(
|
||||
"nip29: loading older messages in {} was refused: {reason}",
|
||||
"loading older messages in {} was refused: {reason}",
|
||||
key.id()
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
@@ -214,10 +213,6 @@ impl BrowsePanel {
|
||||
self.render_card_rows(cards, cx)
|
||||
}
|
||||
|
||||
/// A row holds two group cards side by side.
|
||||
///
|
||||
/// The cards are the relative slice of the visible rows, so they pair up
|
||||
/// in order, two per row.
|
||||
fn render_card_rows(
|
||||
&self,
|
||||
cards: &[GroupCandidate],
|
||||
@@ -235,8 +230,6 @@ impl BrowsePanel {
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.children(first.map(|card| self.render_card(card, cx)))
|
||||
// Keep an odd last card in one column: an empty spacer
|
||||
// takes the second slot so the card cannot stretch.
|
||||
.child(match second {
|
||||
Some(card) => self.render_card(card, cx),
|
||||
None => div().flex_1().into_any_element(),
|
||||
@@ -339,7 +332,7 @@ impl BrowsePanel {
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_ellipsis()
|
||||
.line_clamp(3)
|
||||
.line_clamp(2)
|
||||
.map(|this| {
|
||||
if let Some(about) = about {
|
||||
this.child(about)
|
||||
@@ -458,28 +451,31 @@ impl Render for BrowsePanel {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
v_flex()
|
||||
.h_32()
|
||||
.flex_shrink_0()
|
||||
.child(Input::new(&self.input).cleanable(true).flex_1())
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
Button::new("browse-go")
|
||||
.label("Go")
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.browse(window, cx);
|
||||
})),
|
||||
)
|
||||
.when(self.loading || (discover && discovering), |this| {
|
||||
this.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||
}),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.max_w_56()
|
||||
.gap_1()
|
||||
.child(Input::new(&self.input).cleanable(true).flex_1())
|
||||
.child(Button::new("browse-go").label("Go").primary().on_click(
|
||||
cx.listener(|this, _event, window, cx| {
|
||||
this.browse(window, cx);
|
||||
}),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.px_2()
|
||||
.child(body)
|
||||
.child(Scrollbar::vertical(&self.scroll_handle)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user