Add part8
This commit is contained in:
251
intro/part8/src/Author.elm
Normal file
251
intro/part8/src/Author.elm
Normal file
@@ -0,0 +1,251 @@
|
||||
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, 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.field "following" Decode.bool
|
||||
|> Decode.map (authorFromFollowing prof uname)
|
||||
|
||||
|
||||
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 ]
|
||||
Reference in New Issue
Block a user