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