From e5cfaf0a449e6fa1c850bd3abf76e060620a3a89 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Thu, 19 Dec 2019 15:18:26 -0500 Subject: [PATCH 01/27] logs post state at various points, logs post state on click post header --- src/components/Post/Post.tsx | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index a6d2621..d485f24 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -60,6 +60,25 @@ import ShareMenu from "./PostShareMenu"; import { emojifyString } from "../../utilities/emojis"; import { PollOption } from "../../types/Poll"; +const log = (post: Status, msg = '') => { + let { + replies_count, + reblogs_count, + favourites_count, + favourited, + reblogged, + reblog + } = post + console.log(msg, { + replies_count, + reblogs_count, + favourites_count, + favourited, + reblogged, + reblog + }) +} + interface IPostProps { post: Status; classes: any; @@ -512,11 +531,20 @@ export class Post extends React.Component { toggleFavorited(post: Status) { let _this = this; + log(post, 'before un/favorite') + let { favourites_count, reblog } = post if (post.favourited) { this.client .post(`/statuses/${post.id}/unfavourite`) .then((resp: any) => { let post: Status = resp.data; + if (post.favourites_count === favourites_count) { + post.favourites_count-- + } + if (post.reblog !== reblog) { + post.reblog = reblog + } + log(post, 'after unfavorite') this.setState({ post }); }) .catch((err: Error) => { @@ -533,6 +561,13 @@ export class Post extends React.Component { .post(`/statuses/${post.id}/favourite`) .then((resp: any) => { let post: Status = resp.data; + if (post.reblog !== reblog) { + post.reblog = reblog + } + if (post.favourites_count === favourites_count) { + post.favourites_count++ + } + log(post, 'after favorite') this.setState({ post }); }) .catch((err: Error) => { @@ -548,11 +583,26 @@ export class Post extends React.Component { } toggleReblogged(post: Status) { + log(post, 'before un/reblog') + let { reblogs_count, reblogged, favourited, reblog } = post if (post.reblogged) { this.client .post(`/statuses/${post.id}/unreblog`) .then((resp: any) => { let post: Status = resp.data; + if (post.reblogs_count === reblogs_count) { + post.reblogs_count-- + } + if (post.reblogged === reblogged) { + post.reblogged = !reblogged + } + if (post.favourited !== favourited) { + post.favourited = favourited + } + if (post.reblog === reblog) { + post.reblog = null + } + log(post, 'after unreblog') this.setState({ post }); }) .catch((err: Error) => { @@ -569,6 +619,19 @@ export class Post extends React.Component { .post(`/statuses/${post.id}/reblog`) .then((resp: any) => { let post: Status = resp.data; + if (post.reblogs_count === reblogs_count) { + post.reblogs_count++ + } + if (post.reblogged === reblogged) { + post.reblogged = !reblogged + } + if (post.favourited !== favourited) { + post.favourited = favourited + } + if (post.reblog === null) { + post.reblog = reblog + } + log(post, 'after reblog') this.setState({ post }); }) .catch((err: Error) => { @@ -631,6 +694,7 @@ export class Post extends React.Component { elevation={this.props.threadHeader ? 0 : 1} > log(post)} avatar={ Date: Sun, 22 Dec 2019 13:36:52 -0500 Subject: [PATCH 02/27] Refactor toggle and Mastodon URL methods --- src/components/Post/Post.tsx | 168 +++++++++-------------------------- 1 file changed, 40 insertions(+), 128 deletions(-) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index d485f24..ee471a8 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -60,7 +60,7 @@ import ShareMenu from "./PostShareMenu"; import { emojifyString } from "../../utilities/emojis"; import { PollOption } from "../../types/Poll"; -const log = (post: Status, msg = '') => { +const log = (post: Status, msg = "") => { let { replies_count, reblogs_count, @@ -68,7 +68,7 @@ const log = (post: Status, msg = '') => { favourited, reblogged, reblog - } = post + } = post; console.log(msg, { replies_count, reblogs_count, @@ -76,8 +76,8 @@ const log = (post: Status, msg = '') => { favourited, reblogged, reblog - }) -} + }); +}; interface IPostProps { post: Status; @@ -519,131 +519,39 @@ export class Post extends React.Component { } } + /** + * 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; - log(post, 'before un/favorite') - let { favourites_count, reblog } = post - if (post.favourited) { - this.client - .post(`/statuses/${post.id}/unfavourite`) - .then((resp: any) => { - let post: Status = resp.data; - if (post.favourites_count === favourites_count) { - post.favourites_count-- - } - if (post.reblog !== reblog) { - post.reblog = reblog - } - log(post, 'after unfavorite') - 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; - if (post.reblog !== reblog) { - post.reblog = reblog - } - if (post.favourites_count === favourites_count) { - post.favourites_count++ - } - log(post, 'after favorite') - this.setState({ post }); - }) - .catch((err: Error) => { - _this.props.enqueueSnackbar( - `Couldn't favorite post: ${err.name}`, - { - variant: "error" - } - ); - console.log(err.message); - }); - } - } - - toggleReblogged(post: Status) { - log(post, 'before un/reblog') - let { reblogs_count, reblogged, favourited, reblog } = post - if (post.reblogged) { - this.client - .post(`/statuses/${post.id}/unreblog`) - .then((resp: any) => { - let post: Status = resp.data; - if (post.reblogs_count === reblogs_count) { - post.reblogs_count-- - } - if (post.reblogged === reblogged) { - post.reblogged = !reblogged - } - if (post.favourited !== favourited) { - post.favourited = favourited - } - if (post.reblog === reblog) { - post.reblog = null - } - log(post, 'after unreblog') - 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; - if (post.reblogs_count === reblogs_count) { - post.reblogs_count++ - } - if (post.reblogged === reblogged) { - post.reblogged = !reblogged - } - if (post.favourited !== favourited) { - post.favourited = favourited - } - if (post.reblog === null) { - post.reblog = reblog - } - log(post, 'after reblog') - this.setState({ post }); - }) - .catch((err: Error) => { - this.props.enqueueSnackbar( - `Couldn't boost post: ${err.name}`, - { - variant: "error" - } - ); - console.log(err.message); - }); - } + /** + * Toggle the status of a post's action and update the state + * @param type Either the "reblog" or "favorite" action + * @param post The post to toggle the status of + */ + togglePostStatus(type: "reblog" | "favourite", post: Status) { + const shouldUndo = post.favourited || post.reblogged; + let requestBuilder = `/statuses/${post.id}/${ + shouldUndo ? "un" : "" + }${type}`; + this.client + .post(requestBuilder) + .then((resp: any) => { + this.setState({ post: resp.data as Status }); + }) + .catch((err: Error) => { + this.props.enqueueSnackbar( + `Couldn't ${shouldUndo ? "un" : ""}${type} post: ${ + err.name + }`, + { variant: "error" } + ); + console.error(err.message); + }); } showDeleteDialog() { @@ -764,7 +672,9 @@ export class Post extends React.Component { this.toggleFavorited(post)} + onClick={() => + this.togglePostStatus("favourite", post) + } > { this.toggleReblogged(post)} + onClick={() => + this.togglePostStatus("reblog", post) + } > Date: Tue, 21 Jan 2020 09:40:47 -0500 Subject: [PATCH 03/27] Add redirectToApp in Welcome, start Welcome docs --- src/pages/Welcome.tsx | 126 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index 72fef9d..7e9e717 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -46,32 +46,133 @@ 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; } @@ -89,6 +190,7 @@ class WelcomePage extends Component { authorizing: false, userInputErrorMessage: "", defaultRedirectAddress: "", + redirectAddressIsDynamic: false, openAuthDialog: false, authCode: "", emergencyMode: false, @@ -133,6 +235,7 @@ class WelcomePage extends Component { config.location != "dynamic" ? config.location : `https://${window.location.host}`, + redirectAddressIsDynamic: config.location == "dynamic", version: config.version }); } @@ -433,10 +536,7 @@ class WelcomePage extends Component { "access_token", tokenData.access_token ); - window.location.href = - window.location.protocol === "hyperspace:" - ? "hyperspace://hyperspace/app/" - : `https://${window.location.host}/#/`; + this.redirectToApp(); }) .catch((err: Error) => { this.props.enqueueSnackbar( @@ -453,6 +553,18 @@ class WelcomePage extends Component { } } + /** + * 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 + "/#/"; + } + titlebar() { const { classes } = this.props; if (isDarwinApp()) { @@ -481,11 +593,7 @@ class WelcomePage extends Component { { loginWithAccount(account); - window.location.href = - window.location.protocol === - "hyperspace:" - ? "hyperspace://hyperspace/app/" - : `https://${window.location.host}/#/`; + this.redirectToApp(); }} button={true} > From 8d14ff78dfaa523cf484789b95dbbc93608b89e9 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Tue, 21 Jan 2020 10:05:05 -0500 Subject: [PATCH 04/27] Finish docs for Welcome page --- src/pages/Welcome.tsx | 228 +++++++++++++++++++++++++++++++++++------- 1 file changed, 193 insertions(+), 35 deletions(-) diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index 7e9e717..e2a133d 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -176,12 +176,27 @@ interface IWelcomeState { 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 { + /** + * 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: "", @@ -198,15 +213,21 @@ class WelcomePage extends Component { 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) ) { @@ -215,6 +236,8 @@ class WelcomePage extends Component { ); result.registration.defaultInstance = "mastodon.social"; } + + // Update the state as per the configuration this.setState({ logoUrl: config.branding ? result.branding.logo @@ -240,6 +263,8 @@ class WelcomePage extends Component { }); } }) + + // 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" @@ -247,6 +272,10 @@ class WelcomePage extends Component { }); } + /** + * Look for any existing logins and tokens before presenting + * the login page + */ componentDidMount() { if (localStorage.getItem("login")) { this.getSavedSession(); @@ -257,18 +286,33 @@ class WelcomePage extends Component { } } + /** + * 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; @@ -277,11 +321,18 @@ class WelcomePage extends Component { } } + /** + * 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) { @@ -295,6 +346,9 @@ class WelcomePage extends Component { } } + /** + * Start the emergency login mode. + */ startEmergencyLogin() { if (!this.state.emergencyMode) { this.createEmergencyLogin(); @@ -302,6 +356,11 @@ class WelcomePage extends Component { 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"; @@ -310,15 +369,33 @@ class WelcomePage extends Component { } } + /** + * 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; @@ -338,7 +415,10 @@ class WelcomePage extends Component { : "mastodon.social") ); } - } else { + } + + // Otherwise, treat them as if they're from the server + else { let newUser = `${user}@${ this.state.registerBase ? this.state.registerBase @@ -354,70 +434,104 @@ class WelcomePage extends Component { } } + /** + * 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" @@ -426,6 +540,9 @@ class WelcomePage extends Component { window.location.href = `${redirAddress}/?code=${this.state.authCode}#/`; } + /** + * Restore a login attempt from a session + */ resumeLogin() { let loginData = localStorage.getItem("login"); if (loginData) { @@ -440,10 +557,14 @@ class WelcomePage extends Component { } } + /** + * 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."; @@ -453,6 +574,8 @@ class WelcomePage extends Component { 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, @@ -460,6 +583,7 @@ class WelcomePage extends Component { }); return true; } else { + // Are we unable to ping the server? axios .get( "https://instances.social/api/1.0/instances/show?name=" + @@ -506,12 +630,21 @@ class WelcomePage extends Component { } } + /** + * 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 @@ -531,6 +664,8 @@ class WelcomePage extends Component { ? "hyperspace://hyperspace/app/" : `https://${window.location.host}` ) + // If we succeeded, store the access token and redirect to the + // main view. .then((tokenData: any) => { localStorage.setItem( "access_token", @@ -538,6 +673,8 @@ class WelcomePage extends Component { ); this.redirectToApp(); }) + + // Otherwise, present an error .catch((err: Error) => { this.props.enqueueSnackbar( `Couldn't authorize ${ @@ -565,6 +702,9 @@ class WelcomePage extends Component { : this.state.defaultRedirectAddress + "/#/"; } + /** + * Render the title bar for macOS + */ titlebar() { const { classes } = this.props; if (isDarwinApp()) { @@ -580,6 +720,9 @@ class WelcomePage extends Component { } } + /** + * Show the multi-user account panel + */ showMultiAccount() { const { classes } = this.props; return ( @@ -635,6 +778,9 @@ class WelcomePage extends Component { ); } + /** + * Show the main landing panel + */ showLanding() { const { classes } = this.props; return ( @@ -718,6 +864,9 @@ class WelcomePage extends Component { ); } + /** + * Show the login auth panel + */ showLoginAuth() { const { classes } = this.props; return ( @@ -759,6 +908,9 @@ class WelcomePage extends Component { ); } + /** + * Show the emergency login panel + */ showAuthDialog() { const { classes } = this.props; return ( @@ -813,6 +965,9 @@ class WelcomePage extends Component { ); } + /** + * Show the authorizing panel + */ showAuthorizationLoader() { const { classes } = this.props; return ( @@ -833,6 +988,9 @@ class WelcomePage extends Component { ); } + /** + * Render the page + */ render() { const { classes } = this.props; return ( From a46d9c6c0f11c42a0a9cca267d4064277cb01a12 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Tue, 21 Jan 2020 10:41:45 -0500 Subject: [PATCH 05/27] Read the config in checkForToken --- src/pages/Welcome.tsx | 103 +++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 37 deletions(-) diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index e2a133d..6603119 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -649,43 +649,72 @@ class WelcomePage extends Component { 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}` - ) - // If we succeeded, store the access token and redirect to the - // main view. - .then((tokenData: any) => { - localStorage.setItem( - "access_token", - tokenData.access_token - ); - this.redirectToApp(); - }) - - // Otherwise, present an error - .catch((err: Error) => { - this.props.enqueueSnackbar( - `Couldn't authorize ${ - this.state.brandName - ? this.state.brandName - : "Hyperspace" - }: ${err.name}`, - { variant: "error" } - ); - console.error(err.message); - }); + + // Re-read the config file. It's possible the state doesn't take + // effect immediately. + getConfig().then((result: any) => { + if (result !== undefined) { + let config: Config = result; + let redirectUrl = + `https://${window.location.host}` || undefined; + + // Is this an emergency login? Don't pass a redirect + // URI + if ( + this.state.emergencyMode || + clientLoginSession.authUrl.includes( + "urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob" + ) + ) { + redirectUrl = undefined; + } + + // Is this the desktop app? Change the redirect URI + else if (window.location.protocol === "hyperspace:") { + redirectUrl = "hyperspace://hyperspace/app/"; + } + + // Otherwise, read the config + else if ( + config.location !== "dynamic" && + config.location !== "desktop" && + !inDisallowedDomains(config.location) + ) { + redirectUrl = config.location; + } + + // Fetch the access token + Mastodon.fetchAccessToken( + clientLoginSession.clientId, + clientLoginSession.clientSecret, + code, + localStorage.getItem("baseurl") as string, + redirectUrl + ) + // If we succeeded, store the access token and redirect to the + // main view. + .then((tokenData: any) => { + localStorage.setItem( + "access_token", + tokenData.access_token + ); + this.redirectToApp(); + }) + + // Otherwise, present an error + .catch((err: Error) => { + this.props.enqueueSnackbar( + `Couldn't authorize ${ + this.state.brandName + ? this.state.brandName + : "Hyperspace" + }: ${err.name}`, + { variant: "error" } + ); + console.error(err.message); + }); + } + }); } } } From 073efe137b9f96091e6b8fae1730a583937638f6 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Thu, 23 Jan 2020 22:36:25 -0500 Subject: [PATCH 06/27] loads more posts at scroll position, disables post zoom effect --- src/components/Post/Post.tsx | 4 ---- src/pages/Timeline.tsx | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 629dcd1..2c74fd0 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -405,8 +405,6 @@ export class Post extends React.Component { emojis.concat(reblogger.emojis); } - console.log(post); - return ( <> { const { classes } = this.props; const post = this.state.post; return ( - { {this.showDeleteDialog()} - ); } } diff --git a/src/pages/Timeline.tsx b/src/pages/Timeline.tsx index d5b686b..03c809f 100644 --- a/src/pages/Timeline.tsx +++ b/src/pages/Timeline.tsx @@ -120,6 +120,9 @@ class TimelinePage extends Component { // Create the stream listener from the properties. this.streamListener = this.client.stream(this.props.stream); + + this.loadMoreTimelinePieces = this.loadMoreTimelinePieces.bind(this); + this.shouldLoadMorePosts = this.shouldLoadMorePosts.bind(this); } /** @@ -198,10 +201,18 @@ class TimelinePage extends Component { } /** - * Halt the stream listener when unmounting the component. + * Listen for when scroll position changes + */ + componentDidMount() { + window.addEventListener("scroll", this.shouldLoadMorePosts); + } + + /** + * Halt the stream and scroll listeners when unmounting the component. */ componentWillUnmount() { this.streamListener.stop(); + window.removeEventListener("scroll", this.shouldLoadMorePosts); } /** @@ -261,6 +272,19 @@ class TimelinePage extends Component { } } + /** + * Load more posts when scroll is near the end of the page + */ + shouldLoadMorePosts(e: Event) { + let difference = + document.body.clientHeight - window.scrollY - window.innerHeight; + console.log(difference); + if (difference < 5000) { + console.log("load!"); + this.loadMoreTimelinePieces(); + } + } + /** * Render the timeline page. */ From c94b483b47c0db64c4775672fe174fb3af1a6798 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Fri, 24 Jan 2020 10:49:10 -0500 Subject: [PATCH 07/27] Call userLoggedIn in PrivateRoute --- src/interfaces/overrides.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/overrides.tsx b/src/interfaces/overrides.tsx index f78826b..6fb8e0a 100644 --- a/src/interfaces/overrides.tsx +++ b/src/interfaces/overrides.tsx @@ -79,7 +79,7 @@ export const ProfileRoute = (rest: any, component: Component) => ( export const PrivateRoute = (props: IPrivateRouteProps) => { const { component, render, ...rest } = props; const redir = (comp: any) => - userLoggedIn ? comp : ; + userLoggedIn() ? comp : ; return ( Date: Sat, 25 Jan 2020 15:46:36 -0500 Subject: [PATCH 08/27] Bump package version to 1.1.0-beta4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e636a36..03fc26b 100644 --- a/package.json +++ b/package.json @@ -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 ", "repository": "https://github.com/hyperspacedev/hyperspace.git", From af5bd4a12a07fa8f542640d143764865dbdbe456 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Fri, 7 Feb 2020 10:09:47 -0500 Subject: [PATCH 09/27] Fix login issues due to null redirectAddress on reload --- src/interfaces/overrides.tsx | 2 +- src/pages/Welcome.tsx | 81 ++++++++++++++++++++---------------- src/utilities/login.tsx | 8 ++-- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/interfaces/overrides.tsx b/src/interfaces/overrides.tsx index f78826b..6fb8e0a 100644 --- a/src/interfaces/overrides.tsx +++ b/src/interfaces/overrides.tsx @@ -79,7 +79,7 @@ export const ProfileRoute = (rest: any, component: Component) => ( export const PrivateRoute = (props: IPrivateRouteProps) => { const { component, render, ...rest } = props; const redir = (comp: any) => - userLoggedIn ? comp : ; + userLoggedIn() ? comp : ; return ( { 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); + }); + }); } } } diff --git a/src/utilities/login.tsx b/src/utilities/login.tsx index 9b0a4bb..c881361 100644 --- a/src/utilities/login.tsx +++ b/src/utilities/login.tsx @@ -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; } } From 9a2f7f6ef44923c5e811dbff79b09d79f831b614 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Fri, 7 Feb 2020 12:44:01 -0500 Subject: [PATCH 10/27] Show a menu in mobile on notifications --- src/interfaces/utils.tsx | 3 + src/pages/Notifications.tsx | 187 ++++++++++++++++++++++++++---------- 2 files changed, 139 insertions(+), 51 deletions(-) create mode 100644 src/interfaces/utils.tsx diff --git a/src/interfaces/utils.tsx b/src/interfaces/utils.tsx new file mode 100644 index 0000000..04092ca --- /dev/null +++ b/src/interfaces/utils.tsx @@ -0,0 +1,3 @@ +export interface Dictionary { + [Key: string]: T; +} diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index d11feed..73919e2 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -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; } /** @@ -101,7 +114,8 @@ class NotificationsPage extends Component { // Initialize the state. this.state = { viewIsLoading: true, - deleteDialogOpen: false + deleteDialogOpen: false, + mobileMenuOpen: {} }; } @@ -114,10 +128,17 @@ class NotificationsPage extends Component { .get("/notifications") .then((resp: any) => { let notifications: [Notification] = resp.data; + let notifMenus: Dictionary = {}; + + 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 { 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 { } /> + {this.getActions(notif)} + + + ); + } + + /** + * 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 ( + <> + this.toggleMobileMenu(notif.id)} + className={classes.mobileOnly} + id={`notification-list-${notif.id}`} + > + + + this.toggleMobileMenu(notif.id)} + > + {notif.type == "follow" ? ( + <> + + View Profile + + this.followMember(notif.account)} + > + Follow + + + ) : null} + {notif.type == "mention" && notif.status ? ( + + Reply + + ) : null} + this.removeNotification(notif.id)}> + Remove + + +
{notif.type === "follow" ? ( @@ -363,54 +492,10 @@ class NotificationsPage extends Component { - - +
+ ); - } - - /** - * 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. From 163d7e693e68803a607cfb412ba1a82040b64472 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Fri, 7 Feb 2020 12:45:25 -0500 Subject: [PATCH 11/27] Add docs to new Dictionary type --- src/interfaces/utils.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/interfaces/utils.tsx b/src/interfaces/utils.tsx index 04092ca..28ce69e 100644 --- a/src/interfaces/utils.tsx +++ b/src/interfaces/utils.tsx @@ -1,3 +1,8 @@ +/** + * A Generic dictionary with the value of a specific type. + * + * Keys _must_ be strings. + */ export interface Dictionary { [Key: string]: T; } From f8ec25a0504c7223a024177c62844780b9bb5545 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Sun, 16 Feb 2020 01:39:18 -0500 Subject: [PATCH 12/27] fixes like/retoot bugs --- src/components/Post/Post.tsx | 111 ++++++++++++++++------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index ee471a8..c8c6ac2 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -60,25 +60,6 @@ import ShareMenu from "./PostShareMenu"; import { emojifyString } from "../../utilities/emojis"; import { PollOption } from "../../types/Poll"; -const log = (post: Status, msg = "") => { - let { - replies_count, - reblogs_count, - favourites_count, - favourited, - reblogged, - reblog - } = post; - console.log(msg, { - replies_count, - reblogs_count, - favourites_count, - favourited, - reblogged, - reblog - }); -}; - interface IPostProps { post: Status; classes: any; @@ -529,29 +510,54 @@ export class Post extends React.Component { } /** - * Toggle the status of a post's action and update the state - * @param type Either the "reblog" or "favorite" action - * @param post The post to toggle the status of + * Tell server a post has been un/favorited and update post state + * @param post The post to un/favorite */ - togglePostStatus(type: "reblog" | "favourite", post: Status) { - const shouldUndo = post.favourited || post.reblogged; - let requestBuilder = `/statuses/${post.id}/${ - shouldUndo ? "un" : "" - }${type}`; - this.client - .post(requestBuilder) - .then((resp: any) => { - this.setState({ post: resp.data as Status }); - }) - .catch((err: Error) => { - this.props.enqueueSnackbar( - `Couldn't ${shouldUndo ? "un" : ""}${type} post: ${ - err.name - }`, - { variant: "error" } - ); - console.error(err.message); - }); + async toggleFavorite(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); + } + } + + /** + * Tell server a post has been un/reblogged and update post state + * @param post The post to un/reblog + */ + async toggleReblog(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); + } } showDeleteDialog() { @@ -602,7 +608,6 @@ export class Post extends React.Component { elevation={this.props.threadHeader ? 0 : 1} > log(post)} avatar={ { - this.togglePostStatus("favourite", post) - } + onClick={() => this.toggleFavorite(post)} > - - {post.reblog - ? post.reblog.favourites_count - : post.favourites_count} - + {post.favourites_count} - - this.togglePostStatus("reblog", post) - } - > + this.toggleReblog(post)}> Date: Sun, 16 Feb 2020 11:13:57 -0500 Subject: [PATCH 13/27] fixes duplicate posts bug --- src/pages/Timeline.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/Timeline.tsx b/src/pages/Timeline.tsx index 03c809f..d0491e5 100644 --- a/src/pages/Timeline.tsx +++ b/src/pages/Timeline.tsx @@ -132,7 +132,7 @@ class TimelinePage extends Component { this.streamListener.on("connect", () => { // Get the latest posts from this timeline. this.client - .get(this.props.timeline, { limit: 40 }) + .get(this.props.timeline, { limit: 10 }) // If we succeeded, update the state and turn off loading. .then((resp: any) => { @@ -278,9 +278,7 @@ class TimelinePage extends Component { shouldLoadMorePosts(e: Event) { let difference = document.body.clientHeight - window.scrollY - window.innerHeight; - console.log(difference); - if (difference < 5000) { - console.log("load!"); + if (difference < 10000 && this.state.viewIsLoading === false) { this.loadMoreTimelinePieces(); } } @@ -340,9 +338,9 @@ class TimelinePage extends Component { return (
From 33d42991f3c59f2011351fcd81a93c917b32f311 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Sun, 16 Feb 2020 14:49:55 -0500 Subject: [PATCH 14/27] Comment out unnecessary console logs and update getConfig --- package-lock.json | 2 +- src/components/AppLayout/AppLayout.tsx | 2 +- src/components/Post/Post.tsx | 4 ++-- src/pages/Activity.tsx | 4 ++-- src/pages/Search.tsx | 2 +- src/pages/Settings.tsx | 2 +- src/pages/You.tsx | 2 +- src/utilities/settings.tsx | 9 +++++++++ 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fbea6f..7d9d9be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hyperspace", - "version": "1.1.0-beta2", + "version": "1.1.0-beta4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index bec8751..3f86ee5 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -324,7 +324,7 @@ export class AppLayout extends Component { */ 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; diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 99d8278..b2a0f36 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -119,7 +119,7 @@ export class Post extends React.Component { }) .catch((err: Error) => { this.props.enqueueSnackbar("Couldn't delete post: " + err.name); - console.log(err.message); + console.error(err.message); }); } @@ -405,7 +405,7 @@ export class Post extends React.Component { emojis.concat(reblogger.emojis); } - console.log(post); + // console.log(post); return ( <> diff --git a/src/pages/Activity.tsx b/src/pages/Activity.tsx index ad12078..46655dd 100644 --- a/src/pages/Activity.tsx +++ b/src/pages/Activity.tsx @@ -83,7 +83,7 @@ class ActivityPage extends Component { viewLoading: false, viewErrored: true }); - console.log(err.message); + console.error(err.message); }); this.client @@ -101,7 +101,7 @@ class ActivityPage extends Component { viewLoading: false, viewErrored: true }); - console.log(err.message); + console.error(err.message); }); } diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index 9e00c3e..4baee90 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -159,7 +159,7 @@ class SearchPage extends Component { viewDidLoad: true, viewIsLoading: false }); - console.log(this.state.tagResults); + // console.log(this.state.tagResults); }) .catch((err: Error) => { this.setState({ diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 0dc0efd..a09a7dc 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -170,7 +170,7 @@ class SettingsPage extends Component { getConfig().then((result: any) => { if (result !== undefined) { let config: Config = result; - console.log(!config.federation.allowPublicPosts); + // console.log(!config.federation.allowPublicPosts); this.setState({ federated: config.federation.allowPublicPosts }); diff --git a/src/pages/You.tsx b/src/pages/You.tsx index 62aea51..47fcc00 100644 --- a/src/pages/You.tsx +++ b/src/pages/You.tsx @@ -74,7 +74,7 @@ class You extends Component { getAccount() { let acct = localStorage.getItem("account"); - console.log(acct); + // console.log(acct); if (acct) { return JSON.parse(acct); } diff --git a/src/utilities/settings.tsx b/src/utilities/settings.tsx index b71d1a2..6ca554e 100644 --- a/src/utilities/settings.tsx +++ b/src/utilities/settings.tsx @@ -136,6 +136,15 @@ export function createUserDefaults() { export async function getConfig(): Promise { 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( From 2620bb828213ca39435c1ce8ac148c5ee4afadd2 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Sun, 16 Feb 2020 16:43:49 -0500 Subject: [PATCH 15/27] adds infinite scroll setting, prevents post rerendering on list updates, adds debounce to scroll event listener --- src/components/Post/Post.tsx | 5 ++++ src/pages/Settings.tsx | 28 ++++++++++++++++++++-- src/pages/Timeline.tsx | 45 +++++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 2c74fd0..72cc78f 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -101,6 +101,11 @@ export class Post extends React.Component { }); } + shouldComponentUpdate(nextProps: any, nextState: any) { + if (nextState == this.state) return false + return true + } + togglePostMenu() { this.setState({ menuIsOpen: !this.state.menuIsOpen }); } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 0dc0efd..b7070f1 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -65,6 +65,7 @@ import DomainDisabledIcon from "@material-ui/icons/DomainDisabled"; import AccountSettingsIcon from "mdi-material-ui/AccountSettings"; import AlphabeticalVariantOffIcon from "mdi-material-ui/AlphabeticalVariantOff"; import DashboardIcon from "@material-ui/icons/Dashboard"; +import InfiniteIcon from "@material-ui/icons/AllInclusive"; import { Config } from "../types/Config"; import { Account } from "../types/Account"; @@ -88,6 +89,7 @@ interface ISettingsState { currentUser?: Account; imposeCharacterLimit: boolean; masonryLayout?: boolean; + infiniteScroll?: boolean; } class SettingsPage extends Component { @@ -120,7 +122,8 @@ class SettingsPage extends Component { brandName: "Hyperspace", federated: true, imposeCharacterLimit: getUserDefaultBool("imposeCharacterLimit"), - masonryLayout: getUserDefaultBool("isMasonryLayout") + masonryLayout: getUserDefaultBool("isMasonryLayout"), + infiniteScroll: getUserDefaultBool("isInfiniteScroll") }; this.toggleDarkMode = this.toggleDarkMode.bind(this); @@ -130,6 +133,7 @@ class SettingsPage extends Component { this.toggleThemeDialog = this.toggleThemeDialog.bind(this); this.toggleVisibilityDialog = this.toggleVisibilityDialog.bind(this); this.toggleMasonryLayout = this.toggleMasonryLayout.bind(this); + this.toggleInfiniteScroll = this.toggleInfiniteScroll.bind(this); this.changeThemeName = this.changeThemeName.bind(this); this.changeTheme = this.changeTheme.bind(this); this.setVisibility = this.setVisibility.bind(this); @@ -170,7 +174,6 @@ class SettingsPage extends Component { getConfig().then((result: any) => { if (result !== undefined) { let config: Config = result; - console.log(!config.federation.allowPublicPosts); this.setState({ federated: config.federation.allowPublicPosts }); @@ -250,6 +253,11 @@ class SettingsPage extends Component { setUserDefaultBool("isMasonryLayout", !this.state.masonryLayout); } + toggleInfiniteScroll() { + this.setState({ infiniteScroll: !this.state.infiniteScroll }); + setUserDefaultBool("isInfiniteScroll", !this.state.infiniteScroll); + } + changeTheme() { setUserDefaultTheme(this.state.selectThemeName); window.location.reload(); @@ -675,6 +683,22 @@ class SettingsPage extends Component { /> + + + + + + + + + +
diff --git a/src/pages/Timeline.tsx b/src/pages/Timeline.tsx index d0491e5..65af4fa 100644 --- a/src/pages/Timeline.tsx +++ b/src/pages/Timeline.tsx @@ -77,6 +77,11 @@ interface ITimelinePageState { * the user settings. */ isMasonryLayout?: boolean; + + /** + * Whether posts should automatically load when scrolling. + */ + isInfiniteScroll?: boolean; } /** @@ -109,7 +114,8 @@ class TimelinePage extends Component { this.state = { viewIsLoading: true, backlogPosts: null, - isMasonryLayout: getUserDefaultBool("isMasonryLayout") + isMasonryLayout: getUserDefaultBool("isMasonryLayout"), + isInfiniteScroll: getUserDefaultBool("isInfiniteScroll"), }; // Generate the client. @@ -132,8 +138,7 @@ class TimelinePage extends Component { this.streamListener.on("connect", () => { // Get the latest posts from this timeline. this.client - .get(this.props.timeline, { limit: 10 }) - + .get(this.props.timeline, { limit: 50 }) // If we succeeded, update the state and turn off loading. .then((resp: any) => { let statuses: [Status] = resp.data; @@ -200,11 +205,34 @@ class TimelinePage extends Component { this.streamListener.on("heartbeat", () => {}); } + /** + * Insert a delay between repeated function calls + * codeburst.io/throttling-and-debouncing-in-javascript-646d076d0a44 + * @param delay How long to wait before calling function (ms) + * @param fn The function to call + */ + debounced(delay: number, fn: Function) { + let lastCall = 0 + return function(...args: any) { + const now = (new Date).getTime(); + if (now - lastCall < delay) { + return + } + lastCall = now; + return fn(...args) + } + } + /** * Listen for when scroll position changes */ componentDidMount() { - window.addEventListener("scroll", this.shouldLoadMorePosts); + if (this.state.isInfiniteScroll) { + window.addEventListener( + "scroll", + this.debounced(200, this.shouldLoadMorePosts), + ); + } } /** @@ -212,7 +240,12 @@ class TimelinePage extends Component { */ componentWillUnmount() { this.streamListener.stop(); - window.removeEventListener("scroll", this.shouldLoadMorePosts); + if (this.state.isInfiniteScroll) { + window.removeEventListener( + "scroll", + this.shouldLoadMorePosts, + ); + } } /** @@ -241,7 +274,7 @@ class TimelinePage extends Component { this.client .get(this.props.timeline, { max_id: this.state.posts[this.state.posts.length - 1].id, - limit: 20 + limit: 50 }) // If we succeeded, append them to the end of the list of posts. From 166736e0bc510b682768ec44de4cd75844051143 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Sun, 16 Feb 2020 16:56:29 -0500 Subject: [PATCH 16/27] forgot to prettify Post --- src/components/Post/Post.tsx | 406 +++++++++++++++++------------------ 1 file changed, 198 insertions(+), 208 deletions(-) diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 72cc78f..9e60d31 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -102,8 +102,8 @@ export class Post extends React.Component { } shouldComponentUpdate(nextProps: any, nextState: any) { - if (nextState == this.state) return false - return true + if (nextState == this.state) return false; + return true; } togglePostMenu() { @@ -663,230 +663,220 @@ export class Post extends React.Component { const { classes } = this.props; const post = this.state.post; return ( - - - } - action={ - - this.togglePostMenu()} - > - - - - } - title={ - - {this.getReblogAuthors(post)} - - } - subheader={moment(post.created_at).format( - "MMMM Do YYYY [at] h:mm A" - )} - /> - {post.reblog ? this.getReblogOfPost(post.reblog) : null} - {post.sensitive - ? this.getSensitiveContent(post.spoiler_text, post) - : post.reblog - ? null - : this.materializeContent(post)} - {post.reblog && post.reblog.mentions.length > 0 - ? this.getMentions(post.reblog.mentions) - : this.getMentions(post.mentions)} - {post.reblog && post.reblog.tags.length > 0 - ? this.getTags(post.reblog.tags) - : this.getTags(post.tags)} - - - - - - - - {post.reblog - ? post.reblog.replies_count - : post.replies_count} - - + + + } + action={ + this.toggleFavorited(post)} + key={`${post.id}_submenu`} + id={`${post.id}_submenu`} + onClick={() => this.togglePostMenu()} > - + - - {post.reblog - ? post.reblog.favourites_count - : post.favourites_count} - - - this.toggleReblogged(post)} - > - {this.getReblogAuthors(post)} + } + subheader={moment(post.created_at).format( + "MMMM Do YYYY [at] h:mm A" + )} + /> + {post.reblog ? this.getReblogOfPost(post.reblog) : null} + {post.sensitive + ? this.getSensitiveContent(post.spoiler_text, post) + : post.reblog + ? null + : this.materializeContent(post)} + {post.reblog && post.reblog.mentions.length > 0 + ? this.getMentions(post.reblog.mentions) + : this.getMentions(post.mentions)} + {post.reblog && post.reblog.tags.length > 0 + ? this.getTags(post.reblog.tags) + : this.getTags(post.tags)} + + + + + + + + {post.reblog + ? post.reblog.replies_count + : post.replies_count} + + + this.toggleFavorited(post)}> + - - - - {post.reblog - ? post.reblog.reblogs_count - : post.reblogs_count} - - - - - - - + + + + {post.reblog + ? post.reblog.favourites_count + : post.favourites_count} + + + this.toggleReblogged(post)}> + + + + + {post.reblog + ? post.reblog.reblogs_count + : post.reblogs_count} + + + - - - - -
-
- {this.showVisibilityIcon(post.visibility)} -
- - this.togglePostMenu()} + + + + - - this.props.enqueueSnackbar("Post shared!", { - variant: "success" - }), - onShareError: (error: Error) => { - if (error.name != "AbortError") - this.props.enqueueSnackbar( - `Couldn't share post: ${error.name}`, - { variant: "error" } - ); - } - }} - /> - {post.reblog ? ( -
- - View author profile - - - View reblogger profile - -
- ) : ( + + + +
+
+
+ {this.showVisibilityIcon(post.visibility)} +
+ + this.togglePostMenu()} + > + + this.props.enqueueSnackbar("Post shared!", { + variant: "success" + }), + onShareError: (error: Error) => { + if (error.name != "AbortError") + this.props.enqueueSnackbar( + `Couldn't share post: ${error.name}`, + { variant: "error" } + ); + } + }} + /> + {post.reblog ? ( +
- View profile + View author profile - )} -
- - View thread + View reblogger profile +
+ ) : ( + + View profile + + )} +
+ + + View thread + + + Open in Web + +
+ {this.state.myAccount && + post.account.id === this.state.myAccount ? ( +
+ this.togglePostDeleteDialog()} > - Open in Web + Delete
- {this.state.myAccount && - post.account.id === this.state.myAccount ? ( -
- - - this.togglePostDeleteDialog() - } - > - Delete - -
- ) : null} - {this.showDeleteDialog()} -
- + ) : null} + {this.showDeleteDialog()} +
+ ); } } From 5f50a6d6dc6d663b502f2dab9201825bb1616331 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Mon, 17 Feb 2020 08:14:40 -0500 Subject: [PATCH 17/27] Empty commit for PR re-check Signed-off-by: Marquis Kurt From 3200086a6de6ad1fc64364ca3ce01716c80096f7 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Mon, 17 Feb 2020 12:54:04 -0500 Subject: [PATCH 18/27] prettifies Timeline.tsx --- src/pages/Timeline.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/pages/Timeline.tsx b/src/pages/Timeline.tsx index 65af4fa..6317be7 100644 --- a/src/pages/Timeline.tsx +++ b/src/pages/Timeline.tsx @@ -115,7 +115,7 @@ class TimelinePage extends Component { viewIsLoading: true, backlogPosts: null, isMasonryLayout: getUserDefaultBool("isMasonryLayout"), - isInfiniteScroll: getUserDefaultBool("isInfiniteScroll"), + isInfiniteScroll: getUserDefaultBool("isInfiniteScroll") }; // Generate the client. @@ -212,15 +212,15 @@ class TimelinePage extends Component { * @param fn The function to call */ debounced(delay: number, fn: Function) { - let lastCall = 0 + let lastCall = 0; return function(...args: any) { - const now = (new Date).getTime(); + const now = new Date().getTime(); if (now - lastCall < delay) { - return + return; } lastCall = now; - return fn(...args) - } + return fn(...args); + }; } /** @@ -229,8 +229,8 @@ class TimelinePage extends Component { componentDidMount() { if (this.state.isInfiniteScroll) { window.addEventListener( - "scroll", - this.debounced(200, this.shouldLoadMorePosts), + "scroll", + this.debounced(200, this.shouldLoadMorePosts) ); } } @@ -241,10 +241,7 @@ class TimelinePage extends Component { componentWillUnmount() { this.streamListener.stop(); if (this.state.isInfiniteScroll) { - window.removeEventListener( - "scroll", - this.shouldLoadMorePosts, - ); + window.removeEventListener("scroll", this.shouldLoadMorePosts); } } From 8459102c741a0f00f5e8c0a64ad8b5af05172a3f Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Mon, 17 Feb 2020 14:53:35 -0500 Subject: [PATCH 19/27] removes follow account button on click --- src/pages/Notifications.tsx | 75 ++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index 73919e2..0cc9111 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -53,6 +53,11 @@ interface INotificationsPageState { */ notifications?: [Notification]; + /** + * The ids of accounts you follow associated with a notification + */ + followingAccounts?: string[]; + /** * Whether the view is still loading. */ @@ -122,7 +127,7 @@ class NotificationsPage extends Component { /** * Perform pre-mount tasks. */ - componentWillMount() { + /*componentWillMount() { // Get the list of notifications and update the state. this.client .get("/notifications") @@ -149,6 +154,44 @@ class NotificationsPage extends Component { viewDidErrorCode: err.message }); }); + }*/ + + async componentWillMount() { + try { + let resp: any = await this.client.get("/notifications"); + let notifications: [Notification] = resp.data; + let notifMenus: Dictionary = {}; + notifications.forEach( + (n: Notification) => (notifMenus[n.id] = false) + ); + resp = await this.client.get( + `/accounts/${sessionStorage.getItem("id")}/following` + ); + let followingAcctIds: string[] = resp.data.map( + (acct: Account) => acct.id + ); + let notifAcctIds: string[] = notifications.map( + (n: Notification) => n.account.id + ); + let followingNotifAcctIds = followingAcctIds.filter((id: string) => + notifAcctIds.includes(id) + ); + console.log(followingNotifAcctIds); + this.setState({ + notifications, + followingAccounts: followingNotifAcctIds, + viewIsLoading: false, + viewDidLoad: true, + mobileMenuOpen: notifMenus + }); + } catch (e) { + this.setState({ + viewDidLoad: true, + viewIsLoading: false, + viewDidError: true, + viewDidErrorCode: e.message + }); + } } /** @@ -359,6 +402,13 @@ class NotificationsPage extends Component { this.props.enqueueSnackbar( "You are now following this account." ); + let followingAccounts: string[] + if (this.state.followingAccounts) { + followingAccounts = this.state.followingAccounts.concat(acct.id) + } else { + followingAccounts = [acct.id] + } + this.setState({followingAccounts}) }) .catch((err: Error) => { this.props.enqueueSnackbar( @@ -444,15 +494,20 @@ class NotificationsPage extends Component { - - - this.followMember(notif.account) - } - > - - - + {this.state.followingAccounts && + !this.state.followingAccounts.includes( + notif.account.id + ) ? ( + + + this.followMember(notif.account) + } + > + + + + ) : null} ) : notif.status ? ( From 3542a88d98d5c68f6c5131b84ddd38c5a3bc5039 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Mon, 17 Feb 2020 16:50:30 -0500 Subject: [PATCH 20/27] replaces following fetch with relationships fetch, enables unfollowing --- src/pages/Notifications.tsx | 181 ++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 101 deletions(-) diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index 0cc9111..96d87f8 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -25,6 +25,7 @@ import { import AssignmentIndIcon from "@material-ui/icons/AssignmentInd"; import PersonIcon from "@material-ui/icons/Person"; import PersonAddIcon from "@material-ui/icons/PersonAdd"; +import PersonRemoveIcon from "mdi-material-ui/AccountMinus"; import DeleteIcon from "@material-ui/icons/Delete"; import { styles } from "./PageLayout.styles"; import { @@ -54,9 +55,9 @@ interface INotificationsPageState { notifications?: [Notification]; /** - * The ids of accounts you follow associated with a notification + * The relationships with all notification accounts */ - followingAccounts?: string[]; + relationships: { [id: string]: Relationship }; /** * Whether the view is still loading. @@ -120,66 +121,46 @@ class NotificationsPage extends Component { this.state = { viewIsLoading: true, deleteDialogOpen: false, - mobileMenuOpen: {} + mobileMenuOpen: {}, + relationships: {} }; } /** - * Perform pre-mount tasks. + * Perform pre-mount tasks */ - /*componentWillMount() { - // Get the list of notifications and update the state. - this.client - .get("/notifications") - .then((resp: any) => { - let notifications: [Notification] = resp.data; - let notifMenus: Dictionary = {}; - - notifications.forEach((notif: Notification) => { - notifMenus[notif.id] = false; - }); - - this.setState({ - notifications, - viewIsLoading: false, - viewDidLoad: true, - mobileMenuOpen: notifMenus - }); - }) - .catch((err: Error) => { - this.setState({ - viewDidLoad: true, - viewIsLoading: false, - viewDidError: true, - viewDidErrorCode: err.message - }); - }); - }*/ - async componentWillMount() { try { + // Get the list of notifications let resp: any = await this.client.get("/notifications"); let notifications: [Notification] = resp.data; + + // initialize all menus as closed let notifMenus: Dictionary = {}; notifications.forEach( (n: Notification) => (notifMenus[n.id] = false) ); - resp = await this.client.get( - `/accounts/${sessionStorage.getItem("id")}/following` - ); - let followingAcctIds: string[] = resp.data.map( - (acct: Account) => acct.id - ); - let notifAcctIds: string[] = notifications.map( - (n: Notification) => n.account.id - ); - let followingNotifAcctIds = followingAcctIds.filter((id: string) => - notifAcctIds.includes(id) - ); - console.log(followingNotifAcctIds); + + // compile list of all notification account ids + let accountIds: string[] = []; + notifications.forEach(notif => { + if (!accountIds.includes(notif.account.id)) { + accountIds.push(notif.account.id); + } + }); + + // store relationships in id-relationship pairs + resp = await this.client.get(`/accounts/relationships`, { + id: accountIds + }); + let relationships: Dictionary = {}; + resp.data.forEach((relation: Relationship) => { + relationships[relation.id] = relation; + }); + this.setState({ notifications, - followingAccounts: followingNotifAcctIds, + relationships, viewIsLoading: false, viewDidLoad: true, mobileMenuOpen: notifMenus @@ -383,54 +364,44 @@ class NotificationsPage extends Component { } /** - * Follow an account from a notification if already not followed. - * @param acct The account to follow, if possible + * Un/follow an account and update relationships state. + * @param acct The account to un/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." - ); - let followingAccounts: string[] - if (this.state.followingAccounts) { - followingAccounts = this.state.followingAccounts.concat(acct.id) - } else { - followingAccounts = [acct.id] - } - this.setState({followingAccounts}) - }) - .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" - }); - }); + async toggleFollow(acct: Account) { + let relationships = this.state.relationships; + if (!relationships[acct.id].following) { + try { + let resp: any = await this.client.post( + `/accounts/${acct.id}/follow` + ); + relationships[acct.id] = resp.data; + this.setState({ relationships }); + this.props.enqueueSnackbar( + "You are now following this account." + ); + } catch (e) { + this.props.enqueueSnackbar( + "Couldn't follow acccount: " + e.name + ); + console.error(e.message); + } + } else { + try { + let resp: any = await this.client.post( + `/accounts/${acct.id}/unfollow` + ); + relationships[acct.id] = resp.data; + this.setState({ relationships }); + this.props.enqueueSnackbar( + "You are no longer following this account." + ); + } catch (e) { + this.props.enqueueSnackbar( + "Couldn't unfollow acccount: " + e.name + ); + console.error(e.message); + } + } } getActions = (notif: Notification) => { @@ -459,7 +430,7 @@ class NotificationsPage extends Component { View Profile this.followMember(notif.account)} + onClick={() => this.toggleFollow(notif.account)} > Follow @@ -494,20 +465,28 @@ class NotificationsPage extends Component { - {this.state.followingAccounts && - !this.state.followingAccounts.includes( - notif.account.id - ) ? ( + {!this.state.relationships[notif.account.id] + .following ? ( - this.followMember(notif.account) + this.toggleFollow(notif.account) } > - ) : null} + ) : ( + + + this.toggleFollow(notif.account) + } + > + + + + )} ) : notif.status ? ( From d4663e9bf186891ab6e69a91b34b74dd05ee96e5 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Mon, 17 Feb 2020 16:53:10 -0500 Subject: [PATCH 21/27] updates menu label based on un/following --- src/pages/Notifications.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index 96d87f8..2c52ec7 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -432,7 +432,10 @@ class NotificationsPage extends Component { this.toggleFollow(notif.account)} > - Follow + {this.state.relationships[notif.account.id] + .following + ? "Unfollow" + : "Follow"} ) : null} From 37c01fc1507537603fd0b07194d37979848f14db Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Tue, 18 Feb 2020 17:15:47 -0500 Subject: [PATCH 22/27] prevents line breaks for author names, reblog-icon-reblogger --- src/components/Post/Post.styles.tsx | 3 +++ src/components/Post/Post.tsx | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index 1fa5377..cc38a3a 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -6,6 +6,9 @@ export const styles = (theme: Theme) => marginTop: theme.spacing.unit, marginBottom: theme.spacing.unit }, + postAuthorName: { + whiteSpace: 'nowrap', + }, postReblogChip: { color: theme.palette.common.white, "&:hover": { diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index ac49442..e8c2d73 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -410,11 +410,10 @@ export class Post extends React.Component { emojis.concat(reblogger.emojis); } - // console.log(post); - return ( <> { }} > {reblogger ? ( - <> +
{ ) }} > - +
) : null} ); From a168b614b295062898d6eb499e9b160154f46cb6 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Tue, 18 Feb 2020 17:23:42 -0500 Subject: [PATCH 23/27] hides account name with ellipsis instead of line break, fixes colors --- src/components/Post/Post.styles.tsx | 18 ++++++++++++++---- src/components/Post/Post.tsx | 6 +++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index cc38a3a..cf0c1b1 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -6,8 +6,21 @@ export const styles = (theme: Theme) => marginTop: theme.spacing.unit, marginBottom: theme.spacing.unit }, + postHeaderContent: { + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + postHeaderTitle: { + overflow: 'hidden', + textOverflow: 'ellipsis', + color: theme.palette.text.secondary, + }, postAuthorName: { whiteSpace: 'nowrap', + color: theme.palette.text.primary, + }, + postAuthorAccount: { + marginLeft: theme.spacing.unit * 0.5, }, postReblogChip: { color: theme.palette.common.white, @@ -84,14 +97,11 @@ 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, + color: theme.palette.text.primary, }, postAuthorEmoji: { height: theme.typography.fontSize, diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index e8c2d73..0574d3f 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -647,6 +647,10 @@ export class Post extends React.Component { elevation={this.props.threadHeader ? 0 : 1} > { } title={ - {this.getReblogAuthors(post)} + this.getReblogAuthors(post) } subheader={moment(post.created_at).format( "MMMM Do YYYY [at] h:mm A" From 78d7b02085869bc4e37030b7bf462ff197ed14ef Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Tue, 18 Feb 2020 17:34:20 -0500 Subject: [PATCH 24/27] prettifies files, I need to format on save... --- src/components/Post/Post.styles.tsx | 18 +++++++++--------- src/components/Post/Post.tsx | 6 ++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index cf0c1b1..5d32b88 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -7,20 +7,20 @@ export const styles = (theme: Theme) => marginBottom: theme.spacing.unit }, postHeaderContent: { - overflow: 'hidden', - whiteSpace: 'nowrap', + overflow: "hidden", + whiteSpace: "nowrap" }, postHeaderTitle: { - overflow: 'hidden', - textOverflow: 'ellipsis', - color: theme.palette.text.secondary, + overflow: "hidden", + textOverflow: "ellipsis", + color: theme.palette.text.secondary }, postAuthorName: { - whiteSpace: 'nowrap', - color: theme.palette.text.primary, + whiteSpace: "nowrap", + color: theme.palette.text.primary }, postAuthorAccount: { - marginLeft: theme.spacing.unit * 0.5, + marginLeft: theme.spacing.unit * 0.5 }, postReblogChip: { color: theme.palette.common.white, @@ -101,7 +101,7 @@ export const styles = (theme: Theme) => marginBottom: theme.spacing.unit * -0.5, marginLeft: theme.spacing.unit * 0.5, marginRight: theme.spacing.unit * 0.5, - color: theme.palette.text.primary, + color: theme.palette.text.primary }, postAuthorEmoji: { height: theme.typography.fontSize, diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 0574d3f..3f657ab 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -649,7 +649,7 @@ export class Post extends React.Component { { } - title={ - this.getReblogAuthors(post) - } + title={this.getReblogAuthors(post)} subheader={moment(post.created_at).format( "MMMM Do YYYY [at] h:mm A" )} From f908e8656a6a1a3582ac1afe8f13a40799d5c496 Mon Sep 17 00:00:00 2001 From: Travis Kohlbeck Date: Wed, 19 Feb 2020 09:10:09 -0500 Subject: [PATCH 25/27] keeps reblogger on same line as author when there is enough room --- src/components/Post/Post.styles.tsx | 10 ++++++-- src/components/Post/Post.tsx | 40 +++++++++++++++-------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index 5d32b88..309a7d1 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -11,15 +11,21 @@ export const styles = (theme: Theme) => whiteSpace: "nowrap" }, postHeaderTitle: { - overflow: "hidden", - textOverflow: "ellipsis", + display: 'flex', + flexWrap: 'wrap', color: theme.palette.text.secondary }, + postAuthorNameAndAccount: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, postAuthorName: { whiteSpace: "nowrap", color: theme.palette.text.primary }, postAuthorAccount: { + overflow: 'hidden', + textOverflow: 'ellipsis', marginLeft: theme.spacing.unit * 0.5 }, postReblogChip: { diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 3f657ab..56cae52 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -412,28 +412,30 @@ export class Post extends React.Component { return ( <> - - + + }} + > + +
{reblogger ? (
Date: Wed, 19 Feb 2020 09:11:19 -0500 Subject: [PATCH 26/27] prettifies post styles --- src/components/Post/Post.styles.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index 309a7d1..328c15e 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -11,21 +11,21 @@ export const styles = (theme: Theme) => whiteSpace: "nowrap" }, postHeaderTitle: { - display: 'flex', - flexWrap: 'wrap', + display: "flex", + flexWrap: "wrap", color: theme.palette.text.secondary }, postAuthorNameAndAccount: { - overflow: 'hidden', - textOverflow: 'ellipsis', + overflow: "hidden", + textOverflow: "ellipsis" }, postAuthorName: { whiteSpace: "nowrap", color: theme.palette.text.primary }, postAuthorAccount: { - overflow: 'hidden', - textOverflow: 'ellipsis', + overflow: "hidden", + textOverflow: "ellipsis", marginLeft: theme.spacing.unit * 0.5 }, postReblogChip: { From afd3a7f31cd9c7e8185be7c0daebff76895b4719 Mon Sep 17 00:00:00 2001 From: Marquis Kurt Date: Tue, 25 Feb 2020 17:17:04 -0500 Subject: [PATCH 27/27] Update getConfig to change location field if running in dev mode --- src/utilities/settings.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utilities/settings.tsx b/src/utilities/settings.tsx index 6ca554e..5dfc4a2 100644 --- a/src/utilities/settings.tsx +++ b/src/utilities/settings.tsx @@ -140,11 +140,17 @@ export async function getConfig(): Promise { let { location } = resp.data; if (!location.endsWith("/")) { - console.warn( + console.info( "Location does not have a backslash, so Hyperspace has added it automatically." ); resp.data.location = location + "/"; } + + if (process.env.NODE_ENV === "development") { + resp.data.location = "http://localhost:3000/"; + console.info("Location field has been updated to localhost:3000."); + } + return resp.data as Config; } catch (err) { console.error(