There was a bunch of redundant responses. Let's unify them under the RenderNavAction enum. We unify all action processing under this type. This also centralizes all of our side effects into a single function instead of scattering them everywhere
419 lines
14 KiB
Rust
419 lines
14 KiB
Rust
use crate::{
|
|
accounts::render_accounts_route,
|
|
actionbar::NoteAction,
|
|
app_style::{get_font_size, NotedeckTextStyle},
|
|
fonts::NamedFontFamily,
|
|
notes_holder::NotesHolder,
|
|
profile::Profile,
|
|
relay_pool_manager::RelayPoolManager,
|
|
route::Route,
|
|
storage::{self},
|
|
thread::Thread,
|
|
timeline::{
|
|
route::{render_timeline_route, TimelineRoute},
|
|
Timeline,
|
|
},
|
|
ui::{
|
|
self,
|
|
add_column::render_add_column_routes,
|
|
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
|
note::{PostAction, PostType},
|
|
support::SupportView,
|
|
RelayView, View,
|
|
},
|
|
Damus,
|
|
};
|
|
|
|
use egui::{pos2, Color32, InnerResponse, Stroke};
|
|
use egui_nav::{Nav, NavAction, NavResponse, TitleBarResponse};
|
|
use nostrdb::{Ndb, Transaction};
|
|
use tracing::{error, info};
|
|
|
|
pub enum RenderNavAction {
|
|
PostAction(PostAction),
|
|
NoteAction(NoteAction),
|
|
}
|
|
|
|
impl From<PostAction> for RenderNavAction {
|
|
fn from(post_action: PostAction) -> Self {
|
|
Self::PostAction(post_action)
|
|
}
|
|
}
|
|
|
|
impl From<NoteAction> for RenderNavAction {
|
|
fn from(note_action: NoteAction) -> RenderNavAction {
|
|
Self::NoteAction(note_action)
|
|
}
|
|
}
|
|
|
|
pub struct RenderNavResponse {
|
|
column: usize,
|
|
response: NavResponse<Option<RenderNavAction>, TitleResponse>,
|
|
}
|
|
|
|
impl RenderNavResponse {
|
|
#[allow(private_interfaces)]
|
|
pub fn new(
|
|
column: usize,
|
|
response: NavResponse<Option<RenderNavAction>, TitleResponse>,
|
|
) -> Self {
|
|
RenderNavResponse { column, response }
|
|
}
|
|
|
|
pub fn process_render_nav_response(&self, app: &mut Damus) {
|
|
let mut col_changed: bool = false;
|
|
let col = self.column;
|
|
|
|
if let Some(action) = &self.response.inner {
|
|
// start returning when we're finished posting
|
|
match action {
|
|
RenderNavAction::PostAction(post_action) => {
|
|
let txn = Transaction::new(&app.ndb).expect("txn");
|
|
let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts);
|
|
app.columns_mut().column_mut(col).router_mut().go_back();
|
|
}
|
|
|
|
RenderNavAction::NoteAction(note_action) => {
|
|
let txn = Transaction::new(&app.ndb).expect("txn");
|
|
|
|
note_action.execute_and_process_result(
|
|
&app.ndb,
|
|
&mut app.columns,
|
|
col,
|
|
&mut app.threads,
|
|
&mut app.profiles,
|
|
&mut app.note_cache,
|
|
&mut app.pool,
|
|
&txn,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(NavAction::Returned) = self.response.action {
|
|
let r = app.columns_mut().column_mut(col).router_mut().pop();
|
|
let txn = Transaction::new(&app.ndb).expect("txn");
|
|
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
|
|
let root_id = {
|
|
crate::note::root_note_id_from_selected_id(
|
|
&app.ndb,
|
|
&mut app.note_cache,
|
|
&txn,
|
|
id.bytes(),
|
|
)
|
|
};
|
|
Thread::unsubscribe_locally(
|
|
&txn,
|
|
&app.ndb,
|
|
&mut app.note_cache,
|
|
&mut app.threads,
|
|
&mut app.pool,
|
|
root_id,
|
|
);
|
|
}
|
|
|
|
if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
|
|
Profile::unsubscribe_locally(
|
|
&txn,
|
|
&app.ndb,
|
|
&mut app.note_cache,
|
|
&mut app.profiles,
|
|
&mut app.pool,
|
|
pubkey.bytes(),
|
|
);
|
|
}
|
|
col_changed = true;
|
|
} else if let Some(NavAction::Navigated) = self.response.action {
|
|
let cur_router = app.columns_mut().column_mut(col).router_mut();
|
|
cur_router.navigating = false;
|
|
if cur_router.is_replacing() {
|
|
cur_router.remove_previous_routes();
|
|
}
|
|
col_changed = true;
|
|
}
|
|
|
|
if let Some(title_response) = &self.response.title_response {
|
|
match title_response {
|
|
TitleResponse::RemoveColumn => {
|
|
let tl = app.columns().find_timeline_for_column_index(col);
|
|
if let Some(timeline) = tl {
|
|
unsubscribe_timeline(app.ndb(), timeline);
|
|
}
|
|
|
|
app.columns_mut().delete_column(col);
|
|
col_changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if col_changed {
|
|
storage::save_columns(&app.path, app.columns().as_serializable_columns());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
|
|
pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse {
|
|
let col_id = app.columns.get_column_id_at_index(col);
|
|
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
|
|
let routes = app
|
|
.columns()
|
|
.column(col)
|
|
.router()
|
|
.routes()
|
|
.iter()
|
|
.map(|r| r.get_titled_route(&app.columns, &app.ndb))
|
|
.collect();
|
|
|
|
let nav_response = Nav::new(routes)
|
|
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
|
|
.returning(app.columns_mut().column_mut(col).router_mut().returning)
|
|
.id_source(egui::Id::new(col_id))
|
|
.title(48.0, title_bar)
|
|
.show_mut(ui, |ui, nav| match &nav.top().route {
|
|
Route::Timeline(tlr) => render_timeline_route(
|
|
&app.ndb,
|
|
&mut app.columns,
|
|
&mut app.drafts,
|
|
&mut app.img_cache,
|
|
&mut app.unknown_ids,
|
|
&mut app.note_cache,
|
|
&mut app.threads,
|
|
&mut app.profiles,
|
|
&mut app.accounts,
|
|
*tlr,
|
|
col,
|
|
app.textmode,
|
|
ui,
|
|
),
|
|
Route::Accounts(amr) => {
|
|
let action = render_accounts_route(
|
|
ui,
|
|
&app.ndb,
|
|
col,
|
|
&mut app.columns,
|
|
&mut app.img_cache,
|
|
&mut app.accounts,
|
|
&mut app.view_state.login,
|
|
*amr,
|
|
);
|
|
let txn = Transaction::new(&app.ndb).expect("txn");
|
|
action.process_action(&mut app.unknown_ids, &app.ndb, &txn);
|
|
None
|
|
}
|
|
Route::Relays => {
|
|
let manager = RelayPoolManager::new(app.pool_mut());
|
|
RelayView::new(manager).ui(ui);
|
|
None
|
|
}
|
|
Route::ComposeNote => {
|
|
let kp = app.accounts.selected_or_first_nsec()?;
|
|
let draft = app.drafts.compose_mut();
|
|
|
|
let txn = nostrdb::Transaction::new(&app.ndb).expect("txn");
|
|
let post_response = ui::PostView::new(
|
|
&app.ndb,
|
|
draft,
|
|
PostType::New,
|
|
&mut app.img_cache,
|
|
&mut app.note_cache,
|
|
kp,
|
|
)
|
|
.ui(&txn, ui);
|
|
|
|
post_response.action.map(Into::into)
|
|
}
|
|
Route::AddColumn(route) => {
|
|
render_add_column_routes(ui, app, col, route);
|
|
|
|
None
|
|
}
|
|
|
|
Route::Support => {
|
|
SupportView::new(&mut app.support).show(ui);
|
|
None
|
|
}
|
|
});
|
|
|
|
RenderNavResponse::new(col, nav_response)
|
|
}
|
|
|
|
fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) {
|
|
if let Some(sub_id) = timeline.subscription {
|
|
if let Err(e) = ndb.unsubscribe(sub_id) {
|
|
error!("unsubscribe error: {}", e);
|
|
} else {
|
|
info!(
|
|
"successfully unsubscribed from timeline {} with sub id {}",
|
|
timeline.id,
|
|
sub_id.id()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn title_bar(
|
|
ui: &mut egui::Ui,
|
|
allocated_response: egui::Response,
|
|
title_name: String,
|
|
back_name: Option<String>,
|
|
) -> egui::InnerResponse<TitleBarResponse<TitleResponse>> {
|
|
let icon_width = 32.0;
|
|
let padding_external = 16.0;
|
|
let padding_internal = 8.0;
|
|
let has_back = back_name.is_some();
|
|
|
|
let (spacing_rect, titlebar_rect) = allocated_response
|
|
.rect
|
|
.split_left_right_at_x(allocated_response.rect.left() + padding_external);
|
|
ui.advance_cursor_after_rect(spacing_rect);
|
|
|
|
let (titlebar_resp, maybe_button_resp) = if has_back {
|
|
let (button_rect, titlebar_rect) = titlebar_rect
|
|
.split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external);
|
|
(
|
|
allocated_response.with_new_rect(titlebar_rect),
|
|
Some(back_button(ui, button_rect)),
|
|
)
|
|
} else {
|
|
(allocated_response, None)
|
|
};
|
|
|
|
title(
|
|
ui,
|
|
title_name,
|
|
titlebar_resp.rect,
|
|
icon_width,
|
|
if has_back {
|
|
padding_internal
|
|
} else {
|
|
padding_external
|
|
},
|
|
);
|
|
|
|
let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external);
|
|
let title_response = if delete_button_resp.clicked() {
|
|
Some(TitleResponse::RemoveColumn)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let titlebar_resp = TitleBarResponse {
|
|
title_response,
|
|
go_back: maybe_button_resp.map_or(false, |r| r.clicked()),
|
|
};
|
|
|
|
InnerResponse::new(titlebar_resp, delete_button_resp)
|
|
}
|
|
|
|
fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response {
|
|
let horizontal_length = 10.0;
|
|
let arrow_length = 5.0;
|
|
|
|
let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect);
|
|
let painter = ui.painter_at(helper.get_animation_rect());
|
|
let stroke = Stroke::new(1.5, ui.visuals().text_color());
|
|
|
|
// Horizontal segment
|
|
let left_horizontal_point = pos2(-horizontal_length / 2., 0.);
|
|
let right_horizontal_point = pos2(horizontal_length / 2., 0.);
|
|
let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point);
|
|
let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point);
|
|
|
|
painter.line_segment(
|
|
[scaled_left_horizontal_point, scaled_right_horizontal_point],
|
|
stroke,
|
|
);
|
|
|
|
// Top Arrow
|
|
let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.;
|
|
let right_top_arrow_point = helper.scale_pos_from_center(pos2(
|
|
left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
|
|
right_horizontal_point.y + sqrt_2_over_2 * arrow_length,
|
|
));
|
|
|
|
let scaled_left_arrow_point = scaled_left_horizontal_point;
|
|
painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke);
|
|
|
|
let right_bottom_arrow_point = helper.scale_pos_from_center(pos2(
|
|
left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
|
|
right_horizontal_point.y - sqrt_2_over_2 * arrow_length,
|
|
));
|
|
|
|
painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke);
|
|
|
|
helper.take_animation_response()
|
|
}
|
|
|
|
fn delete_column_button(
|
|
ui: &mut egui::Ui,
|
|
allocation_response: egui::Response,
|
|
icon_width: f32,
|
|
padding: f32,
|
|
) -> egui::Response {
|
|
let img_size = 16.0;
|
|
let max_size = icon_width * ICON_EXPANSION_MULTIPLE;
|
|
|
|
let img_data = egui::include_image!("../assets/icons/column_delete_icon_4x.png");
|
|
let img = egui::Image::new(img_data).max_width(img_size);
|
|
|
|
let button_rect = {
|
|
let titlebar_rect = allocation_response.rect;
|
|
let titlebar_width = titlebar_rect.width();
|
|
let titlebar_center = titlebar_rect.center();
|
|
let button_center_y = titlebar_center.y;
|
|
let button_center_x =
|
|
titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding;
|
|
egui::Rect::from_center_size(
|
|
pos2(button_center_x, button_center_y),
|
|
egui::vec2(max_size, max_size),
|
|
)
|
|
};
|
|
|
|
let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect);
|
|
|
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
|
|
|
let animation_rect = helper.get_animation_rect();
|
|
let animation_resp = helper.take_animation_response();
|
|
if allocation_response.union(animation_resp.clone()).hovered() {
|
|
img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0));
|
|
}
|
|
|
|
animation_resp
|
|
}
|
|
|
|
fn title(
|
|
ui: &mut egui::Ui,
|
|
title_name: String,
|
|
titlebar_rect: egui::Rect,
|
|
icon_width: f32,
|
|
padding: f32,
|
|
) {
|
|
let painter = ui.painter_at(titlebar_rect);
|
|
|
|
let font = egui::FontId::new(
|
|
get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
|
);
|
|
|
|
let max_title_width = titlebar_rect.width() - icon_width - padding * 2.;
|
|
let title_galley =
|
|
ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width));
|
|
|
|
let pos = {
|
|
let titlebar_center = titlebar_rect.center();
|
|
let text_height = title_galley.rect.height();
|
|
|
|
let galley_pos_x = titlebar_rect.left() + padding;
|
|
let galley_pos_y = titlebar_center.y - (text_height / 2.);
|
|
pos2(galley_pos_x, galley_pos_y)
|
|
};
|
|
|
|
painter.galley(pos, title_galley, Color32::WHITE);
|
|
}
|
|
|
|
enum TitleResponse {
|
|
RemoveColumn,
|
|
}
|