refactor: Split new_listing module into logical submodules
- Organized 810-line monolithic file into 10 focused modules - Main mod.rs reduced from 810 to 27 lines (96.7% reduction) - Clear separation of concerns with logical hierarchy: Module Structure: ├── mod.rs (27 lines) - Module coordinator and exports ├── handlers.rs (362 lines) - Main teloxide handler functions ├── callbacks.rs (205 lines) - Callback query processing ├── validations.rs (145 lines) - Input validation logic ├── ui.rs (102 lines) - Display and summary functions ├── types.rs (82 lines) - Data structures and states ├── field_processing.rs (76 lines) - Core field update logic ├── messages.rs (73 lines) - Centralized message constants ├── handler_factory.rs (60 lines) - Teloxide handler tree └── keyboard.rs (55 lines) - Button and keyboard definitions Benefits: - Single responsibility principle enforced - Easy navigation and maintenance - Reduced coupling between components - Enhanced testability - All 112 tests still passing
This commit is contained in:
205
src/commands/new_listing/callbacks.rs
Normal file
205
src/commands/new_listing/callbacks.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
//! Callback handling for the new listing wizard
|
||||||
|
//!
|
||||||
|
//! This module handles all callback query processing for buttons
|
||||||
|
//! in the new listing creation and editing workflows.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands::new_listing::{
|
||||||
|
field_processing::transition_to_field,
|
||||||
|
messages::{get_keyboard_for_field, get_step_message},
|
||||||
|
types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState},
|
||||||
|
ui::show_confirmation_screen,
|
||||||
|
keyboard::*,
|
||||||
|
},
|
||||||
|
db::{listing::ListingFields, ListingDuration},
|
||||||
|
message_utils::*,
|
||||||
|
HandlerResult, RootDialogue,
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use teloxide::{prelude::*, types::CallbackQuery, Bot};
|
||||||
|
|
||||||
|
/// Handle callbacks during the field input phase
|
||||||
|
pub async fn handle_awaiting_draft_field_callback(
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
(field, draft): (ListingField, ListingDraft),
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||||
|
info!("User {from:?} selected callback: {data:?}");
|
||||||
|
let target = (from, message_id);
|
||||||
|
|
||||||
|
if data == "cancel" {
|
||||||
|
return cancel_wizard(&bot, dialogue, target).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified callback dispatch
|
||||||
|
match field {
|
||||||
|
ListingField::Description if data == "skip" => {
|
||||||
|
handle_description_skip_callback(&bot, dialogue, draft, target).await
|
||||||
|
}
|
||||||
|
ListingField::Slots if data.starts_with("slots_") => {
|
||||||
|
handle_slots_callback(&bot, dialogue, draft, &data, target).await
|
||||||
|
}
|
||||||
|
ListingField::StartTime if data.starts_with("start_time_") => {
|
||||||
|
handle_start_time_callback(&bot, dialogue, draft, &data, target).await
|
||||||
|
}
|
||||||
|
ListingField::Duration if data.starts_with("duration_") => {
|
||||||
|
handle_duration_callback(&bot, dialogue, draft, &data, target).await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Unknown callback data for field {field:?}: {data}");
|
||||||
|
dialogue.exit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle description skip callback
|
||||||
|
async fn handle_description_skip_callback(
|
||||||
|
bot: &Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
draft: ListingDraft,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let target = target.into();
|
||||||
|
transition_to_field(dialogue, ListingField::Price, draft).await?;
|
||||||
|
|
||||||
|
let response = format!(
|
||||||
|
"✅ Description skipped!\n\n{}",
|
||||||
|
get_step_message(ListingField::Price)
|
||||||
|
);
|
||||||
|
send_message(
|
||||||
|
bot,
|
||||||
|
target,
|
||||||
|
response,
|
||||||
|
get_keyboard_for_field(ListingField::Price),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle slots selection callback
|
||||||
|
async fn handle_slots_callback(
|
||||||
|
bot: &Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
mut draft: ListingDraft,
|
||||||
|
data: &str,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let target = target.into();
|
||||||
|
let button = SlotsKeyboardButtons::try_from(data)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Unknown SlotsKeyboardButtons data: {}", data))?;
|
||||||
|
let num_slots = match button {
|
||||||
|
SlotsKeyboardButtons::OneSlot => 1,
|
||||||
|
SlotsKeyboardButtons::TwoSlots => 2,
|
||||||
|
SlotsKeyboardButtons::FiveSlots => 5,
|
||||||
|
SlotsKeyboardButtons::TenSlots => 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
fields.slots_available = num_slots;
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
||||||
|
}
|
||||||
|
|
||||||
|
transition_to_field(dialogue, ListingField::StartTime, draft).await?;
|
||||||
|
let response = format!(
|
||||||
|
"✅ Available slots: <b>{num_slots}</b>\n\n{}",
|
||||||
|
get_step_message(ListingField::StartTime)
|
||||||
|
);
|
||||||
|
send_message(
|
||||||
|
bot,
|
||||||
|
target,
|
||||||
|
&response,
|
||||||
|
get_keyboard_for_field(ListingField::StartTime),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle start time selection callback
|
||||||
|
async fn handle_start_time_callback(
|
||||||
|
bot: &Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
mut draft: ListingDraft,
|
||||||
|
data: &str,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let target = target.into();
|
||||||
|
let button = StartTimeKeyboardButtons::try_from(data)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Unknown StartTimeKeyboardButtons data: {}", data))?;
|
||||||
|
let start_time = match button {
|
||||||
|
StartTimeKeyboardButtons::Now => ListingDuration::zero(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
fields.start_delay = start_time;
|
||||||
|
}
|
||||||
|
ListingDraftPersisted::Persisted(_) => {
|
||||||
|
anyhow::bail!("Cannot update start time for persisted listing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transition_to_field(dialogue, ListingField::Duration, draft).await?;
|
||||||
|
let response = format!(
|
||||||
|
"✅ Listing will start: <b>immediately</b>\n\n{}",
|
||||||
|
get_step_message(ListingField::Duration)
|
||||||
|
);
|
||||||
|
send_message(
|
||||||
|
bot,
|
||||||
|
target,
|
||||||
|
&response,
|
||||||
|
get_keyboard_for_field(ListingField::Duration),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle duration selection callback
|
||||||
|
async fn handle_duration_callback(
|
||||||
|
bot: &Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
mut draft: ListingDraft,
|
||||||
|
data: &str,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let target = target.into();
|
||||||
|
let button = DurationKeyboardButtons::try_from(data).unwrap();
|
||||||
|
let duration = ListingDuration::days(match button {
|
||||||
|
DurationKeyboardButtons::OneDay => 1,
|
||||||
|
DurationKeyboardButtons::ThreeDays => 3,
|
||||||
|
DurationKeyboardButtons::SevenDays => 7,
|
||||||
|
DurationKeyboardButtons::FourteenDays => 14,
|
||||||
|
});
|
||||||
|
|
||||||
|
match &mut draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
fields.end_delay = duration;
|
||||||
|
}
|
||||||
|
ListingDraftPersisted::Persisted(_) => {
|
||||||
|
anyhow::bail!("Cannot update duration for persisted listing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show_confirmation_screen(bot, target, &draft).await?;
|
||||||
|
dialogue
|
||||||
|
.update(NewListingState::ViewingDraft(draft))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the wizard and exit
|
||||||
|
pub async fn cancel_wizard(
|
||||||
|
bot: &Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let target = target.into();
|
||||||
|
info!("{target:?} cancelled new listing wizard");
|
||||||
|
dialogue.exit().await?;
|
||||||
|
send_message(bot, target, "❌ Listing creation cancelled.", None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
76
src/commands/new_listing/field_processing.rs
Normal file
76
src/commands/new_listing/field_processing.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
//! Field processing logic for the new listing wizard
|
||||||
|
//!
|
||||||
|
//! This module handles the core logic for processing and updating listing fields
|
||||||
|
//! during both initial creation and editing workflows.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands::new_listing::types::{ListingDraft, ListingDraftPersisted, ListingField},
|
||||||
|
db::listing::ListingFields,
|
||||||
|
HandlerResult, RootDialogue,
|
||||||
|
};
|
||||||
|
use crate::commands::new_listing::{types::NewListingState, validations::*};
|
||||||
|
|
||||||
|
/// Helper function to transition to next field
|
||||||
|
pub async fn transition_to_field(
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
field: ListingField,
|
||||||
|
draft: ListingDraft,
|
||||||
|
) -> HandlerResult {
|
||||||
|
dialogue
|
||||||
|
.update(NewListingState::AwaitingDraftField { field, draft })
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process field input and update the draft
|
||||||
|
pub fn process_field_update(
|
||||||
|
field: ListingField,
|
||||||
|
draft: &mut ListingDraft,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
match field {
|
||||||
|
ListingField::Title => {
|
||||||
|
draft.base.title = validate_title(text).map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
ListingField::Description => {
|
||||||
|
draft.base.description = Some(validate_description(text).map_err(|e| anyhow::anyhow!(e))?);
|
||||||
|
}
|
||||||
|
ListingField::Price => match &mut draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
fields.buy_now_price = validate_price(text).map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
||||||
|
},
|
||||||
|
ListingField::Slots => match &mut draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
fields.slots_available = validate_slots(text).map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
||||||
|
},
|
||||||
|
ListingField::StartTime => match &mut draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
fields.start_delay = validate_start_time(text).map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
||||||
|
},
|
||||||
|
ListingField::Duration => match &mut draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
fields.end_delay = validate_duration(text).map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next field in the wizard sequence
|
||||||
|
pub fn get_next_field(current_field: ListingField) -> Option<ListingField> {
|
||||||
|
match current_field {
|
||||||
|
ListingField::Title => Some(ListingField::Description),
|
||||||
|
ListingField::Description => Some(ListingField::Price),
|
||||||
|
ListingField::Price => Some(ListingField::Slots),
|
||||||
|
ListingField::Slots => Some(ListingField::StartTime),
|
||||||
|
ListingField::StartTime => Some(ListingField::Duration),
|
||||||
|
ListingField::Duration => None, // Final step
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::*;
|
use super::{callbacks::*, handlers::*, types::*};
|
||||||
use crate::{case, Command, DialogueRootState, Handler};
|
use crate::{case, Command, DialogueRootState, Handler};
|
||||||
use teloxide::{dptree, prelude::*, types::Update};
|
use teloxide::{dptree, prelude::*, types::Update};
|
||||||
|
|
||||||
|
|||||||
385
src/commands/new_listing/handlers.rs
Normal file
385
src/commands/new_listing/handlers.rs
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
//! Main handler functions for the new listing wizard
|
||||||
|
//!
|
||||||
|
//! This module contains the primary handler functions that process
|
||||||
|
//! user input and manage the listing creation workflow.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands::new_listing::{
|
||||||
|
callbacks::cancel_wizard,
|
||||||
|
field_processing::{get_next_field, process_field_update, transition_to_field},
|
||||||
|
keyboard::{
|
||||||
|
ConfirmationKeyboardButtons, DurationKeyboardButtons, FieldSelectionKeyboardButtons,
|
||||||
|
SlotsKeyboardButtons, StartTimeKeyboardButtons,
|
||||||
|
},
|
||||||
|
messages::{
|
||||||
|
get_edit_success_message, get_keyboard_for_field, get_step_message, get_success_message,
|
||||||
|
},
|
||||||
|
types::{ListingDraft, ListingDraftPersisted, ListingField, NewListingState},
|
||||||
|
ui::{display_listing_summary, show_confirmation_screen},
|
||||||
|
},
|
||||||
|
db::{
|
||||||
|
listing::{ListingFields, NewListing, PersistedListing},
|
||||||
|
ListingDAO, UserDAO,
|
||||||
|
},
|
||||||
|
message_utils::*,
|
||||||
|
DialogueRootState, HandlerResult, RootDialogue,
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use teloxide::{prelude::*, types::*, Bot};
|
||||||
|
|
||||||
|
/// Handle the /newlisting command - starts the dialogue
|
||||||
|
pub async fn handle_new_listing_command(
|
||||||
|
db_pool: SqlitePool,
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
msg: Message,
|
||||||
|
) -> HandlerResult {
|
||||||
|
info!(
|
||||||
|
"User {} started new fixed price listing wizard",
|
||||||
|
HandleAndId::from_chat(&msg.chat),
|
||||||
|
);
|
||||||
|
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
||||||
|
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
|
||||||
|
|
||||||
|
// Initialize the dialogue to Start state
|
||||||
|
dialogue
|
||||||
|
.update(NewListingState::AwaitingDraftField {
|
||||||
|
field: ListingField::Title,
|
||||||
|
draft: ListingDraft::new_for_seller(user.persisted.id),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response = format!(
|
||||||
|
"🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
|
||||||
|
Let's create your fixed price listing step by step!\n\n{}",
|
||||||
|
get_step_message(ListingField::Title)
|
||||||
|
);
|
||||||
|
|
||||||
|
send_message(
|
||||||
|
&bot,
|
||||||
|
msg.chat,
|
||||||
|
response,
|
||||||
|
get_keyboard_for_field(ListingField::Title),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle text input for any field during creation
|
||||||
|
pub async fn handle_awaiting_draft_field_input(
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
(field, mut draft): (ListingField, ListingDraft),
|
||||||
|
msg: Message,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let chat = msg.chat.clone();
|
||||||
|
let text = msg.text().unwrap_or("");
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"User {} entered input step: {:?}",
|
||||||
|
HandleAndId::from_chat(&chat),
|
||||||
|
field
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_cancel(text) {
|
||||||
|
return cancel_wizard(&bot, dialogue, chat).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the field update
|
||||||
|
process_field_update(field, &mut draft, text)?;
|
||||||
|
|
||||||
|
// Handle final step or transition to next
|
||||||
|
if let Some(next_field) = get_next_field(field) {
|
||||||
|
transition_to_field(dialogue, next_field, draft).await?;
|
||||||
|
let response = format!(
|
||||||
|
"{}\n\n{}",
|
||||||
|
get_success_message(field),
|
||||||
|
get_step_message(next_field)
|
||||||
|
);
|
||||||
|
send_message(&bot, chat, response, get_keyboard_for_field(next_field)).await
|
||||||
|
} else {
|
||||||
|
// Final step - go to confirmation
|
||||||
|
show_confirmation_screen(&bot, chat, &draft).await?;
|
||||||
|
dialogue
|
||||||
|
.update(NewListingState::ViewingDraft(draft))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle text input for field editing
|
||||||
|
pub async fn handle_editing_field_input(
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
(field, mut draft): (ListingField, ListingDraft),
|
||||||
|
msg: Message,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let chat = msg.chat.clone();
|
||||||
|
let text = msg.text().unwrap_or("").trim();
|
||||||
|
|
||||||
|
info!("User {chat:?} editing field {field:?}");
|
||||||
|
|
||||||
|
// Process the field update
|
||||||
|
process_field_update(field, &mut draft, text)?;
|
||||||
|
|
||||||
|
draft.has_changes = true;
|
||||||
|
enter_edit_listing_draft(
|
||||||
|
&bot,
|
||||||
|
chat,
|
||||||
|
draft,
|
||||||
|
dialogue,
|
||||||
|
Some(get_edit_success_message(field)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle viewing draft confirmation callbacks
|
||||||
|
pub async fn handle_viewing_draft_callback(
|
||||||
|
db_pool: SqlitePool,
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
draft: ListingDraft,
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||||
|
|
||||||
|
// Ensure the user exists before saving the listing
|
||||||
|
UserDAO::find_or_create_by_telegram_user(&db_pool, from.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
error!("Error finding or creating user: {e}");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let target = (from.clone(), message_id);
|
||||||
|
|
||||||
|
let button = ConfirmationKeyboardButtons::try_from(data.as_str())
|
||||||
|
.map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?;
|
||||||
|
|
||||||
|
match button {
|
||||||
|
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
|
||||||
|
info!("User {target:?} confirmed listing creation");
|
||||||
|
save_listing(db_pool, bot, target, draft).await?;
|
||||||
|
dialogue.exit().await?;
|
||||||
|
}
|
||||||
|
ConfirmationKeyboardButtons::Cancel => {
|
||||||
|
info!("User {target:?} cancelled listing update");
|
||||||
|
let response = "🗑️ <b>Changes Discarded</b>\n\n\
|
||||||
|
Your changes have been discarded and not saved.";
|
||||||
|
send_message(&bot, target, &response, None).await?;
|
||||||
|
dialogue.exit().await?;
|
||||||
|
}
|
||||||
|
ConfirmationKeyboardButtons::Discard => {
|
||||||
|
info!("User {target:?} discarded listing creation");
|
||||||
|
|
||||||
|
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
||||||
|
Your listing has been discarded and not created.\n\
|
||||||
|
You can start a new listing anytime with /newlisting.";
|
||||||
|
send_message(&bot, target, &response, None).await?;
|
||||||
|
dialogue.exit().await?;
|
||||||
|
}
|
||||||
|
ConfirmationKeyboardButtons::Edit => {
|
||||||
|
info!("User {target:?} chose to edit listing");
|
||||||
|
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle editing draft field selection callbacks
|
||||||
|
pub async fn handle_editing_draft_callback(
|
||||||
|
bot: Bot,
|
||||||
|
draft: ListingDraft,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||||
|
let target = (from, message_id);
|
||||||
|
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid field selection button: {}", e))?;
|
||||||
|
|
||||||
|
info!("User {target:?} in editing screen, showing field selection");
|
||||||
|
|
||||||
|
if button == FieldSelectionKeyboardButtons::Done {
|
||||||
|
show_confirmation_screen(&bot, target, &draft).await?;
|
||||||
|
dialogue
|
||||||
|
.update(DialogueRootState::NewListing(
|
||||||
|
NewListingState::ViewingDraft(draft),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let field = match button {
|
||||||
|
FieldSelectionKeyboardButtons::Title => ListingField::Title,
|
||||||
|
FieldSelectionKeyboardButtons::Description => ListingField::Description,
|
||||||
|
FieldSelectionKeyboardButtons::Price => ListingField::Price,
|
||||||
|
FieldSelectionKeyboardButtons::Slots => ListingField::Slots,
|
||||||
|
FieldSelectionKeyboardButtons::StartTime => ListingField::StartTime,
|
||||||
|
FieldSelectionKeyboardButtons::Duration => ListingField::Duration,
|
||||||
|
FieldSelectionKeyboardButtons::Done => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = get_current_field_value(&draft, field)?;
|
||||||
|
let keyboard = get_edit_keyboard_for_field(field);
|
||||||
|
|
||||||
|
dialogue
|
||||||
|
.update(DialogueRootState::NewListing(
|
||||||
|
NewListingState::EditingDraftField { field, draft },
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response = format!("Editing {field:?}\n\nPrevious value: {value}");
|
||||||
|
send_message(&bot, target, response, Some(keyboard)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle editing draft field callbacks (back button, etc.)
|
||||||
|
pub async fn handle_editing_draft_field_callback(
|
||||||
|
bot: Bot,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
(field, draft): (ListingField, ListingDraft),
|
||||||
|
callback_query: CallbackQuery,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
||||||
|
let target = (from, message_id);
|
||||||
|
info!("User {:?} editing field: {:?} -> {}", target, field, &data);
|
||||||
|
|
||||||
|
if data == "edit_back" {
|
||||||
|
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This callback handler typically receives button presses, not text input
|
||||||
|
// For now, just redirect back to edit screen since callback data isn't suitable for validation
|
||||||
|
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter the edit listing draft screen
|
||||||
|
pub async fn enter_edit_listing_draft(
|
||||||
|
bot: &Bot,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
draft: ListingDraft,
|
||||||
|
dialogue: RootDialogue,
|
||||||
|
flash_message: Option<&str>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
display_listing_summary(
|
||||||
|
bot,
|
||||||
|
target,
|
||||||
|
&draft,
|
||||||
|
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
||||||
|
flash_message,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
dialogue
|
||||||
|
.update(NewListingState::EditingDraft(draft))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the listing to the database
|
||||||
|
async fn save_listing(
|
||||||
|
db_pool: SqlitePool,
|
||||||
|
bot: Bot,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
draft: ListingDraft,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let (listing, success_message) = match draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
let listing = ListingDAO::insert_listing(
|
||||||
|
&db_pool,
|
||||||
|
NewListing {
|
||||||
|
persisted: fields,
|
||||||
|
base: draft.base,
|
||||||
|
fields: draft.fields,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
(listing, "Listing created!")
|
||||||
|
}
|
||||||
|
ListingDraftPersisted::Persisted(fields) => {
|
||||||
|
let listing = ListingDAO::update_listing(
|
||||||
|
&db_pool,
|
||||||
|
PersistedListing {
|
||||||
|
persisted: fields,
|
||||||
|
base: draft.base,
|
||||||
|
fields: draft.fields,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
(listing, "Listing updated!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = format!("✅ <b>{}</b>: {}", success_message, listing.base.title);
|
||||||
|
send_message(&bot, target, response, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of a field for display
|
||||||
|
fn get_current_field_value(
|
||||||
|
draft: &ListingDraft,
|
||||||
|
field: ListingField,
|
||||||
|
) -> Result<String, anyhow::Error> {
|
||||||
|
let value = match field {
|
||||||
|
ListingField::Title => draft.base.title.clone(),
|
||||||
|
ListingField::Description => draft
|
||||||
|
.base
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("(no description)")
|
||||||
|
.to_string(),
|
||||||
|
ListingField::Price => match &draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
format!("${}", fields.buy_now_price)
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
||||||
|
},
|
||||||
|
ListingField::Slots => match &draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
format!("{} slots", fields.slots_available)
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
||||||
|
},
|
||||||
|
ListingField::StartTime => match &draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
format!("{} hours", fields.start_delay)
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
||||||
|
},
|
||||||
|
ListingField::Duration => match &draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => fields.end_delay.to_string(),
|
||||||
|
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the edit keyboard for a field
|
||||||
|
fn get_edit_keyboard_for_field(field: ListingField) -> InlineKeyboardMarkup {
|
||||||
|
use crate::message_utils::create_single_button_keyboard;
|
||||||
|
|
||||||
|
let back_button = create_single_button_keyboard("🔙 Back", "edit_back");
|
||||||
|
|
||||||
|
match field {
|
||||||
|
ListingField::Description => {
|
||||||
|
let clear_button =
|
||||||
|
create_single_button_keyboard("🧹 Clear description", "edit_clear_description");
|
||||||
|
back_button.append_row(clear_button.inline_keyboard[0].clone())
|
||||||
|
}
|
||||||
|
ListingField::Slots => {
|
||||||
|
back_button.append_row(SlotsKeyboardButtons::to_keyboard().inline_keyboard[0].clone())
|
||||||
|
}
|
||||||
|
ListingField::StartTime => back_button
|
||||||
|
.append_row(StartTimeKeyboardButtons::to_keyboard().inline_keyboard[0].clone()),
|
||||||
|
ListingField::Duration => back_button
|
||||||
|
.append_row(DurationKeyboardButtons::to_keyboard().inline_keyboard[0].clone()),
|
||||||
|
_ => back_button,
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/commands/new_listing/messages.rs
Normal file
73
src/commands/new_listing/messages.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//! Message constants and generation functions for the new listing wizard
|
||||||
|
//!
|
||||||
|
//! This module centralizes all user-facing messages to eliminate duplication
|
||||||
|
//! and provide a single source of truth for wizard text.
|
||||||
|
|
||||||
|
use crate::commands::new_listing::keyboard::*;
|
||||||
|
use crate::commands::new_listing::types::ListingField;
|
||||||
|
use crate::message_utils::*;
|
||||||
|
use teloxide::types::InlineKeyboardMarkup;
|
||||||
|
|
||||||
|
// Step messages and responses - centralized to eliminate duplication
|
||||||
|
const STEP_MESSAGES: [&str; 6] = [
|
||||||
|
"<i>Step 1 of 6: Title</i>\nPlease enter a title for your listing (max 100 characters):",
|
||||||
|
"<i>Step 2 of 6: Description</i>\nPlease enter a description for your listing (optional).",
|
||||||
|
"<i>Step 3 of 6: Price</i>\nPlease enter the fixed price for your listing (e.g., 10.50, 25, 0.99):\n\n💡 <i>Price should be in USD</i>",
|
||||||
|
"<i>Step 4 of 6: Available Slots</i>\nHow many items are available for sale?\n\nChoose a common value below or enter a custom number (1-1000):",
|
||||||
|
"<i>Step 5 of 6: Start Time</i>\nWhen should your listing start?\n• Click 'Now' to start immediately\n• Enter duration to delay (e.g., '2 days' for 2 days from now, '1 hour' for 1 hour from now)\n• Maximum delay: 168 hours (7 days)",
|
||||||
|
"<i>Step 6 of 6: Duration</i>\nHow long should your listing run?\nEnter duration in hours (minimum 1 hour, maximum 720 hours / 30 days):",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUCCESS_MESSAGES: [&str; 6] = [
|
||||||
|
"✅ Title saved!",
|
||||||
|
"✅ Description saved!",
|
||||||
|
"✅ Price saved!",
|
||||||
|
"✅ Slots saved!",
|
||||||
|
"✅ Start time saved!",
|
||||||
|
"✅ Duration saved!",
|
||||||
|
];
|
||||||
|
|
||||||
|
const EDIT_SUCCESS_MESSAGES: [&str; 6] = [
|
||||||
|
"✅ Title updated!",
|
||||||
|
"✅ Description updated!",
|
||||||
|
"✅ Price updated!",
|
||||||
|
"✅ Slots updated!",
|
||||||
|
"✅ Start time updated!",
|
||||||
|
"✅ Duration updated!",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Get the step instruction message for a field
|
||||||
|
pub fn get_step_message(field: ListingField) -> &'static str {
|
||||||
|
STEP_MESSAGES[field as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the success message for completing a field
|
||||||
|
pub fn get_success_message(field: ListingField) -> &'static str {
|
||||||
|
SUCCESS_MESSAGES[field as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the success message for editing a field
|
||||||
|
pub fn get_edit_success_message(field: ListingField) -> &'static str {
|
||||||
|
EDIT_SUCCESS_MESSAGES[field as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the appropriate keyboard for a field
|
||||||
|
pub fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarkup> {
|
||||||
|
match field {
|
||||||
|
ListingField::Title => Some(create_cancel_keyboard()),
|
||||||
|
ListingField::Description => Some(create_skip_cancel_keyboard()),
|
||||||
|
ListingField::Price => None,
|
||||||
|
ListingField::Slots => Some(SlotsKeyboardButtons::to_keyboard()),
|
||||||
|
ListingField::StartTime => Some(StartTimeKeyboardButtons::to_keyboard()),
|
||||||
|
ListingField::Duration => Some(DurationKeyboardButtons::to_keyboard()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard creation helpers
|
||||||
|
fn create_cancel_keyboard() -> InlineKeyboardMarkup {
|
||||||
|
create_single_button_keyboard("Cancel", "cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup {
|
||||||
|
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
|
||||||
|
}
|
||||||
@@ -1,809 +1,28 @@
|
|||||||
|
//! New listing creation wizard module
|
||||||
|
//!
|
||||||
|
//! This module provides a complete wizard interface for creating new listings.
|
||||||
|
//! It's organized into logical submodules with clear responsibilities:
|
||||||
|
//!
|
||||||
|
//! - `handlers`: Main handler functions for teloxide
|
||||||
|
//! - `callbacks`: Callback query processing
|
||||||
|
//! - `field_processing`: Core field validation and update logic
|
||||||
|
//! - `messages`: Centralized message constants and generation
|
||||||
|
//! - `ui`: Display and summary functions
|
||||||
|
//! - `keyboard`: Button and keyboard definitions
|
||||||
|
//! - `types`: Type definitions and state management
|
||||||
|
//! - `validations`: Input validation functions
|
||||||
|
|
||||||
|
mod callbacks;
|
||||||
|
mod field_processing;
|
||||||
mod handler_factory;
|
mod handler_factory;
|
||||||
|
mod handlers;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
|
mod messages;
|
||||||
mod types;
|
mod types;
|
||||||
|
mod ui;
|
||||||
mod validations;
|
mod validations;
|
||||||
|
|
||||||
use crate::{
|
// Re-export the main handler for external use
|
||||||
db::{
|
|
||||||
listing::{ListingFields, NewListing, PersistedListing},
|
|
||||||
ListingDAO, ListingDuration, UserDAO,
|
|
||||||
},
|
|
||||||
message_utils::*,
|
|
||||||
DialogueRootState, HandlerResult, RootDialogue,
|
|
||||||
};
|
|
||||||
pub use handler_factory::new_listing_handler;
|
pub use handler_factory::new_listing_handler;
|
||||||
use keyboard::*;
|
pub use handlers::enter_edit_listing_draft;
|
||||||
use log::{error, info};
|
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use teloxide::{prelude::*, types::*, Bot};
|
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
use validations::*;
|
|
||||||
|
|
||||||
// Step messages and responses - centralized to eliminate duplication
|
|
||||||
const STEP_MESSAGES: [&str; 6] = [
|
|
||||||
"<i>Step 1 of 6: Title</i>\nPlease enter a title for your listing (max 100 characters):",
|
|
||||||
"<i>Step 2 of 6: Description</i>\nPlease enter a description for your listing (optional).",
|
|
||||||
"<i>Step 3 of 6: Price</i>\nPlease enter the fixed price for your listing (e.g., 10.50, 25, 0.99):\n\n💡 <i>Price should be in USD</i>",
|
|
||||||
"<i>Step 4 of 6: Available Slots</i>\nHow many items are available for sale?\n\nChoose a common value below or enter a custom number (1-1000):",
|
|
||||||
"<i>Step 5 of 6: Start Time</i>\nWhen should your listing start?\n• Click 'Now' to start immediately\n• Enter duration to delay (e.g., '2 days' for 2 days from now, '1 hour' for 1 hour from now)\n• Maximum delay: 168 hours (7 days)",
|
|
||||||
"<i>Step 6 of 6: Duration</i>\nHow long should your listing run?\nEnter duration in hours (minimum 1 hour, maximum 720 hours / 30 days):",
|
|
||||||
];
|
|
||||||
|
|
||||||
const SUCCESS_MESSAGES: [&str; 6] = [
|
|
||||||
"✅ Title saved!",
|
|
||||||
"✅ Description saved!",
|
|
||||||
"✅ Price saved!",
|
|
||||||
"✅ Slots saved!",
|
|
||||||
"✅ Start time saved!",
|
|
||||||
"✅ Duration saved!",
|
|
||||||
];
|
|
||||||
|
|
||||||
const EDIT_SUCCESS_MESSAGES: [&str; 6] = [
|
|
||||||
"✅ Title updated!",
|
|
||||||
"✅ Description updated!",
|
|
||||||
"✅ Price updated!",
|
|
||||||
"✅ Slots updated!",
|
|
||||||
"✅ Start time updated!",
|
|
||||||
"✅ Duration updated!",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn get_step_message(field: ListingField) -> &'static str {
|
|
||||||
STEP_MESSAGES[field as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_success_message(field: ListingField) -> &'static str {
|
|
||||||
SUCCESS_MESSAGES[field as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_edit_success_message(field: ListingField) -> &'static str {
|
|
||||||
EDIT_SUCCESS_MESSAGES[field as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_keyboard_for_field(field: ListingField) -> Option<InlineKeyboardMarkup> {
|
|
||||||
match field {
|
|
||||||
ListingField::Title => Some(create_cancel_keyboard()),
|
|
||||||
ListingField::Description => Some(create_skip_cancel_keyboard()),
|
|
||||||
ListingField::Price => None,
|
|
||||||
ListingField::Slots => Some(SlotsKeyboardButtons::to_keyboard()),
|
|
||||||
ListingField::StartTime => Some(StartTimeKeyboardButtons::to_keyboard()),
|
|
||||||
ListingField::Duration => Some(DurationKeyboardButtons::to_keyboard()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to transition to next field
|
|
||||||
async fn transition_to_field(
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
field: ListingField,
|
|
||||||
draft: ListingDraft,
|
|
||||||
) -> HandlerResult {
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::AwaitingDraftField { field, draft })
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_back_button_keyboard_with(other_buttons: InlineKeyboardMarkup) -> InlineKeyboardMarkup {
|
|
||||||
other_buttons.append_row([InlineKeyboardButton::callback("🔙 Back", "edit_back")])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_back_button_keyboard() -> InlineKeyboardMarkup {
|
|
||||||
create_single_button_keyboard("🔙 Back", "edit_back")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_back_button_keyboard_with_clear(field: &str) -> InlineKeyboardMarkup {
|
|
||||||
create_single_row_keyboard(&[
|
|
||||||
("🔙 Back", "edit_back"),
|
|
||||||
(&format!("🧹 Clear {field}"), &format!("edit_clear_{field}")),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cancel_keyboard() -> InlineKeyboardMarkup {
|
|
||||||
create_single_button_keyboard("Cancel", "cancel")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_skip_cancel_keyboard() -> InlineKeyboardMarkup {
|
|
||||||
create_multi_row_keyboard(&[&[("Skip", "skip"), ("Cancel", "cancel")]])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the /newlisting command - starts the dialogue by setting it to Start state
|
|
||||||
async fn handle_new_listing_command(
|
|
||||||
db_pool: SqlitePool,
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
msg: Message,
|
|
||||||
) -> HandlerResult {
|
|
||||||
info!(
|
|
||||||
"User {} started new fixed price listing wizard",
|
|
||||||
HandleAndId::from_chat(&msg.chat),
|
|
||||||
);
|
|
||||||
let user = msg.from.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
|
||||||
let user = UserDAO::find_or_create_by_telegram_user(&db_pool, user).await?;
|
|
||||||
|
|
||||||
// Initialize the dialogue to Start state
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::AwaitingDraftField {
|
|
||||||
field: ListingField::Title,
|
|
||||||
draft: ListingDraft::new_for_seller(user.persisted.id),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = format!(
|
|
||||||
"🛍️ <b>Creating New Fixed Price Listing</b>\n\n\
|
|
||||||
Let's create your fixed price listing step by step!\n\n{}",
|
|
||||||
get_step_message(ListingField::Title)
|
|
||||||
);
|
|
||||||
|
|
||||||
send_message(
|
|
||||||
&bot,
|
|
||||||
msg.chat,
|
|
||||||
response,
|
|
||||||
get_keyboard_for_field(ListingField::Title),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_awaiting_draft_field_input(
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
|
||||||
msg: Message,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let chat = msg.chat.clone();
|
|
||||||
let text = msg.text().unwrap_or("");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"User {} entered input step: {:?}",
|
|
||||||
HandleAndId::from_chat(&chat),
|
|
||||||
field
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_cancel(text) {
|
|
||||||
return cancel_wizard(&bot, dialogue, chat).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unified field processing with centralized messages
|
|
||||||
match field {
|
|
||||||
ListingField::Title => {
|
|
||||||
draft.base.title = validate_title(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
ListingField::Description => {
|
|
||||||
draft.base.description =
|
|
||||||
Some(validate_description(text).map_err(|e| anyhow::anyhow!(e))?);
|
|
||||||
}
|
|
||||||
ListingField::Price => match &mut draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
fields.buy_now_price = validate_price(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
|
||||||
},
|
|
||||||
ListingField::Slots => {
|
|
||||||
let slots = validate_slots(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
match &mut draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
fields.slots_available = slots;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListingField::StartTime => {
|
|
||||||
let duration = validate_start_time(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.start_delay = duration;
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(_) => {
|
|
||||||
anyhow::bail!("Cannot update start time for persisted listing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListingField::Duration => {
|
|
||||||
let duration = validate_duration(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.end_delay = duration;
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(_) => {
|
|
||||||
anyhow::bail!("Cannot update duration for persisted listing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Final step - go to confirmation
|
|
||||||
show_confirmation_screen(&bot, chat, &draft).await?;
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::ViewingDraft(draft))
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get next field and send response using centralized messages
|
|
||||||
let next_field = match field {
|
|
||||||
ListingField::Title => ListingField::Description,
|
|
||||||
ListingField::Description => ListingField::Price,
|
|
||||||
ListingField::Price => ListingField::Slots,
|
|
||||||
ListingField::Slots => ListingField::StartTime,
|
|
||||||
ListingField::StartTime => ListingField::Duration,
|
|
||||||
ListingField::Duration => unreachable!(), // Handled above
|
|
||||||
};
|
|
||||||
|
|
||||||
transition_to_field(dialogue, next_field, draft).await?;
|
|
||||||
let response = format!(
|
|
||||||
"{}\n\n{}",
|
|
||||||
get_success_message(field),
|
|
||||||
get_step_message(next_field)
|
|
||||||
);
|
|
||||||
send_message(&bot, chat, response, get_keyboard_for_field(next_field)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_description_callback(
|
|
||||||
bot: &Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
draft: ListingDraft,
|
|
||||||
data: &str,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let target = target.into();
|
|
||||||
match data {
|
|
||||||
"skip" => {
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::AwaitingDraftField {
|
|
||||||
field: ListingField::Price,
|
|
||||||
draft,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = format!(
|
|
||||||
"✅ Description skipped!\n\n{}",
|
|
||||||
get_step_message(ListingField::Price)
|
|
||||||
);
|
|
||||||
send_message(
|
|
||||||
&bot,
|
|
||||||
target,
|
|
||||||
response,
|
|
||||||
get_keyboard_for_field(ListingField::Price),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Unknown callback data: {data}");
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_awaiting_draft_field_callback(
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
(field, draft): (ListingField, ListingDraft),
|
|
||||||
callback_query: CallbackQuery,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
||||||
info!("User {from:?} selected callback: {data:?}");
|
|
||||||
let target = (from, message_id);
|
|
||||||
|
|
||||||
if data == "cancel" {
|
|
||||||
return cancel_wizard(&bot, dialogue, target).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
match field {
|
|
||||||
ListingField::Title => {
|
|
||||||
error!("Unknown callback data: {data}");
|
|
||||||
dialogue.exit().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ListingField::Description => {
|
|
||||||
handle_description_callback(&bot, dialogue, draft, data.as_str(), target).await
|
|
||||||
}
|
|
||||||
ListingField::Price => {
|
|
||||||
error!("Unknown callback data: {data}");
|
|
||||||
dialogue.exit().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ListingField::Slots => {
|
|
||||||
handle_slots_callback(&bot, dialogue, draft, data.as_str(), target).await
|
|
||||||
}
|
|
||||||
ListingField::StartTime => {
|
|
||||||
handle_start_time_callback(&bot, dialogue, draft, data.as_str(), target).await
|
|
||||||
}
|
|
||||||
ListingField::Duration => {
|
|
||||||
handle_duration_callback(&bot, dialogue, draft, data.as_str(), target).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_slots_callback(
|
|
||||||
bot: &Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
mut draft: ListingDraft,
|
|
||||||
data: &str,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let target = target.into();
|
|
||||||
let button = SlotsKeyboardButtons::try_from(data)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Unknown SlotsKeyboardButtons data: {}", data))?;
|
|
||||||
let num_slots = match button {
|
|
||||||
SlotsKeyboardButtons::OneSlot => 1,
|
|
||||||
SlotsKeyboardButtons::TwoSlots => 2,
|
|
||||||
SlotsKeyboardButtons::FiveSlots => 5,
|
|
||||||
SlotsKeyboardButtons::TenSlots => 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &mut draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
fields.slots_available = num_slots;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
||||||
}
|
|
||||||
|
|
||||||
transition_to_field(dialogue, ListingField::StartTime, draft).await?;
|
|
||||||
let response = format!(
|
|
||||||
"✅ Available slots: <b>{num_slots}</b>\n\n{}",
|
|
||||||
get_step_message(ListingField::StartTime)
|
|
||||||
);
|
|
||||||
send_message(
|
|
||||||
bot,
|
|
||||||
target,
|
|
||||||
&response,
|
|
||||||
get_keyboard_for_field(ListingField::StartTime),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_start_time_callback(
|
|
||||||
bot: &Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
mut draft: ListingDraft,
|
|
||||||
data: &str,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let target = target.into();
|
|
||||||
let button = StartTimeKeyboardButtons::try_from(data)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Unknown StartTimeKeyboardButtons data: {}", data))?;
|
|
||||||
let start_time = match button {
|
|
||||||
StartTimeKeyboardButtons::Now => ListingDuration::zero(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.start_delay = start_time;
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(_) => {
|
|
||||||
anyhow::bail!("Cannot update start time for persisted listing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transition_to_field(dialogue, ListingField::Duration, draft).await?;
|
|
||||||
let response = format!(
|
|
||||||
"✅ Listing will start: <b>immediately</b>\n\n{}",
|
|
||||||
get_step_message(ListingField::Duration)
|
|
||||||
);
|
|
||||||
send_message(
|
|
||||||
bot,
|
|
||||||
target,
|
|
||||||
&response,
|
|
||||||
get_keyboard_for_field(ListingField::Duration),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_viewing_draft_callback(
|
|
||||||
db_pool: SqlitePool,
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
draft: ListingDraft,
|
|
||||||
callback_query: CallbackQuery,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
||||||
|
|
||||||
// Ensure the user exists before saving the listing
|
|
||||||
UserDAO::find_or_create_by_telegram_user(&db_pool, from.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| {
|
|
||||||
error!("Error finding or creating user: {e}");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let target = (from.clone(), message_id);
|
|
||||||
|
|
||||||
let button = ConfirmationKeyboardButtons::try_from(data.as_str())
|
|
||||||
.map_err(|_| anyhow::anyhow!("Unknown ConfirmationKeyboardButtons data: {}", data))?;
|
|
||||||
|
|
||||||
match button {
|
|
||||||
ConfirmationKeyboardButtons::Create | ConfirmationKeyboardButtons::Save => {
|
|
||||||
info!("User {target:?} confirmed listing creation");
|
|
||||||
save_listing(db_pool, bot, target, draft).await?;
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
ConfirmationKeyboardButtons::Cancel => {
|
|
||||||
info!("User {target:?} cancelled listing update");
|
|
||||||
let response = "🗑️ <b>Changes Discarded</b>\n\n\
|
|
||||||
Your changes have been discarded and not saved.";
|
|
||||||
send_message(&bot, target, &response, None).await?;
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
ConfirmationKeyboardButtons::Discard => {
|
|
||||||
info!("User {target:?} discarded listing creation");
|
|
||||||
|
|
||||||
let response = "🗑️ <b>Listing Discarded</b>\n\n\
|
|
||||||
Your listing has been discarded and not created.\n\
|
|
||||||
You can start a new listing anytime with /newlisting.";
|
|
||||||
send_message(&bot, target, &response, None).await?;
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
ConfirmationKeyboardButtons::Edit => {
|
|
||||||
info!("User {target:?} chose to edit listing");
|
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_duration_callback(
|
|
||||||
bot: &Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
mut draft: ListingDraft,
|
|
||||||
data: &str,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let target = target.into();
|
|
||||||
let button = DurationKeyboardButtons::try_from(data).unwrap();
|
|
||||||
let duration = ListingDuration::days(match button {
|
|
||||||
DurationKeyboardButtons::OneDay => 1,
|
|
||||||
DurationKeyboardButtons::ThreeDays => 3,
|
|
||||||
DurationKeyboardButtons::SevenDays => 7,
|
|
||||||
DurationKeyboardButtons::FourteenDays => 14,
|
|
||||||
});
|
|
||||||
|
|
||||||
match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.end_delay = duration;
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(_) => {
|
|
||||||
anyhow::bail!("Cannot update duration for persisted listing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show_confirmation_screen(bot, target, &draft).await?;
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::ViewingDraft(draft))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn display_listing_summary(
|
|
||||||
bot: &Bot,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
draft: &ListingDraft,
|
|
||||||
keyboard: Option<InlineKeyboardMarkup>,
|
|
||||||
flash_message: Option<&str>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let mut response_lines = vec![];
|
|
||||||
|
|
||||||
if let Some(flash_message) = flash_message {
|
|
||||||
response_lines.push(flash_message.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let unsaved_changes = if draft.has_changes {
|
|
||||||
"<i>Unsaved changes</i>"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
response_lines.push(format!("📋 <b>Listing Summary</b> {unsaved_changes}"));
|
|
||||||
response_lines.push("".to_string());
|
|
||||||
response_lines.push(format!("<b>Title:</b> {}", draft.base.title));
|
|
||||||
response_lines.push(format!(
|
|
||||||
"📄 <b>Description:</b> {}",
|
|
||||||
draft
|
|
||||||
.base
|
|
||||||
.description
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("<i>No description</i>")
|
|
||||||
));
|
|
||||||
|
|
||||||
match &draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
response_lines.push(format!(
|
|
||||||
"💰 <b>Buy it Now Price:</b> ${}",
|
|
||||||
fields.buy_now_price
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match &draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
response_lines.push(format!("<b>Start delay:</b> {}", fields.start_delay));
|
|
||||||
response_lines.push(format!("<b>Duration:</b> {}", fields.end_delay));
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(fields) => {
|
|
||||||
response_lines.push(format!(
|
|
||||||
"<b>Starts on:</b> {}",
|
|
||||||
format_datetime(fields.start_at)
|
|
||||||
));
|
|
||||||
response_lines.push(format!(
|
|
||||||
"<b>Ends on:</b> {}",
|
|
||||||
format_datetime(fields.end_at)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response_lines.push("".to_string());
|
|
||||||
response_lines.push("Please review your listing and choose an action:".to_string());
|
|
||||||
|
|
||||||
send_message(&bot, target, response_lines.join("\n"), keyboard).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn enter_edit_listing_draft(
|
|
||||||
bot: &Bot,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
draft: ListingDraft,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
flash_message: Option<&str>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
display_listing_summary(
|
|
||||||
bot,
|
|
||||||
target,
|
|
||||||
&draft,
|
|
||||||
Some(FieldSelectionKeyboardButtons::to_keyboard()),
|
|
||||||
flash_message,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
dialogue
|
|
||||||
.update(NewListingState::EditingDraft(draft))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn show_confirmation_screen(
|
|
||||||
bot: &Bot,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
draft: &ListingDraft,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let keyboard = match draft.persisted {
|
|
||||||
ListingDraftPersisted::New(_) => InlineKeyboardMarkup::default().append_row([
|
|
||||||
ConfirmationKeyboardButtons::Create.to_button(),
|
|
||||||
ConfirmationKeyboardButtons::Edit.to_button(),
|
|
||||||
ConfirmationKeyboardButtons::Discard.to_button(),
|
|
||||||
]),
|
|
||||||
ListingDraftPersisted::Persisted(_) => InlineKeyboardMarkup::default().append_row([
|
|
||||||
ConfirmationKeyboardButtons::Save.to_button(),
|
|
||||||
ConfirmationKeyboardButtons::Edit.to_button(),
|
|
||||||
ConfirmationKeyboardButtons::Cancel.to_button(),
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
display_listing_summary(bot, target, draft, Some(keyboard), None).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_editing_field_input(
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
(field, mut draft): (ListingField, ListingDraft),
|
|
||||||
msg: Message,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let chat = msg.chat.clone();
|
|
||||||
let text = msg.text().unwrap_or("").trim();
|
|
||||||
|
|
||||||
info!("User {chat:?} editing field {field:?}");
|
|
||||||
|
|
||||||
// Update field based on type
|
|
||||||
match field {
|
|
||||||
ListingField::Title => {
|
|
||||||
draft.base.title = validate_title(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
ListingField::Description => {
|
|
||||||
draft.base.description =
|
|
||||||
Some(validate_description(text).map_err(|e| anyhow::anyhow!(e))?);
|
|
||||||
}
|
|
||||||
ListingField::Price => match &mut draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
fields.buy_now_price = validate_price(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
|
||||||
},
|
|
||||||
ListingField::Slots => match &mut draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
fields.slots_available = validate_slots(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
||||||
},
|
|
||||||
ListingField::StartTime => match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.start_delay = validate_start_time(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
|
||||||
},
|
|
||||||
ListingField::Duration => match &mut draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
fields.end_delay = validate_duration(text).map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
draft.has_changes = true;
|
|
||||||
enter_edit_listing_draft(
|
|
||||||
&bot,
|
|
||||||
chat,
|
|
||||||
draft,
|
|
||||||
dialogue,
|
|
||||||
Some(get_edit_success_message(field)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_editing_draft_callback(
|
|
||||||
bot: Bot,
|
|
||||||
draft: ListingDraft,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
callback_query: CallbackQuery,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
||||||
let target = (from, message_id);
|
|
||||||
let button = FieldSelectionKeyboardButtons::try_from(data.as_str())
|
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid field selection button: {}", e))?;
|
|
||||||
|
|
||||||
info!("User {target:?} in editing screen, showing field selection");
|
|
||||||
|
|
||||||
let (field, value, keyboard) = match button {
|
|
||||||
FieldSelectionKeyboardButtons::Title => (
|
|
||||||
ListingField::Title,
|
|
||||||
draft.base.title.clone(),
|
|
||||||
create_back_button_keyboard(),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::Description => (
|
|
||||||
ListingField::Description,
|
|
||||||
draft
|
|
||||||
.base
|
|
||||||
.description
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("(no description)")
|
|
||||||
.to_string(),
|
|
||||||
create_back_button_keyboard_with_clear("description"),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::Price => (
|
|
||||||
ListingField::Price,
|
|
||||||
match &draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
format!("${}", fields.buy_now_price)
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update price for non-fixed price listing"),
|
|
||||||
},
|
|
||||||
create_back_button_keyboard(),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::Slots => (
|
|
||||||
ListingField::Slots,
|
|
||||||
match &draft.fields {
|
|
||||||
ListingFields::FixedPriceListing(fields) => {
|
|
||||||
format!("{} slots", fields.slots_available)
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update slots for non-fixed price listing"),
|
|
||||||
},
|
|
||||||
create_back_button_keyboard_with(SlotsKeyboardButtons::to_keyboard()),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::StartTime => (
|
|
||||||
ListingField::StartTime,
|
|
||||||
match &draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
format!("{} hours", fields.start_delay)
|
|
||||||
}
|
|
||||||
_ => anyhow::bail!("Cannot update start time of an existing listing"),
|
|
||||||
},
|
|
||||||
create_back_button_keyboard_with(StartTimeKeyboardButtons::to_keyboard()),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::Duration => (
|
|
||||||
ListingField::Duration,
|
|
||||||
match &draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => fields.end_delay.to_string(),
|
|
||||||
_ => anyhow::bail!("Cannot update duration of an existing listing"),
|
|
||||||
},
|
|
||||||
create_back_button_keyboard_with(DurationKeyboardButtons::to_keyboard()),
|
|
||||||
),
|
|
||||||
FieldSelectionKeyboardButtons::Done => {
|
|
||||||
show_confirmation_screen(&bot, target, &draft).await?;
|
|
||||||
dialogue
|
|
||||||
.update(DialogueRootState::NewListing(
|
|
||||||
NewListingState::ViewingDraft(draft),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dialogue
|
|
||||||
.update(DialogueRootState::NewListing(
|
|
||||||
NewListingState::EditingDraftField { field, draft },
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// update the message to show the edit screen
|
|
||||||
let response = format!(
|
|
||||||
"Editing {field:?}\n\n\
|
|
||||||
Previous value: {value}\
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
send_message(&bot, target, response, Some(keyboard)).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_listing(
|
|
||||||
db_pool: SqlitePool,
|
|
||||||
bot: Bot,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
draft: ListingDraft,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let (listing, success_message) = match draft.persisted {
|
|
||||||
ListingDraftPersisted::New(fields) => {
|
|
||||||
let listing = ListingDAO::insert_listing(
|
|
||||||
&db_pool,
|
|
||||||
NewListing {
|
|
||||||
persisted: fields,
|
|
||||||
base: draft.base,
|
|
||||||
fields: draft.fields,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
(listing, "Listing created!")
|
|
||||||
}
|
|
||||||
ListingDraftPersisted::Persisted(fields) => {
|
|
||||||
let listing = ListingDAO::update_listing(
|
|
||||||
&db_pool,
|
|
||||||
PersistedListing {
|
|
||||||
persisted: fields,
|
|
||||||
base: draft.base,
|
|
||||||
fields: draft.fields,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
(listing, "Listing updated!")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = format!("✅ <b>{}</b>: {}", success_message, listing.base.title);
|
|
||||||
send_message(&bot, target, response, None).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_editing_draft_field_callback(
|
|
||||||
bot: Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
(field, draft): (ListingField, ListingDraft),
|
|
||||||
callback_query: CallbackQuery,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let (data, from, message_id) = extract_callback_data(&bot, callback_query).await?;
|
|
||||||
let target = (from, message_id);
|
|
||||||
info!("User {:?} editing field: {:?} -> {}", target, field, &data);
|
|
||||||
if data == "edit_back" {
|
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback handler typically receives button presses, not text input
|
|
||||||
// For now, just redirect back to edit screen since callback data isn't suitable for validation
|
|
||||||
enter_edit_listing_draft(&bot, target, draft, dialogue, None).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cancel_wizard(
|
|
||||||
bot: &Bot,
|
|
||||||
dialogue: RootDialogue,
|
|
||||||
target: impl Into<MessageTarget>,
|
|
||||||
) -> HandlerResult {
|
|
||||||
let target = target.into();
|
|
||||||
info!("{target:?} cancelled new listing wizard");
|
|
||||||
dialogue.exit().await?;
|
|
||||||
send_message(&bot, target, "❌ Listing creation cancelled.", None).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
102
src/commands/new_listing/ui.rs
Normal file
102
src/commands/new_listing/ui.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//! UI display functions for the new listing wizard
|
||||||
|
//!
|
||||||
|
//! This module handles all user interface display logic including
|
||||||
|
//! listing summaries, confirmation screens, and edit interfaces.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands::new_listing::types::{ListingDraft, ListingDraftPersisted},
|
||||||
|
db::listing::ListingFields,
|
||||||
|
message_utils::*,
|
||||||
|
HandlerResult,
|
||||||
|
};
|
||||||
|
use crate::commands::new_listing::keyboard::ConfirmationKeyboardButtons;
|
||||||
|
use teloxide::{types::InlineKeyboardMarkup, Bot};
|
||||||
|
|
||||||
|
/// Display the listing summary with optional flash message and keyboard
|
||||||
|
pub async fn display_listing_summary(
|
||||||
|
bot: &Bot,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
draft: &ListingDraft,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
flash_message: Option<&str>,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let mut response_lines = vec![];
|
||||||
|
|
||||||
|
if let Some(flash_message) = flash_message {
|
||||||
|
response_lines.push(flash_message.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsaved_changes = if draft.has_changes {
|
||||||
|
"<i>Unsaved changes</i>"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
response_lines.push(format!("📋 <b>Listing Summary</b> {unsaved_changes}"));
|
||||||
|
response_lines.push("".to_string());
|
||||||
|
response_lines.push(format!("<b>Title:</b> {}", draft.base.title));
|
||||||
|
response_lines.push(format!(
|
||||||
|
"📄 <b>Description:</b> {}",
|
||||||
|
draft
|
||||||
|
.base
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("<i>No description</i>")
|
||||||
|
));
|
||||||
|
|
||||||
|
match &draft.fields {
|
||||||
|
ListingFields::FixedPriceListing(fields) => {
|
||||||
|
response_lines.push(format!(
|
||||||
|
"💰 <b>Buy it Now Price:</b> ${}",
|
||||||
|
fields.buy_now_price
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match &draft.persisted {
|
||||||
|
ListingDraftPersisted::New(fields) => {
|
||||||
|
response_lines.push(format!("<b>Start delay:</b> {}", fields.start_delay));
|
||||||
|
response_lines.push(format!("<b>Duration:</b> {}", fields.end_delay));
|
||||||
|
}
|
||||||
|
ListingDraftPersisted::Persisted(fields) => {
|
||||||
|
response_lines.push(format!(
|
||||||
|
"<b>Starts on:</b> {}",
|
||||||
|
format_datetime(fields.start_at)
|
||||||
|
));
|
||||||
|
response_lines.push(format!(
|
||||||
|
"<b>Ends on:</b> {}",
|
||||||
|
format_datetime(fields.end_at)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response_lines.push("".to_string());
|
||||||
|
response_lines.push("Please review your listing and choose an action:".to_string());
|
||||||
|
|
||||||
|
send_message(&bot, target, response_lines.join("\n"), keyboard).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the final confirmation screen before creating/saving the listing
|
||||||
|
pub async fn show_confirmation_screen(
|
||||||
|
bot: &Bot,
|
||||||
|
target: impl Into<MessageTarget>,
|
||||||
|
draft: &ListingDraft,
|
||||||
|
) -> HandlerResult {
|
||||||
|
let keyboard = match draft.persisted {
|
||||||
|
ListingDraftPersisted::New(_) => InlineKeyboardMarkup::default().append_row([
|
||||||
|
ConfirmationKeyboardButtons::Create.to_button(),
|
||||||
|
ConfirmationKeyboardButtons::Edit.to_button(),
|
||||||
|
ConfirmationKeyboardButtons::Discard.to_button(),
|
||||||
|
]),
|
||||||
|
ListingDraftPersisted::Persisted(_) => InlineKeyboardMarkup::default().append_row([
|
||||||
|
ConfirmationKeyboardButtons::Save.to_button(),
|
||||||
|
ConfirmationKeyboardButtons::Edit.to_button(),
|
||||||
|
ConfirmationKeyboardButtons::Cancel.to_button(),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
display_listing_summary(bot, target, draft, Some(keyboard), None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user