From eb7175dd571127e3f26b936b09a92e656ae5a988 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 14 Oct 2024 07:34:32 +1300 Subject: [PATCH] code move --- gossip-bin/src/ui/mod.rs | 1048 +++++++++++++++++++------------------- 1 file changed, 523 insertions(+), 525 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index ee878dc89..3b5468c10 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1374,457 +1374,169 @@ impl GossipUi { fn is_scrolling(&self) -> bool { self.current_scroll_offset != 0.0 } -} - -impl eframe::App for GossipUi { - fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { - // Run only on first frame - if self.initializing { - self.initializing = false; - - // Initialize scaling, now that we have a Viewport - self.init_scaling(ctx); - // Set initial menu state, Feed open since initial page is Following. - self.open_menu(ctx, SubMenu::Feeds); + fn enable_ui(&self) -> bool { + !relays::is_entry_dialog_active(self) + && self.person_qr.is_none() + && self.render_qr.is_none() + && self.render_raw.is_none() + } - // Init first page - self.set_page_inner(ctx, self.page.clone()); + fn begin_ui(&self, ui: &mut Ui) { + // if a dialog is open, disable the rest of the UI + if !self.enable_ui() { + ui.disable(); } + } - let max_fps = read_setting!(max_fps) as f32; + pub fn richtext_from_person_nip05(person: &Person) -> RichText { + if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { + if nip05.starts_with("_@") { + nip05 = nip05.get(2..).unwrap().to_string(); + } - if self.future_scroll_offset != 0.0 { - ctx.request_repaint(); + if person.nip05_valid { + RichText::new(nip05).monospace() + } else { + RichText::new(nip05).monospace().strikethrough() + } } else { - // Wait until the next frame - std::thread::sleep(self.next_frame - Instant::now()); - self.next_frame += Duration::from_secs_f32(1.0 / max_fps); - - // Redraw at least once per second - ctx.request_repaint_after(Duration::from_secs(1)); + RichText::default() } + } - if *GLOBALS.read_runstate.borrow() == RunState::ShuttingDown { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - return; - } + pub fn render_person_name_line( + app: &mut GossipUi, + ui: &mut Ui, + person: &Person, + profile_page: bool, + ) { + // Let the 'People' manager know that we are interested in displaying this person. + // It will take all actions necessary to make the data eventually available. + GLOBALS.people.person_of_interest(person.pubkey); - // How much scrolling has been requested by inputs during this frame? - let compose_area_is_focused = - ctx.memory(|mem| mem.has_focus(egui::Id::new("compose_area"))); - let mut requested_scroll: f32 = 0.0; - ctx.input(|i| { - // Consider mouse inputs - requested_scroll = i.raw_scroll_delta.y * read_setting!(mouse_acceleration); + ui.horizontal_wrapped(|ui| { + let followed = person.is_in_list(PersonList::Followed); + let muted = person.is_in_list(PersonList::Muted); + let is_self = if let Some(pubkey) = GLOBALS.identity.public_key() { + pubkey == person.pubkey + } else { + false + }; - // Consider keyboard inputs unless compose area is focused - if !compose_area_is_focused { - if i.key_pressed(egui::Key::ArrowDown) { - requested_scroll -= 50.0; + let tag_name_menu = { + let text = if !profile_page { + person.best_name() + } else { + "ACTIONS".to_string() + }; + RichText::new(format!("☰ {}", text)) + }; + + ui.menu_button(tag_name_menu, |ui| { + if !profile_page { + if ui.button("View Person").clicked() { + app.set_page(ui.ctx(), Page::Person(person.pubkey)); + } } - if i.key_pressed(egui::Key::ArrowUp) { - requested_scroll += 50.0; + if app.page != Page::Feed(FeedKind::Person(person.pubkey)) { + if ui.button("View Their Posts").clicked() { + app.set_page(ui.ctx(), Page::Feed(FeedKind::Person(person.pubkey))); + } } - if i.key_pressed(egui::Key::PageUp) { - let screen_rect = i.screen_rect; - let window_height = screen_rect.max.y - screen_rect.min.y; - requested_scroll += window_height * 0.75; + if GLOBALS.identity.is_unlocked() { + if ui.button("Send DM").clicked() { + let channel = DmChannel::new(&[person.pubkey]); + app.set_page(ui.ctx(), Page::Feed(FeedKind::DmChat(channel))); + } } - if i.key_pressed(egui::Key::PageDown) { - let screen_rect = i.screen_rect; - let window_height = screen_rect.max.y - screen_rect.min.y; - requested_scroll -= window_height * 0.75; + if !followed && ui.button("Follow").clicked() { + let _ = GLOBALS.people.follow( + &person.pubkey, + true, + PersonList::Followed, + Private(false), + ); + } else if followed && ui.button("Unfollow").clicked() { + let _ = GLOBALS.people.follow( + &person.pubkey, + false, + PersonList::Followed, + Private(false), + ); } - } - }); - - // Inertial scrolling - if read_setting!(inertial_scrolling) { - // Apply some of the requested scrolling, and save some for later so that - // scrolling is animated and not instantaneous. - { - self.future_scroll_offset += requested_scroll; - - // Move by 10% of future scroll offsets - self.current_scroll_offset = 0.1 * self.future_scroll_offset; - self.future_scroll_offset -= self.current_scroll_offset; - // Friction stop when slow enough - if self.future_scroll_offset < 1.0 && self.future_scroll_offset > -1.0 { - self.future_scroll_offset = 0.0; + // Do not show 'Mute' if this is yourself + if muted || !is_self { + let mute_label = if muted { "Unmute" } else { "Mute" }; + if ui.button(mute_label).clicked() { + let _ = GLOBALS.people.mute(&person.pubkey, !muted, Private(false)); + app.notecache.invalidate_person(&person.pubkey); + } } - } - } else { - // Changes to the input state have no effect on the scrolling, because it was copied - // into a private FrameState at the start of the frame. - // So we have to use current_scroll_offset to do this - self.current_scroll_offset = requested_scroll; - } - ctx.input_mut(|i| { - i.smooth_scroll_delta.y = self.current_scroll_offset; - }); + if ui.button("Update Metadata").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdateMetadata(person.pubkey)); + } - // F11 maximizes - if ctx.input(|i| i.key_pressed(egui::Key::F11)) { - let maximized = matches!(ctx.input(|i| i.viewport().maximized), Some(true)); - ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(!maximized)); - } + if ui.button("Copy web link").clicked() { + ui.output_mut(|o| { + let mut profile = Profile { + pubkey: person.pubkey, + relays: Vec::new(), + }; + let relays = GLOBALS.people.get_active_person_write_relays(); + for relay_url in relays { + profile.relays.push(UncheckedUrl(format!("{}", relay_url))); + } + o.copied_text = format!("https://njump.me/{}", profile.as_bech32_string()) + }); + } + }); - let mut reapply = false; - let mut theme = Theme::from_settings(); - if theme.follow_os_dark_mode { - // detect if the OS has changed dark/light mode - let os_dark_mode = ctx.style().visuals.dark_mode; - if os_dark_mode != theme.dark_mode { - // switch to the OS setting - write_setting!(dark_mode, os_dark_mode); - theme.dark_mode = os_dark_mode; - reapply = true; + if person.petname.is_some() { + ui.label(RichText::new("†").color(app.theme.accent_complementary_color())) + .on_hover_text("trusted petname"); } - } - if self.theme != theme { - self.theme = theme; - reapply = true; - } - if reapply { - theme::apply_theme(&self.theme, ctx); - } - - notifications::calc(self); - // dialogues first - if relays::is_entry_dialog_active(self) { - relays::entry_dialog(ctx, self); - } + if followed { + ui.label(RichText::new("🚶").small()) + .on_hover_text("followed"); + } - // If login is forced, it takes over - if GLOBALS.wait_for_login.load(Ordering::Relaxed) { - return force_login(self, ctx); - } + if !profile_page { + if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { + if nip05.starts_with("_@") { + nip05 = nip05.get(2..).unwrap().to_string(); + } - // If data migration, show that screen - if GLOBALS.wait_for_data_migration.load(Ordering::Relaxed) { - return wait_for_data_migration(self, ctx); - } + ui.with_layout( + Layout::left_to_right(Align::Min) + .with_cross_align(Align::Center) + .with_cross_justify(true), + |ui| { + if person.nip05_valid { + ui.label(RichText::new(nip05).monospace().small()); + } else { + ui.label(RichText::new(nip05).monospace().small().strikethrough()); + } + }, + ); + } + } + }); + } - // If database is being pruned, show that screen - let optstatus = GLOBALS.prune_status.read(); - if let Some(status) = optstatus.as_ref() { - return wait_for_prune(self, ctx, status); + pub fn try_get_avatar(&mut self, ctx: &Context, pubkey: &PublicKey) -> Option { + // Do not keep retrying if failed + if GLOBALS.failed_avatars.read().contains(pubkey) { + return None; } - // Wizard does its own panels - if let Page::Wizard(wp) = self.page { - return wizard::update(self, ctx, frame, wp); - } - - // Modal dialogue - if let Some(entry) = &self.modal { - if widgets::modal_popup_dyn(ctx, self, true, entry.clone()) - .inner - .clicked() - { - self.modal = None; - } - } - - // Side panel - self.side_panel(ctx); - - let (show_top_post_area, show_bottom_post_area) = if self.show_post_area_fn() { - if read_setting!(posting_area_at_top) { - (true, false) - } else { - (false, true) - } - } else { - (false, false) - }; - - let has_warning = { - #[cfg(feature = "video-ffmpeg")] - { - !self.warn_no_libsdl2_dismissed && self.audio_device.is_none() - } - #[cfg(not(feature = "video-ffmpeg"))] - { - false - } - }; - - egui::TopBottomPanel::top("top-panel") - .frame( - egui::Frame::side_top_panel(&self.theme.get_style()).inner_margin(egui::Margin { - left: 20.0, - right: 15.0, - top: 10.0, - bottom: 10.0, - }), - ) - .resizable(true) - .show_animated( - ctx, - show_top_post_area || has_warning, - |ui| { - self.begin_ui(ui); - #[cfg(feature = "video-ffmpeg")] - { - if has_warning { - widgets::warning_frame(ui, self, |ui, app| { - ui.label("You have compiled gossip with 'video-ffmpeg' option but no audio device was found on your system. Make sure you have followed the instructions at "); - ui.hyperlink("https://github.com/Rust-SDL2/rust-sdl2"); - ui.label("and installed 'libsdl2-dev' package for your system."); - ui.end_row(); - ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { - if ui.link("Dismiss message").clicked() { - app.warn_no_libsdl2_dismissed = true; - } - }); - }); - } - } - if show_top_post_area { - feed::post::posting_area(self, ctx, frame, ui); - } - }, - ); - - let resizable = true; - - egui::TopBottomPanel::bottom("bottom-panel") - .frame({ - let frame = egui::Frame::side_top_panel(&self.theme.get_style()); - frame.inner_margin(egui::Margin { - left: 20.0, - right: 18.0, - top: 10.0, - bottom: 10.0, - }) - }) - .resizable(resizable) - .show_separator_line(false) - .show_animated(ctx, show_bottom_post_area, |ui| { - self.begin_ui(ui); - if show_bottom_post_area { - ui.add_space(7.0); - feed::post::posting_area(self, ctx, frame, ui); - } - }); - - // Prepare local zap data once per frame for easier compute at render time - self.zap_state = (*GLOBALS.current_zap.read()).clone(); - self.note_being_zapped = match self.zap_state { - ZapState::None => None, - ZapState::CheckingLnurl(id, _, _) => Some(id), - ZapState::SeekingAmount(id, _, _, _) => Some(id), - ZapState::LoadingInvoice(id, _) => Some(id), - ZapState::ReadyToPay(id, _) => Some(id), - }; - - egui::CentralPanel::default() - .frame({ - let frame = egui::Frame::central_panel(&self.theme.get_style()); - frame.inner_margin(egui::Margin { - left: 20.0, - right: 10.0, - top: 10.0, - bottom: 0.0, - }) - }) - .show(ctx, |ui| { - self.begin_ui(ui); - match self.page { - Page::DmChatList => dm_chat_list::update(self, ctx, frame, ui), - Page::Feed(_) => feed::update(self, ctx, ui), - Page::Notifications => notifications::update(self, ui), - Page::PeopleLists | Page::PeopleList(_) | Page::Person(_) => { - people::update(self, ctx, frame, ui) - } - Page::YourKeys - | Page::YourMetadata - | Page::YourDelegation - | Page::YourNostrConnect => you::update(self, ctx, frame, ui), - Page::RelaysActivityMonitor - | Page::RelaysCoverage - | Page::RelaysMine - | Page::RelaysKnownNetwork(_) => relays::update(self, ctx, frame, ui), - Page::Search => search::update(self, ctx, frame, ui), - Page::Settings => settings::update(self, ctx, frame, ui), - Page::HelpHelp | Page::HelpStats | Page::HelpAbout => { - help::update(self, ctx, frame, ui) - } - Page::ThemeTest => theme::test_page::update(self, ctx, frame, ui), - Page::Wizard(_) => unreachable!(), - } - }); - } -} - -impl GossipUi { - fn enable_ui(&self) -> bool { - !relays::is_entry_dialog_active(self) - && self.person_qr.is_none() - && self.render_qr.is_none() - && self.render_raw.is_none() - } - - fn begin_ui(&self, ui: &mut Ui) { - // if a dialog is open, disable the rest of the UI - if !self.enable_ui() { - ui.disable(); - } - } - - pub fn richtext_from_person_nip05(person: &Person) -> RichText { - if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { - if nip05.starts_with("_@") { - nip05 = nip05.get(2..).unwrap().to_string(); - } - - if person.nip05_valid { - RichText::new(nip05).monospace() - } else { - RichText::new(nip05).monospace().strikethrough() - } - } else { - RichText::default() - } - } - - pub fn render_person_name_line( - app: &mut GossipUi, - ui: &mut Ui, - person: &Person, - profile_page: bool, - ) { - // Let the 'People' manager know that we are interested in displaying this person. - // It will take all actions necessary to make the data eventually available. - GLOBALS.people.person_of_interest(person.pubkey); - - ui.horizontal_wrapped(|ui| { - let followed = person.is_in_list(PersonList::Followed); - let muted = person.is_in_list(PersonList::Muted); - let is_self = if let Some(pubkey) = GLOBALS.identity.public_key() { - pubkey == person.pubkey - } else { - false - }; - - let tag_name_menu = { - let text = if !profile_page { - person.best_name() - } else { - "ACTIONS".to_string() - }; - RichText::new(format!("☰ {}", text)) - }; - - ui.menu_button(tag_name_menu, |ui| { - if !profile_page { - if ui.button("View Person").clicked() { - app.set_page(ui.ctx(), Page::Person(person.pubkey)); - } - } - if app.page != Page::Feed(FeedKind::Person(person.pubkey)) { - if ui.button("View Their Posts").clicked() { - app.set_page(ui.ctx(), Page::Feed(FeedKind::Person(person.pubkey))); - } - } - if GLOBALS.identity.is_unlocked() { - if ui.button("Send DM").clicked() { - let channel = DmChannel::new(&[person.pubkey]); - app.set_page(ui.ctx(), Page::Feed(FeedKind::DmChat(channel))); - } - } - if !followed && ui.button("Follow").clicked() { - let _ = GLOBALS.people.follow( - &person.pubkey, - true, - PersonList::Followed, - Private(false), - ); - } else if followed && ui.button("Unfollow").clicked() { - let _ = GLOBALS.people.follow( - &person.pubkey, - false, - PersonList::Followed, - Private(false), - ); - } - - // Do not show 'Mute' if this is yourself - if muted || !is_self { - let mute_label = if muted { "Unmute" } else { "Mute" }; - if ui.button(mute_label).clicked() { - let _ = GLOBALS.people.mute(&person.pubkey, !muted, Private(false)); - app.notecache.invalidate_person(&person.pubkey); - } - } - - if ui.button("Update Metadata").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdateMetadata(person.pubkey)); - } - - if ui.button("Copy web link").clicked() { - ui.output_mut(|o| { - let mut profile = Profile { - pubkey: person.pubkey, - relays: Vec::new(), - }; - let relays = GLOBALS.people.get_active_person_write_relays(); - for relay_url in relays { - profile.relays.push(UncheckedUrl(format!("{}", relay_url))); - } - o.copied_text = format!("https://njump.me/{}", profile.as_bech32_string()) - }); - } - }); - - if person.petname.is_some() { - ui.label(RichText::new("†").color(app.theme.accent_complementary_color())) - .on_hover_text("trusted petname"); - } - - if followed { - ui.label(RichText::new("🚶").small()) - .on_hover_text("followed"); - } - - if !profile_page { - if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { - if nip05.starts_with("_@") { - nip05 = nip05.get(2..).unwrap().to_string(); - } - - ui.with_layout( - Layout::left_to_right(Align::Min) - .with_cross_align(Align::Center) - .with_cross_justify(true), - |ui| { - if person.nip05_valid { - ui.label(RichText::new(nip05).monospace().small()); - } else { - ui.label(RichText::new(nip05).monospace().small().strikethrough()); - } - }, - ); - } - } - }); - } - - pub fn try_get_avatar(&mut self, ctx: &Context, pubkey: &PublicKey) -> Option { - // Do not keep retrying if failed - if GLOBALS.failed_avatars.read().contains(pubkey) { - return None; - } - - if let Some(th) = self.avatars.get(pubkey) { - return Some(th.to_owned()); + if let Some(th) = self.avatars.get(pubkey) { + return Some(th.to_owned()); } if let Some(rgba_image) = @@ -2144,124 +1856,410 @@ impl GossipUi { return; } - // Update when this happened, so we don't accept again too rapidly - self.last_visible_update = Instant::now(); + // Update when this happened, so we don't accept again too rapidly + self.last_visible_update = Instant::now(); + + // Save to self.visible_note_ids + self.visible_note_ids = std::mem::take(&mut self.next_visible_note_ids); + + if !self.visible_note_ids.is_empty() { + tracing::trace!( + "VISIBLE = {:?}", + self.visible_note_ids + .iter() + .map(|id| id.as_hex_string().as_str().get(0..10).unwrap().to_owned()) + .collect::>() + ); + + // Tell the overlord + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::VisibleNotesChanged( + self.visible_note_ids.clone(), + )); + } + } + + // Zap In Progress Area + fn render_zap_area(&mut self, ui: &mut Ui) { + let mut qr_string: Option = None; + + match self.zap_state { + ZapState::None => return, // should not occur + ZapState::CheckingLnurl(_id, _pubkey, ref _lnurl) => { + ui.label("Loading lnurl..."); + } + ZapState::SeekingAmount(id, pubkey, ref _prd, ref _lnurl) => { + let mut amt = 0; + ui.label("Zap Amount:"); + + let amounts = [1, 2, 5, 10, 21, 46, 100, 215, 464, 1000, 2154, 4642, 10000]; + for &amount in &amounts { + if ui.button(amount.to_string()).clicked() { + amt = amount; + } + } + + if amt > 0 { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::Zap( + id, + pubkey, + MilliSatoshi(amt * 1_000), + "".to_owned(), + )); + } + if ui.button("Cancel").clicked() { + *GLOBALS.current_zap.write() = ZapState::None; + } + } + ZapState::LoadingInvoice(_id, _pubkey) => { + ui.label("Loading zap invoice..."); + } + ZapState::ReadyToPay(_id, ref invoice) => { + // we have to copy it and get out of the borrow first + qr_string = Some(invoice.to_owned()); + } + }; + + if let Some(qr) = qr_string { + // Show the QR code and a close button + self.render_qr(ui, "zap", &qr.to_uppercase()); + if ui.button("Close").clicked() { + *GLOBALS.current_zap.write() = ZapState::None; + } + } + } + + fn reset_draft(&mut self) { + if let Page::Feed(FeedKind::DmChat(_)) = &self.page { + self.dm_draft_data.clear(); + self.dm_draft_data_target = None; + } else { + self.draft_data.clear(); + self.show_post_area = false; + self.draft_needs_focus = false; + } + } + + fn show_post_area_fn(&self) -> bool { + if self.page == Page::DmChatList { + return false; + } + + self.show_post_area || matches!(self.page, Page::Feed(FeedKind::DmChat(_))) + } + + #[inline] + fn vert_scroll_area(&self) -> ScrollArea { + ScrollArea::vertical().enable_scrolling(self.enable_ui()) + } + + fn render_status_queue_area(&self, ui: &mut Ui) { + let messages = GLOBALS.status_queue.read().read_all(); + if ui + .add(Label::new(RichText::new(&messages[0])).sense(Sense::click())) + .clicked() + { + GLOBALS.status_queue.write().dismiss(0); + } + if ui + .add(Label::new(RichText::new(&messages[1]).small()).sense(Sense::click())) + .clicked() + { + GLOBALS.status_queue.write().dismiss(1); + } + if ui + .add(Label::new(RichText::new(&messages[2]).weak().small()).sense(Sense::click())) + .clicked() + { + GLOBALS.status_queue.write().dismiss(2); + } + } +} + +impl eframe::App for GossipUi { + fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { + // Run only on first frame + if self.initializing { + self.initializing = false; + + // Initialize scaling, now that we have a Viewport + self.init_scaling(ctx); + + // Set initial menu state, Feed open since initial page is Following. + self.open_menu(ctx, SubMenu::Feeds); + + // Init first page + self.set_page_inner(ctx, self.page.clone()); + } + + let max_fps = read_setting!(max_fps) as f32; + + if self.future_scroll_offset != 0.0 { + ctx.request_repaint(); + } else { + // Wait until the next frame + std::thread::sleep(self.next_frame - Instant::now()); + self.next_frame += Duration::from_secs_f32(1.0 / max_fps); + + // Redraw at least once per second + ctx.request_repaint_after(Duration::from_secs(1)); + } + + if *GLOBALS.read_runstate.borrow() == RunState::ShuttingDown { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + return; + } + + // How much scrolling has been requested by inputs during this frame? + let compose_area_is_focused = + ctx.memory(|mem| mem.has_focus(egui::Id::new("compose_area"))); + let mut requested_scroll: f32 = 0.0; + ctx.input(|i| { + // Consider mouse inputs + requested_scroll = i.raw_scroll_delta.y * read_setting!(mouse_acceleration); + + // Consider keyboard inputs unless compose area is focused + if !compose_area_is_focused { + if i.key_pressed(egui::Key::ArrowDown) { + requested_scroll -= 50.0; + } + if i.key_pressed(egui::Key::ArrowUp) { + requested_scroll += 50.0; + } + if i.key_pressed(egui::Key::PageUp) { + let screen_rect = i.screen_rect; + let window_height = screen_rect.max.y - screen_rect.min.y; + requested_scroll += window_height * 0.75; + } + if i.key_pressed(egui::Key::PageDown) { + let screen_rect = i.screen_rect; + let window_height = screen_rect.max.y - screen_rect.min.y; + requested_scroll -= window_height * 0.75; + } + } + }); + + // Inertial scrolling + if read_setting!(inertial_scrolling) { + // Apply some of the requested scrolling, and save some for later so that + // scrolling is animated and not instantaneous. + { + self.future_scroll_offset += requested_scroll; + + // Move by 10% of future scroll offsets + self.current_scroll_offset = 0.1 * self.future_scroll_offset; + self.future_scroll_offset -= self.current_scroll_offset; + + // Friction stop when slow enough + if self.future_scroll_offset < 1.0 && self.future_scroll_offset > -1.0 { + self.future_scroll_offset = 0.0; + } + } + } else { + // Changes to the input state have no effect on the scrolling, because it was copied + // into a private FrameState at the start of the frame. + // So we have to use current_scroll_offset to do this + self.current_scroll_offset = requested_scroll; + } + + ctx.input_mut(|i| { + i.smooth_scroll_delta.y = self.current_scroll_offset; + }); + + // F11 maximizes + if ctx.input(|i| i.key_pressed(egui::Key::F11)) { + let maximized = matches!(ctx.input(|i| i.viewport().maximized), Some(true)); + ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(!maximized)); + } + + let mut reapply = false; + let mut theme = Theme::from_settings(); + if theme.follow_os_dark_mode { + // detect if the OS has changed dark/light mode + let os_dark_mode = ctx.style().visuals.dark_mode; + if os_dark_mode != theme.dark_mode { + // switch to the OS setting + write_setting!(dark_mode, os_dark_mode); + theme.dark_mode = os_dark_mode; + reapply = true; + } + } + if self.theme != theme { + self.theme = theme; + reapply = true; + } + if reapply { + theme::apply_theme(&self.theme, ctx); + } + + notifications::calc(self); - // Save to self.visible_note_ids - self.visible_note_ids = std::mem::take(&mut self.next_visible_note_ids); + // dialogues first + if relays::is_entry_dialog_active(self) { + relays::entry_dialog(ctx, self); + } - if !self.visible_note_ids.is_empty() { - tracing::trace!( - "VISIBLE = {:?}", - self.visible_note_ids - .iter() - .map(|id| id.as_hex_string().as_str().get(0..10).unwrap().to_owned()) - .collect::>() - ); + // If login is forced, it takes over + if GLOBALS.wait_for_login.load(Ordering::Relaxed) { + return force_login(self, ctx); + } - // Tell the overlord - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::VisibleNotesChanged( - self.visible_note_ids.clone(), - )); + // If data migration, show that screen + if GLOBALS.wait_for_data_migration.load(Ordering::Relaxed) { + return wait_for_data_migration(self, ctx); } - } - // Zap In Progress Area - fn render_zap_area(&mut self, ui: &mut Ui) { - let mut qr_string: Option = None; + // If database is being pruned, show that screen + let optstatus = GLOBALS.prune_status.read(); + if let Some(status) = optstatus.as_ref() { + return wait_for_prune(self, ctx, status); + } - match self.zap_state { - ZapState::None => return, // should not occur - ZapState::CheckingLnurl(_id, _pubkey, ref _lnurl) => { - ui.label("Loading lnurl..."); + // Wizard does its own panels + if let Page::Wizard(wp) = self.page { + return wizard::update(self, ctx, frame, wp); + } + + // Modal dialogue + if let Some(entry) = &self.modal { + if widgets::modal_popup_dyn(ctx, self, true, entry.clone()) + .inner + .clicked() + { + self.modal = None; } - ZapState::SeekingAmount(id, pubkey, ref _prd, ref _lnurl) => { - let mut amt = 0; - ui.label("Zap Amount:"); + } - let amounts = [1, 2, 5, 10, 21, 46, 100, 215, 464, 1000, 2154, 4642, 10000]; - for &amount in &amounts { - if ui.button(amount.to_string()).clicked() { - amt = amount; - } - } + // Side panel + self.side_panel(ctx); - if amt > 0 { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::Zap( - id, - pubkey, - MilliSatoshi(amt * 1_000), - "".to_owned(), - )); - } - if ui.button("Cancel").clicked() { - *GLOBALS.current_zap.write() = ZapState::None; - } - } - ZapState::LoadingInvoice(_id, _pubkey) => { - ui.label("Loading zap invoice..."); - } - ZapState::ReadyToPay(_id, ref invoice) => { - // we have to copy it and get out of the borrow first - qr_string = Some(invoice.to_owned()); + let (show_top_post_area, show_bottom_post_area) = if self.show_post_area_fn() { + if read_setting!(posting_area_at_top) { + (true, false) + } else { + (false, true) } + } else { + (false, false) }; - if let Some(qr) = qr_string { - // Show the QR code and a close button - self.render_qr(ui, "zap", &qr.to_uppercase()); - if ui.button("Close").clicked() { - *GLOBALS.current_zap.write() = ZapState::None; + let has_warning = { + #[cfg(feature = "video-ffmpeg")] + { + !self.warn_no_libsdl2_dismissed && self.audio_device.is_none() } - } - } + #[cfg(not(feature = "video-ffmpeg"))] + { + false + } + }; - fn reset_draft(&mut self) { - if let Page::Feed(FeedKind::DmChat(_)) = &self.page { - self.dm_draft_data.clear(); - self.dm_draft_data_target = None; - } else { - self.draft_data.clear(); - self.show_post_area = false; - self.draft_needs_focus = false; - } - } + egui::TopBottomPanel::top("top-panel") + .frame( + egui::Frame::side_top_panel(&self.theme.get_style()).inner_margin(egui::Margin { + left: 20.0, + right: 15.0, + top: 10.0, + bottom: 10.0, + }), + ) + .resizable(true) + .show_animated( + ctx, + show_top_post_area || has_warning, + |ui| { + self.begin_ui(ui); + #[cfg(feature = "video-ffmpeg")] + { + if has_warning { + widgets::warning_frame(ui, self, |ui, app| { + ui.label("You have compiled gossip with 'video-ffmpeg' option but no audio device was found on your system. Make sure you have followed the instructions at "); + ui.hyperlink("https://github.com/Rust-SDL2/rust-sdl2"); + ui.label("and installed 'libsdl2-dev' package for your system."); + ui.end_row(); + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { + if ui.link("Dismiss message").clicked() { + app.warn_no_libsdl2_dismissed = true; + } + }); + }); + } + } + if show_top_post_area { + feed::post::posting_area(self, ctx, frame, ui); + } + }, + ); - fn show_post_area_fn(&self) -> bool { - if self.page == Page::DmChatList { - return false; - } + let resizable = true; - self.show_post_area || matches!(self.page, Page::Feed(FeedKind::DmChat(_))) - } + egui::TopBottomPanel::bottom("bottom-panel") + .frame({ + let frame = egui::Frame::side_top_panel(&self.theme.get_style()); + frame.inner_margin(egui::Margin { + left: 20.0, + right: 18.0, + top: 10.0, + bottom: 10.0, + }) + }) + .resizable(resizable) + .show_separator_line(false) + .show_animated(ctx, show_bottom_post_area, |ui| { + self.begin_ui(ui); + if show_bottom_post_area { + ui.add_space(7.0); + feed::post::posting_area(self, ctx, frame, ui); + } + }); - #[inline] - fn vert_scroll_area(&self) -> ScrollArea { - ScrollArea::vertical().enable_scrolling(self.enable_ui()) - } + // Prepare local zap data once per frame for easier compute at render time + self.zap_state = (*GLOBALS.current_zap.read()).clone(); + self.note_being_zapped = match self.zap_state { + ZapState::None => None, + ZapState::CheckingLnurl(id, _, _) => Some(id), + ZapState::SeekingAmount(id, _, _, _) => Some(id), + ZapState::LoadingInvoice(id, _) => Some(id), + ZapState::ReadyToPay(id, _) => Some(id), + }; - fn render_status_queue_area(&self, ui: &mut Ui) { - let messages = GLOBALS.status_queue.read().read_all(); - if ui - .add(Label::new(RichText::new(&messages[0])).sense(Sense::click())) - .clicked() - { - GLOBALS.status_queue.write().dismiss(0); - } - if ui - .add(Label::new(RichText::new(&messages[1]).small()).sense(Sense::click())) - .clicked() - { - GLOBALS.status_queue.write().dismiss(1); - } - if ui - .add(Label::new(RichText::new(&messages[2]).weak().small()).sense(Sense::click())) - .clicked() - { - GLOBALS.status_queue.write().dismiss(2); - } + egui::CentralPanel::default() + .frame({ + let frame = egui::Frame::central_panel(&self.theme.get_style()); + frame.inner_margin(egui::Margin { + left: 20.0, + right: 10.0, + top: 10.0, + bottom: 0.0, + }) + }) + .show(ctx, |ui| { + self.begin_ui(ui); + match self.page { + Page::DmChatList => dm_chat_list::update(self, ctx, frame, ui), + Page::Feed(_) => feed::update(self, ctx, ui), + Page::Notifications => notifications::update(self, ui), + Page::PeopleLists | Page::PeopleList(_) | Page::Person(_) => { + people::update(self, ctx, frame, ui) + } + Page::YourKeys + | Page::YourMetadata + | Page::YourDelegation + | Page::YourNostrConnect => you::update(self, ctx, frame, ui), + Page::RelaysActivityMonitor + | Page::RelaysCoverage + | Page::RelaysMine + | Page::RelaysKnownNetwork(_) => relays::update(self, ctx, frame, ui), + Page::Search => search::update(self, ctx, frame, ui), + Page::Settings => settings::update(self, ctx, frame, ui), + Page::HelpHelp | Page::HelpStats | Page::HelpAbout => { + help::update(self, ctx, frame, ui) + } + Page::ThemeTest => theme::test_page::update(self, ctx, frame, ui), + Page::Wizard(_) => unreachable!(), + } + }); } }