Fix #81: Account followers and followings (#104)

This commit is contained in:
Nicolas Perriault 2017-04-29 18:05:06 +02:00 committed by GitHub
parent 017f07d45d
commit 57f7f0cc7c
9 changed files with 621 additions and 443 deletions

View File

@ -92,12 +92,29 @@ body {
margin-top: 2px; margin-top: 2px;
} }
.username { .status .username,
.current-user .username {
font-weight: bold; font-weight: bold;
margin-left: 65px; margin-left: 65px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.follow-entry {
min-height: 38px;
}
.follow-entry .avatar {
width: 38px;
height: 38px;
}
.follow-entry .username {
font-weight: normal;
font-size: 97%;
margin-left: 50px;
overflow: hidden;
text-overflow: ellipsis;
}
.acct { .acct {
font-size: 97%; font-size: 97%;
font-weight: normal; font-weight: normal;
@ -334,6 +351,17 @@ body {
padding: 15px; padding: 15px;
} }
.account-infos a {
color: #c8c8c8;
}
.account-infos a:hover,
.account-infos a:focus,
.account-infos a:active {
text-decoration: none;
color: #fff;
}
/* Viewer */ /* Viewer */
.viewer { .viewer {

231
src/Command.elm Normal file
View File

@ -0,0 +1,231 @@
module Command
exposing
( initCommands
, navigateToAuthUrl
, registerApp
, saveClient
, saveRegistration
, loadNotifications
, loadUserAccount
, loadAccount
, loadAccountInfo
, loadThread
, loadTimelines
, postStatus
, deleteStatus
, reblogStatus
, unreblogStatus
, favouriteStatus
, unfavouriteStatus
)
import Json.Encode as Encode
import Mastodon.Model exposing (..)
import Mastodon.Encoder
import Mastodon.Http
import Navigation
import Ports
import Types exposing (..)
initCommands : Maybe AppRegistration -> Maybe Client -> Maybe String -> Cmd Msg
initCommands registration client authCode =
Cmd.batch <|
case authCode of
Just authCode ->
case registration of
Just registration ->
[ Mastodon.Http.getAccessToken registration authCode
|> Mastodon.Http.send (MastodonEvent << AccessToken)
]
Nothing ->
[]
Nothing ->
[ loadUserAccount client, loadTimelines client ]
navigateToAuthUrl : AppRegistration -> Cmd Msg
navigateToAuthUrl registration =
Navigation.load <| Mastodon.Http.getAuthorizationUrl registration
registerApp : Model -> Cmd Msg
registerApp { server, location } =
let
appUrl =
location.origin ++ location.pathname
cleanServer =
if String.endsWith "/" server then
String.dropRight 1 server
else
server
in
Mastodon.Http.register
cleanServer
"tooty"
appUrl
"read write follow"
"https://github.com/n1k0/tooty"
|> Mastodon.Http.send (MastodonEvent << AppRegistered)
saveClient : Client -> Cmd Msg
saveClient client =
Mastodon.Encoder.clientEncoder client
|> Encode.encode 0
|> Ports.saveClient
saveRegistration : AppRegistration -> Cmd Msg
saveRegistration registration =
Mastodon.Encoder.registrationEncoder registration
|> Encode.encode 0
|> Ports.saveRegistration
loadNotifications : Maybe Client -> Cmd Msg
loadNotifications client =
case client of
Just client ->
Mastodon.Http.fetchNotifications client
|> Mastodon.Http.send (MastodonEvent << Notifications)
Nothing ->
Cmd.none
loadUserAccount : Maybe Client -> Cmd Msg
loadUserAccount client =
case client of
Just client ->
Mastodon.Http.userAccount client
|> Mastodon.Http.send (MastodonEvent << CurrentUser)
Nothing ->
Cmd.none
loadAccount : Maybe Client -> Int -> Cmd Msg
loadAccount client accountId =
case client of
Just client ->
Mastodon.Http.fetchAccount client accountId
|> Mastodon.Http.send (MastodonEvent << AccountReceived)
Nothing ->
Cmd.none
loadAccountInfo : Maybe Client -> Int -> Cmd Msg
loadAccountInfo 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)
]
Nothing ->
Cmd.none
loadThread : Maybe Client -> Status -> Cmd Msg
loadThread client status =
case client of
Just client ->
Mastodon.Http.context client status.id
|> Mastodon.Http.send (MastodonEvent << (ContextLoaded status))
Nothing ->
Cmd.none
loadTimelines : Maybe Client -> Cmd Msg
loadTimelines client =
case client of
Just client ->
Cmd.batch
[ Mastodon.Http.fetchUserTimeline client
|> Mastodon.Http.send (MastodonEvent << UserTimeline)
, Mastodon.Http.fetchLocalTimeline client
|> Mastodon.Http.send (MastodonEvent << LocalTimeline)
, Mastodon.Http.fetchGlobalTimeline client
|> Mastodon.Http.send (MastodonEvent << GlobalTimeline)
, loadNotifications <| Just client
]
Nothing ->
Cmd.none
postStatus : Maybe Client -> StatusRequestBody -> Cmd Msg
postStatus client draft =
case client of
Just client ->
Mastodon.Http.postStatus client draft
|> Mastodon.Http.send (MastodonEvent << StatusPosted)
Nothing ->
Cmd.none
deleteStatus : Maybe Client -> Int -> Cmd Msg
deleteStatus client id =
case client of
Just client ->
Mastodon.Http.deleteStatus client id
|> Mastodon.Http.send (MastodonEvent << StatusDeleted)
Nothing ->
Cmd.none
reblogStatus : Maybe Client -> Int -> Cmd Msg
reblogStatus client statusId =
case client of
Just client ->
Mastodon.Http.reblog client statusId
|> Mastodon.Http.send (MastodonEvent << Reblogged)
Nothing ->
Cmd.none
unreblogStatus : Maybe Client -> Int -> Cmd Msg
unreblogStatus client statusId =
case client of
Just client ->
Mastodon.Http.unreblog client statusId
|> Mastodon.Http.send (MastodonEvent << Unreblogged)
Nothing ->
Cmd.none
favouriteStatus : Maybe Client -> Int -> Cmd Msg
favouriteStatus client statusId =
case client of
Just client ->
Mastodon.Http.favourite client statusId
|> Mastodon.Http.send (MastodonEvent << FavoriteAdded)
Nothing ->
Cmd.none
unfavouriteStatus : Maybe Client -> Int -> Cmd Msg
unfavouriteStatus client statusId =
case client of
Just client ->
Mastodon.Http.unfavourite client statusId
|> Mastodon.Http.send (MastodonEvent << FavoriteRemoved)
Nothing ->
Cmd.none

View File

@ -2,7 +2,8 @@ module Main exposing (..)
import Navigation import Navigation
import View exposing (view) import View exposing (view)
import Model exposing (Flags, Model, Msg(..), init, update, subscriptions) import Model exposing (..)
import Types exposing (..)
main : Program Flags Model Msg main : Program Flags Model Msg

View File

@ -6,6 +6,8 @@ module Mastodon.ApiUrl
, userAccount , userAccount
, account , account
, accountTimeline , accountTimeline
, followers
, following
, status , status
, homeTimeline , homeTimeline
, publicTimeline , publicTimeline
@ -54,6 +56,16 @@ userAccount server =
server ++ accounts ++ "verify_credentials" server ++ accounts ++ "verify_credentials"
followers : Int -> String
followers id =
(account id) ++ "/followers"
following : Int -> String
following id =
(account id) ++ "/following"
homeTimeline : String homeTimeline : String
homeTimeline = homeTimeline =
"/api/v1/timelines/home" "/api/v1/timelines/home"

View File

@ -11,6 +11,8 @@ module Mastodon.Http
, getAccessToken , getAccessToken
, fetchAccount , fetchAccount
, fetchAccountTimeline , fetchAccountTimeline
, fetchAccountFollowers
, fetchAccountFollowing
, fetchLocalTimeline , fetchLocalTimeline
, fetchNotifications , fetchNotifications
, fetchGlobalTimeline , fetchGlobalTimeline
@ -134,6 +136,16 @@ fetchNotifications client =
fetch client (ApiUrl.notifications) <| Decode.list notificationDecoder fetch client (ApiUrl.notifications) <| Decode.list notificationDecoder
fetchAccountFollowers : Client -> Int -> Request (List Account)
fetchAccountFollowers client accountId =
fetch client (ApiUrl.followers accountId) <| Decode.list accountDecoder
fetchAccountFollowing : Client -> Int -> Request (List Account)
fetchAccountFollowing client accountId =
fetch client (ApiUrl.following accountId) <| Decode.list accountDecoder
userAccount : Client -> Request Account userAccount : Client -> Request Account
userAccount client = userAccount client =
HttpBuilder.get (ApiUrl.userAccount client.server) HttpBuilder.get (ApiUrl.userAccount client.server)

View File

@ -1,17 +1,15 @@
module Model exposing (..) module Model exposing (..)
import Command
import Dom import Dom
import Json.Encode as Encode import Dom.Scroll
import Navigation import Navigation
import Mastodon.Decoder import Mastodon.Decoder
import Mastodon.Encoder
import Mastodon.Helper import Mastodon.Helper
import Mastodon.Http import Mastodon.Model exposing (..)
import Mastodon.Model
import Mastodon.WebSocket import Mastodon.WebSocket
import Ports
import Task import Task
import Dom.Scroll import Types exposing (..)
maxBuffer : Int maxBuffer : Int
@ -20,124 +18,6 @@ maxBuffer =
100 100
type alias Flags =
{ client : Maybe Mastodon.Model.Client
, registration : Maybe Mastodon.Model.AppRegistration
}
type DraftMsg
= ClearDraft
| UpdateSensitive Bool
| UpdateSpoiler String
| UpdateStatus String
| UpdateVisibility String
| UpdateReplyTo Mastodon.Model.Status
| ToggleSpoiler Bool
type ViewerMsg
= CloseViewer
| OpenViewer (List Mastodon.Model.Attachment) Mastodon.Model.Attachment
type MastodonMsg
= AccessToken (Result Mastodon.Model.Error Mastodon.Model.AccessTokenResult)
| AppRegistered (Result Mastodon.Model.Error Mastodon.Model.AppRegistration)
| ContextLoaded Mastodon.Model.Status (Result Mastodon.Model.Error Mastodon.Model.Context)
| CurrentUser (Result Mastodon.Model.Error Mastodon.Model.Account)
| FavoriteAdded (Result Mastodon.Model.Error Mastodon.Model.Status)
| FavoriteRemoved (Result Mastodon.Model.Error Mastodon.Model.Status)
| LocalTimeline (Result Mastodon.Model.Error (List Mastodon.Model.Status))
| Notifications (Result Mastodon.Model.Error (List Mastodon.Model.Notification))
| GlobalTimeline (Result Mastodon.Model.Error (List Mastodon.Model.Status))
| Reblogged (Result Mastodon.Model.Error Mastodon.Model.Status)
| StatusDeleted (Result Mastodon.Model.Error Int)
| StatusPosted (Result Mastodon.Model.Error Mastodon.Model.Status)
| Unreblogged (Result Mastodon.Model.Error Mastodon.Model.Status)
| Account (Result Mastodon.Model.Error Mastodon.Model.Account)
| AccountTimeline (Result Mastodon.Model.Error (List Mastodon.Model.Status))
| UserTimeline (Result Mastodon.Model.Error (List Mastodon.Model.Status))
type WebSocketMsg
= NewWebsocketUserMessage String
| NewWebsocketGlobalMessage String
| NewWebsocketLocalMessage String
type Msg
= AddFavorite Int
| ClearOpenedAccount
| CloseThread
| DomResult (Result Dom.Error ())
| DeleteStatus Int
| DraftEvent DraftMsg
| LoadAccount Int
| MastodonEvent MastodonMsg
| NoOp
| OpenThread Mastodon.Model.Status
| Reblog Int
| Register
| RemoveFavorite Int
| ServerChange String
| SubmitDraft
| UrlChange Navigation.Location
| UseGlobalTimeline Bool
| Unreblog Int
| ViewerEvent ViewerMsg
| WebSocketEvent WebSocketMsg
| ScrollColumn String
type alias Draft =
{ status : String
, in_reply_to : Maybe Mastodon.Model.Status
, spoiler_text : Maybe String
, sensitive : Bool
, visibility : String
}
type alias Thread =
{ status : Mastodon.Model.Status
, context : Mastodon.Model.Context
}
type alias Viewer =
{ attachments : List Mastodon.Model.Attachment
, attachment : Mastodon.Model.Attachment
}
type CurrentView
= -- Basically, what we should be displaying in the fourth column
AccountView Mastodon.Model.Account
| ThreadView Thread
| LocalTimelineView
| GlobalTimelineView
type alias Model =
{ server : String
, registration : Maybe Mastodon.Model.AppRegistration
, client : Maybe Mastodon.Model.Client
, userTimeline : List Mastodon.Model.Status
, localTimeline : List Mastodon.Model.Status
, globalTimeline : List Mastodon.Model.Status
, accountTimeline : List Mastodon.Model.Status
, notifications : List Mastodon.Model.NotificationAggregate
, draft : Draft
, errors : List String
, location : Navigation.Location
, useGlobalTimeline : Bool
, viewer : Maybe Viewer
, currentUser : Maybe Mastodon.Model.Account
, currentView : CurrentView
}
extractAuthCode : Navigation.Location -> Maybe String extractAuthCode : Navigation.Location -> Maybe String
extractAuthCode { search } = extractAuthCode { search } =
case (String.split "?code=" search) of case (String.split "?code=" search) of
@ -171,6 +51,8 @@ init flags location =
, localTimeline = [] , localTimeline = []
, globalTimeline = [] , globalTimeline = []
, accountTimeline = [] , accountTimeline = []
, accountFollowers = []
, accountFollowing = []
, notifications = [] , notifications = []
, draft = defaultDraft , draft = defaultDraft
, errors = [] , errors = []
@ -180,100 +62,7 @@ init flags location =
, currentView = LocalTimelineView , currentView = LocalTimelineView
, currentUser = Nothing , currentUser = Nothing
} }
! [ initCommands flags.registration flags.client authCode ] ! [ Command.initCommands flags.registration flags.client authCode ]
initCommands : Maybe Mastodon.Model.AppRegistration -> Maybe Mastodon.Model.Client -> Maybe String -> Cmd Msg
initCommands registration client authCode =
Cmd.batch <|
case authCode of
Just authCode ->
case registration of
Just registration ->
[ Mastodon.Http.getAccessToken registration authCode
|> Mastodon.Http.send (MastodonEvent << AccessToken)
]
Nothing ->
[]
Nothing ->
[ loadUserAccount client, loadTimelines client ]
registerApp : Model -> Cmd Msg
registerApp { server, location } =
let
appUrl =
location.origin ++ location.pathname
cleanServer =
if String.endsWith "/" server then
String.dropRight 1 server
else
server
in
Mastodon.Http.register
cleanServer
"tooty"
appUrl
"read write follow"
"https://github.com/n1k0/tooty"
|> Mastodon.Http.send (MastodonEvent << AppRegistered)
saveClient : Mastodon.Model.Client -> Cmd Msg
saveClient client =
Mastodon.Encoder.clientEncoder client
|> Encode.encode 0
|> Ports.saveClient
saveRegistration : Mastodon.Model.AppRegistration -> Cmd Msg
saveRegistration registration =
Mastodon.Encoder.registrationEncoder registration
|> Encode.encode 0
|> Ports.saveRegistration
loadNotifications : Maybe Mastodon.Model.Client -> Cmd Msg
loadNotifications client =
case client of
Just client ->
Mastodon.Http.fetchNotifications client
|> Mastodon.Http.send (MastodonEvent << Notifications)
Nothing ->
Cmd.none
loadUserAccount : Maybe Mastodon.Model.Client -> Cmd Msg
loadUserAccount client =
case client of
Just client ->
Mastodon.Http.userAccount client
|> Mastodon.Http.send (MastodonEvent << CurrentUser)
Nothing ->
Cmd.none
loadTimelines : Maybe Mastodon.Model.Client -> Cmd Msg
loadTimelines client =
case client of
Just client ->
Cmd.batch
[ Mastodon.Http.fetchUserTimeline client
|> Mastodon.Http.send (MastodonEvent << UserTimeline)
, Mastodon.Http.fetchLocalTimeline client
|> Mastodon.Http.send (MastodonEvent << LocalTimeline)
, Mastodon.Http.fetchGlobalTimeline client
|> Mastodon.Http.send (MastodonEvent << GlobalTimeline)
, loadNotifications <| Just client
]
Nothing ->
Cmd.none
preferredTimeline : Model -> CurrentView preferredTimeline : Model -> CurrentView
@ -289,35 +78,23 @@ truncate entries =
List.take maxBuffer entries List.take maxBuffer entries
postStatus : Mastodon.Model.Client -> Mastodon.Model.StatusRequestBody -> Cmd Msg errorText : Error -> String
postStatus client draft =
Mastodon.Http.postStatus client draft
|> Mastodon.Http.send (MastodonEvent << StatusPosted)
deleteStatus : Mastodon.Model.Client -> Int -> Cmd Msg
deleteStatus client id =
Mastodon.Http.deleteStatus client id
|> Mastodon.Http.send (MastodonEvent << StatusDeleted)
errorText : Mastodon.Model.Error -> String
errorText error = errorText error =
case error of case error of
Mastodon.Model.MastodonError statusCode statusMsg errorMsg -> MastodonError statusCode statusMsg errorMsg ->
"HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg "HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg
Mastodon.Model.ServerError statusCode statusMsg errorMsg -> ServerError statusCode statusMsg errorMsg ->
"HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg "HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg
Mastodon.Model.TimeoutError -> TimeoutError ->
"Request timed out." "Request timed out."
Mastodon.Model.NetworkError -> NetworkError ->
"Unreachable host." "Unreachable host."
toStatusRequestBody : Draft -> Mastodon.Model.StatusRequestBody toStatusRequestBody : Draft -> StatusRequestBody
toStatusRequestBody draft = toStatusRequestBody draft =
{ status = draft.status { status = draft.status
, in_reply_to_id = , in_reply_to_id =
@ -333,7 +110,7 @@ toStatusRequestBody draft =
} }
updateTimelinesWithBoolFlag : Int -> Bool -> (Mastodon.Model.Status -> Mastodon.Model.Status) -> Model -> Model updateTimelinesWithBoolFlag : Int -> Bool -> (Status -> Status) -> Model -> Model
updateTimelinesWithBoolFlag statusId flag statusUpdater model = updateTimelinesWithBoolFlag statusId flag statusUpdater model =
let let
update flag status = update flag status =
@ -351,15 +128,17 @@ updateTimelinesWithBoolFlag statusId flag statusUpdater model =
processFavourite : Int -> Bool -> Model -> Model processFavourite : Int -> Bool -> Model -> Model
processFavourite statusId flag model = processFavourite statusId flag model =
-- TODO: update notifications too
updateTimelinesWithBoolFlag statusId flag (\s -> { s | favourited = Just flag }) model updateTimelinesWithBoolFlag statusId flag (\s -> { s | favourited = Just flag }) model
processReblog : Int -> Bool -> Model -> Model processReblog : Int -> Bool -> Model -> Model
processReblog statusId flag model = processReblog statusId flag model =
-- TODO: update notifications too
updateTimelinesWithBoolFlag statusId flag (\s -> { s | reblogged = Just flag }) model updateTimelinesWithBoolFlag statusId flag (\s -> { s | reblogged = Just flag }) model
deleteStatusFromTimeline : Int -> List Mastodon.Model.Status -> List Mastodon.Model.Status deleteStatusFromTimeline : Int -> List Status -> List Status
deleteStatusFromTimeline statusId timeline = deleteStatusFromTimeline statusId timeline =
timeline timeline
|> List.filter |> List.filter
@ -371,7 +150,7 @@ deleteStatusFromTimeline statusId timeline =
) )
updateDraft : DraftMsg -> Mastodon.Model.Account -> Draft -> ( Draft, Cmd Msg ) updateDraft : DraftMsg -> Account -> Draft -> ( Draft, Cmd Msg )
updateDraft draftMsg currentUser draft = updateDraft draftMsg currentUser draft =
case draftMsg of case draftMsg of
ClearDraft -> ClearDraft ->
@ -432,12 +211,12 @@ processMastodonEvent msg model =
Ok { server, accessToken } -> Ok { server, accessToken } ->
let let
client = client =
Mastodon.Model.Client server accessToken Client server accessToken
in in
{ model | client = Just client } { model | client = Just client }
! [ loadTimelines <| Just client ! [ Command.loadTimelines <| Just client
, Navigation.modifyUrl model.location.pathname , Navigation.modifyUrl model.location.pathname
, saveClient client , Command.saveClient client
] ]
Err error -> Err error ->
@ -447,8 +226,8 @@ processMastodonEvent msg model =
case result of case result of
Ok registration -> Ok registration ->
{ model | registration = Just registration } { model | registration = Just registration }
! [ saveRegistration registration ! [ Command.saveRegistration registration
, Navigation.load <| Mastodon.Http.getAuthorizationUrl registration , Command.navigateToAuthUrl registration
] ]
Err error -> Err error ->
@ -457,11 +236,7 @@ processMastodonEvent msg model =
ContextLoaded status result -> ContextLoaded status result ->
case result of case result of
Ok context -> Ok context ->
let { model | currentView = ThreadView (Thread status context) } ! []
thread =
Thread status context
in
{ model | currentView = ThreadView thread } ! []
Err error -> Err error ->
{ model { model
@ -481,7 +256,7 @@ processMastodonEvent msg model =
FavoriteAdded result -> FavoriteAdded result ->
case result of case result of
Ok status -> Ok status ->
processFavourite status.id True model ! [ loadNotifications model.client ] model ! [ Command.loadNotifications model.client ]
Err error -> Err error ->
{ model | errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
@ -489,7 +264,7 @@ processMastodonEvent msg model =
FavoriteRemoved result -> FavoriteRemoved result ->
case result of case result of
Ok status -> Ok status ->
processFavourite status.id False model ! [ loadNotifications model.client ] model ! [ Command.loadNotifications model.client ]
Err error -> Err error ->
{ model | errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
@ -500,7 +275,7 @@ processMastodonEvent msg model =
{ model | localTimeline = localTimeline } ! [] { model | localTimeline = localTimeline } ! []
Err error -> Err error ->
{ model | localTimeline = [], errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
Notifications result -> Notifications result ->
case result of case result of
@ -508,7 +283,7 @@ processMastodonEvent msg model =
{ model | notifications = Mastodon.Helper.aggregateNotifications notifications } ! [] { model | notifications = Mastodon.Helper.aggregateNotifications notifications } ! []
Err error -> Err error ->
{ model | notifications = [], errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
GlobalTimeline result -> GlobalTimeline result ->
case result of case result of
@ -516,12 +291,12 @@ processMastodonEvent msg model =
{ model | globalTimeline = globalTimeline } ! [] { model | globalTimeline = globalTimeline } ! []
Err error -> Err error ->
{ model | globalTimeline = [], errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
Reblogged result -> Reblogged result ->
case result of case result of
Ok status -> Ok status ->
model ! [ loadNotifications model.client ] model ! [ Command.loadNotifications model.client ]
Err error -> Err error ->
{ model | errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
@ -545,15 +320,16 @@ processMastodonEvent msg model =
Unreblogged result -> Unreblogged result ->
case result of case result of
Ok status -> Ok status ->
model ! [ loadNotifications model.client ] model ! [ Command.loadNotifications model.client ]
Err error -> Err error ->
{ model | errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
Account result -> AccountReceived result ->
case result of case result of
Ok account -> Ok account ->
{ model | currentView = AccountView account } ! [] { model | currentView = AccountView account }
! [ Command.loadAccountInfo model.client account.id ]
Err error -> Err error ->
{ model { model
@ -570,13 +346,29 @@ processMastodonEvent msg model =
Err error -> Err error ->
{ model | errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
AccountFollowers result ->
case result of
Ok statuses ->
{ model | accountFollowers = statuses } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountFollowing result ->
case result of
Ok statuses ->
{ model | accountFollowing = statuses } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
UserTimeline result -> UserTimeline result ->
case result of case result of
Ok userTimeline -> Ok userTimeline ->
{ model | userTimeline = userTimeline } ! [] { model | userTimeline = userTimeline } ! []
Err error -> Err error ->
{ model | userTimeline = [], errors = (errorText error) :: model.errors } ! [] { model | errors = (errorText error) :: model.errors } ! []
processWebSocketMsg : WebSocketMsg -> Model -> ( Model, Cmd Msg ) processWebSocketMsg : WebSocketMsg -> Model -> ( Model, Cmd Msg )
@ -672,9 +464,6 @@ update msg model =
NoOp -> NoOp ->
model ! [] model ! []
DomResult result ->
model ! []
MastodonEvent msg -> MastodonEvent msg ->
let let
( newModel, commands ) = ( newModel, commands ) =
@ -696,76 +485,28 @@ update msg model =
model ! [] model ! []
Register -> Register ->
model ! [ registerApp model ] model ! [ Command.registerApp model ]
OpenThread status -> OpenThread status ->
case model.client of model ! [ Command.loadThread model.client status ]
Just client ->
model
! [ Mastodon.Http.context client status.id
|> Mastodon.Http.send (MastodonEvent << (ContextLoaded status))
]
Nothing ->
model ! []
CloseThread -> CloseThread ->
{ model | currentView = preferredTimeline model } ! [] { model | currentView = preferredTimeline model } ! []
DeleteStatus id -> DeleteStatus id ->
case model.client of model ! [ Command.deleteStatus model.client id ]
Just client ->
model ! [ deleteStatus client id ]
Nothing -> ReblogStatus id ->
model ! [] processReblog id True model ! [ Command.reblogStatus model.client id ]
Reblog id -> UnreblogStatus id ->
-- Note: The case of reblogging is specific as it seems the server processReblog id False model ! [ Command.unreblogStatus model.client id ]
-- response takes a lot of time to be received by the client, so we
-- perform optimistic updates here.
case model.client of
Just client ->
processReblog id True model
! [ Mastodon.Http.reblog client id
|> Mastodon.Http.send (MastodonEvent << Reblogged)
]
Nothing ->
model ! []
Unreblog id ->
case model.client of
Just client ->
processReblog id False model
! [ Mastodon.Http.unfavourite client id
|> Mastodon.Http.send (MastodonEvent << Unreblogged)
]
Nothing ->
model ! []
AddFavorite id -> AddFavorite id ->
model processFavourite id True model ! [ Command.favouriteStatus model.client id ]
! case model.client of
Just client ->
[ Mastodon.Http.favourite client id
|> Mastodon.Http.send (MastodonEvent << FavoriteAdded)
]
Nothing ->
[]
RemoveFavorite id -> RemoveFavorite id ->
model processFavourite id False model ! [ Command.unfavouriteStatus model.client id ]
! case model.client of
Just client ->
[ Mastodon.Http.unfavourite client id
|> Mastodon.Http.send (MastodonEvent << FavoriteRemoved)
]
Nothing ->
[]
DraftEvent draftMsg -> DraftEvent draftMsg ->
case model.currentUser of case model.currentUser of
@ -787,31 +528,20 @@ update msg model =
{ model | viewer = viewer } ! [ commands ] { model | viewer = viewer } ! [ commands ]
SubmitDraft -> SubmitDraft ->
model model ! [ Command.postStatus model.client <| toStatusRequestBody model.draft ]
! case model.client of
Just client ->
[ postStatus client <| toStatusRequestBody model.draft ]
Nothing ->
[]
LoadAccount accountId -> LoadAccount accountId ->
{-
@TODO
When requesting a user profile, we should load a new "page"
so that the URL in the browser matches the user displayed
-}
{ model | currentView = preferredTimeline model } { model | currentView = preferredTimeline model }
! case model.client of ! [ Command.loadAccount model.client accountId ]
Just client ->
[ Mastodon.Http.fetchAccount client accountId
|> Mastodon.Http.send (MastodonEvent << Account)
, Mastodon.Http.fetchAccountTimeline client accountId
|> Mastodon.Http.send (MastodonEvent << AccountTimeline)
]
Nothing -> ViewAccountFollowers account ->
[] { model | currentView = AccountFollowersView account model.accountFollowers } ! []
ViewAccountFollowing account ->
{ model | currentView = AccountFollowingView account model.accountFollowing } ! []
ViewAccountStatuses account ->
{ model | currentView = AccountView account } ! []
UseGlobalTimeline flag -> UseGlobalTimeline flag ->
let let
@ -820,11 +550,17 @@ update msg model =
in in
{ model | currentView = preferredTimeline newModel } ! [] { model | currentView = preferredTimeline newModel } ! []
ClearOpenedAccount -> CloseAccount ->
{ model | currentView = preferredTimeline model } ! [] { model
| currentView = preferredTimeline model
, accountTimeline = []
, accountFollowing = []
, accountFollowers = []
}
! []
ScrollColumn context -> ScrollColumn context ->
model ! [ Task.attempt DomResult <| Dom.Scroll.toTop context ] model ! [ Task.attempt (always NoOp) <| Dom.Scroll.toTop context ]
subscriptions : Model -> Sub Msg subscriptions : Model -> Sub Msg

138
src/Types.elm Normal file
View File

@ -0,0 +1,138 @@
module Types exposing (..)
import Mastodon.Model exposing (..)
import Navigation
type alias Flags =
{ client : Maybe Client
, registration : Maybe AppRegistration
}
type DraftMsg
= ClearDraft
| UpdateSensitive Bool
| UpdateSpoiler String
| UpdateStatus String
| UpdateVisibility String
| UpdateReplyTo Status
| ToggleSpoiler Bool
type ViewerMsg
= CloseViewer
| OpenViewer (List Attachment) Attachment
type MastodonMsg
= AccessToken (Result Error AccessTokenResult)
| AccountFollowers (Result Error (List Account))
| AccountFollowing (Result Error (List Account))
| AccountReceived (Result Error Account)
| AccountTimeline (Result Error (List Status))
| AppRegistered (Result Error AppRegistration)
| ContextLoaded Status (Result Error Context)
| CurrentUser (Result Error Account)
| FavoriteAdded (Result Error Status)
| FavoriteRemoved (Result Error Status)
| GlobalTimeline (Result Error (List Status))
| LocalTimeline (Result Error (List Status))
| Notifications (Result Error (List Notification))
| Reblogged (Result Error Status)
| StatusDeleted (Result Error Int)
| StatusPosted (Result Error Status)
| Unreblogged (Result Error Status)
| UserTimeline (Result Error (List Status))
type WebSocketMsg
= NewWebsocketGlobalMessage String
| NewWebsocketLocalMessage String
| NewWebsocketUserMessage String
type Msg
= AddFavorite Int
| CloseAccount
| CloseThread
| DeleteStatus Int
| DraftEvent DraftMsg
| LoadAccount Int
| MastodonEvent MastodonMsg
| NoOp
| OpenThread Status
| ReblogStatus Int
| Register
| RemoveFavorite Int
| ScrollColumn String
| ServerChange String
| SubmitDraft
| UrlChange Navigation.Location
| UseGlobalTimeline Bool
| UnreblogStatus Int
| ViewAccountFollowing Account
| ViewAccountFollowers Account
| ViewAccountStatuses Account
| ViewerEvent ViewerMsg
| WebSocketEvent WebSocketMsg
type alias AccountViewInfo =
{ account : Account
, timeline : List Status
, folowers : List Account
, following : List Account
}
type alias Draft =
{ status : String
, in_reply_to : Maybe Status
, spoiler_text : Maybe String
, sensitive : Bool
, visibility : String
}
type alias Thread =
{ status : Status
, context : Context
}
type alias Viewer =
{ attachments : List Attachment
, attachment : Attachment
}
type CurrentView
= -- Basically, what we should be displaying in the fourth column
AccountFollowersView Account (List Account)
| AccountFollowingView Account (List Account)
| AccountView Account
| GlobalTimelineView
| LocalTimelineView
| ThreadView Thread
type alias Model =
{ server : String
, registration : Maybe AppRegistration
, client : Maybe Client
, userTimeline : List Status
, localTimeline : List Status
, globalTimeline : List Status
, accountTimeline : List Status
, accountFollowers : List Account
, accountFollowing : List Account
, notifications : List NotificationAggregate
, draft : Draft
, errors : List String
, location : Navigation.Location
, useGlobalTimeline : Bool
, viewer : Maybe Viewer
, currentUser : Maybe Account
, currentView : CurrentView
}

View File

@ -6,8 +6,8 @@ import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import List.Extra exposing (elemIndex, getAt) import List.Extra exposing (elemIndex, getAt)
import Mastodon.Helper import Mastodon.Helper
import Mastodon.Model import Mastodon.Model exposing (..)
import Model exposing (..) import Types exposing (..)
import ViewHelper exposing (..) import ViewHelper exposing (..)
import Date import Date
import Date.Extra.Config.Config_en_au as DateEn import Date.Extra.Config.Config_en_au as DateEn
@ -66,7 +66,7 @@ icon name =
i [ class <| "glyphicon glyphicon-" ++ name ] [] i [ class <| "glyphicon glyphicon-" ++ name ] []
accountLink : Mastodon.Model.Account -> Html Msg accountLink : Account -> Html Msg
accountLink account = accountLink account =
a a
[ href account.url [ href account.url
@ -75,7 +75,7 @@ accountLink account =
[ text <| "@" ++ account.username ] [ text <| "@" ++ account.username ]
accountAvatarLink : Mastodon.Model.Account -> Html Msg accountAvatarLink : Account -> Html Msg
accountAvatarLink account = accountAvatarLink account =
a a
[ href account.url [ href account.url
@ -85,12 +85,7 @@ accountAvatarLink account =
[ img [ class "avatar", src account.avatar ] [] ] [ img [ class "avatar", src account.avatar ] [] ]
attachmentPreview : attachmentPreview : String -> Maybe Bool -> List Attachment -> Attachment -> Html Msg
String
-> Maybe Bool
-> List Mastodon.Model.Attachment
-> Mastodon.Model.Attachment
-> Html Msg
attachmentPreview context sensitive attachments ({ url, preview_url } as attachment) = attachmentPreview context sensitive attachments ({ url, preview_url } as attachment) =
let let
nsfw = nsfw =
@ -133,7 +128,7 @@ attachmentPreview context sensitive attachments ({ url, preview_url } as attachm
[ media ] [ media ]
attachmentListView : String -> Mastodon.Model.Status -> Html Msg attachmentListView : String -> Status -> Html Msg
attachmentListView context { media_attachments, sensitive } = attachmentListView context { media_attachments, sensitive } =
case media_attachments of case media_attachments of
[] -> [] ->
@ -144,7 +139,7 @@ attachmentListView context { media_attachments, sensitive } =
List.map (attachmentPreview context sensitive attachments) attachments List.map (attachmentPreview context sensitive attachments) attachments
statusContentView : String -> Mastodon.Model.Status -> Html Msg statusContentView : String -> Status -> Html Msg
statusContentView context status = statusContentView context status =
case status.spoiler_text of case status.spoiler_text of
"" -> "" ->
@ -170,7 +165,7 @@ statusContentView context status =
] ]
statusView : String -> Mastodon.Model.Status -> Html Msg statusView : String -> Status -> Html Msg
statusView context ({ account, content, media_attachments, reblog, mentions } as status) = statusView context ({ account, content, media_attachments, reblog, mentions } as status) =
let let
accountLinkAttributes = accountLinkAttributes =
@ -183,7 +178,7 @@ statusView context ({ account, content, media_attachments, reblog, mentions } as
] ]
in in
case reblog of case reblog of
Just (Mastodon.Model.Reblog reblog) -> Just (Reblog reblog) ->
div [ class "reblog" ] div [ class "reblog" ]
[ p [ class "status-info" ] [ p [ class "status-info" ]
[ icon "fire" [ icon "fire"
@ -207,58 +202,94 @@ statusView context ({ account, content, media_attachments, reblog, mentions } as
] ]
accountTimelineView : followView : Account -> Html Msg
Mastodon.Model.Account followView account =
-> List Mastodon.Model.Status div [ class "follow-entry" ]
-> String [ accountAvatarLink account
-> String , div [ class "username" ]
-> Html Msg [ strong []
accountTimelineView account statuses label iconName = [ text <|
div [ class "col-md-3 column" ] if account.display_name /= "" then
[ div [ class "panel panel-default" ] account.display_name
[ closeablePanelheading iconName label ClearOpenedAccount else
, div [ class "timeline" ] account.username
[ div
[ class "account-detail"
, style [ ( "background-image", "url('" ++ account.header ++ "')" ) ]
]
[ div [ class "opacity-layer" ]
[ 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 [])
]
]
, div [ class "row account-infos" ]
[ div [ class "col-md-4" ]
[ text "Statuses"
, br [] []
, text <| toString account.statuses_count
]
, div [ class "col-md-4" ]
[ text "Following"
, br [] []
, text <| toString account.following_count
]
, div [ class "col-md-4" ]
[ text "Followers"
, br [] []
, text <| toString account.followers_count
]
]
, ul [ class "list-group" ] <|
List.map
(\s ->
li [ class "list-group-item status" ]
[ statusView "account" s ]
)
statuses
] ]
, br [] []
, text <| "@" ++ account.acct
] ]
] ]
statusActionsView : Mastodon.Model.Status -> Mastodon.Model.Account -> Html Msg accountCounterLink : String -> Int -> (Account -> Msg) -> Account -> Html Msg
accountCounterLink label count tagger account =
a
[ href ""
, class "col-md-4"
, onClickWithPreventAndStop <| tagger account
]
[ text label
, br [] []
, text <| toString count
]
accountView : String -> String -> Account -> Html Msg -> Html Msg
accountView label iconName account 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
, div [ class "timeline" ]
[ div
[ class "account-detail"
, style [ ( "background-image", "url('" ++ account.header ++ "')" ) ]
]
[ div [ class "opacity-layer" ]
[ 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 [])
]
]
, div [ class "row account-infos" ]
[ accountCounterLink "Statuses" statuses_count ViewAccountStatuses account
, accountCounterLink "Following" following_count ViewAccountFollowing account
, accountCounterLink "Followers" followers_count ViewAccountFollowers account
]
, panelContent
]
]
]
accountTimelineView : String -> List Status -> Account -> Html Msg
accountTimelineView label statuses account =
accountView label "user" account <|
ul [ class "list-group" ] <|
List.map
(\s ->
li [ class "list-group-item status" ]
[ statusView "account" s ]
)
statuses
accountFollowView : String -> List Account -> Account -> Html Msg
accountFollowView label accounts account =
accountView label "user" account <|
ul [ class "list-group" ] <|
List.map
(\account ->
li [ class "list-group-item status" ]
[ followView account ]
)
accounts
statusActionsView : Status -> Account -> Html Msg
statusActionsView status currentUser = statusActionsView status currentUser =
let let
sourceStatus = sourceStatus =
@ -270,10 +301,10 @@ statusActionsView status currentUser =
( reblogClasses, reblogEvent ) = ( reblogClasses, reblogEvent ) =
case status.reblogged of case status.reblogged of
Just True -> Just True ->
( baseBtnClasses ++ " reblogged", Unreblog sourceStatus.id ) ( baseBtnClasses ++ " reblogged", UnreblogStatus sourceStatus.id )
_ -> _ ->
( baseBtnClasses, Reblog sourceStatus.id ) ( baseBtnClasses, ReblogStatus sourceStatus.id )
( favClasses, favEvent ) = ( favClasses, favEvent ) =
case status.favourited of case status.favourited of
@ -322,7 +353,7 @@ statusActionsView status currentUser =
] ]
statusEntryView : String -> String -> Mastodon.Model.Account -> Mastodon.Model.Status -> Html Msg statusEntryView : String -> String -> Account -> Status -> Html Msg
statusEntryView context className currentUser status = statusEntryView context className currentUser status =
let let
nsfwClass = nsfwClass =
@ -339,13 +370,7 @@ statusEntryView context className currentUser status =
] ]
timelineView : timelineView : String -> String -> String -> Account -> List Status -> Html Msg
String
-> String
-> String
-> Mastodon.Model.Account
-> List Mastodon.Model.Status
-> Html Msg
timelineView label iconName context currentUser statuses = timelineView label iconName context currentUser statuses =
div [ class "col-md-3 column" ] div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ] [ div [ class "panel panel-default" ]
@ -358,7 +383,7 @@ timelineView label iconName context currentUser statuses =
] ]
notificationHeading : List Mastodon.Model.Account -> String -> String -> Html Msg notificationHeading : List Account -> String -> String -> Html Msg
notificationHeading accounts str iconType = notificationHeading accounts str iconType =
div [ class "status-info" ] div [ class "status-info" ]
[ div [ class "avatars" ] <| List.map accountAvatarLink accounts [ div [ class "avatars" ] <| List.map accountAvatarLink accounts
@ -371,12 +396,7 @@ notificationHeading accounts str iconType =
] ]
notificationStatusView : notificationStatusView : String -> Account -> Status -> NotificationAggregate -> Html Msg
String
-> Mastodon.Model.Account
-> Mastodon.Model.Status
-> Mastodon.Model.NotificationAggregate
-> Html Msg
notificationStatusView context currentUser status { type_, accounts } = notificationStatusView context currentUser status { type_, accounts } =
div [ class <| "notification " ++ type_ ] div [ class <| "notification " ++ type_ ]
[ case type_ of [ case type_ of
@ -393,7 +413,7 @@ notificationStatusView context currentUser status { type_, accounts } =
] ]
notificationFollowView : Mastodon.Model.Account -> Mastodon.Model.NotificationAggregate -> Html Msg notificationFollowView : Account -> NotificationAggregate -> Html Msg
notificationFollowView currentUser { accounts } = notificationFollowView currentUser { accounts } =
let let
profileView account = profileView account =
@ -414,10 +434,7 @@ notificationFollowView currentUser { accounts } =
] ]
notificationEntryView : notificationEntryView : Account -> NotificationAggregate -> Html Msg
Mastodon.Model.Account
-> Mastodon.Model.NotificationAggregate
-> Html Msg
notificationEntryView currentUser notification = notificationEntryView currentUser notification =
li [ class "list-group-item" ] li [ class "list-group-item" ]
[ case notification.status of [ case notification.status of
@ -429,7 +446,7 @@ notificationEntryView currentUser notification =
] ]
notificationListView : Mastodon.Model.Account -> List Mastodon.Model.NotificationAggregate -> Html Msg notificationListView : Account -> List NotificationAggregate -> Html Msg
notificationListView currentUser notifications = notificationListView currentUser notifications =
div [ class "col-md-3 column" ] div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ] [ div [ class "panel panel-default" ]
@ -465,7 +482,7 @@ draftReplyToView draft =
text "" text ""
currentUserView : Maybe Mastodon.Model.Account -> Html Msg currentUserView : Maybe Account -> Html Msg
currentUserView currentUser = currentUserView currentUser =
case currentUser of case currentUser of
Just currentUser -> Just currentUser ->
@ -594,7 +611,7 @@ draftView { draft, currentUser } =
] ]
threadView : Mastodon.Model.Account -> Thread -> Html Msg threadView : Account -> Thread -> Html Msg
threadView currentUser thread = threadView currentUser thread =
let let
statuses = statuses =
@ -663,7 +680,7 @@ homepageView model =
model.userTimeline model.userTimeline
, notificationListView currentUser model.notifications , notificationListView currentUser model.notifications
, case model.currentView of , case model.currentView of
Model.LocalTimelineView -> LocalTimelineView ->
timelineView timelineView
"Local timeline" "Local timeline"
"th-large" "th-large"
@ -671,7 +688,7 @@ homepageView model =
currentUser currentUser
model.localTimeline model.localTimeline
Model.GlobalTimelineView -> GlobalTimelineView ->
timelineView timelineView
"Global timeline" "Global timeline"
"globe" "globe"
@ -679,11 +696,16 @@ homepageView model =
currentUser currentUser
model.globalTimeline model.globalTimeline
Model.AccountView account -> AccountView account ->
-- Todo: Load the user timeline accountTimelineView "Account" model.accountTimeline account
accountTimelineView account model.accountTimeline "Account" "user"
Model.ThreadView thread -> AccountFollowersView account followers ->
accountFollowView "Account followers" model.accountFollowers account
AccountFollowingView account following ->
accountFollowView "Account following" model.accountFollowing account
ThreadView thread ->
threadView currentUser thread threadView currentUser thread
] ]

View File

@ -15,7 +15,7 @@ import HtmlParser
import Json.Decode as Decode import Json.Decode as Decode
import String.Extra exposing (replace) import String.Extra exposing (replace)
import Mastodon.Model import Mastodon.Model
import Model exposing (Msg(LoadAccount)) import Types exposing (..)
-- Custom Events -- Custom Events
@ -83,7 +83,7 @@ createLinkNode attrs children mentions =
Nothing -> Nothing ->
Html.node "a" Html.node "a"
((List.map toAttribute attrs) ((List.map toAttribute attrs)
++ [ onClickWithStop Model.NoOp, target "_blank" ] ++ [ onClickWithStop NoOp, target "_blank" ]
) )
(toVirtualDom mentions children) (toVirtualDom mentions children)
@ -91,10 +91,8 @@ createLinkNode attrs children mentions =
getHrefLink : List ( String, String ) -> Maybe String getHrefLink : List ( String, String ) -> Maybe String
getHrefLink attrs = getHrefLink attrs =
attrs attrs
|> List.filter |> List.filter (\( name, value ) -> (name == "href"))
(\( name, value ) -> (name == "href")) |> List.map (\( name, value ) -> value)
|> List.map
(\( name, value ) -> value)
|> List.head |> List.head