Merge branch 'develop-1.1.0-beta2' into HD-25-clipboard-fire-fix
This commit is contained in:
commit
d07d099c2f
|
@ -12,7 +12,8 @@ jobs:
|
|||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: Run pre-build setup
|
||||
- name: Install certificates and entitlements
|
||||
if: github.actor == 'alicerunsonfedora' || github.actor == 'Nomad1556' || github.actor == 'audmaxwell'
|
||||
run: |
|
||||
echo "Downloading certificates and profiles..."
|
||||
echo "$ascCertificates" > certs.b64
|
||||
|
@ -44,4 +45,4 @@ jobs:
|
|||
run: |
|
||||
npm install
|
||||
npm run build --if-present
|
||||
npm run build-desktop-darwin-nosign
|
||||
npm run build-desktop-darwin-nosign
|
||||
|
|
|
@ -58,6 +58,10 @@ export const styles = (theme: Theme) =>
|
|||
display: "none"
|
||||
}
|
||||
},
|
||||
appBarBackButton: {
|
||||
marginLeft: -12,
|
||||
marginRight: 20
|
||||
},
|
||||
appBarTitle: {
|
||||
display: "none",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
|
|
|
@ -44,6 +44,7 @@ import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
|
|||
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
|
||||
import TrendingUpIcon from "@material-ui/icons/TrendingUp";
|
||||
import BuildIcon from "@material-ui/icons/Build";
|
||||
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||
|
||||
import { styles } from "./AppLayout.styles";
|
||||
import { MultiAccount, UAccount } from "../../types/Account";
|
||||
|
@ -67,30 +68,81 @@ import {
|
|||
getAccountRegistry,
|
||||
removeAccountFromRegistry
|
||||
} from "../../utilities/accounts";
|
||||
import { isChildView } from "../../utilities/appbar";
|
||||
|
||||
/**
|
||||
* The pre-define state interface for the app layout.
|
||||
*/
|
||||
interface IAppLayoutState {
|
||||
/**
|
||||
* Whether the account menu is open or not.
|
||||
*/
|
||||
acctMenuOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the drawer is open (mobile-only).
|
||||
*/
|
||||
drawerOpenOnMobile: boolean;
|
||||
|
||||
/**
|
||||
* The current user signed in.
|
||||
*/
|
||||
currentUser?: UAccount;
|
||||
|
||||
/**
|
||||
* The number of notifications received.
|
||||
*/
|
||||
notificationCount: number;
|
||||
|
||||
/**
|
||||
* Whether the log out dialog is open.
|
||||
*/
|
||||
logOutOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether federation has been enabled in the config.
|
||||
*/
|
||||
enableFederation?: boolean;
|
||||
|
||||
/**
|
||||
* The brand name of the app, if not "Hyperspace".
|
||||
*/
|
||||
brandName?: string;
|
||||
|
||||
/**
|
||||
* Whether the app is in development mode.
|
||||
*/
|
||||
developerMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base app layout class. Responsible for the search bar, navigation menus, etc.
|
||||
*/
|
||||
export class AppLayout extends Component<any, IAppLayoutState> {
|
||||
/**
|
||||
* The Mastodon client to operate with.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* A stream listener to listen for new streaming events from Mastodon.
|
||||
*/
|
||||
streamListener: any;
|
||||
|
||||
/**
|
||||
* Construct the app layout.
|
||||
* @param props The properties to pass in.
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Create the Mastodon client
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
// Initialize the state
|
||||
this.state = {
|
||||
drawerOpenOnMobile: false,
|
||||
acctMenuOpen: false,
|
||||
|
@ -98,14 +150,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
logOutOpen: false
|
||||
};
|
||||
|
||||
// Bind functions as properties to this class for reference
|
||||
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
||||
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
||||
this.clearBadge = this.clearBadge.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run post-mount tasks such as getting account data and refreshing the config file.
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Get the account data.
|
||||
this.getAccountData();
|
||||
|
||||
// Read the config file and then update the state.
|
||||
getConfig().then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
|
@ -119,18 +177,25 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
});
|
||||
|
||||
// Listen for notifications.
|
||||
this.streamNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated credentials from Mastodon or pull information from local storage.
|
||||
*/
|
||||
getAccountData() {
|
||||
// Try to get updated credentials from Mastodon.
|
||||
this.client
|
||||
.get("/accounts/verify_credentials")
|
||||
.then((resp: any) => {
|
||||
// Update the account if possible.
|
||||
let data: UAccount = resp.data;
|
||||
this.setState({ currentUser: data });
|
||||
sessionStorage.setItem("id", data.id);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// Otherwise, pull from local storage.
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't find profile info: " + err.name
|
||||
);
|
||||
|
@ -140,9 +205,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a stream listener and listen for notifications.
|
||||
*/
|
||||
streamNotifications() {
|
||||
// Set up the stream listener.
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
// Set the count if the user asked to display the total count.
|
||||
if (getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||
this.client.get("/notifications").then((resp: any) => {
|
||||
let notifArray = resp.data;
|
||||
|
@ -150,14 +220,17 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
});
|
||||
}
|
||||
|
||||
// Listen for notifications.
|
||||
this.streamListener.on("notification", (notif: Notification) => {
|
||||
const notificationCount = this.state.notificationCount + 1;
|
||||
this.setState({ notificationCount });
|
||||
|
||||
// Update the badge on the desktop.
|
||||
if (isDesktopApp()) {
|
||||
getElectronApp().setBadgeCount(notificationCount);
|
||||
}
|
||||
|
||||
// Set up a push notification if the window isn't in focus.
|
||||
if (!document.hasFocus()) {
|
||||
let primaryMessage = "";
|
||||
let secondaryMessage = "";
|
||||
|
@ -216,25 +289,39 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
break;
|
||||
}
|
||||
|
||||
// Respectfully send the notification request.
|
||||
sendNotificationRequest(primaryMessage, secondaryMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the account menu.
|
||||
*/
|
||||
toggleAcctMenu() {
|
||||
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the app drawer, if on mobile.
|
||||
*/
|
||||
toggleDrawerOnMobile() {
|
||||
this.setState({
|
||||
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the logout dialog.
|
||||
*/
|
||||
toggleLogOutDialog() {
|
||||
this.setState({ logOutOpen: !this.state.logOutOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search and redirect to the search page.
|
||||
* @param what The query input from the search box
|
||||
*/
|
||||
searchForQuery(what: string) {
|
||||
what = what.replace(/^#/g, "tag:");
|
||||
console.log(what);
|
||||
|
@ -243,9 +330,13 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
: "/#/search?query=" + what;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear login information, remove the account from the registry, and reload the web page.
|
||||
*/
|
||||
logOutAndRestart() {
|
||||
let loginData = localStorage.getItem("login");
|
||||
if (loginData) {
|
||||
// Remove account from the registry.
|
||||
let registry = getAccountRegistry();
|
||||
|
||||
registry.forEach((registryItem: MultiAccount, index: number) => {
|
||||
|
@ -257,15 +348,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
});
|
||||
|
||||
// Clear some of the local storage fields.
|
||||
let items = ["login", "account", "baseurl", "access_token"];
|
||||
items.forEach(entry => {
|
||||
localStorage.removeItem(entry);
|
||||
});
|
||||
|
||||
// Finally, reload.
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the notifications badge.
|
||||
*/
|
||||
clearBadge() {
|
||||
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||
this.setState({ notificationCount: 0 });
|
||||
|
@ -276,6 +372,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the title bar.
|
||||
*/
|
||||
titlebar() {
|
||||
const { classes } = this.props;
|
||||
if (isDarwinApp()) {
|
||||
|
@ -307,6 +406,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app drawer. On the desktop, this appears as a sidebar in larger layouts.
|
||||
*/
|
||||
appDrawer() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -476,6 +578,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the entire layout.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -484,6 +589,18 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
{this.titlebar()}
|
||||
<AppBar className={classes.appBar} position="static">
|
||||
<Toolbar>
|
||||
{isDesktopApp() &&
|
||||
isChildView(window.location.hash) ? (
|
||||
<IconButton
|
||||
className={classes.appBarBackButton}
|
||||
color="inherit"
|
||||
aria-label="Go back"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
className={classes.appBarMenuButton}
|
||||
color="inherit"
|
||||
|
@ -675,7 +792,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
variant="temporary"
|
||||
anchor={"left"}
|
||||
open={this.state.drawerOpenOnMobile}
|
||||
onClose={this.toggleDrawerOnMobile}
|
||||
onClick={this.toggleDrawerOnMobile}
|
||||
classes={{ paper: classes.drawerPaper }}
|
||||
>
|
||||
{this.appDrawer()}
|
||||
|
|
|
@ -81,6 +81,15 @@ export const styles = (theme: Theme) =>
|
|||
paddingTop: theme.spacing.unit,
|
||||
paddingBottom: theme.spacing.unit
|
||||
},
|
||||
postAuthorAccount: {
|
||||
color: theme.palette.grey[500],
|
||||
marginLeft: theme.spacing.unit * 0.5,
|
||||
},
|
||||
postReblogIcon: {
|
||||
marginBottom: theme.spacing.unit * -0.5,
|
||||
marginLeft: theme.spacing.unit * 0.5,
|
||||
marginRight: theme.spacing.unit * 0.5,
|
||||
},
|
||||
postAuthorEmoji: {
|
||||
height: theme.typography.fontSize,
|
||||
verticalAlign: "middle"
|
||||
|
|
|
@ -398,21 +398,34 @@ export class Post extends React.Component<any, IPostState> {
|
|||
const { classes } = this.props;
|
||||
if (post.reblog) {
|
||||
let author = post.reblog.account;
|
||||
let origString = `<span>${author.display_name ||
|
||||
author.username} (@${author.acct}) 🔄 ${post.account
|
||||
.display_name || post.account.username}</span>`;
|
||||
let emojis = author.emojis;
|
||||
emojis.concat(post.account.emojis);
|
||||
return emojifyString(origString, emojis, classes.postAuthorEmoji);
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
{emojifyString(author.display_name || author.username, author.emojis, classes.postAuthorEmoji)}
|
||||
</span>
|
||||
<span className={classes.postAuthorAccount}>
|
||||
@{emojifyString(author.acct, author.emojis, classes.postAuthorEmoji)}
|
||||
</span>
|
||||
<AutorenewIcon fontSize='small' className={classes.postReblogIcon} />
|
||||
<span>
|
||||
{emojifyString(post.account.display_name || post.account.username, emojis, classes.postAuthorEmoji)}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
let author = post.account;
|
||||
let origString = `<span>${author.display_name ||
|
||||
author.username} (@${author.acct})</span>`;
|
||||
return emojifyString(
|
||||
origString,
|
||||
author.emojis,
|
||||
classes.postAuthorEmoji
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
{emojifyString(author.display_name || author.username, author.emojis, classes.postAuthorEmoji)}
|
||||
</span>
|
||||
<span className={classes.postAuthorAccount}>
|
||||
@{emojifyString(author.acct, author.emojis, classes.postAuthorEmoji)}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -656,13 +669,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
title={
|
||||
<Typography
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.getReblogAuthors(post)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={<Typography>{this.getReblogAuthors(post)}</Typography>}
|
||||
subheader={moment(post.created_at).format(
|
||||
"MMMM Do YYYY [at] h:mm A"
|
||||
)}
|
||||
|
|
|
@ -45,5 +45,25 @@ export const styles = (theme: Theme) =>
|
|||
},
|
||||
pollWizardFlexGrow: {
|
||||
flexGrow: 1
|
||||
},
|
||||
draftDisplayArea: {
|
||||
display: "flex",
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
borderColor: theme.palette.action.disabledBackground,
|
||||
borderWidth: 0.25,
|
||||
borderStyle: "solid",
|
||||
borderRadius: 2,
|
||||
verticalAlign: "middle",
|
||||
marginLeft: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
draftText: {
|
||||
padding: theme.spacing.unit / 2
|
||||
},
|
||||
draftFlexGrow: {
|
||||
flexGrow: 1
|
||||
}
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
getConfig,
|
||||
getUserDefaultBool
|
||||
} from "../utilities/settings";
|
||||
import { draftExists, writeDraft, loadDraft } from "../utilities/compose";
|
||||
|
||||
/**
|
||||
* The state for the Composer page.
|
||||
|
@ -230,6 +231,31 @@ class Composer extends Component<any, IComposerState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is unsaved text and store it as a draft.
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.state.text !== "") {
|
||||
writeDraft(
|
||||
this.state.text,
|
||||
this.state.reply ? Number(this.state.reply) : -999
|
||||
);
|
||||
this.props.enqueueSnackbar("Draft saved.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the draft from session storage and pre-load it into the state.
|
||||
*/
|
||||
restoreDraft() {
|
||||
const draft = loadDraft();
|
||||
const text = draft.contents;
|
||||
const reply =
|
||||
draft.replyId !== -999 ? draft.replyId.toString() : undefined;
|
||||
this.setState({ text, reply });
|
||||
this.props.enqueueSnackbar("Restored draft.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the location string and attempt to parse it into a parsed query.
|
||||
* @param location The location string from React Router.
|
||||
|
@ -930,6 +956,21 @@ class Composer extends Component<any, IComposerState> {
|
|||
) : null}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
{draftExists() ? (
|
||||
<DialogContent className={classes.draftDisplayArea}>
|
||||
<Typography className={classes.draftText}>
|
||||
You have an unsaved post.
|
||||
</Typography>
|
||||
<div className={classes.draftFlexGrow} />
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => this.restoreDraft()}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</DialogContent>
|
||||
) : null}
|
||||
<DialogActions>
|
||||
<Button color="secondary" onClick={() => this.post()}>
|
||||
Post
|
||||
|
|
|
@ -33,35 +33,83 @@ import NotificationsIcon from "@material-ui/icons/Notifications";
|
|||
import Mastodon from "megalodon";
|
||||
import { Notification } from "../types/Notification";
|
||||
import { Account } from "../types/Account";
|
||||
import { Relationship } from "../types/Relationship";
|
||||
import { withSnackbar } from "notistack";
|
||||
|
||||
/**
|
||||
* The state interface for the notifications page.
|
||||
*/
|
||||
interface INotificationsPageState {
|
||||
/**
|
||||
* The list of notifications, if it exists.
|
||||
*/
|
||||
notifications?: [Notification];
|
||||
|
||||
/**
|
||||
* Whether the view is still loading.
|
||||
*/
|
||||
viewIsLoading: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view has loaded.
|
||||
*/
|
||||
viewDidLoad?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view has loaded but in error.
|
||||
*/
|
||||
viewDidError?: boolean;
|
||||
|
||||
/**
|
||||
* The error code for an errored state, if possible.
|
||||
*/
|
||||
viewDidErrorCode?: string;
|
||||
|
||||
/**
|
||||
* Whether the delete confirmation dialog should be open.
|
||||
*/
|
||||
deleteDialogOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The notifications page.
|
||||
*/
|
||||
class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||
/**
|
||||
* The Mastodon object to perform notification operations on.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* The stream listener for tuning in to notifications.
|
||||
*/
|
||||
streamListener: any;
|
||||
|
||||
/**
|
||||
* Construct the notifications page.
|
||||
* @param props The properties to pass in
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Create the Mastodon object.
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
|
||||
// Initialize the state.
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
deleteDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform pre-mount tasks.
|
||||
*/
|
||||
componentWillMount() {
|
||||
// Get the list of notifications and update the state.
|
||||
this.client
|
||||
.get("/notifications")
|
||||
.then((resp: any) => {
|
||||
|
@ -82,10 +130,17 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post-mount tasks.
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Start listening for new notifications after fetching.
|
||||
this.streamNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a stream listener and keep updating notifications.
|
||||
*/
|
||||
streamNotifications() {
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
|
@ -98,10 +153,19 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the state of the delete dialog.
|
||||
*/
|
||||
toggleDeleteDialog() {
|
||||
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML content from a string containing HTML content.
|
||||
*
|
||||
* @param text The sanitized HTML to strip
|
||||
* @returns A string containing the contents of the sanitized HTML
|
||||
*/
|
||||
removeHTMLContent(text: string) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = text;
|
||||
|
@ -111,6 +175,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
return innerContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a notification from the server.
|
||||
* @param id The notification's ID
|
||||
*/
|
||||
removeNotification(id: string) {
|
||||
this.client
|
||||
.post(`/notifications/${id}/dismiss`)
|
||||
|
@ -142,6 +210,9 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all notifications from the server.
|
||||
*/
|
||||
removeAllNotifications() {
|
||||
this.client
|
||||
.post("/notifications/clear")
|
||||
|
@ -159,6 +230,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single notification unit to be used in a list
|
||||
* @param notif The notification to work with.
|
||||
*/
|
||||
createNotification(notif: Notification) {
|
||||
const { classes } = this.props;
|
||||
let primary = "";
|
||||
|
@ -293,23 +368,53 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
.post(`/accounts/${acct.id}/follow`)
|
||||
.get(`/accounts/relationships`, { id: acct.id })
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"You are now following this account."
|
||||
);
|
||||
// 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 follow account: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
this.props.enqueueSnackbar("Couldn't find relationship.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the notification page.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
|
|
@ -559,7 +559,36 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</Toolbar>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className={classes.pageHeroBackground}>
|
||||
<div className={classes.pageHeroBackgroundImage} />
|
||||
<div className={classes.profileContent}>
|
||||
<br />
|
||||
<Avatar className={classes.settingsAvatar} />
|
||||
<div
|
||||
className={classes.profileUserBox}
|
||||
style={{ margin: "auto" }}
|
||||
>
|
||||
<Typography
|
||||
className={classes.settingsHeaderText}
|
||||
color="inherit"
|
||||
component="h1"
|
||||
>
|
||||
{"Loading..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="inherit"
|
||||
className={classes.settingsDetailText}
|
||||
component="p"
|
||||
>
|
||||
@{"..."}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.pageGrow} />
|
||||
<Toolbar />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
<ListSubheader>Appearance</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Base draft type for a cached draft.
|
||||
*/
|
||||
export type Draft = {
|
||||
/**
|
||||
* The contents of the draft (i.e, its post text).
|
||||
*/
|
||||
contents: string;
|
||||
/**
|
||||
* The ID of the post it replies to, if applicable. If there isn't one, it should be set to -999.
|
||||
*/
|
||||
replyId: number;
|
||||
};
|
|
@ -1,5 +1,14 @@
|
|||
import { isDarwinApp } from "./desktop";
|
||||
|
||||
/**
|
||||
* A list containing the types of child views.
|
||||
*
|
||||
* This list is used to help determine if a back button is necessary, usually because there
|
||||
* is no defined way of returning to the parent view without using the menu bar or keyboard
|
||||
* shortcut in desktop apps.
|
||||
*/
|
||||
export const childViews = ["#/profile", "#/conversation"];
|
||||
|
||||
/**
|
||||
* Determine whether the title bar is being displayed.
|
||||
* This might be useful in cases where styles are dependent on the title bar's visibility, such as heights.
|
||||
|
@ -9,3 +18,20 @@ import { isDarwinApp } from "./desktop";
|
|||
export function isAppbarExpanded(): boolean {
|
||||
return isDarwinApp() || process.env.NODE_ENV === "development";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a path is considered a "child view".
|
||||
*
|
||||
* This is often used to determine whether a back button should be rendered or not.
|
||||
* @param path The path of the page, usually its hash
|
||||
* @returns Boolean distating if the view is a child view.
|
||||
*/
|
||||
export function isChildView(path: string): boolean {
|
||||
let protocolMatched = false;
|
||||
childViews.forEach((childViewProtocol: string) => {
|
||||
if (path.startsWith(childViewProtocol)) {
|
||||
protocolMatched = true;
|
||||
}
|
||||
});
|
||||
return protocolMatched;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { Draft } from "../types/Draft";
|
||||
|
||||
/**
|
||||
* Check whether a cached draft exists.
|
||||
*/
|
||||
export function draftExists(): boolean {
|
||||
return sessionStorage.getItem("cachedDraft") !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a draft to session storage.
|
||||
* @param draft The text of the post.
|
||||
* @param replyId The post's reply ID, if available.
|
||||
*/
|
||||
export function writeDraft(draft: string, replyId?: number) {
|
||||
let cachedDraft = {
|
||||
contents: draft,
|
||||
replyId: replyId ? replyId : -999
|
||||
};
|
||||
sessionStorage.setItem("cachedDraft", JSON.stringify(cachedDraft));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cached draft and remove it from session storage.
|
||||
* @returns A Draft object with the draft's contents and reply ID (or -999).
|
||||
*/
|
||||
export function loadDraft(): Draft {
|
||||
let contents = "";
|
||||
let replyId = -999;
|
||||
if (draftExists()) {
|
||||
let draft = sessionStorage.getItem("cachedDraft");
|
||||
sessionStorage.removeItem("cachedDraft");
|
||||
if (draft != null) {
|
||||
const draftObject = JSON.parse(draft);
|
||||
contents = draftObject.contents;
|
||||
replyId = draftObject.replyId;
|
||||
}
|
||||
}
|
||||
return {
|
||||
contents: contents,
|
||||
replyId: replyId
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue