~vpzom/hitide

531af1705cf1a80b29142ab0a9dd88218626f582 — Colin Reeder 3 months ago c8f98d7
Implement UI for voting in polls
6 files changed, 167 insertions(+), 4 deletions(-)

M res/lang/en.ftl
M src/components/mod.rs
M src/main.rs
M src/query_types.rs
M src/resp_types.rs
M src/routes/posts.rs
M res/lang/en.ftl => res/lang/en.ftl +1 -0
@@ 76,6 76,7 @@ notifications = Notifications
on = on
on_your_post = on your post
or_start = Or
poll_submit = Submit
password_prompt = Password:
posts_page_next = View More
post_approve = Approve

M src/components/mod.rs => src/components/mod.rs +38 -1
@@ 7,7 7,8 @@ use crate::lang;
use crate::resp_types::{
    Content, RespCommentInfo, RespFlagDetails, RespFlagInfo, RespMinimalAuthorInfo,
    RespMinimalCommentInfo, RespMinimalCommunityInfo, RespNotification, RespNotificationInfo,
    RespPostCommentInfo, RespPostInfo, RespPostListPost, RespThingComment, RespThingInfo,
    RespPollInfo, RespPostCommentInfo, RespPostInfo, RespPostListPost, RespThingComment,
    RespThingInfo,
};
use crate::util::{abbreviate_link, author_is_me};
use crate::PageBaseData;


@@ 674,6 675,42 @@ impl<'a> render::Render for NotificationItem<'a> {
    }
}

#[render::component]
pub fn PollView<'a>(poll: &'a RespPollInfo<'a>, action: String, lang: &'a crate::Translator) {
    render::rsx! {
        <div>
            <form method={"post"} action={action}>
                {
                    if poll.multiple {
                        poll.options.iter().map(|option| {
                            render::rsx! {
                                <div>
                                    <label>
                                        <input type={"checkbox"} name={option.id.to_string()} />{" "}
                                        {option.name.as_ref()}
                                    </label>
                                </div>
                            }
                        }).collect::<Vec<_>>()
                    } else {
                        poll.options.iter().map(|option| {
                            render::rsx! {
                                <div>
                                    <label>
                                        <input type={"radio"} name={"choice"} value={option.id.to_string()} />{" "}
                                        {option.name.as_ref()}
                                    </label>
                                </div>
                            }
                        }).collect::<Vec<_>>()
                    }
                }
                <input type={"submit"} value={lang.tr(&lang::POLL_SUBMIT)} />
            </form>
        </div>
    }
}

pub trait IconExt {
    fn img(&self) -> render::SimpleElement<()>;
}

M src/main.rs => src/main.rs +1 -0
@@ 66,6 66,7 @@ pub type RouteNode<P> = trout::Node<
pub enum Error {
    Internal(Box<dyn std::error::Error + Send>),
    InternalStr(String),
    InternalStrStatic(&'static str),
    UserError(hyper::Response<hyper::Body>),
    RoutingError(trout::RoutingFailure),
    RemoteError((hyper::StatusCode, String)),

M src/query_types.rs => src/query_types.rs +7 -0
@@ 20,3 20,10 @@ pub struct FlagListQuery {
    pub to_this_site_admin: Option<bool>,
    pub to_community: Option<i64>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PollVoteBody {
    Single { option: i64 },
    Multiple { options: Vec<i64> },
}

M src/resp_types.rs => src/resp_types.rs +14 -0
@@ 157,6 157,7 @@ pub struct RespPostInfo<'a> {
    pub score: i64,
    pub local: bool,
    pub your_vote: Option<Empty>,
    pub poll: Option<RespPollInfo<'a>>,
}

impl<'a> AsRef<RespSomePostInfo<'a>> for RespPostInfo<'a> {


@@ 166,6 167,19 @@ impl<'a> AsRef<RespSomePostInfo<'a>> for RespPostInfo<'a> {
}

#[derive(Deserialize, Debug)]
pub struct RespPollInfo<'a> {
    pub multiple: bool,
    pub options: Vec<RespPollOption<'a>>,
}

#[derive(Deserialize, Debug)]
pub struct RespPollOption<'a> {
    pub id: i64,
    pub name: Cow<'a, str>,
    pub votes: u32,
}

#[derive(Deserialize, Debug)]
pub struct RespMinimalCommunityInfo<'a> {
    pub id: i64,
    pub name: Cow<'a, str>,

M src/routes/posts.rs => src/routes/posts.rs +106 -3
@@ 4,9 4,11 @@ use super::{
    res_to_error, CookieMap,
};
use crate::components::{
    Comment, CommunityLink, ContentView, HTPage, IconExt, MaybeFillTextArea, TimeAgo, UserLink,
    Comment, CommunityLink, ContentView, HTPage, IconExt, MaybeFillTextArea, PollView, TimeAgo,
    UserLink,
};
use crate::lang;
use crate::query_types::PollVoteBody;
use crate::resp_types::{
    JustContentHTML, JustUser, RespCommunityInfoMaybeYour, RespList, RespPostCommentInfo,
    RespPostInfo,


@@ 35,6 37,7 @@ async fn page_post(
        None,
        None,
        None,
        None,
    )
    .await
}


@@ 45,7 48,8 @@ async fn page_post_inner(
    query: Option<&str>,
    cookies: &CookieMap<'_>,
    ctx: Arc<crate::RouteContext>,
    display_error: Option<String>,
    display_error_comments: Option<String>,
    display_error_poll: Option<String>,
    prev_values: Option<&HashMap<Cow<'_, str>, serde_json::Value>>,
    display_preview: Option<&str>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {


@@ 244,6 248,20 @@ async fn page_post_inner(
            <div class={"postContent"}>
                <ContentView src={&post} />
            </div>
            {
                display_error_poll.map(|msg| {
                    render::rsx! {
                        <div class={"errorBox"}>{msg}</div>
                    }
                })
            }
            {
                post.poll.as_ref().map(|poll| {
                    render::rsx! {
                        <PollView poll={poll} action={format!("/posts/{}/poll/submit", post.as_ref().as_ref().id)} lang={&lang} />
                    }
                })
            }
            <div class={"actionList"}>
                {
                    if author_is_me(&post.as_ref().author, &base_data.login) || (post.local && base_data.is_site_admin()) {


@@ 267,7 285,7 @@ async fn page_post_inner(
            <div>
                <h2>{lang.tr(&lang::comments())}</h2>
                {
                    display_error.map(|msg| {
                    display_error_comments.map(|msg| {
                        render::rsx! {
                            <div class={"errorBox"}>{msg}</div>
                        }


@@ 611,6 629,79 @@ async fn page_post_likes(
    }))
}

async fn handler_post_poll_submit(
    params: (i64,),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (post_id,) = params;

    let (req_parts, body) = req.into_parts();
    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let body = hyper::body::to_bytes(body).await?;
    let body: serde_json::map::Map<String, serde_json::Value> =
        serde_urlencoded::from_bytes(&body)?;

    let body = if let Some(choice) = body.get("choice") {
        let choice = choice
            .as_i64()
            .ok_or(crate::Error::InternalStrStatic("wrong type for choice"))?;

        PollVoteBody::Single { option: choice }
    } else {
        let choices: Vec<_> = body
            .keys()
            .filter_map(|key| {
                if let Ok(key) = key.parse::<i64>() {
                    Some(key)
                } else {
                    None
                }
            })
            .collect();

        PollVoteBody::Multiple { options: choices }
    };

    let api_res = res_to_error(
        ctx.http_client
            .request(for_client(
                hyper::Request::put(format!(
                    "{}/api/unstable/posts/{}/poll/your_vote",
                    ctx.backend_host, post_id
                ))
                .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,
    )
    .await;

    match api_res {
        Err(crate::Error::RemoteError((_, message))) => {
            page_post_inner(
                post_id,
                &req_parts.headers,
                None,
                &cookies,
                ctx,
                None,
                Some(message),
                None,
                None,
            )
            .await
        }
        Err(other) => Err(other),
        Ok(_) => Ok(hyper::Response::builder()
            .status(hyper::StatusCode::SEE_OTHER)
            .header(hyper::header::LOCATION, format!("/posts/{}", post_id))
            .body("Successfully voted.".into())?),
    }
}

async fn handler_post_unlike(
    params: (i64,),
    ctx: Arc<crate::RouteContext>,


@@ 758,6 849,7 @@ async fn handler_post_submit_reply(
                &cookies,
                ctx,
                Some(error),
                None,
                Some(&body_values),
                None,
            )


@@ 798,6 890,7 @@ async fn handler_post_submit_reply(
                    &cookies,
                    ctx,
                    None,
                    None,
                    Some(&body_values),
                    Some(&preview_res.content_html),
                )


@@ 811,6 904,7 @@ async fn handler_post_submit_reply(
                    &cookies,
                    ctx,
                    Some(message),
                    None,
                    Some(&body_values),
                    None,
                )


@@ 844,6 938,7 @@ async fn handler_post_submit_reply(
                &cookies,
                ctx,
                Some(message),
                None,
                Some(&body_values),
                None,
            )


@@ 890,6 985,14 @@ pub fn route_posts() -> crate::RouteNode<()> {
                crate::RouteNode::new().with_handler_async(hyper::Method::GET, page_post_likes),
            )
            .with_child(
                "poll",
                crate::RouteNode::new().with_child(
                    "submit",
                    crate::RouteNode::new()
                        .with_handler_async(hyper::Method::POST, handler_post_poll_submit),
                ),
            )
            .with_child(
                "unlike",
                crate::RouteNode::new()
                    .with_handler_async(hyper::Method::POST, handler_post_unlike),