449 lines
12 KiB
Elm
449 lines
12 KiB
Elm
module Page.Profile exposing (Model, Msg, init, subscriptions, toSession, update, view)
|
|
|
|
{-| An Author's profile.
|
|
-}
|
|
|
|
import Api
|
|
import Article exposing (Article, Preview)
|
|
import Article.Feed as Feed
|
|
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
|
|
import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor)
|
|
import Avatar exposing (Avatar)
|
|
import Html exposing (..)
|
|
import Html.Attributes exposing (..)
|
|
import Http
|
|
import HttpBuilder exposing (RequestBuilder)
|
|
import Loading
|
|
import Log
|
|
import Page
|
|
import PaginatedList exposing (PaginatedList)
|
|
import Profile exposing (Profile)
|
|
import Route
|
|
import Session exposing (Session)
|
|
import Task exposing (Task)
|
|
import Time
|
|
import Username exposing (Username)
|
|
import Viewer exposing (Viewer)
|
|
import Viewer.Cred as Cred exposing (Cred)
|
|
|
|
|
|
|
|
-- MODEL
|
|
|
|
|
|
type alias Model =
|
|
{ session : Session
|
|
, timeZone : Time.Zone
|
|
, errors : List String
|
|
, feedTab : FeedTab
|
|
, feedPage : Int
|
|
|
|
-- Loaded independently from server
|
|
, author : Status Author
|
|
, feed : Status Feed.Model
|
|
}
|
|
|
|
|
|
type FeedTab
|
|
= MyArticles
|
|
| FavoritedArticles
|
|
|
|
|
|
type Status a
|
|
= Loading Username
|
|
| LoadingSlowly Username
|
|
| Loaded a
|
|
| Failed Username
|
|
|
|
|
|
init : Session -> Username -> ( Model, Cmd Msg )
|
|
init session username =
|
|
let
|
|
maybeCred =
|
|
Session.cred session
|
|
|
|
model =
|
|
{ session = session
|
|
, timeZone = Time.utc
|
|
, errors = []
|
|
, feedTab = defaultFeedTab
|
|
, feedPage = 1
|
|
, author = Loading username
|
|
, feed = Loading username
|
|
}
|
|
in
|
|
( model
|
|
, Cmd.batch
|
|
[ Author.fetch username maybeCred
|
|
|> Http.toTask
|
|
|> Task.mapError (Tuple.pair username)
|
|
|> Task.attempt CompletedAuthorLoad
|
|
, fetchFeed model defaultFeedTab 1
|
|
, Task.perform GotTimeZone Time.here
|
|
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
|
]
|
|
)
|
|
|
|
|
|
currentUsername : Model -> Username
|
|
currentUsername model =
|
|
case model.author of
|
|
Loading username ->
|
|
username
|
|
|
|
LoadingSlowly username ->
|
|
username
|
|
|
|
Loaded author ->
|
|
Author.username author
|
|
|
|
Failed username ->
|
|
username
|
|
|
|
|
|
defaultFeedTab : FeedTab
|
|
defaultFeedTab =
|
|
MyArticles
|
|
|
|
|
|
|
|
-- HTTP
|
|
|
|
|
|
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
|
|
|
💡 HINT: It may end up with multiple arguments!
|
|
|
|
-}
|
|
fetchFeed : Model -> FeedTab -> Int -> Cmd Msg
|
|
fetchFeed model feedTabs page =
|
|
let
|
|
username =
|
|
currentUsername model
|
|
|
|
maybeCred =
|
|
Session.cred model.session
|
|
|
|
( extraParamName, extraParamVal ) =
|
|
case feedTabs of
|
|
MyArticles ->
|
|
( "author", Username.toString username )
|
|
|
|
FavoritedArticles ->
|
|
( "favorited", Username.toString username )
|
|
in
|
|
Api.url [ "articles" ]
|
|
|> HttpBuilder.get
|
|
|> HttpBuilder.withExpect (Http.expectJson (Feed.decoder maybeCred articlesPerPage))
|
|
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|
|
|> Cred.addHeaderIfAvailable maybeCred
|
|
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
|
|> Task.map (Feed.init model.session)
|
|
|> Task.mapError (Tuple.pair username)
|
|
|> Task.attempt CompletedFeedLoad
|
|
|
|
|
|
articlesPerPage : Int
|
|
articlesPerPage =
|
|
5
|
|
|
|
|
|
|
|
-- VIEW
|
|
|
|
|
|
view : Model -> { title : String, content : Html Msg }
|
|
view model =
|
|
let
|
|
title =
|
|
case model.author of
|
|
Loaded (IsViewer _ _) ->
|
|
myProfileTitle
|
|
|
|
Loaded ((IsFollowing followedAuthor) as author) ->
|
|
titleForOther (Author.username author)
|
|
|
|
Loaded ((IsNotFollowing unfollowedAuthor) as author) ->
|
|
titleForOther (Author.username author)
|
|
|
|
Loading username ->
|
|
titleForMe (Session.cred model.session) username
|
|
|
|
LoadingSlowly username ->
|
|
titleForMe (Session.cred model.session) username
|
|
|
|
Failed username ->
|
|
titleForMe (Session.cred model.session) username
|
|
in
|
|
{ title = title
|
|
, content =
|
|
case model.author of
|
|
Loaded author ->
|
|
let
|
|
profile =
|
|
Author.profile author
|
|
|
|
username =
|
|
Author.username author
|
|
|
|
followButton =
|
|
case Session.cred model.session of
|
|
Just cred ->
|
|
case author of
|
|
IsViewer _ _ ->
|
|
-- We can't follow ourselves!
|
|
text ""
|
|
|
|
IsFollowing followedAuthor ->
|
|
Author.unfollowButton ClickedUnfollow cred followedAuthor
|
|
|
|
IsNotFollowing unfollowedAuthor ->
|
|
Author.followButton ClickedFollow cred unfollowedAuthor
|
|
|
|
Nothing ->
|
|
-- We can't follow if we're logged out
|
|
text ""
|
|
in
|
|
div [ class "profile-page" ]
|
|
[ Page.viewErrors ClickedDismissErrors model.errors
|
|
, div [ class "user-info" ]
|
|
[ div [ class "container" ]
|
|
[ div [ class "row" ]
|
|
[ div [ class "col-xs-12 col-md-10 offset-md-1" ]
|
|
[ img [ class "user-img", Avatar.src (Profile.avatar profile) ] []
|
|
, h4 [] [ Username.toHtml username ]
|
|
, p [] [ text (Maybe.withDefault "" (Profile.bio profile)) ]
|
|
, followButton
|
|
]
|
|
]
|
|
]
|
|
]
|
|
, case model.feed of
|
|
Loaded feed ->
|
|
div [ class "container" ]
|
|
[ div [ class "row" ]
|
|
[ div [ class "col-xs-12 col-md-10 offset-md-1" ]
|
|
[ div [ class "articles-toggle" ] <|
|
|
List.concat
|
|
[ [ viewTabs model.feedTab ]
|
|
, Feed.viewArticles model.timeZone feed
|
|
|> List.map (Html.map GotFeedMsg)
|
|
, [ Feed.viewPagination ClickedFeedPage feed ]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
Loading _ ->
|
|
text ""
|
|
|
|
LoadingSlowly _ ->
|
|
Loading.icon
|
|
|
|
Failed _ ->
|
|
Loading.error "feed"
|
|
]
|
|
|
|
Loading _ ->
|
|
text ""
|
|
|
|
LoadingSlowly _ ->
|
|
Loading.icon
|
|
|
|
Failed _ ->
|
|
Loading.error "profile"
|
|
}
|
|
|
|
|
|
|
|
-- PAGE TITLE
|
|
|
|
|
|
titleForOther : Username -> String
|
|
titleForOther otherUsername =
|
|
"Profile — " ++ Username.toString otherUsername
|
|
|
|
|
|
titleForMe : Maybe Cred -> Username -> String
|
|
titleForMe maybeCred username =
|
|
case maybeCred of
|
|
Just cred ->
|
|
if username == Cred.username cred then
|
|
myProfileTitle
|
|
|
|
else
|
|
defaultTitle
|
|
|
|
Nothing ->
|
|
defaultTitle
|
|
|
|
|
|
myProfileTitle : String
|
|
myProfileTitle =
|
|
"My Profile"
|
|
|
|
|
|
defaultTitle : String
|
|
defaultTitle =
|
|
"Profile"
|
|
|
|
|
|
|
|
-- TABS
|
|
|
|
|
|
viewTabs : FeedTab -> Html Msg
|
|
viewTabs tab =
|
|
case tab of
|
|
MyArticles ->
|
|
Feed.viewTabs [] myArticles [ favoritedArticles ]
|
|
|
|
FavoritedArticles ->
|
|
Feed.viewTabs [ myArticles ] favoritedArticles []
|
|
|
|
|
|
myArticles : ( String, Msg )
|
|
myArticles =
|
|
( "My Articles", ClickedTab MyArticles )
|
|
|
|
|
|
favoritedArticles : ( String, Msg )
|
|
favoritedArticles =
|
|
( "Favorited Articles", ClickedTab FavoritedArticles )
|
|
|
|
|
|
|
|
-- UPDATE
|
|
|
|
|
|
type Msg
|
|
= ClickedDismissErrors
|
|
| ClickedFollow Cred UnfollowedAuthor
|
|
| ClickedUnfollow Cred FollowedAuthor
|
|
| ClickedTab FeedTab
|
|
| ClickedFeedPage Int
|
|
| CompletedFollowChange (Result Http.Error Author)
|
|
| CompletedAuthorLoad (Result ( Username, Http.Error ) Author)
|
|
| CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model)
|
|
| GotTimeZone Time.Zone
|
|
| GotFeedMsg Feed.Msg
|
|
| GotSession Session
|
|
| PassedSlowLoadThreshold
|
|
|
|
|
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
update msg model =
|
|
case msg of
|
|
ClickedDismissErrors ->
|
|
( { model | errors = [] }, Cmd.none )
|
|
|
|
ClickedUnfollow cred followedAuthor ->
|
|
( model
|
|
, Author.requestUnfollow followedAuthor cred
|
|
|> Http.send CompletedFollowChange
|
|
)
|
|
|
|
ClickedFollow cred unfollowedAuthor ->
|
|
( model
|
|
, Author.requestFollow unfollowedAuthor cred
|
|
|> Http.send CompletedFollowChange
|
|
)
|
|
|
|
ClickedTab tab ->
|
|
( { model | feedTab = tab }
|
|
, fetchFeed model tab 1
|
|
)
|
|
|
|
ClickedFeedPage page ->
|
|
( { model | feedPage = page }
|
|
, fetchFeed model model.feedTab page
|
|
)
|
|
|
|
CompletedFollowChange (Ok newAuthor) ->
|
|
( { model | author = Loaded newAuthor }
|
|
, Cmd.none
|
|
)
|
|
|
|
CompletedFollowChange (Err error) ->
|
|
( model
|
|
, Log.error
|
|
)
|
|
|
|
CompletedAuthorLoad (Ok author) ->
|
|
( { model | author = Loaded author }, Cmd.none )
|
|
|
|
CompletedAuthorLoad (Err ( username, err )) ->
|
|
( { model | author = Failed username }
|
|
, Log.error
|
|
)
|
|
|
|
CompletedFeedLoad (Ok feed) ->
|
|
( { model | feed = Loaded feed }
|
|
, Cmd.none
|
|
)
|
|
|
|
CompletedFeedLoad (Err ( username, err )) ->
|
|
( { model | feed = Failed username }
|
|
, Log.error
|
|
)
|
|
|
|
GotFeedMsg subMsg ->
|
|
case model.feed of
|
|
Loaded feed ->
|
|
let
|
|
( newFeed, subCmd ) =
|
|
Feed.update (Session.cred model.session) subMsg feed
|
|
in
|
|
( { model | feed = Loaded newFeed }
|
|
, Cmd.map GotFeedMsg subCmd
|
|
)
|
|
|
|
Loading _ ->
|
|
( model, Log.error )
|
|
|
|
LoadingSlowly _ ->
|
|
( model, Log.error )
|
|
|
|
Failed _ ->
|
|
( model, Log.error )
|
|
|
|
GotTimeZone tz ->
|
|
( { model | timeZone = tz }, Cmd.none )
|
|
|
|
GotSession session ->
|
|
( { model | session = session }
|
|
, Route.replaceUrl (Session.navKey session) Route.Home
|
|
)
|
|
|
|
PassedSlowLoadThreshold ->
|
|
let
|
|
-- If any data is still Loading, change it to LoadingSlowly
|
|
-- so `view` knows to render a spinner.
|
|
feed =
|
|
case model.feed of
|
|
Loading username ->
|
|
LoadingSlowly username
|
|
|
|
other ->
|
|
other
|
|
in
|
|
( { model | feed = feed }, Cmd.none )
|
|
|
|
|
|
|
|
-- SUBSCRIPTIONS
|
|
|
|
|
|
subscriptions : Model -> Sub Msg
|
|
subscriptions model =
|
|
Session.changes GotSession (Session.navKey model.session)
|
|
|
|
|
|
|
|
-- EXPORT
|
|
|
|
|
|
toSession : Model -> Session
|
|
toSession model =
|
|
model.session
|