app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 64c17a7134590e43b8c2a4ddfc403403b5eb8c81
parent 3d0f5de0c04375b238b76fd30cba35a43696186e
Author: triesap <tyson@radroots.org>
Date:   Sat,  6 Jun 2026 22:41:50 -0700

app: add focused commerce views

Diffstat:
Mcrates/desktop/src/source_guards.rs | 22+++-------------------
Mcrates/desktop/src/window.rs | 1187+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/i18n/src/keys.rs | 3+++
Mcrates/i18n/src/lib.rs | 17++++++++++-------
Mcrates/ui/src/lib.rs | 12++++++------
Mcrates/ui/src/primitives.rs | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/ui/src/theme.rs | 12++++++++++++
Mi18n/locales/en/messages.json | 37++++++++++++++++++++-----------------
8 files changed, 688 insertions(+), 658 deletions(-)

diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -53,6 +53,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "buyer-order-accept-change", "buyer-order-confirm-replace", "buyer-order-cancel", + "buyer-order-detail-back", "buyer-order-keep-current", "buyer-order-keep-order", "buyer-order-close-issue", @@ -182,6 +183,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "finder unavailable", "orders", "orders-reminders", + "orders-detail-back", "pack-day-export", "pack-day-open-customer-labels", "pack-day-open-pack-sheet", @@ -250,6 +252,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "products-sort-stock", "products-sort-updated", "products-add-product", + "products-editor-availability", "products-editor-close", "products-editor-save", "products-editor-status-archived", @@ -285,7 +288,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "sign_event:kind:1", "shell", "shell-account-entry", - "shell-account-label", "shell-mode-farm", "shell-mode-marketplace", "shell.switch_farm_failed", @@ -293,14 +295,11 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "settings", "settings-add-blackout-period", "settings-add-fulfillment-window", - "settings-allow-relay-connections", "settings-farm-add-pickup", "settings-farm-default-pickup", "settings-farm-remove-pickup", "settings-farm-save", "settings-fulfillment-window-pickup-location", - "settings-launch-at-login", - "settings-manage-media-servers", "settings-nav-about", "settings-nav-accounts", "settings-nav-farm", @@ -311,8 +310,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "settings-about-refresh-sync-disabled", "settings-remove-blackout-period", "settings-remove-fulfillment-window", - "settings-use-media-servers", - "settings-use-nip05", "settings.farm.load_failed", "settings.farm.save_failed", "settings.about.sync_refresh_failed", @@ -330,7 +327,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "{} {} {}.", "{quantity} {unit_label}", "{} {}", - "{}...", ]; const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ @@ -413,7 +409,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::PersonalOrdersEmptyBody", "AppTextKey::PersonalOrdersListTitle", "AppTextKey::PersonalOrdersDetailTitle", - "AppTextKey::PersonalOrdersDetailEmptyBody", "AppTextKey::PersonalOrdersDetailFarmLabel", "AppTextKey::PersonalOrdersDetailFulfillmentLabel", "AppTextKey::PersonalOrdersDetailTotalLabel", @@ -491,18 +486,12 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::OrdersEmptyNeedsActionTitle", "AppTextKey::OrdersEmptyNeedsActionBody", "AppTextKey::OrdersDetailTitle", - "AppTextKey::OrdersDetailEmptyBody", "AppTextKey::OrdersDetailItemsTitle", "AppTextKey::OrdersDetailCustomerLabel", "AppTextKey::OrdersDetailWindowLabel", "AppTextKey::OrdersDetailPickupLabel", "AppTextKey::OrdersDetailTotalLabel", "AppTextKey::TradeValidationReceiptSectionLabel", - "AppTextKey::TradeValidationReceiptEventLabel", - "AppTextKey::TradeValidationReceiptTargetLabel", - "AppTextKey::TradeValidationReceiptEventSetRootLabel", - "AppTextKey::TradeValidationReceiptReducerOutputRootLabel", - "AppTextKey::TradeValidationReceiptPublicValuesHashLabel", "AppTextKey::TradeValidationReceiptRecordedAtLabel", "AppTextKey::TradeValidationReceiptResultValid", "AppTextKey::TradeValidationReceiptResultNeedsReview", @@ -510,11 +499,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::TradeValidationReceiptTypeTradeTransition", "AppTextKey::TradeValidationReceiptTypeInventoryState", "AppTextKey::TradeValidationReceiptTypeStateCheckpoint", - "AppTextKey::TradeValidationReceiptProofNone", - "AppTextKey::TradeValidationReceiptProofSp1Core", - "AppTextKey::TradeValidationReceiptProofSp1Compressed", - "AppTextKey::TradeValidationReceiptProofSp1Groth16", - "AppTextKey::TradeValidationReceiptProofSp1Plonk", "AppTextKey::TradeWorkflowAxisAgreement", "AppTextKey::TradeWorkflowAxisRevision", "AppTextKey::TradeWorkflowAxisFulfillment", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -27,23 +27,23 @@ use radroots_app_sync::{ SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, }; use radroots_app_ui::{ - APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, + APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, AppSegmentButtonIconSpec as IconSegmentButtonSpec, LabelValueRow, app_button_card, app_button_choice as choice_button, app_button_compact as action_button_compact, - app_button_icon as action_icon_button, app_button_list_row as list_row_button, - app_button_primary as action_button_primary, + app_button_list_row as list_row_button, app_button_primary as action_button_primary, app_button_primary_disabled as action_button_primary_disabled, app_button_secondary as action_button, app_button_secondary_disabled as action_button_disabled, app_button_text as text_button, app_checkbox_field, app_cluster, app_detail_row, - app_divider as section_divider, app_form_field, app_form_input_text, app_form_section, - app_heading_section, app_heading_view, app_input_text as app_text_input, app_scroll_panel, + app_divider as section_divider, app_focused_detail_view, app_focused_task_view, app_form_field, + app_form_input_text, app_form_section, app_heading_section, app_heading_view, + app_input_text as app_text_input, app_scroll_panel, app_segment_button_icon as icon_segment_button, app_shared_label_text, app_shared_text, app_split_shell, app_stack_h, app_stack_v, app_status_indicator as status_indicator, app_surface_card, app_surface_card_section as home_card, app_surface_panel, app_surface_sidebar, app_surface_window as app_window_shell, app_text_badge as settings_badge_text, app_text_body_subtle as home_body_text, app_text_label, app_text_label as home_farm_setup_field_label, app_text_value, label_value_list, - runtime_metadata_rows, utility_title_row, + runtime_metadata_rows, settings_preferences_general_rows, utility_title_row, }; pub use radroots_app_view::SettingsSection as SettingsPanelViewKey; use radroots_app_view::{ @@ -67,9 +67,8 @@ use radroots_app_view::{ RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTaskKind, TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, TradeInventoryStatus, TradePaymentDisplayStatus, TradeReceiptProjection, TradeRevisionStatus, - TradeValidationReceiptProjection, TradeValidationReceiptProofSystem, - TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, - TradeWorkflowSource, + TradeValidationReceiptProjection, TradeValidationReceiptResult, TradeValidationReceiptType, + TradeWorkflowProjection, TradeWorkflowSource, }; use radroots_nostr::prelude::RadrootsNostrClient; use std::{ @@ -145,6 +144,17 @@ pub fn home_stage(summary: &DesktopAppRuntimeSummary) -> HomeStage { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HomeFocusedView { + FarmSetup, + ProductEditor, + FarmerOrderDetail(OrderId), + BuyerProductDetail(PersonalSection), + BuyerOrderReview, + BuyerOrderDetail(OrderId), + BuyerReceiptIssue(OrderId), +} + pub fn home_window_options(cx: &mut App) -> WindowOptions { let (launch_width_px, launch_height_px) = home_window_launch_size_px(); let (minimum_width_px, minimum_height_px) = home_window_minimum_size_px(); @@ -227,6 +237,7 @@ pub struct HomeView { products_search: Option<ProductsSearchState>, products_stock_editor: Option<ProductsStockEditorState>, product_editor_form: Option<ProductEditorFormState>, + focused_view: Option<HomeFocusedView>, relay_client: Option<RadrootsNostrClient>, buyer_workspace_notice: Option<String>, } @@ -345,6 +356,7 @@ impl HomeView { products_search: None, products_stock_editor: None, product_editor_form: None, + focused_view: None, relay_client: None, buyer_workspace_notice: None, } @@ -366,6 +378,19 @@ impl HomeView { } } + fn clear_focused_view(&mut self) -> bool { + self.focused_view.take().is_some() + } + + fn clear_focused_view_matching(&mut self, view: HomeFocusedView) -> bool { + if self.focused_view == Some(view) { + self.focused_view = None; + true + } else { + false + } + } + fn apply_auto_focus( &mut self, runtime: &DesktopAppRuntimeSummary, @@ -892,6 +917,7 @@ impl HomeView { window, cx, )); + self.focused_view = Some(HomeFocusedView::FarmSetup); cx.notify(); return; } @@ -906,6 +932,7 @@ impl HomeView { window, cx, )); + self.focused_view = Some(HomeFocusedView::FarmSetup); if stage_changed || self.farm_setup_form.is_some() { cx.notify(); } @@ -924,6 +951,7 @@ impl HomeView { .map(|account| account.account.account_id.clone()) else { self.farm_setup_form = None; + self.clear_focused_view_matching(HomeFocusedView::FarmSetup); return; }; @@ -943,6 +971,10 @@ impl HomeView { if should_reset { self.farm_setup_form = Some(FarmSetupFormState::new(account_id, draft, window, cx)); } + + if runtime_summary.home_route == HomeRoute::FarmSetupForm { + self.focused_view = Some(HomeFocusedView::FarmSetup); + } } fn sync_products_search( @@ -1144,6 +1176,7 @@ impl HomeView { .map(|account| account.account.account_id.clone()) else { self.product_editor_form = None; + self.clear_focused_view_matching(HomeFocusedView::ProductEditor); return; }; @@ -1151,6 +1184,7 @@ impl HomeView { || !runtime_summary.farm_setup_projection.has_saved_farm() { self.product_editor_form = None; + self.clear_focused_view_matching(HomeFocusedView::ProductEditor); return; } @@ -1158,10 +1192,12 @@ impl HomeView { &runtime_summary.products_projection.editor else { self.product_editor_form = None; + self.clear_focused_view_matching(HomeFocusedView::ProductEditor); return; }; let Some(product_id) = session.selected_product_id else { self.product_editor_form = None; + self.clear_focused_view_matching(HomeFocusedView::ProductEditor); return; }; let should_reset = self @@ -1184,6 +1220,7 @@ impl HomeView { fn select_farmer_section(&mut self, section: FarmerSection, cx: &mut Context<Self>) { if self.runtime.select_farmer_section(section) { self.products_stock_editor = None; + self.clear_focused_view(); if section != FarmerSection::Products { self.product_editor_form = None; } @@ -1202,6 +1239,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); self.clear_buyer_workspace_notice(); true } @@ -1227,6 +1265,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => {} @@ -1249,6 +1288,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => {} @@ -1472,6 +1512,7 @@ impl HomeView { { Ok(true) => { self.clear_buyer_workspace_notice(); + self.focused_view = Some(HomeFocusedView::BuyerProductDetail(section)); true } Ok(false) => self.clear_buyer_workspace_notice(), @@ -1499,7 +1540,10 @@ impl HomeView { } fn close_personal_product_detail(&mut self, section: PersonalSection, cx: &mut Context<Self>) { - if self.runtime.close_personal_product_detail(section) { + let runtime_changed = self.runtime.close_personal_product_detail(section); + let focus_changed = + self.clear_focused_view_matching(HomeFocusedView::BuyerProductDetail(section)); + if runtime_changed || focus_changed { cx.notify(); } } @@ -1577,11 +1621,14 @@ impl HomeView { window, cx, )); + self.focused_view = Some(HomeFocusedView::BuyerOrderReview); cx.notify(); } fn close_personal_order_review(&mut self, cx: &mut Context<Self>) { - if self.buyer_order_review_form.take().is_some() { + let cleared = self.buyer_order_review_form.take().is_some(); + let focus_changed = self.clear_focused_view_matching(HomeFocusedView::BuyerOrderReview); + if cleared || focus_changed { cx.notify(); } } @@ -1670,6 +1717,7 @@ impl HomeView { { self.buyer_receipt_issue_form = None; } + self.focused_view = Some(HomeFocusedView::BuyerOrderDetail(order_id)); cx.notify(); } Ok(false) => {} @@ -1685,6 +1733,12 @@ impl HomeView { } } + fn close_personal_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) { + if self.clear_focused_view_matching(HomeFocusedView::BuyerOrderDetail(order_id)) { + cx.notify(); + } + } + fn repeat_personal_order( &mut self, order_id: OrderId, @@ -1771,6 +1825,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => {} @@ -1797,6 +1852,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => {} @@ -1821,6 +1877,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => {} @@ -2027,6 +2084,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.clear_focused_view(); cx.notify(); } Ok(false) => self.open_orders_fulfillment_window(fulfillment_window_id, cx), @@ -2062,6 +2120,7 @@ impl HomeView { Ok(true) => { self.products_stock_editor = None; self.product_editor_form = None; + self.focused_view = Some(HomeFocusedView::FarmerOrderDetail(order_id)); cx.notify(); } Ok(false) => {} @@ -2077,6 +2136,12 @@ impl HomeView { } } + fn close_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) { + if self.clear_focused_view_matching(HomeFocusedView::FarmerOrderDetail(order_id)) { + cx.notify(); + } + } + fn dismiss_presented_reminder(&mut self, reminder_id: ReminderId, cx: &mut Context<Self>) { match self.runtime.acknowledge_reminder(reminder_id) { Ok(true) => cx.notify(), @@ -2103,6 +2168,7 @@ impl HomeView { Ok(true) | Ok(false) => { self.products_stock_editor = None; self.product_editor_form = None; + self.focused_view = Some(HomeFocusedView::FarmerOrderDetail(order_id)); self.dismiss_presented_reminder(reminder_id, cx); } Err(runtime_error) => { @@ -2238,11 +2304,27 @@ impl HomeView { cx: &mut Context<Self>, ) { self.buyer_receipt_issue_form = Some(BuyerReceiptIssueFormState::new(order_id, window, cx)); + self.focused_view = Some(HomeFocusedView::BuyerReceiptIssue(order_id)); cx.notify(); } fn close_buyer_receipt_issue_form(&mut self, cx: &mut Context<Self>) { - if self.buyer_receipt_issue_form.take().is_some() { + let order_id = self + .buyer_receipt_issue_form + .as_ref() + .map(|form| form.order_id); + let cleared = self.buyer_receipt_issue_form.take().is_some(); + let focus_changed = order_id + .map(|order_id| { + self.clear_focused_view_matching(HomeFocusedView::BuyerReceiptIssue(order_id)) + }) + .unwrap_or(false); + if focus_changed { + if let Some(order_id) = order_id { + self.focused_view = Some(HomeFocusedView::BuyerOrderDetail(order_id)); + } + } + if cleared || focus_changed { cx.notify(); } } @@ -2503,6 +2585,7 @@ impl HomeView { match self.runtime.open_new_product_editor() { Ok(true) => { self.products_stock_editor = None; + self.focused_view = Some(HomeFocusedView::ProductEditor); cx.notify(); } Ok(false) => {} @@ -2521,6 +2604,7 @@ impl HomeView { match self.runtime.open_existing_product_editor(product_id) { Ok(true) => { self.products_stock_editor = None; + self.focused_view = Some(HomeFocusedView::ProductEditor); cx.notify(); } Ok(false) => {} @@ -2539,8 +2623,9 @@ impl HomeView { fn close_product_editor(&mut self, cx: &mut Context<Self>) { let changed = self.runtime.close_product_editor(); let cleared = self.product_editor_form.take().is_some(); + let focus_changed = self.clear_focused_view_matching(HomeFocusedView::ProductEditor); - if changed || cleared { + if changed || cleared || focus_changed { cx.notify(); } } @@ -2561,6 +2646,7 @@ impl HomeView { }; let matches_input = form.title_input == *state || form.subtitle_input == *state + || form.category_input == *state || form.unit_input == *state || form.price_input == *state || form.stock_input == *state; @@ -2576,6 +2662,24 @@ impl HomeView { cx.notify(); } + fn select_product_editor_availability_window( + &mut self, + availability_window_id: FulfillmentWindowId, + cx: &mut Context<Self>, + ) { + let Some(form) = self.product_editor_form.as_mut() else { + return; + }; + + if form.selected_availability_window_id == Some(availability_window_id) { + return; + } + + form.selected_availability_window_id = Some(availability_window_id); + form.save_failed = false; + cx.notify(); + } + fn select_product_editor_status(&mut self, status: ProductStatus, cx: &mut Context<Self>) { let Some(form) = self.product_editor_form.as_mut() else { return; @@ -2735,25 +2839,7 @@ impl HomeView { ); } - if let Some(farm_setup_form) = self.farm_setup_form.as_ref() { - sections.push( - home_farm_setup_form_card( - farm_setup_form, - cx.listener(|this, checked: &bool, _, cx| { - this.toggle_farm_order_method(FarmOrderMethod::Pickup, *checked, cx) - }), - cx.listener(|this, checked: &bool, _, cx| { - this.toggle_farm_order_method(FarmOrderMethod::Delivery, *checked, cx) - }), - cx.listener(|this, checked: &bool, _, cx| { - this.toggle_farm_order_method(FarmOrderMethod::Shipping, *checked, cx) - }), - cx.listener(|this, _, _, cx| this.finish_farm_setup(cx)), - cx, - ) - .into_any_element(), - ); - } else if let Some(spec) = setup_onboarding { + if let Some(spec) = setup_onboarding { sections.push( home_farm_setup_onboarding_card( spec, @@ -2972,20 +3058,22 @@ impl HomeView { cx: &mut Context<Self>, ) -> AnyElement { let selected_personal_section = selected_personal_section(runtime); - let main_content = match selected_personal_section { - PersonalSection::Browse => self - .render_buyer_browse_content(runtime, cx) - .into_any_element(), - PersonalSection::Search => self - .render_buyer_search_content(runtime, cx) - .into_any_element(), - PersonalSection::Cart => self - .render_buyer_cart_content(runtime, cx) - .into_any_element(), - PersonalSection::Orders => self - .render_buyer_orders_content(runtime, cx) - .into_any_element(), - }; + let main_content = self + .render_buyer_focused_view(runtime, cx) + .unwrap_or_else(|| match selected_personal_section { + PersonalSection::Browse => self + .render_buyer_browse_content(runtime, cx) + .into_any_element(), + PersonalSection::Search => self + .render_buyer_search_content(runtime, cx) + .into_any_element(), + PersonalSection::Cart => self + .render_buyer_cart_content(runtime, cx) + .into_any_element(), + PersonalSection::Orders => self + .render_buyer_orders_content(runtime, cx) + .into_any_element(), + }); app_split_shell( buyer_sidebar( @@ -3031,6 +3119,107 @@ impl HomeView { .into_any_element() } + fn render_buyer_focused_view( + &mut self, + runtime: &DesktopAppRuntimeSummary, + cx: &mut Context<Self>, + ) -> Option<AnyElement> { + match self.focused_view? { + HomeFocusedView::BuyerProductDetail(section) => { + let detail = match section { + PersonalSection::Browse => runtime.personal_projection.browse.detail.as_ref(), + PersonalSection::Search => runtime.personal_projection.search.detail.as_ref(), + PersonalSection::Cart | PersonalSection::Orders => None, + }?; + Some( + buyer_product_detail_card( + detail, + runtime + .personal_projection + .cart + .cart + .replace_confirmation + .as_ref(), + cx.listener(move |this, _, _, cx| { + this.close_personal_product_detail(section, cx) + }), + cx.listener(move |this, _, _, cx| { + this.decrease_personal_product_quantity(section, cx) + }), + cx.listener(move |this, _, _, cx| { + this.increase_personal_product_quantity(section, cx) + }), + cx.listener(move |this, _, _, cx| { + this.add_personal_product_to_cart(section, false, cx) + }), + cx.listener(move |this, _, _, cx| { + this.add_personal_product_to_cart(section, true, cx) + }), + cx.listener(|this, _, _, cx| { + this.clear_personal_cart_replace_confirmation(cx) + }), + cx, + ) + .into_any_element(), + ) + } + HomeFocusedView::BuyerOrderReview => { + let form = self.buyer_order_review_form.as_ref()?; + Some( + buyer_order_review_card( + form, + &runtime.personal_projection.cart.order_review, + cx.listener(|this, _, _, cx| this.close_personal_order_review(cx)), + cx.listener(|this, _, _, cx| this.place_personal_order(cx)), + cx, + ) + .into_any_element(), + ) + } + HomeFocusedView::BuyerOrderDetail(order_id) => { + let detail = runtime + .personal_projection + .orders + .detail + .as_ref() + .filter(|detail| detail.order_id == order_id)?; + Some( + buyer_order_detail_card( + detail, + None, + runtime + .personal_projection + .cart + .cart + .replace_confirmation + .as_ref(), + cx.listener(move |this, _, _, cx| { + this.close_personal_order_detail(order_id, cx) + }), + cx, + ) + .into_any_element(), + ) + } + HomeFocusedView::BuyerReceiptIssue(order_id) => { + let detail = runtime + .personal_projection + .orders + .detail + .as_ref() + .filter(|detail| detail.order_id == order_id)?; + let issue_form = self + .buyer_receipt_issue_form + .as_ref() + .filter(|form| form.order_id == order_id)?; + Some(buyer_receipt_issue_focused_view(detail, issue_form, cx)) + } + HomeFocusedView::FarmSetup + | HomeFocusedView::ProductEditor + | HomeFocusedView::FarmerOrderDetail(_) => None, + } + } + fn render_buyer_browse_content( &mut self, runtime: &DesktopAppRuntimeSummary, @@ -3061,53 +3250,6 @@ impl HomeView { } else { app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() - .when_some( - runtime.personal_projection.browse.detail.as_ref(), - |this, detail| { - this.child(buyer_product_detail_card( - detail, - runtime - .personal_projection - .cart - .cart - .replace_confirmation - .as_ref(), - cx.listener(|this, _, _, cx| { - this.close_personal_product_detail(PersonalSection::Browse, cx) - }), - cx.listener(|this, _, _, cx| { - this.decrease_personal_product_quantity( - PersonalSection::Browse, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.increase_personal_product_quantity( - PersonalSection::Browse, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.add_personal_product_to_cart( - PersonalSection::Browse, - false, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.add_personal_product_to_cart( - PersonalSection::Browse, - true, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.clear_personal_cart_replace_confirmation(cx) - }), - cx, - )) - }, - ) .child(buyer_listings_feed( PersonalSection::Browse, listings, @@ -3238,53 +3380,6 @@ impl HomeView { } else { app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() - .when_some( - runtime.personal_projection.search.detail.as_ref(), - |this, detail| { - this.child(buyer_product_detail_card( - detail, - runtime - .personal_projection - .cart - .cart - .replace_confirmation - .as_ref(), - cx.listener(|this, _, _, cx| { - this.close_personal_product_detail(PersonalSection::Search, cx) - }), - cx.listener(|this, _, _, cx| { - this.decrease_personal_product_quantity( - PersonalSection::Search, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.increase_personal_product_quantity( - PersonalSection::Search, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.add_personal_product_to_cart( - PersonalSection::Search, - false, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.add_personal_product_to_cart( - PersonalSection::Search, - true, - cx, - ) - }), - cx.listener(|this, _, _, cx| { - this.clear_personal_cart_replace_confirmation(cx) - }), - cx, - )) - }, - ) .child(buyer_listings_feed( PersonalSection::Search, listings, @@ -3326,15 +3421,6 @@ impl HomeView { self.buyer_order_review_form.is_some(), cx, )) - .when_some(self.buyer_order_review_form.as_ref(), |this, form| { - this.child(buyer_order_review_card( - form, - order_review, - cx.listener(|this, _, _, cx| this.close_personal_order_review(cx)), - cx.listener(|this, _, _, cx| this.place_personal_order(cx)), - cx, - )) - }) .into_any_element() }) .into_any_element() @@ -3373,30 +3459,6 @@ impl HomeView { selected_order_id, cx, )) - .child( - orders - .detail - .as_ref() - .map(|detail| { - let issue_form = - self.buyer_receipt_issue_form.as_ref().filter(|form| { - form.order_id == detail.order_id - && buyer_receipt_actions_available(detail) - }); - buyer_order_detail_card( - detail, - issue_form, - runtime - .personal_projection - .cart - .cart - .replace_confirmation - .as_ref(), - cx, - ) - }) - .unwrap_or_else(|| buyer_order_detail_empty_card().into_any_element()), - ) .into_any_element() }) .into_any_element() @@ -3408,22 +3470,24 @@ impl HomeView { cx: &mut Context<Self>, ) -> AnyElement { let selected_farmer_section = selected_farmer_section(runtime); - let main_content = match selected_farmer_section { - FarmerSection::Products if farmer_products_available(runtime) => { - self.render_products_content(runtime, cx) - } - FarmerSection::Orders if farmer_products_available(runtime) => { - self.render_orders_content(runtime, cx) - } - FarmerSection::PackDay if farmer_pack_day_available(runtime) => { - self.render_pack_day_content(runtime, cx) - } - FarmerSection::Today - | FarmerSection::Products - | FarmerSection::Orders - | FarmerSection::PackDay - | FarmerSection::Farm => self.render_today_content(runtime, cx), - }; + let main_content = self + .render_farmer_focused_view(runtime, cx) + .unwrap_or_else(|| match selected_farmer_section { + FarmerSection::Products if farmer_products_available(runtime) => { + self.render_products_content(runtime, cx) + } + FarmerSection::Orders if farmer_products_available(runtime) => { + self.render_orders_content(runtime, cx) + } + FarmerSection::PackDay if farmer_pack_day_available(runtime) => { + self.render_pack_day_content(runtime, cx) + } + FarmerSection::Today + | FarmerSection::Products + | FarmerSection::Orders + | FarmerSection::PackDay + | FarmerSection::Farm => self.render_today_content(runtime, cx), + }); app_split_shell( home_sidebar( @@ -3474,6 +3538,55 @@ impl HomeView { .into_any_element() } + fn render_farmer_focused_view( + &mut self, + runtime: &DesktopAppRuntimeSummary, + cx: &mut Context<Self>, + ) -> Option<AnyElement> { + match self.focused_view? { + HomeFocusedView::FarmSetup => { + let form = self.farm_setup_form.as_ref()?; + Some( + home_farm_setup_form_card( + form, + cx.listener(|this, checked: &bool, _, cx| { + this.toggle_farm_order_method(FarmOrderMethod::Pickup, *checked, cx) + }), + cx.listener(|this, checked: &bool, _, cx| { + this.toggle_farm_order_method(FarmOrderMethod::Delivery, *checked, cx) + }), + cx.listener(|this, checked: &bool, _, cx| { + this.toggle_farm_order_method(FarmOrderMethod::Shipping, *checked, cx) + }), + cx.listener(|this, _, _, cx| this.finish_farm_setup(cx)), + cx, + ) + .into_any_element(), + ) + } + HomeFocusedView::ProductEditor => { + let form = self.product_editor_form.as_ref()?; + Some(products_editor_surface(form, runtime, cx).into_any_element()) + } + HomeFocusedView::FarmerOrderDetail(order_id) => { + let detail = runtime + .orders_projection + .detail + .as_ref() + .filter(|detail| detail.order_id == order_id)?; + Some(self.render_order_detail_card( + detail, + cx.listener(move |this, _, _, cx| this.close_order_detail(order_id, cx)), + cx, + )) + } + HomeFocusedView::BuyerProductDetail(_) + | HomeFocusedView::BuyerOrderReview + | HomeFocusedView::BuyerOrderDetail(_) + | HomeFocusedView::BuyerReceiptIssue(_) => None, + } + } + fn render_products_content( &mut self, runtime: &DesktopAppRuntimeSummary, @@ -3544,27 +3657,6 @@ impl HomeView { cx.listener(|this, _, _, cx| this.select_products_sort(ProductsSort::Price, cx)), cx, )) - .when_some(self.product_editor_form.as_ref(), |this, form| { - this.child(products_editor_surface( - form, - runtime, - cx.listener(|this, _, _, cx| { - this.select_product_editor_status(ProductStatus::Draft, cx) - }), - cx.listener(|this, _, _, cx| { - this.select_product_editor_status(ProductStatus::Published, cx) - }), - cx.listener(|this, _, _, cx| { - this.select_product_editor_status(ProductStatus::Paused, cx) - }), - cx.listener(|this, _, _, cx| { - this.select_product_editor_status(ProductStatus::Archived, cx) - }), - cx.listener(|this, _, _, cx| this.close_product_editor(cx)), - cx.listener(|this, _, _, cx| this.save_product_editor(cx)), - cx, - )) - }) .child(if projection.list.is_empty() { products_empty_state_card(projection.query.filter).into_any_element() } else { @@ -3684,21 +3776,6 @@ impl HomeView { cx, ) }) - .when_some(projection.detail.as_ref(), |this, detail| { - this.child(self.render_order_detail_card(detail, cx)) - }) - .when( - projection.detail.is_none() && !projection.list.is_empty(), - |this| { - this.child( - home_card( - app_shared_text(AppTextKey::OrdersDetailTitle), - home_body_text(app_shared_text(AppTextKey::OrdersDetailEmptyBody)), - ) - .into_any_element(), - ) - }, - ) .into_any_element() } @@ -4285,6 +4362,7 @@ impl HomeView { fn render_order_detail_card( &mut self, detail: &OrderDetailProjection, + on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &mut Context<Self>, ) -> AnyElement { let fulfillment_actions = (!detail.fulfillment_actions.is_empty()).then(|| { @@ -4314,9 +4392,10 @@ impl HomeView { ) }); - home_card( + app_focused_detail_view( app_shared_text(AppTextKey::OrdersDetailTitle), app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() .child(app_heading_section(detail.order_number.clone())) .child(home_body_text(detail.customer_display_name.clone())) .child(trade_workflow_detail_badge_strip(&detail.workflow)) @@ -4365,8 +4444,13 @@ impl HomeView { .when_some(fulfillment_actions, |this, fulfillment_actions| { this.child(fulfillment_actions) }), + text_button( + "orders-detail-back", + app_shared_text(AppTextKey::PersonalDetailBackAction), + on_close, + cx, + ), ) - .into_any_element() } fn render_order_recovery_section( @@ -5091,6 +5175,7 @@ struct ProductEditorFormState { product_id: ProductId, initial_draft: ProductEditorDraft, status: ProductStatus, + selected_availability_window_id: Option<FulfillmentWindowId>, title_input: Entity<InputState>, subtitle_input: Entity<InputState>, category_input: Entity<InputState>, @@ -5114,6 +5199,7 @@ impl ProductEditorFormState { window: &mut Window, cx: &mut Context<HomeView>, ) -> Self { + let selected_availability_window_id = draft.availability_window_id; let title_input = cx.new(|cx| InputState::new(window, cx).default_value(draft.title.clone())); let subtitle_input = @@ -5169,6 +5255,7 @@ impl ProductEditorFormState { account_id, product_id, status: draft.status, + selected_availability_window_id, initial_draft: draft, title_input, subtitle_input, @@ -5199,7 +5286,7 @@ impl ProductEditorFormState { stock_quantity: parse_optional_product_editor_stock_input( self.stock_input.read(cx).value().as_ref(), )?, - availability_window_id: self.initial_draft.availability_window_id, + availability_window_id: self.selected_availability_window_id, status: self.status, }) } @@ -6185,7 +6272,6 @@ enum SettingsAutoFocusTarget { Navigation(SettingsPanelViewKey), AccountAdd, FarmNameInput, - SettingsAllowRelayConnections, AboutRefresh, } @@ -6615,19 +6701,14 @@ impl SettingsWindowView { .gap(px(APP_UI_THEME .shells .settings_account_sidebar_footer_button_gap_px)) - .child(action_button( + .child(action_button_disabled( "account-add", app_shared_text(AppTextKey::SettingsAccountAddAction), - |_, _, _| {}, cx, )) - .child(action_icon_button( - AppIconButtonSpec::new( - "account-more", - app_shared_text(AppTextKey::SettingsAccountMoreActions), - IconName::ChevronDown, - ), - |_, _, _| {}, + .child(action_button_disabled( + "account-more", + app_shared_text(AppTextKey::SettingsAccountMoreActions), cx, )), ), @@ -6768,20 +6849,18 @@ impl SettingsWindowView { .gap(px(APP_UI_THEME .shells .settings_account_action_row_gap_px)) - .child(div().child(action_button( + .child(div().child(action_button_disabled( "account-log-out", app_shared_text( AppTextKey::SettingsAccountLogOutAction, ), - |_, _, _| {}, cx, ))) - .child(div().child(action_button( + .child(div().child(action_button_disabled( "account-open-workspace", app_shared_text( AppTextKey::SettingsAccountOpenWorkspaceAction, ), - |_, _, _| {}, cx, ))), ), @@ -6793,13 +6872,6 @@ impl SettingsWindowView { fn settings_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { self.sync_farm_panel_state(window, cx); - let runtime_summary = self.runtime.summary(); - let general_settings = runtime_summary.shell_projection.settings.general; - let general_allow_relay_connections = general_settings.allow_relay_connections; - let general_use_media_servers = general_settings.use_media_servers; - let general_use_nip05 = general_settings.use_nip05; - let general_launch_at_login = general_settings.launch_at_login; - let mut cards = Vec::new(); if let Some(error) = self.farm_panel_error.as_ref() { @@ -7054,84 +7126,9 @@ impl SettingsWindowView { cards.push( home_card( app_shared_text(AppTextKey::SettingsGeneralSectionLabel), - app_stack_v(16.0) + app_stack_v(APP_UI_THEME.foundation.spacing.small_px) .w_full() - .child( - div() - .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "settings-allow-relay-connections", - app_shared_text( - AppTextKey::SettingsGeneralAllowRelayConnections, - ), - Option::<SharedString>::None, - ), - general_allow_relay_connections, - cx, - |_, _, _| {}, - )), - ) - .child( - div() - .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "settings-use-media-servers", - app_shared_text(AppTextKey::SettingsGeneralUseMediaServers), - Option::<SharedString>::None, - ), - general_use_media_servers, - cx, - |_, _, _| {}, - )) - .child(div().flex_none().child(action_button_compact( - "settings-manage-media-servers", - app_shared_text(AppTextKey::SettingsGeneralManageAction), - |_, _, _| {}, - cx, - ))), - ) - .child( - div() - .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "settings-use-nip05", - app_shared_text(AppTextKey::SettingsGeneralUseNip05), - Some(app_shared_text(AppTextKey::SettingsGeneralUseNip05Note)), - ), - general_use_nip05, - cx, - |_, _, _| {}, - )), - ) - .child( - div() - .w_full() - .flex() - .items_start() - .gap(px(APP_UI_THEME.shells.settings_account_detail_value_gap_px)) - .child(app_checkbox_field( - AppCheckboxFieldSpec::new( - "settings-launch-at-login", - app_shared_text(AppTextKey::SettingsGeneralLaunchAtLogin), - Option::<SharedString>::None, - ), - general_launch_at_login, - cx, - |_, _, _| {}, - )), - ), + .child(label_value_list(settings_preferences_general_rows())), ) .into_any_element(), ); @@ -7139,7 +7136,7 @@ impl SettingsWindowView { app_scroll_panel( "settings-panel-scroll", APP_UI_THEME.shells.settings_content_padding_px, - Some(560.0), + Some(APP_UI_THEME.shells.settings_panel_content_max_width_px), app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() .child(home_body_text(app_shared_text( @@ -7426,9 +7423,6 @@ impl SettingsWindowView { .update(cx, |input, cx| input.focus(window, cx)); } } - SettingsAutoFocusTarget::SettingsAllowRelayConnections => { - focus_button(window, "settings-allow-relay-connections", cx); - } SettingsAutoFocusTarget::AboutRefresh => { focus_button(window, "settings-about-refresh-sync", cx); } @@ -7915,9 +7909,9 @@ fn settings_auto_focus_target( .or(Some(SettingsAutoFocusTarget::Navigation( SettingsPanelViewKey::Farm, ))), - SettingsPanelViewKey::Settings => { - Some(SettingsAutoFocusTarget::SettingsAllowRelayConnections) - } + SettingsPanelViewKey::Settings => Some(SettingsAutoFocusTarget::Navigation( + SettingsPanelViewKey::Settings, + )), SettingsPanelViewKey::About => { if about_manual_refresh_enabled(&runtime.sync_status) { Some(SettingsAutoFocusTarget::AboutRefresh) @@ -8167,17 +8161,7 @@ fn shared_shell_mode_button( on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> AnyElement { - if is_active { - div() - .id(id) - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(key)) - .into_any_element() - } else { - action_button_compact(id, app_shared_text(key), on_click, cx).into_any_element() - } + choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element() } fn shell_account_entry( @@ -8195,12 +8179,7 @@ fn shell_account_entry( ) .into_any_element() } else { - div() - .id("shell-account-label") - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(account_label) + action_button_compact("shell-account-entry", account_label, on_open_account, cx) .into_any_element() } } @@ -8217,7 +8196,7 @@ fn shell_account_label(runtime: &DesktopAppRuntimeSummary) -> String { .as_ref() .map(|label| label.trim().to_owned()) .filter(|label| !label.is_empty()) - .or_else(|| Some(account.account.npub.clone())) + .or_else(|| Some(app_shared_text(AppTextKey::HomeHeaderAccountLabel).to_string())) }) .unwrap_or_else(|| app_shared_text(AppTextKey::HomeHeaderGuestLabel).to_string()) } @@ -8436,34 +8415,13 @@ fn buyer_product_detail_card( on_keep_current_cart: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { - app_surface_card( + app_focused_detail_view( + product_display_title(detail.listing.title.as_str()), app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() - .child( - div() - .w_full() - .flex() - .items_start() - .justify_between() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child( - app_stack_v(4.0) - .flex_1() - .min_w_0() - .child(app_text_value(product_display_title( - detail.listing.title.as_str(), - ))) - .child(settings_badge_text( - detail.listing.farm_display_name.clone(), - )), - ) - .child(text_button( - "buyer-detail-back", - app_shared_text(AppTextKey::PersonalDetailBackAction), - on_close, - cx, - )), - ) + .child(settings_badge_text( + detail.listing.farm_display_name.clone(), + )) .when_some( detail .detail_text @@ -8563,6 +8521,12 @@ fn buyer_product_detail_card( on_add_to_cart, cx, )), + text_button( + "buyer-detail-back", + app_shared_text(AppTextKey::PersonalDetailBackAction), + on_close, + cx, + ), ) } @@ -9102,12 +9066,13 @@ fn buyer_order_detail_card( detail: &BuyerOrderDetailProjection, issue_form: Option<&BuyerReceiptIssueFormState>, replace_confirmation: Option<&BuyerCartReplaceConfirmationProjection>, + on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &mut Context<HomeView>, ) -> AnyElement { let repeat_confirmation = replace_confirmation .filter(|confirmation| confirmation.incoming_farm_display_name == detail.farm_display_name); - home_card( + app_focused_detail_view( app_shared_text(AppTextKey::PersonalOrdersDetailTitle), app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() @@ -9317,8 +9282,13 @@ fn buyer_order_detail_card( ), )) }), + text_button( + "buyer-order-detail-back", + app_shared_text(AppTextKey::PersonalDetailBackAction), + on_close, + cx, + ), ) - .into_any_element() } fn buyer_receipt_summary_section(receipt: &TradeReceiptProjection) -> AnyElement { @@ -9366,37 +9336,13 @@ fn validation_receipt_summary_panel(receipt: &TradeValidationReceiptProjection) ))) .child(trade_workflow_value_badge(validation_receipt_type_key( receipt.receipt_type, - ))) - .child(trade_workflow_value_badge( - validation_receipt_proof_system_key(receipt.proof_system), - )), + ))), ) - .child(label_value_list([ - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptEventLabel), - short_evidence_text(receipt.event_id.as_str()), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptTargetLabel), - short_evidence_text(receipt.target_event_id.as_str()), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptEventSetRootLabel), - short_evidence_text(receipt.event_set_root.as_str()), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptReducerOutputRootLabel), - short_evidence_text(receipt.reducer_output_root.as_str()), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptPublicValuesHashLabel), - short_evidence_text(receipt.public_values_hash.as_str()), - ), - LabelValueRow::new( - app_shared_text(AppTextKey::TradeValidationReceiptRecordedAtLabel), - receipt.recorded_at.to_string(), - ), - ])), + .child(home_body_text(format!( + "{} {}", + app_shared_text(AppTextKey::TradeValidationReceiptRecordedAtLabel), + receipt.recorded_at + ))), ) .into_any_element() } @@ -9450,6 +9396,54 @@ fn buyer_receipt_issue_form_section( .into_any_element() } +fn buyer_receipt_issue_focused_view( + detail: &BuyerOrderDetailProjection, + form: &BuyerReceiptIssueFormState, + cx: &mut Context<HomeView>, +) -> AnyElement { + let order_id = form.order_id; + let submit_action = if form.can_submit(cx) { + action_button_primary( + "buyer-order-send-issue", + app_shared_text(AppTextKey::PersonalOrdersActionSendReceiptIssue), + cx.listener(move |this, _, _, cx| this.submit_buyer_order_issue_receipt(order_id, cx)), + cx, + ) + .into_any_element() + } else { + action_button_primary_disabled( + "buyer-order-send-issue", + app_shared_text(AppTextKey::PersonalOrdersActionSendReceiptIssue), + cx, + ) + .into_any_element() + }; + + app_focused_task_view( + app_shared_text(AppTextKey::PersonalOrdersActionReportIssue), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(app_heading_section(detail.order_number.clone())) + .child(settings_badge_text(detail.farm_display_name.clone())) + .child(home_body_text(detail.fulfillment_summary.clone())) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::PersonalOrdersReceiptIssueLabel), + Option::<SharedString>::None, + ), + &form.issue_input, + false, + )) + .child(submit_action), + text_button( + "buyer-order-close-issue", + app_shared_text(AppTextKey::PersonalDetailBackAction), + cx.listener(|this, _, _, cx| this.close_buyer_receipt_issue_form(cx)), + cx, + ), + ) +} + fn buyer_receipt_actions_available(detail: &BuyerOrderDetailProjection) -> bool { detail.workflow.receipt.is_none() && detail.workflow.agreement == TradeAgreementStatus::Confirmed @@ -9493,34 +9487,6 @@ fn validation_receipt_type_key(receipt_type: TradeValidationReceiptType) -> AppT } } -fn validation_receipt_proof_system_key( - proof_system: TradeValidationReceiptProofSystem, -) -> AppTextKey { - match proof_system { - TradeValidationReceiptProofSystem::None => AppTextKey::TradeValidationReceiptProofNone, - TradeValidationReceiptProofSystem::Sp1Core => { - AppTextKey::TradeValidationReceiptProofSp1Core - } - TradeValidationReceiptProofSystem::Sp1Compressed => { - AppTextKey::TradeValidationReceiptProofSp1Compressed - } - TradeValidationReceiptProofSystem::Sp1Groth16 => { - AppTextKey::TradeValidationReceiptProofSp1Groth16 - } - TradeValidationReceiptProofSystem::Sp1Plonk => { - AppTextKey::TradeValidationReceiptProofSp1Plonk - } - } -} - -fn short_evidence_text(value: &str) -> String { - if value.len() <= 16 { - value.to_owned() - } else { - format!("{}...", &value[..16]) - } -} - fn buyer_repeat_demand_action_label(repeat_demand: &RepeatDemandHandoffProjection) -> SharedString { match repeat_demand.eligibility { RepeatDemandEligibility::Eligible => { @@ -9550,13 +9516,6 @@ fn buyer_repeat_demand_note(repeat_demand: &RepeatDemandHandoffProjection) -> Op } } -fn buyer_order_detail_empty_card() -> impl IntoElement { - home_card( - app_shared_text(AppTextKey::PersonalOrdersDetailTitle), - home_body_text(app_shared_text(AppTextKey::PersonalOrdersDetailEmptyBody)), - ) -} - fn buyer_orders_status_color(status: BuyerOrderStatus) -> u32 { match status { BuyerOrderStatus::Placed => APP_UI_THEME.components.app_status_indicator.attention, @@ -10295,21 +10254,11 @@ fn buyer_sidebar_nav_button( on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> AnyElement { - if is_active { - div() - .id(id) - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0)) - .font_weight(gpui::FontWeight::BOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(key)) - .into_any_element() - } else { - action_button(id, app_shared_text(key), on_click, cx).into_any_element() - } + choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element() } fn home_sidebar_navigation_sections( - selected_section: FarmerSection, + _selected_section: FarmerSection, workspace_available: bool, pack_day_available: bool, ) -> Vec<FarmerSection> { @@ -10322,14 +10271,6 @@ fn home_sidebar_navigation_sections( sections.push(FarmerSection::PackDay); } - if let Some(selected_index) = sections - .iter() - .position(|section| *section == selected_section) - { - let selected = sections.remove(selected_index); - sections.insert(0, selected); - } - sections } @@ -10402,17 +10343,7 @@ fn home_sidebar_nav_button( return div().id(id).into_any_element(); } - if is_active { - div() - .id(id) - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0)) - .font_weight(gpui::FontWeight::BOLD) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(key)) - .into_any_element() - } else { - action_button(id, app_shared_text(key), on_click, cx).into_any_element() - } + choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element() } fn products_title_row( @@ -12311,138 +12242,133 @@ fn products_stock_editor_validation_key( fn products_editor_surface( form: &ProductEditorFormState, runtime: &DesktopAppRuntimeSummary, - on_select_draft: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_select_live: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_select_paused: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_select_archived: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - on_save: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - cx: &App, -) -> impl IntoElement { + cx: &mut Context<HomeView>, +) -> AnyElement { let validation_keys = products_editor_validation_keys(form, cx); let save_ready = form.has_changes(cx) && validation_keys.is_empty(); - div().w_full().flex().justify_center().child( - div().w_full().max_w(px(520.0)).child(home_card( - app_shared_text(AppTextKey::ProductsEditorTitle), - div() - .w_full() - .flex() - .flex_col() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child(home_body_text(app_shared_text( - AppTextKey::ProductsEditorBody, - ))) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldTitle), - Option::<SharedString>::None, - ), - &form.title_input, - false, - )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldSubtitle), - Option::<SharedString>::None, - ), - &form.subtitle_input, - false, - )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldCategory), - Option::<SharedString>::None, - ), - &form.category_input, - false, - )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldUnit), - Option::<SharedString>::None, - ), - &form.unit_input, - false, - )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldPrice), - products_editor_invalid_price_key(form, cx).map(app_shared_text), - ), - &form.price_input, - false, - )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text(AppTextKey::ProductsEditorFieldStock), - products_editor_invalid_stock_key(form, cx).map(app_shared_text), - ), - &form.stock_input, - false, - )) - .child(products_editor_status_section( - form.status, - on_select_draft, - on_select_live, - on_select_paused, - on_select_archived, - cx, - )) - .child(products_editor_publish_readiness_section(form, runtime, cx)) - .when(form.save_failed, |this| { - this.child(home_body_text(app_shared_text( - AppTextKey::ProductsEditorSaveFailed, - ))) - }) - .child( - div() - .w_full() - .flex() - .items_center() - .justify_between() - .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) - .child( - div() - .text_size(px(APP_UI_THEME - .foundation - .typography - .utility_title_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(product_display_title( - form.title_input.read(cx).value().as_ref(), - )), - ) - .child( - div() - .flex() - .items_center() - .gap(px(8.0)) - .child(action_button_compact( - "products-editor-close", - app_shared_text(AppTextKey::ProductsEditorCloseAction), - on_close, - cx, - )) - .child(if save_ready { - action_button_primary( - "products-editor-save", - app_shared_text(AppTextKey::ProductsEditorSaveAction), - on_save, - cx, - ) - .into_any_element() - } else { - action_button_primary_disabled( - "products-editor-save", - app_shared_text(AppTextKey::ProductsEditorSaveAction), - cx, - ) - .into_any_element() - }), - ), + let save_action = if save_ready { + action_button_primary( + "products-editor-save", + app_shared_text(AppTextKey::ProductsEditorSaveAction), + cx.listener(|this, _, _, cx| this.save_product_editor(cx)), + cx, + ) + .into_any_element() + } else { + action_button_primary_disabled( + "products-editor-save", + app_shared_text(AppTextKey::ProductsEditorSaveAction), + cx, + ) + .into_any_element() + }; + + app_focused_task_view( + app_shared_text(AppTextKey::ProductsEditorTitle), + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(home_body_text(app_shared_text( + AppTextKey::ProductsEditorBody, + ))) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldTitle), + Option::<SharedString>::None, ), - )), + &form.title_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldSubtitle), + Option::<SharedString>::None, + ), + &form.subtitle_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldCategory), + Option::<SharedString>::None, + ), + &form.category_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldUnit), + Option::<SharedString>::None, + ), + &form.unit_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldPrice), + products_editor_invalid_price_key(form, cx).map(app_shared_text), + ), + &form.price_input, + false, + )) + .child(app_form_input_text( + AppFormFieldSpec::new( + app_shared_text(AppTextKey::ProductsEditorFieldStock), + products_editor_invalid_stock_key(form, cx).map(app_shared_text), + ), + &form.stock_input, + false, + )) + .child(products_editor_availability_section( + form, + &runtime.farm_rules_projection.fulfillment_windows, + cx, + )) + .child(products_editor_status_section( + form.status, + cx.listener(|this, _, _, cx| { + this.select_product_editor_status(ProductStatus::Draft, cx) + }), + cx.listener(|this, _, _, cx| { + this.select_product_editor_status(ProductStatus::Published, cx) + }), + cx.listener(|this, _, _, cx| { + this.select_product_editor_status(ProductStatus::Paused, cx) + }), + cx.listener(|this, _, _, cx| { + this.select_product_editor_status(ProductStatus::Archived, cx) + }), + cx, + )) + .child(products_editor_publish_readiness_section(form, runtime, cx)) + .when(form.save_failed, |this| { + this.child(home_body_text(app_shared_text( + AppTextKey::ProductsEditorSaveFailed, + ))) + }) + .child( + div() + .w_full() + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(product_display_title( + form.title_input.read(cx).value().as_ref(), + )), + ) + .child(save_action), + ), + text_button( + "products-editor-close", + app_shared_text(AppTextKey::ProductsEditorCloseAction), + cx.listener(|this, _, _, cx| this.close_product_editor(cx)), + cx, + ), ) } @@ -12500,6 +12426,49 @@ fn products_editor_status_section( ) } +fn products_editor_availability_section( + form: &ProductEditorFormState, + fulfillment_windows: &[FulfillmentWindowRecord], + cx: &mut Context<HomeView>, +) -> impl IntoElement { + let choices = fulfillment_windows + .iter() + .enumerate() + .map(|(index, fulfillment_window)| { + let fulfillment_window_id = fulfillment_window.fulfillment_window_id; + choice_button( + ("products-editor-availability", index), + SharedString::from(fulfillment_window.label.clone()), + form.selected_availability_window_id == Some(fulfillment_window_id), + cx.listener(move |this, _, _, cx| { + this.select_product_editor_availability_window(fulfillment_window_id, cx) + }), + cx, + ) + .into_any_element() + }) + .collect::<Vec<_>>(); + + div() + .w_full() + .flex() + .flex_col() + .items_start() + .gap(px(APP_UI_THEME.foundation.spacing.small_px)) + .child(home_farm_setup_field_label(app_shared_text( + AppTextKey::ProductsEditorFieldAvailability, + ))) + .child(if choices.is_empty() { + home_body_text(app_shared_text(AppTextKey::ProductsEditorAvailabilityEmpty)) + .into_any_element() + } else { + app_cluster(APP_UI_THEME.foundation.spacing.tight_px) + .w_full() + .children(choices) + .into_any_element() + }) +} + fn products_editor_publish_readiness_section( form: &ProductEditorFormState, runtime: &DesktopAppRuntimeSummary, @@ -14710,7 +14679,9 @@ mod tests { ); assert_eq!( settings_auto_focus_target(SettingsPanelViewKey::Settings, None, &runtime), - Some(SettingsAutoFocusTarget::SettingsAllowRelayConnections) + Some(SettingsAutoFocusTarget::Navigation( + SettingsPanelViewKey::Settings + )) ); let mut about_enabled = runtime.clone(); @@ -15498,7 +15469,7 @@ mod tests { } #[test] - fn sidebar_navigation_keeps_the_active_destination_first() { + fn sidebar_navigation_keeps_destinations_stable() { assert_eq!( home_sidebar_navigation_sections(FarmerSection::Today, true, false), vec![ @@ -15510,26 +15481,26 @@ mod tests { assert_eq!( home_sidebar_navigation_sections(FarmerSection::Products, true, false), vec![ - FarmerSection::Products, FarmerSection::Today, + FarmerSection::Products, FarmerSection::Orders, ] ); assert_eq!( home_sidebar_navigation_sections(FarmerSection::Orders, true, false), vec![ - FarmerSection::Orders, FarmerSection::Today, FarmerSection::Products, + FarmerSection::Orders, ] ); assert_eq!( home_sidebar_navigation_sections(FarmerSection::PackDay, true, true), vec![ - FarmerSection::PackDay, FarmerSection::Today, FarmerSection::Products, FarmerSection::Orders, + FarmerSection::PackDay, ] ); } diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -25,6 +25,7 @@ define_app_text_keys! { HomeHeaderMarketplaceMode => "home.header.marketplace_mode", HomeHeaderFarmMode => "home.header.farm_mode", HomeHeaderAccountSetupAction => "home.header.account_setup_action", + HomeHeaderAccountLabel => "home.header.account_label", HomeHeaderGuestLabel => "home.header.guest_label", HomeNavBrowse => "home.nav.browse", HomeNavSearch => "home.nav.search", @@ -423,7 +424,9 @@ define_app_text_keys! { ProductsEditorFieldUnit => "products.editor.field.unit", ProductsEditorFieldPrice => "products.editor.field.price", ProductsEditorFieldStock => "products.editor.field.stock", + ProductsEditorFieldAvailability => "products.editor.field.availability", ProductsEditorFieldStatus => "products.editor.field.status", + ProductsEditorAvailabilityEmpty => "products.editor.availability.empty", ProductsEditorCloseAction => "products.editor.action.close", ProductsEditorSaveAction => "products.editor.action.save", ProductsEditorSaveFailed => "products.editor.save_failed", diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -607,15 +607,15 @@ mod tests { (AppTextKey::TradeValidationReceiptTargetLabel, "Target"), ( AppTextKey::TradeValidationReceiptEventSetRootLabel, - "Event set", + "Evidence set", ), ( AppTextKey::TradeValidationReceiptReducerOutputRootLabel, - "Output root", + "Review output", ), ( AppTextKey::TradeValidationReceiptPublicValuesHashLabel, - "Values hash", + "Verification values", ), ( AppTextKey::TradeValidationReceiptRecordedAtLabel, @@ -643,16 +643,19 @@ mod tests { "State", ), (AppTextKey::TradeValidationReceiptProofNone, "None"), - (AppTextKey::TradeValidationReceiptProofSp1Core, "SP1 core"), + (AppTextKey::TradeValidationReceiptProofSp1Core, "Core proof"), ( AppTextKey::TradeValidationReceiptProofSp1Compressed, - "SP1 compressed", + "Compressed proof", ), ( AppTextKey::TradeValidationReceiptProofSp1Groth16, - "SP1 Groth16", + "Groth16 proof", + ), + ( + AppTextKey::TradeValidationReceiptProofSp1Plonk, + "Plonk proof", ), - (AppTextKey::TradeValidationReceiptProofSp1Plonk, "SP1 Plonk"), ] { assert_eq!(app_text(key), expected); assert!( diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs @@ -9,12 +9,12 @@ pub use primitives::{ LabelValueRow, app_button_card, app_button_choice, app_button_compact, app_button_icon, app_button_list_row, app_button_primary, app_button_primary_disabled, app_button_secondary, app_button_secondary_disabled, app_button_text, app_checkbox_field, app_cluster, - app_detail_row, app_divider, app_form_field, app_form_input_text, app_form_section, - app_heading_section, app_heading_view, app_input_text, app_scroll_panel, - app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v, app_status_indicator, - app_surface_card, app_surface_card_section, app_surface_panel, app_surface_sidebar, - app_surface_window, app_text_badge, app_text_body, app_text_body_subtle, app_text_label, - app_text_value, label_value_list, utility_title_row, + app_detail_row, app_divider, app_focused_detail_view, app_focused_task_view, app_form_field, + app_form_input_text, app_form_section, app_heading_section, app_heading_view, app_input_text, + app_scroll_panel, app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v, + app_status_indicator, app_surface_card, app_surface_card_section, app_surface_panel, + app_surface_sidebar, app_surface_window, app_text_badge, app_text_body, app_text_body_subtle, + app_text_label, app_text_value, label_value_list, utility_title_row, }; pub use text::{ app_shared_label_text, app_shared_text, runtime_metadata_rows, diff --git a/crates/ui/src/primitives.rs b/crates/ui/src/primitives.rs @@ -143,6 +143,60 @@ pub fn app_surface_card_section( app_surface_card(app_form_section(title, body)) } +pub fn app_focused_task_view( + title: impl Into<SharedString>, + body: impl IntoElement, + actions: impl IntoElement, +) -> AnyElement { + app_focused_view( + APP_UI_THEME.shells.focused_task_max_width_px, + title, + body, + actions, + ) +} + +pub fn app_focused_detail_view( + title: impl Into<SharedString>, + body: impl IntoElement, + actions: impl IntoElement, +) -> AnyElement { + app_focused_view( + APP_UI_THEME.shells.focused_detail_max_width_px, + title, + body, + actions, + ) +} + +fn app_focused_view( + max_width_px: f32, + title: impl Into<SharedString>, + body: impl IntoElement, + actions: impl IntoElement, +) -> AnyElement { + div() + .w_full() + .max_w(px(max_width_px)) + .mx_auto() + .child(app_surface_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child( + div() + .w_full() + .flex() + .items_start() + .justify_between() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(app_text_value(title)) + .child(actions), + ) + .child(body), + )) + .into_any_element() +} + pub fn app_stack_v(gap_px: f32) -> Div { div().flex().flex_col().gap(px(gap_px)) } @@ -440,7 +494,7 @@ pub fn app_checkbox_field( .w_full() .flex() .flex_col() - .gap(px(4.0)) + .gap(px(APP_UI_THEME.foundation.spacing.micro_px)) .child( Button::new((checkbox_id, 0usize)) .custom( diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs @@ -161,6 +161,9 @@ pub struct AppShellTokens { pub home_window_padding_px: f32, pub home_sidebar_width_px: f32, pub home_card_max_width_px: f32, + pub focused_task_max_width_px: f32, + pub focused_detail_max_width_px: f32, + pub settings_panel_content_max_width_px: f32, pub home_card_padding_px: f32, pub home_stack_gap_px: f32, pub startup_stack_gap_px: f32, @@ -334,6 +337,9 @@ pub const APP_UI_THEME: AppUiTheme = AppUiTheme { home_window_padding_px: APP_SPACING_XLARGE_PX, home_sidebar_width_px: 240.0, home_card_max_width_px: 1080.0, + focused_task_max_width_px: 720.0, + focused_detail_max_width_px: 840.0, + settings_panel_content_max_width_px: 560.0, home_card_padding_px: APP_SPACING_XLARGE_PX, home_stack_gap_px: APP_SPACING_MEDIUM_PX, startup_stack_gap_px: APP_SPACING_TIGHT_PX, @@ -401,6 +407,8 @@ mod tests { assert_eq!(APP_UI_THEME.shells.home_min_height_px, 795.0); assert_eq!(APP_UI_THEME.shells.home_sidebar_width_px, 240.0); assert_eq!(APP_UI_THEME.shells.home_window_padding_px, 24.0); + assert_eq!(APP_UI_THEME.shells.focused_task_max_width_px, 720.0); + assert_eq!(APP_UI_THEME.shells.focused_detail_max_width_px, 840.0); } #[test] @@ -409,6 +417,10 @@ mod tests { assert_eq!(APP_UI_THEME.shells.settings_height_px, 540.0); assert_eq!(APP_UI_THEME.shells.settings_chrome_height_px, 88.0); assert_eq!(APP_UI_THEME.shells.settings_content_padding_px, 24.0); + assert_eq!( + APP_UI_THEME.shells.settings_panel_content_max_width_px, + 560.0 + ); assert_eq!(APP_UI_THEME.shells.settings_account_sidebar_width_px, 200.0); assert_eq!( APP_UI_THEME diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -4,6 +4,7 @@ "home.header.marketplace_mode": "Marketplace", "home.header.farm_mode": "Farm", "home.header.account_setup_action": "Set up account", + "home.header.account_label": "Account", "home.header.guest_label": "Guest", "home.nav.browse": "Browse", "home.nav.search": "Search", @@ -213,9 +214,9 @@ "trade.validation.section.label": "Validation", "trade.validation.event.label": "Receipt", "trade.validation.target.label": "Target", - "trade.validation.event_set_root.label": "Event set", - "trade.validation.reducer_output_root.label": "Output root", - "trade.validation.public_values_hash.label": "Values hash", + "trade.validation.event_set_root.label": "Evidence set", + "trade.validation.reducer_output_root.label": "Review output", + "trade.validation.public_values_hash.label": "Verification values", "trade.validation.recorded_at.label": "Recorded", "trade.validation.result.valid": "Valid", "trade.validation.result.needs_review": "Needs review", @@ -224,10 +225,10 @@ "trade.validation.type.inventory_state": "Stock", "trade.validation.type.state_checkpoint": "State", "trade.validation.proof.none": "None", - "trade.validation.proof.sp1_core": "SP1 core", - "trade.validation.proof.sp1_compressed": "SP1 compressed", - "trade.validation.proof.sp1_groth16": "SP1 Groth16", - "trade.validation.proof.sp1_plonk": "SP1 Plonk", + "trade.validation.proof.sp1_core": "Core proof", + "trade.validation.proof.sp1_compressed": "Compressed proof", + "trade.validation.proof.sp1_groth16": "Groth16 proof", + "trade.validation.proof.sp1_plonk": "Plonk proof", "orders.recovery.section.title": "Recovery", "orders.recovery.missed_pickup.title": "Missed pickup", "orders.recovery.missed_pickup.body": "Use this when a buyer did not collect the order as planned.", @@ -399,11 +400,13 @@ "products.editor.field.title": "Name", "products.editor.field.subtitle": "Details", "products.editor.field.category": "Category", - "products.editor.field.unit": "Unit", - "products.editor.field.price": "Price (USD)", - "products.editor.field.stock": "Stock", - "products.editor.field.status": "Status", - "products.editor.action.close": "Close", + "products.editor.field.unit": "Unit", + "products.editor.field.price": "Price (USD)", + "products.editor.field.stock": "Stock", + "products.editor.field.availability": "Availability", + "products.editor.field.status": "Status", + "products.editor.availability.empty": "Add a fulfillment window in Farm settings before publishing.", + "products.editor.action.close": "Close", "products.editor.action.save": "Save changes", "products.editor.save_failed": "Couldn't save product details. Try again.", "products.editor.invalid_price": "Enter dollars and cents, for example 6.50.", @@ -527,12 +530,12 @@ "settings.readiness.field.blackout_overlaps_fulfillment_window": "A blackout period overlaps a fulfillment window.", "settings.readiness.ready": "Ready", "settings.general.section.label": "General", - "settings.general.allow_relay_connections": "Allow relay connections", - "settings.general.use_media_servers": "Use Radroots media servers", - "settings.general.use_nip05": "Use Radroots NIP-05", + "settings.general.allow_relay_connections": "Share updates securely", + "settings.general.use_media_servers": "Use Radroots media storage", + "settings.general.use_nip05": "Use verified farm profile", "settings.general.launch_at_login": "Launch Radroots at login", - "settings.general.action.manage": "Manage...", - "settings.general.use_nip05.note": "A kind-0 metadata event will be posted including your NIP-05 profile name and a request will be sent to radroots over NIP-96 to reserve your configured username", + "settings.general.action.manage": "Manage", + "settings.general.use_nip05.note": "Helps customers verify this farm profile through Radroots.", "settings.about.status.section.label": "Status", "settings.about.conflict_review.section.label": "Conflict review", "settings.about.runtime.section.label": "Runtime",