Compare commits

..

No commits in common. "master" and "v1.1.0-beta4" have entirely different histories.

65 changed files with 11635 additions and 9607 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,6 +1,6 @@
# These are supported funding model platforms
github: [alicerunsonfedora]
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: hyperspacedev
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

View File

@ -1,9 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
title: "Issue title"
labels: "bug"
assignees: ""
title: "[Bug] Issue title"
labels: ''
assignees: ''
---
**Describe the bug**
@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@ -24,10 +24,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**App Information (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari] (if applicable)
- Version [e.g. 22]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari] (if applicable)
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Request a feature
url: "https://github.com/hyperspacedev/hyperspace/discussions/new?category=ideas"
about: Suggest a new idea here.
- name: Discord
url: "https://discord.gg/c69AXwk"
about: Chat with us here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[Request] Request title'
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,28 +1,10 @@
**Changes Overview**
This PR makes the following changes:
<!-- List your changes here as a bullet list. Read the contribution guidelines for more details.-->
-
-
**Does this PR fix, close, or implement any issues?**
- [ ] This PR closes, fixes, or implements the following issues.
<!-- List any issues that this pull request may close or contribute to. Make sure you follow the proper syntax for referencing an issue.
Examples:
- Implements #0
- Closes UnscriptedVN/issues#0
- Contributes to #0
-->
-
-
-
-
<!-- If the following is a release check, uncomment the following line. -->
<!-- - [x] This is a release check. -->
**Pending for review**
@hyperspacedev/desktop
- [] This is a release check.

View File

@ -3,36 +3,17 @@ name: Build Linux Client
on: [push, pull_request]
jobs:
build_linux:
runs-on: ubuntu-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Change desktop field
run: |
from json import load, dump
json_dict = {}
with open('public/config.json', 'r') as file:
json_dict = load(file)
json_dict["location"] = "desktop"
with open('public/config.json', 'w+') as out:
dump(json_dict, out)
shell: python
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build:linux
- name: Upload Linux executables
uses: actions/upload-artifact@v1
if: success()
with:
name: "Linux executables (output dir)"
path: dist
build_linux:
runs-on: ubuntu-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build-desktop-linux

View File

@ -3,65 +3,46 @@ name: Build macOS Client
on: [push, pull_request]
jobs:
build_darwin:
runs-on: macos-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install certificates and entitlements
if: github.actor == 'alicerunsonfedora' || github.actor == 'Nomad1556' || github.actor == 'audmaxwell'
run: |
echo "Downloading certificates and profiles..."
echo "$ascCertificates" > certs.b64
echo "$ascMasProfile" > mas.b64
echo "$ascMacProfile" > mac.b64
echo "$ascEntitlementsMas" > entmas.b64
echo "$ascEntitlementsMac" > entmac.b64
echo "$ascInfoPlist" > info.b64
build_darwin:
runs-on: macos-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install certificates and entitlements
if: github.actor == 'alicerunsonfedora' || github.actor == 'Nomad1556' || github.actor == 'audmaxwell'
run: |
echo "Downloading certificates and profiles..."
echo "$ascCertificates" > certs.b64
echo "$ascMasProfile" > mas.b64
echo "$ascMacProfile" > mac.b64
echo "$ascEntitlementsMas" > entmas.b64
echo "$ascEntitlementsMac" > entmac.b64
echo "$ascInfoPlist" > info.b64
echo "Installing certificates and profiles..."
base64 --decode certs.b64 > Certificates.p12
base64 --decode mas.b64 > desktop/embedded.provisionprofile
base64 --decode mac.b64 > desktop/nonmas.provisionprofile
base64 --decode entmas.b64 > desktop/entitlements.mas.plist
base64 --decode entmac.b64 > desktop/entitlements.mac.plist
base64 --decode info.b64 > desktop/info.plist
security add-generic-password -a "appleseed@marquiskurt.net" -w "$ascPassword" -s "AC_PASSWORD"
sudo security import Certificates.p12 -P "$ascCertsPassword" -k /Library/Keychains/System.keychain
env:
ascPassword: ${{ secrets.ASC_PASSWORD }}
ascCertificates: ${{ secrets.ASC_CERTS }}
ascCertsPassword: ${{ secrets.ASC_CERTS_PASSWORD }}
ascMacProfile: ${{ secrets.ASC_NONMAS_PROFILE }}
ascMasProfile: ${{ secrets.ASC_EMBEDDED_PROFILE }}
ascEntitlementsMas: ${{ secrets.ASC_MAS_ENTITLEMENTS }}
ascEntitlementsMac: ${{ secrets.ASC_MAC_ENTITLEMENTS }}
ascInfoPlist: ${{ secrets.ASC_INFO_PLIST }}
- name: Change desktop field
run: |
from json import load, dump
json_dict = {}
with open('public/config.json', 'r') as file:
json_dict = load(file)
json_dict["location"] = "desktop"
with open('public/config.json', 'w+') as out:
dump(json_dict, out)
shell: python
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build:mac-unsigned
- name: Upload macOS (unsigned) bundle
uses: actions/upload-artifact@v1
if: success()
with:
name: "macOS bundle (output dir)"
path: dist
echo "Installing certificates and profiles..."
base64 --decode certs.b64 > Certificates.p12
base64 --decode mas.b64 > desktop/embedded.provisionprofile
base64 --decode mac.b64 > desktop/nonmas.provisionprofile
base64 --decode entmas.b64 > desktop/entitlements.mas.plist
base64 --decode entmac.b64 > desktop/entitlements.mac.plist
base64 --decode info.b64 > desktop/info.plist
security add-generic-password -a "appleseed@marquiskurt.net" -w "$ascPassword" -s "AC_PASSWORD"
sudo security import Certificates.p12 -P "$ascCertsPassword" -k /Library/Keychains/System.keychain
env:
ascPassword: ${{ secrets.ASC_PASSWORD }}
ascCertificates: ${{ secrets.ASC_CERTS }}
ascCertsPassword: ${{ secrets.ASC_CERTS_PASSWORD }}
ascMacProfile: ${{ secrets.ASC_NONMAS_PROFILE }}
ascMasProfile: ${{ secrets.ASC_EMBEDDED_PROFILE }}
ascEntitlementsMas: ${{ secrets.ASC_MAS_ENTITLEMENTS }}
ascEntitlementsMac: ${{ secrets.ASC_MAC_ENTITLEMENTS }}
ascInfoPlist: ${{ secrets.ASC_INFO_PLIST }}
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build-desktop-darwin-nosign

View File

@ -3,21 +3,21 @@ name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
env:
CI: false
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [8.x, 10.x, 12.x]
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
env:
CI: true

View File

@ -3,36 +3,36 @@ name: Build Windows Client
on: [push, pull_request]
jobs:
build_win:
runs-on: windows-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Change desktop field
run: |
from json import load, dump
build_win:
runs-on: windows-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Change desktop field
run: |
from json import load, dump
json_dict = {}
with open('public/config.json', 'r') as file:
json_dict = load(file)
json_dict = {}
with open('public/config.json', 'r') as file:
json_dict = load(file)
json_dict["location"] = "desktop"
json_dict["location"] = "desktop"
with open('public/config.json', 'w+') as out:
dump(json_dict, out)
shell: python
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build:win
- name: Upload Windows executable
uses: actions/upload-artifact@v1
if: success()
with:
name: "Windows executable (output dir)"
path: dist
with open('public/config.json', 'w+') as out:
dump(json_dict, out)
shell: python
- name: Install dependencies and build
run: |
npm install
npm run build --if-present
npm run build-desktop-win
- name: Upload Windows executable
uses: actions/upload-artifact@v1
if: success()
with:
name: 'Windows executable (output dir)'
path: dist

26
.github/workflows/jira-create.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Create issue on Jira
on:
issues:
types: [opened]
jobs:
jira:
runs-on: ubuntu-latest
steps:
- name: Jira Login
id: login
uses: atlassian/gajira-login@v2.0.0
env:
JIRA_BASE_URL: "https://hyperspacedev.atlassian.net"
JIRA_USER_EMAIL: software@marquiskurt.net
JIRA_API_TOKEN: ${{ secrets.JIRA_TOKEN }}
- name: Jira Create issue
id: create
uses: atlassian/gajira-create@v2.0.0
with:
project: HD
issuetype: Unsorted
summary: ${{ github.event.issue.title }}
description: ${{ github.event.issue.body }}

View File

@ -3,18 +3,18 @@ name: Prettier
on: [pull_request]
jobs:
prettier:
runs-on: ubuntu-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install dependencies and run Prettier
run: |
npm install
npm run test:prettier
env:
CI: true
prettier:
runs-on: ubuntu-latest
steps:
- name: Clone source code
uses: actions/checkout@v1
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Install dependencies and run Prettier
run: |
npm install
npm run check-prettier
env:
CI: true

5
.gitignore vendored
View File

@ -72,7 +72,4 @@ desktop/*.plist
desktop/*.provisionprofile
# JetBrains IDEA directory
.idea/
# Pesky macOS files
**/**.DS_Store
.idea/

263
README.md
View File

@ -1,272 +1,135 @@
<div align="center">
<p align="center">
<img src="desktop/app.iconset/icon_512@2x.png" width="128" max-width="25%" alt=“Hyperspace” />
</p>
<h1 align="center">Hyperspace</h1>
<img src="desktop/app.iconset/icon_512x512@2x.png" width="128" max-width="25%" alt="Hyperspace Desktop icon" />
<p align="center">The new beautiful, fluffy client for the fediverse written in TypeScript and React</p>
# Hyperspace Desktop
The new beautiful, fluffy client for the fediverse written in TypeScript and React
</div>
![Hyperspace Desktop on a MacBook Pro](screenshot.png)
![Hyperspace 1.0 on a MacBook Pro](screenshot.png)
[![Matrix room](https://img.shields.io/matrix/hypermasto:matrix.org.svg)](https://matrix.to/#/#hypermasto:matrix.org)
[![Discord server](https://img.shields.io/discord/554108687434907660.svg?color=blueviolet&label=discord)](https://discord.gg/c69AXwk)
![Build Status](https://github.com/hyperspacedev/hyperspace/workflows/Node%20CI/badge.svg) [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/hyperspacedev/hyperspace?include_prereleases)](https://github.com/hyperspacedev/hyperspace/releases) [![License: NPLv4+](https://img.shields.io/badge/license-NPLv4%2B-blue.svg)](LICENSE.txt) [![Hyperspace](https://snapcraft.io/hyperspace/badge.svg)](https://snapcraft.io/hyperspace)
Socialize and communicate with your friends in the fediverse (ActivityPub-powered social networks like Mastodon and Pleroma) with Hyperspace Desktop. Browse your timelines, check in with friends, and share your experiences across the fediverse in a beautiful, clean, and customizable way.
Hyperspace is the fluffiest client for Mastodon and other fediverse networks written in TypeScript and React. Hyperspace offers a fun, clean, fast, and responsive design that scales beautifully across devices and enhances the fediverse experience.
What Hyperspace Desktop offers:
## Features
- A clean, responsive, and streamlined design that fits in with your Mac
- Support for switching between accounts to access the accounts you use the most
- Customization support, ranging from several beautiful themes to masonry layout and infinite scrolling
- Powerful toot composer with media uploads, emojis, and polls
- Activity and recommended views that give you insight on the community/instance you reside in
- **Responsive by design**: Hyperspace is beautifully designed to put your content front and center and bring a familiar experience to Mastodon. View threads and profiles with ease and compose anywhere with the compose button. And, of course, Hyperspace scales across devices beautifully, providing the same experience anywhere.
- **Customizable**: Hyperspace allows customization and configuration at every level, from the server level with branding and instance setup, down to the user level with dark mode, custom themes, and multi-user account support. And, if the default configuration settings aren't enough, anyone can make their own version of Hyperspace with custom additions.
- **Open-source**: Hyperspace is free (libre) and open-source software. Licensed under the Non-Violent Public License, anyone can modify, redistribute, or contribute to the Hyperspace project without restriction. Hyperspace is written in TypeScript and takes advantage of multiple open-source libraries and projects such as React, Megalodon, and Material-UI, so web and Node.js developers will feel right at home.
## Get started
> If you've used Hyperspace 0.x, you'll note many changes with the 1.x and later series. You can learn more about these changes in the [migration article](MIGRATING.md).
Hyperspace Desktop is available for the major desktop platforms via our downloads page, GitHub, and other store platforms where applicable.
## Downloads
[**Download from our website &rsaquo;**](https://hyperspace.marquiskurt.net/download)
Hyperspace is available for download on GitHub as well as other platforms.
### Download from a store
[**Get latest release &rsaquo;**](https://github.com/hyperspacedev/hyperspace/releases/latest)
[![Get on the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/hyperspace) [![Get on the Mac App Store](https://hyperspace.marquiskurt.net/assets/images/mas.svg)](https://apps.apple.com/us/app/hyperspace-desktop/id1454139710?mt=12)
<!--[![Get on the Mac App Store](https://hyperspace.marquiskurt.net/images/mas.svg)](https://itunes.apple.com/us/app/hyperspace/id1454139710?mt=12)-->
**via [WinGet](https://github.com/microsoft/winget-cli)**:
[![Get on the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/hyperspace)
```
winget install HyperspaceDesktop
```
Looking for the Mac App Store version? [Read more &rsaquo;](https://hyperspace.marquiskurt.net/2019/11/08/post.html)
## Build from source
## Build instructions
To build Hyperspace Desktop, you'll need the following tools and packages:
### Prerequisites
- Node.js v10 or later
- (macOS-only) Xcode 10 or higher
To develop Hyperspace, you'll need the following tools and packages:
- Node.js 8 or later
### Installing dependencies
First, clone the repository from GitHub:
```
```bash
git clone https://github.com/hyperspacedev/hyperspace
```
Then, in the app directory, run the following command to install all of the package dependencies:
```
```npm
npm install
```
### Testing changes
Run any of the following scripts to test:
Before testing Hyperspace, you'll need to modify the `location` key in `public/config.json`. For example:
- `npm start` - Starts a local server hosted at https://localhost:3000.
- `npm run electron:build` - Builds a copy of the source code and then runs the app through Electron. Ensure that the `location` key in `config.json` points to `"desktop"` before running this.
- `npm run electron:prebuilt` - Similar to `electron:build` but doesn't build the project before running.
```json
"location": "https://localhost:3000"
```
The `location` key in `config.json` can take the following values during testing:
The `location` key can take the following values during testing:
- **https://localhost:3000**: Most suitable for running `npm start` or running via `react-scripts`.
- **desktop**: Most suitable for when testing the desktop application.
> Note: Hyperspace Desktop v1.1.0-beta3 and older versions require the location field to be changed to `"https://localhost:3000"` before running.
After changing this setting, run any of the following scripts to test:
- `npm start` - Starts a local server hosted at https://localhost:3000.
- `npm run electrify` - Builds a copy of the source code and then runs the app through Electron. Ensure that the `location` key in `config.json` points to `"desktop"` before running this.
- `npm run electrify-nobuild` - Similar to `electrify` but doesn't build the project before running.
### Building a release
To build a release, run the following command:
```
```npm
npm run build
```
The built files will be available under `build` as static files that can be hosted on a web server. If you plan to release these files alongside the desktop apps, compress these files in a ZIP.
#### Building desktop apps
#### Building desktop releases
You can run any of the following commands to build a release for the desktop:
- `npm run build:desktop-all`: Builds the desktop apps for all platforms (eg. Windows, macOS, Linux). Will run `npm run build` before building.
- `npm run build:win`: Builds the desktop app for Windows without running `npm run build`.
- `npm run build:mac`: Builds the desktop apps for macOS without running `npm run build`. See the details below for more information on building for macOS.
- `npm run build:mas`: Builds the desktop apps for the Mac App Store without running `npm run build`. See the details below for more information on building for macOS.
- `npm run build:linux`: Builds the desktop apps for Linux (eg. Debian package, AppImage, and Snap) without running `npm run build`.
- `npm run build:linux-select-targets`: Builds the desktop app for Linux without running `npm run build`. _Targets are required as parameters._
- `npm run build-desktop`: Builds the desktop apps for all platforms (eg. Windows, macOS, Linux). Will run `npm run build` before building.
- `npm run build-desktop-win`: Builds the desktop app for Windows without running `npm run build`.
- `npm run build-desktop-darwin`: Builds the desktop apps for macOS (eg. disk image, Mac App Store) without running `npm run build`. See the details below for more information on building for macOS.
- `npm run build-desktop-linux`: Builds the desktop apps for Linux (eg. Debian package, AppImage, and Snap) without running `npm run build`.
- `npm run build-desktop-linux-select`: Builds the desktop app for Linux without running `npm run build`. _Target is required as a parameter._
The built files will be available under `dist` that can be uploaded to your app distributor or website.
#### Extra steps for macOS
#### Building for macOS
The macOS builds of Hyperspace Desktop require a bit more effort and resources to build and distribute accordingly. The following is a quick guide to building Hyperspace Desktop for macOS and for the Mac App Store.
More recent version of macOS require that the Hyperspace desktop app be both digitally code-signed and notarized (uploaded to Apple to check for malware). Hyperspace includes the tools necessary to automate this process when building the macOS version either by `npm run build-desktop` or by `npm run build-desktop-darwin`.
##### Gather your tools
Make sure you have your provisioning profiles for the Mac App Store (`embedded.provisionprofile`) and standard distribution (`nonmas.provisionprofile`) in the `desktop` directory. These provision profiles can be obtained through Apple Developer. You'll also need to create entitlements files in the `desktop` directory that list the following entitlements for your app:
To create a code-signed and notarized version of Hyperspace Desktop, you'll need to acquire some provisioning profiles and certificates from a valid Apple Developer account.
- `com.apple.security.app-sandbox`
- `com.apple.security.files.downloads.read-write`
- `com.apple.security.files.user-selected.read-write`
- `com.apple.security.allow-unsigned-executable-memory`
- `com.apple.security.network.client`
For certificates, make sure your Mac has the following certificates installed:
For the child ones (inherited `entitlements.mas.inherit.plist`):
- 3rd Party Mac Developer Application
- 3rd Party Mac Developer Installer
- Developer ID Application
- Developer ID Installer
- Mac Developer
- `com.apple.security.app-sandbox`
- `com.apple.security.inherit`
- `com.apple.security.files.downloads.read-write`
- `com.apple.security.files.user-selected.read-write`
- `com.apple.security.allow-unsigned-executable-memory`
- `com.apple.security.network.client`
The easiest way to handle this is by opening Xcode and going to **Preferences &rsaquo; Accounts** and create the certificates from "Manage Certificates".
> ⚠️ Note that the inherited permissions are the same as that of the parent. This is due to an issue where the hardened runtime fails to pass down the inherited properties (see [electron/electron#20560](https://github.com/electron/electron/issues/20560#issuecomment-546110018)). This might change in future versions of macOS.
You'll also need to [create a provisioning profile for **Mac App Store** distribution](https://developer.apple.com/account/resources/profiles/add) and save it to the `desktop` folder as `embedded.provisonprofile`.
It is also recommended to add the `com.apple.security.applications-groups` entry with your bundle's identifier. You'll also need to create an `info.plist` in the `desktop` directory containing the team identifier and application identifier and install the developer certificates on the Mac you plan to build from.
##### Create your entitlements files
You'll also want to modify the `notarize.js` file to change the details from the default to your App Store Connect account details and app identifier.
You'll also need to create the entitlements files in the `desktop` directory that declares the permissions for Hyperspace Desktop. Replace `TEAM_ID` with the appropriate Apple Developer information and `BUNDLE_ID` with the bundle ID of your app.
###### entitlements.mac.plist
```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
```
###### entitlements.mas.plist
```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>TEAM_ID.BUNDLE_ID</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
```
###### entitlements.mas.inherit.plist
```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
```
###### entitlements.mas.loginhelper.plist
```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
```
###### info.plist
```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ElectronTeamID</key>
<string>TEAM_ID</string>
<key>com.apple.developer.team-identifier</key>
<string>TEAM_ID</string>
<key>com.apple.application-identifier</key>
<string>TEAM_ID.BUNDLE_ID</string>
</dict>
</plist>
```
##### Edit `notarize.js`
You'll also need to edit `notarize.js` in the `desktop` directory. Replace `<TEAM_ID>`, `<BUNDLE_ID>`, and `<APPLE_DEVELOPER_EMAIL>` with the appropriate information from the app and your account from Apple Developer.
```js
// 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: "<BUNDLE_ID>",
appPath: `${appOutDir}/${appName}.app`,
appleId: "<APPLE_DEVELOPER_EMAIL>",
appleIdPassword: password,
ascProvider: "<TEAM_ID>"
});
};
```
Note that the password is pulled from your keychain. You'll need to create an app password and store it in your keychain as `AC_PASSWORD`.
##### Build the apps
Run any of the following commands to build Hyperspace Desktop for the Mac:
- `npm run build:mac` - Builds the macOS app in a DMG container.
- `npm run build:mac-unsigned` - Similar to `build:mac`, but skips code signing and notarization. **Use only for CI or in situations where code signing and notarization is not available.**
- `npm run build:mas` - Builds the Mac App Store package.
> ⚠️ **Warning**: The package.json file also includes the `build-desktop-darwin-nosign` script. This script is specifically intended for automated systems that cannot run notarization (Azure Pipelines, GitHub Actions, etc.). _Do not use this command to build production-ready versions of Hyperspace_.
## Licensing and Credits
Hyperspace Desktop is licensed under the [Non-violent Public License v4+](LICENSE.txt), a permissive license under the conditions that you do not use this for any unethical purposes and to file patent claims. Please read what your rights are as a Hyperspace Desktop user/developer in the license for more information.
Hyperspace is licensed under the [Non-violent Public License v4+](LICENSE.txt), a permissive license under the conditions that you do not use this for any unethical purposes and to file patent claims. Please read what your rights are as a Hyperspace user/developer in the license for more information.
Hyperspace Desktop has been made possible by the React, TypeScript, Megalodon, and Material-UI projects as well our [Patrons](patreon.md) and our contributors on GitHub.
Hyperspace has been made possible by the React, TypeScript, Megalodon, and Material-UI projects as well our [Patrons](patreon.md) and our contributors on GitHub.
## Contribute

Binary file not shown.

BIN
desktop/app.iconset/icon_128.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

BIN
desktop/app.iconset/icon_16.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

BIN
desktop/app.iconset/icon_256.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

BIN
desktop/app.iconset/icon_32.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
desktop/app.iconset/icon_512.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,44 +0,0 @@
#
# Hyperspace Desktop MAS Build Config
# (C) 2020 Hyperspace Developers. Licensed under NPLv4.
#
# The following configuration file is used to configure the Mac App Store builds of Hyperspace
# Desktop. For building cross-platform apps without submitting to the Mac App Store, modify the
# standard.yml config file.
#
appId: net.marquiskurt.hyperspace
afterSign: desktop/notarize.js
directories:
buildResources: desktop
# The bundleVersion and bundleShortVersion keys in this config correspond to builds in the
# Mac App Store. If you are attempting to upload a new build of the same app version to the
# Mac App Store, change the bundle version. The bundle short version should be the same as
# the app version seen in config.json and package.json.
#
# If you are submitting a new app version entirely, make sure the bundle version and short
# version match accordingly, except in cases where the app version is the same version as an
# older Mac App Store build.
mac:
# Bundle version will reflect the build number (i.e., the release number).
bundleVersion: "28"
bundleShortVersion: "1.1.4"
category: public.app-category.social-networking
icon: desktop/app.icns
target: [dmg]
darkModeSupport: true
hardenedRuntime: false
gatekeeperAssess: false
# Note that you will need the proper entitlements files for the following keys below. Refer to
# the Hyperspace Desktop documentation regarding what keys will need to be inserted into the
# entitlements files:
# https://hyperspace.marquiskurt.net/docs/desktop-build-desktop.html
mas:
entitlements: desktop/entitlements.mas.plist
entitlementsInherit: desktop/entitlements.mas.inherit.plist
provisioningProfile: desktop/embedded.provisionprofile
dmg:
sign: false

View File

@ -1,35 +0,0 @@
#
# Hyperspace Desktop Build Config
# (C) 2020 Hyperspace Developers. Licensed under NPLv4.
#
# The following configuration file is used to configure and build the cross-platforms apps,
# excluding the Mac App Store build. For the Mac App Store build, modify the mas.yml file.
#
appId: net.marquiskurt.hyperspace
afterSign: desktop/notarize.js
directories:
buildResources: desktop
mac:
category: public.app-category.social-networking
icon: desktop/app.icns
target: [dmg]
darkModeSupport: true
hardenedRuntime: true
dmg:
sign: false
win:
target: [nsis]
icon: desktop/app.ico
linux:
target: ["${@:1}"]
icon: linux
category: Network
snap:
confinement: strict
summary: The fluffiest client for the fediverse

18506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,66 +1,65 @@
{
"name": "hyperspace",
"productName": "Hyperspace Desktop",
"version": "1.1.4",
"version": "1.1.0-beta4",
"description": "A beautiful, fluffy client for the fediverse",
"author": "Marquis Kurt <hyperspacedev@marquiskurt.net>",
"repository": "https://github.com/hyperspacedev/hyperspace.git",
"private": true,
"homepage": "./",
"devDependencies": {
"@date-io/moment": "^1.3.13",
"@material-ui/core": "^3.9.4",
"@material-ui/icons": "^4.9.1",
"@types/emoji-mart": "^2.11.3",
"@types/jest": "^24.9.1",
"@date-io/moment": "^1.3.11",
"@material-ui/core": "^3.9.3",
"@material-ui/icons": "^4.5.1",
"@types/emoji-mart": "^2.11.0",
"@types/jest": "^24.0.18",
"@types/node": "11.11.6",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/react-router-dom": "^4.3.5",
"@types/react-swipeable-views": "latest",
"axios": "^0.21.1",
"electron": "^9.0.5",
"electron-builder": "^22.7.0",
"emoji-mart": "^2.11.2",
"axios": "^0.19.0",
"electron": "^6.0.11",
"electron-builder": "^21.2.0",
"emoji-mart": "^2.11.1",
"file-dialog": "^0.0.7",
"material-ui-pickers": "^2.2.4",
"mdi-material-ui": "^5.27.0",
"mdi-material-ui": "^5.18.0",
"megalodon": "^0.6.4",
"moment": "^2.27.0",
"moment": "^2.24.0",
"notistack": "^0.5.1",
"prettier": "^1.19.1",
"query-string": "^6.13.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "^3.4.1",
"react-swipeable-views": "^0.13.9",
"prettier": "1.18.2",
"query-string": "^6.8.3",
"react": "^16.10.2",
"react-dom": "^16.10.2",
"react-router-dom": "^5.1.2",
"react-scripts": "^2.1.8",
"react-swipeable-views": "^0.13.3",
"react-web-share-api": "^0.0.2",
"typescript": "^3.9.5"
"typescript": "^3.7.2"
},
"dependencies": {
"electron-notarize": "^0.1.1",
"electron-updater": "^4.3.1",
"electron-updater": "^4.1.2",
"electron-window-state": "^5.0.3",
"react-masonry-css": "^1.0.14"
},
"main": "public/electron.js",
"scripts": {
"start": "react-scripts start",
"test": "react-scripts test",
"test:prettier": "prettier --check src/**/**.tsx",
"eject": "react-scripts eject",
"electron:build": "npm run build; electron .",
"electron:prebuilt": "electron .",
"electrify": "npm run build; electron .",
"electrify-nobuild": "electron .",
"build": "react-scripts build",
"build:icns": "iconutil -c icns desktop/app.iconset -o desktop/app.icns",
"build:desktop-all": "npm run build; npm run build:icns; electron-builder -p 'never' -mwl deb AppImage snap -c ebuild/standard.yml",
"build:win": "electron-builder -p 'never' -w -c ebuild/standard.yml",
"build:mac": "npm run build:icns; electron-builder -p 'never' -m -c ebuild/standard.yml",
"build:mac-unsigned": "npm run build:icns; electron-builder -p 'never' -m dmg -c.mac.identity=null -c.afterSign=\"desktop/donothing.js\" -c ebuild/standard.yml",
"build:mas": "npm run build:icns; electron-builder -p 'never' -m mas -c ebuild/mas.yml",
"build:linux": "electron-builder -p 'never' -l deb AppImage snap -c ebuild/standard.yml",
"build:linux-select-targets": "electron-builder -p 'never' -c ebuild/standard.yml -l "
"create-mac-icon": "cd desktop; iconutil -c icns app.iconset; cd ..",
"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 ",
"check-prettier": "prettier --check src/**/**.tsx",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
@ -70,5 +69,47 @@
"not dead",
"not ie <= 11",
"not op_mini all"
]
],
"build": {
"appId": "net.marquiskurt.hyperspace",
"afterSign": "desktop/notarize.js",
"directories": {
"buildResources": "desktop"
},
"mac": {
"category": "public.app-category.social-networking",
"icon": "desktop/app.icns",
"target": [
"dmg",
"mas"
],
"darkModeSupport": true,
"hardenedRuntime": true
},
"mas": {
"entitlements": "desktop/entitlements.mas.plist",
"entitlementsInherit": "desktop/entitlements.mas.inherit.plist",
"provisioningProfile": "desktop/embedded.provisionprofile"
},
"dmg": {
"sign": false
},
"win": {
"target": [
"nsis"
],
"icon": "desktop/app.ico"
},
"linux": {
"target": [
"${@:1}"
],
"icon": "linux",
"category": "Network"
},
"snap": {
"confinement": "strict",
"summary": "A beautiful, fluffy client for the fediverse"
}
}
}

View File

@ -2,10 +2,6 @@
Hyperspace has been made possible by the efforts of the Hyperspace development team and these amazing contributors on Patreon:
<!-- (Add contributors here) -->
- LucasAzazer
Thanks for your continued support in helping us create the fluffiest client for the fediverse!
## Previous Contributors
- LucasAzazer
Thanks for your continued support in helping us create the fluffiest client for the fediverse!

View File

@ -1,19 +1,19 @@
{
"version": "1.1.4",
"location": "https://hyperspaceapp.herokuapp.com",
"version": "1.1.0",
"location": "https://hyperspaceapp-next.herokuapp.com",
"branding": {
"name": "Hyperspace",
"logo": "logo.svg",
"background": "background.png"
},
"developer": false,
"developer": true,
"federation": {
"universalLogin": true,
"allowPublicPosts": true,
"enablePublicTimeline": true
},
"registration": {
"defaultInstance": "mastodon.online"
"defaultInstance": "mastodon.social"
},
"admin": {
"name": "Hyperspace Developers",

View File

@ -2,17 +2,10 @@
// Electron script to run Hyperspace as an app
// © 2018 Hyperspace developers. Licensed under NPL v1.
const {
app,
Menu,
protocol,
BrowserWindow,
shell,
systemPreferences
} = require("electron");
const windowStateKeeper = require("electron-window-state");
const { autoUpdater } = require("electron-updater");
const path = require("path");
const { app, Menu, protocol, BrowserWindow, shell, systemPreferences } = require('electron');
const windowStateKeeper = require('electron-window-state');
const { autoUpdater } = require('electron-updater');
const path = require('path');
// Check for any updates to the app
autoUpdater.checkForUpdatesAndNotify();
@ -25,7 +18,7 @@ let mainWindow;
// file:// protocol, which is necessary for Mastodon to redirect
// to when authorizing Hyperspace.
protocol.registerSchemesAsPrivileged([
{ scheme: "hyperspace", privileges: { standard: true, secure: true } }
{ scheme: 'hyperspace', privileges: { standard: true, secure: true } }
]);
/**
@ -40,81 +33,80 @@ function isDarwin() {
* Register the protocol for Hyperspace
*/
function registerProtocol() {
protocol.registerFileProtocol(
"hyperspace",
(request, callback) => {
// Check to make sure we're doing a GET request
if (request.method !== "GET") {
callback({ error: -322 });
return null;
}
// Check to make sure we're actually working with a hyperspace
// protocol and that the host is 'hyperspace'
const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== "hyperspace:") {
callback({ error: -302 });
return;
}
if (parsedUrl.host !== "hyperspace") {
callback({ error: -105 });
return;
}
// Convert the parsed URL to a list of strings.
const target = parsedUrl.pathname.split("/");
// Check that the target isn't trying to go somewhere
// else. If it is, throw a "FILE_NOT_FOUND" error
if (target[0] !== "") {
callback({ error: -6 });
return;
}
// Check if the last target item in the list is empty.
// If so, replace it with "index.html" so that it can
// load a page.
if (target[target.length - 1] === "") {
target[target.length - 1] = "index.html";
}
// Check the middle target and redirect to the appropriate
// build files of the desktop app when running.
let baseDirectory;
if (target[1] === "app" || target[1] === "oauth") {
baseDirectory = __dirname + "/../build/";
} else {
// If it doesn't match above, throw a "FILE_NOT_FOUND" error.
callback({ error: -6 });
return;
}
// Create a normalized version of the string.
baseDirectory = path.normalize(baseDirectory);
// Check to make sure the target isn't trying to go out of bounds.
// If it is, throw a "FILE_NOT_FOUND" error.
const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith("..")) {
callback({ error: -6 });
return;
}
// Create the absolute target path and return it.
const absTarget = path.join(baseDirectory, relTarget);
callback({ path: absTarget });
},
error => {
if (error) console.error("Failed to register protocol");
protocol.registerFileProtocol('hyperspace', (request, callback) => {
// Check to make sure we're doing a GET request
if (request.method !== "GET") {
callback({error: -322});
return null;
}
);
// Check to make sure we're actually working with a hyperspace
// protocol and that the host is 'hyperspace'
const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== "hyperspace:") {
callback({error: -302});
return;
}
if (parsedUrl.host !== "hyperspace") {
callback({error: -105});
return;
}
// Convert the parsed URL to a list of strings.
const target = parsedUrl.pathname.split("/");
// Check that the target isn't trying to go somewhere
// else. If it is, throw a "FILE_NOT_FOUND" error
if (target[0] !== "") {
callback({error: -6});
return;
}
// Check if the last target item in the list is empty.
// If so, replace it with "index.html" so that it can
// load a page.
if (target[target.length -1] === "") {
target[target.length -1] = "index.html";
}
// Check the middle target and redirect to the appropriate
// build files of the desktop app when running.
let baseDirectory;
if (target[1] === "app" || target[1] === "oauth") {
baseDirectory = __dirname + "/../build/";
} else {
// If it doesn't match above, throw a "FILE_NOT_FOUND" error.
callback({error: -6});
}
// Create a normalized version of the string.
baseDirectory = path.normalize(baseDirectory);
// Check to make sure the target isn't trying to go out of bounds.
// If it is, throw a "FILE_NOT_FOUND" error.
const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith('..')) {
callback({error: -6});
return;
}
// Create the absolute target path and return it.
const absTarget = path.join(baseDirectory, relTarget);
callback({ path: absTarget });
}, (error) => {
if (error) console.error('Failed to register protocol');
});
}
/**
* Create the window and all of its properties
*/
function createWindow() {
// Create a window state manager that keeps track of the width
// and height of the main window.
let mainWindowState = windowStateKeeper({
@ -123,72 +115,68 @@ function createWindow() {
});
// Create a browser window with some settings
mainWindow = new BrowserWindow({
// Use the values from the window state keeper
// to draw the window exactly as it was left.
// If not possible, derive it from the default
// values defined earlier.
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
mainWindow = new BrowserWindow(
{
// Use the values from the window state keeper
// to draw the window exactly as it was left.
// If not possible, derive it from the default
// values defined earlier.
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
// Set a minimum width to prevent element collisions.
minWidth: 300,
// Set a minimum width to prevent element collisions.
minWidth: 300,
// Set important web preferences.
webPreferences: {nodeIntegration: true},
// Set important web preferences.
webPreferences: { nodeIntegration: true },
// Set some preferences that are specific to macOS.
titleBarStyle: 'hiddenInset',
vibrancy: "sidebar",
transparent: isDarwin(),
backgroundColor: isDarwin()? "#80000000": "#FFF",
// Set some preferences that are specific to macOS.
titleBarStyle: "hiddenInset",
vibrancy: "sidebar",
transparent: isDarwin(),
backgroundColor: isDarwin() ? "#80000000" : "#FFF",
// Hide the window until the contents load
show: false
});
// Hide the window until the contents load
show: false
}
);
// Set up event listeners to track changes in the window state.
mainWindowState.manage(mainWindow);
// 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, as well as accent color
if (isDarwin()) {
systemPreferences.subscribeNotification(
"AppleInterfaceThemeChangedNotification",
() => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
);
});
systemPreferences.subscribeNotification(
"AppleColorPreferencesChangedNotification",
() => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
systemPreferences.subscribeNotification('AppleColorPreferencesChangedNotification', () => {
if (mainWindow != null) {
mainWindow.webContents.reload();
}
);
});
}
// Only show the window when ready
mainWindow.once("ready-to-show", () => {
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Delete the window when closed
mainWindow.on("closed", () => {
mainWindow = null;
mainWindow.on('closed', () => {
mainWindow = null
});
// Hijack any links with a blank target and open them in the default
// browser instead of a new Electron window
mainWindow.webContents.on("new-window", (event, url) => {
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
@ -211,17 +199,18 @@ function safelyGoTo(url) {
* Create the menu bar and attach it to a window
*/
function createMenubar() {
// Create an instance of the Menu class
let menu = Menu;
// Create a menu bar template
const menuBar = [
{
label: "File",
label: 'File',
submenu: [
{
label: "New Window",
accelerator: "CmdOrCtrl+N",
label: 'New Window',
accelerator: 'CmdOrCtrl+N',
click() {
if (mainWindow == null) {
registerProtocol();
@ -230,110 +219,106 @@ function createMenubar() {
}
},
{
label: "New Post",
accelerator: "Shift+CmdOrCtrl+N",
label: 'New Post',
accelerator: 'Shift+CmdOrCtrl+N',
click() {
safelyGoTo("hyperspace://hyperspace/app/#compose");
safelyGoTo("hyperspace://hyperspace/app/#compose")
}
}
]
},
{
label: "Edit",
label: 'Edit',
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" }
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' }
]
},
{
label: "View",
label: 'View',
submenu: [
{
label: "Back",
accelerator: "CmdOrCtrl+[",
label: 'Back',
accelerator: 'CmdOrCtrl+[',
click() {
if (
mainWindow != null &&
mainWindow.webContents.canGoBack()
) {
mainWindow.webContents.goBack();
if (mainWindow != null && mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack()
}
}
},
{
label: "Forward",
accelerator: "CmdOrCtrl+]",
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click() {
if (
mainWindow != null &&
mainWindow.webContents.canGoForward()
) {
mainWindow.webContents.goForward();
if (mainWindow != null && mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward()
}
}
},
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: 'reload' },
{ role: 'forcereload' },
{ type: 'separator' },
{
label: "Open Dev Tools",
click() {
label: 'Open Dev Tools',
click () {
try {
mainWindow.webContents.openDevTools();
} catch (err) {
console.error("Couldn't open dev tools: " + err);
}
},
accelerator: "Shift+CmdOrCtrl+I"
accelerator: 'Shift+CmdOrCtrl+I'
},
{ type: "separator" },
{ role: "togglefullscreen" }
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
label: "Timelines",
submenu: [
{
label: "Home",
label: 'Home',
accelerator: "CmdOrCtrl+0",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/home");
safelyGoTo("hyperspace://hyperspace/app/#/home")
}
},
{
label: "Local",
label: 'Local',
accelerator: "CmdOrCtrl+1",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/local");
safelyGoTo("hyperspace://hyperspace/app/#/local")
}
},
{
label: "Public",
label: 'Public',
accelerator: "CmdOrCtrl+2",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/public");
safelyGoTo("hyperspace://hyperspace/app/#/public")
}
},
{
label: "Messages",
label: 'Messages',
accelerator: "CmdOrCtrl+3",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/messages");
safelyGoTo("hyperspace://hyperspace/app/#/messages")
}
},
{ type: "separator" },
{ type: 'separator' },
{
label: "Activity",
accelerator: "Alt+CmdOrCtrl+A",
label: 'Activity',
accelerator: 'Alt+CmdOrCtrl+A',
click() {
safelyGoTo("hyperspace://hyperspace/app/#/activity");
safelyGoTo("hyperspace://hyperspace/app/#/activity")
}
}
]
@ -342,138 +327,127 @@ function createMenubar() {
label: "Account",
submenu: [
{
label: "Notifications",
label: 'Notifications',
accelerator: "Alt+CmdOrCtrl+N",
click() {
safelyGoTo(
"hyperspace://hyperspace/app/#/notifications"
);
safelyGoTo("hyperspace://hyperspace/app/#/notifications")
}
},
{
label: "Recommendations",
label: 'Recommendations',
accelerator: "Alt+CmdOrCtrl+R",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/recommended");
safelyGoTo("hyperspace://hyperspace/app/#/recommended")
}
},
{ type: "separator" },
{ type: 'separator' },
{
label: "Edit Profile",
label: 'Edit Profile',
accelerator: "Shift+CmdOrCtrl+P",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/you");
safelyGoTo("hyperspace://hyperspace/app/#/you")
}
},
{
label: "Follow Requests",
label: 'Follow Requests',
accelerator: "Alt+CmdOrCtrl+E",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/requests");
safelyGoTo("hyperspace://hyperspace/app/#/requests")
}
},
{
label: "Blocked Servers",
label: 'Blocked Servers',
accelerator: "Shift+CmdOrCtrl+B",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/blocked");
safelyGoTo("hyperspace://hyperspace/app/#/blocked")
}
},
{ type: "separator" },
{ type: 'separator'},
{
label: "Switch Accounts...",
label: 'Switch Accounts...',
click() {
safelyGoTo("hyperspace://hyperspace/app/#/welcome");
safelyGoTo("hyperspace://hyperspace/app/#/welcome")
}
}
]
},
{
role: "window",
role: 'window',
submenu: [
{ role: "minimize" },
{ role: "close" },
{ type: "separator" }
{ role: 'minimize' },
{ role: 'close' },
{ type: 'separator' },
]
},
{
role: "help",
role: 'help',
submenu: [
{
label: "Hyperspace Desktop Docs",
click() {
require("electron").shell.openExternal(
"https://hyperspace.marquiskurt.net/docs/"
);
}
label: 'Hyperspace Desktop Docs',
click () { require('electron').shell.openExternal('https://hyperspace.marquiskurt.net/docs/') }
},
{
label: "Report a Bug",
click() {
require("electron").shell.openExternal(
"https://github.com/hyperspacedev/hyperspace/issues"
);
}
label: 'Report a Bug',
click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/issues') }
},
{ type: "separator" },
{ type: 'separator' },
{
label: "Acknowledgements",
click() {
require("electron").shell.openExternal(
"https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md"
);
}
label: 'Acknowledgements',
click () { require('electron').shell.openExternal('https://github.com/hyperspacedev/hyperspace/blob/master/patreon.md') }
}
]
}
];
if (process.platform === "darwin") {
if (process.platform === 'darwin') {
menuBar.unshift({
label: app.getName(),
submenu: [
{
label: `About ${app.getName()}`,
click() {
safelyGoTo("hyperspace://hyperspace/app/#/about");
safelyGoTo("hyperspace://hyperspace/app/#/about")
}
},
{ type: "separator" },
{ type: 'separator' },
{
label: "Preferences...",
accelerator: "Cmd+,",
accelerator: 'Cmd+,',
click() {
safelyGoTo("hyperspace://hyperspace/app/#/settings");
}
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" }
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
});
// Edit menu
menuBar[2].submenu.push(
{ type: "separator" },
{ type: 'separator' },
{
label: "Speech",
submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }]
label: 'Speech',
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' }
]
}
);
// Window menu
menuBar[6].submenu = [
{ role: "close" },
{ role: "minimize" },
{ role: "zoom" },
{ type: "separator" },
{ role: "front" }
];
{ role: 'close' },
{ role: 'minimize' },
{ role: 'zoom' },
{ type: 'separator' },
{ role: 'front' }
]
}
// Create the template for the menu and attach it to the application
@ -482,21 +456,21 @@ function createMenubar() {
}
// When the app is ready, create the window and menu bar
app.on("ready", () => {
app.on('ready', () => {
registerProtocol();
createWindow();
createMenubar();
});
// Standard quit behavior changes for macOS
app.on("window-all-closed", () => {
app.on('window-all-closed', () => {
if (!isDarwin()) {
app.quit();
app.quit()
}
});
// When the app is activated, create the window and menu bar
app.on("activate", () => {
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
createMenubar();

View File

@ -11,12 +11,12 @@ import ProfilePage from "./pages/ProfilePage";
import TimelinePage from "./pages/Timeline";
import Conversation from "./pages/Conversation";
import NotificationsPage from "./pages/Notifications";
import AnnouncementsPage from "./pages/Announcements";
import SearchPage from "./pages/Search";
import Composer from "./pages/Compose";
import WelcomePage from "./pages/Welcome";
import MessagesPage from "./pages/Messages";
import RecommendationsPage from "./pages/Recommendations";
import Missingno from "./pages/Missingno";
import Blocked from "./pages/Blocked";
import You from "./pages/You";
import RequestsPage from "./pages/Requests";
@ -30,7 +30,6 @@ let theme = setHyperspaceTheme(getUserDefaultTheme());
interface IAppState {
theme: any;
showLayout: boolean;
avatarURL?: string;
}
class App extends Component<any, IAppState> {
@ -45,7 +44,6 @@ class App extends Component<any, IAppState> {
showLayout:
userLoggedIn() && !window.location.hash.includes("#/welcome")
};
this.setAvatarURL = this.setAvatarURL.bind(this);
}
componentWillMount() {
@ -87,13 +85,9 @@ class App extends Component<any, IAppState> {
}
}
setAvatarURL(avatarURL: string) {
this.setState({
avatarURL
});
}
render() {
const { classes } = this.props;
this.removeBodyBackground();
return (
@ -101,9 +95,7 @@ class App extends Component<any, IAppState> {
<CssBaseline />
<Route path="/welcome" component={WelcomePage} />
<div>
{this.state.showLayout ? (
<AppLayout avatarURL={this.state.avatarURL} />
) : null}
{this.state.showLayout ? <AppLayout /> : null}
<PrivateRoute
exact
path="/"
@ -146,10 +138,6 @@ class App extends Component<any, IAppState> {
)}
/>
<PrivateRoute path="/messages" component={MessagesPage} />
<PrivateRoute
path="/announcements"
component={AnnouncementsPage}
/>
<PrivateRoute
path="/notifications"
component={NotificationsPage}
@ -165,9 +153,7 @@ class App extends Component<any, IAppState> {
<PrivateRoute path="/search" component={SearchPage} />
<PrivateRoute path="/settings" component={Settings} />
<PrivateRoute path="/blocked" component={Blocked} />
<PrivateRoute path="/you">
<You onAvatarUpdate={this.setAvatarURL} />
</PrivateRoute>
<PrivateRoute path="/you" component={You} />
<PrivateRoute path="/about" component={AboutPage} />
<PrivateRoute path="/compose" component={Composer} />
<PrivateRoute

View File

@ -32,7 +32,6 @@ import {
import MenuIcon from "@material-ui/icons/Menu";
import SearchIcon from "@material-ui/icons/Search";
import NotificationsIcon from "@material-ui/icons/Notifications";
import AnnouncementIcon from "@material-ui/icons/Announcement";
import MailIcon from "@material-ui/icons/Mail";
import HomeIcon from "@material-ui/icons/Home";
import DomainIcon from "@material-ui/icons/Domain";
@ -62,8 +61,7 @@ import { getConfig, getUserDefaultBool } from "../../utilities/settings";
import {
isDesktopApp,
isDarwinApp,
getElectronApp,
linkablePath
getElectronApp
} from "../../utilities/desktop";
import { Config } from "../../types/Config";
import {
@ -327,10 +325,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
searchForQuery(what: string) {
what = what.replace(/^#/g, "tag:");
// console.log(what);
window.location.href = linkablePath("/#/search?query=" + what);
// window.location.href = isDesktopApp()
// ? "hyperspace://hyperspace/app/index.html#/search?query=" + what
// : "/#/search?query=" + what;
window.location.href = isDesktopApp()
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
: "/#/search?query=" + what;
}
/**
@ -514,16 +511,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<Divider />
<div className={classes.drawerDisplayMobile}>
<ListSubheader>Account</ListSubheader>
<LinkableListItem
button
key="announcements-mobile"
to="/announcements"
>
<ListItemIcon>
<AnnouncementIcon />
</ListItemIcon>
<ListItemText primary="Announcements" />
</LinkableListItem>
<LinkableListItem
button
key="notifications-mobile"
@ -654,14 +641,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
</div>
<div className={classes.appBarFlexGrow} />
<div className={classes.appBarActionButtons}>
<Tooltip title="Announcements">
<LinkableIconButton
to="/announcements"
color="inherit"
>
<AnnouncementIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Notifications">
<LinkableIconButton
color="inherit"
@ -700,9 +679,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}
alt="You"
src={
this.props.avatarURL
? this.props.avatarURL
: this.state.currentUser
this.state.currentUser
? this.state.currentUser
.avatar_static
: ""
@ -725,7 +702,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<div>
<LinkableListItem
button={true}
onClick={this.toggleAcctMenu}
to={`/profile/${
this.state.currentUser
? this.state.currentUser
@ -737,11 +713,8 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<Avatar
alt="You"
src={
this.props.avatarURL
? this.props
.avatarURL
: this.state
.currentUser
this.state
.currentUser
? this.state
.currentUser
.avatar_static
@ -773,7 +746,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<Divider />
<LinkableListItem
button={true}
onClick={this.toggleAcctMenu}
to={"/you"}
>
<ListItemText>
@ -782,7 +754,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
</LinkableListItem>
<LinkableListItem
button={true}
onClick={this.toggleAcctMenu}
to={"/requests"}
>
<ListItemText>

View File

@ -91,11 +91,7 @@ class AttachmentComponent extends Component<
);
case "unknown":
return (
<object
data={slide.url}
className={classes.mediaObject}
aria-label={`Slide: ${slide.id}`}
/>
<object data={slide.url} className={classes.mediaObject} />
);
}
}

View File

@ -6,6 +6,7 @@ import {
LinearProgress,
Tooltip
} from "@material-ui/core";
import { LinkableIconButton } from "../../interfaces/overrides";
import FastRewindIcon from "@material-ui/icons/FastRewind";
import FastForwardIcon from "@material-ui/icons/FastForward";

View File

@ -68,10 +68,7 @@ class ComposeMediaAttachment extends Component<
) : attachment.type === "video" ? (
<video autoPlay={false} src={attachment.url} />
) : (
<object
data={attachment.url}
aria-label={`Attachment: ${attachment.id}`}
/>
<object data={attachment.url} />
)}
<GridListTileBar
classes={{ title: classes.attachmentBar }}

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Picker, PickerProps } from "emoji-mart";
import { Picker, PickerProps, CustomEmoji } from "emoji-mart";
import "emoji-mart/css/emoji-mart.css";
interface IEmojiPickerProps extends PickerProps {

View File

@ -25,7 +25,8 @@ import {
RadioGroup,
Tooltip,
Typography,
withStyles
withStyles,
Zoom
} from "@material-ui/core";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import ReplyIcon from "@material-ui/icons/Reply";
@ -101,7 +102,7 @@ export class Post extends React.Component<any, IPostState> {
}
shouldComponentUpdate(nextProps: any, nextState: any) {
if (nextState === this.state) return false;
if (nextState == this.state) return false;
return true;
}
@ -405,7 +406,7 @@ export class Post extends React.Component<any, IPostState> {
let emojis = author.emojis;
let reblogger = post.reblog ? post.account : undefined;
if (reblogger !== undefined) {
if (reblogger != undefined) {
emojis.concat(reblogger.emojis);
}
@ -800,7 +801,7 @@ export class Post extends React.Component<any, IPostState> {
variant: "success"
}),
onShareError: (error: Error) => {
if (error.name !== "AbortError")
if (error.name != "AbortError")
this.props.enqueueSnackbar(
`Couldn't share post: ${error.name}`,
{ variant: "error" }

View File

@ -28,8 +28,6 @@ import CodeIcon from "@material-ui/icons/Code";
import TicketAccountIcon from "mdi-material-ui/TicketAccount";
import EditIcon from "@material-ui/icons/Edit";
import VpnKeyIcon from "@material-ui/icons/VpnKey";
import BugReportIcon from "@material-ui/icons/BugReport";
import ForumIcon from "@material-ui/icons/Forum";
import { styles } from "./PageLayout.styles";
import { Instance } from "../types/Instance";
@ -86,23 +84,13 @@ class AboutPage extends Component<any, IAboutPageState> {
let account = resp.data;
this.setState({
hyperspaceAdmin: account,
hyperspaceAdminName: config.admin.name
});
})
.catch((err: Error) => {
console.error(err.message);
if (true) {
this.setState({
hyperspaceAdminName: `Could not find ${config.admin.name} on ${config.registration.defaultInstance}`
});
}
})
.finally(() => {
this.setState({
hyperspaceAdminName: config.admin.name,
federation: config.federation,
developer: config.developer ?? false,
developer: config.developer ? config.developer : false,
versionNumber: config.version,
brandName: config.branding.name ?? "Hyperspace",
brandName: config.branding
? config.branding.name
: "Hyperspace",
brandBg: config.branding.background,
license: {
name: config.license.name,
@ -110,12 +98,19 @@ class AboutPage extends Component<any, IAboutPageState> {
},
repository: config.repository
});
})
.catch((err: Error) => {
console.error(err.message);
});
});
}
shouldRenderInstanceContact(): boolean {
return this.state.instance?.version?.match(/Pleroma/) == null ?? false;
if (this.state.instance != null) {
return this.state.instance.version.match(/Pleroma/) == null;
} else {
return false;
}
}
render() {
@ -126,8 +121,9 @@ class AboutPage extends Component<any, IAboutPageState> {
<div
className={classes.instanceHeaderPaper}
style={{
backgroundImage: `url("${this.state.brandBg ??
""}")`
backgroundImage: `url("${
this.state.brandBg ? this.state.brandBg : ""
}")`
}}
>
<div className={classes.instanceToolbar}>
@ -143,38 +139,20 @@ class AboutPage extends Component<any, IAboutPageState> {
</IconButton>
</Tooltip>
) : null}
<Tooltip title="Submit a bug report">
<IconButton
href={
"https://github.com/hyperspacedev/hyperspace/issues/new?assignees=&labels=&template=bug_report.md&title=%5BBug%5D+Issue+title"
}
target="_blank"
rel="noreferrer"
color="inherit"
>
<BugReportIcon />
</IconButton>
</Tooltip>
<Tooltip title="Request a feature">
<IconButton
href={
"https://github.com/hyperspacedev/hyperspace/issues/new?assignees=&labels=&template=feature_request.md&title=%5BRequest%5D+Request+title"
}
target="_blank"
rel="noreferrer"
color="inherit"
>
<ForumIcon />
</IconButton>
</Tooltip>
</div>
<div className={classes.instanceHeaderText}>
<Typography variant="h4" component="p">
{this.state.brandName ?? "Hyperspace Desktop"}
{this.state.brandName
? this.state.brandName
: "Hyperspace Desktop"}
</Typography>
<Typography>
Version{" "}
{`${this.state.versionNumber ?? "1.1.x"} ${
{`${
this.state
? this.state.versionNumber
: "1.0.x"
} ${
this.state &&
this.state.brandName !== "Hyperspace"
? "(Hyperspace-like)"
@ -186,24 +164,21 @@ class AboutPage extends Component<any, IAboutPageState> {
<List className={classes.pageListConstraints}>
<ListItem>
<ListItemAvatar>
{this.state.hyperspaceAdmin ? (
<LinkableAvatar
to={`/profile/${this.state
.hyperspaceAdmin?.id ?? 0}`}
src={
this.state.hyperspaceAdmin
?.avatar_static ?? ""
}
>
<PersonIcon />
</LinkableAvatar>
) : (
<ListItemAvatar>
<Avatar>
<PersonIcon />
</Avatar>
</ListItemAvatar>
)}
<LinkableAvatar
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
src={
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin
.avatar_static
: ""
}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary="App provider"
@ -214,34 +189,38 @@ class AboutPage extends Component<any, IAboutPageState> {
this.state.hyperspaceAdmin
.display_name ||
"@" + this.state.hyperspaceAdmin.acct
: this.state.hyperspaceAdminName ??
"No provider set in config"
: "No provider set in config"
}
/>
{this.state.hyperspaceAdmin ? (
<ListItemSecondaryAction>
<Tooltip title="Send a post or message">
<LinkableIconButton
to={`/compose?visibility=${
this.state.federated
? "public"
: "private"
}&acct=${this.state.hyperspaceAdmin
?.acct ?? ""}`}
>
<ChatIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${this.state
.hyperspaceAdmin?.id ?? 0}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
) : null}
<ListItemSecondaryAction>
<Tooltip title="Send a post or message">
<LinkableIconButton
to={`/compose?visibility=${
this.state.federated
? "public"
: "private"
}&acct=${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin
.acct
: ""
}`}
>
<ChatIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
@ -291,8 +270,12 @@ class AboutPage extends Component<any, IAboutPageState> {
<div
className={classes.instanceHeaderPaper}
style={{
backgroundImage: `url("${this.state.instance
?.thumbnail ?? ""}")`
backgroundImage: `url("${
this.state.instance &&
this.state.instance.thumbnail
? this.state.instance.thumbnail
: ""
}")`
}}
>
<IconButton
@ -306,11 +289,15 @@ class AboutPage extends Component<any, IAboutPageState> {
</IconButton>
<div className={classes.instanceHeaderText}>
<Typography variant="h4" component="p">
{this.state.instance?.uri ?? "Loading..."}
{this.state.instance
? this.state.instance.uri
: "Loading..."}
</Typography>
<Typography>
Server version{" "}
{this.state.instance?.version ?? "x.x.x"}
{this.state.instance
? this.state.instance.version
: "x.x.x"}
</Typography>
</div>
</div>
@ -358,8 +345,12 @@ class AboutPage extends Component<any, IAboutPageState> {
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${this.state.instance
?.contact_account.id ?? 0}`}
to={`/profile/${
this.state.instance
? this.state.instance
.contact_account.id
: 0
}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
@ -437,8 +428,8 @@ class AboutPage extends Component<any, IAboutPageState> {
secondary={
this.state.federation &&
this.state.federation.enablePublicTimeline
? "This copy of Hyperspace is federated."
: "This copy of Hyperspace is not federated."
? "This instance is federated."
: "This instance is not federated."
}
/>
</ListItem>
@ -453,8 +444,8 @@ class AboutPage extends Component<any, IAboutPageState> {
secondary={
this.state.federation &&
this.state.federation.universalLogin
? "This copy of Hyperspace supports universal login."
: "This copy of Hyperspace does not support universal login."
? "This instance supports universal login."
: "This instance does not support universal login."
}
/>
</ListItem>
@ -469,8 +460,8 @@ class AboutPage extends Component<any, IAboutPageState> {
secondary={
this.state.federation &&
this.state.federation.allowPublicPosts
? "This copy of Hyperspace allows posting publicly."
: "This copy of Hyperspace does not allow posting publicly."
? "This instance allows posting publicly."
: "This instance does not allow posting publicly."
}
/>
</ListItem>
@ -480,12 +471,12 @@ class AboutPage extends Component<any, IAboutPageState> {
<div className={classes.pageLayoutFooter}>
<Typography variant="caption">
(C) {new Date().getFullYear()}{" "}
{this.state.brandName ?? "Hyperspace"} developers. All
rights reserved.
{this.state ? this.state.brandName : "Hyperspace"}{" "}
developers. All rights reserved.
</Typography>
<Typography variant="caption" paragraph>
{this.state.brandName ?? "Hyperspace"} Desktop is made
possible by the{" "}
{this.state ? this.state.brandName : "Hyperspace"}{" "}
Desktop is made possible by the{" "}
<Link
href={"https://material-ui.com"}
target="_blank"

View File

@ -1,211 +0,0 @@
import React, { Component } from "react";
import {
ListSubheader,
withStyles,
Typography,
CircularProgress,
Card,
CardContent,
Paper,
CardHeader,
Avatar
} from "@material-ui/core";
import { styles } from "./PageLayout.styles";
import AnnouncementIcon from "@material-ui/icons/Announcement";
import Mastodon from "megalodon";
import { Announcement } from "../types/Announcement";
import { withSnackbar } from "notistack";
import moment from "moment";
/**
* The state interface for the notifications page.
*/
interface IAnnouncementsPageState {
/**
* The list of notifications, if it exists.
*/
announcements?: [Announcement];
/**
* Whether the view is still loading.
*/
viewIsLoading: boolean;
/**
* Whether the view has loaded.
*/
viewDidLoad?: boolean;
/**
* Whether the view has loaded but in error.
*/
viewDidError?: boolean;
/**
* The error code for an errored state, if possible.
*/
viewDidErrorCode?: string;
}
/**
* The notifications page.
*/
class AnnouncementsPage extends Component<any, IAnnouncementsPageState> {
/**
* The Mastodon object to perform notification operations on.
*/
client: Mastodon;
/**
* The stream listener for tuning in to notifications.
*/
streamListener: any;
/**
* Construct the notifications page.
* @param props The properties to pass in
*/
constructor(props: any) {
super(props);
// Create the Mastodon object.
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1"
);
// Initialize the state.
this.state = {
viewIsLoading: true
};
}
/**
* Perform pre-mount tasks
*/
async componentWillMount() {
try {
// Get the list of notifications
let resp: any = await this.client.get("/announcements");
let announcements: [Announcement] = resp.data;
this.setState({
announcements,
viewIsLoading: false,
viewDidLoad: true
});
} catch (e) {
this.setState({
viewDidLoad: true,
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: e.message
});
}
}
/**
* Render the announcements page.
*/
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? (
this.state.announcements &&
this.state.announcements.length > 0 ? (
<div>
<ListSubheader>Current announcements</ListSubheader>
{this.state.announcements.map(
(announcement: Announcement) => {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<AnnouncementIcon />
</Avatar>
}
title={`Published on ${moment(
announcement.published_at
).format(
"MMMM Do, YYYY [at] hh:mmA"
)}`}
subheader={
announcement.ends_at
? `Expires ${moment(
announcement.ends_at
).format(
"MMMM Do, YYYY"
)}`
: ""
}
></CardHeader>
<CardContent>
<Typography
dangerouslySetInnerHTML={{
__html:
announcement.content
}}
></Typography>
</CardContent>
</Card>
);
}
)}
</div>
) : (
<div
className={classes.pageLayoutEmptyTextConstraints}
style={{ textAlign: "center" }}
>
<AnnouncementIcon
color="action"
style={{ fontSize: 48 }}
/>
<Typography variant="h6">
No server announcements
</Typography>
<Typography paragraph>
There aren't any announcements in your
community. Announcements that use the
announcement feature on Mastodon will appear
here.
</Typography>
<br />
</div>
)
) : null}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading announcements.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div>
);
}
}
export default withStyles(styles)(withSnackbar(AnnouncementsPage));

View File

@ -139,7 +139,7 @@ class Blocked extends Component<any, IBlockedState> {
variant="outlined"
fullWidth
value={this.state.blockTextField}
placeholder="mastodon.online"
placeholder="mastodon.social"
onChange={e => this.updateTextField(e.target.value)}
></TextField>
</DialogContent>

View File

@ -193,9 +193,9 @@ class Composer extends Component<any, IComposerState> {
let fileList: File[] = [];
if (thePasteEvent.clipboardData != null) {
let clipitems = thePasteEvent.clipboardData.items;
if (clipitems !== undefined) {
if (clipitems != undefined) {
for (let i = 0; i < clipitems.length; i++) {
if (clipitems[i].type.indexOf("image") !== -1) {
if (clipitems[i].type.indexOf("image") != -1) {
let clipfile = clipitems[i].getAsFile();
if (clipfile != null) {
fileList.push(clipfile);
@ -395,9 +395,9 @@ class Composer extends Component<any, IComposerState> {
getOnlyMediaIds() {
let ids: string[] = [];
if (this.state.attachments) {
return this.state.attachments.map(
(attachment: Attachment) => attachment.id
);
this.state.attachments.map((attachment: Attachment) => {
ids.push(attachment.id);
});
}
return ids;
}
@ -494,7 +494,7 @@ class Composer extends Component<any, IComposerState> {
this.setState({
poll: poll
});
} else if (this.state.poll && this.state.poll.options.length === 4) {
} else if (this.state.poll && this.state.poll.options.length == 4) {
this.props.enqueueSnackbar(
"You've reached the options limit in your poll.",
{ variant: "error" }

View File

@ -8,6 +8,7 @@ import {
ListItemText,
CircularProgress,
ListItemAvatar,
Avatar,
ListItemSecondaryAction,
Tooltip,
Typography

View File

@ -1,6 +1,5 @@
import React, { Component } from "react";
import {
Link,
List,
ListItem,
ListItemText,
@ -45,7 +44,6 @@ import { Account } from "../types/Account";
import { Relationship } from "../types/Relationship";
import { withSnackbar } from "notistack";
import { Dictionary } from "../interfaces/utils";
import { linkablePath } from "../utilities/desktop";
/**
* The state interface for the notifications page.
@ -424,7 +422,7 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
)}
onClose={() => this.toggleMobileMenu(notif.id)}
>
{notif.type === "follow" ? (
{notif.type == "follow" ? (
<>
<LinkableMenuItem
to={`profile/${notif.account.id}`}
@ -441,7 +439,7 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
</MenuItem>
</>
) : null}
{notif.type === "mention" && notif.status ? (
{notif.type == "mention" && notif.status ? (
<LinkableMenuItem
to={`/compose?reply=${
notif.status.reblog
@ -612,19 +610,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
<span />
)}
<div
className={classes.pageLayoutEmptyTextConstraints}
style={{ textAlign: "center" }}
>
<Typography>
<Link
href={linkablePath("/#/settings#sp-notifications")}
>
Manage notification settings
</Link>
</Typography>
</div>
<Dialog
open={this.state.deleteDialogOpen}
onClose={() => this.toggleDeleteDialog()}

View File

@ -1,4 +1,4 @@
import { Theme, createStyles } from "@material-ui/core";
import { Theme, createStyles, FormHelperText } from "@material-ui/core";
import { isDarwinApp } from "../utilities/desktop";
import { isAppbarExpanded } from "../utilities/appbar";

View File

@ -3,6 +3,7 @@ import {
withStyles,
Typography,
Avatar,
Divider,
Button,
CircularProgress,
Paper,

View File

@ -6,6 +6,7 @@ import {
ListSubheader,
ListItemSecondaryAction,
ListItemAvatar,
Avatar,
Paper,
withStyles,
Typography,
@ -13,13 +14,13 @@ import {
Tooltip,
IconButton
} from "@material-ui/core";
import PersonIcon from "@material-ui/icons/Person";
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
import PersonAddIcon from "@material-ui/icons/PersonAdd";
import { styles } from "./PageLayout.styles";
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
import Mastodon from "megalodon";
import { parse as parseParams } from "query-string";
import { parse as parseParams, ParsedQuery } from "query-string";
import { Results } from "../types/Search";
import { withSnackbar } from "notistack";
import Post from "../components/Post";
@ -84,23 +85,35 @@ class SearchPage extends Component<any, ISearchPageState> {
}
}
runQueryCheck(newLocation?: string): ParsedQuery {
let searchParams = "";
if (newLocation !== undefined && typeof newLocation === "string") {
searchParams = newLocation.replace("#/search", "");
} else {
searchParams = location.hash.replace("#/search", "");
}
return parseParams(searchParams);
}
getQueryAndType(props: any) {
const { search }: { search: string } = props.location;
let newSearch = parseParams(search);
let query: string | string[] = "";
let newSearch = this.runQueryCheck(props.location);
let query: string | string[];
let type;
if (newSearch.query) {
if (search.includes("tag:")) {
if (newSearch.query.toString().startsWith("tag:")) {
type = "tag";
query = newSearch.query.toString().replace("tag:", "");
} else {
query = newSearch.query;
}
query = newSearch.query.toString().replace("tag:", "");
} else {
query = "";
}
if (newSearch.type && newSearch.type !== undefined) {
type = newSearch.type;
}
return {
query: query,
type: type
@ -143,11 +156,14 @@ class SearchPage extends Component<any, ISearchPageState> {
let tagResults: [Status] = resp.data;
this.setState({
tagResults,
viewDidLoad: true
viewDidLoad: true,
viewIsLoading: false
});
// console.log(this.state.tagResults);
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
@ -156,9 +172,6 @@ class SearchPage extends Component<any, ISearchPageState> {
`Couldn't search for posts with tag ${this.state.query}: ${err.name}`,
{ variant: "error" }
);
})
.finally(() => {
this.setState({ viewIsLoading: false });
});
}

View File

@ -38,15 +38,19 @@ import {
} from "../utilities/settings";
import {
canSendNotifications,
browserSupportsNotificationRequests,
getNotificationRequestPermission
browserSupportsNotificationRequests
} from "../utilities/notifications";
import { themes, defaultTheme } from "../types/HyperspaceTheme";
import ThemePreview from "../components/ThemePreview";
import { setHyperspaceTheme, getHyperspaceTheme } from "../utilities/themes";
import {
setHyperspaceTheme,
getHyperspaceTheme,
getDarkModeFromSystem
} from "../utilities/themes";
import { Visibility } from "../types/Visibility";
import { LinkableIconButton } from "../interfaces/overrides";
import { LinkableButton, LinkableIconButton } from "../interfaces/overrides";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import DevicesIcon from "@material-ui/icons/Devices";
import Brightness3Icon from "@material-ui/icons/Brightness3";
import PaletteIcon from "@material-ui/icons/Palette";
@ -66,7 +70,7 @@ import InfiniteIcon from "@material-ui/icons/AllInclusive";
import { Config } from "../types/Config";
import { Account } from "../types/Account";
import Mastodon from "megalodon";
import { withSnackbar } from "notistack";
import { isDarwinApp } from "../utilities/desktop";
interface ISettingsState {
darkModeEnabled: boolean;
@ -135,15 +139,6 @@ class SettingsPage extends Component<any, ISettingsState> {
this.setVisibility = this.setVisibility.bind(this);
}
componentWillReceiveProps() {
const path = window.location.hash.split("#");
const lastPath = document.getElementById(path[path.length - 1]);
if (lastPath !== null) {
lastPath.scrollIntoView();
window.scrollBy(0, -64);
}
}
componentDidMount() {
getConfig()
.then((config: any) => {
@ -173,13 +168,6 @@ class SettingsPage extends Component<any, ISettingsState> {
console.error(err.message);
}
});
const path = window.location.hash.split("#");
const lastPath = document.getElementById(path[path.length - 1]);
if (lastPath !== null) {
lastPath.scrollIntoView();
window.scrollBy(0, -64);
}
}
getFederatedStatus() {
@ -211,47 +199,14 @@ class SettingsPage extends Component<any, ISettingsState> {
window.location.reload();
}
/**
* Toggle the setting for enabling/disabling push notifications.
*
* If the notification permission wasn't set yet (i.e., `Notification.permission`)
* is in `"default"` state, get the permission request first.
*/
togglePushNotifications() {
if (!browserSupportsNotificationRequests()) {
return;
}
if (Notification.permission === "default") {
getNotificationRequestPermission()
.then(permission => {
if (permission === "granted") {
setUserDefaultBool(
"enablePushNotifications",
!this.state.pushNotificationsEnabled
);
this.setState({
pushNotificationsEnabled: !this.state
.pushNotificationsEnabled
});
} else if (permission === "denied") {
this.props.enqueueSnackbar(
"Permission request was denied.",
{ variant: "error" }
);
}
})
.catch(reason =>
this.props.enqueueSnackbar(reason, { variant: "error" })
);
} else {
setUserDefaultBool(
"enablePushNotifications",
!this.state.pushNotificationsEnabled
);
this.setState({
pushNotificationsEnabled: !this.state.pushNotificationsEnabled
});
}
this.setState({
pushNotificationsEnabled: !this.state.pushNotificationsEnabled
});
setUserDefaultBool(
"enablePushNotifications",
!this.state.pushNotificationsEnabled
);
}
toggleBadgeCount() {
@ -343,227 +298,6 @@ class SettingsPage extends Component<any, ISettingsState> {
window.location.reload();
}
settingsList = () => {
const { classes } = this.props;
return (
<>
<ListSubheader id="sp-appearance">Appearance</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<DevicesIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Match system appearance"
secondary="Follows your device's preferences to toggle dark mode"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.systemDecidesDarkMode}
onChange={this.toggleSystemDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
{!this.state.systemDecidesDarkMode ? (
<ListItem>
<ListItemAvatar>
<Brightness3Icon color="action" />
</ListItemAvatar>
<ListItemText
primary="Dark mode"
secondary="Toggles light or dark theme"
/>
<ListItemSecondaryAction>
<Switch
disabled={
this.state.systemDecidesDarkMode
}
checked={this.state.darkModeEnabled}
onChange={this.toggleDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
) : null}
<ListItem>
<ListItemAvatar>
<PaletteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Interface theme"
secondary="Defines the color palette used for the interface"
/>
<ListItemSecondaryAction>
<Button onClick={this.toggleThemeDialog}>
Set theme
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<DashboardIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Show more posts"
secondary="Shows additional columns of posts on wider screens"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.masonryLayout}
onChange={this.toggleMasonryLayout}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<InfiniteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable infinite scroll"
secondary="Automatically load more posts when scrolling"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.infiniteScroll}
onChange={this.toggleInfiniteScroll}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader id="sp-composer">Composer</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<VisibilityIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Default post visibility"
secondary="Creating posts in the composer will use this visiblity"
/>
<ListItemSecondaryAction>
<Button onClick={this.toggleVisibilityDialog}>
Change
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<AlphabeticalVariantOffIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Impose character limit"
secondary="Impose a character limit when creating posts"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.imposeCharacterLimit}
onChange={() => this.toggleCharacterLimit()}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader id="sp-notifications">
Notifications
</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<NotificationsIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable push notifications"
secondary={
getUserDefaultBool("userDeniedNotification")
? "Check your browser's notification permissions."
: browserSupportsNotificationRequests()
? "Sends a push notification when not focused."
: "Notifications aren't supported."
}
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state.pushNotificationsEnabled
}
onChange={this.togglePushNotifications}
disabled={
!browserSupportsNotificationRequests()
}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<BellAlertIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Notification badge counts all notifications"
secondary={
"Counts all notifications, read or unread."
}
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.badgeDisplaysAllNotifs}
onChange={this.toggleBadgeCount}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader id="sp-advanced">Advanced</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<RefreshIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Refresh settings"
secondary="Resets the settings to defaults."
/>
<ListItemSecondaryAction>
<Button
onClick={() =>
this.toggleResetSettingsDialog()
}
>
Refresh
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<UndoIcon color="action" />
</ListItemAvatar>
<ListItemText
primary={`Reset ${this.state.brandName}`}
secondary="Deletes all data and resets the app"
/>
<ListItemSecondaryAction>
<Button
onClick={() => this.toggleResetDialog()}
>
Reset
</Button>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
</>
);
};
showThemeDialog() {
const { classes } = this.props;
return (
@ -597,6 +331,7 @@ class SettingsPage extends Component<any, ISettingsState> {
label={theme.name}
/>
))}
))}
</RadioGroup>
</Grid>
<Grid
@ -873,7 +608,245 @@ class SettingsPage extends Component<any, ISettingsState> {
</div>
)}
<div className={classes.pageContentLayoutConstraints}>
{this.settingsList()}
<ListSubheader>Appearance</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<DevicesIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Match system appearance"
secondary="Follows your device's preferences to toggle dark mode"
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state.systemDecidesDarkMode
}
onChange={this.toggleSystemDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
{!this.state.systemDecidesDarkMode ? (
<ListItem>
<ListItemAvatar>
<Brightness3Icon color="action" />
</ListItemAvatar>
<ListItemText
primary="Dark mode"
secondary="Toggles light or dark theme"
/>
<ListItemSecondaryAction>
<Switch
disabled={
this.state
.systemDecidesDarkMode
}
checked={
this.state.darkModeEnabled
}
onChange={this.toggleDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
) : null}
<ListItem>
<ListItemAvatar>
<PaletteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Interface theme"
secondary="Defines the color palette used for the interface"
/>
<ListItemSecondaryAction>
<Button
onClick={this.toggleThemeDialog}
>
Set theme
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<DashboardIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Show more posts"
secondary="Shows additional columns of posts on wider screens"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.masonryLayout}
onChange={this.toggleMasonryLayout}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<InfiniteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable infinite scroll"
secondary="Automatically load more posts when scrolling"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.infiniteScroll}
onChange={this.toggleInfiniteScroll}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Composer</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<VisibilityIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Default post visibility"
secondary="Creating posts in the composer will use this visiblity"
/>
<ListItemSecondaryAction>
<Button
onClick={
this.toggleVisibilityDialog
}
>
Change
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<AlphabeticalVariantOffIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Impose character limit"
secondary="Impose a character limit when creating posts"
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state.imposeCharacterLimit
}
onChange={() =>
this.toggleCharacterLimit()
}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Notifications</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<NotificationsIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable push notifications"
secondary={
getUserDefaultBool(
"userDeniedNotification"
)
? "Check your browser's notification permissions."
: browserSupportsNotificationRequests()
? "Sends a push notification when not focused."
: "Notifications aren't supported."
}
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state
.pushNotificationsEnabled
}
onChange={
this.togglePushNotifications
}
disabled={
!browserSupportsNotificationRequests() ||
getUserDefaultBool(
"userDeniedNotification"
)
}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<BellAlertIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Notification badge counts all notifications"
secondary={
"Counts all notifications, read or unread."
}
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state
.badgeDisplaysAllNotifs
}
onChange={this.toggleBadgeCount}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Advanced</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<RefreshIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Refresh settings"
secondary="Resets the settings to defaults."
/>
<ListItemSecondaryAction>
<Button
onClick={() =>
this.toggleResetSettingsDialog()
}
>
Refresh
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<UndoIcon color="action" />
</ListItemAvatar>
<ListItemText
primary={`Reset ${this.state.brandName}`}
secondary="Deletes all data and resets the app"
/>
<ListItemSecondaryAction>
<Button
onClick={() =>
this.toggleResetDialog()
}
>
Reset
</Button>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
{this.showThemeDialog()}
{this.showVisibilityDialog()}
{this.showResetDialog()}
@ -885,4 +858,4 @@ class SettingsPage extends Component<any, ISettingsState> {
}
}
export default withStyles(styles)(withSnackbar(SettingsPage));
export default withStyles(styles)(SettingsPage);

View File

@ -18,8 +18,7 @@ import {
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
IconButton,
InputAdornment
IconButton
} from "@material-ui/core";
import { styles } from "./WelcomePage.styles";
import Mastodon from "megalodon";
@ -37,11 +36,12 @@ import axios from "axios";
import { withSnackbar, withSnackbarProps } from "notistack";
import { Config } from "../types/Config";
import {
addAccountToRegistry,
getAccountRegistry,
loginWithAccount,
removeAccountFromRegistry
} from "../utilities/accounts";
import { MultiAccount } from "../types/Account";
import { Account, MultiAccount } from "../types/Account";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import CloseIcon from "@material-ui/icons/Close";
@ -220,39 +220,45 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
let config: Config = result;
// Warn if the location is dynamic (unexpected behavior)
if (config.location === "dynamic") {
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.online if the location is a disallowed
// Reset to mastodon.social if the location is a disallowed
// domain.
if (
inDisallowedDomains(result.registration.defaultInstance)
) {
console.warn(
`The default instance field in config.json contains an unsupported domain (${result.registration.defaultInstance}), so it's been reset to mastodon.online.`
`The default instance field in config.json contains an unsupported domain (${result.registration.defaultInstance}), so it's been reset to mastodon.social.`
);
result.registration.defaultInstance = "mastodon.online";
result.registration.defaultInstance = "mastodon.social";
}
// Update the state as per the configuration
this.setState({
logoUrl: config.branding?.logo ?? "logo.png",
backgroundUrl:
config.branding?.background ?? "background.png",
brandName: config.branding?.name ?? "Hyperspace",
registerBase:
result.registration?.defaultInstance ?? "",
logoUrl: config.branding
? result.branding.logo
: "logo.png",
backgroundUrl: config.branding
? result.branding.background
: "background.png",
brandName: config.branding
? result.branding.name
: "Hyperspace",
registerBase: config.registration
? result.registration.defaultInstance
: "",
federates: config.federation.universalLogin,
license: config.license.url,
repo: config.repository,
defaultRedirectAddress:
config.location !== "dynamic"
config.location != "dynamic"
? config.location
: `https://${window.location.host}`,
redirectAddressIsDynamic: config.location === "dynamic",
redirectAddressIsDynamic: config.location == "dynamic",
version: config.version
});
}
@ -285,7 +291,6 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
* @param user The string to update the state to
*/
updateUserInfo(user: string) {
this.checkForErrors(user);
this.setState({ user });
}
@ -309,7 +314,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
* process.
*/
readyForAuth() {
return localStorage.getItem("baseurl") !== null;
if (localStorage.getItem("baseurl")) {
return true;
} else {
return false;
}
}
/**
@ -325,17 +334,16 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
* attempt and update the state
*/
getSavedSession() {
if (localStorage.getItem("login") === null) {
return;
let loginData = localStorage.getItem("login");
if (loginData) {
let session: SaveClientSession = JSON.parse(loginData);
this.setState({
clientId: session.clientId,
clientSecret: session.clientSecret,
authUrl: session.authUrl,
emergencyMode: session.emergency
});
}
let loginData = localStorage.getItem("login") as string;
let session: SaveClientSession = JSON.parse(loginData);
this.setState({
clientId: session.clientId,
clientSecret: session.clientSecret,
authUrl: session.authUrl,
emergencyMode: session.emergency
});
}
/**
@ -354,9 +362,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
* in the config's `registerBase` field
*/
startRegistration() {
return this.state.registerBase
? "https://" + this.state.registerBase + "/auth/sign_up"
: "https://joinmastodon.org/#getting-started";
if (this.state.registerBase) {
return "https://" + this.state.registerBase + "/auth/sign_up";
} else {
return "https://joinmastodon.org/#getting-started";
}
}
/**
@ -392,32 +402,45 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
this.setState({ user: newUser });
return "https://" + newUser.split("@")[1];
} else {
let newUser = `${user}@${this.state.registerBase ??
"mastodon.online"}`;
let newUser = `${user}@${
this.state.registerBase
? this.state.registerBase
: "mastodon.social"
}`;
this.setState({ user: newUser });
return (
"https://" + (this.state.registerBase ?? "mastodon.online")
"https://" +
(this.state.registerBase
? this.state.registerBase
: "mastodon.social")
);
}
}
// Otherwise, treat them as if they're from the server
else {
let newUser = `${user}@${this.state.registerBase ??
"mastodon.online"}`;
let newUser = `${user}@${
this.state.registerBase
? this.state.registerBase
: "mastodon.social"
}`;
this.setState({ user: newUser });
return "https://" + (this.state.registerBase ?? "mastodon.online");
return (
"https://" +
(this.state.registerBase
? this.state.registerBase
: "mastodon.social")
);
}
}
/**
* Check the user string for any errors and then create a client with an
* ID and secret to start the authorization process.
* @param bypassChecks Whether to bypass the checks in place.
*/
startLogin(bypassChecks: boolean = false) {
startLogin() {
// Check if we have errored
let error = this.checkForErrors(this.state.user, bypassChecks);
let error = this.checkForErrors();
// If we didn't, create the Hyperspace app to register onto that Mastodon
// server.
@ -429,7 +452,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
// Create the Hyperspace app
createHyperspaceApp(
this.state.brandName ?? "Hyperspace",
this.state.brandName ? this.state.brandName : "Hyperspace",
scopes,
baseurl,
getRedirectAddress(this.state.defaultRedirectAddress)
@ -454,15 +477,6 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
authUrl: resp.url,
proceedToGetCode: true
});
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
`Failed to register app at ${baseurl.replace(
"https://",
""
)}`
);
console.error(err);
});
}
}
@ -483,7 +497,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
// Register the Mastodon app with the Mastodon server
Mastodon.registerApp(
this.state.brandName ?? "Hyperspace",
this.state.brandName ? this.state.brandName : "Hyperspace",
{
scopes: scopes
},
@ -521,7 +535,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
authorizeEmergencyLogin() {
let redirAddress =
this.state.defaultRedirectAddress === "desktop"
? "hyperspace://hyperspace/app/"
? "hyperspace://hyperspace/app"
: this.state.defaultRedirectAddress;
window.location.href = `${redirAddress}/?code=${this.state.authCode}#/`;
}
@ -545,27 +559,21 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
/**
* Check the user input string for any possible errors
* @param username The username to read and check for errors
* @param bypassesInstanceNameCheck Whether to bypass the instance name validation process. Defaults to false.
* @return Whether an error has occured in the validation.
*/
checkForErrors(
username: string,
bypassesInstanceNameCheck: boolean = false
): boolean {
checkForErrors(): boolean {
let userInputError = false;
let userInputErrorMessage = "";
// Is the user string blank?
if (username === "") {
if (this.state.user === "") {
userInputError = true;
userInputErrorMessage = "Username cannot be blank.";
this.setState({ userInputError, userInputErrorMessage });
return true;
} else {
if (username.includes("@")) {
if (this.state.user.includes("@")) {
if (this.state.federates && this.state.federates === true) {
let baseUrl = username.split("@")[1];
let baseUrl = this.state.user.split("@")[1];
// Is the user's domain in the disallowed list?
if (inDisallowedDomains(baseUrl)) {
@ -575,9 +583,6 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
});
return true;
} else {
if (bypassesInstanceNameCheck) {
return false;
}
// Are we unable to ping the server?
axios
.get(
@ -592,7 +597,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
.catch((err: Error) => {
let userInputError = true;
let userInputErrorMessage =
"We couldn't recognize this instance.";
"Instance name is invalid.";
this.setState({
userInputError,
userInputErrorMessage
@ -601,8 +606,10 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
});
}
} else if (
username.includes(
this.state.registerBase ?? "mastodon.online"
this.state.user.includes(
this.state.registerBase
? this.state.registerBase
: "mastodon.social"
)
) {
this.setState({ userInputError, userInputErrorMessage });
@ -644,7 +651,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
);
getConfig().then((resp: any) => {
if (resp === undefined) {
if (resp == undefined) {
return;
}
@ -677,8 +684,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
`Couldn't authorize ${this.state.brandName ??
"Hyperspace"}: ${err.name}`,
`Couldn't authorize ${
this.state.brandName
? this.state.brandName
: "Hyperspace"
}: ${err.name}`,
{ variant: "error" }
);
console.error(err.message);
@ -694,7 +704,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
redirectToApp() {
window.location.href =
window.location.protocol === "hyperspace:"
? "hyperspace://hyperspace/app/"
? "hyperspace://hyperspace/app"
: this.state.redirectAddressIsDynamic
? `https://${window.location.host}/#/`
: this.state.defaultRedirectAddress + "/#/";
@ -709,7 +719,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
return (
<div className={classes.titleBarRoot}>
<Typography className={classes.titleBarText}>
{this.state.brandName ?? "Hyperspace"}
{this.state.brandName
? this.state.brandName
: "Hyperspace"}
</Typography>
</div>
);
@ -782,7 +794,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
return (
<div>
<Typography variant="h5">Sign in</Typography>
<Typography>with your fediverse account</Typography>
<Typography>with your Mastodon account</Typography>
<div className={classes.middlePadding} />
<TextField
variant="outlined"
@ -792,37 +804,25 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
onChange={event => this.updateUserInfo(event.target.value)}
onKeyDown={event => this.watchUsernameField(event)}
error={this.state.userInputError}
InputProps={{
startAdornment: (
<InputAdornment position="start">@</InputAdornment>
)
}}
onBlur={() => this.checkForErrors()}
/>
{this.state.userInputError ? (
<Typography color="error">
{this.state.userInputErrorMessage}
{this.state.userInputErrorMessage ===
"We couldn't recognize this instance." ? (
<span>
<br />
<Link
// className={classes.welcomeLink}
onClick={() => this.startLogin(true)}
>
Try anyway
</Link>
</span>
) : null}
</Typography>
) : null}
<br />
{this.state.registerBase && this.state.federates ? (
<Typography variant="caption">
Not from{" "}
<b>{this.state.registerBase ?? "noinstance"}</b>? Sign
in with your{" "}
<b>
{this.state.registerBase
? this.state.registerBase
: "noinstance"}
</b>
? Sign in with your{" "}
<Link
href="https://docs.joinmastodon.org/user/signup/#address"
href="https://docs.joinmastodon.org/usage/decentralization/#addressing-people"
target="_blank"
rel="noopener noreferrer"
color="secondary"
@ -880,11 +880,14 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
return (
<div>
<Typography variant="h5">
Howdy, {this.state.user?.split("@")[0] ?? "user"}
Howdy,{" "}
{this.state.user ? this.state.user.split("@")[0] : "user"}
</Typography>
<Typography>
To continue, finish signing in on your instance's website
and authorize {this.state.brandName ?? "Hyperspace"}.
and authorize{" "}
{this.state.brandName ? this.state.brandName : "Hyperspace"}
.
</Typography>
<div className={classes.middlePadding} />
<div style={{ display: "flex" }}>
@ -893,7 +896,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
color="primary"
variant="contained"
size="large"
href={this.state.authUrl ?? ""}
href={this.state.authUrl ? this.state.authUrl : ""}
>
Authorize
</Button>
@ -917,6 +920,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
* Show the emergency login panel
*/
showAuthDialog() {
const { classes } = this.props;
return (
<Dialog
open={this.state.openAuthDialog}
@ -936,7 +940,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
<Button
color="primary"
variant="contained"
href={this.state.authUrl ?? ""}
href={this.state.authUrl ? this.state.authUrl : ""}
target="_blank"
rel="noopener noreferrer"
>
@ -978,8 +982,8 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
<div>
<Typography variant="h5">Authorizing</Typography>
<Typography>
Please wait while Hyperspace authorizes with your instance.
This shouldn't take long...
Please wait while Hyperspace authorizes with Mastodon. This
shouldn't take long...
</Typography>
<div className={classes.middlePadding} />
<div style={{ display: "flex" }}>
@ -1003,15 +1007,20 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
<div
className={classes.root}
style={{
backgroundImage: `url(${this.state.backgroundUrl ??
"background.png"})`
backgroundImage: `url(${
this.state !== null
? this.state.backgroundUrl
: "background.png"
})`
}}
>
<Paper className={classes.paper}>
<img
className={classes.logo}
alt={this.state.brandName ?? "Hyperspace"}
src={this.state.logoUrl ?? "logo.png"}
alt={
this.state ? this.state.brandName : "Hyperspace"
}
src={this.state ? this.state.logoUrl : "logo.png"}
/>
<br />
<Fade in={true}>
@ -1047,8 +1056,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
<Link
className={classes.welcomeLink}
href={
this.state.repo ??
"https://github.com/hyperspacedev"
this.state.repo
? this.state.repo
: "https://github.com/hyperspacedev"
}
target="_blank"
rel="noreferrer"
@ -1061,15 +1071,16 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
<Link
className={classes.welcomeLink}
href={
this.state.license ??
"https://thufie.lain.haus/NPL.html"
this.state.license
? this.state.license
: "https://www.apache.org/licenses/LICENSE-2.0"
}
target="_blank"
rel="noreferrer"
>
License
</Link>{" "}
|{" "}
|
<Link
className={classes.welcomeLink}
href="https://github.com/hyperspacedev/hyperspace/issues/new"
@ -1080,7 +1091,10 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
</Link>
</Typography>
<Typography variant="caption" color="textSecondary">
{this.state.brandName ?? "Hypersapce"} v.
{this.state.brandName
? this.state.brandName
: "Hypersapce"}{" "}
v.
{this.state.version}{" "}
{this.state.brandName &&
this.state.brandName !== "Hyperspace"

View File

@ -16,7 +16,6 @@ import filedialog from "file-dialog";
interface IYouProps extends withSnackbarProps {
classes: any;
onAvatarUpdate: Function;
}
interface IYouState {
@ -107,9 +106,6 @@ class You extends Component<IYouProps, IYouState> {
this.props.enqueueSnackbar(
"Avatar updated successfully."
);
this.props.onAvatarUpdate(
currentAccount.avatar_static
);
})
.catch((err: Error) => {
this.props.closeSnackbar("persistAvatar");

View File

@ -42,7 +42,7 @@ export type UAccount = {
*/
export type MultiAccount = {
/**
* The host name of the account (ex.: mastodon.online)
* The host name of the account (ex.: mastodon.social)
*/
host: string;

View File

@ -1,17 +0,0 @@
import { Account } from "./Account";
import { Tag } from "./Tag";
import { MastodonEmoji } from "./Emojis";
export type Announcement = {
id: string;
content: string;
starts_at?: string;
ends_at?: string;
all_day: boolean;
published_at: string;
updated_at: string;
read: boolean;
mentions: [Account];
tags: [Tag];
emojis: [MastodonEmoji];
};

View File

@ -27,7 +27,7 @@ export function isDarkMode() {
// Lift window to an ElectronWindow and add use require()
const eWin = window as ElectronWindow;
const { remote } = eWin.require("electron");
return remote.nativeTheme.shouldUseDarkColors;
return remote.systemPreferences.isDarkMode();
}
/**
@ -56,11 +56,3 @@ export function getElectronApp() {
const { remote } = eWin.require("electron");
return remote.app;
}
/**
* Get the linkable version of a path for the web and desktop.
* @param path The path to make a linkable version of
*/
export function linkablePath(path: string): string {
return isDesktopApp() ? "/app" + path : path;
}

View File

@ -2,28 +2,23 @@ import { getUserDefaultBool, setUserDefaultBool } from "./settings";
/**
* Get the person's permission to send notification requests.
*
* @returns Promise containing the notification permission, or a rejection if
* either the browser doesn't support notifications.
*/
export function getNotificationRequestPermission() {
if ("Notification" in window) {
Notification.requestPermission().then(request => {
setUserDefaultBool(
"enablePushNotifications",
request === "granted"
);
setUserDefaultBool("userDeniedNotification", request === "denied");
});
return Promise.resolve(Notification.permission);
Notification.requestPermission();
let request = Notification.permission;
if (request === "granted") {
setUserDefaultBool("enablePushNotifications", true);
setUserDefaultBool("userDeniedNotification", false);
} else {
setUserDefaultBool("enablePushNotifications", false);
setUserDefaultBool("userDeniedNotification", true);
}
} else {
console.warn(
"Notifications aren't supported in this browser. The setting will be disabled."
);
setUserDefaultBool("enablePushNotifications", false);
return Promise.reject(
"Notifications are not supported in this browser."
);
}
}
@ -40,10 +35,7 @@ export function browserSupportsNotificationRequests(): boolean {
* @returns Boolean value of `enablePushNotifications`
*/
export function canSendNotifications() {
return (
getUserDefaultBool("enablePushNotifications") &&
Notification.permission === "granted"
);
return getUserDefaultBool("enablePushNotifications");
}
/**

View File

@ -1,4 +1,5 @@
import { defaultTheme, themes } from "../types/HyperspaceTheme";
import { getNotificationRequestPermission } from "./notifications";
import axios from "axios";
import { Config } from "../types/Config";
import { Visibility } from "../types/Visibility";
@ -12,7 +13,6 @@ type SettingsTemplate = {
displayAllOnNotificationBadge: boolean;
defaultVisibility: string;
imposeCharacterLimit: boolean;
canSendNotifications: boolean;
};
/**
@ -102,8 +102,7 @@ export function createUserDefaults() {
displayAllOnNotificationBadge: false,
defaultVisibility: "public",
imposeCharacterLimit: true,
isMasonryLayout: false,
canSendNotifications: false
isMasonryLayout: false
};
let settings = [
@ -113,8 +112,7 @@ export function createUserDefaults() {
"displayAllOnNotificationBadge",
"defaultVisibility",
"imposeCharacterLimit",
"isMasonryLayout",
"canSendNotifications"
"isMasonryLayout"
];
migrateExistingSettings();
@ -128,31 +126,22 @@ export function createUserDefaults() {
}
}
});
setUserDefaultBool("userDeniedNotications", false);
getNotificationRequestPermission();
}
/**
* Gets the configuration data from `config.json`.
*
* In scenarios where the app is being run from the desktop or from a local React server
* started by react-scripts, the location field is adjusted accordingly.
*
* Gets the configuration data from `config.json`
* @returns The Promise data from getting the config.
*/
export async function getConfig(): Promise<Config | undefined> {
try {
const resp = await axios.get("config.json");
let { location }: { location: string } = resp.data;
let { location } = resp.data;
if (
!location.endsWith("/") &&
location !== "desktop" &&
location !== "dynamic"
) {
if (!location.endsWith("/")) {
console.info(
"Location does not have a forward slash, so Hyperspace has added it automatically."
"Location does not have a backslash, so Hyperspace has added it automatically."
);
resp.data.location = location + "/";
}