diff --git a/public/style.css b/public/style.css index 70278ac..ceeacfd 100644 --- a/public/style.css +++ b/public/style.css @@ -3,7 +3,7 @@ body { } .status { - min-height: 75px; + min-height: 50px; clear: both; } @@ -11,9 +11,18 @@ body { background: #493438; } -.reblog > p:first-of-type { +.reblog > p:first-of-type, +.notification > p:first-of-type { color: #999; } +.reblog > p:first-of-type > a, +.notification > p:first-of-type > a { + color: #ccc; +} + +.notification.follow > p { + margin-bottom: 0; +} .panel-heading { font-weight: bold; @@ -53,7 +62,7 @@ body { color: #efefef; } -.status-text a, .u-url, .mention, .hashtag, .tag { +.status-text a, .u-url, .status .mention, .hashtag, .tag { color: #9baec8; } diff --git a/src/Mastodon.elm b/src/Mastodon.elm index 2e2bedb..8a66fcb 100644 --- a/src/Mastodon.elm +++ b/src/Mastodon.elm @@ -7,6 +7,7 @@ module Mastodon , Client , Error(..) , Mention + , Notification , Reblog(..) , Status , StatusRequestBody @@ -17,8 +18,9 @@ module Mastodon , getAuthorizationUrl , getAccessToken , fetchAccount - , fetchPublicTimeline , fetchLocalTimeline + , fetchNotifications + , fetchPublicTimeline , fetchUserTimeline , postStatus , send @@ -125,6 +127,22 @@ type alias Mention = } +type alias Notification = + {- + - id: The notification ID + - type_: One of: "mention", "reblog", "favourite", "follow" + - created_at: The time the notification was created + - account: The Account sending the notification to the user + - status: The Status associated with the notification, if applicable + -} + { id : Int + , type_ : String + , created_at : String + , account : Account + , status : Maybe Status + } + + type alias Tag = { name : String , url : String @@ -287,6 +305,16 @@ mentionDecoder = |> Pipe.required "acct" Decode.string +notificationDecoder : Decode.Decoder Notification +notificationDecoder = + Pipe.decode Notification + |> Pipe.required "id" Decode.int + |> Pipe.required "type" Decode.string + |> Pipe.required "created_at" Decode.string + |> Pipe.required "account" accountDecoder + |> Pipe.optional "status" (Decode.nullable statusDecoder) Nothing + + tagDecoder : Decode.Decoder Tag tagDecoder = Pipe.decode Tag @@ -450,17 +478,22 @@ fetchAccount client accountId = fetchUserTimeline : Client -> Request (List Status) fetchUserTimeline client = - fetch client "/api/v1/timelines/home" (Decode.list statusDecoder) + fetch client "/api/v1/timelines/home" <| Decode.list statusDecoder fetchLocalTimeline : Client -> Request (List Status) fetchLocalTimeline client = - fetch client "/api/v1/timelines/public?local=true" (Decode.list statusDecoder) + fetch client "/api/v1/timelines/public?local=true" <| Decode.list statusDecoder fetchPublicTimeline : Client -> Request (List Status) fetchPublicTimeline client = - fetch client "/api/v1/timelines/public" (Decode.list statusDecoder) + fetch client "/api/v1/timelines/public" <| Decode.list statusDecoder + + +fetchNotifications : Client -> Request (List Notification) +fetchNotifications client = + fetch client "/api/v1/notifications" <| Decode.list notificationDecoder postStatus : Client -> StatusRequestBody -> Request Status diff --git a/src/Model.elm b/src/Model.elm index 86e3df8..7a91cae 100644 --- a/src/Model.elm +++ b/src/Model.elm @@ -25,13 +25,15 @@ type Msg | AppRegistered (Result Mastodon.Error Mastodon.AppRegistration) | DraftEvent DraftMsg | LocalTimeline (Result Mastodon.Error (List Mastodon.Status)) - | PublicTimeline (Result Mastodon.Error (List Mastodon.Status)) + | Notifications (Result Mastodon.Error (List Mastodon.Notification)) | OnLoadUserAccount Int + | PublicTimeline (Result Mastodon.Error (List Mastodon.Status)) | Register | ServerChange String | StatusPosted (Result Mastodon.Error Mastodon.Status) | SubmitDraft | UrlChange Navigation.Location + | UseGlobalTimeline Bool | UserAccount (Result Mastodon.Error Mastodon.Account) | UserTimeline (Result Mastodon.Error (List Mastodon.Status)) @@ -43,10 +45,12 @@ type alias Model = , userTimeline : List Mastodon.Status , localTimeline : List Mastodon.Status , publicTimeline : List Mastodon.Status + , notifications : List Mastodon.Notification , draft : Mastodon.StatusRequestBody , account : Maybe Mastodon.Account , errors : List String , location : Navigation.Location + , useGlobalTimeline : Bool } @@ -82,10 +86,12 @@ init flags location = , userTimeline = [] , localTimeline = [] , publicTimeline = [] + , notifications = [] , draft = defaultDraft , account = Nothing , errors = [] , location = location + , useGlobalTimeline = False } ! [ initCommands flags.registration flags.client authCode ] @@ -143,6 +149,7 @@ loadTimelines client = [ Mastodon.fetchUserTimeline client |> Mastodon.send UserTimeline , Mastodon.fetchLocalTimeline client |> Mastodon.send LocalTimeline , Mastodon.fetchPublicTimeline client |> Mastodon.send PublicTimeline + , Mastodon.fetchNotifications client |> Mastodon.send Notifications ] Nothing -> @@ -271,6 +278,9 @@ update msg model = Nothing -> [] + UseGlobalTimeline flag -> + { model | useGlobalTimeline = flag } ! [] + LocalTimeline result -> case result of Ok localTimeline -> @@ -297,3 +307,11 @@ update msg model = StatusPosted _ -> { model | draft = defaultDraft } ! [ loadTimelines model.client ] + + Notifications result -> + case result of + Ok notifications -> + { model | notifications = notifications } ! [] + + Err error -> + { model | notifications = [], errors = (errorText error) :: model.errors } ! [] diff --git a/src/View.elm b/src/View.elm index 3274c00..a19e74d 100644 --- a/src/View.elm +++ b/src/View.elm @@ -39,6 +39,15 @@ icon name = i [ class <| "glyphicon glyphicon-" ++ name ] [] +accountLink : Mastodon.Account -> Html Msg +accountLink account = + a + [ href account.url + , ViewHelper.onClickWithPreventAndStop (OnLoadUserAccount account.id) + ] + [ text <| "@" ++ account.username ] + + attachmentPreview : Maybe Bool -> Mastodon.Attachment -> Html Msg attachmentPreview sensitive ({ url, preview_url } as attachment) = let @@ -227,6 +236,61 @@ timelineView statuses label iconName = ] +notificationHeading : Mastodon.Account -> String -> String -> Html Msg +notificationHeading account str iconType = + p [] <| + List.intersperse (text " ") + [ icon iconType, accountLink account, text str ] + + +notificationStatusView : Mastodon.Status -> Mastodon.Notification -> Html Msg +notificationStatusView status { type_, account } = + div [ class "notification mention" ] + [ case type_ of + "reblog" -> + notificationHeading account "boosted your toot" "fire" + + "favourite" -> + notificationHeading account "favourited your toot" "star" + + _ -> + text "" + , statusView status + ] + + +notificationFollowView : Mastodon.Notification -> Html Msg +notificationFollowView { account } = + div [ class "notification follow" ] + [ notificationHeading account "started following you" "user" ] + + +notificationEntryView : Mastodon.Notification -> Html Msg +notificationEntryView notification = + li [ class "list-group-item" ] + [ case notification.status of + Just status -> + notificationStatusView status notification + + Nothing -> + notificationFollowView notification + ] + + +notificationListView : List Mastodon.Notification -> Html Msg +notificationListView notifications = + div [ class "col-md-3" ] + [ div [ class "panel panel-default" ] + [ div [ class "panel-heading" ] + [ icon "bell" + , text "Notifications" + ] + , ul [ class "list-group" ] <| + List.map notificationEntryView notifications + ] + ] + + draftView : Model -> Html Msg draftView { draft } = let @@ -237,108 +301,136 @@ draftView { draft } = option [ value visibility ] [ text <| visibility ++ ": " ++ description ] in - div [ class "col-md-3" ] - [ div [ class "panel panel-default" ] - [ div [ class "panel-heading" ] [ icon "envelope", text "Post a message" ] - , div [ class "panel-body" ] - [ Html.form [ class "form", onSubmit SubmitDraft ] - [ div [ class "form-group checkbox" ] - [ label [] - [ input - [ type_ "checkbox" - , onCheck <| DraftEvent << ToggleSpoiler - , checked hasSpoiler - ] - [] - , text " Add a spoiler" + div [ class "panel panel-default" ] + [ div [ class "panel-heading" ] [ icon "envelope", text "Post a message" ] + , div [ class "panel-body" ] + [ Html.form [ class "form", onSubmit SubmitDraft ] + [ div [ class "form-group checkbox" ] + [ label [] + [ input + [ type_ "checkbox" + , onCheck <| DraftEvent << ToggleSpoiler + , checked hasSpoiler ] + [] + , text " Add a spoiler" ] - , if hasSpoiler then - div [ class "form-group" ] - [ label [ for "spoiler" ] [ text "Visible part" ] - , textarea - [ id "spoiler" - , class "form-control" - , rows 5 - , placeholder "This text will always be visible." - , onInput <| DraftEvent << UpdateSpoiler - , required True - , value <| Maybe.withDefault "" draft.spoiler_text - ] - [] - ] - else - text "" - , div [ class "form-group" ] - [ label [ for "status" ] - [ text <| - if hasSpoiler then - "Hidden part" - else - "Status" - ] + ] + , if hasSpoiler then + div [ class "form-group" ] + [ label [ for "spoiler" ] [ text "Visible part" ] , textarea - [ id "status" + [ id "spoiler" , class "form-control" - , rows 8 - , placeholder <| - if hasSpoiler then - "This text will be hidden by default, as you have enabled a spoiler." - else - "Once upon a time..." - , onInput <| DraftEvent << UpdateStatus + , rows 5 + , placeholder "This text will always be visible." + , onInput <| DraftEvent << UpdateSpoiler , required True - , value draft.status + , value <| Maybe.withDefault "" draft.spoiler_text ] [] ] - , div [ class "form-group" ] - [ label [ for "visibility" ] [ text "Visibility" ] - , select - [ id "visibility" - , class "form-control" - , onInput <| DraftEvent << UpdateVisibility - , required True - , value draft.visibility + else + text "" + , div [ class "form-group" ] + [ label [ for "status" ] + [ text <| + if hasSpoiler then + "Hidden part" + else + "Status" + ] + , textarea + [ id "status" + , class "form-control" + , rows 8 + , placeholder <| + if hasSpoiler then + "This text will be hidden by default, as you have enabled a spoiler." + else + "Once upon a time..." + , onInput <| DraftEvent << UpdateStatus + , required True + , value draft.status + ] + [] + ] + , div [ class "form-group" ] + [ label [ for "visibility" ] [ text "Visibility" ] + , select + [ id "visibility" + , class "form-control" + , onInput <| DraftEvent << UpdateVisibility + , required True + , value draft.visibility + ] + <| + List.map visibilityOptionView <| + Dict.toList visibilities + ] + , div [ class "form-group checkbox" ] + [ label [] + [ input + [ type_ "checkbox" + , onCheck <| DraftEvent << UpdateSensitive + , checked draft.sensitive ] - <| - List.map visibilityOptionView <| - Dict.toList visibilities - ] - , div [ class "form-group checkbox" ] - [ label [] - [ input - [ type_ "checkbox" - , onCheck <| DraftEvent << UpdateSensitive - , checked draft.sensitive - ] - [] - , text " This post is NSFW" - ] - ] - , p [ class "text-right" ] - [ button [ class "btn btn-primary" ] - [ text "Toot!" ] + [] + , text " This post is NSFW" ] ] + , p [ class "text-right" ] + [ button [ class "btn btn-primary" ] + [ text "Toot!" ] + ] ] ] ] +optionsView : Model -> Html Msg +optionsView model = + div [ class "panel panel-default" ] + [ div [ class "panel-heading" ] [ icon "cog", text "options" ] + , div [ class "panel-body" ] + [ div [ class "checkbox" ] + [ label [] + [ input + [ type_ "checkbox" + , onCheck UseGlobalTimeline + ] + [] + , text " 4th column renders the global timeline" + ] + ] + ] + ] + + +sidebarView : Model -> Html Msg +sidebarView model = + div [ class "col-md-3" ] + [ draftView model + , optionsView model + ] + + homepageView : Model -> Html Msg homepageView model = div [ class "row" ] - [ draftView model + [ sidebarView model , timelineView model.userTimeline "Home timeline" "home" - , timelineView model.localTimeline "Local timeline" "th-large" + , notificationListView model.notifications , case model.account of Just account -> -- Todo: Load the user timeline accountTimelineView account [] "Account" "user" Nothing -> - timelineView model.publicTimeline "Public timeline" "globe" + if model.useGlobalTimeline then + timelineView model.publicTimeline "Global timeline" "globe" + else + timelineView model.localTimeline "Local timeline" "th-large" ] diff --git a/src/ViewHelper.elm b/src/ViewHelper.elm index 15a822b..525f50e 100644 --- a/src/ViewHelper.elm +++ b/src/ViewHelper.elm @@ -11,7 +11,6 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onWithOptions) import HtmlParser -import HtmlParser.Util import Json.Decode as Decode import Mastodon import Model exposing (Msg(OnLoadUserAccount))