open Brr open Note open Note_brr open Types open Client_types open Util let nick post = El.a ~at: [ class' "user-link" ; class' "post-author-nick" ; mk_at "data-user-id" post.poster_id ; Fmt.kstr href "/user/%s" post.poster_id ] [ el_txt post.poster_nick ] let date post = let print_date t = let t = Unix.localtime t in Fmt.str "%02d-%02d-%02d %02d:%02d" (1900 + t.tm_year) (1 + t.tm_mon) t.tm_mday t.tm_hour t.tm_min in El.span ~at:[ class' "post-date"; mk_at "data-time" (string_of_float post.date) ] [ el_txt (print_date post.date) ] (* TODO rm this since we can click the post_id? *) let link_to_post ?(is_vignette = false) post = let url = if is_vignette then Fmt.str "/thread/%d#%d" post.parent_t_id post.id else Fmt.str "#%d" post.id in El.a ~at:[ href url; title "Link to this post"; class' "post-link-to-self" ] [ el_txt "#" ] let post_id post = let el = let at = [ class' "post-id" ; title "Reply to this post" ; mk_at "data-id" (string_of_int post.id) ; Fmt.kstr href "#%d" post.id ] in El.a ~at [ el_txt (Fmt.str ">>%d" post.id) ] in hold_on el Ev.click (fun _ev -> Events.send_action (Post_form_change Form_open); Events.send_action (Post_form_change (Form_insert_quote post.id)) ); el let post_id_quote = let is_local_link t_s id = (* list of post currently on the page (consider thread page case only) *) let post_l = match (S.value t_s).Model.page with | Thread (Ready v) -> v.reply_l | _ -> [] in List.find_opt (fun p -> p.id = id) post_l |> Option.is_some in let hold_highlight_event el id = let mouseenter = Evr.on_el Ev.mouseenter Evr.unit el in let mouseleave = Evr.on_el Ev.mouseleave Evr.unit el in let focus = Evr.on_el Ev.focus Evr.unit el in let blur = Evr.on_el Ev.blur Evr.unit el in let off = E.select [ mouseleave; blur ] |> E.map (fun () -> None) in let on = E.select [ mouseenter; focus ] |> E.map (fun () -> Some (get_bounds el, id)) in let event = E.select [ off; on ] in hold_event_on el event (fun opt -> Events.send_action (Quickview_change opt) ); () in fun t_s id -> let at = [ class' "post-id-quote"; mk_at "data-id" (string_of_int id) ] in let txt = el_txt (Fmt.str ">>%d" id) in match is_local_link t_s id with | true -> (* simple #%d link *) let at = [ Fmt.kstr href "#%d" id ] @ at in let el = El.a ~at [ txt ] in hold_highlight_event el id; el | false -> (* remote link *) let at = [ class' "remote" ] @ at in let container = El.span [] in hold_highlight_event container id; let children = let open Page in S.map (fun t -> t.Model.quickview) t_s |> S.changes |> E.filter_map Fun.id |> E.map (fun (rect, v) -> let quickview_id = unwrap_post_id v in fun last_value -> match quickview_id = id with | true -> Some (rect, v) | false -> last_value ) |> S.accum None |> S.map (function | None -> [ El.button ~at [ txt ] ] | Some (_rect, v) -> ( match v with | Loading _ -> let at = [ class' "loading" ] @ at in [ El.button ~at [ txt ] ] | Not_found _ -> let at = [ class' "not-found" ] @ at in [ El.button ~at [ txt ] ] | Ready p -> let at = [ class' "ready" ; Fmt.kstr href "/thread/%d#%d" p.parent_t_id p.id ] @ at in [ El.a ~at [ txt ] ] ) ) in Elr.def_children container children; container let post_menu t_s post = let mk s = El.a ~at: [ Fmt.kstr href "/%s/%d" s post.id ; Fmt.kstr class' "%s-link" s ; mk_at "data-post-id" (string_of_int post.id) ] [ el_txt (String.capitalize_ascii s) ] in let mk_content () = let delete = mk "delete" in let report = mk "report" in let own_post = S.map Model.get_user t_s |> S.map (function | None -> false | Some u -> String.equal u.user_id post.poster_id ) in def_visibility `On own_post delete; [ delete; report ] in Html_util.mk_dropdown_menu ~class_prefix:"post-info" ~label:"" ~at_title:"Post menu" ~placeholder:false mk_content let backlinks t_s post = let l = List.map (post_id_quote t_s) post.backlinks in El.div ~at:[ class' "post-replies" ] l let image _t_s ?(is_vignette = false) post = match post.image_info with | None -> None | Some image -> ( let img_small = Html_util.mk_image ~is_small:true (Img_info.Post (post.id, image)) in let el = El.div ~at:[ class' "post-image-div" ] [ img_small ] in match is_vignette with | true -> Some el | false -> hold_on el Ev.click (fun _ev -> let v = Img_info.Post (post.id, image) in Events.send_action (Image_change (Some v)) ); Some el ) let comment = let open Comment in let insert_br_between_lines l = match l with | [] -> [] | hd :: tl -> List.rev @@ List.fold_left (fun acc x -> x :: [ El.br () ] :: acc) [ hd ] tl in let item t_s = function Txt s -> el_txt s | Id i -> post_id_quote t_s i in let items t_s l = List.map (item t_s) l in let line t_s = function | Line l -> items t_s l | Line_quote l -> [ El.span ~at:[ class' "line-quote" ] (el_txt ">" :: items t_s l) ] in fun t_s comment -> let content = List.map (line t_s) comment |> insert_br_between_lines |> List.flatten in El.div ~at:[ class' "post-comment" ] content let info t_s post = El.div ~at:[ class' "post-info" ] [ nick post ; date post ; post_id post ; link_to_post post ; post_menu t_s post ; backlinks t_s post ] let post_view t_s post = let info = info t_s post in let content = let comment = comment t_s post.comment in let l = match image t_s post with | None -> [ comment ] | Some image -> [ image; comment ] in El.div ~at:[ class' "post-content" ] l in let at = [ class' "post"; id (string_of_int post.id) ] in let el = El.div ~at [ info; content ] in let is_selected = S.map (fun t -> match t.Model.fragment with | Id v -> let id = Fragment.unwrap_id v in post.id = id | Empty | Top | Bottom -> false ) t_s in Elr.def_class (Jstr.v "selected") is_selected el; let is_highlighted = S.map (fun t -> t.Model.quickview) t_s |> S.map (function | None -> false | Some (_rect, v) -> Int.equal post.id (Page.unwrap_post_id v) ) in Elr.def_class (Jstr.v "highlighted") is_highlighted el; el module Quickview = struct open Model let quickview_class = "quickview-div" let to_px_jstr x = x |> int_of_float |> Fmt.str "%dpx" |> Jstr.of_string let is_in_viewport post = (* find highlighted post DOM element *) let id = string_of_int post.id in match find_html_el_by_id id with | None -> false | Some el -> (* check bounds *) let x, y, w, h = get_bounds el in let ( <= ) x y = Float.compare x y <= 0 in 0. <= x && 0. <= y && x +. w <= window_width () && y +. h <= window_height () let f t_s = let container = El.div ~at:[ class' quickview_class ] [] in let mk (id_x, id_y, id_w, id_h) post = if is_in_viewport post then [] else let quickview = post_view t_s post in (* ensure we don't have duplicate html id attribute *) El.set_at At.Name.id (Some (Jstr.v "quickview")) quickview; (* hack: insert hidden quickview into DOM so we can compute it's bounds we don't use the viewed post's already in DOM element for this - it might actually not be in DOM - it might have it's image opened and size changed *) El.set_inline_style El.Style.visibility (Jstr.v "hidden") quickview; El.set_children container [ quickview ]; (* compute quickview position *) let quickview_x = id_x +. id_w in let quickview_h = El.bound_h quickview in let quickview_y = id_y +. (0.5 *. id_h) -. (0.5 *. quickview_h) in let quickview_y = clamp ~min:0. ~max:(window_height () -. quickview_h) quickview_y in (* undo quickview DOM insertion *) El.set_inline_style El.Style.visibility (Jstr.v "visible") quickview; El.remove quickview; (* set quickview style *) El.set_inline_style El.Style.position (Jstr.v "fixed") quickview; El.set_inline_style El.Style.z_index (Jstr.v "99999") quickview; El.set_inline_style El.Style.left (to_px_jstr quickview_x) quickview; El.set_inline_style El.Style.top (to_px_jstr quickview_y) quickview; [ quickview ] in let children = S.map (fun t -> t.quickview) t_s |> S.map (function | None -> [] | Some (rect, v) -> ( match v with | Page.Loading _ | Not_found _ -> [] | Ready post -> mk rect post ) ) in Elr.def_children container children; container end