Update advanced/part1
This commit is contained in:
@@ -2,11 +2,12 @@ module Article.Feed
|
|||||||
exposing
|
exposing
|
||||||
( Model
|
( Model
|
||||||
, Msg
|
, Msg
|
||||||
|
, decoder
|
||||||
, init
|
, init
|
||||||
, selectTag
|
|
||||||
, update
|
, update
|
||||||
, viewArticles
|
, viewArticles
|
||||||
, viewFeedSources
|
, viewPagination
|
||||||
|
, viewTabs
|
||||||
)
|
)
|
||||||
|
|
||||||
import Api
|
import Api
|
||||||
@@ -16,12 +17,11 @@ import Article.Slug as ArticleSlug exposing (Slug)
|
|||||||
import Article.Tag as Tag exposing (Tag)
|
import Article.Tag as Tag exposing (Tag)
|
||||||
import Author
|
import Author
|
||||||
import Avatar exposing (Avatar)
|
import Avatar exposing (Avatar)
|
||||||
import Browser.Dom as Dom
|
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src)
|
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src)
|
||||||
import Html.Events exposing (onClick)
|
import Html.Events exposing (onClick)
|
||||||
import Http
|
import Http
|
||||||
import HttpBuilder exposing (RequestBuilder, withExpect, withQueryParams)
|
import HttpBuilder exposing (RequestBuilder)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
import Json.Decode.Pipeline exposing (required)
|
import Json.Decode.Pipeline exposing (required)
|
||||||
import Page
|
import Page
|
||||||
@@ -32,6 +32,7 @@ import Session exposing (Session)
|
|||||||
import Task exposing (Task)
|
import Task exposing (Task)
|
||||||
import Time
|
import Time
|
||||||
import Timestamp
|
import Timestamp
|
||||||
|
import Url exposing (Url)
|
||||||
import Username exposing (Username)
|
import Username exposing (Username)
|
||||||
import Viewer exposing (Viewer)
|
import Viewer exposing (Viewer)
|
||||||
import Viewer.Cred as Cred exposing (Cred)
|
import Viewer.Cred as Cred exposing (Cred)
|
||||||
@@ -59,37 +60,29 @@ overkill, so we use simpler APIs instead.
|
|||||||
|
|
||||||
|
|
||||||
type Model
|
type Model
|
||||||
= Model InternalModel
|
= Model Internals
|
||||||
|
|
||||||
|
|
||||||
{-| This should not be exposed! We want to benefit from the guarantee that only
|
{-| This should not be exposed! We want to benefit from the guarantee that only
|
||||||
this module can create or alter this model. This way if it ever ends up in
|
this module can create or alter this model. This way if it ever ends up in
|
||||||
a surprising state, we know exactly where to look: this module.
|
a surprising state, we know exactly where to look: this module.
|
||||||
-}
|
-}
|
||||||
type alias InternalModel =
|
type alias Internals =
|
||||||
{ session : Session
|
{ session : Session
|
||||||
, errors : List String
|
, errors : List String
|
||||||
, articles : PaginatedList (Article Preview)
|
, articles : PaginatedList (Article Preview)
|
||||||
, sources : FeedSources
|
|
||||||
, isLoading : Bool
|
, isLoading : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : Session -> FeedSources -> Task Http.Error Model
|
init : Session -> PaginatedList (Article Preview) -> Model
|
||||||
init session sources =
|
init session articles =
|
||||||
let
|
|
||||||
fromArticles articles =
|
|
||||||
Model
|
Model
|
||||||
{ session = session
|
{ session = session
|
||||||
, errors = []
|
, errors = []
|
||||||
, articles = articles
|
, articles = articles
|
||||||
, sources = sources
|
|
||||||
, isLoading = False
|
, isLoading = False
|
||||||
}
|
}
|
||||||
in
|
|
||||||
FeedSources.selected sources
|
|
||||||
|> fetch (Session.cred session) 1
|
|
||||||
|> Task.map fromArticles
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +90,7 @@ init session sources =
|
|||||||
|
|
||||||
|
|
||||||
viewArticles : Time.Zone -> Model -> List (Html Msg)
|
viewArticles : Time.Zone -> Model -> List (Html Msg)
|
||||||
viewArticles timeZone (Model { articles, sources, session }) =
|
viewArticles timeZone (Model { articles, session, errors }) =
|
||||||
let
|
let
|
||||||
maybeCred =
|
maybeCred =
|
||||||
Session.cred session
|
Session.cred session
|
||||||
@@ -105,14 +98,8 @@ viewArticles timeZone (Model { articles, sources, session }) =
|
|||||||
articlesHtml =
|
articlesHtml =
|
||||||
PaginatedList.values articles
|
PaginatedList.values articles
|
||||||
|> List.map (viewPreview maybeCred timeZone)
|
|> List.map (viewPreview maybeCred timeZone)
|
||||||
|
|
||||||
feedSource =
|
|
||||||
FeedSources.selected sources
|
|
||||||
|
|
||||||
pagination =
|
|
||||||
PaginatedList.view ClickedFeedPage articles (limit feedSource)
|
|
||||||
in
|
in
|
||||||
List.append articlesHtml [ pagination ]
|
Page.viewErrors ClickedDismissErrors errors :: articlesHtml
|
||||||
|
|
||||||
|
|
||||||
viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg
|
viewPreview : Maybe Cred -> Time.Zone -> Article Preview -> Html Msg
|
||||||
@@ -167,85 +154,43 @@ viewPreview maybeCred timeZone article =
|
|||||||
[ h1 [] [ text title ]
|
[ h1 [] [ text title ]
|
||||||
, p [] [ text description ]
|
, p [] [ text description ]
|
||||||
, span [] [ text "Read more..." ]
|
, span [] [ text "Read more..." ]
|
||||||
|
, ul [ class "tag-list" ]
|
||||||
|
(List.map viewTag (Article.metadata article).tags)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewFeedSources : Model -> Html Msg
|
viewTabs :
|
||||||
viewFeedSources (Model { sources, isLoading, errors }) =
|
List ( String, msg )
|
||||||
let
|
-> ( String, msg )
|
||||||
errorsHtml =
|
-> List ( String, msg )
|
||||||
Page.viewErrors ClickedDismissErrors errors
|
-> Html msg
|
||||||
in
|
viewTabs before selected after =
|
||||||
ul [ class "nav nav-pills outline-active" ] <|
|
ul [ class "nav nav-pills outline-active" ] <|
|
||||||
List.concat
|
List.concat
|
||||||
[ List.map (viewFeedSource False) (FeedSources.before sources)
|
[ List.map (viewTab []) before
|
||||||
, [ viewFeedSource True (FeedSources.selected sources) ]
|
, [ viewTab [ class "active" ] selected ]
|
||||||
, List.map (viewFeedSource False) (FeedSources.after sources)
|
, List.map (viewTab []) after
|
||||||
, [ errorsHtml ]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewFeedSource : Bool -> Source -> Html Msg
|
viewTab : List (Attribute msg) -> ( String, msg ) -> Html msg
|
||||||
viewFeedSource isSelected source =
|
viewTab attrs ( name, msg ) =
|
||||||
li [ class "nav-item" ]
|
li [ class "nav-item" ]
|
||||||
[ a
|
[ -- Note: The RealWorld CSS requires an href to work properly.
|
||||||
[ classList [ ( "nav-link", True ), ( "active", isSelected ) ]
|
a (class "nav-link" :: onClick msg :: href "" :: attrs)
|
||||||
, onClick (ClickedFeedSource source)
|
[ text name ]
|
||||||
|
|
||||||
-- The RealWorld CSS requires an href to work properly.
|
|
||||||
, href ""
|
|
||||||
]
|
|
||||||
[ text (sourceName source) ]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
selectTag : Maybe Cred -> Tag -> Cmd Msg
|
viewPagination : (Int -> msg) -> Model -> Html msg
|
||||||
selectTag maybeCred tag =
|
viewPagination toMsg (Model feed) =
|
||||||
let
|
PaginatedList.view toMsg feed.articles
|
||||||
source =
|
|
||||||
TagFeed tag
|
|
||||||
in
|
|
||||||
fetch maybeCred 1 source
|
|
||||||
|> Task.attempt (CompletedFeedLoad source)
|
|
||||||
|
|
||||||
|
|
||||||
sourceName : Source -> String
|
viewTag : String -> Html msg
|
||||||
sourceName source =
|
viewTag tagName =
|
||||||
case source of
|
li [ class "tag-default tag-pill tag-outline" ] [ text tagName ]
|
||||||
YourFeed _ ->
|
|
||||||
"Your Feed"
|
|
||||||
|
|
||||||
GlobalFeed ->
|
|
||||||
"Global Feed"
|
|
||||||
|
|
||||||
TagFeed tagName ->
|
|
||||||
"#" ++ Tag.toString tagName
|
|
||||||
|
|
||||||
FavoritedFeed username ->
|
|
||||||
"Favorited Articles"
|
|
||||||
|
|
||||||
AuthorFeed username ->
|
|
||||||
"My Articles"
|
|
||||||
|
|
||||||
|
|
||||||
limit : Source -> Int
|
|
||||||
limit feedSource =
|
|
||||||
case feedSource of
|
|
||||||
YourFeed _ ->
|
|
||||||
10
|
|
||||||
|
|
||||||
GlobalFeed ->
|
|
||||||
10
|
|
||||||
|
|
||||||
TagFeed tagName ->
|
|
||||||
10
|
|
||||||
|
|
||||||
FavoritedFeed username ->
|
|
||||||
5
|
|
||||||
|
|
||||||
AuthorFeed username ->
|
|
||||||
5
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -256,10 +201,7 @@ type Msg
|
|||||||
= ClickedDismissErrors
|
= ClickedDismissErrors
|
||||||
| ClickedFavorite Cred Slug
|
| ClickedFavorite Cred Slug
|
||||||
| ClickedUnfavorite Cred Slug
|
| ClickedUnfavorite Cred Slug
|
||||||
| ClickedFeedPage Int
|
|
||||||
| ClickedFeedSource Source
|
|
||||||
| CompletedFavorite (Result Http.Error (Article Preview))
|
| CompletedFavorite (Result Http.Error (Article Preview))
|
||||||
| CompletedFeedLoad Source (Result Http.Error (PaginatedList (Article Preview)))
|
|
||||||
|
|
||||||
|
|
||||||
update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg )
|
update : Maybe Cred -> Msg -> Model -> ( Model, Cmd Msg )
|
||||||
@@ -268,32 +210,6 @@ update maybeCred msg (Model model) =
|
|||||||
ClickedDismissErrors ->
|
ClickedDismissErrors ->
|
||||||
( Model { model | errors = [] }, Cmd.none )
|
( Model { model | errors = [] }, Cmd.none )
|
||||||
|
|
||||||
ClickedFeedSource source ->
|
|
||||||
( Model { model | isLoading = True }
|
|
||||||
, source
|
|
||||||
|> fetch maybeCred 1
|
|
||||||
|> Task.attempt (CompletedFeedLoad source)
|
|
||||||
)
|
|
||||||
|
|
||||||
CompletedFeedLoad source (Ok articles) ->
|
|
||||||
( Model
|
|
||||||
{ model
|
|
||||||
| articles = articles
|
|
||||||
, sources = FeedSources.select source model.sources
|
|
||||||
, isLoading = False
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
CompletedFeedLoad _ (Err error) ->
|
|
||||||
( Model
|
|
||||||
{ model
|
|
||||||
| errors = Api.addServerError model.errors
|
|
||||||
, isLoading = False
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
ClickedFavorite cred slug ->
|
ClickedFavorite cred slug ->
|
||||||
fave Article.favorite cred slug model
|
fave Article.favorite cred slug model
|
||||||
|
|
||||||
@@ -310,72 +226,6 @@ update maybeCred msg (Model model) =
|
|||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
ClickedFeedPage page ->
|
|
||||||
let
|
|
||||||
source =
|
|
||||||
FeedSources.selected model.sources
|
|
||||||
in
|
|
||||||
( Model model
|
|
||||||
, fetch maybeCred page source
|
|
||||||
|> Task.andThen (\articles -> Task.map (\_ -> articles) scrollToTop)
|
|
||||||
|> Task.attempt (CompletedFeedLoad source)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
scrollToTop : Task x ()
|
|
||||||
scrollToTop =
|
|
||||||
Dom.setViewport 0 0
|
|
||||||
-- It's not worth showing the user anything special if scrolling fails.
|
|
||||||
-- If anything, we'd log this to an error recording service.
|
|
||||||
|> Task.onError (\_ -> Task.succeed ())
|
|
||||||
|
|
||||||
|
|
||||||
fetch : Maybe Cred -> Int -> Source -> Task Http.Error (PaginatedList (Article Preview))
|
|
||||||
fetch maybeCred page feedSource =
|
|
||||||
let
|
|
||||||
articlesPerPage =
|
|
||||||
limit feedSource
|
|
||||||
|
|
||||||
offset =
|
|
||||||
(page - 1) * articlesPerPage
|
|
||||||
|
|
||||||
params =
|
|
||||||
[ ( "limit", String.fromInt articlesPerPage )
|
|
||||||
, ( "offset", String.fromInt offset )
|
|
||||||
]
|
|
||||||
in
|
|
||||||
Task.map (PaginatedList.mapPage (\_ -> page)) <|
|
|
||||||
case feedSource of
|
|
||||||
YourFeed cred ->
|
|
||||||
params
|
|
||||||
|> buildFromQueryParams (Just cred) (Api.url [ "articles", "feed" ])
|
|
||||||
|> Cred.addHeader cred
|
|
||||||
|> HttpBuilder.toRequest
|
|
||||||
|> Http.toTask
|
|
||||||
|
|
||||||
GlobalFeed ->
|
|
||||||
list maybeCred params
|
|
||||||
|
|
||||||
TagFeed tagName ->
|
|
||||||
list maybeCred (( "tag", Tag.toString tagName ) :: params)
|
|
||||||
|
|
||||||
FavoritedFeed username ->
|
|
||||||
list maybeCred (( "favorited", Username.toString username ) :: params)
|
|
||||||
|
|
||||||
AuthorFeed username ->
|
|
||||||
list maybeCred (( "author", Username.toString username ) :: params)
|
|
||||||
|
|
||||||
|
|
||||||
list :
|
|
||||||
Maybe Cred
|
|
||||||
-> List ( String, String )
|
|
||||||
-> Task Http.Error (PaginatedList (Article Preview))
|
|
||||||
list maybeCred params =
|
|
||||||
buildFromQueryParams maybeCred (Api.url [ "articles" ]) params
|
|
||||||
|> Cred.addHeaderIfAvailable maybeCred
|
|
||||||
|> HttpBuilder.toRequest
|
|
||||||
|> Http.toTask
|
|
||||||
|
|
||||||
|
|
||||||
replaceArticle : Article a -> Article a -> Article a
|
replaceArticle : Article a -> Article a -> Article a
|
||||||
replaceArticle newArticle oldArticle =
|
replaceArticle newArticle oldArticle =
|
||||||
@@ -390,29 +240,24 @@ replaceArticle newArticle oldArticle =
|
|||||||
-- SERIALIZATION
|
-- SERIALIZATION
|
||||||
|
|
||||||
|
|
||||||
decoder : Maybe Cred -> Decoder (PaginatedList (Article Preview))
|
decoder : Maybe Cred -> Int -> Decoder (PaginatedList (Article Preview))
|
||||||
decoder maybeCred =
|
decoder maybeCred resultsPerPage =
|
||||||
Decode.succeed PaginatedList.fromList
|
Decode.succeed PaginatedList.fromList
|
||||||
|> required "articlesCount" Decode.int
|
|> required "articlesCount" (pageCountDecoder resultsPerPage)
|
||||||
|> required "articles" (Decode.list (Article.previewDecoder maybeCred))
|
|> required "articles" (Decode.list (Article.previewDecoder maybeCred))
|
||||||
|
|
||||||
|
|
||||||
|
pageCountDecoder : Int -> Decoder Int
|
||||||
-- REQUEST
|
pageCountDecoder resultsPerPage =
|
||||||
|
Decode.int
|
||||||
|
|> Decode.map (\total -> ceiling (toFloat total / toFloat resultsPerPage))
|
||||||
buildFromQueryParams : Maybe Cred -> String -> List ( String, String ) -> RequestBuilder (PaginatedList (Article Preview))
|
|
||||||
buildFromQueryParams maybeCred url queryParams =
|
|
||||||
HttpBuilder.get url
|
|
||||||
|> withExpect (Http.expectJson (decoder maybeCred))
|
|
||||||
|> withQueryParams queryParams
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INTERNAL
|
-- INTERNAL
|
||||||
|
|
||||||
|
|
||||||
fave : (Slug -> Cred -> Http.Request (Article Preview)) -> Cred -> Slug -> InternalModel -> ( Model, Cmd Msg )
|
fave : (Slug -> Cred -> Http.Request (Article Preview)) -> Cred -> Slug -> Internals -> ( Model, Cmd Msg )
|
||||||
fave toRequest cred slug model =
|
fave toRequest cred slug model =
|
||||||
( Model model
|
( Model model
|
||||||
, toRequest slug cred
|
, toRequest slug cred
|
||||||
|
|||||||
@@ -3,20 +3,25 @@ module Page.Home exposing (Model, Msg, init, subscriptions, toSession, update, v
|
|||||||
{-| The homepage. You can get here via either the / or /#/ routes.
|
{-| The homepage. You can get here via either the / or /#/ routes.
|
||||||
-}
|
-}
|
||||||
|
|
||||||
import Article
|
import Api
|
||||||
|
import Article exposing (Article, Preview)
|
||||||
import Article.Feed as Feed
|
import Article.Feed as Feed
|
||||||
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
|
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
|
||||||
import Article.Tag as Tag exposing (Tag)
|
import Article.Tag as Tag exposing (Tag)
|
||||||
|
import Browser.Dom as Dom
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder)
|
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder)
|
||||||
import Html.Events exposing (onClick)
|
import Html.Events exposing (onClick)
|
||||||
import Http
|
import Http
|
||||||
|
import HttpBuilder
|
||||||
import Loading
|
import Loading
|
||||||
import Log
|
import Log
|
||||||
import Page
|
import Page
|
||||||
|
import PaginatedList exposing (PaginatedList)
|
||||||
import Session exposing (Session)
|
import Session exposing (Session)
|
||||||
import Task exposing (Task)
|
import Task exposing (Task)
|
||||||
import Time
|
import Time
|
||||||
|
import Username exposing (Username)
|
||||||
import Viewer.Cred as Cred exposing (Cred)
|
import Viewer.Cred as Cred exposing (Cred)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +32,8 @@ import Viewer.Cred as Cred exposing (Cred)
|
|||||||
type alias Model =
|
type alias Model =
|
||||||
{ session : Session
|
{ session : Session
|
||||||
, timeZone : Time.Zone
|
, timeZone : Time.Zone
|
||||||
|
, feedTab : FeedTab
|
||||||
|
, feedPage : Int
|
||||||
|
|
||||||
-- Loaded independently from server
|
-- Loaded independently from server
|
||||||
, tags : Status (List Tag)
|
, tags : Status (List Tag)
|
||||||
@@ -41,28 +48,35 @@ type Status a
|
|||||||
| Failed
|
| Failed
|
||||||
|
|
||||||
|
|
||||||
|
type FeedTab
|
||||||
|
= YourFeed Cred
|
||||||
|
| GlobalFeed
|
||||||
|
| TagFeed Tag
|
||||||
|
|
||||||
|
|
||||||
init : Session -> ( Model, Cmd Msg )
|
init : Session -> ( Model, Cmd Msg )
|
||||||
init session =
|
init session =
|
||||||
let
|
let
|
||||||
feedSources =
|
feedTab =
|
||||||
case Session.cred session of
|
case Session.cred session of
|
||||||
Just cred ->
|
Just cred ->
|
||||||
FeedSources.fromLists (YourFeed cred) [ GlobalFeed ]
|
YourFeed cred
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
FeedSources.fromLists GlobalFeed []
|
GlobalFeed
|
||||||
|
|
||||||
loadTags =
|
loadTags =
|
||||||
Tag.list
|
Http.toTask Tag.list
|
||||||
|> Http.toTask
|
|
||||||
in
|
in
|
||||||
( { session = session
|
( { session = session
|
||||||
, timeZone = Time.utc
|
, timeZone = Time.utc
|
||||||
|
, feedTab = feedTab
|
||||||
|
, feedPage = 1
|
||||||
, tags = Loading
|
, tags = Loading
|
||||||
, feed = Loading
|
, feed = Loading
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Feed.init session feedSources
|
[ fetchFeed session feedTab 1
|
||||||
|> Task.attempt CompletedFeedLoad
|
|> Task.attempt CompletedFeedLoad
|
||||||
, Tag.list
|
, Tag.list
|
||||||
|> Http.send CompletedTagsLoad
|
|> Http.send CompletedTagsLoad
|
||||||
@@ -87,7 +101,17 @@ view model =
|
|||||||
[ div [ class "col-md-9" ] <|
|
[ div [ class "col-md-9" ] <|
|
||||||
case model.feed of
|
case model.feed of
|
||||||
Loaded feed ->
|
Loaded feed ->
|
||||||
viewFeed model.timeZone feed
|
[ div [ class "feed-toggle" ] <|
|
||||||
|
List.concat
|
||||||
|
[ [ viewTabs
|
||||||
|
(Session.cred model.session)
|
||||||
|
model.feedTab
|
||||||
|
]
|
||||||
|
, Feed.viewArticles model.timeZone feed
|
||||||
|
|> List.map (Html.map GotFeedMsg)
|
||||||
|
, [ Feed.viewPagination ClickedFeedPage feed ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
Loading ->
|
Loading ->
|
||||||
[]
|
[]
|
||||||
@@ -130,11 +154,58 @@ viewBanner =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewFeed : Time.Zone -> Feed.Model -> List (Html Msg)
|
|
||||||
viewFeed timeZone feed =
|
-- TABS
|
||||||
div [ class "feed-toggle" ]
|
|
||||||
[ Feed.viewFeedSources feed |> Html.map GotFeedMsg ]
|
|
||||||
:: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg))
|
viewTabs : Maybe Cred -> FeedTab -> Html Msg
|
||||||
|
viewTabs maybeCred tab =
|
||||||
|
case tab of
|
||||||
|
YourFeed cred ->
|
||||||
|
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
|
||||||
|
|
||||||
|
GlobalFeed ->
|
||||||
|
let
|
||||||
|
otherTabs =
|
||||||
|
case maybeCred of
|
||||||
|
Just cred ->
|
||||||
|
[ yourFeed cred ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Feed.viewTabs otherTabs globalFeed []
|
||||||
|
|
||||||
|
TagFeed tag ->
|
||||||
|
let
|
||||||
|
otherTabs =
|
||||||
|
case maybeCred of
|
||||||
|
Just cred ->
|
||||||
|
[ yourFeed cred, globalFeed ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
[ globalFeed ]
|
||||||
|
in
|
||||||
|
Feed.viewTabs otherTabs (tagFeed tag) []
|
||||||
|
|
||||||
|
|
||||||
|
yourFeed : Cred -> ( String, Msg )
|
||||||
|
yourFeed cred =
|
||||||
|
( "Your Feed", ClickedTab (YourFeed cred) )
|
||||||
|
|
||||||
|
|
||||||
|
globalFeed : ( String, Msg )
|
||||||
|
globalFeed =
|
||||||
|
( "Global Feed", ClickedTab GlobalFeed )
|
||||||
|
|
||||||
|
|
||||||
|
tagFeed : Tag -> ( String, Msg )
|
||||||
|
tagFeed tag =
|
||||||
|
( "#" ++ Tag.toString tag, ClickedTab (TagFeed tag) )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- TAGS
|
||||||
|
|
||||||
|
|
||||||
viewTags : List Tag -> Html Msg
|
viewTags : List Tag -> Html Msg
|
||||||
@@ -160,6 +231,8 @@ viewTag tagName =
|
|||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= ClickedTag Tag
|
= ClickedTag Tag
|
||||||
|
| ClickedTab FeedTab
|
||||||
|
| ClickedFeedPage Int
|
||||||
| CompletedFeedLoad (Result Http.Error Feed.Model)
|
| CompletedFeedLoad (Result Http.Error Feed.Model)
|
||||||
| CompletedTagsLoad (Result Http.Error (List Tag))
|
| CompletedTagsLoad (Result Http.Error (List Tag))
|
||||||
| GotTimeZone Time.Zone
|
| GotTimeZone Time.Zone
|
||||||
@@ -171,12 +244,28 @@ type Msg
|
|||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
ClickedTag tagName ->
|
ClickedTag tag ->
|
||||||
let
|
let
|
||||||
subCmd =
|
feedTab =
|
||||||
Feed.selectTag (Session.cred model.session) tagName
|
TagFeed tag
|
||||||
in
|
in
|
||||||
( model, Cmd.map GotFeedMsg subCmd )
|
( { model | feedTab = feedTab }
|
||||||
|
, fetchFeed model.session feedTab 1
|
||||||
|
|> Task.attempt CompletedFeedLoad
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickedTab tab ->
|
||||||
|
( { model | feedTab = tab }
|
||||||
|
, fetchFeed model.session tab 1
|
||||||
|
|> Task.attempt CompletedFeedLoad
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickedFeedPage page ->
|
||||||
|
( { model | feedPage = page }
|
||||||
|
, fetchFeed model.session model.feedTab page
|
||||||
|
|> Task.andThen (\feed -> Task.map (\_ -> feed) scrollToTop)
|
||||||
|
|> Task.attempt CompletedFeedLoad
|
||||||
|
)
|
||||||
|
|
||||||
CompletedFeedLoad (Ok feed) ->
|
CompletedFeedLoad (Ok feed) ->
|
||||||
( { model | feed = Loaded feed }, Cmd.none )
|
( { model | feed = Loaded feed }, Cmd.none )
|
||||||
@@ -242,6 +331,53 @@ update msg model =
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HTTP
|
||||||
|
|
||||||
|
|
||||||
|
fetchFeed : Session -> FeedTab -> Int -> Task Http.Error Feed.Model
|
||||||
|
fetchFeed session feedTabs page =
|
||||||
|
let
|
||||||
|
maybeCred =
|
||||||
|
Session.cred session
|
||||||
|
|
||||||
|
builder =
|
||||||
|
case feedTabs of
|
||||||
|
YourFeed cred ->
|
||||||
|
Api.url [ "articles", "feed" ]
|
||||||
|
|> HttpBuilder.get
|
||||||
|
|> Cred.addHeader cred
|
||||||
|
|
||||||
|
GlobalFeed ->
|
||||||
|
Api.url [ "articles" ]
|
||||||
|
|> HttpBuilder.get
|
||||||
|
|> Cred.addHeaderIfAvailable maybeCred
|
||||||
|
|
||||||
|
TagFeed tag ->
|
||||||
|
Api.url [ "articles" ]
|
||||||
|
|> HttpBuilder.get
|
||||||
|
|> Cred.addHeaderIfAvailable maybeCred
|
||||||
|
|> HttpBuilder.withQueryParam "tag" (Tag.toString tag)
|
||||||
|
in
|
||||||
|
builder
|
||||||
|
|> HttpBuilder.withExpect (Http.expectJson (Feed.decoder maybeCred articlesPerPage))
|
||||||
|
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
||||||
|
|> Task.map (Feed.init session)
|
||||||
|
|
||||||
|
|
||||||
|
articlesPerPage : Int
|
||||||
|
articlesPerPage =
|
||||||
|
10
|
||||||
|
|
||||||
|
|
||||||
|
scrollToTop : Task x ()
|
||||||
|
scrollToTop =
|
||||||
|
Dom.setViewport 0 0
|
||||||
|
-- It's not worth showing the user anything special if scrolling fails.
|
||||||
|
-- If anything, we'd log this to an error recording service.
|
||||||
|
|> Task.onError (\_ -> Task.succeed ())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- SUBSCRIPTIONS
|
-- SUBSCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ module Page.Profile exposing (Model, Msg, init, subscriptions, toSession, update
|
|||||||
{-| An Author's profile.
|
{-| An Author's profile.
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
import Api
|
||||||
|
import Article exposing (Article, Preview)
|
||||||
import Article.Feed as Feed
|
import Article.Feed as Feed
|
||||||
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
|
import Article.FeedSources as FeedSources exposing (FeedSources, Source(..))
|
||||||
import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor)
|
import Author exposing (Author(..), FollowedAuthor, UnfollowedAuthor)
|
||||||
@@ -10,9 +12,11 @@ import Avatar exposing (Avatar)
|
|||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Http
|
import Http
|
||||||
|
import HttpBuilder exposing (RequestBuilder)
|
||||||
import Loading
|
import Loading
|
||||||
import Log
|
import Log
|
||||||
import Page
|
import Page
|
||||||
|
import PaginatedList exposing (PaginatedList)
|
||||||
import Profile exposing (Profile)
|
import Profile exposing (Profile)
|
||||||
import Route
|
import Route
|
||||||
import Session exposing (Session)
|
import Session exposing (Session)
|
||||||
@@ -31,6 +35,8 @@ type alias Model =
|
|||||||
{ session : Session
|
{ session : Session
|
||||||
, timeZone : Time.Zone
|
, timeZone : Time.Zone
|
||||||
, errors : List String
|
, errors : List String
|
||||||
|
, feedTab : FeedTab
|
||||||
|
, feedPage : Int
|
||||||
|
|
||||||
-- Loaded independently from server
|
-- Loaded independently from server
|
||||||
, author : Status Author
|
, author : Status Author
|
||||||
@@ -38,6 +44,11 @@ type alias Model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type FeedTab
|
||||||
|
= MyArticles
|
||||||
|
| FavoritedArticles
|
||||||
|
|
||||||
|
|
||||||
type Status a
|
type Status a
|
||||||
= Loading Username
|
= Loading Username
|
||||||
| LoadingSlowly Username
|
| LoadingSlowly Username
|
||||||
@@ -54,6 +65,8 @@ init session username =
|
|||||||
( { session = session
|
( { session = session
|
||||||
, timeZone = Time.utc
|
, timeZone = Time.utc
|
||||||
, errors = []
|
, errors = []
|
||||||
|
, feedTab = defaultFeedTab
|
||||||
|
, feedPage = 1
|
||||||
, author = Loading username
|
, author = Loading username
|
||||||
, feed = Loading username
|
, feed = Loading username
|
||||||
}
|
}
|
||||||
@@ -62,16 +75,68 @@ init session username =
|
|||||||
|> Http.toTask
|
|> Http.toTask
|
||||||
|> Task.mapError (Tuple.pair username)
|
|> Task.mapError (Tuple.pair username)
|
||||||
|> Task.attempt CompletedAuthorLoad
|
|> Task.attempt CompletedAuthorLoad
|
||||||
, defaultFeedSources username
|
, fetchFeed session defaultFeedTab username 1
|
||||||
|> Feed.init session
|
|
||||||
|> Task.mapError (Tuple.pair username)
|
|
||||||
|> Task.attempt CompletedFeedLoad
|
|
||||||
, Task.perform GotTimeZone Time.here
|
, Task.perform GotTimeZone Time.here
|
||||||
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
, 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
|
||||||
|
|
||||||
|
|
||||||
|
fetchFeed : Session -> FeedTab -> Username -> Int -> Cmd Msg
|
||||||
|
fetchFeed session feedTabs username page =
|
||||||
|
let
|
||||||
|
maybeCred =
|
||||||
|
Session.cred 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 session)
|
||||||
|
|> Task.mapError (Tuple.pair username)
|
||||||
|
|> Task.attempt CompletedFeedLoad
|
||||||
|
|
||||||
|
|
||||||
|
articlesPerPage : Int
|
||||||
|
articlesPerPage =
|
||||||
|
5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
@@ -145,7 +210,13 @@ view model =
|
|||||||
, case model.feed of
|
, case model.feed of
|
||||||
Loaded feed ->
|
Loaded feed ->
|
||||||
div [ class "container" ]
|
div [ class "container" ]
|
||||||
[ div [ class "row" ] [ viewFeed model.timeZone feed ] ]
|
[ div [ class "row" ]
|
||||||
|
[ div [ class "col-xs-12 col-md-10 offset-md-1" ] <|
|
||||||
|
div [ class "articles-toggle" ]
|
||||||
|
[ viewTabs model.feedTab ]
|
||||||
|
:: (Feed.viewArticles model.timeZone feed |> List.map (Html.map GotFeedMsg))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
Loading _ ->
|
Loading _ ->
|
||||||
text ""
|
text ""
|
||||||
@@ -181,7 +252,7 @@ titleForMe : Maybe Cred -> Username -> String
|
|||||||
titleForMe maybeCred username =
|
titleForMe maybeCred username =
|
||||||
case maybeCred of
|
case maybeCred of
|
||||||
Just cred ->
|
Just cred ->
|
||||||
if username == cred.username then
|
if username == Cred.username cred then
|
||||||
myProfileTitle
|
myProfileTitle
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -202,15 +273,27 @@ defaultTitle =
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- FEED
|
-- TABS
|
||||||
|
|
||||||
|
|
||||||
viewFeed : Time.Zone -> Feed.Model -> Html Msg
|
viewTabs : FeedTab -> Html Msg
|
||||||
viewFeed timeZone feed =
|
viewTabs tab =
|
||||||
div [ class "col-xs-12 col-md-10 offset-md-1" ] <|
|
case tab of
|
||||||
div [ class "articles-toggle" ]
|
MyArticles ->
|
||||||
[ Feed.viewFeedSources feed |> Html.map GotFeedMsg ]
|
Feed.viewTabs [] myArticles [ favoritedArticles ]
|
||||||
:: (Feed.viewArticles timeZone feed |> List.map (Html.map GotFeedMsg))
|
|
||||||
|
FavoritedArticles ->
|
||||||
|
Feed.viewTabs [ myArticles ] favoritedArticles []
|
||||||
|
|
||||||
|
|
||||||
|
myArticles : ( String, Msg )
|
||||||
|
myArticles =
|
||||||
|
( "My Articles", ClickedTab MyArticles )
|
||||||
|
|
||||||
|
|
||||||
|
favoritedArticles : ( String, Msg )
|
||||||
|
favoritedArticles =
|
||||||
|
( "Favorited Articles", ClickedTab FavoritedArticles )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -221,6 +304,7 @@ type Msg
|
|||||||
= ClickedDismissErrors
|
= ClickedDismissErrors
|
||||||
| ClickedFollow Cred UnfollowedAuthor
|
| ClickedFollow Cred UnfollowedAuthor
|
||||||
| ClickedUnfollow Cred FollowedAuthor
|
| ClickedUnfollow Cred FollowedAuthor
|
||||||
|
| ClickedTab FeedTab
|
||||||
| CompletedFollowChange (Result Http.Error Author)
|
| CompletedFollowChange (Result Http.Error Author)
|
||||||
| CompletedAuthorLoad (Result ( Username, Http.Error ) Author)
|
| CompletedAuthorLoad (Result ( Username, Http.Error ) Author)
|
||||||
| CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model)
|
| CompletedFeedLoad (Result ( Username, Http.Error ) Feed.Model)
|
||||||
@@ -248,6 +332,11 @@ update msg model =
|
|||||||
|> Http.send CompletedFollowChange
|
|> Http.send CompletedFollowChange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ClickedTab tab ->
|
||||||
|
( { model | feedTab = tab }
|
||||||
|
, fetchFeed model.session tab (currentUsername model) 1
|
||||||
|
)
|
||||||
|
|
||||||
CompletedFollowChange (Ok newAuthor) ->
|
CompletedFollowChange (Ok newAuthor) ->
|
||||||
( { model | author = Loaded newAuthor }
|
( { model | author = Loaded newAuthor }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
@@ -335,12 +424,3 @@ subscriptions model =
|
|||||||
toSession : Model -> Session
|
toSession : Model -> Session
|
||||||
toSession model =
|
toSession model =
|
||||||
model.session
|
model.session
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INTERNAL
|
|
||||||
|
|
||||||
|
|
||||||
defaultFeedSources : Username -> FeedSources
|
|
||||||
defaultFeedSources username =
|
|
||||||
FeedSources.fromLists (AuthorFeed username) [ FavoritedFeed username ]
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
module PaginatedList exposing (PaginatedList, fromList, map, mapPage, page, total, values, view)
|
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values, view)
|
||||||
|
|
||||||
import Html exposing (Html, a, li, text, ul)
|
import Html exposing (Html, a, li, text, ul)
|
||||||
import Html.Attributes exposing (class, classList, href)
|
import Html.Attributes exposing (class, classList, href)
|
||||||
import Html.Events exposing (onClick)
|
import Html.Events exposing (onClick)
|
||||||
|
import Http
|
||||||
|
import HttpBuilder exposing (RequestBuilder)
|
||||||
|
import Task exposing (Task)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -54,29 +57,18 @@ map transform (PaginatedList info) =
|
|||||||
PaginatedList { info | values = List.map transform info.values }
|
PaginatedList { info | values = List.map transform info.values }
|
||||||
|
|
||||||
|
|
||||||
mapPage : (Int -> Int) -> PaginatedList a -> PaginatedList a
|
|
||||||
mapPage transform (PaginatedList info) =
|
|
||||||
PaginatedList { info | page = transform info.page }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- VIEW
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
view : (Int -> msg) -> PaginatedList a -> Int -> Html msg
|
view : (Int -> msg) -> PaginatedList a -> Html msg
|
||||||
view toMsg list resultsPerPage =
|
view toMsg (PaginatedList info) =
|
||||||
let
|
let
|
||||||
totalPages =
|
|
||||||
ceiling (toFloat (total list) / toFloat resultsPerPage)
|
|
||||||
|
|
||||||
activePage =
|
|
||||||
page list
|
|
||||||
|
|
||||||
viewPageLink currentPage =
|
viewPageLink currentPage =
|
||||||
pageLink toMsg currentPage (currentPage == activePage)
|
pageLink toMsg currentPage (currentPage == info.page)
|
||||||
in
|
in
|
||||||
if totalPages > 1 then
|
if info.total > 1 then
|
||||||
List.range 1 totalPages
|
List.range 1 info.total
|
||||||
|> List.map viewPageLink
|
|> List.map viewPageLink
|
||||||
|> ul [ class "pagination" ]
|
|> ul [ class "pagination" ]
|
||||||
|
|
||||||
@@ -96,3 +88,31 @@ pageLink toMsg targetPage isActive =
|
|||||||
]
|
]
|
||||||
[ text (String.fromInt targetPage) ]
|
[ text (String.fromInt targetPage) ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HTTP
|
||||||
|
|
||||||
|
|
||||||
|
{-| I considered accepting a record here so I don't mess up the argument order.
|
||||||
|
-}
|
||||||
|
fromRequestBuilder :
|
||||||
|
Int
|
||||||
|
-> Int
|
||||||
|
-> RequestBuilder (PaginatedList a)
|
||||||
|
-> Task Http.Error (PaginatedList a)
|
||||||
|
fromRequestBuilder resultsPerPage pageNumber builder =
|
||||||
|
let
|
||||||
|
offset =
|
||||||
|
(pageNumber - 1) * resultsPerPage
|
||||||
|
|
||||||
|
params =
|
||||||
|
[ ( "limit", String.fromInt resultsPerPage )
|
||||||
|
, ( "offset", String.fromInt offset )
|
||||||
|
]
|
||||||
|
in
|
||||||
|
builder
|
||||||
|
|> HttpBuilder.withQueryParams params
|
||||||
|
|> HttpBuilder.toRequest
|
||||||
|
|> Http.toTask
|
||||||
|
|> Task.map (\(PaginatedList info) -> PaginatedList { info | page = pageNumber })
|
||||||
|
|||||||
Reference in New Issue
Block a user