1
0
Fork 0

Fix #152: Multiple accounts. (#153)

* Update model to store multiple clients.

* Delete tmp registration data after client creation.

* Add minimal account selector view

* Update clients so they can have an account attached.

* List clients in the account selector.

* List accounts in the account selector view.

* It works™.

* Minor CSS fix.

* Reset server value when switching account.

* Fix empty black screen on reauth with new client format.

* Fix typo.

[skip-ci]
This commit is contained in:
Nicolas Perriault 2017-05-09 18:43:12 +02:00 committed by GitHub
parent 51a802f096
commit 7a053b9fa0
17 changed files with 331 additions and 108 deletions

View File

@ -17,20 +17,35 @@
<body>
<script src="app.js"></script>
<script>
// Note: this is a transitional upgrade to new client storage format, which
// is now now a list of clients, which all require an "account" property set.
const oldClient = JSON.parse(localStorage.getItem("tooty.client"));
const defaultClients = oldClient ? [oldClient] : [];
const clients = (JSON.parse(localStorage.getItem("tooty.clients")) || defaultClients)
.map(client => {
if (!client.hasOwnProperty("account")) {
client.account = null;
}
return client;
});
const app = Elm.Main.fullscreen({
client: JSON.parse(localStorage.getItem("tooty.client")),
clients: clients,
registration: JSON.parse(localStorage.getItem("tooty.registration"))
});
app.ports.saveClient.subscribe(json => {
localStorage.setItem("tooty.client", json);
app.ports.saveClients.subscribe(json => {
localStorage.setItem("tooty.clients", json);
});
app.ports.saveRegistration.subscribe(json => {
localStorage.setItem("tooty.registration", json);
});
app.ports.deleteRegistration.subscribe(json => {
localStorage.removeItem("tooty.registration");
});
app.ports.setStatus.subscribe(function(data) {
var element = document.getElementById(data.id);
if (element) {

View File

@ -157,6 +157,10 @@ li.load-more {
margin-bottom: 4px;
}
.current-user .username > span {
font-weight: normal;
}
.follow-entry {
display: flex;
flex-direction: row;
@ -467,6 +471,10 @@ li.load-more {
padding: 15px;
}
.account-infos.row {
margin-right: 0;
}
.account-infos a {
color: #c8c8c8;
}
@ -561,71 +569,71 @@ li.load-more {
/* Scrollbars */
::-webkit-scrollbar {
width: 10px;
height: 8px
width: 10px;
height: 8px
}
::-webkit-scrollbar-thumb {
background: #444;
border: 0px none #ffffff;
border-radius: 50px;
background: #444;
border: 0px none #ffffff;
border-radius: 50px;
}
::-webkit-scrollbar-thumb:hover {
background: #777;
background: #777;
}
::-webkit-scrollbar-thumb:active {
background: #777;
background: #777;
}
::-webkit-scrollbar-track {
border: 0px none #ffffff;
border-radius: 0;
background: rgba(0,0,0,0.1);
border: 0px none #ffffff;
border-radius: 0;
background: rgba(0,0,0,0.1);
}
::-webkit-scrollbar-track:hover {
background: #2a2e31;
background: #2a2e31;
}
::-webkit-scrollbar-track:active {
background: #2a2e31;
background: #2a2e31;
}
::-webkit-scrollbar-corner {
background: transparent;
background: transparent;
}
/* Autocomplete */
.autocomplete-menu {
position: relative;
margin-top: -10px;
min-width: 120px;
position: relative;
margin-top: -10px;
min-width: 120px;
}
.autocomplete-list {
list-style: none;
padding: 0;
margin: auto;
max-height: 200px;
overflow-y: auto;
list-style: none;
padding: 0;
margin: auto;
max-height: 200px;
overflow-y: auto;
}
.autocomplete-item {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: flex-start;
padding: 8px;
cursor: pointer;
background: #fff;
color: #777;
border-color: #bbb;
border-left-color: #272b30;
border-right-color: #272b30;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: flex-start;
padding: 8px;
cursor: pointer;
background: #fff;
color: #777;
border-color: #bbb;
border-left-color: #272b30;
border-right-color: #272b30;
}
.autocomplete-item:hover,
@ -644,11 +652,11 @@ li.load-more {
}
.autocomplete-item > img {
flex: 1 1 auto;
height: 20px;
width: 20px;
margin-right: 10px;
border-radius: 2px;
flex: 1 1 auto;
height: 20px;
width: 20px;
margin-right: 10px;
border-radius: 2px;
}
.autocomplete-item > strong {
@ -667,3 +675,39 @@ li.load-more {
overflow: hidden;
text-overflow: ellipsis;
}
/* Account selector */
.account-selector-item {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: flex-start;
padding: 16px;
}
.account-selector-item > img {
flex: 1 1 auto;
height: 42px;
width: 42px;
margin-right: 10px;
border-radius: 2px;
}
.account-selector-item > span {
flex: 100 1 auto;
align-self: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.account-selector-item > button {
flex: 1 1 auto;
text-align: right;
align-self: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -3,7 +3,7 @@ module Command
( initCommands
, navigateToAuthUrl
, registerApp
, saveClient
, saveClients
, saveRegistration
, loadNotifications
, loadUserAccount
@ -57,7 +57,9 @@ initCommands registration client authCode =
Just authCode ->
case registration of
Just registration ->
[ getAccessToken registration authCode ]
[ getAccessToken registration authCode
, Ports.deleteRegistration ""
]
Nothing ->
[]
@ -107,11 +109,13 @@ registerApp { server, location } =
|> send (MastodonEvent << AppRegistered)
saveClient : Client -> Cmd Msg
saveClient client =
clientEncoder client
saveClients : List Client -> Cmd Msg
saveClients clients =
clients
|> List.map clientEncoder
|> Encode.list
|> Encode.encode 0
|> Ports.saveClient
|> Ports.saveClients
saveRegistration : AppRegistration -> Cmd Msg

View File

@ -9,11 +9,11 @@ import Util
init : Flags -> Navigation.Location -> ( Model, Cmd Msg )
init { registration, client } location =
init { registration, clients } location =
{ server = ""
, currentTime = 0
, registration = registration
, client = client
, clients = clients
, homeTimeline = Update.Timeline.empty "home-timeline"
, localTimeline = Update.Timeline.empty "local-timeline"
, globalTimeline = Update.Timeline.empty "global-timeline"
@ -32,4 +32,4 @@ init { registration, client } location =
, currentUser = Nothing
, notificationFilter = NotificationAll
}
! [ Command.initCommands registration client (Util.extractAuthCode location) ]
! [ Command.initCommands registration (List.head clients) (Util.extractAuthCode location) ]

View File

@ -51,11 +51,31 @@ authorizationCodeEncoder registration authCode =
]
accountEncoder : Account -> Encode.Value
accountEncoder account =
Encode.object
[ ( "acct", Encode.string account.acct )
, ( "avatar", Encode.string account.avatar )
, ( "created_at", Encode.string account.created_at )
, ( "display_name", Encode.string account.display_name )
, ( "followers_count", Encode.int account.followers_count )
, ( "following_count", Encode.int account.following_count )
, ( "header", Encode.string account.header )
, ( "id", Encode.int account.id )
, ( "locked", Encode.bool account.locked )
, ( "note", Encode.string account.note )
, ( "statuses_count", Encode.int account.statuses_count )
, ( "url", Encode.string account.url )
, ( "username", Encode.string account.username )
]
clientEncoder : Client -> Encode.Value
clientEncoder client =
Encode.object
[ ( "server", Encode.string client.server )
, ( "token", Encode.string client.token )
, ( "account", encodeMaybe accountEncoder client.account )
]

View File

@ -104,6 +104,7 @@ type alias Attachment =
type alias Client =
{ server : Server
, token : Token
, account : Maybe Account
}

View File

@ -1,8 +1,9 @@
port module Ports
exposing
( saveRegistration
, deleteRegistration
, scrollIntoView
, saveClient
, saveClients
, setStatus
)
@ -10,7 +11,10 @@ port module Ports
port saveRegistration : String -> Cmd msg
port saveClient : String -> Cmd msg
port deleteRegistration : String -> Cmd msg
port saveClients : String -> Cmd msg
port setStatus : { id : String, status : String } -> Cmd msg

View File

@ -7,14 +7,14 @@ import Types exposing (..)
subscriptions : Model -> Sub Msg
subscriptions { client, currentView } =
subscriptions { clients, currentView } =
let
timeSub =
Time.every Time.millisecond Tick
userWsSub =
Mastodon.WebSocket.subscribeToWebSockets
client
(List.head clients)
Mastodon.WebSocket.UserStream
NewWebsocketUserMessage
|> Sub.map WebSocketEvent
@ -22,13 +22,13 @@ subscriptions { client, currentView } =
otherWsSub =
if currentView == GlobalTimelineView then
Mastodon.WebSocket.subscribeToWebSockets
client
(List.head clients)
Mastodon.WebSocket.GlobalPublicStream
NewWebsocketGlobalMessage
|> Sub.map WebSocketEvent
else if currentView == LocalTimelineView then
Mastodon.WebSocket.subscribeToWebSockets
client
(List.head clients)
Mastodon.WebSocket.LocalPublicStream
NewWebsocketLocalMessage
|> Sub.map WebSocketEvent

View File

@ -8,7 +8,7 @@ import Time exposing (Time)
type alias Flags =
{ client : Maybe Client
{ clients : List Client
, registration : Maybe AppRegistration
}
@ -72,6 +72,7 @@ type Msg
= AddFavorite Int
| ClearError Int
| CloseAccount
| CloseAccountSelector
| CloseThread
| DeleteStatus Int
| DraftEvent DraftMsg
@ -82,12 +83,14 @@ type Msg
| MastodonEvent MastodonMsg
| NoOp
| OpenThread Status
| OpenAccountSelector
| ReblogStatus Int
| Register
| RemoveFavorite Int
| ScrollColumn ScrollDirection String
| ServerChange String
| SubmitDraft
| SwitchClient Client
| Tick Time
| UnfollowAccount Int
| UrlChange Navigation.Location
@ -105,6 +108,7 @@ type CurrentView
AccountFollowersView Account (Timeline Account)
| AccountFollowingView Account (Timeline Account)
| AccountView Account
| AccountSelectorView
| GlobalTimelineView
| LocalTimelineView
| ThreadView Thread
@ -172,7 +176,7 @@ type alias Model =
{ server : String
, currentTime : Time
, registration : Maybe AppRegistration
, client : Maybe Client
, clients : List Client
, homeTimeline : Timeline Status
, localTimeline : Timeline Status
, globalTimeline : Timeline Status

View File

@ -165,7 +165,7 @@ update draftMsg currentUser model =
in
{ model | draft = newDraft }
! if query /= "" && atPosition /= Nothing then
[ Command.searchAccounts model.client query model.draft.autoMaxResults False ]
[ Command.searchAccounts (List.head model.clients) query model.draft.autoMaxResults False ]
else
[]

View File

@ -43,6 +43,19 @@ update msg model =
ClearError index ->
{ model | errors = removeAt index model.errors } ! []
SwitchClient client ->
let
newClients =
client :: (List.filter (\c -> c.token /= client.token) model.clients)
in
{ model
| clients = newClients
, currentView = Update.Timeline.preferred model
}
! [ Command.loadUserAccount <| Just client
, Command.loadTimelines <| Just client
]
MastodonEvent msg ->
let
( newModel, commands ) =
@ -67,35 +80,38 @@ update msg model =
model ! [ Command.registerApp model ]
OpenThread status ->
model ! [ Command.loadThread model.client status ]
model ! [ Command.loadThread (List.head model.clients) status ]
OpenAccountSelector ->
{ model | currentView = AccountSelectorView, server = "" } ! []
CloseThread ->
{ model | currentView = Update.Timeline.preferred model } ! []
FollowAccount id ->
model ! [ Command.follow model.client id ]
model ! [ Command.follow (List.head model.clients) id ]
UnfollowAccount id ->
model ! [ Command.unfollow model.client id ]
model ! [ Command.unfollow (List.head model.clients) id ]
DeleteStatus id ->
model ! [ Command.deleteStatus model.client id ]
model ! [ Command.deleteStatus (List.head model.clients) id ]
ReblogStatus id ->
Update.Timeline.processReblog id True model
! [ Command.reblogStatus model.client id ]
! [ Command.reblogStatus (List.head model.clients) id ]
UnreblogStatus id ->
Update.Timeline.processReblog id False model
! [ Command.unreblogStatus model.client id ]
! [ Command.unreblogStatus (List.head model.clients) id ]
AddFavorite id ->
Update.Timeline.processFavourite id True model
! [ Command.favouriteStatus model.client id ]
! [ Command.favouriteStatus (List.head model.clients) id ]
RemoveFavorite id ->
Update.Timeline.processFavourite id False model
! [ Command.unfavouriteStatus model.client id ]
! [ Command.unfavouriteStatus (List.head model.clients) id ]
DraftEvent draftMsg ->
case model.currentUser of
@ -113,7 +129,7 @@ update msg model =
{ model | viewer = viewer } ! [ commands ]
SubmitDraft ->
model ! [ Command.postStatus model.client <| toStatusRequestBody model.draft ]
model ! [ Command.postStatus (List.head model.clients) <| toStatusRequestBody model.draft ]
LoadAccount accountId ->
{ model
@ -123,25 +139,25 @@ update msg model =
, accountRelationships = []
, accountRelationship = Nothing
}
! [ Command.loadAccount model.client accountId ]
! [ Command.loadAccount (List.head model.clients) accountId ]
TimelineLoadNext id next ->
Update.Timeline.markAsLoading True id model
! [ Command.loadNextTimeline model.client model.currentView id next ]
! [ Command.loadNextTimeline (List.head model.clients) model.currentView id next ]
ViewAccountFollowers account ->
{ model
| currentView = AccountFollowersView account model.accountFollowers
, accountRelationships = []
}
! [ Command.loadAccountFollowers model.client account.id Nothing ]
! [ Command.loadAccountFollowers (List.head model.clients) account.id Nothing ]
ViewAccountFollowing account ->
{ model
| currentView = AccountFollowingView account model.accountFollowing
, accountRelationships = []
}
! [ Command.loadAccountFollowing model.client account.id Nothing ]
! [ Command.loadAccountFollowing (List.head model.clients) account.id Nothing ]
ViewAccountStatuses account ->
{ model | currentView = AccountView account } ! []
@ -162,6 +178,9 @@ update msg model =
}
! []
CloseAccountSelector ->
{ model | currentView = Update.Timeline.preferred model } ! []
FilterNotifications filter ->
{ model | notificationFilter = filter } ! []

View File

@ -35,11 +35,11 @@ update msg model =
Ok { decoded } ->
let
client =
Client decoded.server decoded.accessToken
Client decoded.server decoded.accessToken Nothing
in
{ model | client = Just client }
{ model | clients = client :: model.clients }
! [ Command.loadTimelines <| Just client
, Command.saveClient client
, Command.saveClients <| client :: model.clients
, Navigation.modifyUrl model.location.pathname
, Navigation.reload
]
@ -90,7 +90,17 @@ update msg model =
CurrentUser result ->
case result of
Ok { decoded } ->
{ model | currentUser = Just decoded } ! []
let
updatedClients =
case model.clients of
client :: xs ->
({ client | account = Just decoded }) :: xs
_ ->
model.clients
in
{ model | currentUser = Just decoded, clients = updatedClients }
! [ Command.saveClients updatedClients ]
Err error ->
{ model | errors = addErrorNotification (errorText error) model } ! []
@ -182,7 +192,11 @@ update msg model =
| currentView = AccountView decoded
, accountRelationships = []
}
! [ Command.loadAccountTimeline model.client decoded.id model.accountTimeline.links.next ]
! [ Command.loadAccountTimeline
(List.head model.clients)
decoded.id
model.accountTimeline.links.next
]
Err error ->
{ model
@ -203,7 +217,7 @@ update msg model =
case result of
Ok { decoded, links } ->
{ model | accountFollowers = Update.Timeline.update append decoded links model.accountFollowers }
! [ Command.loadRelationships model.client <| List.map .id decoded ]
! [ Command.loadRelationships (List.head model.clients) <| List.map .id decoded ]
Err error ->
{ model | errors = addErrorNotification (errorText error) model } ! []
@ -212,7 +226,7 @@ update msg model =
case result of
Ok { decoded, links } ->
{ model | accountFollowing = Update.Timeline.update append decoded links model.accountFollowing }
! [ Command.loadRelationships model.client <| List.map .id decoded ]
! [ Command.loadRelationships (List.head model.clients) <| List.map .id decoded ]
Err error ->
{ model | errors = addErrorNotification (errorText error) model } ! []

View File

@ -0,0 +1,76 @@
module View.AccountSelector exposing (accountSelectorView)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Mastodon.Helper exposing (..)
import Mastodon.Model exposing (..)
import String.Extra exposing (replace)
import Types exposing (..)
import View.Auth exposing (authForm)
import View.Common exposing (..)
type alias CurrentUser =
Maybe Account
accountIdentityView : CurrentUser -> Client -> Html Msg
accountIdentityView currentUser client =
case client.account of
Just account ->
let
( isCurrentUser, entryClass ) =
case currentUser of
Just currentUser ->
if sameAccount account currentUser then
( True, "active" )
else
( False, "" )
Nothing ->
( False, "" )
in
li [ class <| "list-group-item account-selector-item " ++ entryClass ]
[ accountAvatar "" account
, span []
[ strong []
[ text <|
if account.display_name /= "" then
account.display_name
else
account.username
]
, br [] []
, account.url
|> replace "https://" "@"
|> replace "/@" "@"
|> text
]
, if isCurrentUser then
text ""
else
button
[ class "btn btn-default"
, onClick <| SwitchClient client
]
[ text "Use" ]
]
Nothing ->
text ""
accountSelectorView : Model -> Html Msg
accountSelectorView model =
div [ class "col-md-3 column" ]
[ div [ class "panel panel-default" ]
[ closeablePanelheading "account-selector" "user" "Account selector" CloseAccountSelector
, ul [ class "list-group " ] <|
List.map (accountIdentityView model.currentUser) model.clients
, div [ class "panel-body" ]
[ h3 [] [ text "Add an account" ]
, authForm model
]
]
]

View File

@ -7,6 +7,7 @@ import Html.Attributes exposing (..)
import Mastodon.Model exposing (..)
import Types exposing (..)
import View.Account exposing (accountFollowView, accountTimelineView)
import View.AccountSelector exposing (accountSelectorView)
import View.Auth exposing (authView)
import View.Common as Common
import View.Draft exposing (draftView)
@ -114,6 +115,9 @@ homepageView model =
model.accountRelationship
account
AccountSelectorView ->
accountSelectorView model
AccountFollowersView account followers ->
accountFollowView
currentUser
@ -139,7 +143,7 @@ view : Model -> Html Msg
view model =
div [ class "container-fluid" ]
[ errorsListView model
, case model.client of
, case (List.head model.clients) of
Just client ->
homepageView model

View File

@ -1,4 +1,4 @@
module View.Auth exposing (authView)
module View.Auth exposing (authForm, authView)
import Html exposing (..)
import Html.Attributes exposing (..)
@ -6,6 +6,33 @@ import Html.Events exposing (..)
import Types exposing (..)
authForm : Model -> Html Msg
authForm model =
Html.form [ class "form", onSubmit Register ]
[ div [ class "form-group" ]
[ label [ for "server" ] [ text "Mastodon server root URL" ]
, input
[ type_ "url"
, class "form-control"
, id "server"
, required True
, placeholder "https://mastodon.social"
, value model.server
, pattern "https://.+"
, onInput ServerChange
]
[]
, p [ class "help-block" ]
[ text <|
"You'll be redirected to that server to authenticate yourself. "
++ "We don't have access to your password."
]
]
, button [ class "btn btn-primary", type_ "submit" ]
[ text "Sign into Tooty" ]
]
authView : Model -> Html Msg
authView model =
div [ class "col-md-4 col-md-offset-4" ]
@ -26,29 +53,6 @@ authView model =
, div [ class "panel panel-default" ]
[ div [ class "panel-heading" ] [ text "Authenticate" ]
, div [ class "panel-body" ]
[ Html.form [ class "form", onSubmit Register ]
[ div [ class "form-group" ]
[ label [ for "server" ] [ text "Mastodon server root URL" ]
, input
[ type_ "url"
, class "form-control"
, id "server"
, required True
, placeholder "https://mastodon.social"
, value model.server
, pattern "https://.+"
, onInput ServerChange
]
[]
, p [ class "help-block" ]
[ text <|
"You'll be redirected to that server to authenticate yourself. "
++ "We don't have access to your password."
]
]
, button [ class "btn btn-primary", type_ "submit" ]
[ text "Sign into Tooty" ]
]
]
[ authForm model ]
]
]

View File

@ -1,6 +1,7 @@
module View.Common
exposing
( accountAvatarLink
( accountAvatar
, accountAvatarLink
, accountLink
, closeablePanelheading
, icon
@ -16,6 +17,11 @@ import Types exposing (..)
import View.Events exposing (..)
accountAvatar : String -> Account -> Html Msg
accountAvatar avatarClass account =
img [ class avatarClass, src account.avatar ] []
accountLink : Bool -> Account -> Html Msg
accountLink external account =
let
@ -52,7 +58,7 @@ accountAvatarLink external account =
, accountHref
, title <| "@" ++ account.username
]
[ img [ class avatarClass, src account.avatar ] [] ]
[ accountAvatar avatarClass account ]
closeablePanelheading : String -> String -> String -> Msg -> Html Msg

View File

@ -77,7 +77,15 @@ currentUserView currentUser =
Just currentUser ->
div [ class "current-user" ]
[ Common.accountAvatarLink False currentUser
, div [ class "username" ] [ Common.accountLink False currentUser ]
, div [ class "username" ]
[ Common.accountLink False currentUser
, span []
[ text " ("
, a [ href "", onClickWithPreventAndStop <| OpenAccountSelector ]
[ text "switch account" ]
, text ")"
]
]
, p [ class "status-text" ] <| formatContent currentUser.note []
]