tooty/src/Model.elm
2017-04-29 18:05:06 +02:00

595 lines
19 KiB
Elm

module Model exposing (..)
import Command
import Dom
import Dom.Scroll
import Navigation
import Mastodon.Decoder
import Mastodon.Helper
import Mastodon.Model exposing (..)
import Mastodon.WebSocket
import Task
import Types exposing (..)
maxBuffer : Int
maxBuffer =
-- Max number of entries to keep in columns
100
extractAuthCode : Navigation.Location -> Maybe String
extractAuthCode { search } =
case (String.split "?code=" search) of
[ _, authCode ] ->
Just authCode
_ ->
Nothing
defaultDraft : Draft
defaultDraft =
{ status = ""
, in_reply_to = Nothing
, spoiler_text = Nothing
, sensitive = False
, visibility = "public"
}
init : Flags -> Navigation.Location -> ( Model, Cmd Msg )
init flags location =
let
authCode =
extractAuthCode location
in
{ server = ""
, registration = flags.registration
, client = flags.client
, userTimeline = []
, localTimeline = []
, globalTimeline = []
, accountTimeline = []
, accountFollowers = []
, accountFollowing = []
, notifications = []
, draft = defaultDraft
, errors = []
, location = location
, useGlobalTimeline = False
, viewer = Nothing
, currentView = LocalTimelineView
, currentUser = Nothing
}
! [ Command.initCommands flags.registration flags.client authCode ]
preferredTimeline : Model -> CurrentView
preferredTimeline model =
if model.useGlobalTimeline then
GlobalTimelineView
else
LocalTimelineView
truncate : List a -> List a
truncate entries =
List.take maxBuffer entries
errorText : Error -> String
errorText error =
case error of
MastodonError statusCode statusMsg errorMsg ->
"HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg
ServerError statusCode statusMsg errorMsg ->
"HTTP " ++ (toString statusCode) ++ " " ++ statusMsg ++ ": " ++ errorMsg
TimeoutError ->
"Request timed out."
NetworkError ->
"Unreachable host."
toStatusRequestBody : Draft -> StatusRequestBody
toStatusRequestBody draft =
{ status = draft.status
, in_reply_to_id =
case draft.in_reply_to of
Just status ->
Just status.id
Nothing ->
Nothing
, spoiler_text = draft.spoiler_text
, sensitive = draft.sensitive
, visibility = draft.visibility
}
updateTimelinesWithBoolFlag : Int -> Bool -> (Status -> Status) -> Model -> Model
updateTimelinesWithBoolFlag statusId flag statusUpdater model =
let
update flag status =
if (Mastodon.Helper.extractReblog status).id == statusId then
statusUpdater status
else
status
in
{ model
| userTimeline = List.map (update flag) model.userTimeline
, localTimeline = List.map (update flag) model.localTimeline
, globalTimeline = List.map (update flag) model.globalTimeline
}
processFavourite : Int -> Bool -> Model -> Model
processFavourite statusId flag model =
-- TODO: update notifications too
updateTimelinesWithBoolFlag statusId flag (\s -> { s | favourited = Just flag }) model
processReblog : Int -> Bool -> Model -> Model
processReblog statusId flag model =
-- TODO: update notifications too
updateTimelinesWithBoolFlag statusId flag (\s -> { s | reblogged = Just flag }) model
deleteStatusFromTimeline : Int -> List Status -> List Status
deleteStatusFromTimeline statusId timeline =
timeline
|> List.filter
(\s ->
s.id
/= statusId
&& (Mastodon.Helper.extractReblog s).id
/= statusId
)
updateDraft : DraftMsg -> Account -> Draft -> ( Draft, Cmd Msg )
updateDraft draftMsg currentUser draft =
case draftMsg of
ClearDraft ->
defaultDraft ! []
ToggleSpoiler enabled ->
{ draft
| spoiler_text =
if enabled then
Just ""
else
Nothing
}
! []
UpdateSensitive sensitive ->
{ draft | sensitive = sensitive } ! []
UpdateSpoiler spoiler_text ->
{ draft | spoiler_text = Just spoiler_text } ! []
UpdateStatus status ->
{ draft | status = status } ! []
UpdateVisibility visibility ->
{ draft | visibility = visibility } ! []
UpdateReplyTo status ->
{ draft
| in_reply_to = Just status
, status = Mastodon.Helper.getReplyPrefix currentUser status
, sensitive = Maybe.withDefault False status.sensitive
, spoiler_text =
if status.spoiler_text == "" then
Nothing
else
Just status.spoiler_text
, visibility = status.visibility
}
! [ Dom.focus "status" |> Task.attempt (always NoOp) ]
updateViewer : ViewerMsg -> Maybe Viewer -> ( Maybe Viewer, Cmd Msg )
updateViewer viewerMsg viewer =
case viewerMsg of
CloseViewer ->
Nothing ! []
OpenViewer attachments attachment ->
(Just <| Viewer attachments attachment) ! []
processMastodonEvent : MastodonMsg -> Model -> ( Model, Cmd Msg )
processMastodonEvent msg model =
case msg of
AccessToken result ->
case result of
Ok { server, accessToken } ->
let
client =
Client server accessToken
in
{ model | client = Just client }
! [ Command.loadTimelines <| Just client
, Navigation.modifyUrl model.location.pathname
, Command.saveClient client
]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AppRegistered result ->
case result of
Ok registration ->
{ model | registration = Just registration }
! [ Command.saveRegistration registration
, Command.navigateToAuthUrl registration
]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
ContextLoaded status result ->
case result of
Ok context ->
{ model | currentView = ThreadView (Thread status context) } ! []
Err error ->
{ model
| currentView = preferredTimeline model
, errors = (errorText error) :: model.errors
}
! []
CurrentUser result ->
case result of
Ok currentUser ->
{ model | currentUser = Just currentUser } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
FavoriteAdded result ->
case result of
Ok status ->
model ! [ Command.loadNotifications model.client ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
FavoriteRemoved result ->
case result of
Ok status ->
model ! [ Command.loadNotifications model.client ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
LocalTimeline result ->
case result of
Ok localTimeline ->
{ model | localTimeline = localTimeline } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
Notifications result ->
case result of
Ok notifications ->
{ model | notifications = Mastodon.Helper.aggregateNotifications notifications } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
GlobalTimeline result ->
case result of
Ok globalTimeline ->
{ model | globalTimeline = globalTimeline } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
Reblogged result ->
case result of
Ok status ->
model ! [ Command.loadNotifications model.client ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
StatusPosted _ ->
{ model | draft = defaultDraft } ! []
StatusDeleted result ->
case result of
Ok id ->
{ model
| userTimeline = deleteStatusFromTimeline id model.userTimeline
, localTimeline = deleteStatusFromTimeline id model.localTimeline
, globalTimeline = deleteStatusFromTimeline id model.globalTimeline
}
! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
Unreblogged result ->
case result of
Ok status ->
model ! [ Command.loadNotifications model.client ]
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
AccountReceived result ->
case result of
Ok account ->
{ model | currentView = AccountView account }
! [ Command.loadAccountInfo model.client account.id ]
Err error ->
{ model
| currentView = preferredTimeline model
, errors = (errorText error) :: model.errors
}
! []
AccountTimeline result ->
case result of
Ok statuses ->
{ model | accountTimeline = statuses } ! []
Err error ->
{ 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 ->
case result of
Ok userTimeline ->
{ model | userTimeline = userTimeline } ! []
Err error ->
{ model | errors = (errorText error) :: model.errors } ! []
processWebSocketMsg : WebSocketMsg -> Model -> ( Model, Cmd Msg )
processWebSocketMsg msg model =
case msg of
NewWebsocketUserMessage message ->
case (Mastodon.Decoder.decodeWebSocketMessage message) of
Mastodon.WebSocket.ErrorEvent error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusUpdateEvent result ->
case result of
Ok status ->
{ model | userTimeline = truncate (status :: model.userTimeline) } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusDeleteEvent result ->
case result of
Ok id ->
{ model | userTimeline = deleteStatusFromTimeline id model.userTimeline } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.NotificationEvent result ->
case result of
Ok notification ->
let
notifications =
Mastodon.Helper.addNotificationToAggregates
notification
model.notifications
in
{ model | notifications = truncate notifications } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
NewWebsocketLocalMessage message ->
case (Mastodon.Decoder.decodeWebSocketMessage message) of
Mastodon.WebSocket.ErrorEvent error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusUpdateEvent result ->
case result of
Ok status ->
{ model | localTimeline = truncate (status :: model.localTimeline) } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusDeleteEvent result ->
case result of
Ok id ->
{ model | localTimeline = deleteStatusFromTimeline id model.localTimeline } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
_ ->
model ! []
NewWebsocketGlobalMessage message ->
case (Mastodon.Decoder.decodeWebSocketMessage message) of
Mastodon.WebSocket.ErrorEvent error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusUpdateEvent result ->
case result of
Ok status ->
{ model | globalTimeline = truncate (status :: model.globalTimeline) } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
Mastodon.WebSocket.StatusDeleteEvent result ->
case result of
Ok id ->
{ model | globalTimeline = deleteStatusFromTimeline id model.globalTimeline } ! []
Err error ->
{ model | errors = error :: model.errors } ! []
_ ->
model ! []
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
MastodonEvent msg ->
let
( newModel, commands ) =
processMastodonEvent msg model
in
newModel ! [ commands ]
WebSocketEvent msg ->
let
( newModel, commands ) =
processWebSocketMsg msg model
in
newModel ! [ commands ]
ServerChange server ->
{ model | server = server } ! []
UrlChange location ->
model ! []
Register ->
model ! [ Command.registerApp model ]
OpenThread status ->
model ! [ Command.loadThread model.client status ]
CloseThread ->
{ model | currentView = preferredTimeline model } ! []
DeleteStatus id ->
model ! [ Command.deleteStatus model.client id ]
ReblogStatus id ->
processReblog id True model ! [ Command.reblogStatus model.client id ]
UnreblogStatus id ->
processReblog id False model ! [ Command.unreblogStatus model.client id ]
AddFavorite id ->
processFavourite id True model ! [ Command.favouriteStatus model.client id ]
RemoveFavorite id ->
processFavourite id False model ! [ Command.unfavouriteStatus model.client id ]
DraftEvent draftMsg ->
case model.currentUser of
Just user ->
let
( draft, commands ) =
updateDraft draftMsg user model.draft
in
{ model | draft = draft } ! [ commands ]
Nothing ->
model ! []
ViewerEvent viewerMsg ->
let
( viewer, commands ) =
updateViewer viewerMsg model.viewer
in
{ model | viewer = viewer } ! [ commands ]
SubmitDraft ->
model ! [ Command.postStatus model.client <| toStatusRequestBody model.draft ]
LoadAccount accountId ->
{ model | currentView = preferredTimeline model }
! [ Command.loadAccount model.client accountId ]
ViewAccountFollowers account ->
{ model | currentView = AccountFollowersView account model.accountFollowers } ! []
ViewAccountFollowing account ->
{ model | currentView = AccountFollowingView account model.accountFollowing } ! []
ViewAccountStatuses account ->
{ model | currentView = AccountView account } ! []
UseGlobalTimeline flag ->
let
newModel =
{ model | useGlobalTimeline = flag }
in
{ model | currentView = preferredTimeline newModel } ! []
CloseAccount ->
{ model
| currentView = preferredTimeline model
, accountTimeline = []
, accountFollowing = []
, accountFollowers = []
}
! []
ScrollColumn context ->
model ! [ Task.attempt (always NoOp) <| Dom.Scroll.toTop context ]
subscriptions : Model -> Sub Msg
subscriptions model =
case model.client of
Just client ->
let
subs =
[ Mastodon.WebSocket.subscribeToWebSockets
client
Mastodon.WebSocket.UserStream
NewWebsocketUserMessage
]
++ (if model.useGlobalTimeline then
[ Mastodon.WebSocket.subscribeToWebSockets
client
Mastodon.WebSocket.GlobalPublicStream
NewWebsocketGlobalMessage
]
else
[ Mastodon.WebSocket.subscribeToWebSockets
client
Mastodon.WebSocket.LocalPublicStream
NewWebsocketLocalMessage
]
)
in
Sub.batch <| List.map (Sub.map WebSocketEvent) subs
Nothing ->
Sub.batch []