Merge branch 'develop-1.1.0-beta4' into HD-44-uneven-masonry-columns
This commit is contained in:
commit
cc43d90607
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hyperspace",
|
||||
"version": "1.1.0-beta2",
|
||||
"version": "1.1.0-beta4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hyperspace",
|
||||
"productName": "Hyperspace Desktop",
|
||||
"version": "1.1.0-beta3",
|
||||
"version": "1.1.0-beta4",
|
||||
"description": "A beautiful, fluffy client for the fediverse",
|
||||
"author": "Marquis Kurt <hyperspacedev@marquiskurt.net>",
|
||||
"repository": "https://github.com/hyperspacedev/hyperspace.git",
|
||||
|
|
|
@ -324,7 +324,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
*/
|
||||
searchForQuery(what: string) {
|
||||
what = what.replace(/^#/g, "tag:");
|
||||
console.log(what);
|
||||
// console.log(what);
|
||||
window.location.href = isDesktopApp()
|
||||
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
|
||||
: "/#/search?query=" + what;
|
||||
|
|
|
@ -124,7 +124,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't delete post: " + err.name);
|
||||
console.log(err.message);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -410,6 +410,8 @@ export class Post extends React.Component<any, IPostState> {
|
|||
emojis.concat(reblogger.emojis);
|
||||
}
|
||||
|
||||
// console.log(post);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
|
@ -539,86 +541,63 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post's URL
|
||||
* @param post The post to get the URL from
|
||||
* @returns A string containing the post's URI
|
||||
*/
|
||||
getMastodonUrl(post: Status) {
|
||||
let url = "";
|
||||
if (post.reblog) {
|
||||
url = post.reblog.uri;
|
||||
} else {
|
||||
url = post.uri;
|
||||
}
|
||||
return url;
|
||||
return post.reblog ? post.reblog.uri : post.uri;
|
||||
}
|
||||
|
||||
toggleFavorited(post: Status) {
|
||||
let _this = this;
|
||||
if (post.favourited) {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unfavourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(
|
||||
`Couldn't unfavorite post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
} else {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/favourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(
|
||||
`Couldn't favorite post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
/**
|
||||
* Tell server a post has been un/favorited and update post state
|
||||
* @param post The post to un/favorite
|
||||
*/
|
||||
async toggleFavorited(post: Status) {
|
||||
let action: string = post.favourited ? "unfavourite" : "favourite";
|
||||
try {
|
||||
// favorite the original post, not the reblog
|
||||
let resp: any = await this.client.post(
|
||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
||||
);
|
||||
// compensate for slow server update
|
||||
if (action === "unfavourite") {
|
||||
resp.data.favourites_count -= 1;
|
||||
// if you unlike both original and reblog before refresh
|
||||
// and the post has only one favorite:
|
||||
if (resp.data.favourites_count < 0) {
|
||||
resp.data.favourites_count = 0;
|
||||
}
|
||||
}
|
||||
this.setState({ post: resp.data as Status });
|
||||
} catch (e) {
|
||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
toggleReblogged(post: Status) {
|
||||
if (post.reblogged) {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unreblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't unboost post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
} else {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/reblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't boost post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
/**
|
||||
* Tell server a post has been un/reblogged and update post state
|
||||
* @param post The post to un/reblog
|
||||
*/
|
||||
async toggleReblogged(post: Status) {
|
||||
let action: string =
|
||||
post.reblogged || post.reblog ? "unreblog" : "reblog";
|
||||
try {
|
||||
// modify the original post, not the reblog
|
||||
let resp: any = await this.client.post(
|
||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
||||
);
|
||||
// compensate for slow server update
|
||||
if (action === "unreblog") {
|
||||
resp.data.reblogs_count -= 1;
|
||||
}
|
||||
if (resp.data.reblog) resp.data = resp.data.reblog;
|
||||
this.setState({ post: resp.data as Status });
|
||||
} catch (e) {
|
||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* A Generic dictionary with the value of a specific type.
|
||||
*
|
||||
* Keys _must_ be strings.
|
||||
*/
|
||||
export interface Dictionary<T> {
|
||||
[Key: string]: T;
|
||||
}
|
|
@ -83,7 +83,7 @@ class ActivityPage extends Component<any, IActivityPageState> {
|
|||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.log(err.message);
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
this.client
|
||||
|
@ -101,7 +101,7 @@ class ActivityPage extends Component<any, IActivityPageState> {
|
|||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.log(err.message);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -626,9 +626,15 @@ class Composer extends Component<any, IComposerState> {
|
|||
: null
|
||||
})
|
||||
|
||||
// If we succeed, send a success message and go back.
|
||||
// If we succeed, send a success message, clear the status
|
||||
// text field, and go back.
|
||||
.then(() => {
|
||||
this.props.enqueueSnackbar("Posted!");
|
||||
|
||||
// This is necessary to prevent session drafts from saving
|
||||
// posts that were already posted.
|
||||
this.setState({ text: "" });
|
||||
|
||||
window.history.back();
|
||||
})
|
||||
|
||||
|
|
|
@ -17,7 +17,9 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from "@material-ui/core";
|
||||
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
|
@ -25,16 +27,22 @@ import PersonIcon from "@material-ui/icons/Person";
|
|||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import {
|
||||
LinkableIconButton,
|
||||
LinkableAvatar,
|
||||
LinkableMenuItem
|
||||
} from "../interfaces/overrides";
|
||||
import ForumIcon from "@material-ui/icons/Forum";
|
||||
import ReplyIcon from "@material-ui/icons/Reply";
|
||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||
|
||||
import Mastodon from "megalodon";
|
||||
import { Notification } from "../types/Notification";
|
||||
import { Account } from "../types/Account";
|
||||
import { Relationship } from "../types/Relationship";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { Dictionary } from "../interfaces/utils";
|
||||
|
||||
/**
|
||||
* The state interface for the notifications page.
|
||||
|
@ -69,6 +77,11 @@ interface INotificationsPageState {
|
|||
* Whether the delete confirmation dialog should be open.
|
||||
*/
|
||||
deleteDialogOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the menu should be open on smaller devices.
|
||||
*/
|
||||
mobileMenuOpen: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +114,8 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
// Initialize the state.
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
deleteDialogOpen: false
|
||||
deleteDialogOpen: false,
|
||||
mobileMenuOpen: {}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -114,10 +128,17 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
.get("/notifications")
|
||||
.then((resp: any) => {
|
||||
let notifications: [Notification] = resp.data;
|
||||
let notifMenus: Dictionary<boolean> = {};
|
||||
|
||||
notifications.forEach((notif: Notification) => {
|
||||
notifMenus[notif.id] = false;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
notifications,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true
|
||||
viewDidLoad: true,
|
||||
mobileMenuOpen: notifMenus
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
|
@ -160,6 +181,12 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
||||
}
|
||||
|
||||
toggleMobileMenu(id: string) {
|
||||
let mobileMenuOpen = this.state.mobileMenuOpen;
|
||||
mobileMenuOpen[id] = !mobileMenuOpen[id];
|
||||
this.setState({ mobileMenuOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML content from a string containing HTML content.
|
||||
*
|
||||
|
@ -306,6 +333,108 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{this.getActions(notif)}
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow an account from a notification if already not followed.
|
||||
* @param acct The account to follow, if possible
|
||||
*/
|
||||
followMember(acct: Account) {
|
||||
// Get the relationships for this account.
|
||||
this.client
|
||||
.get(`/accounts/relationships`, { id: acct.id })
|
||||
.then((resp: any) => {
|
||||
// Returns a list, so grab only the first item.
|
||||
let relationship: Relationship = resp.data[0];
|
||||
|
||||
// Follow if not following already.
|
||||
if (relationship.following == false) {
|
||||
this.client
|
||||
.post(`/accounts/${acct.id}/follow`)
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"You are now following this account."
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't follow account: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise notify the user.
|
||||
else {
|
||||
this.props.enqueueSnackbar(
|
||||
"You already follow this account."
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't find relationship.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getActions = (notif: Notification) => {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => this.toggleMobileMenu(notif.id)}
|
||||
className={classes.mobileOnly}
|
||||
id={`notification-list-${notif.id}`}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
open={this.state.mobileMenuOpen[notif.id]}
|
||||
anchorEl={document.getElementById(
|
||||
`notification-list-${notif.id}`
|
||||
)}
|
||||
onClose={() => this.toggleMobileMenu(notif.id)}
|
||||
>
|
||||
{notif.type == "follow" ? (
|
||||
<>
|
||||
<LinkableMenuItem
|
||||
to={`profile/${notif.account.id}`}
|
||||
>
|
||||
View Profile
|
||||
</LinkableMenuItem>
|
||||
<MenuItem
|
||||
onClick={() => this.followMember(notif.account)}
|
||||
>
|
||||
Follow
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
{notif.type == "mention" && notif.status ? (
|
||||
<LinkableMenuItem
|
||||
to={`/compose?reply=${
|
||||
notif.status.reblog
|
||||
? notif.status.reblog.id
|
||||
: notif.status.id
|
||||
}&visibility=${notif.status.visibility}&acct=${
|
||||
notif.status.reblog
|
||||
? notif.status.reblog.account.acct
|
||||
: notif.status.account.acct
|
||||
}`}
|
||||
>
|
||||
Reply
|
||||
</LinkableMenuItem>
|
||||
) : null}
|
||||
<MenuItem onClick={() => this.removeNotification(notif.id)}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<div className={classes.desktopOnly}>
|
||||
{notif.type === "follow" ? (
|
||||
<span>
|
||||
<Tooltip title="View profile">
|
||||
|
@ -363,54 +492,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow an account from a notification if already not followed.
|
||||
* @param acct The account to follow, if possible
|
||||
*/
|
||||
followMember(acct: Account) {
|
||||
// Get the relationships for this account.
|
||||
this.client
|
||||
.get(`/accounts/relationships`, { id: acct.id })
|
||||
.then((resp: any) => {
|
||||
// Returns a list, so grab only the first item.
|
||||
let relationship: Relationship = resp.data[0];
|
||||
|
||||
// Follow if not following already.
|
||||
if (relationship.following == false) {
|
||||
this.client
|
||||
.post(`/accounts/${acct.id}/follow`)
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"You are now following this account."
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't follow account: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise notify the user.
|
||||
else {
|
||||
this.props.enqueueSnackbar(
|
||||
"You already follow this account."
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't find relationship.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the notification page.
|
||||
|
|
|
@ -335,5 +335,9 @@ export const styles = (theme: Theme) =>
|
|||
"my-masonry-grid_column": {
|
||||
// non-standard name fixes react-masonry-css bug :shrug:
|
||||
padding: 5
|
||||
},
|
||||
noTopPaddingMargin: {
|
||||
marginTop: 0,
|
||||
paddingTop: 0
|
||||
}
|
||||
});
|
||||
|
|
|
@ -25,6 +25,8 @@ import Post from "../components/Post";
|
|||
import { withSnackbar } from "notistack";
|
||||
import { LinkableIconButton } from "../interfaces/overrides";
|
||||
import { emojifyString } from "../utilities/emojis";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "..//utilities/settings";
|
||||
|
||||
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
|
@ -44,6 +46,7 @@ interface IProfilePageState {
|
|||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: string;
|
||||
blockDialogOpen: boolean;
|
||||
isMasonryLayout?: boolean;
|
||||
}
|
||||
|
||||
class ProfilePage extends Component<any, IProfilePageState> {
|
||||
|
@ -59,7 +62,8 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
blockDialogOpen: false
|
||||
blockDialogOpen: false,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -305,8 +309,36 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
}
|
||||
}
|
||||
|
||||
renderPosts(posts: Status[]) {
|
||||
const { classes } = this.props;
|
||||
const postComponents = posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client} />;
|
||||
});
|
||||
if (this.state.isMasonryLayout) {
|
||||
return (
|
||||
<Masonry
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={classes["my-masonry-grid_column"]}
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
>
|
||||
{postComponents}
|
||||
</Masonry>
|
||||
);
|
||||
} else {
|
||||
return <div>{postComponents}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageContentLayoutConstraints} ${
|
||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
return (
|
||||
<div className={classes.pageLayoutMinimalConstraints}>
|
||||
<div className={classes.pageHeroBackground}>
|
||||
|
@ -464,7 +496,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
<div className={containerClasses}>
|
||||
{this.state.viewDidError ? (
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
|
@ -482,15 +514,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
)}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{this.renderPosts(this.state.posts)}
|
||||
<br />
|
||||
{this.state.viewDidLoad &&
|
||||
!this.state.viewDidError ? (
|
||||
|
|
|
@ -26,6 +26,8 @@ import { withSnackbar } from "notistack";
|
|||
import Post from "../components/Post";
|
||||
import { Status } from "../types/Status";
|
||||
import { Account } from "../types/Account";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
|
||||
interface ISearchPageState {
|
||||
query: string[] | string;
|
||||
|
@ -36,6 +38,7 @@ interface ISearchPageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: string;
|
||||
isMasonryLayout: boolean;
|
||||
}
|
||||
|
||||
class SearchPage extends Component<any, ISearchPageState> {
|
||||
|
@ -54,7 +57,8 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
query: searchParams.query,
|
||||
type: searchParams.type
|
||||
type: searchParams.type,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
};
|
||||
|
||||
if (searchParams.type === "tag") {
|
||||
|
@ -155,7 +159,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
viewDidLoad: true,
|
||||
viewIsLoading: false
|
||||
});
|
||||
console.log(this.state.tagResults);
|
||||
// console.log(this.state.tagResults);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
|
@ -195,7 +199,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
showAllAccountsFromQuery() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
<ListSubheader>Accounts</ListSubheader>
|
||||
|
||||
{this.state.results &&
|
||||
|
@ -260,22 +264,44 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
);
|
||||
}
|
||||
|
||||
renderPosts(posts: Status[]) {
|
||||
const { classes } = this.props;
|
||||
const postComponents = posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client} />;
|
||||
});
|
||||
if (this.state.isMasonryLayout) {
|
||||
return (
|
||||
<Masonry
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={classes["my-masonry-grid_column"]}
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
>
|
||||
{postComponents}
|
||||
</Masonry>
|
||||
);
|
||||
} else {
|
||||
return <div>{postComponents}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
showAllPostsFromQuery() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutConstraints} ${
|
||||
this.state.isMasonryLayout
|
||||
? classes.pageLayoutMasonry + " " + classes.noTopPaddingMargin
|
||||
: ""
|
||||
}`;
|
||||
return (
|
||||
<div>
|
||||
<div className={containerClasses}>
|
||||
<ListSubheader>Posts</ListSubheader>
|
||||
{this.state.results ? (
|
||||
this.state.results.statuses.length > 0 ? (
|
||||
this.state.results.statuses.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})
|
||||
this.renderPosts(this.state.results.statuses)
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
@ -291,20 +317,15 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
|
||||
showAllPostsWithTag() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints} ${
|
||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
return (
|
||||
<div>
|
||||
<div className={containerClasses}>
|
||||
<ListSubheader>Tagged posts</ListSubheader>
|
||||
{this.state.tagResults ? (
|
||||
this.state.tagResults.length > 0 ? (
|
||||
this.state.tagResults.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})
|
||||
this.renderPosts(this.state.tagResults)
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
@ -321,7 +342,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
<div>
|
||||
{this.state.type && this.state.type === "tag" ? (
|
||||
this.showAllPostsWithTag()
|
||||
) : (
|
||||
|
|
|
@ -174,6 +174,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
getConfig().then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
// console.log(!config.federation.allowPublicPosts);
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts
|
||||
});
|
||||
|
|
|
@ -46,41 +46,157 @@ import { Account, 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: "",
|
||||
|
@ -89,6 +205,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
authorizing: false,
|
||||
userInputErrorMessage: "",
|
||||
defaultRedirectAddress: "",
|
||||
redirectAddressIsDynamic: false,
|
||||
openAuthDialog: false,
|
||||
authCode: "",
|
||||
emergencyMode: false,
|
||||
|
@ -96,15 +213,21 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
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 (result.location === "dynamic") {
|
||||
console.warn(
|
||||
"Redirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!"
|
||||
);
|
||||
}
|
||||
|
||||
// Reset to mastodon.social if the location is a disallowed
|
||||
// domain.
|
||||
if (
|
||||
inDisallowedDomains(result.registration.defaultInstance)
|
||||
) {
|
||||
|
@ -113,6 +236,8 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
result.registration.defaultInstance = "mastodon.social";
|
||||
}
|
||||
|
||||
// Update the state as per the configuration
|
||||
this.setState({
|
||||
logoUrl: config.branding
|
||||
? result.branding.logo
|
||||
|
@ -133,10 +258,13 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
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"
|
||||
|
@ -144,6 +272,10 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for any existing logins and tokens before presenting
|
||||
* the login page
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (localStorage.getItem("login")) {
|
||||
this.getSavedSession();
|
||||
|
@ -154,18 +286,33 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user field in the state
|
||||
* @param user The string to update the state to
|
||||
*/
|
||||
updateUserInfo(user: string) {
|
||||
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() {
|
||||
if (localStorage.getItem("baseurl")) {
|
||||
return true;
|
||||
|
@ -174,11 +321,18 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
let loginData = localStorage.getItem("login");
|
||||
if (loginData) {
|
||||
|
@ -192,6 +346,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the emergency login mode.
|
||||
*/
|
||||
startEmergencyLogin() {
|
||||
if (!this.state.emergencyMode) {
|
||||
this.createEmergencyLogin();
|
||||
|
@ -199,6 +356,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
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() {
|
||||
if (this.state.registerBase) {
|
||||
return "https://" + this.state.registerBase + "/auth/sign_up";
|
||||
|
@ -207,15 +369,33 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
@ -235,7 +415,10 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
: "mastodon.social")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// Otherwise, treat them as if they're from the server
|
||||
else {
|
||||
let newUser = `${user}@${
|
||||
this.state.registerBase
|
||||
? this.state.registerBase
|
||||
|
@ -251,70 +434,104 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the user string for any errors and then create a client with an
|
||||
* ID and secret to start the authorization process.
|
||||
*/
|
||||
startLogin() {
|
||||
// Check if we have errored
|
||||
let error = this.checkForErrors();
|
||||
|
||||
// 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 ? this.state.brandName : "Hyperspace",
|
||||
scopes,
|
||||
baseurl,
|
||||
getRedirectAddress(this.state.defaultRedirectAddress)
|
||||
).then((resp: any) => {
|
||||
let saveSessionForCrashing: SaveClientSession = {
|
||||
clientId: resp.clientId,
|
||||
clientSecret: resp.clientSecret,
|
||||
authUrl: resp.url,
|
||||
emergency: false
|
||||
};
|
||||
localStorage.setItem(
|
||||
"login",
|
||||
JSON.stringify(saveSessionForCrashing)
|
||||
);
|
||||
this.setState({
|
||||
clientId: resp.clientId,
|
||||
clientSecret: resp.clientSecret,
|
||||
authUrl: resp.url,
|
||||
proceedToGetCode: true
|
||||
)
|
||||
// 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
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ? this.state.brandName : "Hyperspace",
|
||||
{
|
||||
scopes: scopes
|
||||
},
|
||||
baseurl
|
||||
).then((appData: any) => {
|
||||
let saveSessionForCrashing: SaveClientSession = {
|
||||
clientId: appData.clientId,
|
||||
clientSecret: appData.clientSecret,
|
||||
authUrl: appData.url,
|
||||
emergency: true
|
||||
};
|
||||
localStorage.setItem(
|
||||
"login",
|
||||
JSON.stringify(saveSessionForCrashing)
|
||||
);
|
||||
this.setState({
|
||||
clientId: appData.clientId,
|
||||
clientSecret: appData.clientSecret,
|
||||
authUrl: appData.url
|
||||
)
|
||||
// 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"
|
||||
|
@ -323,6 +540,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
window.location.href = `${redirAddress}/?code=${this.state.authCode}#/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a login attempt from a session
|
||||
*/
|
||||
resumeLogin() {
|
||||
let loginData = localStorage.getItem("login");
|
||||
if (loginData) {
|
||||
|
@ -337,10 +557,14 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the user input string for any possible errors
|
||||
*/
|
||||
checkForErrors(): boolean {
|
||||
let userInputError = false;
|
||||
let userInputErrorMessage = "";
|
||||
|
||||
// Is the user string blank?
|
||||
if (this.state.user === "") {
|
||||
userInputError = true;
|
||||
userInputErrorMessage = "Username cannot be blank.";
|
||||
|
@ -350,6 +574,8 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
if (this.state.user.includes("@")) {
|
||||
if (this.state.federates && this.state.federates === true) {
|
||||
let baseUrl = this.state.user.split("@")[1];
|
||||
|
||||
// Is the user's domain in the disallowed list?
|
||||
if (inDisallowedDomains(baseUrl)) {
|
||||
this.setState({
|
||||
userInputError: true,
|
||||
|
@ -357,6 +583,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
});
|
||||
return true;
|
||||
} else {
|
||||
// Are we unable to ping the server?
|
||||
axios
|
||||
.get(
|
||||
"https://instances.social/api/1.0/instances/show?name=" +
|
||||
|
@ -403,56 +630,89 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
Mastodon.fetchAccessToken(
|
||||
clientLoginSession.clientId,
|
||||
clientLoginSession.clientSecret,
|
||||
code,
|
||||
localStorage.getItem("baseurl") as string,
|
||||
this.state.emergencyMode
|
||||
? undefined
|
||||
: clientLoginSession.authUrl.includes(
|
||||
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
||||
)
|
||||
? undefined
|
||||
: window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}`
|
||||
)
|
||||
.then((tokenData: any) => {
|
||||
localStorage.setItem(
|
||||
"access_token",
|
||||
tokenData.access_token
|
||||
);
|
||||
window.location.href =
|
||||
window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}/#/`;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't authorize ${
|
||||
this.state.brandName
|
||||
? this.state.brandName
|
||||
: "Hyperspace"
|
||||
}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
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
|
||||
? 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()) {
|
||||
|
@ -468,6 +728,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the multi-user account panel
|
||||
*/
|
||||
showMultiAccount() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -481,11 +744,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
<ListItem
|
||||
onClick={() => {
|
||||
loginWithAccount(account);
|
||||
window.location.href =
|
||||
window.location.protocol ===
|
||||
"hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}/#/`;
|
||||
this.redirectToApp();
|
||||
}}
|
||||
button={true}
|
||||
>
|
||||
|
@ -527,6 +786,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the main landing panel
|
||||
*/
|
||||
showLanding() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -610,6 +872,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the login auth panel
|
||||
*/
|
||||
showLoginAuth() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -651,6 +916,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the emergency login panel
|
||||
*/
|
||||
showAuthDialog() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -705,6 +973,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the authorizing panel
|
||||
*/
|
||||
showAuthorizationLoader() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -725,6 +996,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the page
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
|
|
@ -74,7 +74,7 @@ class You extends Component<IYouProps, IYouState> {
|
|||
|
||||
getAccount() {
|
||||
let acct = localStorage.getItem("account");
|
||||
console.log(acct);
|
||||
// console.log(acct);
|
||||
if (acct) {
|
||||
return JSON.parse(acct);
|
||||
}
|
||||
|
|
|
@ -44,18 +44,18 @@ export function createHyperspaceApp(
|
|||
|
||||
/**
|
||||
* Gets the appropriate redirect address.
|
||||
* @param type The address or configuration to use
|
||||
* @param url The address or configuration to use
|
||||
*/
|
||||
export function getRedirectAddress(
|
||||
type: "desktop" | "dynamic" | string
|
||||
url: "desktop" | "dynamic" | string
|
||||
): string {
|
||||
switch (type) {
|
||||
switch (url) {
|
||||
case "desktop":
|
||||
return "hyperspace://hyperspace/app/";
|
||||
case "dynamic":
|
||||
return `https://${window.location.host}`;
|
||||
default:
|
||||
return type;
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -136,6 +136,15 @@ export function createUserDefaults() {
|
|||
export async function getConfig(): Promise<Config | undefined> {
|
||||
try {
|
||||
const resp = await axios.get("config.json");
|
||||
|
||||
let { location } = resp.data;
|
||||
|
||||
if (!location.endsWith("/")) {
|
||||
console.warn(
|
||||
"Location does not have a backslash, so Hyperspace has added it automatically."
|
||||
);
|
||||
resp.data.location = location + "/";
|
||||
}
|
||||
return resp.data as Config;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
|
|
Loading…
Reference in New Issue