Merge pull request #83 from hyperspacedev/beta6

1.0.0beta6 (final check)
This commit is contained in:
Marquis Kurt 2019-09-16 14:13:25 -04:00 committed by GitHub
commit 410fe9be54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 3301 additions and 814 deletions

View File

@ -37,7 +37,7 @@ npm install
### Testing changes
Before testing Hyperspace, make the following change in `config.json`, located in the public directory:
Before testing Hyperspace, make the following change in `config.json`, located in the `public` directory:
```json
"location": "https://localhost:3000"
@ -55,7 +55,7 @@ npm start
The site will be hosted at `https://localhost:3000`, where you can sign in and test Hyperspace using your Mastodon account. If you have signed in before, you will be automatically logged in.
Alternatively, if you are testing the desktop version of Hyperspace, run `npm run electrify` (or `npm run electrify`, if you don't want to make another production build). Hyperspace will open in a window where you can sign in and test Hyperspace with your Mastodon account. You'll be logged in automatically if you've signed in before.
Alternatively, if you are testing the desktop version of Hyperspace, run `npm run electrify` (or `npm run electrify-nobuild`, if you don't want to make another production build). Hyperspace will open in a window where you can sign in and test Hyperspace with your Mastodon account. You'll be logged in automatically if you've signed in before.
### Building a release
@ -81,6 +81,10 @@ You can run any of the following commands to build a release for the desktop:
>
> While the command will run without needing the signature, it is recommended that you make a signed copy to protect users.
> ⚠️ **Notarization**: If you are building the macOS version of Hyperspace, you will also need to set up notarization processes. Hyperspace will _not_ run on devices running macOS Catalina or later without this notarization; please ensure you have the correct certificates and updated notarization scripts in `desktop/notarize.js`.
>
> When building, the script will aotumatically notarize the app for you after signing it.
The built files will be available under `dist` that can be uploaded to your app distributor or website.
## Contribute
@ -89,4 +93,4 @@ Contrubition guidelines are available in the [contributing file](.github/contrib
If you want to aid the project in other ways, consider supporting the project on [Patreon](https://patreon.com/marquiskurt). You can also [view all of our contributors](patreon.md) that help make Hyperspace possible.
If you have Matrix, you can join the Hyperspace community ([+hyperspace-masto:matrix.org](https://matrix.to/#/+hyperspace-masto:matrix.org)).
If you have Matrix, you can join the Hyperspace community ([+hyperspace-masto:matrix.org](https://matrix.to/#/+hyperspace-masto:matrix.org)).

View File

@ -22,6 +22,9 @@ jobs:
pool:
vmImage: 'macOS-latest'
steps:
- task: CmdLine@2
inputs:
script: 'security add-generic-password -a "appleseed@marquiskurt.net" -w "$(ascPassword)" -s "AC_PASSWORD"'
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'Certificates.p12'
@ -32,7 +35,7 @@ jobs:
- template: .azure/build-web.yml
- template: .azure/mac-files.yml
- script: |
npm run build-desktop-darwin
npm run build-desktop-darwin-nosign
displayName: 'Build desktop client'
displayName: 'Build macOS client'

Binary file not shown.

3
desktop/donothing.js Normal file
View File

@ -0,0 +1,3 @@
exports.default = async function doNothing() {
return await 0;
}

29
desktop/notarize.js Normal file
View File

@ -0,0 +1,29 @@
// notarize.js
// Script to notarize Hyperspace for macOS
// © 2019 Hyperspace developers. Licensed under Apache 2.0.
const { notarize } = require('electron-notarize');
// This is pulled from the Apple Keychain. To set this up,
// follow the instructions provided here:
// https://github.com/electron/electron-notarize#safety-when-using-appleidpassword
const password = `@keychain:AC_PASSWORD`;
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
console.log("Notarizing Hyperspace...");
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: 'net.marquiskurt.hyperspace',
appPath: `${appOutDir}/${appName}.app`,
appleId: "appleseed@marquiskurt.net",
appleIdPassword: password,
ascProvider: "FQQXSP79X3"
});
};

3768
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +1,45 @@
{
"name": "hyperspace",
"productName": "Hyperspace",
"version": "1.0.0-beta5",
"version": "1.0.0-beta6",
"description": "A beautiful, fluffy client for the fediverse",
"author": "Marquis Kurt <hyperspacedev@marquiskurt.net>",
"repository": "https://github.com/hyperspacedev/hyperspace.git",
"private": true,
"homepage": "./",
"devDependencies": {
"@date-io/moment": "^1.3.5",
"@date-io/moment": "^1.3.8",
"@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.2",
"@types/emoji-mart": "^2.8.2",
"@types/jest": "^24.0.11",
"@types/jest": "^24.0.15",
"@types/node": "11.11.6",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/react-router-dom": "^4.3.2",
"@types/react-router-dom": "^4.3.4",
"@types/react-swipeable-views": "latest",
"axios": "^0.18.0",
"emoji-mart": "^2.8.2",
"axios": "^0.19.0",
"electron": "^5.0.8",
"electron-builder": "^21.1.5",
"emoji-mart": "^2.11.1",
"file-dialog": "^0.0.7",
"material-ui-pickers": "^2.2.4",
"mdi-material-ui": "^5.11.0",
"megalodon": "^0.6.3",
"mdi-material-ui": "^5.13.0",
"megalodon": "^0.6.4",
"moment": "^2.24.0",
"notistack": "^0.5.1",
"query-string": "^6.4.2",
"query-string": "^6.8.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"react-router-dom": "^5.0.1",
"react-scripts": "^2.1.8",
"react-swipeable-views": "^0.13.1",
"react-swipeable-views": "^0.13.3",
"react-web-share-api": "^0.0.2",
"typescript": "3.4.1",
"electron": "^5.0.0",
"electron-builder": "^20.39.0"
"typescript": "3.4.1"
},
"dependencies": {
"electron-updater": "^4.0.6",
"electron-notarize": "^0.1.1",
"electron-updater": "^4.1.2",
"electron-window-state": "^5.0.3"
},
"main": "public/electron.js",
@ -48,11 +49,12 @@
"electrify-nobuild": "electron .",
"build": "react-scripts build",
"create-mac-icon": "cd desktop; iconutil -c icns app.iconset; cd ..",
"build-desktop": "npm run build; npm run create-mac-icon; build -mwl deb AppImage snap",
"build-desktop-win": "build -w",
"build-desktop-darwin": "npm run create-mac-icon; build -m",
"build-desktop-linux": "build -l deb AppImage snap",
"build-desktop-linux-select": "build -l ",
"build-desktop": "npm run build; npm run create-mac-icon; electron-builder -p 'never' -mwl deb AppImage snap",
"build-desktop-win": "electron-builder -p 'never' -w",
"build-desktop-darwin": "npm run create-mac-icon; electron-builder -p 'never' -m",
"build-desktop-darwin-nosign": "npm run create-mac-icon; electron-builder -p 'never' -m dmg -c.mac.identity=null -c.afterSign=\"desktop/donothing.js\"",
"build-desktop-linux": "electron-builder -p 'never' -l deb AppImage snap",
"build-desktop-linux-select": "electron-builder -p 'never' -l ",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
@ -67,6 +69,7 @@
],
"build": {
"appId": "net.marquiskurt.hyperspace",
"afterSign": "desktop/notarize.js",
"directories": {
"buildResources": "desktop"
},
@ -77,12 +80,16 @@
"dmg",
"mas"
],
"darkModeSupport": true
"darkModeSupport": true,
"hardenedRuntime": true
},
"mas": {
"entitlements": "desktop/entitlements.mas.plist",
"provisioningProfile": "desktop/embedded.provisionprofile"
},
"dmg": {
"sign": false
},
"win": {
"target": [
"nsis"

View File

@ -1,5 +1,5 @@
{
"version": "1.0.0beta5",
"version": "1.0.0beta6",
"location": "https://hyperspaceapp-next.herokuapp.com",
"branding": {
"name": "Hyperspace",

View File

@ -6,6 +6,7 @@ const { app, Menu, protocol, BrowserWindow, shell, systemPreferences } = require
const windowStateKeeper = require('electron-window-state');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const os = require('os');
// Check for any updates to the app
autoUpdater.checkForUpdatesAndNotify();
@ -23,11 +24,16 @@ protocol.registerSchemesAsPrivileged([
/**
* Determine whether the desktop app is on macOS
* - Returns: Boolean of whether platform is Darwin
*/
function darwin() {
return process.platform === "darwin";
}
function catalina() {
return os.release() >= "19.0.0";
}
/**
* Register the protocol for Hyperspace
*/
@ -133,7 +139,7 @@ function createWindow() {
// Set some preferences that are specific to macOS.
titleBarStyle: 'hidden',
vibrancy: systemPreferences.isDarkMode()? "ultra-dark": "light",
vibrancy: catalina()? "sidebar": systemPreferences.isDarkMode()? "ultra-dark": "light",
transparent: darwin(),
backgroundColor: darwin()? "#80FFFFFF": "#FFF"
}
@ -144,14 +150,18 @@ function createWindow() {
// Load the main app and open the index page.
mainWindow.loadURL("hyperspace://hyperspace/app/");
// Watch for a change in macOS's dark mode and reload the window to apply changes
systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
if (darwin()) {
systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
if (mainWindow != null) {
mainWindow.setVibrancy(systemPreferences.isDarkMode()? "ultra-dark": "light");
if (!catalina()) {
mainWindow.setVibrancy(systemPreferences.isDarkMode()? "ultra-dark": "light");
}
mainWindow.webContents.reload();
}
})
}
// Delete the window when closed
mainWindow.on('closed', () => {

View File

@ -102,6 +102,7 @@ class AboutPage extends Component<any, IAboutPageState> {
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
<Paper>
@ -117,7 +118,7 @@ class AboutPage extends Component<any, IAboutPageState> {
<Typography className={classes.instanceHeaderText} variant="h4" component="p">{this.state.instance ? this.state.instance.uri: "Loading..."}</Typography>
</div>
<List className={classes.pageListConstraints}>
<ListItem>
{(localStorage['isPleroma'] == "false") && <ListItem>
<ListItemAvatar>
<LinkableAvatar to={`/profile/${this.state.instance? this.state.instance.contact_account.id: 0}`} alt="Instance admin" src={this.state.instance? this.state.instance.contact_account.avatar_static: ""}/>
</ListItemAvatar>
@ -137,7 +138,7 @@ class AboutPage extends Component<any, IAboutPageState> {
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
</ListItem>}
<ListItem>
<ListItemAvatar>
<Avatar>

View File

@ -1,5 +1,6 @@
import { Theme, createStyles } from "@material-ui/core";
import { isDarwinApp } from "../utilities/desktop";
import { isAppbarExpanded } from "../utilities/appbar";
export const styles = (theme: Theme) => createStyles({
root: {
@ -69,7 +70,8 @@ export const styles = (theme: Theme) => createStyles({
backgroundColor: theme.palette.primary.dark,
width: '100%',
color: theme.palette.common.white,
zIndex: 1
zIndex: 1,
top: isAppbarExpanded()? 80: 64,
},
pageHeroBackgroundImage: {
position: 'absolute',
@ -80,18 +82,19 @@ export const styles = (theme: Theme) => createStyles({
backgroundSize: 'cover',
height: '100%',
width: '100%',
opacity: 0.40,
zIndex: -1
opacity: 0.35,
zIndex: -1,
filter: 'blur(2px)'
},
pageHeroContent: {
padding: 16,
paddingTop: 116,
paddingTop: 8,
textAlign: 'center',
width: '100%',
height: '100%',
[theme.breakpoints.up('md')]: {
paddingLeft: '25%',
paddingRight: '25%',
paddingLeft: '5%',
paddingRight: '5%',
},
position: "relative",
zIndex: 1
@ -110,6 +113,38 @@ export const styles = (theme: Theme) => createStyles({
},
//backgroundColor: theme.palette.background.default
},
profileToolbar: {
zIndex: 2,
paddingTop: 8,
},
profileContent: {
padding: 16,
[theme.breakpoints.up('md')]: {
paddingLeft: '5%',
paddingRight: '5%',
paddingBottom: 48,
paddingTop: 24,
},
width: '100%',
height: '100%',
position: 'relative',
zIndex: 1,
display: 'flex',
paddingBottom: 24,
paddingTop: 24,
},
profileAvatar: {
width: 64,
height: 64,
[theme.breakpoints.up('md')]: {
width: 128,
height: 128,
},
backgroundColor: theme.palette.primary.main
},
profileUserBox: {
paddingLeft: theme.spacing.unit * 2
},
pageProfileAvatar: {
width: 128,
height: 128,
@ -120,6 +155,7 @@ export const styles = (theme: Theme) => createStyles({
},
pageProfileNameEmoji: {
height: theme.typography.h4.fontSize,
fontWeight: theme.typography.fontWeightMedium,
},
pageProfileStatsDiv: {
display: 'inline-flex',
@ -139,9 +175,9 @@ export const styles = (theme: Theme) => createStyles({
pageContentLayoutConstraints: {
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingTop: theme.spacing.unit * 4,
paddingTop: theme.spacing.unit * 12,
paddingBottom: theme.spacing.unit * 2,
[theme.breakpoints.up('md')]: {
[theme.breakpoints.up('lg')]: {
paddingLeft: theme.spacing.unit * 32,
paddingRight: theme.spacing.unit * 32
},
@ -239,5 +275,8 @@ export const styles = (theme: Theme) => createStyles({
top: theme.spacing.unit,
right: theme.spacing.unit,
color: theme.palette.common.white
},
pageGrow: {
flexGrow: 1
}
});

View File

@ -1,5 +1,21 @@
import React, {Component} from 'react';
import {withStyles, Typography, Avatar, Divider, Button, CircularProgress, Paper, Tooltip, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions} from '@material-ui/core';
import {
withStyles,
Typography,
Avatar,
Divider,
Button,
CircularProgress,
Paper,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Toolbar,
IconButton
} from '@material-ui/core';
import {styles} from './PageLayout.styles';
import Mastodon from 'megalodon';
import { Account } from '../types/Account';
@ -7,10 +23,19 @@ import { Status } from '../types/Status';
import { Relationship } from '../types/Relationship';
import Post from '../components/Post';
import {withSnackbar} from 'notistack';
import { LinkableButton, LinkableIconButton } from '../interfaces/overrides';
import { LinkableIconButton } from '../interfaces/overrides';
import { emojifyString } from '../utilities/emojis';
import AccountEditIcon from 'mdi-material-ui/AccountEdit';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import PersonAddDisabledIcon from '@material-ui/icons/PersonAddDisabled';
import AccountMinusIcon from 'mdi-material-ui/AccountMinus';
import ChatIcon from '@material-ui/icons/Chat';
import AccountRemoveIcon from 'mdi-material-ui/AccountRemove';
import AccountHeartIcon from 'mdi-material-ui/AccountHeart';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
interface IProfilePageState {
account?: Account;
@ -112,23 +137,6 @@ class ProfilePage extends Component<any, IProfilePageState> {
});
}
statElement(classes: any, stat: 'following' | 'followers' | 'posts') {
let number = 0;
if (this.state.account) {
if (stat == 'following') {
number = this.state.account.following_count;
} else if (stat == 'followers') {
number = this.state.account.followers_count;
} else if (stat == 'posts') {
number = this.state.account.statuses_count;
}
}
return <div className={classes.pageProfileStat}>
<Typography variant="h6" color="inherit">{number}</Typography>
<Typography color="inherit">{stat}</Typography>
</div>;
}
loadMoreTimelinePieces() {
const { match: {params}} = this.props;
this.setState({ viewDidLoad: false, viewIsLoading: true})
@ -225,50 +233,73 @@ class ProfilePage extends Component<any, IProfilePageState> {
<div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}>
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
<div className={classes.pageHeroContent}>
{
this.isItMe()?
<Tooltip title="Edit profile">
<LinkableIconButton to="/you" color="inherit" className={classes.pageHeroToolbar}>
<AccountEditIcon/>
</LinkableIconButton>
</Tooltip>: null
}
<Avatar className={classes.pageProfileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
<Typography variant="h4" color="inherit" dangerouslySetInnerHTML={{__html: this.state.account? emojifyString(this.state.account.display_name, this.state.account.emojis, classes.pageProfileNameEmoji): ""}}></Typography>
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
<Typography paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
<Divider/>
<div className={classes.pageProfileStatsDiv}>
{this.statElement(classes, 'followers')}
{this.statElement(classes, 'following')}
{this.statElement(classes, 'posts')}
<Toolbar className={classes.profileToolbar}>
<div className={classes.pageGrow}/>
<Tooltip title={
this.isItMe()?
"You can't follow yourself.":
this.state.relationship && this.state.relationship.following?
"Unfollow":
"Follow"
}>
<IconButton color={"inherit"} disabled={this.isItMe()} onClick={() => this.toggleFollow()}>
{
this.isItMe()?
<PersonAddDisabledIcon/>:
this.state.relationship && this.state.relationship.following?
<AccountMinusIcon/>:
<PersonAddIcon/>
}
</IconButton>
</Tooltip>
<Tooltip title={"Send a message or post"}>
<LinkableIconButton to={`/compose?acct=${this.state.account? this.state.account.acct: ""}`} color={"inherit"}>
<ChatIcon/>
</LinkableIconButton>
</Tooltip>
<Tooltip title={this.state.relationship && this.state.relationship.blocking? "Unblock this account": "Block this account"}>
<IconButton color={"inherit"} disabled={this.isItMe()} onClick={() => this.toggleBlockDialog()}>
{
this.state.relationship && this.state.relationship.blocking? <AccountHeartIcon/>: <AccountRemoveIcon/>
}
</IconButton>
</Tooltip>
<Tooltip title="Open in web">
<IconButton href={this.state.account? this.state.account.url: ""} target="_blank" rel={"nofollower noreferrer noopener"} color={"inherit"}>
<OpenInNewIcon/>
</IconButton>
</Tooltip>
{
this.isItMe()?
<Tooltip title="Edit profile">
<LinkableIconButton to="/you" color="inherit">
<AccountEditIcon/>
</LinkableIconButton>
</Tooltip>: null
}
</Toolbar>
<div className={classes.profileContent}>
<Avatar className={classes.profileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
<div className={classes.profileUserBox}>
<Typography variant="h4" color="inherit" dangerouslySetInnerHTML={
{__html: this.state.account?
this.state.account.display_name?
emojifyString(this.state.account.display_name, this.state.account.emojis, classes.pageProfileNameEmoji)
: this.state.account.username
: ""}}
className={classes.pageProfileNameEmoji}/>
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
<Typography paragraph color="inherit">{
this.state.account ?
this.state.account.note?
this.state.account.note
: "No bio provided by user."
: "No bio available."
}</Typography>
<Typography color={"inherit"}>
{this.state.account? this.state.account.followers_count: 0} followers | {this.state.account? this.state.account.following_count: 0} following | {this.state.account? this.state.account.statuses_count: 0} posts
</Typography>
</div>
<Divider/>
{
this.state.relationship?
<div>
<Button
variant="contained"
color="primary"
className={classes.pageProfileFollowButton}
onClick={() => this.toggleFollow()}
disabled={this.state.account? this.state.account.id === JSON.parse(localStorage.getItem('account') as string).id: false}
>
{this.state.relationship.following? "Unfollow": "Follow"}
</Button>
<LinkableButton to={`/compose?mention=${this.state.account? this.state.account.acct: ""}`} variant="contained" className={classes.pageProfileFollowButton}>Mention</LinkableButton>
<Button
variant="contained"
className={classes.pageProfileFollowButton}
disabled={this.state.account? this.state.account.id === JSON.parse(localStorage.getItem('account') as string).id: false}
onClick={() => this.toggleBlockDialog()}
>
{this.state.relationship.blocking? "Unblock": "Block"}
</Button>
</div>: null
}
</div>
</div>
<div className={classes.pageContentLayoutConstraints}>

View File

@ -15,4 +15,7 @@ export function refreshUserAccountData() {
}).catch((err: Error) => {
console.error(err.message);
});
client.get('/instance').then((resp: any) => {
localStorage.setItem('isPleroma', (resp.data.version.match(/Pleroma/) ? "true" : "false"))
})
}

11
src/utilities/appbar.tsx Normal file
View File

@ -0,0 +1,11 @@
import { isDarwinApp } from "./desktop";
/**
* 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.
*
* @returns Boolean dictating if the title bar is visible
*/
export function isAppbarExpanded(): boolean {
return isDarwinApp() || process.env.NODE_ENV === "development";
}

View File

@ -22,4 +22,4 @@
"include": [
"src"
]
}
}