2019-10-19 22:14:43 +02:00
import React , { Component } from "react" ;
2019-09-18 19:52:39 +02:00
import {
withStyles ,
Paper ,
Typography ,
Button ,
TextField ,
Fade ,
Link ,
CircularProgress ,
Tooltip ,
Dialog ,
DialogTitle ,
DialogActions ,
2019-10-03 17:16:21 +02:00
DialogContent ,
List ,
ListItem ,
ListItemText ,
ListItemAvatar ,
ListItemSecondaryAction ,
2020-05-18 17:14:21 +02:00
IconButton ,
InputAdornment
2019-09-18 19:52:39 +02:00
} from "@material-ui/core" ;
import { styles } from "./WelcomePage.styles" ;
import Mastodon from "megalodon" ;
import { SaveClientSession } from "../types/SessionData" ;
2019-09-23 23:28:01 +02:00
import {
createHyperspaceApp ,
getRedirectAddress ,
2019-10-19 22:14:43 +02:00
inDisallowedDomains ,
instancesBearerKey
2019-09-23 23:28:01 +02:00
} from "../utilities/login" ;
2019-09-18 19:52:39 +02:00
import { parseUrl } from "query-string" ;
import { getConfig } from "../utilities/settings" ;
import { isDarwinApp } from "../utilities/desktop" ;
import axios from "axios" ;
import { withSnackbar , withSnackbarProps } from "notistack" ;
import { Config } from "../types/Config" ;
2019-10-03 17:16:21 +02:00
import {
getAccountRegistry ,
loginWithAccount ,
removeAccountFromRegistry
} from "../utilities/accounts" ;
2020-03-24 18:20:59 +01:00
import { MultiAccount } from "../types/Account" ;
2019-10-03 17:16:21 +02:00
import AccountCircleIcon from "@material-ui/icons/AccountCircle" ;
import CloseIcon from "@material-ui/icons/Close" ;
2019-04-23 19:06:31 +02:00
2020-01-21 15:40:47 +01:00
/ * *
* Basic props for Welcome page
* /
2019-04-23 19:06:31 +02:00
interface IWelcomeProps extends withSnackbarProps {
classes : any ;
}
2019-04-07 23:25:39 +02:00
2020-01-21 15:40:47 +01:00
/ * *
* Basic state for welcome page
* /
2019-04-07 23:25:39 +02:00
interface IWelcomeState {
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined URL to the logo to display
* /
2019-04-07 23:25:39 +02:00
logoUrl? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined URL to the background image to display
* /
2019-04-07 23:25:39 +02:00
backgroundUrl? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined brand name of this app
* /
2019-04-07 23:25:39 +02:00
brandName? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined server address to register to
* /
2019-04-07 23:25:39 +02:00
registerBase? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether this version of Hyperspace has federation
* /
2019-04-07 23:25:39 +02:00
federates? : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether Hyperspace is ready to get the auth code
* /
2019-10-03 17:16:21 +02:00
proceedToGetCode : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* The currently "logged-in" user after the first step
* /
2019-04-07 23:25:39 +02:00
user : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether the user ' s input errors
* /
2019-04-07 23:25:39 +02:00
userInputError : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* The user input error message , if any
* /
2019-04-09 22:53:00 +02:00
userInputErrorMessage : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The app ' s client ID , if registered
* /
2019-04-07 23:25:39 +02:00
clientId? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The app ' s client secret , if registered
* /
2019-04-07 23:25:39 +02:00
clientSecret? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The authorization URL provided by Mastodon from the
* client ID and secret
* /
2019-04-07 23:25:39 +02:00
authUrl? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether a previous login attempt is present
* /
2019-04-07 23:25:39 +02:00
foundSavedLogin : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether Hyperspace is in the process of authorizing
* /
2019-10-03 17:16:21 +02:00
authorizing : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined license for the Hyperspace source code
* /
2019-04-12 20:26:27 +02:00
license? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The custom - defined URL to the source code of Hyperspace
* /
2019-04-12 20:26:27 +02:00
repo? : string ;
2020-01-21 15:40:47 +01:00
/ * *
* The default address to redirect to . Used in login inits and
* when the authorization code completes .
* /
2019-04-20 20:59:32 +02:00
defaultRedirectAddress : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether the redirect address is set to 'dynamic' .
* /
redirectAddressIsDynamic : boolean ;
/ * *
* Whether the authorization dialog for the emergency login is
* open .
* /
2019-04-23 01:05:30 +02:00
openAuthDialog : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* The authorization code to fetch an access token with
* /
2019-04-23 01:05:30 +02:00
authCode : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether the Emergency Mode has been initiated
* /
2019-04-23 01:05:30 +02:00
emergencyMode : boolean ;
2020-01-21 15:40:47 +01:00
/ * *
* The current app version
* /
2019-04-26 21:53:40 +02:00
version : string ;
2020-01-21 15:40:47 +01:00
/ * *
* Whether we are in the process of adding a new account or not
* /
2019-10-03 17:16:21 +02:00
willAddAccount : boolean ;
2019-04-07 23:25:39 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* The base class for the Welcome page .
*
* The Welcome page is responsible for handling the registration ,
* login , and authorization of accounts into the Hyperspace app .
* /
2019-04-23 19:06:31 +02:00
class WelcomePage extends Component < IWelcomeProps , IWelcomeState > {
2020-01-21 16:05:05 +01:00
/ * *
* The associated Mastodon client to handle logins / authorizations
* with
* /
2019-04-07 23:25:39 +02:00
client : any ;
2020-01-21 16:05:05 +01:00
/ * *
* Construct the state and other components of the Welcome page
* @param props The properties passed onto the page
* /
2019-04-07 23:25:39 +02:00
constructor ( props : any ) {
super ( props ) ;
2020-01-21 16:05:05 +01:00
// Set up our state
2019-04-07 23:25:39 +02:00
this . state = {
2019-10-03 17:16:21 +02:00
proceedToGetCode : false ,
2019-04-07 23:25:39 +02:00
user : "" ,
userInputError : false ,
foundSavedLogin : false ,
2019-10-03 17:16:21 +02:00
authorizing : false ,
2019-09-18 19:52:39 +02:00
userInputErrorMessage : "" ,
defaultRedirectAddress : "" ,
2020-01-21 15:40:47 +01:00
redirectAddressIsDynamic : false ,
2019-04-23 01:05:30 +02:00
openAuthDialog : false ,
2019-09-18 19:52:39 +02:00
authCode : "" ,
2019-04-26 21:53:40 +02:00
emergencyMode : false ,
2019-10-03 17:16:21 +02:00
version : "" ,
willAddAccount : false
2019-09-18 19:52:39 +02:00
} ;
2019-04-07 23:25:39 +02:00
2020-01-21 16:05:05 +01:00
// Read the configuration data and update the state
2019-09-18 19:52:39 +02:00
getConfig ( )
. then ( ( result : any ) = > {
if ( result !== undefined ) {
let config : Config = result ;
2020-01-21 16:05:05 +01:00
// Warn if the location is dynamic (unexpected behavior)
2020-05-18 17:14:21 +02:00
if ( config . location === "dynamic" ) {
2019-09-18 19:52:39 +02:00
console . warn (
2019-09-23 23:28:01 +02:00
"Redirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!"
) ;
}
2020-01-21 16:05:05 +01:00
// Reset to mastodon.social if the location is a disallowed
// domain.
2019-09-23 23:28:01 +02:00
if (
inDisallowedDomains ( result . registration . defaultInstance )
) {
console . warn (
` The default instance field in config.json contains an unsupported domain ( ${ result . registration . defaultInstance } ), so it's been reset to mastodon.social. `
2019-09-18 19:52:39 +02:00
) ;
2019-09-23 23:28:01 +02:00
result . registration . defaultInstance = "mastodon.social" ;
2019-09-18 19:52:39 +02:00
}
2020-01-21 16:05:05 +01:00
// Update the state as per the configuration
2019-05-11 18:59:36 +02:00
this . setState ( {
2020-05-18 17:14:21 +02:00
logoUrl : config.branding?.logo ? ? "logo.png" ,
backgroundUrl :
config . branding ? . background ? ? "background.png" ,
brandName : config.branding?.name ? ? "Hyperspace" ,
registerBase :
result . registration ? . defaultInstance ? ? "" ,
2019-05-11 18:59:36 +02:00
federates : config.federation.universalLogin ,
license : config.license.url ,
repo : config.repository ,
2019-09-18 19:52:39 +02:00
defaultRedirectAddress :
2020-03-24 18:20:59 +01:00
config . location !== "dynamic"
2019-09-18 19:52:39 +02:00
? config . location
: ` https:// ${ window . location . host } ` ,
2020-03-24 18:20:59 +01:00
redirectAddressIsDynamic : config.location === "dynamic" ,
2019-05-11 18:59:36 +02:00
version : config.version
} ) ;
2019-05-08 15:14:46 +02:00
}
2019-09-18 19:52:39 +02:00
} )
2020-01-21 16:05:05 +01:00
// Print an error if the config wasn't found.
2019-09-18 19:52:39 +02:00
. catch ( ( ) = > {
console . error (
"config.json is missing. If you want to customize Hyperspace, please include config.json"
) ;
} ) ;
2019-04-07 23:25:39 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Look for any existing logins and tokens before presenting
* the login page
* /
2019-05-13 17:22:35 +02:00
componentDidMount() {
if ( localStorage . getItem ( "login" ) ) {
this . getSavedSession ( ) ;
this . setState ( {
foundSavedLogin : true
2019-09-18 19:52:39 +02:00
} ) ;
2019-05-13 17:22:35 +02:00
this . checkForToken ( ) ;
}
}
2020-01-21 16:05:05 +01:00
/ * *
* Update the user field in the state
* @param user The string to update the state to
* /
2019-04-23 19:06:31 +02:00
updateUserInfo ( user : string ) {
this . setState ( { user } ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Update the auth code in the state
* @param code The authorization code to update the state to
* /
2019-04-23 19:06:31 +02:00
updateAuthCode ( code : string ) {
this . setState ( { authCode : code } ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Toggle the visibility of the authorization dialog
* /
2019-04-23 19:06:31 +02:00
toggleAuthDialog() {
this . setState ( { openAuthDialog : ! this . state . openAuthDialog } ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Determine whether the app is ready to open the authorization
* process .
* /
2019-04-23 19:06:31 +02:00
readyForAuth() {
2020-05-18 17:14:21 +02:00
return localStorage . getItem ( "baseurl" ) !== null ;
2019-04-23 19:06:31 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Clear the current access token and base URL
* /
2019-10-03 17:16:21 +02:00
clear() {
localStorage . removeItem ( "access_token" ) ;
localStorage . removeItem ( "baseurl" ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Get the current saved session from the previous login
* attempt and update the state
* /
2019-04-23 19:06:31 +02:00
getSavedSession() {
2020-05-18 17:14:21 +02:00
if ( localStorage . getItem ( "login" ) === null ) {
return ;
2019-04-07 23:25:39 +02:00
}
2020-05-18 17:14:21 +02:00
let loginData = localStorage . getItem ( "login" ) as string ;
let session : SaveClientSession = JSON . parse ( loginData ) ;
this . setState ( {
clientId : session.clientId ,
clientSecret : session.clientSecret ,
authUrl : session.authUrl ,
emergencyMode : session.emergency
} ) ;
2019-04-07 23:25:39 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Start the emergency login mode .
* /
2019-04-23 01:05:30 +02:00
startEmergencyLogin() {
if ( ! this . state . emergencyMode ) {
this . createEmergencyLogin ( ) ;
2019-09-18 19:52:39 +02:00
}
2019-04-23 01:05:30 +02:00
this . toggleAuthDialog ( ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Start the registration process .
* @returns A URL pointing to the signup page of the base as defined
* in the config ' s ` registerBase ` field
* /
2019-04-23 19:06:31 +02:00
startRegistration() {
2020-05-18 17:14:21 +02:00
return this . state . registerBase
? "https://" + this . state . registerBase + "/auth/sign_up"
: "https://joinmastodon.org/#getting-started" ;
2019-04-23 19:06:31 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Watch the keyboard and start the login procedure if the user
* presses the ENTER / RETURN key
* @param event The keyboard event
* /
2019-05-13 17:22:35 +02:00
watchUsernameField ( event : any ) {
2019-09-18 19:52:39 +02:00
if ( event . keyCode === 13 ) this . startLogin ( ) ;
2019-05-13 17:22:35 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Watch the keyboard and start the emergency login auth procedure
* if the user presses the ENTER / RETURN key
* @param event The keyboard event
* /
2019-05-13 17:22:35 +02:00
watchAuthField ( event : any ) {
2019-09-18 19:52:39 +02:00
if ( event . keyCode === 13 ) this . authorizeEmergencyLogin ( ) ;
2019-05-13 17:22:35 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Get the "logged-in" user by reading the username string
* from the first field on the login page .
* @param user The user string to parse
* @returns The base URL of the user
* /
2019-04-07 23:25:39 +02:00
getLoginUser ( user : string ) {
2020-01-21 16:05:05 +01:00
// Did the user include "@"? They probably are not from the
// server defined in config
2019-04-29 20:44:05 +02:00
if ( user . includes ( "@" ) ) {
if ( this . state . federates ) {
let newUser = user ;
this . setState ( { user : newUser } ) ;
return "https://" + newUser . split ( "@" ) [ 1 ] ;
} else {
2020-05-18 17:14:21 +02:00
let newUser = ` ${ user } @ ${ this . state . registerBase ? ?
"mastodon.social" } ` ;
2019-04-29 20:44:05 +02:00
this . setState ( { user : newUser } ) ;
2019-09-18 19:52:39 +02:00
return (
2020-05-18 17:14:21 +02:00
"https://" + ( this . state . registerBase ? ? "mastodon.social" )
2019-09-18 19:52:39 +02:00
) ;
2019-04-29 20:44:05 +02:00
}
2020-01-21 16:05:05 +01:00
}
// Otherwise, treat them as if they're from the server
else {
2020-05-18 17:14:21 +02:00
let newUser = ` ${ user } @ ${ this . state . registerBase ? ?
"mastodon.social" } ` ;
2019-04-07 23:25:39 +02:00
this . setState ( { user : newUser } ) ;
2020-05-18 17:14:21 +02:00
return "https://" + ( this . state . registerBase ? ? "mastodon.social" ) ;
2019-04-07 23:25:39 +02:00
}
}
2020-01-21 16:05:05 +01:00
/ * *
* Check the user string for any errors and then create a client with an
* ID and secret to start the authorization process .
* /
2019-04-07 23:25:39 +02:00
startLogin() {
2020-01-21 16:05:05 +01:00
// Check if we have errored
2019-04-12 20:26:27 +02:00
let error = this . checkForErrors ( ) ;
2020-01-21 16:05:05 +01:00
// If we didn't, create the Hyperspace app to register onto that Mastodon
// server.
2019-04-12 20:26:27 +02:00
if ( ! error ) {
2020-01-21 16:05:05 +01:00
// Define the app's scopes and base URL
2019-09-18 19:52:39 +02:00
const scopes = "read write follow" ;
2019-04-07 23:25:39 +02:00
const baseurl = this . getLoginUser ( this . state . user ) ;
localStorage . setItem ( "baseurl" , baseurl ) ;
2020-01-21 16:05:05 +01:00
// Create the Hyperspace app
2019-04-28 21:50:18 +02:00
createHyperspaceApp (
2020-05-18 17:14:21 +02:00
this . state . brandName ? ? "Hyperspace" ,
2019-09-18 19:52:39 +02:00
scopes ,
baseurl ,
2019-05-11 22:03:40 +02:00
getRedirectAddress ( this . state . defaultRedirectAddress )
2020-01-21 16:05:05 +01:00
)
// If we succeeded, create a login attempt for later reference
. then ( ( resp : any ) = > {
let saveSessionForCrashing : SaveClientSession = {
clientId : resp.clientId ,
clientSecret : resp.clientSecret ,
authUrl : resp.url ,
emergency : false
} ;
localStorage . setItem (
"login" ,
JSON . stringify ( saveSessionForCrashing )
) ;
// Finally, update the state
this . setState ( {
clientId : resp.clientId ,
clientSecret : resp.clientSecret ,
authUrl : resp.url ,
proceedToGetCode : true
} ) ;
2019-09-18 19:52:39 +02:00
} ) ;
2019-04-07 23:25:39 +02:00
}
}
2020-01-21 16:05:05 +01:00
/ * *
* Create an emergency mode login . This is usually initiated when the
* "click-to-authorize" method fails and the user needs to copy and paste
* an authorization code manually .
* /
2019-04-23 01:05:30 +02:00
createEmergencyLogin() {
2019-09-18 19:52:39 +02:00
console . log ( "Creating an emergency login..." ) ;
2020-01-21 16:05:05 +01:00
// Set up the scopes and base URL
2019-04-23 01:05:30 +02:00
const scopes = "read write follow" ;
2019-09-18 19:52:39 +02:00
const baseurl =
localStorage . getItem ( "baseurl" ) ||
this . getLoginUser ( this . state . user ) ;
2020-01-21 16:05:05 +01:00
// Register the Mastodon app with the Mastodon server
2019-04-28 21:50:18 +02:00
Mastodon . registerApp (
2020-05-18 17:14:21 +02:00
this . state . brandName ? ? "Hyperspace" ,
2019-04-28 21:50:18 +02:00
{
scopes : scopes
2019-09-18 19:52:39 +02:00
} ,
2019-04-28 21:50:18 +02:00
baseurl
2020-01-21 16:05:05 +01:00
)
// If we succeed, create a login attempt for later reference
. then ( ( appData : any ) = > {
let saveSessionForCrashing : SaveClientSession = {
clientId : appData.clientId ,
clientSecret : appData.clientSecret ,
authUrl : appData.url ,
emergency : true
} ;
localStorage . setItem (
"login" ,
JSON . stringify ( saveSessionForCrashing )
) ;
// Finally, update the state
this . setState ( {
clientId : appData.clientId ,
clientSecret : appData.clientSecret ,
authUrl : appData.url
} ) ;
2019-04-23 01:05:30 +02:00
} ) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Open the URL to redirect to an authorization sequence from an emergency
* login .
*
* Since Hyperspace reads the auth code from the URL , we need to redirect to
* a URL with the code inside to trigger an auth
* /
2019-04-23 01:05:30 +02:00
authorizeEmergencyLogin() {
2019-11-22 22:22:17 +01:00
let redirAddress =
this . state . defaultRedirectAddress === "desktop"
? "hyperspace://hyperspace/app"
: this . state . defaultRedirectAddress ;
window . location . href = ` ${ redirAddress } /?code= ${ this . state . authCode } #/ ` ;
2019-04-23 01:05:30 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Restore a login attempt from a session
* /
2019-04-07 23:25:39 +02:00
resumeLogin() {
let loginData = localStorage . getItem ( "login" ) ;
if ( loginData ) {
let session : SaveClientSession = JSON . parse ( loginData ) ;
this . setState ( {
clientId : session.clientId ,
clientSecret : session.clientSecret ,
authUrl : session.authUrl ,
2019-04-23 01:05:30 +02:00
emergencyMode : session.emergency ,
2019-10-03 17:16:21 +02:00
proceedToGetCode : true
2019-09-18 19:52:39 +02:00
} ) ;
2019-04-07 23:25:39 +02:00
}
}
2020-01-21 16:05:05 +01:00
/ * *
* Check the user input string for any possible errors
* /
2019-04-12 20:26:27 +02:00
checkForErrors ( ) : boolean {
2019-04-09 22:53:00 +02:00
let userInputError = false ;
let userInputErrorMessage = "" ;
2020-01-21 16:05:05 +01:00
// Is the user string blank?
2019-04-09 22:53:00 +02:00
if ( this . state . user === "" ) {
userInputError = true ;
2019-04-12 20:26:27 +02:00
userInputErrorMessage = "Username cannot be blank." ;
2019-04-09 22:53:00 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return true ;
2019-04-09 22:53:00 +02:00
} else {
if ( this . state . user . includes ( "@" ) ) {
2019-09-18 19:52:39 +02:00
if ( this . state . federates && this . state . federates === true ) {
2019-04-27 19:37:36 +02:00
let baseUrl = this . state . user . split ( "@" ) [ 1 ] ;
2020-01-21 16:05:05 +01:00
// Is the user's domain in the disallowed list?
2019-09-23 23:28:01 +02:00
if ( inDisallowedDomains ( baseUrl ) ) {
this . setState ( {
userInputError : true ,
userInputErrorMessage : ` Signing in with an account from ${ baseUrl } isn't supported. `
2019-09-18 19:52:39 +02:00
} ) ;
2019-09-23 23:28:01 +02:00
return true ;
} else {
2020-01-21 16:05:05 +01:00
// Are we unable to ping the server?
2019-09-23 23:28:01 +02:00
axios
. get (
2019-10-19 22:14:43 +02:00
"https://instances.social/api/1.0/instances/show?name=" +
baseUrl ,
{
headers : {
Authorization : ` Bearer ${ instancesBearerKey } `
}
}
2019-09-23 23:28:01 +02:00
)
. catch ( ( err : Error ) = > {
let userInputError = true ;
let userInputErrorMessage =
"Instance name is invalid." ;
this . setState ( {
userInputError ,
userInputErrorMessage
} ) ;
return true ;
} ) ;
}
2019-09-18 19:52:39 +02:00
} else if (
this . state . user . includes (
2020-05-18 17:14:21 +02:00
this . state . registerBase ? ? "mastodon.social"
2019-09-18 19:52:39 +02:00
)
) {
2019-04-27 19:37:36 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
return false ;
} else {
userInputError = true ;
2019-09-18 19:52:39 +02:00
userInputErrorMessage =
"You cannot sign in with this username." ;
2019-04-09 22:53:00 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return true ;
2019-04-27 19:37:36 +02:00
}
2019-04-09 22:53:00 +02:00
} else {
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return false ;
2019-04-09 22:53:00 +02:00
}
2019-04-27 19:37:36 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return false ;
2019-04-09 22:53:00 +02:00
}
2019-04-07 23:25:39 +02:00
}
2020-01-21 16:05:05 +01:00
/ * *
* Read the URL and determine whether or not there ' s an auth code
* in the URL . If there is , try to authorize and get the access
* token for storage .
* /
2019-04-23 19:06:31 +02:00
checkForToken() {
let location = window . location . href ;
2020-01-21 16:05:05 +01:00
// Is there an auth code?
2019-04-23 19:06:31 +02:00
if ( location . includes ( "?code=" ) ) {
let code = parseUrl ( location ) . query . code as string ;
2019-10-03 17:16:21 +02:00
this . setState ( { authorizing : true } ) ;
2019-04-23 19:06:31 +02:00
let loginData = localStorage . getItem ( "login" ) ;
2020-01-21 16:05:05 +01:00
// If there's login data, try to fetch an access token
2019-04-23 19:06:31 +02:00
if ( loginData ) {
2019-09-18 19:52:39 +02:00
let clientLoginSession : SaveClientSession = JSON . parse (
loginData
) ;
2020-01-21 16:41:45 +01:00
2020-02-07 16:09:47 +01:00
getConfig ( ) . then ( ( resp : any ) = > {
2020-03-24 18:20:59 +01:00
if ( resp === undefined ) {
2020-02-07 16:09:47 +01:00
return ;
2020-01-21 16:41:45 +01:00
}
2020-02-07 16:09:47 +01:00
let conf : Config = resp ;
let redirectUrl : string | undefined =
this . state . emergencyMode ||
clientLoginSession . authUrl . includes (
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
)
? undefined
: getRedirectAddress ( conf . location ) ;
Mastodon . fetchAccessToken (
clientLoginSession . clientId ,
clientLoginSession . clientSecret ,
code ,
localStorage . getItem ( "baseurl" ) as string ,
redirectUrl
)
. then ( ( tokenData : any ) = > {
localStorage . setItem (
"access_token" ,
tokenData . access_token
) ;
window . location . href =
window . location . protocol === "hyperspace:"
? "hyperspace://hyperspace/app/"
: this . state . defaultRedirectAddress ;
} )
. catch ( ( err : Error ) = > {
this . props . enqueueSnackbar (
2020-05-18 17:14:21 +02:00
` Couldn't authorize ${ this . state . brandName ? ?
"Hyperspace" } : $ { err . name } ` ,
2020-02-07 16:09:47 +01:00
{ variant : "error" }
) ;
console . error ( err . message ) ;
} ) ;
2020-01-21 16:41:45 +01:00
} ) ;
2019-04-23 19:06:31 +02:00
}
2019-04-07 23:25:39 +02:00
}
}
2020-01-21 15:40:47 +01:00
/ * *
* Redirect to the app ' s main view after a login .
* /
redirectToApp() {
window . location . href =
window . location . protocol === "hyperspace:"
? "hyperspace://hyperspace/app"
: this . state . redirectAddressIsDynamic
? ` https:// ${ window . location . host } /#/ `
: this . state . defaultRedirectAddress + "/#/" ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Render the title bar for macOS
* /
2019-05-12 20:11:21 +02:00
titlebar() {
const { classes } = this . props ;
2019-05-16 17:00:37 +02:00
if ( isDarwinApp ( ) ) {
2019-09-18 19:52:39 +02:00
return (
< div className = { classes . titleBarRoot } >
< Typography className = { classes . titleBarText } >
2020-05-18 17:14:21 +02:00
{ this . state . brandName ? ? "Hyperspace" }
2019-09-18 19:52:39 +02:00
< / Typography >
< / div >
) ;
2019-05-12 20:11:21 +02:00
}
2019-09-18 19:52:39 +02:00
}
2019-05-12 20:11:21 +02:00
2020-01-21 16:05:05 +01:00
/ * *
* Show the multi - user account panel
* /
2019-10-03 17:16:21 +02:00
showMultiAccount() {
const { classes } = this . props ;
return (
< div >
< Typography variant = "h5" > Select an account < / Typography >
< Typography > from the list below or add a new one < / Typography >
< List >
{ getAccountRegistry ( ) . map (
( account : MultiAccount , index : number ) = > (
< ListItem
onClick = { ( ) = > {
loginWithAccount ( account ) ;
2020-01-21 15:40:47 +01:00
this . redirectToApp ( ) ;
2019-10-03 17:16:21 +02:00
} }
button = { true }
>
< ListItemAvatar >
< AccountCircleIcon color = "action" / >
< / ListItemAvatar >
< ListItemText
primary = { ` @ ${ account . username } ` }
secondary = { account . host }
/ >
< ListItemSecondaryAction >
< IconButton
onClick = { ( e : any ) = > {
e . preventDefault ( ) ;
removeAccountFromRegistry ( index ) ;
window . location . reload ( ) ;
} }
>
< CloseIcon / >
< / IconButton >
< / ListItemSecondaryAction >
< / ListItem >
)
) }
< / List >
< div className = { classes . middlePadding } / >
< Button
onClick = { ( ) = > {
this . setState ( { willAddAccount : true } ) ;
this . clear ( ) ;
} }
color = { "primary" }
variant = { "contained" }
>
Add Account
< / Button >
< / div >
) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Show the main landing panel
* /
2019-04-07 23:25:39 +02:00
showLanding() {
const { classes } = this . props ;
return (
2019-09-18 19:52:39 +02:00
< div >
< Typography variant = "h5" > Sign in < / Typography >
2020-05-18 17:14:21 +02:00
< Typography > with your fediverse account < / Typography >
2019-09-18 19:52:39 +02:00
< div className = { classes . middlePadding } / >
< TextField
variant = "outlined"
label = "Username"
fullWidth
placeholder = "example@mastodon.example"
onChange = { event = > this . updateUserInfo ( event . target . value ) }
onKeyDown = { event = > this . watchUsernameField ( event ) }
error = { this . state . userInputError }
onBlur = { ( ) = > this . checkForErrors ( ) }
2020-05-18 17:14:21 +02:00
InputProps = { {
startAdornment : (
< InputAdornment position = "start" > @ < / InputAdornment >
)
} }
2019-10-03 17:16:21 +02:00
/ >
2019-09-18 19:52:39 +02:00
{ this . state . userInputError ? (
< Typography color = "error" >
{ this . state . userInputErrorMessage }
< / Typography >
) : null }
< br / >
{ this . state . registerBase && this . state . federates ? (
< Typography variant = "caption" >
Not from { " " }
2020-05-18 17:14:21 +02:00
< b > { this . state . registerBase ? ? "noinstance" } < / b > ? Sign
in with your { " " }
2019-09-18 19:52:39 +02:00
< Link
2020-03-29 01:43:43 +01:00
href = "https://docs.joinmastodon.org/user/signup/#address"
2019-09-18 19:52:39 +02:00
target = "_blank"
rel = "noopener noreferrer"
color = "secondary"
>
full username
< / Link >
.
< / Typography >
) : null }
< br / >
{ this . state . foundSavedLogin ? (
< Typography >
Signing in from a previous session ? { " " }
< Link
className = { classes . welcomeLink }
onClick = { ( ) = > this . resumeLogin ( ) }
>
Continue login
< / Link >
.
< / Typography >
) : null }
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
< Tooltip title = "Create account on site" >
< Button
href = { this . startRegistration ( ) }
target = "_blank"
rel = "noreferrer"
>
Create account
< / Button >
< / Tooltip >
< div className = { classes . flexGrow } / >
< Tooltip title = "Continue sign-in" >
< Button
color = "primary"
variant = "contained"
onClick = { ( ) = > this . startLogin ( ) }
>
Next
< / Button >
< / Tooltip >
2019-04-07 23:25:39 +02:00
< / div >
2019-09-18 19:52:39 +02:00
< / div >
2019-04-07 23:25:39 +02:00
) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Show the login auth panel
* /
2019-04-07 23:25:39 +02:00
showLoginAuth() {
const { classes } = this . props ;
return (
2019-09-18 19:52:39 +02:00
< div >
< Typography variant = "h5" >
2020-05-18 17:14:21 +02:00
Howdy , { this . state . user ? . split ( "@" ) [ 0 ] ? ? "user" }
2019-09-18 19:52:39 +02:00
< / Typography >
< Typography >
To continue , finish signing in on your instance ' s website
2020-05-18 17:14:21 +02:00
and authorize { this . state . brandName ? ? "Hyperspace" } .
2019-09-18 19:52:39 +02:00
< / Typography >
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
< div className = { classes . flexGrow } / >
< Button
color = "primary"
variant = "contained"
size = "large"
2020-05-18 17:14:21 +02:00
href = { this . state . authUrl ? ? "" }
2019-09-18 19:52:39 +02:00
>
Authorize
< / Button >
< div className = { classes . flexGrow } / >
2019-04-07 23:25:39 +02:00
< / div >
2019-09-18 19:52:39 +02:00
< div className = { classes . middlePadding } / >
< Typography >
Having trouble signing in ? { " " }
< Link
onClick = { ( ) = > this . startEmergencyLogin ( ) }
className = { classes . welcomeLink }
>
Sign in with a code .
< / Link >
< / Typography >
< / div >
2019-04-07 23:25:39 +02:00
) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Show the emergency login panel
* /
2019-04-23 01:05:30 +02:00
showAuthDialog() {
return (
< Dialog
open = { this . state . openAuthDialog }
disableBackdropClick
disableEscapeKeyDown
maxWidth = "sm"
fullWidth = { true }
>
2019-09-18 19:52:39 +02:00
< DialogTitle > Authorize with a code < / DialogTitle >
2019-04-23 01:05:30 +02:00
< DialogContent >
< Typography paragraph >
2019-09-18 19:52:39 +02:00
If you ' re having trouble authorizing Hyperspace , you can
manually request for an authorization code . Click
'Request Code' and then paste the code in the
authorization code box to continue .
2019-04-23 01:05:30 +02:00
< / Typography >
< Button
2019-09-18 19:52:39 +02:00
color = "primary"
variant = "contained"
2020-05-18 17:14:21 +02:00
href = { this . state . authUrl ? ? "" }
2019-04-23 01:05:30 +02:00
target = "_blank"
rel = "noopener noreferrer"
2019-09-18 19:52:39 +02:00
>
Request Code
< / Button >
< br / >
< br / >
2019-04-23 01:05:30 +02:00
< TextField
variant = "outlined"
label = "Authorization code"
fullWidth
2019-09-18 19:52:39 +02:00
onChange = { event = >
this . updateAuthCode ( event . target . value )
}
onKeyDown = { event = > this . watchAuthField ( event ) }
2019-10-03 17:16:21 +02:00
/ >
2019-04-23 01:05:30 +02:00
< / DialogContent >
< DialogActions >
2019-09-18 19:52:39 +02:00
< Button onClick = { ( ) = > this . toggleAuthDialog ( ) } >
Cancel
< / Button >
< Button
color = "secondary"
onClick = { ( ) = > this . authorizeEmergencyLogin ( ) }
>
Authorize
< / Button >
2019-04-23 01:05:30 +02:00
< / DialogActions >
< / Dialog >
) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Show the authorizing panel
* /
2019-10-03 17:16:21 +02:00
showAuthorizationLoader() {
2019-04-07 23:25:39 +02:00
const { classes } = this . props ;
return (
2019-09-18 19:52:39 +02:00
< div >
< Typography variant = "h5" > Authorizing < / Typography >
< Typography >
2020-05-18 17:14:21 +02:00
Please wait while Hyperspace authorizes with your instance .
This shouldn ' t take long . . .
2019-09-18 19:52:39 +02:00
< / Typography >
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
< div className = { classes . flexGrow } / >
< CircularProgress / >
< div className = { classes . flexGrow } / >
2019-04-07 23:25:39 +02:00
< / div >
2019-09-18 19:52:39 +02:00
< div className = { classes . middlePadding } / >
< / div >
2019-04-07 23:25:39 +02:00
) ;
}
2020-01-21 16:05:05 +01:00
/ * *
* Render the page
* /
2019-04-07 23:25:39 +02:00
render() {
const { classes } = this . props ;
return (
2019-05-12 20:11:21 +02:00
< div >
{ this . titlebar ( ) }
2019-09-18 19:52:39 +02:00
< div
className = { classes . root }
style = { {
2020-05-18 17:14:21 +02:00
backgroundImage : ` url( ${ this . state . backgroundUrl ? ?
"background.png" } ) `
2019-09-18 19:52:39 +02:00
} }
>
2019-05-12 20:11:21 +02:00
< Paper className = { classes . paper } >
2019-09-18 19:52:39 +02:00
< img
className = { classes . logo }
2020-05-18 17:14:21 +02:00
alt = { this . state . brandName ? ? "Hyperspace" }
src = { this . state . logoUrl ? ? "logo.png" }
2019-09-18 19:52:39 +02:00
/ >
< br / >
< Fade in = { true } >
2019-10-03 17:16:21 +02:00
{ this . state . authorizing
? this . showAuthorizationLoader ( )
: this . state . proceedToGetCode
? this . showLoginAuth ( )
: getAccountRegistry ( ) . length > 0 &&
! this . state . willAddAccount
? this . showMultiAccount ( )
: this . showLanding ( ) }
2019-05-12 20:11:21 +02:00
< / Fade >
2019-09-18 19:52:39 +02:00
< br / >
2019-05-12 20:11:21 +02:00
< Typography variant = "caption" >
2019-09-18 19:52:39 +02:00
& copy ; { new Date ( ) . getFullYear ( ) } { " " }
{ this . state . brandName &&
this . state . brandName !== "Hyperspace"
? ` ${ this . state . brandName } developers and the `
: "" } { " " }
< Link
className = { classes . welcomeLink }
href = "https://hyperspace.marquiskurt.net"
target = "_blank"
rel = "noreferrer"
>
Hyperspace
< / Link > { " " }
developers . All rights reserved .
2019-05-12 20:11:21 +02:00
< / Typography >
< Typography variant = "caption" >
2019-09-18 19:52:39 +02:00
{ this . state . repo ? (
< span >
< Link
className = { classes . welcomeLink }
href = {
2020-05-18 17:14:21 +02:00
this . state . repo ? ?
"https://github.com/hyperspacedev"
2019-09-18 19:52:39 +02:00
}
target = "_blank"
rel = "noreferrer"
>
Source code
< / Link > { " " }
| { " " }
< / span >
) : null }
< Link
className = { classes . welcomeLink }
href = {
2020-05-18 17:14:21 +02:00
this . state . license ? ?
"https://thufie.lain.haus/NPL.html"
2019-09-18 19:52:39 +02:00
}
target = "_blank"
rel = "noreferrer"
>
License
< / Link > { " " }
|
< Link
className = { classes . welcomeLink }
href = "https://github.com/hyperspacedev/hyperspace/issues/new"
target = "_blank"
rel = "noreferrer"
>
File an Issue
< / Link >
2019-05-12 20:11:21 +02:00
< / Typography >
< Typography variant = "caption" color = "textSecondary" >
2020-05-18 17:14:21 +02:00
{ this . state . brandName ? ? "Hypersapce" } v .
2019-09-23 23:28:01 +02:00
{ this . state . version } { " " }
2019-09-18 19:52:39 +02:00
{ this . state . brandName &&
this . state . brandName !== "Hyperspace"
? "(Hyperspace-like)"
: null }
2019-05-12 20:11:21 +02:00
< / Typography >
< / Paper >
{ this . showAuthDialog ( ) }
< / div >
< / div >
2019-04-07 23:25:39 +02:00
) ;
}
}
2019-10-03 17:16:21 +02:00
export default withStyles ( styles ) ( withSnackbar ( WelcomePage ) ) ;