1099 lines
38 KiB
TypeScript
1099 lines
38 KiB
TypeScript
import React, { Component } from "react";
|
|
import {
|
|
withStyles,
|
|
Paper,
|
|
Typography,
|
|
Button,
|
|
TextField,
|
|
Fade,
|
|
Link,
|
|
CircularProgress,
|
|
Tooltip,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogActions,
|
|
DialogContent,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemAvatar,
|
|
ListItemSecondaryAction,
|
|
IconButton,
|
|
InputAdornment
|
|
} from "@material-ui/core";
|
|
import { styles } from "./WelcomePage.styles";
|
|
import Mastodon from "megalodon";
|
|
import { SaveClientSession } from "../types/SessionData";
|
|
import {
|
|
createHyperspaceApp,
|
|
getRedirectAddress,
|
|
inDisallowedDomains,
|
|
instancesBearerKey
|
|
} from "../utilities/login";
|
|
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";
|
|
import {
|
|
getAccountRegistry,
|
|
loginWithAccount,
|
|
removeAccountFromRegistry
|
|
} from "../utilities/accounts";
|
|
import { MultiAccount } from "../types/Account";
|
|
|
|
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
|
import CloseIcon from "@material-ui/icons/Close";
|
|
|
|
/**
|
|
* Basic props for Welcome page
|
|
*/
|
|
interface IWelcomeProps extends withSnackbarProps {
|
|
classes: any;
|
|
}
|
|
|
|
/**
|
|
* Basic state for welcome page
|
|
*/
|
|
interface IWelcomeState {
|
|
/**
|
|
* The custom-defined URL to the logo to display
|
|
*/
|
|
logoUrl?: string;
|
|
|
|
/**
|
|
* The custom-defined URL to the background image to display
|
|
*/
|
|
backgroundUrl?: string;
|
|
|
|
/**
|
|
* The custom-defined brand name of this app
|
|
*/
|
|
brandName?: string;
|
|
|
|
/**
|
|
* The custom-defined server address to register to
|
|
*/
|
|
registerBase?: string;
|
|
|
|
/**
|
|
* Whether this version of Hyperspace has federation
|
|
*/
|
|
federates?: boolean;
|
|
|
|
/**
|
|
* Whether Hyperspace is ready to get the auth code
|
|
*/
|
|
proceedToGetCode: boolean;
|
|
|
|
/**
|
|
* The currently "logged-in" user after the first step
|
|
*/
|
|
user: string;
|
|
|
|
/**
|
|
* Whether the user's input errors
|
|
*/
|
|
userInputError: boolean;
|
|
|
|
/**
|
|
* The user input error message, if any
|
|
*/
|
|
userInputErrorMessage: string;
|
|
|
|
/**
|
|
* The app's client ID, if registered
|
|
*/
|
|
clientId?: string;
|
|
|
|
/**
|
|
* The app's client secret, if registered
|
|
*/
|
|
clientSecret?: string;
|
|
|
|
/**
|
|
* The authorization URL provided by Mastodon from the
|
|
* client ID and secret
|
|
*/
|
|
authUrl?: string;
|
|
|
|
/**
|
|
* Whether a previous login attempt is present
|
|
*/
|
|
foundSavedLogin: boolean;
|
|
|
|
/**
|
|
* Whether Hyperspace is in the process of authorizing
|
|
*/
|
|
authorizing: boolean;
|
|
|
|
/**
|
|
* The custom-defined license for the Hyperspace source code
|
|
*/
|
|
license?: string;
|
|
|
|
/**
|
|
* The custom-defined URL to the source code of Hyperspace
|
|
*/
|
|
repo?: string;
|
|
|
|
/**
|
|
* The default address to redirect to. Used in login inits and
|
|
* when the authorization code completes.
|
|
*/
|
|
defaultRedirectAddress: string;
|
|
|
|
/**
|
|
* Whether the redirect address is set to 'dynamic'.
|
|
*/
|
|
redirectAddressIsDynamic: boolean;
|
|
|
|
/**
|
|
* Whether the authorization dialog for the emergency login is
|
|
* open.
|
|
*/
|
|
openAuthDialog: boolean;
|
|
|
|
/**
|
|
* The authorization code to fetch an access token with
|
|
*/
|
|
authCode: string;
|
|
|
|
/**
|
|
* Whether the Emergency Mode has been initiated
|
|
*/
|
|
emergencyMode: boolean;
|
|
|
|
/**
|
|
* The current app version
|
|
*/
|
|
version: string;
|
|
|
|
/**
|
|
* Whether we are in the process of adding a new account or not
|
|
*/
|
|
willAddAccount: boolean;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|
/**
|
|
* The associated Mastodon client to handle logins/authorizations
|
|
* with
|
|
*/
|
|
client: any;
|
|
|
|
/**
|
|
* Construct the state and other components of the Welcome page
|
|
* @param props The properties passed onto the page
|
|
*/
|
|
constructor(props: any) {
|
|
super(props);
|
|
|
|
// Set up our state
|
|
this.state = {
|
|
proceedToGetCode: false,
|
|
user: "",
|
|
userInputError: false,
|
|
foundSavedLogin: false,
|
|
authorizing: false,
|
|
userInputErrorMessage: "",
|
|
defaultRedirectAddress: "",
|
|
redirectAddressIsDynamic: false,
|
|
openAuthDialog: false,
|
|
authCode: "",
|
|
emergencyMode: false,
|
|
version: "",
|
|
willAddAccount: false
|
|
};
|
|
|
|
// Read the configuration data and update the state
|
|
getConfig()
|
|
.then((result: any) => {
|
|
if (result !== undefined) {
|
|
let config: Config = result;
|
|
|
|
// Warn if the location is dynamic (unexpected behavior)
|
|
if (config.location === "dynamic") {
|
|
console.warn(
|
|
"Redirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!"
|
|
);
|
|
}
|
|
|
|
// Reset to mastodon.online if the location is a disallowed
|
|
// domain.
|
|
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.online.`
|
|
);
|
|
result.registration.defaultInstance = "mastodon.online";
|
|
}
|
|
|
|
// Update the state as per the configuration
|
|
this.setState({
|
|
logoUrl: config.branding?.logo ?? "logo.png",
|
|
backgroundUrl:
|
|
config.branding?.background ?? "background.png",
|
|
brandName: config.branding?.name ?? "Hyperspace",
|
|
registerBase:
|
|
result.registration?.defaultInstance ?? "",
|
|
federates: config.federation.universalLogin,
|
|
license: config.license.url,
|
|
repo: config.repository,
|
|
defaultRedirectAddress:
|
|
config.location !== "dynamic"
|
|
? config.location
|
|
: `https://${window.location.host}`,
|
|
redirectAddressIsDynamic: config.location === "dynamic",
|
|
version: config.version
|
|
});
|
|
}
|
|
})
|
|
|
|
// Print an error if the config wasn't found.
|
|
.catch(() => {
|
|
console.error(
|
|
"config.json is missing. If you want to customize Hyperspace, please include config.json"
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Look for any existing logins and tokens before presenting
|
|
* the login page
|
|
*/
|
|
componentDidMount() {
|
|
if (localStorage.getItem("login")) {
|
|
this.getSavedSession();
|
|
this.setState({
|
|
foundSavedLogin: true
|
|
});
|
|
this.checkForToken();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the user field in the state
|
|
* @param user The string to update the state to
|
|
*/
|
|
updateUserInfo(user: string) {
|
|
this.checkForErrors(user);
|
|
this.setState({ user });
|
|
}
|
|
|
|
/**
|
|
* Update the auth code in the state
|
|
* @param code The authorization code to update the state to
|
|
*/
|
|
updateAuthCode(code: string) {
|
|
this.setState({ authCode: code });
|
|
}
|
|
|
|
/**
|
|
* Toggle the visibility of the authorization dialog
|
|
*/
|
|
toggleAuthDialog() {
|
|
this.setState({ openAuthDialog: !this.state.openAuthDialog });
|
|
}
|
|
|
|
/**
|
|
* Determine whether the app is ready to open the authorization
|
|
* process.
|
|
*/
|
|
readyForAuth() {
|
|
return localStorage.getItem("baseurl") !== null;
|
|
}
|
|
|
|
/**
|
|
* Clear the current access token and base URL
|
|
*/
|
|
clear() {
|
|
localStorage.removeItem("access_token");
|
|
localStorage.removeItem("baseurl");
|
|
}
|
|
|
|
/**
|
|
* Get the current saved session from the previous login
|
|
* attempt and update the state
|
|
*/
|
|
getSavedSession() {
|
|
if (localStorage.getItem("login") === null) {
|
|
return;
|
|
}
|
|
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
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start the emergency login mode.
|
|
*/
|
|
startEmergencyLogin() {
|
|
if (!this.state.emergencyMode) {
|
|
this.createEmergencyLogin();
|
|
}
|
|
this.toggleAuthDialog();
|
|
}
|
|
|
|
/**
|
|
* Start the registration process.
|
|
* @returns A URL pointing to the signup page of the base as defined
|
|
* in the config's `registerBase` field
|
|
*/
|
|
startRegistration() {
|
|
return this.state.registerBase
|
|
? "https://" + this.state.registerBase + "/auth/sign_up"
|
|
: "https://joinmastodon.org/#getting-started";
|
|
}
|
|
|
|
/**
|
|
* Watch the keyboard and start the login procedure if the user
|
|
* presses the ENTER/RETURN key
|
|
* @param event The keyboard event
|
|
*/
|
|
watchUsernameField(event: any) {
|
|
if (event.keyCode === 13) this.startLogin();
|
|
}
|
|
|
|
/**
|
|
* Watch the keyboard and start the emergency login auth procedure
|
|
* if the user presses the ENTER/RETURN key
|
|
* @param event The keyboard event
|
|
*/
|
|
watchAuthField(event: any) {
|
|
if (event.keyCode === 13) this.authorizeEmergencyLogin();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
getLoginUser(user: string) {
|
|
// Did the user include "@"? They probably are not from the
|
|
// server defined in config
|
|
if (user.includes("@")) {
|
|
if (this.state.federates) {
|
|
let newUser = user;
|
|
this.setState({ user: newUser });
|
|
return "https://" + newUser.split("@")[1];
|
|
} else {
|
|
let newUser = `${user}@${this.state.registerBase ??
|
|
"mastodon.online"}`;
|
|
this.setState({ user: newUser });
|
|
return (
|
|
"https://" + (this.state.registerBase ?? "mastodon.online")
|
|
);
|
|
}
|
|
}
|
|
|
|
// Otherwise, treat them as if they're from the server
|
|
else {
|
|
let newUser = `${user}@${this.state.registerBase ??
|
|
"mastodon.online"}`;
|
|
this.setState({ user: newUser });
|
|
return "https://" + (this.state.registerBase ?? "mastodon.online");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the user string for any errors and then create a client with an
|
|
* ID and secret to start the authorization process.
|
|
* @param bypassChecks Whether to bypass the checks in place.
|
|
*/
|
|
startLogin(bypassChecks: boolean = false) {
|
|
// Check if we have errored
|
|
let error = this.checkForErrors(this.state.user, bypassChecks);
|
|
|
|
// If we didn't, create the Hyperspace app to register onto that Mastodon
|
|
// server.
|
|
if (!error) {
|
|
// Define the app's scopes and base URL
|
|
const scopes = "read write follow";
|
|
const baseurl = this.getLoginUser(this.state.user);
|
|
localStorage.setItem("baseurl", baseurl);
|
|
|
|
// Create the Hyperspace app
|
|
createHyperspaceApp(
|
|
this.state.brandName ?? "Hyperspace",
|
|
scopes,
|
|
baseurl,
|
|
getRedirectAddress(this.state.defaultRedirectAddress)
|
|
)
|
|
// 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
|
|
});
|
|
})
|
|
.catch((err: Error) => {
|
|
this.props.enqueueSnackbar(
|
|
`Failed to register app at ${baseurl.replace(
|
|
"https://",
|
|
""
|
|
)}`
|
|
);
|
|
console.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
createEmergencyLogin() {
|
|
console.log("Creating an emergency login...");
|
|
|
|
// Set up the scopes and base URL
|
|
const scopes = "read write follow";
|
|
const baseurl =
|
|
localStorage.getItem("baseurl") ||
|
|
this.getLoginUser(this.state.user);
|
|
|
|
// Register the Mastodon app with the Mastodon server
|
|
Mastodon.registerApp(
|
|
this.state.brandName ?? "Hyperspace",
|
|
{
|
|
scopes: scopes
|
|
},
|
|
baseurl
|
|
)
|
|
// 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
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
authorizeEmergencyLogin() {
|
|
let redirAddress =
|
|
this.state.defaultRedirectAddress === "desktop"
|
|
? "hyperspace://hyperspace/app/"
|
|
: this.state.defaultRedirectAddress;
|
|
window.location.href = `${redirAddress}/?code=${this.state.authCode}#/`;
|
|
}
|
|
|
|
/**
|
|
* Restore a login attempt from a session
|
|
*/
|
|
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,
|
|
emergencyMode: session.emergency,
|
|
proceedToGetCode: true
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the user input string for any possible errors
|
|
* @param username The username to read and check for errors
|
|
* @param bypassesInstanceNameCheck Whether to bypass the instance name validation process. Defaults to false.
|
|
* @return Whether an error has occured in the validation.
|
|
*/
|
|
checkForErrors(
|
|
username: string,
|
|
bypassesInstanceNameCheck: boolean = false
|
|
): boolean {
|
|
let userInputError = false;
|
|
let userInputErrorMessage = "";
|
|
|
|
// Is the user string blank?
|
|
if (username === "") {
|
|
userInputError = true;
|
|
userInputErrorMessage = "Username cannot be blank.";
|
|
this.setState({ userInputError, userInputErrorMessage });
|
|
return true;
|
|
} else {
|
|
if (username.includes("@")) {
|
|
if (this.state.federates && this.state.federates === true) {
|
|
let baseUrl = username.split("@")[1];
|
|
|
|
// Is the user's domain in the disallowed list?
|
|
if (inDisallowedDomains(baseUrl)) {
|
|
this.setState({
|
|
userInputError: true,
|
|
userInputErrorMessage: `Signing in with an account from ${baseUrl} isn't supported.`
|
|
});
|
|
return true;
|
|
} else {
|
|
if (bypassesInstanceNameCheck) {
|
|
return false;
|
|
}
|
|
// Are we unable to ping the server?
|
|
axios
|
|
.get(
|
|
"https://instances.social/api/1.0/instances/show?name=" +
|
|
baseUrl,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${instancesBearerKey}`
|
|
}
|
|
}
|
|
)
|
|
.catch((err: Error) => {
|
|
let userInputError = true;
|
|
let userInputErrorMessage =
|
|
"We couldn't recognize this instance.";
|
|
this.setState({
|
|
userInputError,
|
|
userInputErrorMessage
|
|
});
|
|
return true;
|
|
});
|
|
}
|
|
} else if (
|
|
username.includes(
|
|
this.state.registerBase ?? "mastodon.online"
|
|
)
|
|
) {
|
|
this.setState({ userInputError, userInputErrorMessage });
|
|
return false;
|
|
} else {
|
|
userInputError = true;
|
|
userInputErrorMessage =
|
|
"You cannot sign in with this username.";
|
|
this.setState({ userInputError, userInputErrorMessage });
|
|
return true;
|
|
}
|
|
} else {
|
|
this.setState({ userInputError, userInputErrorMessage });
|
|
return false;
|
|
}
|
|
this.setState({ userInputError, userInputErrorMessage });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
checkForToken() {
|
|
let location = window.location.href;
|
|
|
|
// Is there an auth code?
|
|
if (location.includes("?code=")) {
|
|
let code = parseUrl(location).query.code as string;
|
|
this.setState({ authorizing: true });
|
|
let loginData = localStorage.getItem("login");
|
|
|
|
// If there's login data, try to fetch an access token
|
|
if (loginData) {
|
|
let clientLoginSession: SaveClientSession = JSON.parse(
|
|
loginData
|
|
);
|
|
|
|
getConfig().then((resp: any) => {
|
|
if (resp === undefined) {
|
|
return;
|
|
}
|
|
|
|
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(
|
|
`Couldn't authorize ${this.state.brandName ??
|
|
"Hyperspace"}: ${err.name}`,
|
|
{ variant: "error" }
|
|
);
|
|
console.error(err.message);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 + "/#/";
|
|
}
|
|
|
|
/**
|
|
* Render the title bar for macOS
|
|
*/
|
|
titlebar() {
|
|
const { classes } = this.props;
|
|
if (isDarwinApp()) {
|
|
return (
|
|
<div className={classes.titleBarRoot}>
|
|
<Typography className={classes.titleBarText}>
|
|
{this.state.brandName ?? "Hyperspace"}
|
|
</Typography>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the multi-user account panel
|
|
*/
|
|
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);
|
|
this.redirectToApp();
|
|
}}
|
|
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>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the main landing panel
|
|
*/
|
|
showLanding() {
|
|
const { classes } = this.props;
|
|
return (
|
|
<div>
|
|
<Typography variant="h5">Sign in</Typography>
|
|
<Typography>with your fediverse account</Typography>
|
|
<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}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">@</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
{this.state.userInputError ? (
|
|
<Typography color="error">
|
|
{this.state.userInputErrorMessage}
|
|
{this.state.userInputErrorMessage ===
|
|
"We couldn't recognize this instance." ? (
|
|
<span>
|
|
<br />
|
|
<Link
|
|
// className={classes.welcomeLink}
|
|
onClick={() => this.startLogin(true)}
|
|
>
|
|
Try anyway
|
|
</Link>
|
|
</span>
|
|
) : null}
|
|
</Typography>
|
|
) : null}
|
|
<br />
|
|
{this.state.registerBase && this.state.federates ? (
|
|
<Typography variant="caption">
|
|
Not from{" "}
|
|
<b>{this.state.registerBase ?? "noinstance"}</b>? Sign
|
|
in with your{" "}
|
|
<Link
|
|
href="https://docs.joinmastodon.org/user/signup/#address"
|
|
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>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the login auth panel
|
|
*/
|
|
showLoginAuth() {
|
|
const { classes } = this.props;
|
|
return (
|
|
<div>
|
|
<Typography variant="h5">
|
|
Howdy, {this.state.user?.split("@")[0] ?? "user"}
|
|
</Typography>
|
|
<Typography>
|
|
To continue, finish signing in on your instance's website
|
|
and authorize {this.state.brandName ?? "Hyperspace"}.
|
|
</Typography>
|
|
<div className={classes.middlePadding} />
|
|
<div style={{ display: "flex" }}>
|
|
<div className={classes.flexGrow} />
|
|
<Button
|
|
color="primary"
|
|
variant="contained"
|
|
size="large"
|
|
href={this.state.authUrl ?? ""}
|
|
>
|
|
Authorize
|
|
</Button>
|
|
<div className={classes.flexGrow} />
|
|
</div>
|
|
<div className={classes.middlePadding} />
|
|
<Typography>
|
|
Having trouble signing in?{" "}
|
|
<Link
|
|
onClick={() => this.startEmergencyLogin()}
|
|
className={classes.welcomeLink}
|
|
>
|
|
Sign in with a code.
|
|
</Link>
|
|
</Typography>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the emergency login panel
|
|
*/
|
|
showAuthDialog() {
|
|
return (
|
|
<Dialog
|
|
open={this.state.openAuthDialog}
|
|
disableBackdropClick
|
|
disableEscapeKeyDown
|
|
maxWidth="sm"
|
|
fullWidth={true}
|
|
>
|
|
<DialogTitle>Authorize with a code</DialogTitle>
|
|
<DialogContent>
|
|
<Typography paragraph>
|
|
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.
|
|
</Typography>
|
|
<Button
|
|
color="primary"
|
|
variant="contained"
|
|
href={this.state.authUrl ?? ""}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Request Code
|
|
</Button>
|
|
<br />
|
|
<br />
|
|
<TextField
|
|
variant="outlined"
|
|
label="Authorization code"
|
|
fullWidth
|
|
onChange={event =>
|
|
this.updateAuthCode(event.target.value)
|
|
}
|
|
onKeyDown={event => this.watchAuthField(event)}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => this.toggleAuthDialog()}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
color="secondary"
|
|
onClick={() => this.authorizeEmergencyLogin()}
|
|
>
|
|
Authorize
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the authorizing panel
|
|
*/
|
|
showAuthorizationLoader() {
|
|
const { classes } = this.props;
|
|
return (
|
|
<div>
|
|
<Typography variant="h5">Authorizing</Typography>
|
|
<Typography>
|
|
Please wait while Hyperspace authorizes with your instance.
|
|
This shouldn't take long...
|
|
</Typography>
|
|
<div className={classes.middlePadding} />
|
|
<div style={{ display: "flex" }}>
|
|
<div className={classes.flexGrow} />
|
|
<CircularProgress />
|
|
<div className={classes.flexGrow} />
|
|
</div>
|
|
<div className={classes.middlePadding} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render the page
|
|
*/
|
|
render() {
|
|
const { classes } = this.props;
|
|
return (
|
|
<div>
|
|
{this.titlebar()}
|
|
<div
|
|
className={classes.root}
|
|
style={{
|
|
backgroundImage: `url(${this.state.backgroundUrl ??
|
|
"background.png"})`
|
|
}}
|
|
>
|
|
<Paper className={classes.paper}>
|
|
<img
|
|
className={classes.logo}
|
|
alt={this.state.brandName ?? "Hyperspace"}
|
|
src={this.state.logoUrl ?? "logo.png"}
|
|
/>
|
|
<br />
|
|
<Fade in={true}>
|
|
{this.state.authorizing
|
|
? this.showAuthorizationLoader()
|
|
: this.state.proceedToGetCode
|
|
? this.showLoginAuth()
|
|
: getAccountRegistry().length > 0 &&
|
|
!this.state.willAddAccount
|
|
? this.showMultiAccount()
|
|
: this.showLanding()}
|
|
</Fade>
|
|
<br />
|
|
<Typography variant="caption">
|
|
© {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.
|
|
</Typography>
|
|
<Typography variant="caption">
|
|
{this.state.repo ? (
|
|
<span>
|
|
<Link
|
|
className={classes.welcomeLink}
|
|
href={
|
|
this.state.repo ??
|
|
"https://github.com/hyperspacedev"
|
|
}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Source code
|
|
</Link>{" "}
|
|
|{" "}
|
|
</span>
|
|
) : null}
|
|
<Link
|
|
className={classes.welcomeLink}
|
|
href={
|
|
this.state.license ??
|
|
"https://thufie.lain.haus/NPL.html"
|
|
}
|
|
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>
|
|
</Typography>
|
|
<Typography variant="caption" color="textSecondary">
|
|
{this.state.brandName ?? "Hypersapce"} v.
|
|
{this.state.version}{" "}
|
|
{this.state.brandName &&
|
|
this.state.brandName !== "Hyperspace"
|
|
? "(Hyperspace-like)"
|
|
: null}
|
|
</Typography>
|
|
</Paper>
|
|
{this.showAuthDialog()}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withStyles(styles)(withSnackbar(WelcomePage));
|