WiP: Add status action buttons. (#32)
* Add status action buttons. * Handle favorite actions. * Handle reblog actions. * Optimistic updates for reblogs.
This commit is contained in:
parent
82ada5ede8
commit
225a95a637
@ -10,7 +10,11 @@ An [experimental Mastodon client](https://n1k0.github.io/tooty/) written in Elm.
|
|||||||
|
|
||||||
### Starting the dev server
|
### Starting the dev server
|
||||||
|
|
||||||
$ npm run live
|
$ npm start
|
||||||
|
|
||||||
|
### Starting the dev server in live debug mode
|
||||||
|
|
||||||
|
$ npm run debug
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
||||||
|
"elm-lang/dom": "1.1.1 <= v < 2.0.0",
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
||||||
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
||||||
"elm-lang/navigation": "2.1.0 <= v < 3.0.0",
|
"elm-lang/navigation": "2.1.0 <= v < 3.0.0",
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node_modules/.bin/elm-make src/Main.elm --warn --output=build/app.js && npm run copy-assets",
|
"build": "node_modules/.bin/elm-make src/Main.elm --warn --output=build/app.js && npm run copy-assets",
|
||||||
"copy-assets": "cp public/index.html build/ && cp public/style.css build/",
|
"copy-assets": "cp public/index.html build/ && cp public/style.css build/",
|
||||||
|
"debug": "node_modules/.bin/elm-live src/Main.elm --dir=public/ --output=public/app.js --debug",
|
||||||
"deploy": "npm run build && node_modules/.bin/gh-pages --dist build/",
|
"deploy": "npm run build && node_modules/.bin/gh-pages --dist build/",
|
||||||
"live": "node_modules/.bin/elm-live src/Main.elm --dir=public/ --output=public/app.js --debug",
|
"start": "node_modules/.bin/elm-live src/Main.elm --dir=public/ --output=public/app.js",
|
||||||
"test": "node_modules/.bin/elm-make src/Main.elm --warn --output /tmp/tooty.html"
|
"test": "node_modules/.bin/elm-make src/Main.elm --warn --output /tmp/tooty.html"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -15,6 +15,7 @@ body {
|
|||||||
.reblog > p:first-of-type,
|
.reblog > p:first-of-type,
|
||||||
.notification > p:first-of-type {
|
.notification > p:first-of-type {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.reblog > p:first-of-type > a,
|
.reblog > p:first-of-type > a,
|
||||||
.notification > p:first-of-type > a {
|
.notification > p:first-of-type > a {
|
||||||
@ -68,10 +69,37 @@ body {
|
|||||||
color: #9baec8;
|
color: #9baec8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status actions */
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-left: 65px;
|
||||||
|
width: calc(100% - 65px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > .btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #aaa;
|
||||||
|
padding: 0 2.4em 0 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > .btn > .glyphicon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .favourited {
|
||||||
|
color: #d1ac0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .reblogged {
|
||||||
|
color: #d56344;
|
||||||
|
}
|
||||||
|
|
||||||
/* Attachments */
|
/* Attachments */
|
||||||
|
|
||||||
.attachments {
|
.attachments {
|
||||||
margin: 0;
|
margin: 0 0 15px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -151,6 +179,20 @@ body {
|
|||||||
transition: all .6s;
|
transition: all .6s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Draft form */
|
||||||
|
|
||||||
|
.in-reply-to .attachments {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.in-reply-to .attachments li {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.in-reply-to .attachments:after {
|
||||||
|
content: "[attachments hidden]";
|
||||||
|
font-size: .9em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
/* Status text content rules */
|
/* Status text content rules */
|
||||||
|
|
||||||
.attachment {
|
.attachment {
|
||||||
|
@ -12,6 +12,11 @@ module Mastodon
|
|||||||
, Status
|
, Status
|
||||||
, StatusRequestBody
|
, StatusRequestBody
|
||||||
, Tag
|
, Tag
|
||||||
|
, reblog
|
||||||
|
, unreblog
|
||||||
|
, favourite
|
||||||
|
, unfavourite
|
||||||
|
, extractReblog
|
||||||
, register
|
, register
|
||||||
, registrationEncoder
|
, registrationEncoder
|
||||||
, clientEncoder
|
, clientEncoder
|
||||||
@ -406,6 +411,16 @@ extractError error =
|
|||||||
NetworkError
|
NetworkError
|
||||||
|
|
||||||
|
|
||||||
|
extractReblog : Status -> Status
|
||||||
|
extractReblog status =
|
||||||
|
case status.reblog of
|
||||||
|
Just (Reblog reblog) ->
|
||||||
|
reblog
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
status
|
||||||
|
|
||||||
|
|
||||||
toResponse : Result Http.Error a -> Result Error a
|
toResponse : Result Http.Error a -> Result Error a
|
||||||
toResponse result =
|
toResponse result =
|
||||||
Result.mapError extractError result
|
Result.mapError extractError result
|
||||||
@ -502,3 +517,31 @@ postStatus client statusRequestBody =
|
|||||||
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
||||||
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
||||||
|> HttpBuilder.withJsonBody (statusRequestBodyEncoder statusRequestBody)
|
|> HttpBuilder.withJsonBody (statusRequestBodyEncoder statusRequestBody)
|
||||||
|
|
||||||
|
|
||||||
|
reblog : Client -> Int -> Request Status
|
||||||
|
reblog client id =
|
||||||
|
HttpBuilder.post (client.server ++ "/api/v1/statuses/" ++ (toString id) ++ "/reblog")
|
||||||
|
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
||||||
|
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
unreblog : Client -> Int -> Request Status
|
||||||
|
unreblog client id =
|
||||||
|
HttpBuilder.post (client.server ++ "/api/v1/statuses/" ++ (toString id) ++ "/unreblog")
|
||||||
|
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
||||||
|
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
favourite : Client -> Int -> Request Status
|
||||||
|
favourite client id =
|
||||||
|
HttpBuilder.post (client.server ++ "/api/v1/statuses/" ++ (toString id) ++ "/favourite")
|
||||||
|
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
||||||
|
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
unfavourite : Client -> Int -> Request Status
|
||||||
|
unfavourite client id =
|
||||||
|
HttpBuilder.post (client.server ++ "/api/v1/statuses/" ++ (toString id) ++ "/unfavourite")
|
||||||
|
|> HttpBuilder.withHeader "Authorization" ("Bearer " ++ client.token)
|
||||||
|
|> HttpBuilder.withExpect (Http.expectJson statusDecoder)
|
||||||
|
210
src/Model.elm
210
src/Model.elm
@ -1,9 +1,11 @@
|
|||||||
module Model exposing (..)
|
module Model exposing (..)
|
||||||
|
|
||||||
|
import Dom
|
||||||
import Json.Encode as Encode
|
import Json.Encode as Encode
|
||||||
import Navigation
|
import Navigation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import Ports
|
import Ports
|
||||||
|
import Task
|
||||||
|
|
||||||
|
|
||||||
type alias Flags =
|
type alias Flags =
|
||||||
@ -13,22 +15,37 @@ type alias Flags =
|
|||||||
|
|
||||||
|
|
||||||
type DraftMsg
|
type DraftMsg
|
||||||
= ToggleSpoiler Bool
|
= ClearDraft
|
||||||
|
| ClearReplyTo
|
||||||
| UpdateSensitive Bool
|
| UpdateSensitive Bool
|
||||||
| UpdateSpoiler String
|
| UpdateSpoiler String
|
||||||
| UpdateStatus String
|
| UpdateStatus String
|
||||||
| UpdateVisibility String
|
| UpdateVisibility String
|
||||||
|
| UpdateReplyTo Mastodon.Status
|
||||||
|
| ToggleSpoiler Bool
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type
|
||||||
|
Msg
|
||||||
|
{-
|
||||||
|
FIXME: Mastodon server response messages should be extracted to their own
|
||||||
|
MastodonMsg type at some point.
|
||||||
|
-}
|
||||||
= AccessToken (Result Mastodon.Error Mastodon.AccessTokenResult)
|
= AccessToken (Result Mastodon.Error Mastodon.AccessTokenResult)
|
||||||
|
| AddFavorite Int
|
||||||
| AppRegistered (Result Mastodon.Error Mastodon.AppRegistration)
|
| AppRegistered (Result Mastodon.Error Mastodon.AppRegistration)
|
||||||
| DraftEvent DraftMsg
|
| DraftEvent DraftMsg
|
||||||
|
| FavoriteAdded (Result Mastodon.Error Mastodon.Status)
|
||||||
|
| FavoriteRemoved (Result Mastodon.Error Mastodon.Status)
|
||||||
| LocalTimeline (Result Mastodon.Error (List Mastodon.Status))
|
| LocalTimeline (Result Mastodon.Error (List Mastodon.Status))
|
||||||
|
| NoOp
|
||||||
| Notifications (Result Mastodon.Error (List Mastodon.Notification))
|
| Notifications (Result Mastodon.Error (List Mastodon.Notification))
|
||||||
| OnLoadUserAccount Int
|
| OnLoadUserAccount Int
|
||||||
| PublicTimeline (Result Mastodon.Error (List Mastodon.Status))
|
| PublicTimeline (Result Mastodon.Error (List Mastodon.Status))
|
||||||
|
| Reblog Int
|
||||||
|
| Reblogged (Result Mastodon.Error Mastodon.Status)
|
||||||
| Register
|
| Register
|
||||||
|
| RemoveFavorite Int
|
||||||
| ServerChange String
|
| ServerChange String
|
||||||
| StatusPosted (Result Mastodon.Error Mastodon.Status)
|
| StatusPosted (Result Mastodon.Error Mastodon.Status)
|
||||||
| SubmitDraft
|
| SubmitDraft
|
||||||
@ -36,9 +53,25 @@ type Msg
|
|||||||
| UseGlobalTimeline Bool
|
| UseGlobalTimeline Bool
|
||||||
| UserAccount (Result Mastodon.Error Mastodon.Account)
|
| UserAccount (Result Mastodon.Error Mastodon.Account)
|
||||||
| ClearOpenedAccount
|
| ClearOpenedAccount
|
||||||
|
| Unreblog Int
|
||||||
|
| Unreblogged (Result Mastodon.Error Mastodon.Status)
|
||||||
| UserTimeline (Result Mastodon.Error (List Mastodon.Status))
|
| UserTimeline (Result Mastodon.Error (List Mastodon.Status))
|
||||||
|
|
||||||
|
|
||||||
|
type Crud
|
||||||
|
= Add
|
||||||
|
| Remove
|
||||||
|
|
||||||
|
|
||||||
|
type alias Draft =
|
||||||
|
{ status : String
|
||||||
|
, in_reply_to : Maybe Mastodon.Status
|
||||||
|
, spoiler_text : Maybe String
|
||||||
|
, sensitive : Bool
|
||||||
|
, visibility : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ server : String
|
{ server : String
|
||||||
, registration : Maybe Mastodon.AppRegistration
|
, registration : Maybe Mastodon.AppRegistration
|
||||||
@ -47,7 +80,7 @@ type alias Model =
|
|||||||
, localTimeline : List Mastodon.Status
|
, localTimeline : List Mastodon.Status
|
||||||
, publicTimeline : List Mastodon.Status
|
, publicTimeline : List Mastodon.Status
|
||||||
, notifications : List Mastodon.Notification
|
, notifications : List Mastodon.Notification
|
||||||
, draft : Mastodon.StatusRequestBody
|
, draft : Draft
|
||||||
, account : Maybe Mastodon.Account
|
, account : Maybe Mastodon.Account
|
||||||
, errors : List String
|
, errors : List String
|
||||||
, location : Navigation.Location
|
, location : Navigation.Location
|
||||||
@ -65,10 +98,10 @@ extractAuthCode { search } =
|
|||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
|
|
||||||
defaultDraft : Mastodon.StatusRequestBody
|
defaultDraft : Draft
|
||||||
defaultDraft =
|
defaultDraft =
|
||||||
{ status = ""
|
{ status = ""
|
||||||
, in_reply_to_id = Nothing
|
, in_reply_to = Nothing
|
||||||
, spoiler_text = Nothing
|
, spoiler_text = Nothing
|
||||||
, sensitive = False
|
, sensitive = False
|
||||||
, visibility = "public"
|
, visibility = "public"
|
||||||
@ -142,6 +175,16 @@ saveRegistration registration =
|
|||||||
|> Ports.saveRegistration
|
|> Ports.saveRegistration
|
||||||
|
|
||||||
|
|
||||||
|
loadNotifications : Maybe Mastodon.Client -> Cmd Msg
|
||||||
|
loadNotifications client =
|
||||||
|
case client of
|
||||||
|
Just client ->
|
||||||
|
Mastodon.fetchNotifications client |> Mastodon.send Notifications
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
|
||||||
loadTimelines : Maybe Mastodon.Client -> Cmd Msg
|
loadTimelines : Maybe Mastodon.Client -> Cmd Msg
|
||||||
loadTimelines client =
|
loadTimelines client =
|
||||||
case client of
|
case client of
|
||||||
@ -150,7 +193,7 @@ loadTimelines client =
|
|||||||
[ Mastodon.fetchUserTimeline client |> Mastodon.send UserTimeline
|
[ Mastodon.fetchUserTimeline client |> Mastodon.send UserTimeline
|
||||||
, Mastodon.fetchLocalTimeline client |> Mastodon.send LocalTimeline
|
, Mastodon.fetchLocalTimeline client |> Mastodon.send LocalTimeline
|
||||||
, Mastodon.fetchPublicTimeline client |> Mastodon.send PublicTimeline
|
, Mastodon.fetchPublicTimeline client |> Mastodon.send PublicTimeline
|
||||||
, Mastodon.fetchNotifications client |> Mastodon.send Notifications
|
, loadNotifications <| Just client
|
||||||
]
|
]
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
@ -179,11 +222,56 @@ errorText error =
|
|||||||
"Unreachable host."
|
"Unreachable host."
|
||||||
|
|
||||||
|
|
||||||
updateDraft : DraftMsg -> Mastodon.StatusRequestBody -> Mastodon.StatusRequestBody
|
toStatusRequestBody : Draft -> Mastodon.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 -> (Mastodon.Status -> Mastodon.Status) -> Model -> Model
|
||||||
|
updateTimelinesWithBoolFlag statusId flag statusUpdater model =
|
||||||
|
let
|
||||||
|
update flag status =
|
||||||
|
if (Mastodon.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
|
||||||
|
, publicTimeline = List.map (update flag) model.publicTimeline
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
processFavourite : Int -> Bool -> Model -> Model
|
||||||
|
processFavourite statusId flag model =
|
||||||
|
updateTimelinesWithBoolFlag statusId flag (\s -> { s | favourited = Just flag }) model
|
||||||
|
|
||||||
|
|
||||||
|
processReblog : Int -> Bool -> Model -> Model
|
||||||
|
processReblog statusId flag model =
|
||||||
|
updateTimelinesWithBoolFlag statusId flag (\s -> { s | reblogged = Just flag }) model
|
||||||
|
|
||||||
|
|
||||||
|
updateDraft : DraftMsg -> Draft -> ( Draft, Cmd Msg )
|
||||||
updateDraft draftMsg draft =
|
updateDraft draftMsg draft =
|
||||||
-- TODO: later we'll probably want to handle more events like when the user
|
-- TODO: later we'll probably want to handle more events like when the user
|
||||||
-- wants to add CW, medias, etc.
|
-- wants to add CW, medias, etc.
|
||||||
case draftMsg of
|
case draftMsg of
|
||||||
|
ClearDraft ->
|
||||||
|
defaultDraft ! []
|
||||||
|
|
||||||
ToggleSpoiler enabled ->
|
ToggleSpoiler enabled ->
|
||||||
{ draft
|
{ draft
|
||||||
| spoiler_text =
|
| spoiler_text =
|
||||||
@ -192,23 +280,45 @@ updateDraft draftMsg draft =
|
|||||||
else
|
else
|
||||||
Nothing
|
Nothing
|
||||||
}
|
}
|
||||||
|
! []
|
||||||
|
|
||||||
UpdateSensitive sensitive ->
|
UpdateSensitive sensitive ->
|
||||||
{ draft | sensitive = sensitive }
|
{ draft | sensitive = sensitive } ! []
|
||||||
|
|
||||||
UpdateSpoiler spoiler_text ->
|
UpdateSpoiler spoiler_text ->
|
||||||
{ draft | spoiler_text = Just spoiler_text }
|
{ draft | spoiler_text = Just spoiler_text } ! []
|
||||||
|
|
||||||
UpdateStatus status ->
|
UpdateStatus status ->
|
||||||
{ draft | status = status }
|
{ draft | status = status } ! []
|
||||||
|
|
||||||
UpdateVisibility visibility ->
|
UpdateVisibility visibility ->
|
||||||
{ draft | visibility = visibility }
|
{ draft | visibility = visibility } ! []
|
||||||
|
|
||||||
|
UpdateReplyTo status ->
|
||||||
|
let
|
||||||
|
mention =
|
||||||
|
"@" ++ status.account.acct
|
||||||
|
in
|
||||||
|
{ draft
|
||||||
|
| in_reply_to = Just status
|
||||||
|
, status =
|
||||||
|
if String.startsWith mention draft.status then
|
||||||
|
draft.status
|
||||||
|
else
|
||||||
|
mention ++ " " ++ draft.status
|
||||||
|
}
|
||||||
|
! [ Dom.focus "status" |> Task.attempt (always NoOp) ]
|
||||||
|
|
||||||
|
ClearReplyTo ->
|
||||||
|
{ draft | in_reply_to = Nothing } ! []
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
|
NoOp ->
|
||||||
|
model ! []
|
||||||
|
|
||||||
ServerChange server ->
|
ServerChange server ->
|
||||||
{ model | server = server } ! []
|
{ model | server = server } ! []
|
||||||
|
|
||||||
@ -245,14 +355,88 @@ update msg model =
|
|||||||
Err error ->
|
Err error ->
|
||||||
{ model | errors = (errorText error) :: model.errors } ! []
|
{ model | errors = (errorText error) :: model.errors } ! []
|
||||||
|
|
||||||
|
Reblog id ->
|
||||||
|
-- Note: The case of reblogging is specific as it seems the server
|
||||||
|
-- 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.reblog client id |> Mastodon.send Reblogged ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
model ! []
|
||||||
|
|
||||||
|
Reblogged result ->
|
||||||
|
case result of
|
||||||
|
Ok status ->
|
||||||
|
model ! [ loadNotifications model.client ]
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
{ model | errors = (errorText error) :: model.errors } ! []
|
||||||
|
|
||||||
|
Unreblog id ->
|
||||||
|
case model.client of
|
||||||
|
Just client ->
|
||||||
|
processReblog id False model ! [ Mastodon.unfavourite client id |> Mastodon.send Unreblogged ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
model ! []
|
||||||
|
|
||||||
|
Unreblogged result ->
|
||||||
|
case result of
|
||||||
|
Ok status ->
|
||||||
|
model ! [ loadNotifications model.client ]
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
{ model | errors = (errorText error) :: model.errors } ! []
|
||||||
|
|
||||||
|
AddFavorite id ->
|
||||||
|
model
|
||||||
|
! case model.client of
|
||||||
|
Just client ->
|
||||||
|
[ Mastodon.favourite client id |> Mastodon.send FavoriteAdded ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
FavoriteAdded result ->
|
||||||
|
case result of
|
||||||
|
Ok status ->
|
||||||
|
processFavourite status.id True model ! [ loadNotifications model.client ]
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
{ model | errors = (errorText error) :: model.errors } ! []
|
||||||
|
|
||||||
|
RemoveFavorite id ->
|
||||||
|
model
|
||||||
|
! case model.client of
|
||||||
|
Just client ->
|
||||||
|
[ Mastodon.unfavourite client id |> Mastodon.send FavoriteRemoved ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
FavoriteRemoved result ->
|
||||||
|
case result of
|
||||||
|
Ok status ->
|
||||||
|
processFavourite status.id False model ! [ loadNotifications model.client ]
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
{ model | errors = (errorText error) :: model.errors } ! []
|
||||||
|
|
||||||
DraftEvent draftMsg ->
|
DraftEvent draftMsg ->
|
||||||
{ model | draft = updateDraft draftMsg model.draft } ! []
|
let
|
||||||
|
( draft, commands ) =
|
||||||
|
updateDraft draftMsg model.draft
|
||||||
|
in
|
||||||
|
{ model | draft = draft } ! [ commands ]
|
||||||
|
|
||||||
SubmitDraft ->
|
SubmitDraft ->
|
||||||
model
|
model
|
||||||
! case model.client of
|
! case model.client of
|
||||||
Just client ->
|
Just client ->
|
||||||
[ postStatus client model.draft ]
|
[ postStatus client <| toStatusRequestBody model.draft ]
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
[]
|
[]
|
||||||
|
106
src/View.elm
106
src/View.elm
@ -5,7 +5,7 @@ import Html exposing (..)
|
|||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (..)
|
import Html.Events exposing (..)
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import Model exposing (Model, DraftMsg(..), Msg(..))
|
import Model exposing (Model, Draft, DraftMsg(..), Msg(..))
|
||||||
import ViewHelper
|
import ViewHelper
|
||||||
|
|
||||||
|
|
||||||
@ -34,6 +34,12 @@ errorsListView model =
|
|||||||
div [] <| List.map errorView model.errors
|
div [] <| List.map errorView model.errors
|
||||||
|
|
||||||
|
|
||||||
|
justifiedButtonGroup : List (Html Msg) -> Html Msg
|
||||||
|
justifiedButtonGroup buttons =
|
||||||
|
div [ class "btn-group btn-group-justified" ] <|
|
||||||
|
List.map (\b -> div [ class "btn-group" ] [ b ]) buttons
|
||||||
|
|
||||||
|
|
||||||
icon : String -> Html Msg
|
icon : String -> Html Msg
|
||||||
icon name =
|
icon name =
|
||||||
i [ class <| "glyphicon glyphicon-" ++ name ] []
|
i [ class <| "glyphicon glyphicon-" ++ name ] []
|
||||||
@ -215,6 +221,51 @@ accountTimelineView account statuses label iconName =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
statusActionsView : Mastodon.Status -> Html Msg
|
||||||
|
statusActionsView status =
|
||||||
|
let
|
||||||
|
target =
|
||||||
|
Mastodon.extractReblog status
|
||||||
|
|
||||||
|
baseBtnClasses =
|
||||||
|
"btn btn-sm btn-default"
|
||||||
|
|
||||||
|
( reblogClasses, reblogEvent ) =
|
||||||
|
case status.favourited of
|
||||||
|
Just True ->
|
||||||
|
( baseBtnClasses ++ " reblogged", Unreblog target.id )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( baseBtnClasses, AddFavorite target.id )
|
||||||
|
|
||||||
|
( favClasses, favEvent ) =
|
||||||
|
case status.favourited of
|
||||||
|
Just True ->
|
||||||
|
( baseBtnClasses ++ " favourited", RemoveFavorite target.id )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( baseBtnClasses, AddFavorite target.id )
|
||||||
|
in
|
||||||
|
div [ class "btn-group actions" ]
|
||||||
|
[ a
|
||||||
|
[ class baseBtnClasses
|
||||||
|
, ViewHelper.onClickWithPreventAndStop <|
|
||||||
|
DraftEvent (UpdateReplyTo target)
|
||||||
|
]
|
||||||
|
[ icon "share-alt" ]
|
||||||
|
, a
|
||||||
|
[ class reblogClasses
|
||||||
|
, ViewHelper.onClickWithPreventAndStop reblogEvent
|
||||||
|
]
|
||||||
|
[ icon "fire", text (toString status.reblogs_count) ]
|
||||||
|
, a
|
||||||
|
[ class favClasses
|
||||||
|
, ViewHelper.onClickWithPreventAndStop favEvent
|
||||||
|
]
|
||||||
|
[ icon "star", text (toString status.favourites_count) ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
statusEntryView : Mastodon.Status -> Html Msg
|
statusEntryView : Mastodon.Status -> Html Msg
|
||||||
statusEntryView status =
|
statusEntryView status =
|
||||||
let
|
let
|
||||||
@ -227,7 +278,9 @@ statusEntryView status =
|
|||||||
""
|
""
|
||||||
in
|
in
|
||||||
li [ class <| "list-group-item " ++ nsfwClass ]
|
li [ class <| "list-group-item " ++ nsfwClass ]
|
||||||
[ statusView status ]
|
[ statusView status
|
||||||
|
, statusActionsView status
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
timelineView : List Mastodon.Status -> String -> String -> Html Msg
|
timelineView : List Mastodon.Status -> String -> String -> Html Msg
|
||||||
@ -264,6 +317,7 @@ notificationStatusView status { type_, account } =
|
|||||||
_ ->
|
_ ->
|
||||||
text ""
|
text ""
|
||||||
, statusView status
|
, statusView status
|
||||||
|
, statusActionsView status
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -299,6 +353,29 @@ notificationListView notifications =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
draftReplyToView : Draft -> Html Msg
|
||||||
|
draftReplyToView draft =
|
||||||
|
case draft.in_reply_to of
|
||||||
|
Just status ->
|
||||||
|
div [ class "in-reply-to" ]
|
||||||
|
[ p []
|
||||||
|
[ strong []
|
||||||
|
[ text "In reply to this toot ("
|
||||||
|
, a
|
||||||
|
[ href ""
|
||||||
|
, ViewHelper.onClickWithPreventAndStop <| DraftEvent ClearReplyTo
|
||||||
|
]
|
||||||
|
[ icon "remove" ]
|
||||||
|
, text ")"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "well" ] [ statusView status ]
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
text ""
|
||||||
|
|
||||||
|
|
||||||
draftView : Model -> Html Msg
|
draftView : Model -> Html Msg
|
||||||
draftView { draft } =
|
draftView { draft } =
|
||||||
let
|
let
|
||||||
@ -310,9 +387,17 @@ draftView { draft } =
|
|||||||
[ text <| visibility ++ ": " ++ description ]
|
[ text <| visibility ++ ": " ++ description ]
|
||||||
in
|
in
|
||||||
div [ class "panel panel-default" ]
|
div [ class "panel panel-default" ]
|
||||||
[ div [ class "panel-heading" ] [ icon "envelope", text "Post a message" ]
|
[ div [ class "panel-heading" ]
|
||||||
|
[ icon "envelope"
|
||||||
|
, text <|
|
||||||
|
if draft.in_reply_to /= Nothing then
|
||||||
|
"Post a reply"
|
||||||
|
else
|
||||||
|
"Post a message"
|
||||||
|
]
|
||||||
, div [ class "panel-body" ]
|
, div [ class "panel-body" ]
|
||||||
[ Html.form [ class "form", onSubmit SubmitDraft ]
|
[ draftReplyToView draft
|
||||||
|
, Html.form [ class "form", onSubmit SubmitDraft ]
|
||||||
[ div [ class "form-group checkbox" ]
|
[ div [ class "form-group checkbox" ]
|
||||||
[ label []
|
[ label []
|
||||||
[ input
|
[ input
|
||||||
@ -387,8 +472,17 @@ draftView { draft } =
|
|||||||
, text " This post is NSFW"
|
, text " This post is NSFW"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, p [ class "text-right" ]
|
, justifiedButtonGroup
|
||||||
[ button [ class "btn btn-primary" ]
|
[ button
|
||||||
|
[ type_ "button"
|
||||||
|
, class "btn btn-default"
|
||||||
|
, onClick (DraftEvent ClearDraft)
|
||||||
|
]
|
||||||
|
[ text "Clear" ]
|
||||||
|
, button
|
||||||
|
[ type_ "submit"
|
||||||
|
, class "btn btn-primary"
|
||||||
|
]
|
||||||
[ text "Toot!" ]
|
[ text "Toot!" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -34,7 +34,6 @@ onClickWithPreventAndStop msg =
|
|||||||
formatContent : String -> List Mastodon.Mention -> List (Html Msg)
|
formatContent : String -> List Mastodon.Mention -> List (Html Msg)
|
||||||
formatContent content mentions =
|
formatContent content mentions =
|
||||||
content
|
content
|
||||||
|> replace "'" "'"
|
|
||||||
|> replace " ?" " ?"
|
|> replace " ?" " ?"
|
||||||
|> replace " !" " !"
|
|> replace " !" " !"
|
||||||
|> replace " :" " :"
|
|> replace " :" " :"
|
||||||
|
Loading…
Reference in New Issue
Block a user