Files
elm-0.19-workshop/intro/part9/src/Author.elm
Richard Feldman ad816cc3ff Fix Author
2018-08-14 03:37:19 -04:00

252 lines
6.9 KiB
Elm
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module Author
exposing
( Author(..)
, FollowedAuthor
, UnfollowedAuthor
, decoder
, fetch
, follow
, followButton
, profile
, requestFollow
, requestUnfollow
, unfollow
, unfollowButton
, username
, view
)
{-| The author of an Article. It includes a Profile.
I designed this to make sure the compiler would help me keep these three
possibilities straight when displaying follow buttons and such:
- I'm following this author.
- I'm not following this author.
- I _can't_ follow this author, because it's me!
To do this, I defined `Author` a custom type with three variants, one for each
of those possibilities.
I also made separate types for FollowedAuthor and UnfollowedAuthor.
They are custom type wrappers around Profile, and thier sole purpose is to
help me keep track of which operations are supported.
For example, consider these functions:
requestFollow : UnfollowedAuthor -> Cred -> Http.Request Author
requestUnfollow : FollowedAuthor -> Cred -> Http.Request Author
These types help the compiler prevent several mistakes:
- Displaying a Follow button for an author the user already follows.
- Displaying an Unfollow button for an author the user already doesn't follow.
- Displaying either button when the author is ourself.
There are still ways we could mess things up (e.g. make a button that calls Author.unfollow when you click it, but which displays "Follow" to the user) - but this rules out a bunch of potential problems.
-}
import Api
import Html exposing (Html, a, i, text)
import Html.Attributes exposing (attribute, class, href, id, placeholder)
import Html.Events exposing (onClick)
import Http
import HttpBuilder exposing (RequestBuilder, withExpect)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (custom, optional, required)
import Json.Encode as Encode exposing (Value)
import Profile exposing (Profile)
import Route exposing (Route)
import Username exposing (Username)
import Viewer exposing (Viewer)
import Viewer.Cred as Cred exposing (Cred)
{-| An author - either the current user, another user we're following, or
another user we aren't following.
These distinctions matter because we can only perform "follow" requests for
users we aren't following, we can only perform "unfollow" requests for
users we _are_ following, and we can't perform either for ourselves.
-}
type Author
= IsFollowing FollowedAuthor
| IsNotFollowing UnfollowedAuthor
| IsViewer Cred Profile
{-| An author we're following.
-}
type FollowedAuthor
= FollowedAuthor Username Profile
{-| An author we're not following.
-}
type UnfollowedAuthor
= UnfollowedAuthor Username Profile
{-| Return an Author's username.
-}
username : Author -> Username
username author =
case author of
IsViewer cred _ ->
Cred.username cred
IsFollowing (FollowedAuthor val _) ->
val
IsNotFollowing (UnfollowedAuthor val _) ->
val
{-| Return an Author's profile.
-}
profile : Author -> Profile
profile author =
case author of
IsViewer _ val ->
val
IsFollowing (FollowedAuthor _ val) ->
val
IsNotFollowing (UnfollowedAuthor _ val) ->
val
-- FETCH
fetch : Username -> Maybe Cred -> Http.Request Author
fetch uname maybeCred =
Api.url [ "profiles", Username.toString uname ]
|> HttpBuilder.get
|> HttpBuilder.withExpect (Http.expectJson (Decode.field "profile" (decoder maybeCred)))
|> Cred.addHeaderIfAvailable maybeCred
|> HttpBuilder.toRequest
-- FOLLOWING
follow : UnfollowedAuthor -> FollowedAuthor
follow (UnfollowedAuthor uname prof) =
FollowedAuthor uname prof
unfollow : FollowedAuthor -> UnfollowedAuthor
unfollow (FollowedAuthor uname prof) =
UnfollowedAuthor uname prof
requestFollow : UnfollowedAuthor -> Cred -> Http.Request Author
requestFollow (UnfollowedAuthor uname _) cred =
requestHelp HttpBuilder.post uname cred
requestUnfollow : FollowedAuthor -> Cred -> Http.Request Author
requestUnfollow (FollowedAuthor uname _) cred =
requestHelp HttpBuilder.delete uname cred
requestHelp :
(String -> RequestBuilder a)
-> Username
-> Cred
-> Http.Request Author
requestHelp builderFromUrl uname cred =
Api.url [ "profiles", Username.toString uname, "follow" ]
|> builderFromUrl
|> Cred.addHeader cred
|> withExpect (Http.expectJson (Decode.field "profile" (decoder Nothing)))
|> HttpBuilder.toRequest
followButton : (Cred -> UnfollowedAuthor -> msg) -> Cred -> UnfollowedAuthor -> Html msg
followButton toMsg cred ((UnfollowedAuthor uname _) as author) =
toggleFollowButton "Follow"
[ "btn-outline-secondary" ]
(toMsg cred author)
uname
unfollowButton : (Cred -> FollowedAuthor -> msg) -> Cred -> FollowedAuthor -> Html msg
unfollowButton toMsg cred ((FollowedAuthor uname _) as author) =
toggleFollowButton "Unfollow"
[ "btn-secondary" ]
(toMsg cred author)
uname
toggleFollowButton : String -> List String -> msg -> Username -> Html msg
toggleFollowButton txt extraClasses msgWhenClicked uname =
let
classStr =
"btn btn-sm " ++ String.join " " extraClasses ++ " action-btn"
caption =
" " ++ txt ++ " " ++ Username.toString uname
in
Html.button [ class classStr, onClick msgWhenClicked ]
[ i [ class "ion-plus-round" ] []
, text caption
]
-- SERIALIZATION
decoder : Maybe Cred -> Decoder Author
decoder maybeCred =
Decode.succeed Tuple.pair
|> custom Profile.decoder
|> required "username" Username.decoder
|> Decode.andThen (decodeFromPair maybeCred)
decodeFromPair : Maybe Cred -> ( Profile, Username ) -> Decoder Author
decodeFromPair maybeCred ( prof, uname ) =
case maybeCred of
Nothing ->
-- If you're logged out, you can't be following anyone!
Decode.succeed (IsNotFollowing (UnfollowedAuthor uname prof))
Just cred ->
if uname == Cred.username cred then
Decode.succeed (IsViewer cred prof)
else
nonViewerDecoder prof uname
nonViewerDecoder : Profile -> Username -> Decoder Author
nonViewerDecoder prof uname =
Decode.succeed (authorFromFollowing prof uname)
|> optional "following" Decode.bool False
authorFromFollowing : Profile -> Username -> Bool -> Author
authorFromFollowing prof uname isFollowing =
if isFollowing then
IsFollowing (FollowedAuthor uname prof)
else
IsNotFollowing (UnfollowedAuthor uname prof)
{-| View an author. We basically render their username and a link to their
profile, and that's it.
-}
view : Username -> Html msg
view uname =
a [ class "author", Route.href (Route.Profile uname) ]
[ Username.toHtml uname ]