Fix #49: Denote account relationships, follow & unfollow actions. (#105)

* Check relationships.
* Follow & unfollow actions.
* Don't display follow button to ourselves.
* Profile view follow button.
* Added follow/unfollow button to viewed account profile.
This commit is contained in:
Nicolas Perriault 2017-04-29 22:48:55 +02:00 committed by GitHub
parent 57f7f0cc7c
commit 059b2726e6
9 changed files with 379 additions and 59 deletions

View File

@ -100,21 +100,39 @@ body {
}
.follow-entry {
min-height: 38px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: flex-start;
}
.follow-entry .avatar {
order: 0;
flex: 0 1 auto;
align-self: auto;
width: 38px;
height: 38px;
margin-right: 10px;
}
.follow-entry .username {
font-weight: normal;
font-size: 97%;
margin-left: 50px;
.follow-entry .userinfo {
order: 0;
flex: 10 1 auto;
align-self: auto;
overflow: hidden;
text-overflow: ellipsis;
}
.follow-entry button {
order: 0;
flex: 0 1 auto;
align-self: auto;
width: 40px;
height: 40px;
}
.acct {
font-size: 97%;
font-weight: normal;
@ -317,8 +335,10 @@ body {
/* Account rules */
.account-detail {
text-align:center;
position: relative;
text-align: center;
}
.account-detail .opacity-layer{
background: rgba(49,53,67,0.9);
}
@ -330,6 +350,14 @@ body {
margin:0 auto 0;
}
.account-detail .btn {
position: absolute;
top: 1em;
left: 1em;
width: 50px;
height: 50px;
opacity: .8;
}
.account-detail .account-display-name {
display: block;

View File

@ -8,7 +8,10 @@ module Command
, loadNotifications
, loadUserAccount
, loadAccount
, loadAccountInfo
, loadAccountTimeline
, loadAccountFollowers
, loadAccountFollowing
, loadRelationships
, loadThread
, loadTimelines
, postStatus
@ -17,6 +20,8 @@ module Command
, unreblogStatus
, favouriteStatus
, unfavouriteStatus
, follow
, unfollow
)
import Json.Encode as Encode
@ -112,25 +117,56 @@ loadAccount : Maybe Client -> Int -> Cmd Msg
loadAccount client accountId =
case client of
Just client ->
Mastodon.Http.fetchAccount client accountId
|> Mastodon.Http.send (MastodonEvent << AccountReceived)
Cmd.batch
[ Mastodon.Http.fetchAccount client accountId
|> Mastodon.Http.send (MastodonEvent << AccountReceived)
, Mastodon.Http.fetchRelationships client [ accountId ]
|> Mastodon.Http.send (MastodonEvent << AccountRelationship)
]
Nothing ->
Cmd.none
loadAccountInfo : Maybe Client -> Int -> Cmd Msg
loadAccountInfo client accountId =
loadAccountTimeline : Maybe Client -> Int -> Cmd Msg
loadAccountTimeline client accountId =
case client of
Just client ->
Cmd.batch
[ Mastodon.Http.fetchAccountTimeline client accountId
|> Mastodon.Http.send (MastodonEvent << AccountTimeline)
, Mastodon.Http.fetchAccountFollowers client accountId
|> Mastodon.Http.send (MastodonEvent << AccountFollowers)
, Mastodon.Http.fetchAccountFollowing client accountId
|> Mastodon.Http.send (MastodonEvent << AccountFollowing)
]
Mastodon.Http.fetchAccountTimeline client accountId
|> Mastodon.Http.send (MastodonEvent << AccountTimeline)
Nothing ->
Cmd.none
loadAccountFollowers : Maybe Client -> Int -> Cmd Msg
loadAccountFollowers client accountId =
case client of
Just client ->
Mastodon.Http.fetchAccountFollowers client accountId
|> Mastodon.Http.send (MastodonEvent << AccountFollowers)
Nothing ->
Cmd.none
loadAccountFollowing : Maybe Client -> Int -> Cmd Msg
loadAccountFollowing client accountId =
case client of
Just client ->
Mastodon.Http.fetchAccountFollowing client accountId
|> Mastodon.Http.send (MastodonEvent << AccountFollowing)
Nothing ->
Cmd.none
loadRelationships : Maybe Client -> List Int -> Cmd Msg
loadRelationships client accountIds =
case client of
Just client ->
Mastodon.Http.fetchRelationships client accountIds
|> Mastodon.Http.send (MastodonEvent << AccountRelationships)
Nothing ->
Cmd.none
@ -229,3 +265,25 @@ unfavouriteStatus client statusId =
Nothing ->
Cmd.none
follow : Maybe Client -> Int -> Cmd Msg
follow client id =
case client of
Just client ->
Mastodon.Http.follow client id
|> Mastodon.Http.send (MastodonEvent << AccountFollowed)
Nothing ->
Cmd.none
unfollow : Maybe Client -> Int -> Cmd Msg
unfollow client id =
case client of
Just client ->
Mastodon.Http.unfollow client id
|> Mastodon.Http.send (MastodonEvent << AccountUnfollowed)
Nothing ->
Cmd.none

View File

@ -12,12 +12,15 @@ module Mastodon.ApiUrl
, homeTimeline
, publicTimeline
, notifications
, relationships
, statuses
, context
, reblog
, unreblog
, favourite
, unfavourite
, follow
, unfollow
, streaming
)
@ -51,11 +54,32 @@ account id =
accounts ++ (toString id)
follow : Server -> Int -> String
follow server id =
server ++ accounts ++ (toString id) ++ "/follow"
unfollow : Server -> Int -> String
unfollow server id =
server ++ accounts ++ (toString id) ++ "/unfollow"
userAccount : Server -> String
userAccount server =
server ++ accounts ++ "verify_credentials"
relationships : List Int -> String
relationships ids =
let
qs =
ids
|> List.map (\id -> "id[]=" ++ (toString id))
|> String.join "&"
in
accounts ++ "relationships?" ++ qs
followers : Int -> String
followers id =
(account id) ++ "/followers"

View File

@ -11,6 +11,7 @@ module Mastodon.Decoder
, notificationDecoder
, tagDecoder
, reblogDecoder
, relationshipDecoder
, statusDecoder
, webSocketPayloadDecoder
, webSocketEventDecoder
@ -100,6 +101,17 @@ notificationDecoder =
|> Pipe.optional "status" (Decode.nullable statusDecoder) Nothing
relationshipDecoder : Decode.Decoder Relationship
relationshipDecoder =
Pipe.decode Relationship
|> Pipe.required "id" Decode.int
|> Pipe.required "blocking" Decode.bool
|> Pipe.required "followed_by" Decode.bool
|> Pipe.required "following" Decode.bool
|> Pipe.required "muting" Decode.bool
|> Pipe.required "requested" Decode.bool
tagDecoder : Decode.Decoder Tag
tagDecoder =
Pipe.decode Tag

View File

@ -6,6 +6,8 @@ module Mastodon.Http
, unreblog
, favourite
, unfavourite
, follow
, unfollow
, register
, getAuthorizationUrl
, getAccessToken
@ -17,6 +19,7 @@ module Mastodon.Http
, fetchNotifications
, fetchGlobalTimeline
, fetchUserTimeline
, fetchRelationships
, postStatus
, deleteStatus
, userAccount
@ -116,6 +119,11 @@ fetchUserTimeline client =
fetch client ApiUrl.homeTimeline <| Decode.list statusDecoder
fetchRelationships : Client -> List Int -> Request (List Relationship)
fetchRelationships client ids =
fetch client (ApiUrl.relationships ids) <| Decode.list relationshipDecoder
fetchLocalTimeline : Client -> Request (List Status)
fetchLocalTimeline client =
fetch client (ApiUrl.publicTimeline (Just "public")) <| Decode.list statusDecoder
@ -201,3 +209,17 @@ unfavourite client id =
HttpBuilder.post (ApiUrl.unfavourite client.server id)
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
follow : Client -> Int -> Request Relationship
follow client id =
HttpBuilder.post (ApiUrl.follow client.server id)
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|> HttpBuilder.withExpect (Http.expectJson relationshipDecoder)
unfollow : Client -> Int -> Request Relationship
unfollow client id =
HttpBuilder.post (ApiUrl.unfollow client.server id)
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|> HttpBuilder.withExpect (Http.expectJson relationshipDecoder)

View File

@ -11,6 +11,7 @@ module Mastodon.Model
, Notification
, NotificationAggregate
, Reblog(..)
, Relationship
, Tag
, Status
, StatusRequestBody
@ -148,6 +149,16 @@ type Reblog
= Reblog Status
type alias Relationship =
{ id : Int
, blocking : Bool
, followed_by : Bool
, following : Bool
, muting : Bool
, requested : Bool
}
type alias Status =
{ account : Account
, content : String

View File

@ -53,6 +53,8 @@ init flags location =
, accountTimeline = []
, accountFollowers = []
, accountFollowing = []
, accountRelationships = []
, accountRelationship = Nothing
, notifications = []
, draft = defaultDraft
, errors = []
@ -150,6 +152,38 @@ deleteStatusFromTimeline statusId timeline =
)
{-| Update viewed account relationships as well as the relationship with the
current connected user, both according to the "following" status provided.
-}
processFollowEvent : Relationship -> Bool -> Model -> Model
processFollowEvent relationship flag model =
let
updateRelationship r =
if r.id == relationship.id then
{ r | following = flag }
else
r
accountRelationships =
model.accountRelationships |> List.map updateRelationship
accountRelationship =
case model.accountRelationship of
Just accountRelationship ->
if accountRelationship.id == relationship.id then
Just { relationship | following = flag }
else
model.accountRelationship
Nothing ->
Nothing
in
{ model
| accountRelationships = accountRelationships
, accountRelationship = accountRelationship
}
updateDraft : DraftMsg -> Account -> Draft -> ( Draft, Cmd Msg )
updateDraft draftMsg currentUser draft =
case draftMsg of
@ -222,6 +256,22 @@ processMastodonEvent msg model =
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountFollowed result ->
case result of
Ok relationship ->
processFollowEvent relationship True model ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountUnfollowed result ->
case result of
Ok relationship ->
processFollowEvent relationship False model ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AppRegistered result ->
case result of
Ok registration ->
@ -329,7 +379,7 @@ processMastodonEvent msg model =
case result of
Ok account ->
{ model | currentView = AccountView account }
! [ Command.loadAccountInfo model.client account.id ]
! [ Command.loadAccountTimeline model.client account.id ]
Err error ->
{ model
@ -348,16 +398,37 @@ processMastodonEvent msg model =
AccountFollowers result ->
case result of
Ok statuses ->
{ model | accountFollowers = statuses } ! []
Ok followers ->
{ model | accountFollowers = followers }
! [ Command.loadRelationships model.client <| List.map .id followers ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountFollowing result ->
case result of
Ok statuses ->
{ model | accountFollowing = statuses } ! []
Ok following ->
{ model | accountFollowing = following }
! [ Command.loadRelationships model.client <| List.map .id following ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountRelationship result ->
case result of
Ok [ relationship ] ->
{ model | accountRelationship = Just relationship } ! []
Ok _ ->
model ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountRelationships result ->
case result of
Ok relationships ->
{ model | accountRelationships = relationships } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
@ -493,6 +564,12 @@ update msg model =
CloseThread ->
{ model | currentView = preferredTimeline model } ! []
FollowAccount id ->
model ! [ Command.follow model.client id ]
UnfollowAccount id ->
model ! [ Command.unfollow model.client id ]
DeleteStatus id ->
model ! [ Command.deleteStatus model.client id ]
@ -531,14 +608,22 @@ update msg model =
model ! [ Command.postStatus model.client <| toStatusRequestBody model.draft ]
LoadAccount accountId ->
{ model | currentView = preferredTimeline model }
{ model
| accountTimeline = []
, accountFollowers = []
, accountFollowing = []
, accountRelationships = []
, accountRelationship = Nothing
}
! [ Command.loadAccount model.client accountId ]
ViewAccountFollowers account ->
{ model | currentView = AccountFollowersView account model.accountFollowers } ! []
{ model | currentView = AccountFollowersView account model.accountFollowers }
! [ Command.loadAccountFollowers model.client account.id ]
ViewAccountFollowing account ->
{ model | currentView = AccountFollowingView account model.accountFollowing } ! []
{ model | currentView = AccountFollowingView account model.accountFollowing }
! [ Command.loadAccountFollowing model.client account.id ]
ViewAccountStatuses account ->
{ model | currentView = AccountView account } ! []

View File

@ -27,10 +27,14 @@ type ViewerMsg
type MastodonMsg
= AccessToken (Result Error AccessTokenResult)
| AccountFollowed (Result Error Relationship)
| AccountFollowers (Result Error (List Account))
| AccountFollowing (Result Error (List Account))
| AccountReceived (Result Error Account)
| AccountRelationship (Result Error (List Relationship))
| AccountRelationships (Result Error (List Relationship))
| AccountTimeline (Result Error (List Status))
| AccountUnfollowed (Result Error Relationship)
| AppRegistered (Result Error AppRegistration)
| ContextLoaded Status (Result Error Context)
| CurrentUser (Result Error Account)
@ -58,6 +62,7 @@ type Msg
| CloseThread
| DeleteStatus Int
| DraftEvent DraftMsg
| FollowAccount Int
| LoadAccount Int
| MastodonEvent MastodonMsg
| NoOp
@ -68,6 +73,7 @@ type Msg
| ScrollColumn String
| ServerChange String
| SubmitDraft
| UnfollowAccount Int
| UrlChange Navigation.Location
| UseGlobalTimeline Bool
| UnreblogStatus Int
@ -127,6 +133,8 @@ type alias Model =
, accountTimeline : List Status
, accountFollowers : List Account
, accountFollowing : List Account
, accountRelationships : List Relationship
, accountRelationship : Maybe Relationship
, notifications : List NotificationAggregate
, draft : Draft
, errors : List String

View File

@ -4,7 +4,7 @@ import Dict
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import List.Extra exposing (elemIndex, getAt)
import List.Extra exposing (find, elemIndex, getAt)
import Mastodon.Helper
import Mastodon.Model exposing (..)
import Types exposing (..)
@ -14,6 +14,14 @@ import Date.Extra.Config.Config_en_au as DateEn
import Date.Extra.Format as DateFormat
type alias CurrentUser =
Account
type alias CurrentUserRelation =
Maybe Relationship
visibilities : Dict.Dict String String
visibilities =
Dict.fromList
@ -202,21 +210,60 @@ statusView context ({ account, content, media_attachments, reblog, mentions } as
]
followView : Account -> Html Msg
followView account =
followButton : CurrentUser -> CurrentUserRelation -> Account -> Html Msg
followButton currentUser relationship account =
if Mastodon.Helper.sameAccount account currentUser then
text ""
else
let
( followEvent, btnClasses, iconName, tooltip ) =
case relationship of
Nothing ->
( NoOp
, "btn btn-default btn-disabled"
, "question-sign"
, "Unknown relationship"
)
Just relationship ->
if relationship.following then
( UnfollowAccount account.id
, "btn btn-default btn-primary"
, "eye-close"
, "Unfollow"
)
else
( FollowAccount account.id
, "btn btn-default"
, "eye-open"
, "Follow"
)
in
button [ class btnClasses, title tooltip, onClick followEvent ]
[ icon iconName ]
followView : CurrentUser -> Maybe Relationship -> Account -> Html Msg
followView currentUser relationship account =
div [ class "follow-entry" ]
[ accountAvatarLink account
, div [ class "username" ]
, div [ class "userinfo" ]
[ strong []
[ text <|
if account.display_name /= "" then
account.display_name
else
account.username
[ a
[ href account.url
, onClickWithPreventAndStop <| LoadAccount account.id
]
[ text <|
if account.display_name /= "" then
account.display_name
else
account.username
]
]
, br [] []
, text <| "@" ++ account.acct
]
, followButton currentUser relationship account
]
@ -233,22 +280,23 @@ accountCounterLink label count tagger account =
]
accountView : String -> String -> Account -> Html Msg -> Html Msg
accountView label iconName account panelContent =
accountView : CurrentUser -> Account -> CurrentUserRelation -> Html Msg -> Html Msg
accountView currentUser account relationship panelContent =
let
{ statuses_count, following_count, followers_count } =
account
in
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
[ closeablePanelheading iconName label CloseAccount
[ closeablePanelheading "user" "Account" CloseAccount
, div [ class "timeline" ]
[ div
[ class "account-detail"
, style [ ( "background-image", "url('" ++ account.header ++ "')" ) ]
]
[ div [ class "opacity-layer" ]
[ img [ src account.avatar ] []
[ followButton currentUser relationship account
, img [ src account.avatar ] []
, span [ class "account-display-name" ] [ text account.display_name ]
, span [ class "account-username" ] [ text ("@" ++ account.username) ]
, span [ class "account-note" ] (formatContent account.note [])
@ -265,9 +313,9 @@ accountView label iconName account panelContent =
]
accountTimelineView : String -> List Status -> Account -> Html Msg
accountTimelineView label statuses account =
accountView label "user" account <|
accountTimelineView : CurrentUser -> List Status -> CurrentUserRelation -> Account -> Html Msg
accountTimelineView currentUser statuses relationship account =
accountView currentUser account relationship <|
ul [ class "list-group" ] <|
List.map
(\s ->
@ -277,19 +325,29 @@ accountTimelineView label statuses account =
statuses
accountFollowView : String -> List Account -> Account -> Html Msg
accountFollowView label accounts account =
accountView label "user" account <|
accountFollowView :
CurrentUser
-> List Account
-> List Relationship
-> CurrentUserRelation
-> Account
-> Html Msg
accountFollowView currentUser accounts relationships relationship account =
accountView currentUser account relationship <|
ul [ class "list-group" ] <|
List.map
(\account ->
li [ class "list-group-item status" ]
[ followView account ]
[ followView
currentUser
(find (\r -> r.id == account.id) relationships)
account
]
)
accounts
statusActionsView : Status -> Account -> Html Msg
statusActionsView : Status -> CurrentUser -> Html Msg
statusActionsView status currentUser =
let
sourceStatus =
@ -353,7 +411,7 @@ statusActionsView status currentUser =
]
statusEntryView : String -> String -> Account -> Status -> Html Msg
statusEntryView : String -> String -> CurrentUser -> Status -> Html Msg
statusEntryView context className currentUser status =
let
nsfwClass =
@ -370,7 +428,7 @@ statusEntryView context className currentUser status =
]
timelineView : String -> String -> String -> Account -> List Status -> Html Msg
timelineView : String -> String -> String -> CurrentUser -> List Status -> Html Msg
timelineView label iconName context currentUser statuses =
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
@ -396,7 +454,7 @@ notificationHeading accounts str iconType =
]
notificationStatusView : String -> Account -> Status -> NotificationAggregate -> Html Msg
notificationStatusView : String -> CurrentUser -> Status -> NotificationAggregate -> Html Msg
notificationStatusView context currentUser status { type_, accounts } =
div [ class <| "notification " ++ type_ ]
[ case type_ of
@ -413,7 +471,7 @@ notificationStatusView context currentUser status { type_, accounts } =
]
notificationFollowView : Account -> NotificationAggregate -> Html Msg
notificationFollowView : CurrentUser -> NotificationAggregate -> Html Msg
notificationFollowView currentUser { accounts } =
let
profileView account =
@ -434,7 +492,7 @@ notificationFollowView currentUser { accounts } =
]
notificationEntryView : Account -> NotificationAggregate -> Html Msg
notificationEntryView : CurrentUser -> NotificationAggregate -> Html Msg
notificationEntryView currentUser notification =
li [ class "list-group-item" ]
[ case notification.status of
@ -446,7 +504,7 @@ notificationEntryView currentUser notification =
]
notificationListView : Account -> List NotificationAggregate -> Html Msg
notificationListView : CurrentUser -> List NotificationAggregate -> Html Msg
notificationListView currentUser notifications =
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
@ -482,7 +540,7 @@ draftReplyToView draft =
text ""
currentUserView : Maybe Account -> Html Msg
currentUserView : Maybe CurrentUser -> Html Msg
currentUserView currentUser =
case currentUser of
Just currentUser ->
@ -611,7 +669,7 @@ draftView { draft, currentUser } =
]
threadView : Account -> Thread -> Html Msg
threadView : CurrentUser -> Thread -> Html Msg
threadView currentUser thread =
let
statuses =
@ -697,13 +755,27 @@ homepageView model =
model.globalTimeline
AccountView account ->
accountTimelineView "Account" model.accountTimeline account
accountTimelineView
currentUser
model.accountTimeline
model.accountRelationship
account
AccountFollowersView account followers ->
accountFollowView "Account followers" model.accountFollowers account
accountFollowView
currentUser
model.accountFollowers
model.accountRelationships
model.accountRelationship
account
AccountFollowingView account following ->
accountFollowView "Account following" model.accountFollowing account
accountFollowView
currentUser
model.accountFollowing
model.accountRelationships
model.accountRelationship
account
ThreadView thread ->
threadView currentUser thread