Add login via routing!

This commit is contained in:
Marquis Kurt 2019-04-07 17:25:39 -04:00
parent fb663257fd
commit 9b02639b4b
15 changed files with 580 additions and 40 deletions

View File

@ -26,10 +26,11 @@
"file-dialog": "^0.0.7",
"emoji-mart": "^2.8.2",
"material-ui-pickers": "^2.2.4",
"@date-io/moment": "^1.1.0"
"@date-io/moment": "^1.1.0",
"axios": "^0.18.0"
},
"scripts": {
"start": "BROWSER='Safari Technology Preview' react-scripts start",
"start": "HTTPS=true BROWSER='Safari Technology Preview' react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"

BIN
public/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

15
public/config.json Normal file
View File

@ -0,0 +1,15 @@
{
"branding": {
"name": "Hyperspace",
"logo": "logo.svg",
"background": "background.png"
},
"federated": "true",
"registration": {
"defaultInstance": "mastodon.social"
},
"admin": {
"name": "Marquis Kurt",
"account": "alicerunsonfedora@mastodon.social"
}
}

50
public/logo.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:square;stroke-miterlimit:3;">
<g>
<path d="M159.362,49.421C156.656,47.047 153.343,46.439 150.14,46.66C147.434,46.826 144.507,47.82 142.574,49.974L141.636,51.078L141.636,37.493L136.5,37.493L136.5,77.253L142.298,77.253L142.298,64.166C142.298,62.012 142.298,59.748 142.685,58.091C143.016,56.269 143.845,54.667 145.115,53.673C146.33,52.679 147.821,52.182 149.477,52.072C151.576,52.017 153.398,52.569 154.668,53.729C156.987,55.717 157.429,59.196 157.429,63.006L157.429,77.253L163.283,77.253L163.283,62.785C163.338,57.815 163.007,52.624 159.362,49.421Z" style="fill-rule:nonzero;"/>
<path d="M196.526,47.433L187.912,70.737L178.855,47.433L173.002,47.433L185.206,78.137C183.77,82.058 183.052,83.383 181.948,84.211C180.899,84.929 179.794,84.984 177.972,84.929L177.972,90.507C181.34,90.507 183.107,90.451 184.875,89.181C186.807,87.746 187.967,85.261 189.127,82.223L202.325,47.433L196.526,47.433Z" style="fill-rule:nonzero;"/>
<path d="M236.949,51.354C234.464,48.262 230.598,46.605 226.512,46.605C222.923,46.605 219.554,47.93 217.179,50.25L217.179,47.433L212.044,47.433L212.044,90.507L217.842,90.507L217.842,75.044C220.161,77.088 223.254,78.082 226.622,78.082C230.654,78.082 234.464,76.37 236.949,73.277C239.434,70.185 240.318,66.319 240.318,62.343C240.318,58.312 239.324,54.391 236.949,51.354ZM232.31,69.743C230.819,71.897 228.334,72.836 225.849,72.836C223.254,72.836 220.99,71.952 219.444,70.13C217.842,68.197 217.179,65.27 217.179,62.343C217.179,59.361 217.842,56.49 219.444,54.557C220.879,52.679 223.143,51.851 225.628,51.851C228.39,51.851 230.764,52.735 232.255,54.888C233.636,56.766 234.188,59.472 234.188,62.343C234.188,65.16 233.691,67.866 232.31,69.743Z" style="fill-rule:nonzero;"/>
<path d="M256.221,63.945L278.476,63.945C278.752,59.085 277.979,54.667 275.218,51.299C272.512,48.096 268.978,46.605 264.615,46.605C260.308,46.605 256.221,48.262 253.516,51.52C251.141,54.502 250.037,58.478 250.037,62.619C250.037,66.375 251.086,70.13 253.516,73.167C256.166,76.37 260.529,78.082 264.947,78.082C267.652,78.082 270.469,77.309 272.788,75.873C275.052,74.492 276.93,72.283 278.034,69.688L272.402,67.866C271.794,69.136 270.966,70.24 269.64,71.179C268.26,72.173 266.603,72.615 264.615,72.615C262.02,72.615 259.59,71.621 258.154,69.633C256.939,68.031 256.387,66.043 256.221,63.945ZM264.615,51.741C267.1,51.685 269.53,52.624 270.966,54.502C272.015,55.882 272.402,57.484 272.567,59.306L256.332,59.306C256.608,57.594 257.105,56.048 258.154,54.667C259.811,52.514 262.241,51.685 264.615,51.741Z" style="fill-rule:nonzero;"/>
<path d="M303.989,46.66C301.835,46.715 299.681,47.323 297.859,48.648C296.644,49.532 295.705,50.581 295.043,51.741L295.043,47.433L289.907,47.433L289.907,77.253L295.705,77.253L295.705,62.233C295.705,58.92 296.037,56.214 298.135,54.226C299.35,52.956 301.227,52.182 303.492,52.072C305.314,51.962 307.081,52.238 308.462,52.735L308.462,47.323C307.026,46.771 305.48,46.55 303.989,46.66Z" style="fill-rule:nonzero;"/>
<path d="M341.319,55.275C340.325,52.293 337.95,49.477 335.079,48.096C333.091,47.047 330.827,46.605 328.783,46.605C326.685,46.605 324.531,46.771 322.322,47.709C319.396,48.98 317.187,51.796 317.187,55.22C317.187,56.655 317.684,58.754 318.954,60.134C320.334,61.57 322.322,62.454 324.089,63.116C325.525,63.613 326.74,63.945 330.992,65.16C333.643,65.933 336.183,66.651 336.183,69.025C336.183,70.185 335.631,71.068 334.637,71.786C333.477,72.615 331.158,73.001 329.556,73.001C327.348,73.001 325.139,72.394 323.592,70.848C322.598,69.909 321.936,68.528 321.494,67.148L316.248,68.639C316.745,70.516 317.684,72.504 319.451,74.382C322.157,77.198 325.58,78.082 329.501,78.082C332.373,78.082 335.023,77.64 337.508,76.094C339.938,74.603 341.484,71.786 341.484,69.025C341.484,64.221 337.674,61.294 332.373,59.858C329.998,59.196 328.231,58.699 326.574,58.202C324.145,57.539 322.488,56.876 322.488,54.999C322.488,52.9 325.47,51.575 328.452,51.685C331.765,51.741 334.582,53.232 335.852,56.6L341.319,55.275Z" style="fill-rule:nonzero;"/>
<path d="M377.821,51.354C375.336,48.262 371.47,46.605 367.384,46.605C363.794,46.605 360.426,47.93 358.051,50.25L358.051,47.433L352.915,47.433L352.915,90.507L358.714,90.507L358.714,75.044C361.033,77.088 364.125,78.082 367.494,78.082C371.525,78.082 375.336,76.37 377.821,73.277C380.306,70.185 381.189,66.319 381.189,62.343C381.189,58.312 380.195,54.391 377.821,51.354ZM373.182,69.743C371.691,71.897 369.206,72.836 366.721,72.836C364.125,72.836 361.861,71.952 360.315,70.13C358.714,68.197 358.051,65.27 358.051,62.343C358.051,59.361 358.714,56.49 360.315,54.557C361.751,52.679 364.015,51.851 366.5,51.851C369.261,51.851 371.636,52.735 373.127,54.888C374.507,56.766 375.059,59.472 375.059,62.343C375.059,65.16 374.562,67.866 373.182,69.743Z" style="fill-rule:nonzero;"/>
<path d="M416.807,77.253L416.807,59.196C416.807,55.551 415.924,52.017 413.107,49.642C411.064,47.875 407.917,46.605 404.824,46.605C399.744,46.605 395.823,48.262 392.73,51.63L396.209,55.441C398.474,52.956 401.235,52.072 404.327,52.072C408.193,52.072 410.733,53.95 411.009,57.429C405.929,58.202 399.854,58.809 396.154,60.576C392.399,62.399 390.908,65.16 390.908,68.97C390.908,74.547 394.884,78.082 400.738,78.082C405.542,78.082 408.911,76.756 411.727,73.001L411.727,77.253L416.807,77.253ZM409.96,68.915C408.414,71.565 405.542,73.167 401.953,73.167C398.915,73.167 396.651,71.289 396.651,68.528C396.651,65.933 399.191,64.387 402.173,63.724C404.493,63.227 407.751,62.619 411.064,62.122C411.064,64.773 411.119,66.982 409.96,68.915Z" style="fill-rule:nonzero;"/>
<path d="M455.076,55.717C454.248,52.514 452.26,50.084 449.72,48.538C447.345,47.102 445.136,46.605 442.099,46.605C437.405,46.605 433.871,48.206 431.11,51.52C428.625,54.557 427.631,58.312 427.631,62.343C427.631,66.319 428.625,70.019 431.11,73.222C433.705,76.535 437.35,78.082 442.044,78.082C445.468,78.082 447.787,77.474 450.272,75.707C452.481,74.161 454.193,71.676 455.076,69.08L449.167,67.645C448.615,69.136 447.897,70.295 446.627,71.289C445.357,72.283 443.645,72.615 442.044,72.615C439.448,72.615 437.184,71.621 435.748,69.633C434.257,67.645 433.76,64.994 433.76,62.343C433.76,59.693 434.257,56.987 435.748,54.999C437.184,53.011 439.448,52.072 442.044,52.072C443.645,52.072 445.247,52.514 446.517,53.453C447.842,54.391 448.781,55.882 449.333,57.318L455.076,55.717Z" style="fill-rule:nonzero;"/>
<path d="M469.655,63.945L491.909,63.945C492.185,59.085 491.412,54.667 488.651,51.299C485.945,48.096 482.411,46.605 478.049,46.605C473.741,46.605 469.655,48.262 466.949,51.52C464.574,54.502 463.47,58.478 463.47,62.619C463.47,66.375 464.519,70.13 466.949,73.167C469.6,76.37 473.962,78.082 478.38,78.082C481.086,78.082 483.902,77.309 486.221,75.873C488.486,74.492 490.363,72.283 491.468,69.688L485.835,67.866C485.227,69.136 484.399,70.24 483.074,71.179C481.693,72.173 480.037,72.615 478.049,72.615C475.453,72.615 473.023,71.621 471.588,69.633C470.373,68.031 469.82,66.043 469.655,63.945ZM478.049,51.741C480.534,51.685 482.963,52.624 484.399,54.502C485.448,55.882 485.835,57.484 486.001,59.306L469.765,59.306C470.041,57.594 470.538,56.048 471.588,54.667C473.244,52.514 475.674,51.685 478.049,51.741Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.125,0,0,0.125,0,0)">
<path d="M512,61C760.914,61 963,263.086 963,512C963,760.914 760.914,963 512,963C263.086,963 61,760.914 61,512C61,263.086 263.086,61 512,61Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
<g>
<path d="M924,512C924,739.389 739.389,924 512,924C284.611,924 100,739.389 100,512C100,284.611 284.611,100 512,100C739.389,100 924,284.611 924,512Z" style="fill:url(#_Radial2);fill-rule:nonzero;"/>
<g>
<clipPath id="_clip3">
<path d="M924,512C924,739.389 739.389,924 512,924C284.611,924 100,739.389 100,512C100,284.611 284.611,100 512,100C739.389,100 924,284.611 924,512Z"/>
</clipPath>
<g clip-path="url(#_clip3)">
<g>
<path d="M238,360C295.197,285.459 375.604,257.821 477,255C578.52,253.771 646.196,309.114 680,421C687.544,471.569 684.554,519.246 671,564C675.111,576.014 683.121,583.691 695,587C715.324,582.169 729.334,569.513 737,549C736.711,489.315 733.721,442.992 728,410C747.516,373.313 773.859,370.323 807,401C861.984,489.569 863.66,571.912 812,648C785.405,684.18 746.415,708.19 695,720C652.148,718.82 617.824,714.163 592,706C486.826,769.085 386.836,773.762 292,720C205.635,611.833 173.334,488.563 238,360Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
<g>
<path d="M602.409,773.749C482.57,695.717 306.969,619.474 126.706,654.912C198.381,718.826 290.006,753.401 276.782,803.086L602.409,773.749Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
<path d="M820.149,828.878C760.745,894.654 658.821,936.37 514.347,953.998C384.053,947.037 270.948,893.682 175,793.905C434.437,723.169 649.497,734.836 820.149,828.878Z" style="fill:url(#_Linear6);fill-rule:nonzero;"/>
<path d="M653.396,768.646C527.357,720.669 349.042,683.971 185,746.594C263.265,790.499 355.436,805.054 351.273,851.18L653.396,768.646Z" style="fill:url(#_Linear7);fill-rule:nonzero;"/>
</g>
<path d="M523,644C548.476,652.094 565.102,674.42 586,681C619.78,682.401 650.155,665.915 668,673C687.177,680.298 692.854,695.975 685,720C650.656,778.986 497.06,764.977 486,690C490.825,659.361 503.168,644.038 523,644Z" style="fill:url(#_Linear8);fill-rule:nonzero;"/>
<path d="M268,366C126.262,375.98 128.366,675.501 235,706C312.361,728.913 367.715,695.599 389,636L387,466C379.239,399.883 339.582,366.559 268,366Z" style="fill:url(#_Linear9);fill-rule:nonzero;"/>
</g>
<path d="M617.754,487.937C590.587,462.68 563.765,462.695 537.246,487.937" style="fill:none;stroke:black;stroke-width:3px;"/>
</g>
</g>
</g>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.45053e-13,902,-902,-1.45053e-13,512,61)"><stop offset="0" style="stop-color:rgb(147,111,189);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(83,41,131);stop-opacity:1"/></linearGradient>
<radialGradient id="_Radial2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(919.965,0.9165,0,412,512,512)"><stop offset="0" style="stop-color:rgb(92,86,142);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(70,34,110);stop-opacity:1"/></radialGradient>
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.42238e-13,502.118,-502.118,1.42238e-13,526.694,254.98)"><stop offset="0" style="stop-color:rgb(246,224,152);stop-opacity:1"/><stop offset="0.01" style="stop-color:rgb(246,224,152);stop-opacity:1"/><stop offset="0.98" style="stop-color:rgb(243,209,135);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(243,209,135);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(334.98,122.112,-122.112,334.98,196.547,667.537)"><stop offset="0" style="stop-color:rgb(83,41,131);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(70,34,110);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(5.459,206.595,-206.595,5.459,495.892,747.746)"><stop offset="0" style="stop-color:rgb(47,13,87);stop-opacity:1"/><stop offset="0.02" style="stop-color:rgb(47,13,87);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(26,5,54);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear7" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(336.259,49.4246,-49.4246,336.259,252.961,745.579)"><stop offset="0" style="stop-color:rgb(83,41,131);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(70,34,110);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear8" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(143.133,79.6096,-79.6096,143.133,515.644,660.488)"><stop offset="0" style="stop-color:white;stop-opacity:1"/><stop offset="0.98" style="stop-color:rgb(209,209,209);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(209,209,209);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear9" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.12647e-14,347.279,-347.279,2.12647e-14,273.5,366)"><stop offset="0" style="stop-color:rgb(248,251,202);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(253,255,194);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -15,8 +15,10 @@ import Conversation from './pages/Conversation';
import NotificationsPage from './pages/Notifications';
import SearchPage from './pages/Search';
import Composer from './pages/Compose';
import WelcomePage from './pages/Welcome';
import {withSnackbar} from 'notistack';
import {PrivateRoute} from './interfaces/overrides';
import { userLoggedIn, refreshUserAccountData } from './utilities/accounts';
let theme = setHyperspaceTheme(getUserDefaultTheme());
class App extends Component<any, any> {
@ -42,19 +44,23 @@ class App extends Component<any, any> {
return (
<MuiThemeProvider theme={this.state.theme}>
<CssBaseline/>
<AppLayout/>
<Route exact path="/" component={HomePage}/>
<Route path="/home" component={HomePage}/>
<Route path="/local" component={LocalPage}/>
<Route path="/public" component={PublicPage}/>
<Route path="/messages"/>
<Route path="/notifications" component={NotificationsPage}/>
<Route path="/profile/:profileId" render={props => <ProfilePage {...props}></ProfilePage>}/>
<Route path="/conversation/:conversationId" component={Conversation}/>
<Route path="/search" component={SearchPage}/>
<Route path="/settings" component={Settings}/>
<Route path="/about" component={AboutPage}/>
<Route path="/compose" component={Composer}/>
<Route path="/welcome" component={WelcomePage}/>
<div>
{ userLoggedIn()? <AppLayout/>: null}
<PrivateRoute exact path="/" component={HomePage}/>
<PrivateRoute path="/home" component={HomePage}/>
<PrivateRoute path="/local" component={LocalPage}/>
<PrivateRoute path="/public" component={PublicPage}/>
<Route path="/messages"/>
<PrivateRoute path="/notifications" component={NotificationsPage}/>
<PrivateRoute path="/profile/:profileId" component={ProfilePage}/>
<PrivateRoute path="/conversation/:conversationId" component={Conversation}/>
<PrivateRoute path="/search" component={SearchPage}/>
<PrivateRoute path="/settings" component={Settings}/>
<PrivateRoute path="/about" component={AboutPage}/>
<PrivateRoute path="/compose" component={Composer}/>
</div>
</MuiThemeProvider>
);
}

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge } from '@material-ui/core';
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import NotificationsIcon from '@material-ui/icons/Notifications';
@ -19,12 +19,14 @@ import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interface
import Mastodon from 'megalodon';
import { Notification } from '../../types/Notification';
import {sendNotificationRequest} from '../../utilities/notifications';
import {withSnackbar} from 'notistack';
interface IAppLayoutState {
acctMenuOpen: boolean;
drawerOpenOnMobile: boolean;
currentUser: UAccount;
currentUser?: UAccount;
notificationCount: number;
logOutOpen: boolean;
}
export class AppLayout extends Component<any, IAppLayoutState> {
@ -35,15 +37,13 @@ export class AppLayout extends Component<any, IAppLayoutState> {
constructor(props: any) {
super(props);
let accountData = JSON.parse(localStorage.getItem('account') as string);
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
this.state = {
drawerOpenOnMobile: false,
acctMenuOpen: false,
currentUser: accountData,
notificationCount: 0
notificationCount: 0,
logOutOpen: false
}
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
@ -51,6 +51,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}
componentDidMount() {
let acct = localStorage.getItem('account');
if (acct) {
this.setState({ currentUser: JSON.parse(acct) });
} else {
this.client.get('/accounts/verify_credentials').then((resp: any) => {
let data: UAccount = resp.data;
this.setState({ currentUser: data });
}).catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't find profile info: " + err.name);
console.error(err.message);
})
}
this.streamListener = this.client.stream('/streaming/user');
this.streamListener.on('notification', (notif: Notification) => {
@ -105,11 +119,23 @@ export class AppLayout extends Component<any, IAppLayoutState> {
})
}
toggleLogOutDialog() {
this.setState({ logOutOpen: !this.state.logOutOpen });
}
searchForQuery(what: string) {
window.location.href = "/#/search?query=" + what;
window.location.reload;
}
logOutAndRestart() {
let loginData = localStorage.getItem("login");
if (loginData) {
localStorage.clear();
window.location.reload();
}
}
titlebar() {
const { classes } = this.props;
if (process.env.NODE_ENV === "development") {
@ -133,11 +159,11 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<div>
<List>
<div className={classes.drawerDisplayMobile}>
<LinkableListItem button key="profile-mobile" to={`/profile/${this.state.currentUser.id}`}>
<LinkableListItem button key="profile-mobile" to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
<ListItemAvatar>
<Avatar alt="You" src={this.state.currentUser.avatar_static}/>
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</ListItemAvatar>
<ListItemText primary={this.state.currentUser.display_name} secondary={this.state.currentUser.acct}/>
<ListItemText primary={this.state.currentUser? (this.state.currentUser.display_name || this.state.currentUser.acct): "Loading..."} secondary={this.state.currentUser? this.state.currentUser.acct: "Loading..."}/>
</LinkableListItem>
<LinkableListItem button key="notifications-mobile" to="/notifications">
<ListItemIcon><NotificationsIcon/></ListItemIcon>
@ -237,7 +263,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<MailIcon/>
</LinkableIconButton>
<IconButton id="acctMenuBtn" onClick={this.toggleAcctMenu}>
<Avatar className={classes.appBarAcctMenuIcon} alt="You" src={this.state.currentUser.avatar_static}/>
<Avatar className={classes.appBarAcctMenuIcon} alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</IconButton>
<Menu
id="acct-menu"
@ -247,15 +273,18 @@ export class AppLayout extends Component<any, IAppLayoutState> {
>
<ClickAwayListener onClickAway={this.toggleAcctMenu}>
<div>
<LinkableListItem to={`/profile/${this.state.currentUser.id}`}>
<LinkableListItem to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
<ListItemAvatar>
<Avatar alt="You" src={this.state.currentUser.avatar_static}/>
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</ListItemAvatar>
<ListItemText primary={this.state.currentUser.display_name || this.state.currentUser.acct} secondary={'@' + this.state.currentUser.acct}/>
<ListItemText
primary={this.state.currentUser? (this.state.currentUser.display_name || this.state.currentUser.acct): "Loading..."}
secondary={'@' + (this.state.currentUser? this.state.currentUser.acct: "Loading...")}
/>
</LinkableListItem>
<Divider/>
{/* <MenuItem>Switch account</MenuItem> */}
<MenuItem>Log out</MenuItem>
<MenuItem onClick={() => this.toggleLogOutDialog()}>Log out</MenuItem>
</div>
</ClickAwayListener>
</Menu>
@ -288,6 +317,27 @@ export class AppLayout extends Component<any, IAppLayoutState> {
</Hidden>
</nav>
</div>
<Dialog
open={this.state.logOutOpen}
onClose={() => this.toggleLogOutDialog()}
>
<DialogTitle id="alert-dialog-title">Log out of Hyperspace?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You'll need to remove Hyperspace from your list of authorized apps and log in again if you want to use Hyperspace.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => this.toggleLogOutDialog()} color="primary" autoFocus>
Cancel
</Button>
<Button onClick={() => {
this.logOutAndRestart();
}} color="primary">
Log out
</Button>
</DialogActions>
</Dialog>
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
<EditIcon/>
</LinkableFab>
@ -296,4 +346,4 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}
}
export default withStyles(styles)(AppLayout);
export default withStyles(styles)(withSnackbar(AppLayout));

View File

@ -1,5 +1,6 @@
import { AppLayout } from './AppLayout';
import { withStyles } from '@material-ui/core';
import { styles } from './AppLayout.styles';
import {withSnackbar} from 'notistack';
export default withStyles(styles)(AppLayout);
export default withStyles(styles)(withSnackbar(AppLayout));

View File

@ -3,14 +3,23 @@ import ReactDOM from 'react-dom';
import App from './App';
import { HashRouter } from 'react-router-dom';
import * as serviceWorker from './serviceWorker';
import {createUserDefaults, getUserDefaultBool} from './utilities/settings';
import {refreshUserAccountData} from './utilities/accounts';
import {createUserDefaults} from './utilities/settings';
import {collectEmojisFromServer} from './utilities/emojis';
import {SnackbarProvider} from 'notistack';
import axios from 'axios';
import { userLoggedIn, refreshUserAccountData } from './utilities/accounts';
axios.get('config.json').then((resp: any) => {
document.title = resp.data.branding.name || "Hyperspace";
}).catch((err: Error) => {
console.error(err);
})
createUserDefaults();
refreshUserAccountData();
collectEmojisFromServer();
if (userLoggedIn()) {
collectEmojisFromServer();
refreshUserAccountData();
}
ReactDOM.render(
<HashRouter>

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import ListItem, { ListItemProps } from "@material-ui/core/ListItem";
import IconButton, { IconButtonProps } from "@material-ui/core/IconButton";
import { Link, Route } from "react-router-dom";
import { Link, Route, Redirect, RouteProps } from "react-router-dom";
import Chip, { ChipProps } from '@material-ui/core/Chip';
import { MenuItemProps } from '@material-ui/core/MenuItem';
import { MenuItem } from '@material-ui/core';
import Button, { ButtonProps } from '@material-ui/core/Button';
import Fab, { FabProps } from '@material-ui/core/Fab';
import { userLoggedIn } from '../utilities/accounts';
export interface ILinkableListItemProps extends ListItemProps {
to: string;
@ -67,4 +68,19 @@ export const ProfileRoute = (rest: any, component: Component) => (
<Component {...props}/>
)}/>
)
export const PrivateRoute = (props: IPrivateRouteProps) => {
const { component, render, ...rest } = props;
return (<Route {...rest}
render={(compProps: any) => (
userLoggedIn()?
React.createElement(component, compProps):
<Redirect to="/welcome"/>
)}
/>
)}
interface IPrivateRouteProps extends RouteProps {
component: any
}

290
src/pages/Welcome.tsx Normal file
View File

@ -0,0 +1,290 @@
import React, { Component } from 'react';
import {withStyles, Paper, Typography, Button, TextField, Fade, Checkbox, FormControlLabel, Link, CircularProgress} from '@material-ui/core';
import {styles} from './WelcomePage.styles';
import axios from 'axios';
import Mastodon from 'megalodon';
import {Config} from '../types/Config';
import {SaveClientSession} from '../types/SessionData';
import { createHyperspaceApp } from '../utilities/login';
import {parseUrl} from 'query-string';
interface IWelcomeState {
logoUrl?: string;
backgroundUrl?: string;
brandName?: string;
registerBase?: string;
federates?: boolean;
wantsToLogin: boolean;
user: string;
userInputError: boolean;
clientId?: string;
clientSecret?: string;
authUrl?: string;
foundSavedLogin: boolean;
authority: boolean;
}
class WelcomePage extends Component<any, IWelcomeState> {
client: any;
constructor(props: any) {
super(props);
this.state = {
wantsToLogin: false,
user: "",
userInputError: false,
foundSavedLogin: false,
authority: false
}
axios.get('config.json').then((resp: any) => {
let result: Config = resp.data;
this.setState({
logoUrl: result.branding? result.branding.logo: "logo.png",
backgroundUrl: result.branding? result.branding.background: "background.png",
brandName: result.branding? result.branding.name: "Hyperspace",
registerBase: result.registration? result.registration.defaultInstance: "",
federates: result.federated? result.federated === "true": true
});
}).catch(() => {
console.warn('config.json is missing. If you want to customize Hyperspace, please include config.json');
})
}
componentDidMount() {
if (localStorage.getItem("login")) {
this.setState({
foundSavedLogin: true
})
this.getSavedSession();
this.checkForToken();
}
}
checkForToken() {
let location = window.location.href;
if (location.includes("?code=")) {
let code = parseUrl(location).query.code as string;
this.setState({ authority: true });
let loginData = localStorage.getItem("login");
if (loginData) {
let clientLoginSession: SaveClientSession = JSON.parse(loginData);
console.log(clientLoginSession);
Mastodon.fetchAccessToken(
clientLoginSession.clientId,
clientLoginSession.clientSecret,
code,
(localStorage.getItem("baseurl") as string),
`https://${window.location.host}`,
).then((tokenData: any) => {
localStorage.setItem("access_token", tokenData.access_token);
window.location.href=`https://${window.location.host}/#/`;
}).catch((err: Error) => {
console.log(err.message);
})
}
}
}
updateUserInfo(user: string) {
this.setState({ user });
}
getLoginUser(user: string) {
if (user.includes("@")) {
let newUser = user;
this.setState({ user: newUser })
return "https://" + newUser.split("@")[1];
} else {
let newUser = `${user}@${this.state.registerBase? this.state.registerBase: "mastodon.social"}`;
this.setState({ user: newUser });
return "https://" + (this.state.registerBase? this.state.registerBase: "mastodon.social");
}
}
startRegistration() {
if (this.state.registerBase) {
return "https://" + this.state.registerBase + "/auth/sign_up";
} else {
return "https://joinmastodon.org/#getting-started";
}
}
startLogin() {
if (this.state.user != "") {
const scopes = 'read write follow';
const baseurl = this.getLoginUser(this.state.user);
localStorage.setItem("baseurl", baseurl);
createHyperspaceApp(scopes, baseurl, `https://${window.location.host}`).then((resp: any) => {
let saveSessionForCrashing: SaveClientSession = {
clientId: resp.clientId,
clientSecret: resp.clientSecret,
authUrl: resp.url
}
localStorage.setItem("login", JSON.stringify(saveSessionForCrashing));
this.setState({
clientId: resp.clientId,
clientSecret: resp.clientSecret,
authUrl: resp.url,
wantsToLogin: true
})
})
} else {
this.setState({ userInputError: true });
}
}
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,
wantsToLogin: true
})
}
}
getSavedSession() {
let loginData = localStorage.getItem("login");
if (loginData) {
let session: SaveClientSession = JSON.parse(loginData);
this.setState({
clientId: session.clientId,
clientSecret: session.clientSecret,
authUrl: session.authUrl
})
}
}
checkForErrors() {
this.setState({ userInputError: this.state.user === "" })
}
readyForAuth() {
if (localStorage.getItem('baseurl')) {
return true;
} else {
return false;
}
}
showLanding() {
const { classes } = this.props;
return (
<div>
<Typography variant="h5">Sign in</Typography>
<Typography>with your Mastodon account</Typography>
<div className={classes.middlePadding}/>
<TextField
variant="outlined"
label="Account name"
fullWidth
placeholder="example@mastodon.host"
onChange={(event) => this.updateUserInfo(event.target.value)}
error={this.state.userInputError}
onBlur={() => this.checkForErrors()}
></TextField>
{
this.state.registerBase? <Typography variant="caption">If you are from <b>{this.state.registerBase? this.state.registerBase: "noinstance"}</b>, sign in with your username.</Typography>: null
}
<br/>
{
this.state.foundSavedLogin?
<Typography>
Signing in from a previous session? <Link onClick={() => this.resumeLogin()}>Continue login</Link>.
</Typography>: null
}
<div className={classes.middlePadding}/>
<div style={{ display: "flex" }}>
<Button
color="primary"
href={this.startRegistration()}
target="_blank"
rel="noreferrer"
>Create account</Button>
<div className={classes.flexGrow}/>
<Button color="primary" variant="contained" onClick={() => this.startLogin()}>Next</Button>
</div>
</div>
);
}
showLoginAuth() {
const { classes } = this.props;
return (
<div>
<Typography variant="h5">Howdy, {this.state.user? this.state.user.split("@")[0]: "user"}</Typography>
<Typography>To continue, finish signing in on your instance's website and authorize 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? this.state.authUrl: ""}
>
Authorize
</Button>
<div className={classes.flexGrow}/>
</div>
<div className={classes.middlePadding}/>
</div>
);
}
showAuthority() {
const { classes } = this.props;
return (
<div>
<Typography variant="h5">Authorizing</Typography>
<Typography>Please wait while Hyperspace authorizes with Mastodon. 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() {
const { classes } = this.props;
return (
<div className={classes.root} style={{ backgroundImage: `url(${this.state !== null? this.state.backgroundUrl: "background.png"})`}}>
<Paper className={classes.paper}>
<img className={classes.logo} src={this.state? this.state.logoUrl: "logo.png"}/>
<br/>
<Fade in={true}>
{
this.state.authority?
this.showAuthority():
this.state.wantsToLogin?
this.showLoginAuth():
this.showLanding()
}
</Fade>
<br/>
<Typography variant="caption">
&copy; 2019 <Link href="https://hyperspace.marquiskurt.net" target="_blank" rel="noreferrer">Hyperspace</Link> developers. All rights reserved.
</Typography>
<Typography variant="caption">
<Link href="https://github.com/hyperspacedev" target="_blank" rel="noreferrer">GitHub</Link> | <Link href="https://www.apache.org/licenses/LICENSE-2.0" target="_blank" rel="noreferrer">License</Link> | <Link href="https://github.com/hyperspacedev/hyperspace/issues/new" target="_blank" rel="noreferrer">File an Issue</Link>
</Typography>
</Paper>
</div>
);
}
}
export default withStyles(styles)(WelcomePage);

View File

@ -0,0 +1,50 @@
import { Theme, createStyles } from '@material-ui/core';
export const styles = (theme: Theme) => createStyles({
root: {
width: '100%',
height: '100%',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
top: 0,
left: 0,
position: "absolute",
[theme.breakpoints.up('sm')]: {
paddingTop: theme.spacing.unit * 4,
paddingLeft: '25%',
paddingRight: '25%',
},
[theme.breakpoints.up('lg')]: {
paddingTop: theme.spacing.unit * 12,
paddingLeft: '35%',
paddingRight: '35%',
}
},
paper: {
height: '100%',
[theme.breakpoints.up('sm')]: {
height: 'auto',
paddingLeft: theme.spacing.unit * 8,
paddingRight: theme.spacing.unit * 8,
paddingTop: theme.spacing.unit * 6,
},
paddingTop: theme.spacing.unit * 12,
paddingLeft: theme.spacing.unit * 4,
paddingRight: theme.spacing.unit * 4,
paddingBottom: theme.spacing.unit * 6,
textAlign: 'center'
},
flexGrow: {
flexGrow: 1
},
middlePadding: {
height: theme.spacing.unit * 6
},
logo: {
[theme.breakpoints.up('sm')]: {
height: 64,
width: "auto"
},
}
});

15
src/types/Config.tsx Normal file
View File

@ -0,0 +1,15 @@
export type Config = {
branding?: {
name?: string;
logo?: string;
background?: string;
};
federated?: string;
registration?: {
defaultInstance?: string;
};
admin?: {
name?: string;
account?: string;
};
}

View File

@ -0,0 +1,5 @@
export type SaveClientSession = {
clientId: string;
clientSecret: string;
authUrl: string;
}

View File

@ -1,9 +1,18 @@
import Mastodon from "megalodon";
export function userLoggedIn(): boolean {
if (localStorage.getItem('baseurl') && localStorage.getItem('access_token')) {
return true;
} else {
return false;
}
}
export function refreshUserAccountData() {
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1/");
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
client.get('/accounts/verify_credentials').then((resp: any) => {
if (JSON.stringify(resp.data) !== localStorage.getItem('account'))
localStorage.setItem('account', JSON.stringify(resp.data));
localStorage.setItem('account', JSON.stringify(resp.data));
}).catch((err: Error) => {
console.error(err.message);
});
}

23
src/utilities/login.tsx Normal file
View File

@ -0,0 +1,23 @@
import Mastodon from 'megalodon';
/**
* Creates the Hyperspace app with the appropriate Redirect URI
* @param scopes The scopes that the app needs
* @param baseurl The base URL of the instance
* @param redirect_uri The URL to redirect to when authorizing
*/
export function createHyperspaceApp(scopes: string, baseurl: string, redirect_uri: string) {
return Mastodon.createApp("Hyperspace", {
scopes: scopes,
redirect_uris: redirect_uri,
website: 'https://hyperspace.marquiskurt.net'
}).then(appData => {
return Mastodon.generateAuthUrl(appData.clientId, appData.clientSecret, {
redirect_uri: redirect_uri,
scope: scopes
}, baseurl).then(url => {
appData.url = url;
return appData;
})
})
}