From c0cd2dfb29534e5e1880c9a8027392906480c5e5 Mon Sep 17 00:00:00 2001 From: rupansh Date: Fri, 23 Aug 2024 03:33:11 +0530 Subject: [PATCH] feat+fix: implement UI changes for profile view --- ssr/src/component/mod.rs | 2 +- ssr/src/component/no_more_posts.rs | 21 ---- ssr/src/component/profile_placeholders.rs | 70 +++++++++++++ ssr/src/error_template.rs | 2 + ssr/src/page/post_view/bet.rs | 11 +- ssr/src/page/profile/ic.rs | 6 +- ssr/src/page/profile/posts.rs | 6 +- ssr/src/page/profile/profile_iter.rs | 95 ++++++++++++----- ssr/src/page/profile/profile_post.rs | 122 +++++++++++++++------- ssr/src/page/profile/speculation.rs | 97 ++++++++++++----- 10 files changed, 307 insertions(+), 125 deletions(-) delete mode 100644 ssr/src/component/no_more_posts.rs create mode 100644 ssr/src/component/profile_placeholders.rs diff --git a/ssr/src/component/mod.rs b/ssr/src/component/mod.rs index c8b83a5b..055845f9 100644 --- a/ssr/src/component/mod.rs +++ b/ssr/src/component/mod.rs @@ -17,9 +17,9 @@ pub mod login_modal; pub mod modal; pub mod nav; pub mod nav_icons; -pub mod no_more_posts; pub mod option; pub mod overlay; +pub mod profile_placeholders; pub mod scrolling_post_view; pub mod social; pub mod spinner; diff --git a/ssr/src/component/no_more_posts.rs b/ssr/src/component/no_more_posts.rs deleted file mode 100644 index 1c255938..00000000 --- a/ssr/src/component/no_more_posts.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::utils::icon::icon_gen; - -icon_gen!( - NoMorePostsGraphic, - view_box = "0 0 186 174", - r###" - - - - - - - - - - - - - - "### -); diff --git a/ssr/src/component/profile_placeholders.rs b/ssr/src/component/profile_placeholders.rs new file mode 100644 index 00000000..baa8045c --- /dev/null +++ b/ssr/src/component/profile_placeholders.rs @@ -0,0 +1,70 @@ +use crate::utils::icon::icon_gen; + +icon_gen!( + NoMorePostsGraphic, + view_box = "0 0 186 174", + r###" + + + + + + + + + + + + + + "### +); + +icon_gen!( + NoMoreBetsGraphic, + view_box = "0 0 210 205", + r###" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"### +); diff --git a/ssr/src/error_template.rs b/ssr/src/error_template.rs index de165d73..c3b5cfea 100644 --- a/ssr/src/error_template.rs +++ b/ssr/src/error_template.rs @@ -54,6 +54,7 @@ pub fn ErrorTemplate( } view! { +

{if errors.len() > 1 { "Errors" } else { "Error" }}

+
} } diff --git a/ssr/src/page/post_view/bet.rs b/ssr/src/page/post_view/bet.rs index 37827d04..29c28df5 100644 --- a/ssr/src/page/post_view/bet.rs +++ b/ssr/src/page/post_view/bet.rs @@ -294,13 +294,16 @@ fn HNWonLost(participation: BetDetails) -> impl IntoView { fn BetTimer(participation: BetDetails, refetch_bet: Trigger) -> impl IntoView { let bet_duration = participation.bet_duration().as_secs(); // Add some overhead to avoid fetching bet status multiple times - let time_remaining = create_rw_signal(participation.time_remaining() + Duration::from_secs(30)); + // let time_remaining = create_rw_signal(participation.time_remaining() + Duration::from_secs(30)); + let time_remaining = create_rw_signal(participation.time_remaining()); _ = use_interval_fn( move || { time_remaining.try_update(|t| *t = t.saturating_sub(Duration::from_secs(1))); - if time_remaining.try_get_untracked() == Some(Duration::ZERO) { - refetch_bet.notify(); - } + _ = refetch_bet; + // TODO: notify once time_remaining is correct + // if time_remaining.try_get_untracked() == Some(Duration::ZERO) { + // refetch_bet.notify(); + // } }, 1000, ); diff --git a/ssr/src/page/profile/ic.rs b/ssr/src/page/profile/ic.rs index 02c1878f..c7d39506 100644 --- a/ssr/src/page/profile/ic.rs +++ b/ssr/src/page/profile/ic.rs @@ -13,8 +13,8 @@ use crate::{ pub fn ProfileStream( provider: Prov, children: EF, - #[prop(optional)] empty_graphic: Option, - #[prop(optional)] empty_text: String, + empty_graphic: icondata::Icon, + #[prop(into)] empty_text: String, ) -> impl IntoView where Prov: CursoredDataProvider + Clone + 'static, @@ -30,7 +30,7 @@ where empty_content=move || { view! {
- + {empty_text.clone()}
} diff --git a/ssr/src/page/profile/posts.rs b/ssr/src/page/profile/posts.rs index 4e242e23..0f9900fe 100644 --- a/ssr/src/page/profile/posts.rs +++ b/ssr/src/page/profile/posts.rs @@ -5,7 +5,7 @@ use candid::Principal; use crate::{ canister::utils::bg_url, - component::no_more_posts::NoMorePostsGraphic, + component::profile_placeholders::NoMorePostsGraphic, state::canisters::{auth_canisters_store, unauth_canisters}, utils::{ event_streaming::events::ProfileViewVideo, posts::PostDetails, profile::PostsProvider, @@ -35,7 +35,7 @@ fn Post(details: PostDetails, user_canister: Principal, _ref: NodeRef }; let handle_image_error = - move |_| image_error.update(|image_error| *image_error = !*image_error); + move |_| _ = image_error.try_update(|image_error| *image_error = !*image_error); let canisters = auth_canisters_store(); let post_details = details.clone(); @@ -100,7 +100,7 @@ pub fn ProfilePosts(user_canister: Principal) -> impl IntoView { { - cursor: FetchCursor, - canisters: &'a Canisters, - user_canister: Principal, +#[derive(Clone, Copy, PartialEq)] +pub struct FixedFetchCursor { + pub start: u64, + pub limit: u64, } -impl<'a, const AUTH: bool> ProfileVideoStream<'a, AUTH> { - pub fn new( - cursor: FetchCursor, - canisters: &'a Canisters, +impl FixedFetchCursor { + pub fn advance(&mut self) { + self.start += self.limit; + self.limit = LIMIT; + } +} + +pub struct PostsRes { + pub posts: Vec, + pub end: bool, +} + +pub(crate) trait ProfVideoStream: Sized { + async fn fetch_next_posts( + cursor: FixedFetchCursor, + canisters: &Canisters, user_canister: Principal, - ) -> Self { - Self { - cursor, - canisters, - user_canister, - } + ) -> Result; +} + +pub struct ProfileVideoBetsStream; + +impl ProfVideoStream<10> for ProfileVideoBetsStream { + async fn fetch_next_posts( + cursor: FixedFetchCursor<10>, + canisters: &Canisters, + user_canister: Principal, + ) -> Result { + let user = canisters.individual_user(user_canister).await?; + let bets = user + .get_hot_or_not_bets_placed_by_this_profile_with_pagination(cursor.start) + .await?; + let end = bets.len() < 10; + let posts = bets + .into_iter() + .map(|bet| get_post_uid(canisters, bet.canister_id, bet.post_id)) + .collect::>() + .filter_map(|res| async { res.transpose() }) + .try_collect::>() + .await?; + Ok(PostsRes { posts, end }) } +} + +pub struct ProfileVideoStream; - pub async fn fetch_next_profile_posts(&self) -> Result, PostViewError> { - let user = self.canisters.individual_user(self.user_canister).await?; +impl ProfVideoStream for ProfileVideoStream { + async fn fetch_next_posts( + cursor: FixedFetchCursor, + canisters: &Canisters, + user_canister: Principal, + ) -> Result { + let user = canisters.individual_user(user_canister).await?; let posts = user - .get_posts_of_this_user_profile_with_pagination_cursor( - self.cursor.start, - self.cursor.limit, - ) + .get_posts_of_this_user_profile_with_pagination_cursor(cursor.start, cursor.limit) .await?; match posts { - Result5::Ok(v) => Ok(v - .into_iter() - .map(|details| PostDetails::from_canister_post(AUTH, self.user_canister, details)) - .collect::>()), - Result5::Err(GetPostsOfUserProfileError::ReachedEndOfItemsList) => Ok(vec![]), + Result5::Ok(v) => { + let end = v.len() < LIMIT as usize; + let posts = v + .into_iter() + .map(|details| PostDetails::from_canister_post(AUTH, user_canister, details)) + .collect::>(); + Ok(PostsRes { posts, end }) + } + Result5::Err(GetPostsOfUserProfileError::ReachedEndOfItemsList) => Ok(PostsRes { + posts: vec![], + end: true, + }), _ => Err(PostViewError::Canister( "user canister refused to send posts".into(), )), diff --git a/ssr/src/page/profile/profile_post.rs b/ssr/src/page/profile/profile_post.rs index 4a7d6048..d191a03a 100644 --- a/ssr/src/page/profile/profile_post.rs +++ b/ssr/src/page/profile/profile_post.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use candid::Principal; use leptos::*; use leptos_router::*; @@ -7,28 +9,25 @@ use crate::{ component::{ back_btn::BackButton, scrolling_post_view::ScrollingPostView, spinner::FullScreenSpinner, }, - page::profile::ProfilePostsContext, + page::profile::{profile_iter::FixedFetchCursor, ProfilePostsContext}, state::canisters::{auth_canisters_store, unauth_canisters}, try_or_redirect, - utils::{ - posts::{get_post_uid, FetchCursor}, - route::failure_redirect, - }, + utils::{posts::get_post_uid, route::failure_redirect}, }; -use super::overlay::YourProfileOverlay; -use super::profile_iter::ProfileVideoStream; +use super::{ + overlay::YourProfileOverlay, + profile_iter::{ProfVideoStream, ProfileVideoStream}, +}; use crate::utils::posts::PostDetails; -#[derive(Params, PartialEq)] -struct ProfileVideoParams { - canister_id: String, - post_id: u64, -} - #[component] -pub fn ProfilePostWithUpdates(initial_post: PostDetails) -> impl IntoView { +fn ProfilePostWithUpdates>( + initial_post: PostDetails, + user_canister: Principal, + #[prop(optional)] _stream_phantom: PhantomData, +) -> impl IntoView { let ProfilePostsContext { video_queue, start_index, @@ -36,7 +35,7 @@ pub fn ProfilePostWithUpdates(initial_post: PostDetails) -> impl IntoView { queue_end, } = expect_context(); let recovering_state = create_rw_signal(true); - let fetch_cursor = create_rw_signal(FetchCursor { + let fetch_cursor = create_rw_signal(FixedFetchCursor:: { start: start_index.get_untracked() as u64, limit: 10, }); @@ -52,27 +51,23 @@ pub fn ProfilePostWithUpdates(initial_post: PostDetails) -> impl IntoView { video_queue.update_untracked(|vq| { vq.push(initial_post.clone()); }); - queue_end.update(|end| { - *end = true; - }) + queue_end.set(true) } let next_videos = create_action(move |_| async move { - let user_canister = initial_post.canister_id; let cursor = fetch_cursor.get_untracked(); let posts_res = if let Some(canisters) = auth_canister.get_untracked() { - let profile_posts = ProfileVideoStream::new(cursor, &canisters, user_canister); - profile_posts.fetch_next_profile_posts().await + VidStream::fetch_next_posts(cursor, &canisters, user_canister).await } else { - let unauth_canister = unauth_canisters(); - let profile_posts = ProfileVideoStream::new(cursor, &unauth_canister, user_canister); - profile_posts.fetch_next_profile_posts().await + let canisters = unauth_canisters(); + VidStream::fetch_next_posts(cursor, &canisters, user_canister).await }; - let posts = try_or_redirect!(posts_res); + let res = try_or_redirect!(posts_res); - posts.into_iter().for_each(|p| { + queue_end.set(res.end); + res.posts.into_iter().for_each(|p| { video_queue.try_update(|q| { q.push(p); }); @@ -130,18 +125,10 @@ pub fn ProfilePostWithUpdates(initial_post: PostDetails) -> impl IntoView { } #[component] -pub fn ProfilePost() -> impl IntoView { - let params = use_params::(); - - let canister_and_post = move || { - params.with_untracked(|p| { - let p = p.as_ref().ok()?; - let canister_id = Principal::from_text(&p.canister_id).ok()?; - - Some((canister_id, p.post_id)) - }) - }; - +fn ProfilePostBase IV + Clone + 'static>( + #[prop(into)] canister_and_post: Signal>, + children: C, +) -> impl IntoView { let ProfilePostsContext { video_queue, current_index, @@ -177,6 +164,7 @@ pub fn ProfilePost() -> impl IntoView { } } }); + let children_s = store_value(children); view! { @@ -190,7 +178,7 @@ pub fn ProfilePost() -> impl IntoView {
- + {(children_s.get_value())(pd)} }, ) }) @@ -199,3 +187,59 @@ pub fn ProfilePost() -> impl IntoView {
} } + +#[derive(Params, PartialEq)] +struct ProfileVideoParams { + canister_id: Principal, + post_id: u64, +} + +const PROFILE_POST_LIMIT: u64 = 25; +type DefProfileVidStream = ProfileVideoStream; + +#[component] +pub fn ProfilePost() -> impl IntoView { + let params = use_params::(); + + let canister_and_post = Signal::derive(move || { + params.with_untracked(|p| { + let p = p.as_ref().ok()?; + Some((p.canister_id, p.post_id)) + }) + }); + + view! { + + user_canister=pd.canister_id initial_post=pd/> + + } +} + +// TODO: handle custom context management for bets +// #[derive(Params, PartialEq)] +// struct ProfileBetsParams { +// bet_canister: Principal, +// post_canister: Principal, +// post_id: u64, +// } + +// const PROFILE_POST_BET_LIMIT: u64 = 10; + +// #[component] +// pub fn ProfilePostBets() -> impl IntoView { +// let params = use_params::(); + +// let user_canister = params.with_untracked(|p| p.as_ref().map(|p| p.bet_canister).unwrap_or(Principal::anonymous())); +// let canister_and_post = Signal::derive(move || { +// params.with_untracked(|p| { +// let p = p.as_ref().ok()?; +// Some((p.post_canister, p.post_id)) +// }) +// }); + +// view! { +// +// user_canister initial_post=pd/> +// +// } +// } diff --git a/ssr/src/page/profile/speculation.rs b/ssr/src/page/profile/speculation.rs index bfbbc367..e4deefb6 100644 --- a/ssr/src/page/profile/speculation.rs +++ b/ssr/src/page/profile/speculation.rs @@ -1,14 +1,18 @@ use candid::Principal; use leptos::*; use leptos_icons::*; +use leptos_use::use_interval_fn; +use web_time::Duration; use super::ic::ProfileStream; use crate::{ canister::utils::bg_url, + component::profile_placeholders::NoMoreBetsGraphic, state::canisters::unauth_canisters, utils::{ posts::PostDetails, profile::{BetDetails, BetOutcome, BetsProvider, ProfileDetails}, + timestamp::to_hh_mm_ss, }, }; @@ -24,12 +28,14 @@ pub fn ExternalUser(user: Option) -> impl IntoView { .unwrap_or_default(); view! { -
- -
+
+
+ +
+
{name}
@@ -66,43 +72,74 @@ pub fn FallbackUser() -> impl IntoView { } } +#[component] +fn BetTimer(details: BetDetails) -> impl IntoView { + let bet_duration = details.bet_duration().as_secs(); + let time_remaining = create_rw_signal(details.time_remaining()); + _ = use_interval_fn( + move || { + time_remaining.try_update(|t| *t = t.saturating_sub(Duration::from_secs(1))); + }, + 1000, + ); + + let percentage = create_memo(move |_| { + let remaining_secs = time_remaining().as_secs(); + 100 - ((remaining_secs * 100) / bet_duration).min(100) + }); + let gradient = move || { + let perc = percentage(); + format!("background: linear-gradient(to right, rgb(var(--color-primary-600)) {perc}%, #00000020 0 {}%);", 100 - perc) + }; + + view! { +
+
+ + {move || to_hh_mm_ss(time_remaining())} +
+
+ } +} + #[component] pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoView { + // TODO: enable scrolling videos for bets + let profile_post_url = format!("/hot-or-not/{}/{}", details.canister_id, details.post_id); let (bet_res, amt, icon) = match details.outcome { BetOutcome::Won(amt) => ( - "RECEIVED", + "YOU RECEIVED", amt, view! { -
- Won +
+ + You Won
- }, + }.into_view(), ), BetOutcome::Draw(amt) => ( - "RECEIVED", + "YOU RECEIVED", amt, view! { -
+
Draw
- }, + }.into_view(), ), BetOutcome::Lost => ( - "LOST", + "YOUR BET", details.bet_amount, view! { -
- Lost +
+ You Lost
- }, + }.into_view(), ), BetOutcome::AwaitingResult => ( - "VOTED", + "YOUR BET", details.bet_amount, view! { -
- -
+ }, ), }; @@ -130,8 +167,8 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi ); view! { -
- } } @@ -173,7 +212,9 @@ pub fn ProfileSpeculations(user_canister: Principal) -> impl IntoView { view! { } } />