tooty/src/View.elm

802 lines
27 KiB
Elm
Raw Normal View History

2017-04-20 07:46:18 +00:00
module View exposing (view)
2017-04-21 12:03:39 +00:00
import Dict
2017-04-20 07:46:18 +00:00
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
2017-04-24 19:21:43 +00:00
import List.Extra exposing (elemIndex, getAt)
2017-04-27 14:34:27 +00:00
import Mastodon.Helper
import Mastodon.Model
import Model exposing (..)
import ViewHelper exposing (..)
2017-04-25 10:15:45 +00:00
import Date
2017-04-25 10:18:24 +00:00
import Date.Extra.Config.Config_en_au as DateEn
import Date.Extra.Format as DateFormat
2017-04-20 07:46:18 +00:00
2017-04-21 12:03:39 +00:00
visibilities : Dict.Dict String String
visibilities =
Dict.fromList
[ ( "public", "post to public timelines" )
, ( "unlisted", "do not show in public timelines" )
, ( "private", "post to followers only" )
, ( "direct", "post to mentioned users only" )
]
closeablePanelheading : String -> String -> Msg -> Html Msg
closeablePanelheading iconName label onClose =
div [ class "panel-heading" ]
[ div [ class "row" ]
[ div [ class "col-xs-9 heading" ] [ icon iconName, text label ]
, div [ class "col-xs-3 text-right" ]
[ a
[ href ""
, onClickWithPreventAndStop onClose
]
[ icon "remove" ]
]
]
]
2017-04-20 07:46:18 +00:00
errorView : String -> Html Msg
errorView error =
div [ class "alert alert-danger" ] [ text error ]
errorsListView : Model -> Html Msg
errorsListView model =
case model.errors of
[] ->
text ""
errors ->
div [] <| List.map errorView model.errors
justifiedButtonGroup : List (Html Msg) -> Html Msg
justifiedButtonGroup buttons =
div [ class "btn-group btn-group-justified" ] <|
List.map (\b -> div [ class "btn-group" ] [ b ]) buttons
icon : String -> Html Msg
icon name =
i [ class <| "glyphicon glyphicon-" ++ name ] []
2017-04-27 14:34:27 +00:00
accountLink : Mastodon.Model.Account -> Html Msg
accountLink account =
a
[ href account.url
, onClickWithPreventAndStop (LoadAccount account.id)
]
[ text <| "@" ++ account.username ]
2017-04-27 14:34:27 +00:00
accountAvatarLink : Mastodon.Model.Account -> Html Msg
2017-04-23 19:49:04 +00:00
accountAvatarLink account =
a
[ href account.url
, onClickWithPreventAndStop (LoadAccount account.id)
2017-04-23 19:49:04 +00:00
, title <| "@" ++ account.username
]
[ img [ class "avatar", src account.avatar ] [] ]
2017-04-23 19:49:04 +00:00
2017-04-29 07:20:26 +00:00
attachmentPreview :
String
-> Maybe Bool
-> List Mastodon.Model.Attachment
-> Mastodon.Model.Attachment
-> Html Msg
attachmentPreview context sensitive attachments ({ url, preview_url } as attachment) =
let
nsfw =
case sensitive of
Just sensitive ->
sensitive
Nothing ->
False
attId =
"att" ++ (toString attachment.id) ++ context
media =
a
[ class "attachment-image"
, href url
, onClickWithPreventAndStop <|
2017-04-24 19:21:43 +00:00
ViewerEvent (OpenViewer attachments attachment)
, style
[ ( "background"
, "url(" ++ preview_url ++ ") center center / cover no-repeat"
)
]
]
[]
in
li [ class "attachment-entry" ] <|
if nsfw then
[ input [ type_ "radio", id attId ] []
, label [ for attId ]
[ text "Sensitive content"
, br [] []
, br [] []
, text "click to show image"
]
, media
]
else
[ media ]
2017-04-27 14:34:27 +00:00
attachmentListView : String -> Mastodon.Model.Status -> Html Msg
attachmentListView context { media_attachments, sensitive } =
case media_attachments of
[] ->
text ""
attachments ->
2017-04-24 19:21:43 +00:00
ul [ class "attachments" ] <|
List.map (attachmentPreview context sensitive attachments) attachments
2017-04-27 14:34:27 +00:00
statusContentView : String -> Mastodon.Model.Status -> Html Msg
statusContentView context status =
case status.spoiler_text of
"" ->
div [ class "status-text", onClickWithStop <| OpenThread status ]
[ div [] <| formatContent status.content status.mentions
, attachmentListView context status
]
spoiler ->
-- Note: Spoilers are dealt with using pure CSS.
let
statusId =
"spoiler" ++ (toString status.id) ++ context
in
div [ class "status-text spoiled" ]
[ div [ class "spoiler" ] [ text status.spoiler_text ]
, input [ type_ "checkbox", id statusId, class "spoiler-toggler" ] []
, label [ for statusId ] [ text "Reveal content" ]
, div [ class "spoiled-content" ]
[ div [] <| formatContent status.content status.mentions
, attachmentListView context status
]
]
2017-04-27 14:34:27 +00:00
statusView : String -> Mastodon.Model.Status -> Html Msg
statusView context ({ account, content, media_attachments, reblog, mentions } as status) =
let
accountLinkAttributes =
[ href account.url
2017-04-27 14:34:27 +00:00
-- When clicking on a status, we should not let the browser
-- redirect to a new page. That's why we're preventing the default
-- behavior here
, onClickWithPreventAndStop (LoadAccount account.id)
]
in
case reblog of
2017-04-27 14:34:27 +00:00
Just (Mastodon.Model.Reblog reblog) ->
div [ class "reblog" ]
2017-04-23 19:49:04 +00:00
[ p [ class "status-info" ]
[ icon "fire"
, a (accountLinkAttributes ++ [ class "reblogger" ])
2017-04-23 19:49:04 +00:00
[ text <| " @" ++ account.username ]
, text " boosted"
]
, statusView context reblog
2017-04-20 07:46:18 +00:00
]
Nothing ->
div [ class "status" ]
[ accountAvatarLink account
, div [ class "username" ]
[ a accountLinkAttributes
[ text account.display_name
, span [ class "acct" ] [ text <| " @" ++ account.username ]
]
]
, statusContentView context status
2017-04-20 13:43:15 +00:00
]
2017-04-29 07:20:26 +00:00
accountTimelineView :
Mastodon.Model.Account
-> List Mastodon.Model.Status
-> String
-> String
-> Html Msg
accountTimelineView account statuses label iconName =
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
[ closeablePanelheading iconName label ClearOpenedAccount
, div [ class "timeline" ]
2017-04-29 07:20:26 +00:00
[ div
[ class "account-detail"
, style [ ( "background-image", "url('" ++ account.header ++ "')" ) ]
]
[ div [ class "opacity-layer" ]
[ img [ src account.avatar ] []
, span [ class "account-display-name" ] [ text account.display_name ]
, span [ class "account-username" ] [ text ("@" ++ account.username) ]
, span [ class "account-note" ] (formatContent account.note [])
]
]
, div [ class "row account-infos" ]
[ div [ class "col-md-4" ]
[ text "Statuses"
, br [] []
, text <| toString account.statuses_count
]
, div [ class "col-md-4" ]
[ text "Following"
, br [] []
, text <| toString account.following_count
]
, div [ class "col-md-4" ]
[ text "Followers"
, br [] []
, text <| toString account.followers_count
]
]
, ul [ class "list-group" ] <|
List.map
(\s ->
li [ class "list-group-item status" ]
[ statusView "account" s ]
)
statuses
]
]
]
2017-04-20 07:46:18 +00:00
2017-04-29 07:20:26 +00:00
statusActionsView : Mastodon.Model.Status -> Mastodon.Model.Account -> Html Msg
statusActionsView status currentUser =
let
sourceStatus =
2017-04-27 14:34:27 +00:00
Mastodon.Helper.extractReblog status
baseBtnClasses =
"btn btn-sm btn-default"
( reblogClasses, reblogEvent ) =
case status.reblogged of
Just True ->
( baseBtnClasses ++ " reblogged", Unreblog sourceStatus.id )
_ ->
( baseBtnClasses, Reblog sourceStatus.id )
( favClasses, favEvent ) =
case status.favourited of
Just True ->
( baseBtnClasses ++ " favourited", RemoveFavorite sourceStatus.id )
_ ->
( baseBtnClasses, AddFavorite sourceStatus.id )
2017-04-25 10:15:45 +00:00
statusDate =
2017-04-25 10:15:45 +00:00
Date.fromString status.created_at
|> Result.withDefault (Date.fromTime 0)
formatDate =
text <| DateFormat.format DateEn.config "%m/%d/%Y %H:%M" statusDate
in
div [ class "btn-group actions" ]
[ a
[ class baseBtnClasses
, onClickWithPreventAndStop <|
DraftEvent (UpdateReplyTo status)
]
[ icon "share-alt" ]
, a
[ class reblogClasses
, onClickWithPreventAndStop reblogEvent
]
[ icon "fire", text (toString sourceStatus.reblogs_count) ]
, a
[ class favClasses
, onClickWithPreventAndStop favEvent
]
[ icon "star", text (toString sourceStatus.favourites_count) ]
, if Mastodon.Helper.sameAccount sourceStatus.account currentUser then
2017-04-29 07:20:26 +00:00
a
[ class <| baseBtnClasses ++ " btn-delete"
, href ""
, onClickWithPreventAndStop <| DeleteStatus sourceStatus.id
2017-04-29 07:20:26 +00:00
]
[ icon "trash" ]
else
text ""
, a
2017-04-29 07:20:26 +00:00
[ class baseBtnClasses, href status.url, target "_blank" ]
[ icon "time", formatDate ]
]
2017-04-29 07:20:26 +00:00
statusEntryView : String -> String -> Mastodon.Model.Account -> Mastodon.Model.Status -> Html Msg
statusEntryView context className currentUser status =
let
nsfwClass =
case status.sensitive of
Just True ->
"nsfw"
_ ->
""
in
li [ class <| "list-group-item " ++ className ++ " " ++ nsfwClass ]
[ statusView context status
2017-04-29 07:20:26 +00:00
, statusActionsView status currentUser
]
2017-04-29 07:20:26 +00:00
timelineView :
String
-> String
-> String
-> Mastodon.Model.Account
-> List Mastodon.Model.Status
-> Html Msg
timelineView label iconName context currentUser statuses =
div [ class "col-md-3 column" ]
2017-04-20 07:46:18 +00:00
[ div [ class "panel panel-default" ]
[ a
[ href "", onClickWithPreventAndStop <| ScrollColumn context ]
[ div [ class "panel-heading" ] [ icon iconName, text label ] ]
, ul [ id context, class "list-group timeline" ] <|
2017-04-29 07:20:26 +00:00
List.map (statusEntryView context "" currentUser) statuses
2017-04-20 07:46:18 +00:00
]
]
2017-04-27 14:34:27 +00:00
notificationHeading : List Mastodon.Model.Account -> String -> String -> Html Msg
2017-04-23 19:49:04 +00:00
notificationHeading accounts str iconType =
div [ class "status-info" ]
[ div [ class "avatars" ] <| List.map accountAvatarLink accounts
, p [ class "status-info-text" ] <|
2017-04-23 19:49:04 +00:00
List.intersperse (text " ")
[ icon iconType
, span [] <| List.intersperse (text ", ") (List.map accountLink accounts)
, text str
]
]
2017-04-29 07:20:26 +00:00
notificationStatusView :
String
-> Mastodon.Model.Account
-> Mastodon.Model.Status
-> Mastodon.Model.NotificationAggregate
-> Html Msg
notificationStatusView context currentUser status { type_, accounts } =
2017-04-23 19:49:04 +00:00
div [ class <| "notification " ++ type_ ]
[ case type_ of
"reblog" ->
2017-04-23 19:49:04 +00:00
notificationHeading accounts "boosted your toot" "fire"
"favourite" ->
2017-04-23 19:49:04 +00:00
notificationHeading accounts "favourited your toot" "star"
_ ->
text ""
, statusView context status
2017-04-29 07:20:26 +00:00
, statusActionsView status currentUser
]
2017-04-29 07:20:26 +00:00
notificationFollowView : Mastodon.Model.Account -> Mastodon.Model.NotificationAggregate -> Html Msg
notificationFollowView currentUser { accounts } =
let
profileView account =
div [ class "status follow-profile" ]
[ accountAvatarLink account
, div [ class "username" ] [ accountLink account ]
, p
[ class "status-text"
, onClick <| LoadAccount account.id
]
<|
formatContent account.note []
]
in
div [ class "notification follow" ]
[ notificationHeading accounts "started following you" "user"
, div [ class "" ] <| List.map profileView accounts
]
2017-04-29 07:20:26 +00:00
notificationEntryView :
Mastodon.Model.Account
-> Mastodon.Model.NotificationAggregate
-> Html Msg
notificationEntryView currentUser notification =
li [ class "list-group-item" ]
[ case notification.status of
Just status ->
2017-04-29 07:20:26 +00:00
notificationStatusView "notification" currentUser status notification
Nothing ->
2017-04-29 07:20:26 +00:00
notificationFollowView currentUser notification
]
2017-04-29 07:20:26 +00:00
notificationListView : Mastodon.Model.Account -> List Mastodon.Model.NotificationAggregate -> Html Msg
notificationListView currentUser notifications =
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
[ a
[ href "", onClickWithPreventAndStop <| ScrollColumn "notifications" ]
[ div [ class "panel-heading" ] [ icon "bell", text "Notifications" ] ]
, ul [ id "notifications", class "list-group timeline" ] <|
2017-04-29 07:20:26 +00:00
List.map (notificationEntryView currentUser) notifications
]
]
draftReplyToView : Draft -> Html Msg
draftReplyToView draft =
case draft.in_reply_to of
Just status ->
div [ class "in-reply-to" ]
[ p []
[ strong []
[ text "In reply to this toot ("
, a
[ href ""
, onClickWithPreventAndStop <| DraftEvent ClearDraft
]
[ icon "remove" ]
, text ")"
]
]
, div [ class "well" ] [ statusView "draft" status ]
]
Nothing ->
text ""
currentUserView : Maybe Mastodon.Model.Account -> Html Msg
currentUserView currentUser =
case currentUser of
Just currentUser ->
div [ class "current-user" ]
[ accountAvatarLink currentUser
, div [ class "username" ] [ accountLink currentUser ]
, p [ class "status-text" ] <| formatContent currentUser.note []
]
Nothing ->
text ""
2017-04-20 18:30:19 +00:00
draftView : Model -> Html Msg
draftView { draft, currentUser } =
2017-04-20 18:30:19 +00:00
let
hasSpoiler =
draft.spoiler_text /= Nothing
2017-04-21 12:03:39 +00:00
visibilityOptionView ( visibility, description ) =
option [ value visibility ]
[ text <| visibility ++ ": " ++ description ]
2017-04-20 18:30:19 +00:00
in
div [ class "panel panel-default" ]
[ div [ class "panel-heading" ]
[ icon "envelope"
, text <|
if draft.in_reply_to /= Nothing then
"Post a reply"
else
"Post a message"
]
, div [ class "panel-body" ]
[ currentUserView currentUser
, draftReplyToView draft
, Html.form [ class "form", onSubmit SubmitDraft ]
[ div [ class "form-group checkbox" ]
[ label []
[ input
[ type_ "checkbox"
, onCheck <| DraftEvent << ToggleSpoiler
, checked hasSpoiler
2017-04-20 18:30:19 +00:00
]
[]
, text " Add a spoiler"
2017-04-20 18:30:19 +00:00
]
]
, if hasSpoiler then
div [ class "form-group" ]
[ label [ for "spoiler" ] [ text "Visible part" ]
2017-04-20 18:30:19 +00:00
, textarea
[ id "spoiler"
2017-04-20 18:30:19 +00:00
, class "form-control"
, rows 5
, placeholder "This text will always be visible."
, onInput <| DraftEvent << UpdateSpoiler
2017-04-20 18:30:19 +00:00
, required True
, value <| Maybe.withDefault "" draft.spoiler_text
2017-04-20 18:30:19 +00:00
]
[]
]
else
text ""
, div [ class "form-group" ]
[ label [ for "status" ]
[ text <|
if hasSpoiler then
"Hidden part"
else
"Status"
2017-04-21 12:03:39 +00:00
]
, textarea
[ id "status"
, class "form-control"
, rows 8
, placeholder <|
if hasSpoiler then
"This text will be hidden by default, as you have enabled a spoiler."
else
"Once upon a time..."
, onInput <| DraftEvent << UpdateStatus
, required True
, value draft.status
2017-04-20 18:30:19 +00:00
]
[]
]
, div [ class "form-group" ]
[ label [ for "visibility" ] [ text "Visibility" ]
, select
[ id "visibility"
, class "form-control"
, onInput <| DraftEvent << UpdateVisibility
, required True
, value draft.visibility
]
<|
List.map visibilityOptionView <|
Dict.toList visibilities
]
, div [ class "form-group checkbox" ]
[ label []
[ input
[ type_ "checkbox"
, onCheck <| DraftEvent << UpdateSensitive
, checked draft.sensitive
]
[]
, text " This post is NSFW"
2017-04-20 18:30:19 +00:00
]
]
, justifiedButtonGroup
[ button
[ type_ "button"
, class "btn btn-default"
, onClick (DraftEvent ClearDraft)
]
[ text "Clear" ]
, button
[ type_ "submit"
, class "btn btn-primary"
]
[ text "Toot!" ]
]
]
]
]
2017-04-29 07:20:26 +00:00
threadView : Mastodon.Model.Account -> Thread -> Html Msg
threadView currentUser thread =
let
statuses =
List.concat
[ thread.context.ancestors
, [ thread.status ]
, thread.context.descendants
]
threadEntry status =
statusEntryView "thread"
(if status == thread.status then
"thread-target"
else
""
)
2017-04-29 07:20:26 +00:00
currentUser
status
in
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
[ closeablePanelheading "list" "Thread" CloseThread
2017-04-27 17:40:45 +00:00
, ul [ class "list-group timeline" ] <|
List.map threadEntry statuses
]
]
optionsView : Model -> Html Msg
optionsView model =
div [ class "panel panel-default" ]
[ div [ class "panel-heading" ] [ icon "cog", text "options" ]
, div [ class "panel-body" ]
[ div [ class "checkbox" ]
[ label []
[ input [ type_ "checkbox", onCheck UseGlobalTimeline ] []
, text " 4th column renders the global timeline"
2017-04-20 18:30:19 +00:00
]
]
]
]
sidebarView : Model -> Html Msg
sidebarView model =
div [ class "col-md-3 column" ]
[ draftView model
, optionsView model
]
2017-04-20 18:30:19 +00:00
2017-04-20 07:46:18 +00:00
homepageView : Model -> Html Msg
homepageView model =
2017-04-29 07:20:26 +00:00
case model.currentUser of
Nothing ->
text ""
Just currentUser ->
div [ class "row" ]
[ sidebarView model
, timelineView
"Home timeline"
"home"
"home"
currentUser
model.userTimeline
, notificationListView currentUser model.notifications
, case model.currentView of
Model.LocalTimelineView ->
timelineView
"Local timeline"
"th-large"
"local"
currentUser
model.localTimeline
Model.GlobalTimelineView ->
timelineView
"Global timeline"
"globe"
"global"
currentUser
model.globalTimeline
Model.AccountView account ->
-- Todo: Load the user timeline
accountTimelineView account model.accountTimeline "Account" "user"
Model.ThreadView thread ->
threadView currentUser thread
]
2017-04-20 07:46:18 +00:00
authView : Model -> Html Msg
authView model =
div [ class "col-md-4 col-md-offset-4" ]
[ div [ class "page-header" ]
[ h1 []
[ text "tooty"
, small []
[ text " is a Web client for the "
, a
[ href "https://github.com/tootsuite/mastodon"
, target "_blank"
]
[ text "Mastodon" ]
, text " API."
]
]
]
, div [ class "panel panel-default" ]
2017-04-20 07:46:18 +00:00
[ div [ class "panel-heading" ] [ text "Authenticate" ]
, div [ class "panel-body" ]
[ Html.form [ class "form", onSubmit Register ]
[ div [ class "form-group" ]
[ label [ for "server" ] [ text "Mastodon server root URL" ]
, input
[ type_ "url"
, class "form-control"
, id "server"
, required True
, placeholder "https://mastodon.social"
, value model.server
, pattern "https://.+"
, onInput ServerChange
]
[]
, p [ class "help-block" ]
2017-04-21 12:03:39 +00:00
[ text <|
"You'll be redirected to that server to authenticate yourself. "
++ "We don't have access to your password."
]
2017-04-20 07:46:18 +00:00
]
, button [ class "btn btn-primary", type_ "submit" ]
[ text "Sign into Tooty" ]
]
]
]
]
2017-04-24 19:21:43 +00:00
viewerView : Viewer -> Html Msg
viewerView { attachments, attachment } =
let
index =
Maybe.withDefault -1 <| elemIndex attachment attachments
( prev, next ) =
( getAt (index - 1) attachments, getAt (index + 1) attachments )
navLink label className target =
case target of
Nothing ->
text ""
Just target ->
a
[ href ""
, class className
, onClickWithPreventAndStop <|
2017-04-24 19:21:43 +00:00
ViewerEvent (OpenViewer attachments target)
]
[ text label ]
in
div
[ class "viewer"
, tabindex -1
, onClickWithPreventAndStop <| ViewerEvent CloseViewer
2017-04-24 19:21:43 +00:00
]
[ span [ class "close" ] [ text "×" ]
, navLink "" "prev" prev
, case attachment.type_ of
"image" ->
img [ class "viewer-content", src attachment.url ] []
_ ->
video
[ class "viewer-content"
, preload "auto"
, autoplay True
, loop True
]
[ source [ src attachment.url ] [] ]
, navLink "" "next" next
]
2017-04-20 07:46:18 +00:00
view : Model -> Html Msg
view model =
div [ class "container-fluid" ]
2017-04-22 14:43:45 +00:00
[ errorsListView model
2017-04-20 07:46:18 +00:00
, case model.client of
Just client ->
homepageView model
Nothing ->
authView model
2017-04-24 19:21:43 +00:00
, case model.viewer of
Just viewer ->
viewerView viewer
Nothing ->
text ""
2017-04-20 07:46:18 +00:00
]