- Added `SoftKeyboardContext` enum and support for calculating keyboard insets from both virtual and platform sources - Updated `AppContext` to provide `soft_keyboard_rect` for determining visible keyboard area - Adjusted UI rendering to shift content when input boxes intersect with the soft keyboard, preventing overlap - Modified `MainActivity` and Android manifest to use `windowSoftInputMode="adjustResize"` and updated window inset handling - Introduced helper functions (`include_input`, `input_rect`, `clear_input_rect`) in `notedeck_ui` for tracking focused input boxes - Fixed Android JNI keyboard height reporting to clamp negative values Together, these changes allow the app to correctly detect and respond to soft keyboard visibility on Android, ensuring input fields remain accessible when typing. Fixes: https://github.com/damus-io/notedeck/issues/946 Fixes: https://github.com/damus-io/notedeck/issues/1043
1143 lines
39 KiB
Rust
1143 lines
39 KiB
Rust
use crate::{
|
|
accounts::{render_accounts_route, AccountsAction, AccountsResponse},
|
|
app::{get_active_columns_mut, get_decks_mut},
|
|
column::ColumnsAction,
|
|
deck_state::DeckState,
|
|
decks::{Deck, DecksAction, DecksCache},
|
|
drag::{get_drag_id, get_drag_id_through_frame},
|
|
options::AppOptions,
|
|
profile::{ProfileAction, SaveProfileChanges},
|
|
route::{Route, Router, SingletonRouter},
|
|
subscriptions::Subscriptions,
|
|
timeline::{
|
|
kind::ListKind,
|
|
route::{render_thread_route, render_timeline_route},
|
|
TimelineCache, TimelineKind,
|
|
},
|
|
ui::{
|
|
self,
|
|
add_column::{render_add_column_routes, AddColumnView},
|
|
column::NavTitle,
|
|
configure_deck::ConfigureDeckView,
|
|
edit_deck::{EditDeckResponse, EditDeckView},
|
|
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
|
onboarding::FollowPackOnboardingView,
|
|
profile::EditProfileView,
|
|
search::{FocusState, SearchView},
|
|
settings::SettingsAction,
|
|
support::SupportView,
|
|
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
|
|
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
|
|
TimelineView,
|
|
},
|
|
Damus,
|
|
};
|
|
|
|
use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet};
|
|
use enostr::ProfileState;
|
|
use nostrdb::{Filter, Ndb, Transaction};
|
|
use notedeck::{
|
|
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
|
RelayAction,
|
|
};
|
|
use notedeck_ui::NoteOptions;
|
|
use tracing::error;
|
|
|
|
/// The result of processing a nav response
|
|
pub enum ProcessNavResult {
|
|
SwitchOccurred,
|
|
PfpClicked,
|
|
}
|
|
|
|
impl ProcessNavResult {
|
|
pub fn switch_occurred(&self) -> bool {
|
|
matches!(self, Self::SwitchOccurred)
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::enum_variant_names)]
|
|
pub enum RenderNavAction {
|
|
Back,
|
|
RemoveColumn,
|
|
/// The response when the user interacts with a pfp in the nav header
|
|
PfpClicked,
|
|
PostAction(NewPostAction),
|
|
NoteAction(NoteAction),
|
|
ProfileAction(ProfileAction),
|
|
SwitchingAction(SwitchingAction),
|
|
WalletAction(WalletAction),
|
|
RelayAction(RelayAction),
|
|
SettingsAction(SettingsAction),
|
|
}
|
|
|
|
pub enum SwitchingAction {
|
|
Accounts(AccountsAction),
|
|
Columns(ColumnsAction),
|
|
Decks(crate::decks::DecksAction),
|
|
}
|
|
|
|
impl SwitchingAction {
|
|
/// process the action, and return whether switching occured
|
|
pub fn process(
|
|
&self,
|
|
timeline_cache: &mut TimelineCache,
|
|
decks_cache: &mut DecksCache,
|
|
ctx: &mut AppContext<'_>,
|
|
subs: &mut Subscriptions,
|
|
ui_ctx: &egui::Context,
|
|
) -> bool {
|
|
match &self {
|
|
SwitchingAction::Accounts(account_action) => match account_action {
|
|
AccountsAction::Switch(switch_action) => {
|
|
{
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
ctx.accounts.select_account(
|
|
&switch_action.switch_to,
|
|
ctx.ndb,
|
|
&txn,
|
|
ctx.pool,
|
|
ui_ctx,
|
|
);
|
|
|
|
let contacts_sub = ctx.accounts.get_subs().contacts.remote.clone();
|
|
// this is cringe but we're gonna get a new sub manager soon...
|
|
subs.subs.insert(
|
|
contacts_sub,
|
|
crate::subscriptions::SubKind::FetchingContactList(TimelineKind::List(
|
|
ListKind::Contact(*ctx.accounts.selected_account_pubkey()),
|
|
)),
|
|
);
|
|
}
|
|
|
|
if switch_action.switching_to_new {
|
|
decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to);
|
|
}
|
|
|
|
// pop nav after switch
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
|
|
.column_mut(switch_action.source_column)
|
|
.router_mut()
|
|
.go_back();
|
|
}
|
|
AccountsAction::Remove(to_remove) => 's: {
|
|
if !ctx
|
|
.accounts
|
|
.remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx)
|
|
{
|
|
break 's;
|
|
}
|
|
|
|
decks_cache.remove(ctx.i18n, to_remove, timeline_cache, ctx.ndb, ctx.pool);
|
|
}
|
|
},
|
|
SwitchingAction::Columns(columns_action) => match *columns_action {
|
|
ColumnsAction::Remove(index) => {
|
|
let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
|
|
.delete_column(index);
|
|
for kind in &kinds_to_pop {
|
|
if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
|
error!("error popping timeline: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnsAction::Switch(from, to) => {
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to);
|
|
}
|
|
},
|
|
SwitchingAction::Decks(decks_action) => match *decks_action {
|
|
DecksAction::Switch(index) => {
|
|
get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index)
|
|
}
|
|
DecksAction::Removing(index) => {
|
|
get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck(
|
|
index,
|
|
timeline_cache,
|
|
ctx.ndb,
|
|
ctx.pool,
|
|
);
|
|
}
|
|
},
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
impl From<PostAction> for RenderNavAction {
|
|
fn from(post_action: PostAction) -> Self {
|
|
match post_action {
|
|
PostAction::QuotedNoteAction(note_action) => Self::NoteAction(note_action),
|
|
PostAction::NewPostAction(new_post) => Self::PostAction(new_post),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<NewPostAction> for RenderNavAction {
|
|
fn from(post_action: NewPostAction) -> Self {
|
|
Self::PostAction(post_action)
|
|
}
|
|
}
|
|
|
|
impl From<NoteAction> for RenderNavAction {
|
|
fn from(note_action: NoteAction) -> RenderNavAction {
|
|
Self::NoteAction(note_action)
|
|
}
|
|
}
|
|
|
|
enum NotedeckNavResponse {
|
|
Popup(Box<PopupResponse<Option<RenderNavAction>>>),
|
|
Nav(Box<NavResponse<Option<RenderNavAction>>>),
|
|
}
|
|
|
|
pub struct RenderNavResponse {
|
|
column: usize,
|
|
response: NotedeckNavResponse,
|
|
}
|
|
|
|
impl RenderNavResponse {
|
|
#[allow(private_interfaces)]
|
|
pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
|
|
RenderNavResponse { column, response }
|
|
}
|
|
|
|
#[must_use = "Make sure to save columns if result is true"]
|
|
pub fn process_render_nav_response(
|
|
self,
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext<'_>,
|
|
ui: &mut egui::Ui,
|
|
) -> Option<ProcessNavResult> {
|
|
match self.response {
|
|
NotedeckNavResponse::Popup(nav_action) => {
|
|
process_popup_resp(*nav_action, app, ctx, ui, self.column)
|
|
}
|
|
NotedeckNavResponse::Nav(nav_response) => {
|
|
process_nav_resp(app, ctx, ui, *nav_response, self.column)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_popup_resp(
|
|
action: PopupResponse<Option<RenderNavAction>>,
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext<'_>,
|
|
ui: &mut egui::Ui,
|
|
col: usize,
|
|
) -> Option<ProcessNavResult> {
|
|
let mut process_result: Option<ProcessNavResult> = None;
|
|
if let Some(nav_action) = action.response {
|
|
process_result = process_render_nav_action(app, ctx, ui, col, nav_action);
|
|
}
|
|
|
|
if let Some(NavAction::Returned(_)) = action.action {
|
|
let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
|
|
column.sheet_router.clear();
|
|
} else if let Some(NavAction::Navigating) = action.action {
|
|
let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
|
|
column.sheet_router.navigating = false;
|
|
}
|
|
|
|
process_result
|
|
}
|
|
|
|
fn process_nav_resp(
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext<'_>,
|
|
ui: &mut egui::Ui,
|
|
response: NavResponse<Option<RenderNavAction>>,
|
|
col: usize,
|
|
) -> Option<ProcessNavResult> {
|
|
let mut process_result: Option<ProcessNavResult> = None;
|
|
|
|
if let Some(action) = response.response.or(response.title_response) {
|
|
// start returning when we're finished posting
|
|
|
|
process_result = process_render_nav_action(app, ctx, ui, col, action);
|
|
}
|
|
|
|
if let Some(action) = response.action {
|
|
match action {
|
|
NavAction::Returned(return_type) => {
|
|
let r = app
|
|
.columns_mut(ctx.i18n, ctx.accounts)
|
|
.column_mut(col)
|
|
.router_mut()
|
|
.pop();
|
|
|
|
if let Some(Route::Timeline(kind)) = &r {
|
|
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
|
error!("popping timeline had an error: {err} for {:?}", kind);
|
|
}
|
|
};
|
|
|
|
if let Some(Route::Thread(selection)) = &r {
|
|
app.threads
|
|
.close(ctx.ndb, ctx.pool, selection, return_type, col);
|
|
}
|
|
|
|
// we should remove profile state once we've returned
|
|
if let Some(Route::EditProfile(pk)) = &r {
|
|
app.view_state.pubkey_to_profile_state.remove(pk);
|
|
}
|
|
|
|
process_result = Some(ProcessNavResult::SwitchOccurred);
|
|
}
|
|
|
|
NavAction::Navigated => {
|
|
let cur_router = app
|
|
.columns_mut(ctx.i18n, ctx.accounts)
|
|
.column_mut(col)
|
|
.router_mut();
|
|
cur_router.navigating = false;
|
|
if cur_router.is_replacing() {
|
|
cur_router.remove_previous_routes();
|
|
}
|
|
|
|
process_result = Some(ProcessNavResult::SwitchOccurred);
|
|
}
|
|
|
|
NavAction::Dragging => {}
|
|
NavAction::Returning(_) => {}
|
|
NavAction::Resetting => {}
|
|
NavAction::Navigating => {
|
|
// explicitly update the edit profile state when navigating
|
|
handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
|
|
}
|
|
}
|
|
}
|
|
|
|
process_result
|
|
}
|
|
|
|
/// We are navigating to edit profile, prepare the profile state
|
|
/// if we don't have it
|
|
fn handle_navigating_edit_profile(ndb: &Ndb, accounts: &Accounts, app: &mut Damus, col: usize) {
|
|
let pk = {
|
|
let Route::EditProfile(pk) = app.columns(accounts).column(col).router().top() else {
|
|
return;
|
|
};
|
|
|
|
if app.view_state.pubkey_to_profile_state.contains_key(pk) {
|
|
return;
|
|
}
|
|
|
|
pk.to_owned()
|
|
};
|
|
|
|
let txn = Transaction::new(ndb).expect("txn");
|
|
app.view_state.pubkey_to_profile_state.insert(pk, {
|
|
let filter = Filter::new_with_capacity(1)
|
|
.kinds([0])
|
|
.authors([pk.bytes()])
|
|
.build();
|
|
|
|
if let Ok(results) = ndb.query(&txn, &[filter], 1) {
|
|
if let Some(result) = results.first() {
|
|
tracing::debug!(
|
|
"refreshing profile state for edit view: {}",
|
|
result.note.content()
|
|
);
|
|
ProfileState::from_note_contents(result.note.content())
|
|
} else {
|
|
ProfileState::default()
|
|
}
|
|
} else {
|
|
ProfileState::default()
|
|
}
|
|
});
|
|
}
|
|
|
|
pub enum RouterAction {
|
|
GoBack,
|
|
/// We clicked on a pfp in a route. We currently don't carry any
|
|
/// information about the pfp since we only use it for toggling the
|
|
/// chrome atm
|
|
PfpClicked,
|
|
RouteTo(Route, RouterType),
|
|
Overlay {
|
|
route: Route,
|
|
make_new: bool,
|
|
},
|
|
}
|
|
|
|
pub enum RouterType {
|
|
Sheet,
|
|
Stack,
|
|
}
|
|
|
|
fn go_back(stack: &mut Router<Route>, sheet: &mut SingletonRouter<Route>) {
|
|
if sheet.route().is_some() {
|
|
sheet.go_back();
|
|
} else {
|
|
stack.go_back();
|
|
}
|
|
}
|
|
|
|
impl RouterAction {
|
|
pub fn process(
|
|
self,
|
|
stack_router: &mut Router<Route>,
|
|
sheet_router: &mut SingletonRouter<Route>,
|
|
) -> Option<ProcessNavResult> {
|
|
match self {
|
|
RouterAction::GoBack => {
|
|
go_back(stack_router, sheet_router);
|
|
|
|
None
|
|
}
|
|
|
|
RouterAction::PfpClicked => {
|
|
if stack_router.routes().len() == 1 {
|
|
// if we're at the top level and we click a profile pic,
|
|
// bubble it up so that it can be handled by the chrome
|
|
// to open the sidebar
|
|
Some(ProcessNavResult::PfpClicked)
|
|
} else {
|
|
// Otherwise just execute a back action
|
|
go_back(stack_router, sheet_router);
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
RouterAction::RouteTo(route, router_type) => match router_type {
|
|
RouterType::Sheet => {
|
|
sheet_router.route_to(route);
|
|
None
|
|
}
|
|
RouterType::Stack => {
|
|
stack_router.route_to(route);
|
|
None
|
|
}
|
|
},
|
|
RouterAction::Overlay { route, make_new } => {
|
|
if make_new {
|
|
stack_router.route_to_overlaid_new(route);
|
|
} else {
|
|
stack_router.route_to_overlaid(route);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn route_to(route: Route) -> Self {
|
|
RouterAction::RouteTo(route, RouterType::Stack)
|
|
}
|
|
|
|
pub fn route_to_sheet(route: Route) -> Self {
|
|
RouterAction::RouteTo(route, RouterType::Sheet)
|
|
}
|
|
}
|
|
|
|
fn process_render_nav_action(
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext<'_>,
|
|
ui: &mut egui::Ui,
|
|
col: usize,
|
|
action: RenderNavAction,
|
|
) -> Option<ProcessNavResult> {
|
|
let router_action = match action {
|
|
RenderNavAction::Back => Some(RouterAction::GoBack),
|
|
RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
|
|
RenderNavAction::RemoveColumn => {
|
|
let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col);
|
|
|
|
for kind in &kinds_to_pop {
|
|
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
|
error!("error popping timeline: {err}");
|
|
}
|
|
}
|
|
|
|
return Some(ProcessNavResult::SwitchOccurred);
|
|
}
|
|
RenderNavAction::PostAction(new_post_action) => {
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
match new_post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts) {
|
|
Err(err) => tracing::error!("Error executing post action: {err}"),
|
|
Ok(_) => tracing::debug!("Post action executed"),
|
|
}
|
|
|
|
Some(RouterAction::GoBack)
|
|
}
|
|
RenderNavAction::NoteAction(note_action) => {
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
|
|
crate::actionbar::execute_and_process_note_action(
|
|
note_action,
|
|
ctx.ndb,
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
|
|
col,
|
|
&mut app.timeline_cache,
|
|
&mut app.threads,
|
|
ctx.note_cache,
|
|
ctx.pool,
|
|
&txn,
|
|
ctx.unknown_ids,
|
|
ctx.accounts,
|
|
ctx.global_wallet,
|
|
ctx.zaps,
|
|
ctx.img_cache,
|
|
&mut app.view_state,
|
|
ui,
|
|
)
|
|
}
|
|
RenderNavAction::SwitchingAction(switching_action) => {
|
|
if switching_action.process(
|
|
&mut app.timeline_cache,
|
|
&mut app.decks_cache,
|
|
ctx,
|
|
&mut app.subscriptions,
|
|
ui.ctx(),
|
|
) {
|
|
return Some(ProcessNavResult::SwitchOccurred);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
RenderNavAction::ProfileAction(profile_action) => {
|
|
profile_action.process_profile_action(ctx.ndb, ctx.pool, ctx.accounts)
|
|
}
|
|
RenderNavAction::WalletAction(wallet_action) => {
|
|
wallet_action.process(ctx.accounts, ctx.global_wallet)
|
|
}
|
|
RenderNavAction::RelayAction(action) => {
|
|
ctx.accounts
|
|
.process_relay_action(ui.ctx(), ctx.pool, action);
|
|
None
|
|
}
|
|
RenderNavAction::SettingsAction(action) => {
|
|
action.process_settings_action(app, ctx.settings, ctx.i18n, ctx.img_cache, ui.ctx())
|
|
}
|
|
};
|
|
|
|
if let Some(action) = router_action {
|
|
let cols =
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col);
|
|
let router = &mut cols.router;
|
|
let sheet_router = &mut cols.sheet_router;
|
|
|
|
action.process(router, sheet_router)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn render_nav_body(
|
|
ui: &mut egui::Ui,
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext,
|
|
top: &Route,
|
|
depth: usize,
|
|
col: usize,
|
|
inner_rect: egui::Rect,
|
|
) -> Option<RenderNavAction> {
|
|
let mut note_context = NoteContext {
|
|
ndb: ctx.ndb,
|
|
accounts: ctx.accounts,
|
|
img_cache: ctx.img_cache,
|
|
note_cache: ctx.note_cache,
|
|
zaps: ctx.zaps,
|
|
pool: ctx.pool,
|
|
job_pool: ctx.job_pool,
|
|
unknown_ids: ctx.unknown_ids,
|
|
clipboard: ctx.clipboard,
|
|
i18n: ctx.i18n,
|
|
global_wallet: ctx.global_wallet,
|
|
};
|
|
match top {
|
|
Route::Timeline(kind) => {
|
|
// did something request scroll to top for the selection column?
|
|
let scroll_to_top = app
|
|
.decks_cache
|
|
.selected_column_index(ctx.accounts)
|
|
.is_some_and(|ind| ind == col)
|
|
&& app.options.contains(AppOptions::ScrollToTop);
|
|
|
|
let nav_action = render_timeline_route(
|
|
&mut app.timeline_cache,
|
|
kind,
|
|
col,
|
|
app.note_options,
|
|
depth,
|
|
ui,
|
|
&mut note_context,
|
|
&mut app.jobs,
|
|
scroll_to_top,
|
|
);
|
|
|
|
app.timeline_cache.set_fresh(kind);
|
|
|
|
// always clear the scroll_to_top request
|
|
if scroll_to_top {
|
|
app.options.remove(AppOptions::ScrollToTop);
|
|
}
|
|
|
|
nav_action
|
|
}
|
|
Route::Thread(selection) => render_thread_route(
|
|
&mut app.threads,
|
|
selection,
|
|
col,
|
|
app.note_options,
|
|
ui,
|
|
&mut note_context,
|
|
&mut app.jobs,
|
|
),
|
|
Route::Accounts(amr) => 's: {
|
|
let Some(action) = render_accounts_route(
|
|
ui,
|
|
ctx,
|
|
&mut app.jobs,
|
|
&mut app.view_state.login,
|
|
&app.onboarding,
|
|
&mut app.view_state.follow_packs,
|
|
*amr,
|
|
) else {
|
|
break 's None;
|
|
};
|
|
|
|
match action {
|
|
AccountsResponse::ViewProfile(pubkey) => {
|
|
Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey)))
|
|
}
|
|
AccountsResponse::Account(accounts_route_response) => {
|
|
let mut action = accounts_route_response.process(ctx, app, col);
|
|
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
|
|
action
|
|
.accounts_action
|
|
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
|
|
}
|
|
}
|
|
}
|
|
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
|
|
.ui(ui)
|
|
.map(RenderNavAction::RelayAction),
|
|
|
|
Route::Settings => SettingsView::new(
|
|
ctx.settings.get_settings_mut(),
|
|
&mut note_context,
|
|
&mut app.note_options,
|
|
&mut app.jobs,
|
|
)
|
|
.ui(ui)
|
|
.map(RenderNavAction::SettingsAction),
|
|
|
|
Route::Reply(id) => {
|
|
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
|
txn
|
|
} else {
|
|
ui.label(tr!(
|
|
note_context.i18n,
|
|
"Reply to unknown note",
|
|
"Error message when reply note cannot be found"
|
|
));
|
|
return None;
|
|
};
|
|
|
|
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
|
|
note
|
|
} else {
|
|
ui.label(tr!(
|
|
note_context.i18n,
|
|
"Reply to unknown note",
|
|
"Error message when reply note cannot be found"
|
|
));
|
|
return None;
|
|
};
|
|
|
|
let poster = ctx.accounts.selected_filled()?;
|
|
|
|
let action = {
|
|
let draft = app.drafts.reply_mut(note.id());
|
|
|
|
let mut options = app.note_options;
|
|
options.set(NoteOptions::Wide, false);
|
|
|
|
let response = ui::PostReplyView::new(
|
|
&mut note_context,
|
|
poster,
|
|
draft,
|
|
¬e,
|
|
inner_rect,
|
|
options,
|
|
&mut app.jobs,
|
|
col,
|
|
)
|
|
.show(ui);
|
|
|
|
response.action
|
|
};
|
|
|
|
action.map(Into::into)
|
|
}
|
|
Route::Quote(id) => {
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
|
|
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
|
|
note
|
|
} else {
|
|
ui.label(tr!(
|
|
note_context.i18n,
|
|
"Quote of unknown note",
|
|
"Error message when quote note cannot be found"
|
|
));
|
|
return None;
|
|
};
|
|
|
|
let poster = ctx.accounts.selected_filled()?;
|
|
let draft = app.drafts.quote_mut(note.id());
|
|
|
|
let response = crate::ui::note::QuoteRepostView::new(
|
|
&mut note_context,
|
|
poster,
|
|
draft,
|
|
¬e,
|
|
inner_rect,
|
|
app.note_options,
|
|
&mut app.jobs,
|
|
col,
|
|
)
|
|
.show(ui);
|
|
|
|
response.action.map(Into::into)
|
|
}
|
|
Route::ComposeNote => {
|
|
let kp = ctx.accounts.get_selected_account().key.to_full()?;
|
|
let draft = app.drafts.compose_mut();
|
|
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
let post_response = ui::PostView::new(
|
|
&mut note_context,
|
|
draft,
|
|
PostType::New,
|
|
kp,
|
|
inner_rect,
|
|
app.note_options,
|
|
&mut app.jobs,
|
|
)
|
|
.ui(&txn, ui);
|
|
|
|
post_response.action.map(Into::into)
|
|
}
|
|
Route::AddColumn(route) => {
|
|
render_add_column_routes(ui, app, ctx, col, route);
|
|
|
|
None
|
|
}
|
|
Route::Support => {
|
|
SupportView::new(&mut app.support, ctx.i18n).show(ui);
|
|
None
|
|
}
|
|
Route::Search => {
|
|
let id = ui.id().with(("search", depth, col));
|
|
let navigating =
|
|
get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.column(col)
|
|
.router()
|
|
.navigating;
|
|
let search_buffer = app.view_state.searches.entry(id).or_default();
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
|
|
if navigating {
|
|
search_buffer.focus_state = FocusState::Navigating
|
|
} else if search_buffer.focus_state == FocusState::Navigating {
|
|
// we're not navigating but our last search buffer state
|
|
// says we were navigating. This means that navigating has
|
|
// stopped. Let's make sure to focus the input field
|
|
search_buffer.focus_state = FocusState::ShouldRequestFocus;
|
|
}
|
|
|
|
SearchView::new(
|
|
&txn,
|
|
app.note_options,
|
|
search_buffer,
|
|
&mut note_context,
|
|
&mut app.jobs,
|
|
)
|
|
.show(ui)
|
|
.map(RenderNavAction::NoteAction)
|
|
}
|
|
Route::NewDeck => {
|
|
let id = ui.id().with("new-deck");
|
|
let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
|
|
let mut resp = None;
|
|
if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) {
|
|
let cur_acc = ctx.accounts.selected_account_pubkey();
|
|
app.decks_cache
|
|
.add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name));
|
|
|
|
// set new deck as active
|
|
let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.decks()
|
|
.len()
|
|
- 1;
|
|
resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
|
|
DecksAction::Switch(cur_index),
|
|
)));
|
|
|
|
new_deck_state.clear();
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.get_selected_router()
|
|
.go_back();
|
|
}
|
|
resp
|
|
}
|
|
Route::EditDeck(index) => {
|
|
let mut action = None;
|
|
let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.decks_mut()
|
|
.get_mut(*index)
|
|
.expect("index wasn't valid");
|
|
let id = ui
|
|
.id()
|
|
.with(("edit-deck", ctx.accounts.selected_account_pubkey(), index));
|
|
let deck_state = app
|
|
.view_state
|
|
.id_to_deck_state
|
|
.entry(id)
|
|
.or_insert_with(|| DeckState::from_deck(cur_deck));
|
|
if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) {
|
|
match resp {
|
|
EditDeckResponse::Edit(configure_deck_response) => {
|
|
cur_deck.edit(configure_deck_response);
|
|
}
|
|
EditDeckResponse::Delete => {
|
|
action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
|
|
DecksAction::Removing(*index),
|
|
)));
|
|
}
|
|
}
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.get_selected_router()
|
|
.go_back();
|
|
}
|
|
|
|
action
|
|
}
|
|
Route::EditProfile(pubkey) => {
|
|
let mut action = None;
|
|
let Some(kp) = ctx.accounts.get_full(pubkey) else {
|
|
error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
|
|
return None;
|
|
};
|
|
|
|
let Some(state) = app.view_state.pubkey_to_profile_state.get_mut(kp.pubkey) else {
|
|
tracing::error!(
|
|
"No profile state when navigating to EditProfile... was handle_navigating_edit_profile not called?"
|
|
);
|
|
return action;
|
|
};
|
|
|
|
if EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard).ui(ui) {
|
|
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
|
|
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
|
|
SaveProfileChanges::new(kp.to_full(), state.clone()),
|
|
)))
|
|
}
|
|
}
|
|
|
|
action
|
|
}
|
|
Route::Wallet(wallet_type) => {
|
|
let state = match wallet_type {
|
|
notedeck::WalletType::Auto => 's: {
|
|
if let Some(cur_acc_wallet) = ctx.accounts.get_selected_wallet_mut() {
|
|
let default_zap_state =
|
|
get_default_zap_state(&mut cur_acc_wallet.default_zap);
|
|
break 's WalletState::Wallet {
|
|
wallet: &mut cur_acc_wallet.wallet,
|
|
default_zap_state,
|
|
can_create_local_wallet: false,
|
|
};
|
|
}
|
|
|
|
let Some(wallet) = &mut ctx.global_wallet.wallet else {
|
|
break 's WalletState::NoWallet {
|
|
state: &mut ctx.global_wallet.ui_state,
|
|
show_local_only: true,
|
|
};
|
|
};
|
|
|
|
let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
|
|
WalletState::Wallet {
|
|
wallet: &mut wallet.wallet,
|
|
default_zap_state,
|
|
can_create_local_wallet: true,
|
|
}
|
|
}
|
|
notedeck::WalletType::Local => 's: {
|
|
let cur_acc = ctx.accounts.get_selected_wallet_mut();
|
|
let Some(wallet) = cur_acc else {
|
|
break 's WalletState::NoWallet {
|
|
state: &mut ctx.global_wallet.ui_state,
|
|
show_local_only: false,
|
|
};
|
|
};
|
|
|
|
let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
|
|
WalletState::Wallet {
|
|
wallet: &mut wallet.wallet,
|
|
default_zap_state,
|
|
can_create_local_wallet: false,
|
|
}
|
|
}
|
|
};
|
|
|
|
WalletView::new(state, ctx.i18n, ctx.clipboard)
|
|
.ui(ui)
|
|
.map(RenderNavAction::WalletAction)
|
|
}
|
|
Route::CustomizeZapAmount(target) => {
|
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
|
|
CustomZapView::new(
|
|
ctx.i18n,
|
|
ctx.img_cache,
|
|
ctx.ndb,
|
|
&txn,
|
|
&target.zap_recipient,
|
|
default_msats,
|
|
)
|
|
.ui(ui)
|
|
.map(|msats| {
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.column_mut(col)
|
|
.router_mut()
|
|
.go_back();
|
|
RenderNavAction::NoteAction(NoteAction::Zap(notedeck::ZapAction::Send(
|
|
notedeck::note::ZapTargetAmount {
|
|
target: target.clone(),
|
|
specified_msats: Some(msats),
|
|
},
|
|
)))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
|
|
pub fn render_nav(
|
|
col: usize,
|
|
inner_rect: egui::Rect,
|
|
app: &mut Damus,
|
|
ctx: &mut AppContext<'_>,
|
|
ui: &mut egui::Ui,
|
|
) -> RenderNavResponse {
|
|
let narrow = is_narrow(ui.ctx());
|
|
|
|
if let Some(sheet_route) = app
|
|
.columns(ctx.accounts)
|
|
.column(col)
|
|
.sheet_router
|
|
.route()
|
|
.clone()
|
|
{
|
|
let navigating = app
|
|
.columns(ctx.accounts)
|
|
.column(col)
|
|
.sheet_router
|
|
.navigating;
|
|
let returning = app.columns(ctx.accounts).column(col).sheet_router.returning;
|
|
let bg_route = app
|
|
.columns(ctx.accounts)
|
|
.column(col)
|
|
.router()
|
|
.routes()
|
|
.last()
|
|
.cloned();
|
|
if let Some(bg_route) = bg_route {
|
|
let resp = PopupSheet::new(&bg_route, &sheet_route)
|
|
.id_source(egui::Id::new(("nav", col)))
|
|
.navigating(navigating)
|
|
.returning(returning)
|
|
.with_split_percent_from_top(Percent::new(35).expect("35 <= 100"))
|
|
.show_mut(ui, |ui, typ, route| match typ {
|
|
NavUiType::Title => NavTitle::new(
|
|
ctx.ndb,
|
|
ctx.img_cache,
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
|
|
std::slice::from_ref(route),
|
|
col,
|
|
ctx.i18n,
|
|
)
|
|
.show_move_button(!narrow)
|
|
.show_delete_button(!narrow)
|
|
.show(ui),
|
|
NavUiType::Body => render_nav_body(ui, app, ctx, route, 1, col, inner_rect),
|
|
});
|
|
|
|
return RenderNavResponse::new(col, NotedeckNavResponse::Popup(Box::new(resp)));
|
|
}
|
|
};
|
|
|
|
let routes = app
|
|
.columns(ctx.accounts)
|
|
.column(col)
|
|
.router()
|
|
.routes()
|
|
.clone();
|
|
let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col)));
|
|
|
|
let drag_ids = 's: {
|
|
let Some(top_route) = &routes.last().cloned() else {
|
|
break 's None;
|
|
};
|
|
|
|
let Some(scroll_id) = get_scroll_id(
|
|
top_route,
|
|
app.columns(ctx.accounts)
|
|
.column(col)
|
|
.router()
|
|
.routes()
|
|
.len(),
|
|
&app.timeline_cache,
|
|
col,
|
|
) else {
|
|
break 's None;
|
|
};
|
|
|
|
let vertical_drag_id = if route_uses_frame(top_route) {
|
|
get_drag_id_through_frame(ui, scroll_id)
|
|
} else {
|
|
get_drag_id(ui, scroll_id)
|
|
};
|
|
|
|
let horizontal_drag_id = nav.drag_id(ui);
|
|
|
|
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.column_mut(col)
|
|
.drag;
|
|
|
|
drag.update(horizontal_drag_id, vertical_drag_id, ui.ctx());
|
|
|
|
Some((horizontal_drag_id, vertical_drag_id))
|
|
};
|
|
|
|
let nav_response = nav
|
|
.navigating(
|
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
|
.column_mut(col)
|
|
.router_mut()
|
|
.navigating,
|
|
)
|
|
.returning(
|
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
|
.column_mut(col)
|
|
.router_mut()
|
|
.returning,
|
|
)
|
|
.show_mut(ui, |ui, render_type, nav| match render_type {
|
|
NavUiType::Title => NavTitle::new(
|
|
ctx.ndb,
|
|
ctx.img_cache,
|
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
|
|
nav.routes(),
|
|
col,
|
|
ctx.i18n,
|
|
)
|
|
.show_move_button(!narrow)
|
|
.show_delete_button(!narrow)
|
|
.show(ui),
|
|
|
|
NavUiType::Body => {
|
|
if let Some(top) = nav.routes().last() {
|
|
render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
});
|
|
|
|
if let Some((horizontal_drag_id, vertical_drag_id)) = drag_ids {
|
|
let drag = &mut get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
|
|
.column_mut(col)
|
|
.drag;
|
|
drag.check_for_drag_start(ui.ctx(), horizontal_drag_id, vertical_drag_id);
|
|
}
|
|
|
|
RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response)))
|
|
}
|
|
|
|
fn get_scroll_id(
|
|
top: &Route,
|
|
depth: usize,
|
|
timeline_cache: &TimelineCache,
|
|
col: usize,
|
|
) -> Option<egui::Id> {
|
|
match top {
|
|
Route::Timeline(timeline_kind) => match timeline_kind {
|
|
TimelineKind::List(_)
|
|
| TimelineKind::Search(_)
|
|
| TimelineKind::Algo(_)
|
|
| TimelineKind::Notifications(_)
|
|
| TimelineKind::Universe
|
|
| TimelineKind::Hashtag(_)
|
|
| TimelineKind::Generic(_) => {
|
|
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
|
|
}
|
|
TimelineKind::Profile(pubkey) => {
|
|
if depth > 1 {
|
|
Some(ProfileView::scroll_id(col, pubkey))
|
|
} else {
|
|
TimelineView::scroll_id(timeline_cache, timeline_kind, col)
|
|
}
|
|
}
|
|
},
|
|
Route::Thread(thread_selection) => Some(ThreadView::scroll_id(
|
|
thread_selection.selected_or_root(),
|
|
col,
|
|
)),
|
|
Route::Accounts(accounts_route) => match accounts_route {
|
|
crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
|
|
crate::accounts::AccountsRoute::AddAccount => None,
|
|
crate::accounts::AccountsRoute::Onboarding => {
|
|
Some(FollowPackOnboardingView::scroll_id())
|
|
}
|
|
},
|
|
Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
|
|
Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
|
|
Route::Relays => Some(RelayView::scroll_id()),
|
|
Route::ComposeNote => Some(PostView::scroll_id()),
|
|
Route::AddColumn(add_column_route) => Some(AddColumnView::scroll_id(add_column_route)),
|
|
Route::EditProfile(_) => Some(EditProfileView::scroll_id()),
|
|
Route::Support => None,
|
|
Route::NewDeck => Some(ConfigureDeckView::scroll_id()),
|
|
Route::Search => Some(SearchView::scroll_id()),
|
|
Route::EditDeck(_) => None,
|
|
Route::Wallet(_) => None,
|
|
Route::CustomizeZapAmount(_) => None,
|
|
Route::Settings => None,
|
|
}
|
|
}
|
|
|
|
/// Does the corresponding View for the route use a egui::Frame to wrap the ScrollArea?
|
|
/// TODO(kernelkind): this is quite hacky...
|
|
fn route_uses_frame(route: &Route) -> bool {
|
|
match route {
|
|
Route::Accounts(accounts_route) => match accounts_route {
|
|
crate::accounts::AccountsRoute::Accounts => true,
|
|
crate::accounts::AccountsRoute::AddAccount => false,
|
|
crate::accounts::AccountsRoute::Onboarding => false,
|
|
},
|
|
Route::Relays => true,
|
|
Route::Timeline(_) => false,
|
|
Route::Thread(_) => false,
|
|
Route::Reply(_) => false,
|
|
Route::Quote(_) => false,
|
|
Route::Settings => false,
|
|
Route::ComposeNote => false,
|
|
Route::AddColumn(_) => false,
|
|
Route::EditProfile(_) => false,
|
|
Route::Support => false,
|
|
Route::NewDeck => false,
|
|
Route::Search => false,
|
|
Route::EditDeck(_) => false,
|
|
Route::Wallet(_) => false,
|
|
Route::CustomizeZapAmount(_) => false,
|
|
}
|
|
}
|