Revert "Merge pull request #173 from hyperspacedev/develop-1.1.0-feed-bug-masonry"
This reverts commitdc83c5c224
, reversing changes made to9966aec312
.
This commit is contained in:
parent
dc83c5c224
commit
c02b20c4bb
|
@ -12,27 +12,8 @@ jobs:
|
|||
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-desktop-win
|
||||
- name: Upload Windows executable
|
||||
uses: actions/upload-artifact@v1
|
||||
if: success()
|
||||
with:
|
||||
name: 'Windows executable (output dir)'
|
||||
path: dist
|
||||
npm run build-desktop-win
|
|
@ -1,24 +1,10 @@
|
|||
Hyperspace Desktop
|
||||
Copyright Hyperspace Developers 2020
|
||||
Hyperspace
|
||||
Copyright Hyperspace developers 2019
|
||||
|
||||
NON-VIOLENT PUBLIC LICENSE v4
|
||||
|
||||
Preamble
|
||||
|
||||
The Non-Violent Public license is a freedom-respecting sharealike license
|
||||
for both the author of a work as well as those subject to a work. It aims
|
||||
to protect the basic rights of human beings from exploitation and the earth
|
||||
from plunder. It aims to ensure a copyrighted work is forever available
|
||||
for public use, modification, and redistribution under the same terms so
|
||||
long as the work is not used for harm. For more information about the NPL
|
||||
refer to the official webpage
|
||||
|
||||
Official Webpage: https://thufie.lain.haus/NPL.html
|
||||
|
||||
Terms and Conditions
|
||||
NON-VIOLENT PUBLIC LICENSE v1
|
||||
|
||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
|
||||
NON-VIOLENT PUBLIC LICENSE v4 ("LICENSE"). THE WORK IS PROTECTED BY
|
||||
NON-VIOLENT PUBLIC LICENSE v1 ("LICENSE"). THE WORK IS PROTECTED BY
|
||||
COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN
|
||||
AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY
|
||||
EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE
|
||||
|
@ -52,9 +38,8 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
timed-relation with a moving image ("synching") will be
|
||||
considered an Adaptation for the purpose of this License.
|
||||
|
||||
c. "Bodily Harm" means any physical hurt or injury to a person that
|
||||
interferes with the health or comfort of the person and that is more
|
||||
more than merely transient or trifling in nature.
|
||||
c. "Bodily Harm" means any action of one person towards another
|
||||
in an intentional manner.
|
||||
|
||||
d. "Collection" means a collection of literary or artistic
|
||||
works, such as encyclopedias and anthologies, or performances,
|
||||
|
@ -76,7 +61,8 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
|
||||
f. "Incarceration" means confinement in a jail, prison, or any
|
||||
other place where individuals of any kind are held against
|
||||
either their will or the will of their legal guardians.
|
||||
either their will or the will of their legal guardians by physical
|
||||
means.
|
||||
|
||||
g. "Licensor" means the individual, individuals, entity or
|
||||
entities that offer(s) the Work under the terms of this License.
|
||||
|
@ -148,23 +134,13 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
through which the Original Author and/or Distributor originally
|
||||
created, derived, and/or modified it.
|
||||
|
||||
o. "Surveilling" means the use of the Work to either
|
||||
overtly or covertly observe and record persons and or their
|
||||
activities.
|
||||
o. "Surveilling" means the use of the Work to
|
||||
overtly or covertly observe persons or their activities.
|
||||
|
||||
p. "Web Service" means the use of a piece of Software to
|
||||
interpret or modify information that is subsequently and directly
|
||||
served to users over the Internet.
|
||||
|
||||
q. "Discriminate" means the use of a work to differentiate between
|
||||
humans in a such a way which prioritizes some above others on the
|
||||
basis of percieved membership within certain groups.
|
||||
|
||||
r. "Hate Speech" means communication or any form
|
||||
of expression which is solely for the purpose of expressing hatred
|
||||
for some group or advocating a form of Discrimination
|
||||
(to Discriminate per definition in (q)) between humans.
|
||||
|
||||
2. FAIR DEALING RIGHTS
|
||||
|
||||
Nothing in this License is intended to reduce, limit, or restrict any
|
||||
|
@ -201,6 +177,7 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
exercise the rights in other media and formats. Subject to
|
||||
Section 8(g), all rights not expressly granted by Licensor are
|
||||
hereby reserved.
|
||||
|
||||
|
||||
4. RESTRICTIONS
|
||||
|
||||
|
@ -255,15 +232,15 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
or tracking individuals for financial gain.
|
||||
iii. You do not use the Work in an Act of War.
|
||||
iv. You do not use the Work for the purpose of supporting
|
||||
or profiting from an Act of War.
|
||||
an Act of War.
|
||||
v. You do not use the Work for the purpose of Incarceration.
|
||||
vi. You do not use the Work for the purpose of extracting
|
||||
oil, gas, or coal.
|
||||
vii. You do not use the Work for the purpose of
|
||||
expediting, coordinating, or facilitating paid work
|
||||
undertaken by individuals under the age of 12 years.
|
||||
viii. You do not use the Work to either Discriminate or
|
||||
spread Hate Speech on the basis of sex, sexual orientation,
|
||||
viii. You do not use the Work to either discriminate or
|
||||
spread hate speech on the basis of sex, sexual orientation,
|
||||
gender identity, race, age, disability, color, national origin,
|
||||
religion, or lower economic status.
|
||||
|
||||
|
@ -442,4 +419,4 @@ AND CONDITIONS OF THIS LICENSE.
|
|||
additional rights not granted under this License, such
|
||||
additional rights are deemed to be included in the License; this
|
||||
License is not intended to restrict the license of any rights
|
||||
under applicable law.
|
||||
under applicable law.
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
[![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)
|
||||
![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) <!-- [![iTunes App Store](https://img.shields.io/itunes/v/1454139710?label=Mac%20App%20Store&logo=apple&logoColor=white)](https://apps.apple.com/us/app/hyperspace/id1454139710?mt=12)--> [![Hyperspace](https://snapcraft.io/hyperspace/badge.svg)](https://snapcraft.io/hyperspace)
|
||||
|
||||
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.
|
||||
|
||||
|
@ -127,12 +127,12 @@ You'll also want to modify the `notarize.js` file to change the details from the
|
|||
|
||||
## Licensing and Credits
|
||||
|
||||
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 is licensed under the [Non-violent Public License](LICENSE), 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 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
|
||||
|
||||
Contribution guidelines are available in the [contributing file](.github/contributing.md) and when you make an issue/pull request. Additionally, you can access our [Code of Conduct](.github/code_of_conduct.md).
|
||||
Contrubition guidelines are available in the [contributing file](.github/contributing.md) and when you make an issue/pull request. Additionally, you can access our [Code of Conduct](.github/code_of_conduct.md).
|
||||
|
||||
If you want to aid the project in other ways, consider supporting the project on [Patreon](https://patreon.com/hyperspacedev).
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hyperspace",
|
||||
"version": "1.1.0-beta2",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1055,23 +1055,13 @@
|
|||
}
|
||||
},
|
||||
"@material-ui/icons": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.5.1.tgz",
|
||||
"integrity": "sha512-YZ/BgJbXX4a0gOuKWb30mBaHaoXRqPanlePam83JQPZ/y4kl+3aW0Wv9tlR70hB5EGAkEJGW5m4ktJwMgxQAeA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-3.0.2.tgz",
|
||||
"integrity": "sha512-QY/3gJnObZQ3O/e6WjH+0ah2M3MOgLOzCy8HTUoUx9B6dDrS18vP7Ycw3qrDEKlB6q1KNxy6CZHm5FCauWGy2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz",
|
||||
"integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
}
|
||||
}
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"recompose": "0.28.0 - 0.30.0"
|
||||
}
|
||||
},
|
||||
"@material-ui/system": {
|
||||
|
@ -9461,9 +9451,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
|
||||
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
|
@ -10448,9 +10438,9 @@
|
|||
}
|
||||
},
|
||||
"invert-kv": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
|
||||
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
|
||||
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
|
||||
"dev": true
|
||||
},
|
||||
"ip": {
|
||||
|
@ -11848,12 +11838,12 @@
|
|||
"integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q=="
|
||||
},
|
||||
"lcid": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
|
||||
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
|
||||
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"invert-kv": "^2.0.0"
|
||||
"invert-kv": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"left-pad": {
|
||||
|
@ -12275,22 +12265,12 @@
|
|||
}
|
||||
},
|
||||
"mem": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
|
||||
"integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
|
||||
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"map-age-cleaner": "^0.1.1",
|
||||
"mimic-fn": "^2.0.0",
|
||||
"p-is-promise": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
}
|
||||
"mimic-fn": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"memory-fs": {
|
||||
|
@ -13256,40 +13236,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"os-locale": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
|
||||
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
|
||||
"integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"execa": "^1.0.0",
|
||||
"lcid": "^2.0.0",
|
||||
"mem": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
}
|
||||
"execa": "^0.7.0",
|
||||
"lcid": "^1.0.0",
|
||||
"mem": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
|
@ -16822,11 +16776,6 @@
|
|||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"dev": true
|
||||
},
|
||||
"react-masonry-css": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/react-masonry-css/-/react-masonry-css-1.0.14.tgz",
|
||||
"integrity": "sha512-oAPVOCMApTT0HkxZJy84yU1EWaaQNZnJE0DjDMy/L+LxZoJEph4RRXsT9ppPKbFSo/tCzj+cCLwiBHjZmZ2eXA=="
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||
|
@ -19912,9 +19861,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
|
||||
"integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz",
|
||||
"integrity": "sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
|
@ -21476,16 +21425,16 @@
|
|||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz",
|
||||
"integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
|
||||
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^4.0.0",
|
||||
"decamelize": "^1.1.1",
|
||||
"find-up": "^2.1.0",
|
||||
"get-caller-file": "^1.0.1",
|
||||
"os-locale": "^3.1.0",
|
||||
"os-locale": "^2.0.0",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^1.0.1",
|
||||
"set-blocking": "^2.0.0",
|
||||
|
|
215
package.json
215
package.json
|
@ -1,115 +1,114 @@
|
|||
{
|
||||
"name": "hyperspace",
|
||||
"productName": "Hyperspace Desktop",
|
||||
"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.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.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.18.0",
|
||||
"megalodon": "^0.6.4",
|
||||
"moment": "^2.24.0",
|
||||
"notistack": "^0.5.1",
|
||||
"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.7.2"
|
||||
"name": "hyperspace",
|
||||
"productName": "Hyperspace Desktop",
|
||||
"version": "1.0.4",
|
||||
"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.11",
|
||||
"@material-ui/core": "^3.9.3",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"@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.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.18.0",
|
||||
"megalodon": "^0.6.4",
|
||||
"moment": "^2.24.0",
|
||||
"notistack": "^0.5.1",
|
||||
"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.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-notarize": "^0.1.1",
|
||||
"electron-updater": "^4.1.2",
|
||||
"electron-window-state": "^5.0.3"
|
||||
},
|
||||
"main": "public/electron.js",
|
||||
"scripts": {
|
||||
"start": "HTTPS=true react-scripts start",
|
||||
"electrify": "npm run build; electron .",
|
||||
"electrify-nobuild": "electron .",
|
||||
"build": "react-scripts build",
|
||||
"create-mac-icon": "cd desktop; iconutil -c icns app.iconset; cd ..",
|
||||
"build-desktop": "npm run build; npm run create-mac-icon; 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"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"build": {
|
||||
"appId": "net.marquiskurt.hyperspace",
|
||||
"afterSign": "desktop/notarize.js",
|
||||
"directories": {
|
||||
"buildResources": "desktop"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-notarize": "^0.1.1",
|
||||
"electron-updater": "^4.1.2",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"react-masonry-css": "^1.0.14"
|
||||
"mac": {
|
||||
"category": "public.app-category.social-networking",
|
||||
"icon": "desktop/app.icns",
|
||||
"target": [
|
||||
"dmg",
|
||||
"mas"
|
||||
],
|
||||
"darkModeSupport": true,
|
||||
"hardenedRuntime": true
|
||||
},
|
||||
"main": "public/electron.js",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"electrify": "npm run build; electron .",
|
||||
"electrify-nobuild": "electron .",
|
||||
"build": "react-scripts build",
|
||||
"create-mac-icon": "cd desktop; iconutil -c icns app.iconset; cd ..",
|
||||
"build-desktop": "npm run build; npm run create-mac-icon; 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"
|
||||
"mas": {
|
||||
"entitlements": "desktop/entitlements.mas.plist",
|
||||
"entitlementsInherit": "desktop/entitlements.mas.inherit.plist",
|
||||
"provisioningProfile": "desktop/embedded.provisionprofile"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"dmg": {
|
||||
"sign": false
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"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"
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"version": "1.1.0",
|
||||
"location": "https://hyperspaceapp-next.herokuapp.com",
|
||||
"version": "1.0.4",
|
||||
"location": "https://hyperspaceapp.herokuapp.com",
|
||||
"branding": {
|
||||
"name": "Hyperspace",
|
||||
"logo": "logo.svg",
|
||||
"background": "background.png"
|
||||
},
|
||||
"developer": true,
|
||||
"developer": false,
|
||||
"federation": {
|
||||
"universalLogin": true,
|
||||
"allowPublicPosts": true,
|
||||
|
@ -20,7 +20,7 @@
|
|||
"account": "774314"
|
||||
},
|
||||
"license": {
|
||||
"name": "Non-violent Public License v4+",
|
||||
"name": "Non-violent Public License",
|
||||
"url": "https://thufie.lain.haus/NPL.html"
|
||||
},
|
||||
"repository": "https://github.com/hyperspacedev/hyperspace"
|
||||
|
|
|
@ -312,14 +312,6 @@ function createMenubar() {
|
|||
click() {
|
||||
safelyGoTo("hyperspace://hyperspace/app/#/messages")
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Activity',
|
||||
accelerator: 'Alt+CmdOrCtrl+A',
|
||||
click() {
|
||||
safelyGoTo("hyperspace://hyperspace/app/#/activity")
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -334,7 +326,7 @@ function createMenubar() {
|
|||
}
|
||||
},
|
||||
{
|
||||
label: 'Recommendations',
|
||||
label: 'Recommendations...',
|
||||
accelerator: "Alt+CmdOrCtrl+R",
|
||||
click() {
|
||||
safelyGoTo("hyperspace://hyperspace/app/#/recommended")
|
||||
|
@ -348,13 +340,6 @@ function createMenubar() {
|
|||
safelyGoTo("hyperspace://hyperspace/app/#/you")
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Follow Requests',
|
||||
accelerator: "Alt+CmdOrCtrl+E",
|
||||
click() {
|
||||
safelyGoTo("hyperspace://hyperspace/app/#/requests")
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Blocked Servers',
|
||||
accelerator: "Shift+CmdOrCtrl+B",
|
||||
|
|
53
src/App.tsx
53
src/App.tsx
|
@ -8,7 +8,9 @@ import AboutPage from "./pages/About";
|
|||
import Settings from "./pages/Settings";
|
||||
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
import TimelinePage from "./pages/Timeline";
|
||||
import HomePage from "./pages/Home";
|
||||
import LocalPage from "./pages/Local";
|
||||
import PublicPage from "./pages/Public";
|
||||
import Conversation from "./pages/Conversation";
|
||||
import NotificationsPage from "./pages/Notifications";
|
||||
import SearchPage from "./pages/Search";
|
||||
|
@ -19,8 +21,6 @@ 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";
|
||||
import ActivityPage from "./pages/Activity";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { PrivateRoute } from "./interfaces/overrides";
|
||||
import { userLoggedIn } from "./utilities/accounts";
|
||||
|
@ -96,47 +96,10 @@ class App extends Component<any, IAppState> {
|
|||
<Route path="/welcome" component={WelcomePage} />
|
||||
<div>
|
||||
{this.state.showLayout ? <AppLayout /> : null}
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/"
|
||||
render={(props: any) => (
|
||||
<TimelinePage
|
||||
{...props}
|
||||
stream="/streaming/user"
|
||||
timeline="/timelines/home"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/home"
|
||||
render={(props: any) => (
|
||||
<TimelinePage
|
||||
{...props}
|
||||
stream="/streaming/user"
|
||||
timeline="/timelines/home"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/local"
|
||||
render={(props: any) => (
|
||||
<TimelinePage
|
||||
{...props}
|
||||
stream="/streaming/public/local"
|
||||
timeline="/timelines/public?local=true"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/public"
|
||||
render={(props: any) => (
|
||||
<TimelinePage
|
||||
{...props}
|
||||
stream="/streaming/public"
|
||||
timeline="/timelines/public"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute exact path="/" component={HomePage} />
|
||||
<PrivateRoute path="/home" component={HomePage} />
|
||||
<PrivateRoute path="/local" component={LocalPage} />
|
||||
<PrivateRoute path="/public" component={PublicPage} />
|
||||
<PrivateRoute path="/messages" component={MessagesPage} />
|
||||
<PrivateRoute
|
||||
path="/notifications"
|
||||
|
@ -160,8 +123,6 @@ class App extends Component<any, IAppState> {
|
|||
path="/recommended"
|
||||
component={RecommendationsPage}
|
||||
/>
|
||||
<PrivateRoute path="/requests" component={RequestsPage} />
|
||||
<PrivateRoute path="/activity" component={ActivityPage} />
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
|
|
|
@ -35,8 +35,7 @@ export const styles = (theme: Theme) =>
|
|||
titleBarText: {
|
||||
fontSize: 12,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 1,
|
||||
color: theme.palette.getContrastText(theme.palette.primary.main)
|
||||
paddingBottom: 1
|
||||
},
|
||||
appBar: {
|
||||
zIndex: 1000,
|
||||
|
@ -58,10 +57,6 @@ export const styles = (theme: Theme) =>
|
|||
display: "none"
|
||||
}
|
||||
},
|
||||
appBarBackButton: {
|
||||
marginLeft: -12,
|
||||
marginRight: 20
|
||||
},
|
||||
appBarTitle: {
|
||||
display: "none",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
ListItem,
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||
|
@ -42,10 +41,6 @@ import InfoIcon from "@material-ui/icons/Info";
|
|||
import CreateIcon from "@material-ui/icons/Create";
|
||||
import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
|
||||
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
|
||||
import TrendingUpIcon from "@material-ui/icons/TrendingUp";
|
||||
import BuildIcon from "@material-ui/icons/Build";
|
||||
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||
|
||||
import { styles } from "./AppLayout.styles";
|
||||
import { MultiAccount, UAccount } from "../../types/Account";
|
||||
import {
|
||||
|
@ -68,81 +63,30 @@ import {
|
|||
getAccountRegistry,
|
||||
removeAccountFromRegistry
|
||||
} from "../../utilities/accounts";
|
||||
import { isChildView } from "../../utilities/appbar";
|
||||
|
||||
/**
|
||||
* The pre-define state interface for the app layout.
|
||||
*/
|
||||
interface IAppLayoutState {
|
||||
/**
|
||||
* Whether the account menu is open or not.
|
||||
*/
|
||||
acctMenuOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the drawer is open (mobile-only).
|
||||
*/
|
||||
drawerOpenOnMobile: boolean;
|
||||
|
||||
/**
|
||||
* The current user signed in.
|
||||
*/
|
||||
currentUser?: UAccount;
|
||||
|
||||
/**
|
||||
* The number of notifications received.
|
||||
*/
|
||||
notificationCount: number;
|
||||
|
||||
/**
|
||||
* Whether the log out dialog is open.
|
||||
*/
|
||||
logOutOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether federation has been enabled in the config.
|
||||
*/
|
||||
enableFederation?: boolean;
|
||||
|
||||
/**
|
||||
* The brand name of the app, if not "Hyperspace".
|
||||
*/
|
||||
brandName?: string;
|
||||
|
||||
/**
|
||||
* Whether the app is in development mode.
|
||||
*/
|
||||
developerMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base app layout class. Responsible for the search bar, navigation menus, etc.
|
||||
*/
|
||||
export class AppLayout extends Component<any, IAppLayoutState> {
|
||||
/**
|
||||
* The Mastodon client to operate with.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* A stream listener to listen for new streaming events from Mastodon.
|
||||
*/
|
||||
streamListener: any;
|
||||
|
||||
/**
|
||||
* Construct the app layout.
|
||||
* @param props The properties to pass in.
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Create the Mastodon client
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
// Initialize the state
|
||||
this.state = {
|
||||
drawerOpenOnMobile: false,
|
||||
acctMenuOpen: false,
|
||||
|
@ -150,20 +94,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
logOutOpen: false
|
||||
};
|
||||
|
||||
// Bind functions as properties to this class for reference
|
||||
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
||||
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
||||
this.clearBadge = this.clearBadge.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run post-mount tasks such as getting account data and refreshing the config file.
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Get the account data.
|
||||
this.getAccountData();
|
||||
|
||||
// Read the config file and then update the state.
|
||||
getConfig().then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
|
@ -177,25 +115,18 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
});
|
||||
|
||||
// Listen for notifications.
|
||||
this.streamNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated credentials from Mastodon or pull information from local storage.
|
||||
*/
|
||||
getAccountData() {
|
||||
// Try to get updated credentials from Mastodon.
|
||||
this.client
|
||||
.get("/accounts/verify_credentials")
|
||||
.then((resp: any) => {
|
||||
// Update the account if possible.
|
||||
let data: UAccount = resp.data;
|
||||
this.setState({ currentUser: data });
|
||||
sessionStorage.setItem("id", data.id);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// Otherwise, pull from local storage.
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't find profile info: " + err.name
|
||||
);
|
||||
|
@ -205,14 +136,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a stream listener and listen for notifications.
|
||||
*/
|
||||
streamNotifications() {
|
||||
// Set up the stream listener.
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
// Set the count if the user asked to display the total count.
|
||||
if (getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||
this.client.get("/notifications").then((resp: any) => {
|
||||
let notifArray = resp.data;
|
||||
|
@ -220,17 +146,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
});
|
||||
}
|
||||
|
||||
// Listen for notifications.
|
||||
this.streamListener.on("notification", (notif: Notification) => {
|
||||
const notificationCount = this.state.notificationCount + 1;
|
||||
this.setState({ notificationCount });
|
||||
|
||||
// Update the badge on the desktop.
|
||||
if (isDesktopApp()) {
|
||||
getElectronApp().setBadgeCount(notificationCount);
|
||||
}
|
||||
|
||||
// Set up a push notification if the window isn't in focus.
|
||||
if (!document.hasFocus()) {
|
||||
let primaryMessage = "";
|
||||
let secondaryMessage = "";
|
||||
|
@ -289,54 +212,34 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
break;
|
||||
}
|
||||
|
||||
// Respectfully send the notification request.
|
||||
sendNotificationRequest(primaryMessage, secondaryMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the account menu.
|
||||
*/
|
||||
toggleAcctMenu() {
|
||||
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the app drawer, if on mobile.
|
||||
*/
|
||||
toggleDrawerOnMobile() {
|
||||
this.setState({
|
||||
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the logout dialog.
|
||||
*/
|
||||
toggleLogOutDialog() {
|
||||
this.setState({ logOutOpen: !this.state.logOutOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search and redirect to the search page.
|
||||
* @param what The query input from the search box
|
||||
*/
|
||||
searchForQuery(what: string) {
|
||||
what = what.replace(/^#/g, "tag:");
|
||||
console.log(what);
|
||||
window.location.href = isDesktopApp()
|
||||
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
|
||||
: "/#/search?query=" + what;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear login information, remove the account from the registry, and reload the web page.
|
||||
*/
|
||||
logOutAndRestart() {
|
||||
let loginData = localStorage.getItem("login");
|
||||
if (loginData) {
|
||||
// Remove account from the registry.
|
||||
let registry = getAccountRegistry();
|
||||
|
||||
registry.forEach((registryItem: MultiAccount, index: number) => {
|
||||
|
@ -348,20 +251,15 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
});
|
||||
|
||||
// Clear some of the local storage fields.
|
||||
let items = ["login", "account", "baseurl", "access_token"];
|
||||
items.forEach(entry => {
|
||||
localStorage.removeItem(entry);
|
||||
});
|
||||
|
||||
// Finally, reload.
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the notifications badge.
|
||||
*/
|
||||
clearBadge() {
|
||||
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||
this.setState({ notificationCount: 0 });
|
||||
|
@ -372,9 +270,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the title bar.
|
||||
*/
|
||||
titlebar() {
|
||||
const { classes } = this.props;
|
||||
if (isDarwinApp()) {
|
||||
|
@ -395,20 +290,13 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
return (
|
||||
<div className={classes.titleBarRoot}>
|
||||
<Typography className={classes.titleBarText}>
|
||||
<BuildIcon
|
||||
color="inherit"
|
||||
style={{ fontSize: "1em", verticalAlign: "middle" }}
|
||||
/>{" "}
|
||||
Careful: you're running in developer mode.
|
||||
🛠 Careful: you're running in developer mode.
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app drawer. On the desktop, this appears as a sidebar in larger layouts.
|
||||
*/
|
||||
appDrawer() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -542,13 +430,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
</LinkableListItem>
|
||||
<Divider />
|
||||
</div>
|
||||
<ListSubheader>Community</ListSubheader>
|
||||
<LinkableListItem button key="activity" to="/activity">
|
||||
<ListItemIcon>
|
||||
<TrendingUpIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Activity" />
|
||||
</LinkableListItem>
|
||||
<ListSubheader>More</ListSubheader>
|
||||
<LinkableListItem
|
||||
button
|
||||
key="recommended"
|
||||
|
@ -557,10 +439,8 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
<ListItemIcon>
|
||||
<GroupIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Recommended" />
|
||||
<ListItemText primary="Who to follow" />
|
||||
</LinkableListItem>
|
||||
<Divider />
|
||||
<ListSubheader>More</ListSubheader>
|
||||
<LinkableListItem button key="settings" to="/settings">
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
|
@ -578,9 +458,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the entire layout.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -589,18 +466,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
{this.titlebar()}
|
||||
<AppBar className={classes.appBar} position="static">
|
||||
<Toolbar>
|
||||
{isDesktopApp() &&
|
||||
isChildView(window.location.hash) ? (
|
||||
<IconButton
|
||||
className={classes.appBarBackButton}
|
||||
color="inherit"
|
||||
aria-label="Go back"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
className={classes.appBarMenuButton}
|
||||
color="inherit"
|
||||
|
@ -752,15 +617,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
Edit profile
|
||||
</ListItemText>
|
||||
</LinkableListItem>
|
||||
<LinkableListItem
|
||||
button={true}
|
||||
to={"/requests"}
|
||||
>
|
||||
<ListItemText>
|
||||
Manage follow requests
|
||||
</ListItemText>
|
||||
</LinkableListItem>
|
||||
<Divider />
|
||||
<LinkableListItem
|
||||
to={"/welcome"}
|
||||
button={true}
|
||||
|
@ -792,7 +648,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
variant="temporary"
|
||||
anchor={"left"}
|
||||
open={this.state.drawerOpenOnMobile}
|
||||
onClick={this.toggleDrawerOnMobile}
|
||||
onClose={this.toggleDrawerOnMobile}
|
||||
classes={{ paper: classes.drawerPaper }}
|
||||
>
|
||||
{this.appDrawer()}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "@material-ui/core";
|
||||
import { styles } from "./Attachment.styles";
|
||||
import { Attachment } from "../../types/Attachment";
|
||||
import AudioPlayer from "../AudioPlayer";
|
||||
import SwipeableViews from "react-swipeable-views";
|
||||
|
||||
interface IAttachmentProps {
|
||||
|
@ -77,15 +76,11 @@ class AttachmentComponent extends Component<
|
|||
className={classes.mediaObject}
|
||||
/>
|
||||
);
|
||||
case "audio":
|
||||
return <AudioPlayer src={slide.url} id={slide.id} />;
|
||||
case "gifv":
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
<img
|
||||
src={slide.url}
|
||||
title={slide.description ? slide.description : ""}
|
||||
alt={slide.description ? slide.description : ""}
|
||||
className={classes.mediaObject}
|
||||
/>
|
||||
);
|
||||
|
@ -111,36 +106,33 @@ class AttachmentComponent extends Component<
|
|||
);
|
||||
})}
|
||||
</SwipeableViews>
|
||||
{this.state.totalSteps > 1 ? (
|
||||
<MobileStepper
|
||||
steps={this.state.totalSteps}
|
||||
position="static"
|
||||
activeStep={this.state.currentStep}
|
||||
className={classes.mobileStepper}
|
||||
nextButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => this.moveForward()}
|
||||
disabled={
|
||||
this.state.currentStep ===
|
||||
this.state.totalSteps - 1
|
||||
}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
backButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => this.moveBack()}
|
||||
disabled={this.state.currentStep === 0}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<br />
|
||||
<MobileStepper
|
||||
steps={this.state.totalSteps}
|
||||
position="static"
|
||||
activeStep={this.state.currentStep}
|
||||
className={classes.mobileStepper}
|
||||
nextButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => this.moveForward()}
|
||||
disabled={
|
||||
this.state.currentStep ===
|
||||
this.state.totalSteps - 1
|
||||
}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
backButton={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => this.moveBack()}
|
||||
disabled={this.state.currentStep === 0}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{mediaItem.description
|
||||
? mediaItem.description
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { Theme, createStyles } from "@material-ui/core";
|
||||
|
||||
export const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.action.disabledBackground,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid"
|
||||
},
|
||||
progressBar: {
|
||||
width: "100%"
|
||||
},
|
||||
download: {
|
||||
color: `${theme.palette.action.active} !important`
|
||||
}
|
||||
});
|
|
@ -1,141 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import {
|
||||
Toolbar,
|
||||
IconButton,
|
||||
withStyles,
|
||||
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";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import PauseIcon from "@material-ui/icons/Pause";
|
||||
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
|
||||
|
||||
import { styles } from "./AudioPlayer.styles";
|
||||
|
||||
interface IAudioPlayerProps {
|
||||
src: string;
|
||||
id: string;
|
||||
classes: any;
|
||||
}
|
||||
|
||||
interface IAudioPlayerState {
|
||||
src: string;
|
||||
elementId: string;
|
||||
playing: boolean;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
class AudioPlayer extends Component<IAudioPlayerProps, IAudioPlayerState> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
src: this.props.src,
|
||||
elementId: "audioplayer-" + this.props.id,
|
||||
playing: false,
|
||||
progress: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let audioPlayerElement = this.getAudioPlayer();
|
||||
|
||||
if (audioPlayerElement) {
|
||||
audioPlayerElement.ontimeupdate = () => {
|
||||
let music = audioPlayerElement as HTMLAudioElement;
|
||||
let progress = 100 * (music.currentTime / music.duration);
|
||||
this.setState({ progress });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getAudioPlayer(): HTMLAudioElement | null {
|
||||
return document.getElementById(
|
||||
this.state.elementId
|
||||
) as HTMLAudioElement | null;
|
||||
}
|
||||
|
||||
toggleAudio() {
|
||||
let audioPlayerElement = this.getAudioPlayer();
|
||||
|
||||
if (audioPlayerElement && this.state.playing) {
|
||||
audioPlayerElement.pause();
|
||||
this.setState({ playing: false });
|
||||
} else if (audioPlayerElement) {
|
||||
audioPlayerElement.play();
|
||||
this.setState({ playing: true });
|
||||
}
|
||||
}
|
||||
|
||||
fastForward() {
|
||||
let audioPlayerElement = this.getAudioPlayer();
|
||||
|
||||
if (audioPlayerElement) {
|
||||
audioPlayerElement.currentTime += 15.0;
|
||||
}
|
||||
}
|
||||
|
||||
rewind() {
|
||||
let audioPlayerElement = this.getAudioPlayer();
|
||||
|
||||
if (audioPlayerElement) {
|
||||
audioPlayerElement.currentTime -= 15.0;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<audio
|
||||
id={this.state.elementId}
|
||||
src={this.state.src}
|
||||
autoPlay={false}
|
||||
/>
|
||||
<Toolbar>
|
||||
<Tooltip title="Rewind by 15s">
|
||||
<IconButton onClick={() => this.rewind()}>
|
||||
<FastRewindIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={this.state.playing ? "Pause" : "Play"}>
|
||||
<IconButton onClick={() => this.toggleAudio()}>
|
||||
{this.state.playing ? (
|
||||
<PauseIcon />
|
||||
) : (
|
||||
<PlayArrowIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Fast-forward by 15s">
|
||||
<IconButton onClick={() => this.fastForward()}>
|
||||
<FastForwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<LinearProgress
|
||||
className={classes.progressBar}
|
||||
variant="determinate"
|
||||
color={"secondary"}
|
||||
value={this.state.progress}
|
||||
/>
|
||||
<Tooltip title="Download">
|
||||
<IconButton
|
||||
href={this.state.src}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollower"
|
||||
className={classes.download}
|
||||
>
|
||||
<CloudDownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(AudioPlayer);
|
|
@ -1,3 +0,0 @@
|
|||
import AudioPlayer from "./AudioPlayer";
|
||||
|
||||
export default AudioPlayer;
|
|
@ -81,15 +81,6 @@ export const styles = (theme: Theme) =>
|
|||
paddingTop: theme.spacing.unit,
|
||||
paddingBottom: theme.spacing.unit
|
||||
},
|
||||
postAuthorAccount: {
|
||||
color: theme.palette.grey[500],
|
||||
marginLeft: theme.spacing.unit * 0.5,
|
||||
},
|
||||
postReblogIcon: {
|
||||
marginBottom: theme.spacing.unit * -0.5,
|
||||
marginLeft: theme.spacing.unit * 0.5,
|
||||
marginRight: theme.spacing.unit * 0.5,
|
||||
},
|
||||
postAuthorEmoji: {
|
||||
height: theme.typography.fontSize,
|
||||
verticalAlign: "middle"
|
||||
|
|
|
@ -101,11 +101,6 @@ export class Post extends React.Component<any, IPostState> {
|
|||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: any, nextState: any) {
|
||||
if (nextState == this.state) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
togglePostMenu() {
|
||||
this.setState({ menuIsOpen: !this.state.menuIsOpen });
|
||||
}
|
||||
|
@ -401,58 +396,24 @@ export class Post extends React.Component<any, IPostState> {
|
|||
|
||||
getReblogAuthors(post: Status) {
|
||||
const { classes } = this.props;
|
||||
|
||||
let author = post.reblog ? post.reblog.account : post.account;
|
||||
let emojis = author.emojis;
|
||||
let reblogger = post.reblog ? post.account : undefined;
|
||||
|
||||
if (reblogger != undefined) {
|
||||
emojis.concat(reblogger.emojis);
|
||||
if (post.reblog) {
|
||||
let author = post.reblog.account;
|
||||
let origString = `<span>${author.display_name ||
|
||||
author.username} (@${author.acct}) 🔄 ${post.account
|
||||
.display_name || post.account.username}</span>`;
|
||||
let emojis = author.emojis;
|
||||
emojis.concat(post.account.emojis);
|
||||
return emojifyString(origString, emojis, classes.postAuthorEmoji);
|
||||
} else {
|
||||
let author = post.account;
|
||||
let origString = `<span>${author.display_name ||
|
||||
author.username} (@${author.acct})</span>`;
|
||||
return emojifyString(
|
||||
origString,
|
||||
author.emojis,
|
||||
classes.postAuthorEmoji
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyString(
|
||||
author.display_name || author.username,
|
||||
emojis,
|
||||
classes.postAuthorEmoji
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
<span
|
||||
className={classes.postAuthorAccount}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
"@" +
|
||||
emojifyString(
|
||||
author.acct || author.username,
|
||||
emojis,
|
||||
classes.postAuthorEmoji
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
{reblogger ? (
|
||||
<>
|
||||
<AutorenewIcon
|
||||
fontSize="small"
|
||||
className={classes.postReblogIcon}
|
||||
/>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyString(
|
||||
reblogger.display_name ||
|
||||
reblogger.username,
|
||||
emojis,
|
||||
classes.postAuthorEmoji
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
getMentions(mention: [Mention]) {
|
||||
|
@ -539,63 +500,86 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post's URL
|
||||
* @param post The post to get the URL from
|
||||
* @returns A string containing the post's URI
|
||||
*/
|
||||
getMastodonUrl(post: Status) {
|
||||
return post.reblog ? post.reblog.uri : post.uri;
|
||||
let url = "";
|
||||
if (post.reblog) {
|
||||
url = post.reblog.uri;
|
||||
} else {
|
||||
url = post.uri;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell server a post has been un/favorited and update post state
|
||||
* @param post The post to un/favorite
|
||||
*/
|
||||
async toggleFavorite(post: Status) {
|
||||
let action: string = post.favourited ? "unfavourite" : "favourite";
|
||||
try {
|
||||
// favorite the original post, not the reblog
|
||||
let resp: any = await this.client.post(
|
||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
||||
);
|
||||
// compensate for slow server update
|
||||
if (action === "unfavourite") {
|
||||
resp.data.favourites_count -= 1;
|
||||
// if you unlike both original and reblog before refresh
|
||||
// and the post has only one favorite:
|
||||
if (resp.data.favourites_count < 0) {
|
||||
resp.data.favourites_count = 0;
|
||||
}
|
||||
}
|
||||
this.setState({ post: resp.data as Status });
|
||||
} catch (e) {
|
||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
||||
console.error(e.message);
|
||||
toggleFavorited(post: Status) {
|
||||
let _this = this;
|
||||
if (post.favourited) {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unfavourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(
|
||||
`Couldn't unfavorite post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
} else {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/favourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(
|
||||
`Couldn't favorite post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell server a post has been un/reblogged and update post state
|
||||
* @param post The post to un/reblog
|
||||
*/
|
||||
async toggleReblog(post: Status) {
|
||||
let action: string =
|
||||
post.reblogged || post.reblog ? "unreblog" : "reblog";
|
||||
try {
|
||||
// modify the original post, not the reblog
|
||||
let resp: any = await this.client.post(
|
||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
||||
);
|
||||
// compensate for slow server update
|
||||
if (action === "unreblog") {
|
||||
resp.data.reblogs_count -= 1;
|
||||
}
|
||||
if (resp.data.reblog) resp.data = resp.data.reblog;
|
||||
this.setState({ post: resp.data as Status });
|
||||
} catch (e) {
|
||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
||||
console.error(e.message);
|
||||
toggleReblogged(post: Status) {
|
||||
if (post.reblogged) {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unreblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't unboost post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
} else {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/reblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't boost post: ${err.name}`,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
console.log(err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -640,210 +624,234 @@ export class Post extends React.Component<any, IPostState> {
|
|||
const { classes } = this.props;
|
||||
const post = this.state.post;
|
||||
return (
|
||||
<Card
|
||||
className={classes.post}
|
||||
id={`post_${post.id}`}
|
||||
elevation={this.props.threadHeader ? 0 : 1}
|
||||
>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<LinkableAvatar
|
||||
to={`/profile/${
|
||||
post.reblog
|
||||
? post.reblog.account.id
|
||||
: post.account.id
|
||||
}`}
|
||||
src={
|
||||
post.reblog
|
||||
? post.reblog.account.avatar_static
|
||||
: post.account.avatar_static
|
||||
}
|
||||
/>
|
||||
}
|
||||
action={
|
||||
<Tooltip title="More" placement="left">
|
||||
<IconButton
|
||||
key={`${post.id}_submenu`}
|
||||
id={`${post.id}_submenu`}
|
||||
onClick={() => this.togglePostMenu()}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
title={
|
||||
<Typography>{this.getReblogAuthors(post)}</Typography>
|
||||
}
|
||||
subheader={moment(post.created_at).format(
|
||||
"MMMM Do YYYY [at] h:mm A"
|
||||
)}
|
||||
/>
|
||||
{post.reblog ? this.getReblogOfPost(post.reblog) : null}
|
||||
{post.sensitive
|
||||
? this.getSensitiveContent(post.spoiler_text, post)
|
||||
: post.reblog
|
||||
? null
|
||||
: this.materializeContent(post)}
|
||||
{post.reblog && post.reblog.mentions.length > 0
|
||||
? this.getMentions(post.reblog.mentions)
|
||||
: this.getMentions(post.mentions)}
|
||||
{post.reblog && post.reblog.tags.length > 0
|
||||
? this.getTags(post.reblog.tags)
|
||||
: this.getTags(post.tags)}
|
||||
<CardActions>
|
||||
<Tooltip title="Reply">
|
||||
<LinkableIconButton
|
||||
to={`/compose?reply=${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}&visibility=${post.visibility}&acct=${
|
||||
post.reblog
|
||||
? post.reblog.account.acct
|
||||
: post.account.acct
|
||||
}`}
|
||||
>
|
||||
<ReplyIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.replies_count
|
||||
: post.replies_count}
|
||||
</Typography>
|
||||
<Tooltip title="Favorite">
|
||||
<IconButton onClick={() => this.toggleFavorite(post)}>
|
||||
<FavoriteIcon
|
||||
className={
|
||||
post.favourited ? classes.postDidAction : ""
|
||||
<Zoom in={true}>
|
||||
<Card
|
||||
className={classes.post}
|
||||
id={`post_${post.id}`}
|
||||
elevation={this.props.threadHeader ? 0 : 1}
|
||||
>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<LinkableAvatar
|
||||
to={`/profile/${
|
||||
post.reblog
|
||||
? post.reblog.account.id
|
||||
: post.account.id
|
||||
}`}
|
||||
src={
|
||||
post.reblog
|
||||
? post.reblog.account.avatar_static
|
||||
: post.account.avatar_static
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{post.favourites_count}</Typography>
|
||||
<Tooltip title="Boost">
|
||||
<IconButton onClick={() => this.toggleReblog(post)}>
|
||||
<AutorenewIcon
|
||||
className={
|
||||
}
|
||||
action={
|
||||
<Tooltip title="More" placement="left">
|
||||
<IconButton
|
||||
key={`${post.id}_submenu`}
|
||||
id={`${post.id}_submenu`}
|
||||
onClick={() => this.togglePostMenu()}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
title={
|
||||
<Typography
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.getReblogAuthors(post)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
subheader={moment(post.created_at).format(
|
||||
"MMMM Do YYYY [at] h:mm A"
|
||||
)}
|
||||
/>
|
||||
{post.reblog ? this.getReblogOfPost(post.reblog) : null}
|
||||
{post.sensitive
|
||||
? this.getSensitiveContent(post.spoiler_text, post)
|
||||
: post.reblog
|
||||
? null
|
||||
: this.materializeContent(post)}
|
||||
{post.reblog && post.reblog.mentions.length > 0
|
||||
? this.getMentions(post.reblog.mentions)
|
||||
: this.getMentions(post.mentions)}
|
||||
{post.reblog && post.reblog.tags.length > 0
|
||||
? this.getTags(post.reblog.tags)
|
||||
: this.getTags(post.tags)}
|
||||
<CardActions>
|
||||
<Tooltip title="Reply">
|
||||
<LinkableIconButton
|
||||
to={`/compose?reply=${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}&visibility=${post.visibility}&acct=${
|
||||
post.reblog
|
||||
? post.reblog.reblogged
|
||||
? post.reblog.account.acct
|
||||
: post.account.acct
|
||||
}`}
|
||||
>
|
||||
<ReplyIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.replies_count
|
||||
: post.replies_count}
|
||||
</Typography>
|
||||
<Tooltip title="Favorite">
|
||||
<IconButton
|
||||
onClick={() => this.toggleFavorited(post)}
|
||||
>
|
||||
<FavoriteIcon
|
||||
className={
|
||||
post.reblog
|
||||
? post.reblog.favourited
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
: post.favourited
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
: post.reblogged
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.reblogs_count
|
||||
: post.reblogs_count}
|
||||
</Typography>
|
||||
<Tooltip
|
||||
className={classes.desktopOnly}
|
||||
title="View thread"
|
||||
>
|
||||
<LinkableIconButton
|
||||
to={`/conversation/${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}`}
|
||||
>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
className={classes.desktopOnly}
|
||||
title="Open in Web"
|
||||
>
|
||||
<IconButton
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={classes.postFlexGrow} />
|
||||
<div className={classes.postTypeIconDiv}>
|
||||
{this.showVisibilityIcon(post.visibility)}
|
||||
</div>
|
||||
</CardActions>
|
||||
<Menu
|
||||
id="postmenu"
|
||||
anchorEl={document.getElementById(`${post.id}_submenu`)}
|
||||
open={this.state.menuIsOpen}
|
||||
onClose={() => this.togglePostMenu()}
|
||||
>
|
||||
<ShareMenu
|
||||
config={{
|
||||
params: {
|
||||
title: `@${post.account.username} posted on Mastodon: `,
|
||||
text: post.content,
|
||||
url: this.getMastodonUrl(post)
|
||||
},
|
||||
onShareSuccess: () =>
|
||||
this.props.enqueueSnackbar("Post shared!", {
|
||||
variant: "success"
|
||||
}),
|
||||
onShareError: (error: Error) => {
|
||||
if (error.name != "AbortError")
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't share post: ${error.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{post.reblog ? (
|
||||
<div className={classes.postReblogMenu}>
|
||||
<LinkableMenuItem
|
||||
to={`/profile/${post.reblog.account.id}`}
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.favourites_count
|
||||
: post.favourites_count}
|
||||
</Typography>
|
||||
<Tooltip title="Boost">
|
||||
<IconButton
|
||||
onClick={() => this.toggleReblogged(post)}
|
||||
>
|
||||
View author profile
|
||||
</LinkableMenuItem>
|
||||
<AutorenewIcon
|
||||
className={
|
||||
post.reblog
|
||||
? post.reblog.reblogged
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
: post.reblogged
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.reblogs_count
|
||||
: post.reblogs_count}
|
||||
</Typography>
|
||||
<Tooltip
|
||||
className={classes.desktopOnly}
|
||||
title="View thread"
|
||||
>
|
||||
<LinkableIconButton
|
||||
to={`/conversation/${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}`}
|
||||
>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
className={classes.desktopOnly}
|
||||
title="Open in Web"
|
||||
>
|
||||
<IconButton
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={classes.postFlexGrow} />
|
||||
<div className={classes.postTypeIconDiv}>
|
||||
{this.showVisibilityIcon(post.visibility)}
|
||||
</div>
|
||||
</CardActions>
|
||||
<Menu
|
||||
id="postmenu"
|
||||
anchorEl={document.getElementById(`${post.id}_submenu`)}
|
||||
open={this.state.menuIsOpen}
|
||||
onClose={() => this.togglePostMenu()}
|
||||
>
|
||||
<ShareMenu
|
||||
config={{
|
||||
params: {
|
||||
title: `@${post.account.username} posted on Mastodon: `,
|
||||
text: post.content,
|
||||
url: this.getMastodonUrl(post)
|
||||
},
|
||||
onShareSuccess: () =>
|
||||
this.props.enqueueSnackbar("Post shared!", {
|
||||
variant: "success"
|
||||
}),
|
||||
onShareError: (error: Error) => {
|
||||
if (error.name != "AbortError")
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't share post: ${error.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{post.reblog ? (
|
||||
<div className={classes.postReblogMenu}>
|
||||
<LinkableMenuItem
|
||||
to={`/profile/${post.reblog.account.id}`}
|
||||
>
|
||||
View author profile
|
||||
</LinkableMenuItem>
|
||||
<LinkableMenuItem
|
||||
to={`/profile/${post.account.id}`}
|
||||
>
|
||||
View reblogger profile
|
||||
</LinkableMenuItem>
|
||||
</div>
|
||||
) : (
|
||||
<LinkableMenuItem
|
||||
to={`/profile/${post.account.id}`}
|
||||
>
|
||||
View reblogger profile
|
||||
View profile
|
||||
</LinkableMenuItem>
|
||||
</div>
|
||||
) : (
|
||||
<LinkableMenuItem to={`/profile/${post.account.id}`}>
|
||||
View profile
|
||||
</LinkableMenuItem>
|
||||
)}
|
||||
<div className={classes.mobileOnly}>
|
||||
<Divider />
|
||||
<LinkableMenuItem
|
||||
to={`/conversation/${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}`}
|
||||
>
|
||||
View thread
|
||||
</LinkableMenuItem>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Open in Web
|
||||
</MenuItem>
|
||||
</div>
|
||||
{this.state.myAccount &&
|
||||
post.account.id === this.state.myAccount ? (
|
||||
<div>
|
||||
)}
|
||||
<div className={classes.mobileOnly}>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() => this.togglePostDeleteDialog()}
|
||||
<LinkableMenuItem
|
||||
to={`/conversation/${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}`}
|
||||
>
|
||||
Delete
|
||||
View thread
|
||||
</LinkableMenuItem>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Open in Web
|
||||
</MenuItem>
|
||||
</div>
|
||||
) : null}
|
||||
{this.showDeleteDialog()}
|
||||
</Menu>
|
||||
</Card>
|
||||
{this.state.myAccount &&
|
||||
post.account.id === this.state.myAccount ? (
|
||||
<div>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
this.togglePostDeleteDialog()
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</div>
|
||||
) : null}
|
||||
{this.showDeleteDialog()}
|
||||
</Menu>
|
||||
</Card>
|
||||
</Zoom>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,14 +78,14 @@ export const ProfileRoute = (rest: any, component: Component) => (
|
|||
|
||||
export const PrivateRoute = (props: IPrivateRouteProps) => {
|
||||
const { component, render, ...rest } = props;
|
||||
const redir = (comp: any) =>
|
||||
userLoggedIn() ? comp : <Redirect to="/welcome" />;
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={(compProps: any) =>
|
||||
redir(
|
||||
React.createElement(render ? render : component, compProps)
|
||||
userLoggedIn() ? (
|
||||
React.createElement(component, compProps)
|
||||
) : (
|
||||
<Redirect to="/welcome" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
@ -93,6 +93,5 @@ export const PrivateRoute = (props: IPrivateRouteProps) => {
|
|||
};
|
||||
|
||||
interface IPrivateRouteProps extends RouteProps {
|
||||
component?: any;
|
||||
render?: any;
|
||||
component: any;
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* A Generic dictionary with the value of a specific type.
|
||||
*
|
||||
* Keys _must_ be strings.
|
||||
*/
|
||||
export interface Dictionary<T> {
|
||||
[Key: string]: T;
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
ListSubheader,
|
||||
Link,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
IconButton
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { UAccount, Account } from "../types/Account";
|
||||
import { Tag } from "../types/Tag";
|
||||
import Mastodon from "megalodon";
|
||||
import { LinkableAvatar, LinkableIconButton } from "../interfaces/overrides";
|
||||
import moment from "moment";
|
||||
|
||||
import FireplaceIcon from "@material-ui/icons/Fireplace";
|
||||
import TrendingUpIcon from "@material-ui/icons/TrendingUp";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
|
||||
interface IActivityPageState {
|
||||
user?: UAccount;
|
||||
trendingTags?: [Tag];
|
||||
activeProfileDirectory?: [Account];
|
||||
newProfileDirectory?: [Account];
|
||||
viewLoading: boolean;
|
||||
viewLoaded?: boolean;
|
||||
viewErrored?: boolean;
|
||||
}
|
||||
|
||||
class ActivityPage extends Component<any, IActivityPageState> {
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
|
||||
this.state = {
|
||||
viewLoading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getAccountData();
|
||||
|
||||
this.client
|
||||
.get("/trends", { limit: 3 })
|
||||
.then((resp: any) => {
|
||||
let trendingTags: [Tag] = resp.data;
|
||||
this.setState({ trendingTags });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
this.client
|
||||
.get("/directory", { local: true, order: "active", limit: 5 })
|
||||
.then((resp: any) => {
|
||||
let profileDirectory: [Account] = resp.data;
|
||||
this.setState({
|
||||
activeProfileDirectory: profileDirectory
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.log(err.message);
|
||||
});
|
||||
|
||||
this.client
|
||||
.get("/directory", { local: true, order: "new", limit: 5 })
|
||||
.then((resp: any) => {
|
||||
let profileDirectory: [Account] = resp.data;
|
||||
this.setState({
|
||||
newProfileDirectory: profileDirectory,
|
||||
viewLoading: false,
|
||||
viewLoaded: true
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.log(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
getAccountData() {
|
||||
this.client
|
||||
.get("/accounts/verify_credentials")
|
||||
.then((resp: any) => {
|
||||
let data: UAccount = resp.data;
|
||||
this.setState({ user: data });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't find profile info: " + err.name
|
||||
);
|
||||
console.error(err.message);
|
||||
let acct = localStorage.getItem("account") as string;
|
||||
this.setState({ user: JSON.parse(acct) });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<FireplaceIcon style={{ fontSize: 64 }} color="action" />
|
||||
<Typography variant="h6">
|
||||
Hey there,{" "}
|
||||
{this.state.user
|
||||
? this.state.user.display_name ||
|
||||
this.state.user.acct
|
||||
: "user"}
|
||||
!
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Take a look at what's been happening on your instance.
|
||||
</Typography>
|
||||
</div>
|
||||
{this.state.viewLoaded ? (
|
||||
<div>
|
||||
<ListSubheader>Trending hashtags</ListSubheader>
|
||||
{this.state.trendingTags &&
|
||||
this.state.trendingTags.length > 0 ? (
|
||||
<Paper>
|
||||
<List className={classes.pageListConstraints}>
|
||||
{this.state.trendingTags.map((tag: Tag) => (
|
||||
<ListItem
|
||||
id={"trending_tag_" + tag.name}
|
||||
key={"trending_tag_" + tag.name}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<TrendingUpIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"#" + tag.name}
|
||||
secondary={
|
||||
tag.history
|
||||
? `${tag.history[0].accounts} people talking in ${tag.history[0].uses} posts`
|
||||
: "Couldn't determine usage"
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Search">
|
||||
<LinkableIconButton
|
||||
to={`/search?query=tag:${tag.name}`}
|
||||
>
|
||||
<SearchIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View on web">
|
||||
<IconButton>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography paragraph>
|
||||
It looks like there aren't any trending tags on
|
||||
your instance as of right now.
|
||||
</Typography>
|
||||
)}
|
||||
<br />
|
||||
<ListSubheader>Who's been active</ListSubheader>
|
||||
{this.state.activeProfileDirectory &&
|
||||
this.state.activeProfileDirectory.length > 0 ? (
|
||||
<Paper>
|
||||
<List className={classes.pageListConstraints}>
|
||||
{this.state.activeProfileDirectory.map(
|
||||
(account: Account) => (
|
||||
<ListItem
|
||||
key={
|
||||
"account_active_" +
|
||||
account.acct
|
||||
}
|
||||
id={
|
||||
"account_active_" +
|
||||
account.acct
|
||||
}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${account.id}`}
|
||||
src={
|
||||
account.avatar_static
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
`${account.display_name} (@${account.username})` ||
|
||||
`@${account.username}`
|
||||
}
|
||||
secondary={`Last posted ${moment(
|
||||
account.last_status_at
|
||||
)
|
||||
.startOf("minute")
|
||||
.fromNow()}`}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View account">
|
||||
<LinkableIconButton
|
||||
to={`/profile/${account.id}`}
|
||||
>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography paragraph>
|
||||
It looks like there aren't any active people in
|
||||
the profile directory yet.
|
||||
</Typography>
|
||||
)}
|
||||
<br />
|
||||
<ListSubheader>New arrivals</ListSubheader>
|
||||
{this.state.newProfileDirectory &&
|
||||
this.state.newProfileDirectory.length > 0 ? (
|
||||
<Paper>
|
||||
<List className={classes.pageListConstraints}>
|
||||
{this.state.newProfileDirectory.map(
|
||||
(account: Account) => (
|
||||
<ListItem
|
||||
key={
|
||||
"account_new_" +
|
||||
account.acct
|
||||
}
|
||||
id={
|
||||
"account_new_" +
|
||||
account.acct
|
||||
}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${account.id}`}
|
||||
src={
|
||||
account.avatar_static
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
`${account.display_name} (@${account.username})` ||
|
||||
`@${account.username}`
|
||||
}
|
||||
secondary={`Joined ${moment(
|
||||
account.created_at
|
||||
)
|
||||
.startOf("minute")
|
||||
.fromNow()}`}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View account">
|
||||
<LinkableIconButton
|
||||
to={`/profile/${account.id}`}
|
||||
>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography paragraph>
|
||||
It looks like there aren't any new arrivals
|
||||
listed in the profile directory yet.
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.viewErrored ? (
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">
|
||||
Something went wrong when loading instance activity.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{this.state.viewLoading ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<CircularProgress
|
||||
className={classes.progress}
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<br />
|
||||
<div>
|
||||
<Typography variant="caption">
|
||||
Trending hashtags and the profile directory may not
|
||||
appear here if your instance isn't up to date. Check the{" "}
|
||||
<Link href="/#/about">about page</Link> to see if your
|
||||
instance is running the latest version.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ActivityPage);
|
|
@ -45,25 +45,5 @@ export const styles = (theme: Theme) =>
|
|||
},
|
||||
pollWizardFlexGrow: {
|
||||
flexGrow: 1
|
||||
},
|
||||
draftDisplayArea: {
|
||||
display: "flex",
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
borderColor: theme.palette.action.disabledBackground,
|
||||
borderWidth: 0.25,
|
||||
borderStyle: "solid",
|
||||
borderRadius: 2,
|
||||
verticalAlign: "middle",
|
||||
marginLeft: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
draftText: {
|
||||
padding: theme.spacing.unit / 2
|
||||
},
|
||||
draftFlexGrow: {
|
||||
flexGrow: 1
|
||||
}
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ import { parse as parseParams, ParsedQuery } from "query-string";
|
|||
import { styles } from "./Compose.styles";
|
||||
import { UAccount } from "../types/Account";
|
||||
import { Visibility } from "../types/Visibility";
|
||||
import AttachFileIcon from "@material-ui/icons/AttachFile";
|
||||
import CameraAltIcon from "@material-ui/icons/CameraAlt";
|
||||
import TagFacesIcon from "@material-ui/icons/TagFaces";
|
||||
import HowToVoteIcon from "@material-ui/icons/HowToVote";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
|
@ -39,140 +39,55 @@ import ComposeMediaAttachment from "../components/ComposeMediaAttachment";
|
|||
import EmojiPicker from "../components/EmojiPicker";
|
||||
import { DateTimePicker, MuiPickersUtilsProvider } from "material-ui-pickers";
|
||||
import MomentUtils from "@date-io/moment";
|
||||
import {
|
||||
getUserDefaultVisibility,
|
||||
getConfig,
|
||||
getUserDefaultBool
|
||||
} from "../utilities/settings";
|
||||
import { draftExists, writeDraft, loadDraft } from "../utilities/compose";
|
||||
import { getUserDefaultVisibility, getConfig } from "../utilities/settings";
|
||||
|
||||
/**
|
||||
* The state for the Composer page.
|
||||
*/
|
||||
interface IComposerState {
|
||||
/**
|
||||
* The current user as an Account.
|
||||
*/
|
||||
account: UAccount;
|
||||
|
||||
/**
|
||||
* The visibility of the post.
|
||||
*/
|
||||
visibility: Visibility;
|
||||
|
||||
/**
|
||||
* Whether there should be a content warning.
|
||||
*/
|
||||
sensitive: boolean;
|
||||
|
||||
/**
|
||||
* The content warning message.
|
||||
*/
|
||||
sensitiveText?: string;
|
||||
|
||||
/**
|
||||
* Whether the visibility drop-down should be visible.
|
||||
*/
|
||||
visibilityMenu: boolean;
|
||||
|
||||
/**
|
||||
* The text contents of the post.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* The remaining amount of characters.
|
||||
*/
|
||||
remainingChars: number;
|
||||
|
||||
/**
|
||||
* An optional reply ID.
|
||||
*/
|
||||
reply?: string;
|
||||
|
||||
/**
|
||||
* The account to reply to, if it exists.
|
||||
*/
|
||||
acct?: string;
|
||||
|
||||
/**
|
||||
* An optional list of media attachments.
|
||||
*/
|
||||
attachments?: [Attachment];
|
||||
|
||||
/**
|
||||
* An optional poll for the post.
|
||||
*/
|
||||
poll?: PollWizard;
|
||||
|
||||
/**
|
||||
* The expiration date of a poll, if it exists.
|
||||
*/
|
||||
pollExpiresDate?: any;
|
||||
|
||||
/**
|
||||
* Whether the emoji picker should be visible.
|
||||
*/
|
||||
showEmojis: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the account's instance is federated.
|
||||
*/
|
||||
federated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Compose page contains all of the information to create a UI for post creation.
|
||||
*/
|
||||
class Composer extends Component<any, IComposerState> {
|
||||
/**
|
||||
* The Mastodon client to work with.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* Construct the Compose page by generating the Mastodon client and setting default values.
|
||||
* @param props The properties passed into the Compose component, usually the page queries.
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Generate the Mastodon client
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
|
||||
// Set the initial state
|
||||
this.state = {
|
||||
account: JSON.parse(localStorage.getItem("account") as string),
|
||||
visibility: getUserDefaultVisibility(),
|
||||
sensitive: false,
|
||||
visibilityMenu: false,
|
||||
text: "",
|
||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
||||
? 500
|
||||
: 9999999999999,
|
||||
remainingChars: 500,
|
||||
showEmojis: false,
|
||||
federated: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run any additional state checks and setup once the page has mounted. This includes
|
||||
* parsing the query parameters and loading the configuration, as well as defining the
|
||||
* clipboard listener.
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Parse the parameters and get the account information if available.
|
||||
let state = this.getComposerParams(this.props);
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
this.client.get("/accounts/verify_credentials").then((resp: any) => {
|
||||
let account: UAccount = resp.data;
|
||||
this.setState({ account });
|
||||
});
|
||||
|
||||
// Get the configuration and load the config values.
|
||||
getConfig().then((config: any) => {
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts,
|
||||
|
@ -180,43 +95,11 @@ class Composer extends Component<any, IComposerState> {
|
|||
acct: state.acct,
|
||||
visibility: state.visibility,
|
||||
text,
|
||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
||||
? 500 - text.length
|
||||
: 99999999
|
||||
remainingChars: 500 - text.length
|
||||
});
|
||||
});
|
||||
|
||||
// Attach the paste listener to listen for the clipboard and upload media
|
||||
// if possible.
|
||||
window.addEventListener("paste", (evt: Event) => {
|
||||
let thePasteEvent = evt as ClipboardEvent;
|
||||
let fileList: File[] = [];
|
||||
if (thePasteEvent.clipboardData != null) {
|
||||
let clipitems = thePasteEvent.clipboardData.items;
|
||||
if (clipitems != undefined) {
|
||||
for (let i = 0; i < clipitems.length; i++) {
|
||||
if (clipitems[i].type.indexOf("image") != -1) {
|
||||
let clipfile = clipitems[i].getAsFile();
|
||||
if (clipfile != null) {
|
||||
fileList.push(clipfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileList.length > 0) {
|
||||
this.uploadMedia(fileList);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the properties and set the state to those new properties. This usually
|
||||
* occurs when the page is either reloaded or changes but React doesn't see the
|
||||
* properties change.
|
||||
* @param props The properties passed into the Compose component, usually the page queries.
|
||||
*/
|
||||
componentWillReceiveProps(props: any) {
|
||||
let state = this.getComposerParams(props);
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
|
@ -225,42 +108,10 @@ class Composer extends Component<any, IComposerState> {
|
|||
acct: state.acct,
|
||||
visibility: state.visibility,
|
||||
text,
|
||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
||||
? 500 - text.length
|
||||
: 99999999
|
||||
remainingChars: 500 - text.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is unsaved text and store it as a draft.
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.state.text !== "") {
|
||||
writeDraft(
|
||||
this.state.text,
|
||||
this.state.reply ? Number(this.state.reply) : -999
|
||||
);
|
||||
this.props.enqueueSnackbar("Draft saved.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the draft from session storage and pre-load it into the state.
|
||||
*/
|
||||
restoreDraft() {
|
||||
const draft = loadDraft();
|
||||
const text = draft.contents;
|
||||
const reply =
|
||||
draft.replyId !== -999 ? draft.replyId.toString() : undefined;
|
||||
this.setState({ text, reply });
|
||||
this.props.enqueueSnackbar("Restored draft.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the location string and attempt to parse it into a parsed query.
|
||||
* @param location The location string from React Router.
|
||||
* @returns The ParsedQuery object containing all of the parameters.
|
||||
*/
|
||||
checkComposerParams(location?: string): ParsedQuery {
|
||||
let params = "";
|
||||
if (location !== undefined && typeof location === "string") {
|
||||
|
@ -271,11 +122,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
return parseParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the property's location string, parse it, and return it.
|
||||
* @param props The properties passed into the Compose component, usually the page queries.
|
||||
* @returns An object containing the reply ID, reply account, and visibility.
|
||||
*/
|
||||
getComposerParams(props: any) {
|
||||
let params = this.checkComposerParams(props.location);
|
||||
let reply: string = "";
|
||||
|
@ -298,44 +144,52 @@ class Composer extends Component<any, IComposerState> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text in the state and calculate the remaining character length.
|
||||
* @param text The text to update the state to
|
||||
*/
|
||||
updateTextFromField(text: string) {
|
||||
this.setState({
|
||||
text,
|
||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
||||
? 500 - text.length
|
||||
: 99999999
|
||||
});
|
||||
this.setState({ text, remainingChars: 500 - text.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the content warning text in the state
|
||||
* @param sensitiveText The text to update the state to
|
||||
*/
|
||||
updateWarningFromField(sensitiveText: string) {
|
||||
this.setState({ sensitiveText });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visibility in the state
|
||||
* @param visibility The visibility to update the state to
|
||||
*/
|
||||
changeVisibility(visibility: Visibility) {
|
||||
this.setState({ visibility });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file dialog to let the user choose files to upload to the server and then upload them.
|
||||
*/
|
||||
promptMediaDialog() {
|
||||
uploadMedia() {
|
||||
filedialog({
|
||||
multiple: false,
|
||||
accept: ".jpeg,.jpg,.png,.gif,.webm,.mp4,.mov,.ogg,.wav,.mp3,.flac"
|
||||
accept: "image/*, video/*"
|
||||
})
|
||||
.then((media: FileList) => this.uploadMedia(media))
|
||||
.then((media: FileList) => {
|
||||
let mediaForm = new FormData();
|
||||
mediaForm.append("file", media[0]);
|
||||
this.props.enqueueSnackbar("Uploading media...", {
|
||||
persist: true,
|
||||
key: "media-upload"
|
||||
});
|
||||
this.client
|
||||
.post("/media", mediaForm)
|
||||
.then((resp: any) => {
|
||||
let attachment: Attachment = resp.data;
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
attachments.push(attachment);
|
||||
} else {
|
||||
attachments = [attachment];
|
||||
}
|
||||
this.setState({ attachments });
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar("Media uploaded.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't upload media: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't get media: " + err.name, {
|
||||
variant: "error"
|
||||
|
@ -344,54 +198,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a list of files to Mastodon as attachments. Reads the first item in the list.
|
||||
* This also updates the attachments state after a successful upload.
|
||||
* @param media The list of files (`FileList` or `File[]`) to send to Mastodon.
|
||||
*/
|
||||
uploadMedia(media: FileList | File[]) {
|
||||
// Create a new FormData for Mastodon
|
||||
let mediaForm = new FormData();
|
||||
mediaForm.append("file", media[0]);
|
||||
|
||||
// Let the user know we're uploading the file
|
||||
this.props.enqueueSnackbar("Uploading media...", {
|
||||
persist: true,
|
||||
key: "media-upload"
|
||||
});
|
||||
|
||||
// Try to upload the media to the server.
|
||||
this.client
|
||||
.post("/media", mediaForm)
|
||||
|
||||
// If we succeed, get the attachments and update the state.
|
||||
.then((resp: any) => {
|
||||
let attachment: Attachment = resp.data;
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
attachments.push(attachment);
|
||||
} else {
|
||||
attachments = [attachment];
|
||||
}
|
||||
this.setState({ attachments });
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar("Media uploaded.");
|
||||
})
|
||||
|
||||
// If we fail, display an error.
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't upload media: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the attachments and grab the attachments' IDs.
|
||||
* @returns A list of IDs as `string[]`
|
||||
*/
|
||||
getOnlyMediaIds() {
|
||||
let ids: string[] = [];
|
||||
if (this.state.attachments) {
|
||||
|
@ -402,10 +208,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of attachments by inserting an attachment.
|
||||
* @param attachment The attachment to insert into the attachments list.
|
||||
*/
|
||||
fetchAttachmentAfterUpdate(attachment: Attachment) {
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
|
@ -418,10 +220,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an attachment from the list of attachments and update the state.
|
||||
* @param attachment The attachment to remove from the list
|
||||
*/
|
||||
deleteMediaAttachment(attachment: Attachment) {
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
|
@ -435,10 +233,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an emoji at the end of text string and update the state
|
||||
* @param e The emoji to insert into the text
|
||||
*/
|
||||
insertEmoji(e: any) {
|
||||
if (e.custom) {
|
||||
let text = this.state.text + e.colons;
|
||||
|
@ -455,9 +249,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty poll.
|
||||
*/
|
||||
createPoll() {
|
||||
if (this.state.poll === undefined) {
|
||||
let expiration = new Date();
|
||||
|
@ -477,9 +268,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new poll item into the poll.
|
||||
*/
|
||||
addPollItem() {
|
||||
if (
|
||||
this.state.poll !== undefined &&
|
||||
|
@ -502,11 +290,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an existing poll item with new text
|
||||
* @param position The position of the poll item in the list
|
||||
* @param newTitle The new text to update
|
||||
*/
|
||||
editPollItem(position: number, newTitle: any) {
|
||||
if (this.state.poll !== undefined) {
|
||||
let poll = this.state.poll;
|
||||
|
@ -524,10 +307,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a poll item from the poll
|
||||
* @param item The item to remove
|
||||
*/
|
||||
removePollItem(item: string) {
|
||||
if (
|
||||
this.state.poll !== undefined &&
|
||||
|
@ -554,16 +333,13 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiration date of the poll.
|
||||
* @param date The new expiration date
|
||||
*/
|
||||
setPollExpires(date: string) {
|
||||
let currentDate = new Date();
|
||||
let newDate = new Date(date);
|
||||
let poll = this.state.poll;
|
||||
if (poll) {
|
||||
let expiry = (newDate.getTime() - currentDate.getTime()) / 1000;
|
||||
console.log(expiry);
|
||||
if (expiry >= 1800) {
|
||||
poll.expires_at = expiry.toString();
|
||||
this.setState({ poll, pollExpiresDate: date });
|
||||
|
@ -577,38 +353,25 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the poll from the post.
|
||||
*/
|
||||
removePoll() {
|
||||
this.setState({
|
||||
poll: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user presses the Ctrl/Cmd+Enter key and post to the server if possible.
|
||||
* @param event The keyboard event
|
||||
*/
|
||||
postViaKeyboard(event: any) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) {
|
||||
this.post();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the post to Mastodon and return to the previous page, if possible.
|
||||
*/
|
||||
post() {
|
||||
// First, finalize the poll.
|
||||
let pollOptions: string[] = [];
|
||||
if (this.state.poll) {
|
||||
this.state.poll.options.forEach((option: PollWizardOption) => {
|
||||
pollOptions.push(option.title);
|
||||
});
|
||||
}
|
||||
|
||||
// Send a post request to Mastodon.
|
||||
this.client
|
||||
.post("/statuses", {
|
||||
status: this.state.text,
|
||||
|
@ -625,52 +388,31 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
: null
|
||||
})
|
||||
|
||||
// If we succeed, send a success message, clear the status
|
||||
// text field, and go back.
|
||||
.then(() => {
|
||||
this.props.enqueueSnackbar("Posted!");
|
||||
|
||||
// This is necessary to prevent session drafts from saving
|
||||
// posts that were already posted.
|
||||
this.setState({ text: "" });
|
||||
|
||||
window.history.back();
|
||||
})
|
||||
|
||||
// Otherwise, show an error message and don't do anything.
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
||||
console.error(err.message);
|
||||
console.log(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the content warning section.
|
||||
*/
|
||||
toggleSensitive() {
|
||||
this.setState({ sensitive: !this.state.sensitive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the visibility drop down menu.
|
||||
*/
|
||||
toggleVisibilityMenu() {
|
||||
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the emoji picker.
|
||||
*/
|
||||
toggleEmojis() {
|
||||
this.setState({ showEmojis: !this.state.showEmojis });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all of the components on the page given a set of classes.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
console.log(this.state);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -727,28 +469,18 @@ class Composer extends Component<any, IComposerState> {
|
|||
}}
|
||||
value={this.state.text}
|
||||
/>
|
||||
{getUserDefaultBool("imposeCharacterLimit") ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={
|
||||
this.state.remainingChars <= 100
|
||||
? classes.charsReachingLimit
|
||||
: null
|
||||
}
|
||||
>
|
||||
{`${this.state.remainingChars} character${
|
||||
this.state.remainingChars === 1 ? "" : "s"
|
||||
} remaining`}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="caption">
|
||||
<WarningIcon className={classes.warningCaption} />{" "}
|
||||
You have the character limit turned off. Make sure
|
||||
that your post matches your instance's character
|
||||
limit before posting.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={
|
||||
this.state.remainingChars <= 100
|
||||
? classes.charsReachingLimit
|
||||
: null
|
||||
}
|
||||
>
|
||||
{`${this.state.remainingChars} character${
|
||||
this.state.remainingChars === 1 ? "" : "s"
|
||||
} remaining`}
|
||||
</Typography>
|
||||
{this.state.attachments &&
|
||||
this.state.attachments.length > 0 ? (
|
||||
<div className={classes.composeAttachmentArea}>
|
||||
|
@ -873,13 +605,13 @@ class Composer extends Component<any, IComposerState> {
|
|||
) : null}
|
||||
</DialogContent>
|
||||
<Toolbar className={classes.dialogActions}>
|
||||
<Tooltip title="Add photos, videos, or audio">
|
||||
<Tooltip title="Add photos or videos">
|
||||
<IconButton
|
||||
disabled={this.state.poll !== undefined}
|
||||
onClick={() => this.promptMediaDialog()}
|
||||
onClick={() => this.uploadMedia()}
|
||||
id="compose-media"
|
||||
>
|
||||
<AttachFileIcon />
|
||||
<CameraAltIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Insert emoji">
|
||||
|
@ -962,21 +694,6 @@ class Composer extends Component<any, IComposerState> {
|
|||
) : null}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
{draftExists() ? (
|
||||
<DialogContent className={classes.draftDisplayArea}>
|
||||
<Typography className={classes.draftText}>
|
||||
You have an unsaved post.
|
||||
</Typography>
|
||||
<div className={classes.draftFlexGrow} />
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => this.restoreDraft()}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</DialogContent>
|
||||
) : null}
|
||||
<DialogActions>
|
||||
<Button color="secondary" onClick={() => this.post()}>
|
||||
Post
|
||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
|||
import { Status } from "../types/Status";
|
||||
import Mastodon, { StreamListener } from "megalodon";
|
||||
import { withSnackbar } from "notistack";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
|
||||
interface IHomePageState {
|
||||
|
@ -25,14 +23,8 @@ interface IHomePageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
isMasonryLayout?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class for the home timeline.
|
||||
* @deprecated Use TimelinePage with the props `timeline="/timelines/home"`
|
||||
* and `stream="/streaming/user"`.
|
||||
*/
|
||||
class HomePage extends Component<any, IHomePageState> {
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
@ -42,8 +34,7 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
backlogPosts: null
|
||||
};
|
||||
|
||||
this.client = new Mastodon(
|
||||
|
@ -163,11 +154,9 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
|
@ -196,46 +185,15 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.isMasonryLayout ? (
|
||||
<Masonry
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={
|
||||
classes["my-masonry-grid_column"]
|
||||
}
|
||||
>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
>
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
) : (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||
<div
|
||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
|||
import { Status } from "../types/Status";
|
||||
import Mastodon, { StreamListener } from "megalodon";
|
||||
import { withSnackbar } from "notistack";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
|
||||
interface ILocalPageState {
|
||||
|
@ -25,14 +23,8 @@ interface ILocalPageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
isMasonryLayout?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class for the local timeline.
|
||||
* @deprecated Use TimelinePage with the props `timeline="/timelines/public?local=true"`
|
||||
* and `stream="/streaming/public/local"`.
|
||||
*/
|
||||
class LocalPage extends Component<any, ILocalPageState> {
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
@ -42,8 +34,7 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
backlogPosts: null
|
||||
};
|
||||
|
||||
this.client = new Mastodon(
|
||||
|
@ -164,12 +155,9 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
|
@ -198,50 +186,15 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.isMasonryLayout ? (
|
||||
<Masonry
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={
|
||||
classes["my-masonry-grid_column"]
|
||||
}
|
||||
>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
>
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
) : (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
>
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||
<div
|
||||
|
|
|
@ -10,12 +10,10 @@ import {
|
|||
ListItemAvatar,
|
||||
Avatar,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
Typography
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import ForumIcon from "@material-ui/icons/Forum";
|
||||
import MailIcon from "@material-ui/icons/Mail";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import Mastodon from "megalodon";
|
||||
import { Status } from "../types/Status";
|
||||
|
@ -72,82 +70,67 @@ class MessagesPage extends Component<any, IMessagesState> {
|
|||
return innerContent;
|
||||
}
|
||||
|
||||
renderMessage(message: Status) {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${message.account.id}`}
|
||||
alt={message.account.username}
|
||||
src={message.account.avatar_static}
|
||||
>
|
||||
<PersonIcon />
|
||||
</LinkableAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
message.account.display_name ||
|
||||
"@" + message.account.acct
|
||||
}
|
||||
secondary={this.removeHTMLContent(message.content)}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View conversation">
|
||||
<LinkableIconButton to={`/conversation/${message.id}`}>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{this.state.viewDidLoad ? (
|
||||
<div className={classes.pageListContsraints}>
|
||||
{this.state.posts && this.state.posts.length > 0 ? (
|
||||
<div>
|
||||
<ListSubheader>Recent messages</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{this.state.posts
|
||||
? this.state.posts.map(
|
||||
(message: Status) =>
|
||||
this.renderMessage(
|
||||
message
|
||||
)
|
||||
)
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
classes.pageLayoutEmptyTextConstraints
|
||||
}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<MailIcon
|
||||
color="action"
|
||||
style={{ fontSize: 48 }}
|
||||
/>
|
||||
<Typography variant="h6">
|
||||
You don't have any messages.
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Why not interact with the fediverse a
|
||||
bit by sending a message?
|
||||
</Typography>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ListSubheader>Recent messages</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{this.state.posts
|
||||
? this.state.posts.map(
|
||||
(message: Status) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${message.account.id}`}
|
||||
alt={
|
||||
message
|
||||
.account
|
||||
.username
|
||||
}
|
||||
src={
|
||||
message
|
||||
.account
|
||||
.avatar_static
|
||||
}
|
||||
>
|
||||
<PersonIcon />
|
||||
</LinkableAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
message.account
|
||||
.display_name ||
|
||||
"@" +
|
||||
message
|
||||
.account
|
||||
.acct
|
||||
}
|
||||
secondary={this.removeHTMLContent(
|
||||
message.content
|
||||
)}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View conversation">
|
||||
<LinkableIconButton
|
||||
to={`/conversation/${message.id}`}
|
||||
>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br />
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.viewIsLoading ? (
|
||||
|
|
|
@ -17,128 +17,56 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import {
|
||||
LinkableIconButton,
|
||||
LinkableAvatar,
|
||||
LinkableMenuItem
|
||||
} from "../interfaces/overrides";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import ForumIcon from "@material-ui/icons/Forum";
|
||||
import ReplyIcon from "@material-ui/icons/Reply";
|
||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||
|
||||
import Mastodon from "megalodon";
|
||||
import { Notification } from "../types/Notification";
|
||||
import { Account } from "../types/Account";
|
||||
import { Relationship } from "../types/Relationship";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { Dictionary } from "../interfaces/utils";
|
||||
|
||||
/**
|
||||
* The state interface for the notifications page.
|
||||
*/
|
||||
interface INotificationsPageState {
|
||||
/**
|
||||
* The list of notifications, if it exists.
|
||||
*/
|
||||
notifications?: [Notification];
|
||||
|
||||
/**
|
||||
* Whether the view is still loading.
|
||||
*/
|
||||
viewIsLoading: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view has loaded.
|
||||
*/
|
||||
viewDidLoad?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view has loaded but in error.
|
||||
*/
|
||||
viewDidError?: boolean;
|
||||
|
||||
/**
|
||||
* The error code for an errored state, if possible.
|
||||
*/
|
||||
viewDidErrorCode?: string;
|
||||
|
||||
/**
|
||||
* Whether the delete confirmation dialog should be open.
|
||||
*/
|
||||
deleteDialogOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the menu should be open on smaller devices.
|
||||
*/
|
||||
mobileMenuOpen: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The notifications page.
|
||||
*/
|
||||
class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||
/**
|
||||
* The Mastodon object to perform notification operations on.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* The stream listener for tuning in to notifications.
|
||||
*/
|
||||
streamListener: any;
|
||||
|
||||
/**
|
||||
* Construct the notifications page.
|
||||
* @param props The properties to pass in
|
||||
*/
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Create the Mastodon object.
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
|
||||
// Initialize the state.
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
deleteDialogOpen: false,
|
||||
mobileMenuOpen: {}
|
||||
deleteDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform pre-mount tasks.
|
||||
*/
|
||||
componentWillMount() {
|
||||
// Get the list of notifications and update the state.
|
||||
this.client
|
||||
.get("/notifications")
|
||||
.then((resp: any) => {
|
||||
let notifications: [Notification] = resp.data;
|
||||
let notifMenus: Dictionary<boolean> = {};
|
||||
|
||||
notifications.forEach((notif: Notification) => {
|
||||
notifMenus[notif.id] = false;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
notifications,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
mobileMenuOpen: notifMenus
|
||||
viewDidLoad: true
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
|
@ -151,17 +79,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post-mount tasks.
|
||||
*/
|
||||
componentDidMount() {
|
||||
// Start listening for new notifications after fetching.
|
||||
this.streamNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a stream listener and keep updating notifications.
|
||||
*/
|
||||
streamNotifications() {
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
|
@ -174,25 +95,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the state of the delete dialog.
|
||||
*/
|
||||
toggleDeleteDialog() {
|
||||
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
||||
}
|
||||
|
||||
toggleMobileMenu(id: string) {
|
||||
let mobileMenuOpen = this.state.mobileMenuOpen;
|
||||
mobileMenuOpen[id] = !mobileMenuOpen[id];
|
||||
this.setState({ mobileMenuOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML content from a string containing HTML content.
|
||||
*
|
||||
* @param text The sanitized HTML to strip
|
||||
* @returns A string containing the contents of the sanitized HTML
|
||||
*/
|
||||
removeHTMLContent(text: string) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = text;
|
||||
|
@ -202,10 +108,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
return innerContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a notification from the server.
|
||||
* @param id The notification's ID
|
||||
*/
|
||||
removeNotification(id: string) {
|
||||
this.client
|
||||
.post(`/notifications/${id}/dismiss`)
|
||||
|
@ -237,9 +139,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all notifications from the server.
|
||||
*/
|
||||
removeAllNotifications() {
|
||||
this.client
|
||||
.post("/notifications/clear")
|
||||
|
@ -257,10 +156,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single notification unit to be used in a list
|
||||
* @param notif The notification to work with.
|
||||
*/
|
||||
createNotification(notif: Notification) {
|
||||
const { classes } = this.props;
|
||||
let primary = "";
|
||||
|
@ -333,108 +228,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{this.getActions(notif)}
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow an account from a notification if already not followed.
|
||||
* @param acct The account to follow, if possible
|
||||
*/
|
||||
followMember(acct: Account) {
|
||||
// Get the relationships for this account.
|
||||
this.client
|
||||
.get(`/accounts/relationships`, { id: acct.id })
|
||||
.then((resp: any) => {
|
||||
// Returns a list, so grab only the first item.
|
||||
let relationship: Relationship = resp.data[0];
|
||||
|
||||
// Follow if not following already.
|
||||
if (relationship.following == false) {
|
||||
this.client
|
||||
.post(`/accounts/${acct.id}/follow`)
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"You are now following this account."
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't follow account: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise notify the user.
|
||||
else {
|
||||
this.props.enqueueSnackbar(
|
||||
"You already follow this account."
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't find relationship.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getActions = (notif: Notification) => {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => this.toggleMobileMenu(notif.id)}
|
||||
className={classes.mobileOnly}
|
||||
id={`notification-list-${notif.id}`}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
open={this.state.mobileMenuOpen[notif.id]}
|
||||
anchorEl={document.getElementById(
|
||||
`notification-list-${notif.id}`
|
||||
)}
|
||||
onClose={() => this.toggleMobileMenu(notif.id)}
|
||||
>
|
||||
{notif.type == "follow" ? (
|
||||
<>
|
||||
<LinkableMenuItem
|
||||
to={`profile/${notif.account.id}`}
|
||||
>
|
||||
View Profile
|
||||
</LinkableMenuItem>
|
||||
<MenuItem
|
||||
onClick={() => this.followMember(notif.account)}
|
||||
>
|
||||
Follow
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
{notif.type == "mention" && notif.status ? (
|
||||
<LinkableMenuItem
|
||||
to={`/compose?reply=${
|
||||
notif.status.reblog
|
||||
? notif.status.reblog.id
|
||||
: notif.status.id
|
||||
}&visibility=${notif.status.visibility}&acct=${
|
||||
notif.status.reblog
|
||||
? notif.status.reblog.account.acct
|
||||
: notif.status.account.acct
|
||||
}`}
|
||||
>
|
||||
Reply
|
||||
</LinkableMenuItem>
|
||||
) : null}
|
||||
<MenuItem onClick={() => this.removeNotification(notif.id)}>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<div className={classes.desktopOnly}>
|
||||
{notif.type === "follow" ? (
|
||||
<span>
|
||||
<Tooltip title="View profile">
|
||||
|
@ -492,14 +285,28 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
followMember(acct: Account) {
|
||||
this.client
|
||||
.post(`/accounts/${acct.id}/follow`)
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"You are now following this account."
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't follow account: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the notification page.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -530,20 +337,12 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
</Paper>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classes.pageLayoutEmptyTextConstraints}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<NotificationsIcon
|
||||
color="action"
|
||||
style={{ fontSize: 48 }}
|
||||
/>
|
||||
<Typography variant="h6">All clear!</Typography>
|
||||
<div className={classes.pageLayoutEmptyTextConstraints}>
|
||||
<Typography variant="h4">All clear!</Typography>
|
||||
<Typography paragraph>
|
||||
It looks like you have no notifications. Why not
|
||||
get the conversation going with a new post?
|
||||
</Typography>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Theme, createStyles, FormHelperText } from "@material-ui/core";
|
||||
import { Theme, createStyles } from "@material-ui/core";
|
||||
import { isDarwinApp } from "../utilities/desktop";
|
||||
import { isAppbarExpanded } from "../utilities/appbar";
|
||||
|
||||
|
@ -323,21 +323,5 @@ export const styles = (theme: Theme) =>
|
|||
display: "block"
|
||||
},
|
||||
backgroundColor: theme.palette.primary.main
|
||||
},
|
||||
pageLayoutMasonry: {
|
||||
paddingLeft: theme.spacing.unit * 3,
|
||||
paddingRight: theme.spacing.unit * 3
|
||||
},
|
||||
masonryGrid: {
|
||||
display: "flex",
|
||||
width: "auto"
|
||||
},
|
||||
"my-masonry-grid_column": {
|
||||
// non-standard name fixes react-masonry-css bug :shrug:
|
||||
padding: 5
|
||||
},
|
||||
noTopPaddingMargin: {
|
||||
marginTop: 0,
|
||||
paddingTop: 0
|
||||
}
|
||||
});
|
||||
|
|
|
@ -25,8 +25,6 @@ import Post from "../components/Post";
|
|||
import { withSnackbar } from "notistack";
|
||||
import { LinkableIconButton } from "../interfaces/overrides";
|
||||
import { emojifyString } from "../utilities/emojis";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "..//utilities/settings";
|
||||
|
||||
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
|
@ -46,7 +44,6 @@ interface IProfilePageState {
|
|||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: string;
|
||||
blockDialogOpen: boolean;
|
||||
isMasonryLayout?: boolean;
|
||||
}
|
||||
|
||||
class ProfilePage extends Component<any, IProfilePageState> {
|
||||
|
@ -62,8 +59,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
blockDialogOpen: false,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
blockDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -309,36 +305,8 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
}
|
||||
}
|
||||
|
||||
renderPosts(posts: Status[]) {
|
||||
const { classes } = this.props;
|
||||
const postComponents = posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client} />;
|
||||
});
|
||||
if (this.state.isMasonryLayout) {
|
||||
return (
|
||||
<Masonry
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={classes["my-masonry-grid_column"]}
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
>
|
||||
{postComponents}
|
||||
</Masonry>
|
||||
);
|
||||
} else {
|
||||
return <div>{postComponents}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageContentLayoutConstraints} ${
|
||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
return (
|
||||
<div className={classes.pageLayoutMinimalConstraints}>
|
||||
<div className={classes.pageHeroBackground}>
|
||||
|
@ -496,7 +464,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={containerClasses}>
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
{this.state.viewDidError ? (
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
|
@ -514,7 +482,15 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
)}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.renderPosts(this.state.posts)}
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
{this.state.viewDidLoad &&
|
||||
!this.state.viewDidError ? (
|
||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
|||
import { Status } from "../types/Status";
|
||||
import Mastodon, { StreamListener } from "megalodon";
|
||||
import { withSnackbar } from "notistack";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
|
||||
interface IPublicPageState {
|
||||
|
@ -25,14 +23,8 @@ interface IPublicPageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
isMasonryLayout?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class for the public timeline.
|
||||
* @deprecated Use TimelinePage with the props `timeline="/timelines/public"`
|
||||
* and `stream="/streaming/public"`.
|
||||
*/
|
||||
class PublicPage extends Component<any, IPublicPageState> {
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
@ -42,8 +34,7 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
backlogPosts: null
|
||||
};
|
||||
|
||||
this.client = new Mastodon(
|
||||
|
@ -163,12 +154,9 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
|
@ -197,50 +185,15 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.isMasonryLayout ? (
|
||||
<Masonry
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={
|
||||
classes["my-masonry-grid_column"]
|
||||
}
|
||||
>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
>
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
) : (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
>
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||
<div
|
||||
|
|
|
@ -6,22 +6,25 @@ import {
|
|||
ListItem,
|
||||
Paper,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemAvatar,
|
||||
ListSubheader,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Link
|
||||
Divider,
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import Mastodon from "megalodon";
|
||||
import { Account } from "../types/Account";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
import CheckIcon from "@material-ui/icons/Check";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
|
||||
interface IRecommendationsPageProps extends withSnackbarProps {
|
||||
classes: any;
|
||||
|
@ -32,6 +35,7 @@ interface IRecommendationsPageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: Boolean;
|
||||
viewDidErrorCode?: string;
|
||||
requestedFollows?: [Account];
|
||||
followSuggestions?: [Account];
|
||||
}
|
||||
|
||||
|
@ -53,6 +57,21 @@ class RecommendationsPage extends Component<
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.client
|
||||
.get("/follow_requests")
|
||||
.then((resp: any) => {
|
||||
let requestedFollows: [Account] = resp.data;
|
||||
this.setState({ requestedFollows });
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.name
|
||||
});
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
this.client
|
||||
.get("/suggestions")
|
||||
.then((resp: any) => {
|
||||
|
@ -106,6 +125,110 @@ class RecommendationsPage extends Component<
|
|||
});
|
||||
}
|
||||
|
||||
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
|
||||
this.client
|
||||
.post(`/follow_requests/${acct.id}/${type}`)
|
||||
.then((resp: any) => {
|
||||
let requestedFollows = this.state.requestedFollows;
|
||||
if (requestedFollows) {
|
||||
requestedFollows.forEach(
|
||||
(request: Account, index: number) => {
|
||||
if (requestedFollows && request.id === acct.id) {
|
||||
requestedFollows.splice(index, 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
this.setState({ requestedFollows });
|
||||
|
||||
let verb: string = type;
|
||||
verb === "authorize"
|
||||
? (verb = "authorized")
|
||||
: (verb = "rejected");
|
||||
this.props.enqueueSnackbar(`You have ${verb} this request.`);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't ${type} this request: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
showFollowRequests() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Follow requests</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{this.state.requestedFollows
|
||||
? this.state.requestedFollows.map(
|
||||
(request: Account) => {
|
||||
return (
|
||||
<ListItem key={request.id}>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${request.id}`}
|
||||
alt={request.username}
|
||||
src={
|
||||
request.avatar_static
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
request.display_name ||
|
||||
request.acct
|
||||
}
|
||||
secondary={request.acct}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Accept request">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(
|
||||
request,
|
||||
"authorize"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reject request">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(
|
||||
request,
|
||||
"reject"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton
|
||||
to={`/profile/${request.id}`}
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
showFollowSuggestions() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
|
@ -172,6 +295,23 @@ class RecommendationsPage extends Component<
|
|||
<div className={classes.pageLayoutConstraints}>
|
||||
{this.state.viewDidLoad ? (
|
||||
<div>
|
||||
{this.state.requestedFollows &&
|
||||
this.state.requestedFollows.length > 0 ? (
|
||||
this.showFollowRequests()
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
classes.pageLayoutEmptyTextConstraints
|
||||
}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
You don't have any follow requests.
|
||||
</Typography>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
<Divider />
|
||||
<br />
|
||||
{this.state.followSuggestions &&
|
||||
this.state.followSuggestions.length > 0 ? (
|
||||
this.showFollowSuggestions()
|
||||
|
@ -180,35 +320,23 @@ class RecommendationsPage extends Component<
|
|||
className={
|
||||
classes.pageLayoutEmptyTextConstraints
|
||||
}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<GroupIcon
|
||||
color="action"
|
||||
style={{ fontSize: 48 }}
|
||||
/>
|
||||
<Typography variant="h6">
|
||||
<Typography variant="h5">
|
||||
We don't have any suggestions for you.
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Take a look around the fediverse or check
|
||||
out the Activity page for more.
|
||||
Why not interact with the fediverse a bit by
|
||||
creating a new post?
|
||||
</Typography>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
<br />
|
||||
<Typography variant="caption" paragraph>
|
||||
Looking for follow requests? You can find them in
|
||||
Settings or in the account menu. You can also{" "}
|
||||
<Link href="/#/requests">click here</Link>.
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.viewDidError ? (
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">
|
||||
Something went wrong when loading recommendations.
|
||||
Something went wrong when loading this timeline.
|
||||
</Typography>
|
||||
<Typography>
|
||||
{this.state.viewDidErrorCode
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import {
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Typography,
|
||||
withStyles
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { Account } from "../types/Account";
|
||||
import Mastodon from "megalodon";
|
||||
import { LinkableAvatar, LinkableIconButton } from "../interfaces/overrides";
|
||||
import CheckIcon from "@material-ui/icons/Check";
|
||||
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
|
||||
import { withSnackbar } from "notistack";
|
||||
|
||||
interface IRequestsPageState {
|
||||
viewLoading: boolean;
|
||||
viewLoaded?: boolean;
|
||||
viewErrored?: boolean;
|
||||
requestedAccounts?: [Account];
|
||||
}
|
||||
|
||||
class RequestsPage extends Component<any, IRequestsPageState> {
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
this.state = {
|
||||
viewLoading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.client
|
||||
.get("/follow_requests")
|
||||
.then((resp: any) => {
|
||||
let requestedAccounts: [Account] = resp.data;
|
||||
this.setState({
|
||||
requestedAccounts,
|
||||
viewLoading: false,
|
||||
viewLoaded: true
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewLoading: false,
|
||||
viewErrored: true
|
||||
});
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
|
||||
this.client
|
||||
.post(`/follow_requests/${acct.id}/${type}`)
|
||||
.then((resp: any) => {
|
||||
let requestedAccounts = this.state.requestedAccounts;
|
||||
if (requestedAccounts) {
|
||||
requestedAccounts.forEach(
|
||||
(request: Account, index: number) => {
|
||||
if (requestedAccounts && request.id === acct.id) {
|
||||
requestedAccounts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
this.setState({ requestedAccounts });
|
||||
|
||||
let verb: string = type;
|
||||
verb === "authorize"
|
||||
? (verb = "authorized")
|
||||
: (verb = "rejected");
|
||||
this.props.enqueueSnackbar(`You have ${verb} this request.`);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't ${type} this request: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
showFollowRequests() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Follow requests</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{this.state.requestedAccounts
|
||||
? this.state.requestedAccounts.map(
|
||||
(request: Account) => {
|
||||
return (
|
||||
<ListItem key={request.id}>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${request.id}`}
|
||||
alt={request.username}
|
||||
src={
|
||||
request.avatar_static
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
request.display_name ||
|
||||
request.acct
|
||||
}
|
||||
secondary={request.acct}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Accept request">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(
|
||||
request,
|
||||
"authorize"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reject request">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(
|
||||
request,
|
||||
"reject"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton
|
||||
to={`/profile/${request.id}`}
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{this.state.viewLoaded ? (
|
||||
<div>
|
||||
{this.state.requestedAccounts &&
|
||||
this.state.requestedAccounts.length > 0 ? (
|
||||
this.showFollowRequests()
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
classes.pageLayoutEmptyTextConstraints
|
||||
}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<CheckCircleIcon
|
||||
color="action"
|
||||
style={{ fontSize: 48 }}
|
||||
/>
|
||||
<Typography variant="h6">
|
||||
You don't have any follow requests.
|
||||
</Typography>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.viewLoading ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<CircularProgress
|
||||
className={classes.progress}
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(withSnackbar(RequestsPage));
|
|
@ -26,8 +26,6 @@ import { withSnackbar } from "notistack";
|
|||
import Post from "../components/Post";
|
||||
import { Status } from "../types/Status";
|
||||
import { Account } from "../types/Account";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
|
||||
interface ISearchPageState {
|
||||
query: string[] | string;
|
||||
|
@ -38,7 +36,6 @@ interface ISearchPageState {
|
|||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: string;
|
||||
isMasonryLayout: boolean;
|
||||
}
|
||||
|
||||
class SearchPage extends Component<any, ISearchPageState> {
|
||||
|
@ -57,8 +54,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
query: searchParams.query,
|
||||
type: searchParams.type,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
||||
type: searchParams.type
|
||||
};
|
||||
|
||||
if (searchParams.type === "tag") {
|
||||
|
@ -199,7 +195,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
showAllAccountsFromQuery() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
<div>
|
||||
<ListSubheader>Accounts</ListSubheader>
|
||||
|
||||
{this.state.results &&
|
||||
|
@ -264,44 +260,22 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
);
|
||||
}
|
||||
|
||||
renderPosts(posts: Status[]) {
|
||||
const { classes } = this.props;
|
||||
const postComponents = posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client} />;
|
||||
});
|
||||
if (this.state.isMasonryLayout) {
|
||||
return (
|
||||
<Masonry
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={classes["my-masonry-grid_column"]}
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
>
|
||||
{postComponents}
|
||||
</Masonry>
|
||||
);
|
||||
} else {
|
||||
return <div>{postComponents}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
showAllPostsFromQuery() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutConstraints} ${
|
||||
this.state.isMasonryLayout
|
||||
? classes.pageLayoutMasonry + " " + classes.noTopPaddingMargin
|
||||
: ""
|
||||
}`;
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div>
|
||||
<ListSubheader>Posts</ListSubheader>
|
||||
{this.state.results ? (
|
||||
this.state.results.statuses.length > 0 ? (
|
||||
this.renderPosts(this.state.results.statuses)
|
||||
this.state.results.statuses.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
@ -317,15 +291,20 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
|
||||
showAllPostsWithTag() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints} ${
|
||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div>
|
||||
<ListSubheader>Tagged posts</ListSubheader>
|
||||
{this.state.tagResults ? (
|
||||
this.state.tagResults.length > 0 ? (
|
||||
this.renderPosts(this.state.tagResults)
|
||||
this.state.tagResults.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
@ -342,7 +321,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{this.state.type && this.state.type === "tag" ? (
|
||||
this.showAllPostsWithTag()
|
||||
) : (
|
||||
|
|
|
@ -62,11 +62,6 @@ import BellAlertIcon from "mdi-material-ui/BellAlert";
|
|||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import UndoIcon from "@material-ui/icons/Undo";
|
||||
import DomainDisabledIcon from "@material-ui/icons/DomainDisabled";
|
||||
import AccountSettingsIcon from "mdi-material-ui/AccountSettings";
|
||||
import AlphabeticalVariantOffIcon from "mdi-material-ui/AlphabeticalVariantOff";
|
||||
import DashboardIcon from "@material-ui/icons/Dashboard";
|
||||
import InfiniteIcon from "@material-ui/icons/AllInclusive";
|
||||
|
||||
import { Config } from "../types/Config";
|
||||
import { Account } from "../types/Account";
|
||||
import Mastodon from "megalodon";
|
||||
|
@ -87,9 +82,6 @@ interface ISettingsState {
|
|||
brandName: string;
|
||||
federated: boolean;
|
||||
currentUser?: Account;
|
||||
imposeCharacterLimit: boolean;
|
||||
masonryLayout?: boolean;
|
||||
infiniteScroll?: boolean;
|
||||
}
|
||||
|
||||
class SettingsPage extends Component<any, ISettingsState> {
|
||||
|
@ -120,10 +112,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
setHyperspaceTheme(defaultTheme),
|
||||
defaultVisibility: getUserDefaultVisibility() || "public",
|
||||
brandName: "Hyperspace",
|
||||
federated: true,
|
||||
imposeCharacterLimit: getUserDefaultBool("imposeCharacterLimit"),
|
||||
masonryLayout: getUserDefaultBool("isMasonryLayout"),
|
||||
infiniteScroll: getUserDefaultBool("isInfiniteScroll")
|
||||
federated: true
|
||||
};
|
||||
|
||||
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
||||
|
@ -132,8 +121,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
this.toggleBadgeCount = this.toggleBadgeCount.bind(this);
|
||||
this.toggleThemeDialog = this.toggleThemeDialog.bind(this);
|
||||
this.toggleVisibilityDialog = this.toggleVisibilityDialog.bind(this);
|
||||
this.toggleMasonryLayout = this.toggleMasonryLayout.bind(this);
|
||||
this.toggleInfiniteScroll = this.toggleInfiniteScroll.bind(this);
|
||||
this.changeThemeName = this.changeThemeName.bind(this);
|
||||
this.changeTheme = this.changeTheme.bind(this);
|
||||
this.setVisibility = this.setVisibility.bind(this);
|
||||
|
@ -174,6 +161,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
getConfig().then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
console.log(!config.federation.allowPublicPosts);
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts
|
||||
});
|
||||
|
@ -228,16 +216,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
});
|
||||
}
|
||||
|
||||
toggleCharacterLimit() {
|
||||
this.setState({
|
||||
imposeCharacterLimit: !this.state.imposeCharacterLimit
|
||||
});
|
||||
setUserDefaultBool(
|
||||
"imposeCharacterLimit",
|
||||
!this.state.imposeCharacterLimit
|
||||
);
|
||||
}
|
||||
|
||||
toggleResetDialog() {
|
||||
this.setState({
|
||||
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
|
||||
|
@ -248,16 +226,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
this.setState({ resetSettingsDialog: !this.state.resetSettingsDialog });
|
||||
}
|
||||
|
||||
toggleMasonryLayout() {
|
||||
this.setState({ masonryLayout: !this.state.masonryLayout });
|
||||
setUserDefaultBool("isMasonryLayout", !this.state.masonryLayout);
|
||||
}
|
||||
|
||||
toggleInfiniteScroll() {
|
||||
this.setState({ infiniteScroll: !this.state.infiniteScroll });
|
||||
setUserDefaultBool("isInfiniteScroll", !this.state.infiniteScroll);
|
||||
}
|
||||
|
||||
changeTheme() {
|
||||
setUserDefaultTheme(this.state.selectThemeName);
|
||||
window.location.reload();
|
||||
|
@ -534,7 +502,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</div>
|
||||
<div className={classes.pageGrow} />
|
||||
<Toolbar>
|
||||
<Tooltip title="Edit profile">
|
||||
<Tooltip title="Edit Profile">
|
||||
<LinkableIconButton
|
||||
to={"/you"}
|
||||
color="inherit"
|
||||
|
@ -550,14 +518,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
<DomainDisabledIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Manage follow requests">
|
||||
<LinkableIconButton
|
||||
to={"/requests"}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountSettingsIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Configure on Mastodon">
|
||||
<IconButton
|
||||
href={
|
||||
|
@ -576,36 +536,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</Toolbar>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.pageHeroBackground}>
|
||||
<div className={classes.pageHeroBackgroundImage} />
|
||||
<div className={classes.profileContent}>
|
||||
<br />
|
||||
<Avatar className={classes.settingsAvatar} />
|
||||
<div
|
||||
className={classes.profileUserBox}
|
||||
style={{ margin: "auto" }}
|
||||
>
|
||||
<Typography
|
||||
className={classes.settingsHeaderText}
|
||||
color="inherit"
|
||||
component="h1"
|
||||
>
|
||||
{"Loading..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="inherit"
|
||||
className={classes.settingsDetailText}
|
||||
component="p"
|
||||
>
|
||||
@{"..."}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.pageGrow} />
|
||||
<Toolbar />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
<ListSubheader>Appearance</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
|
@ -667,38 +598,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</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 />
|
||||
|
@ -723,25 +622,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</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 />
|
||||
|
|
|
@ -1,440 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Avatar,
|
||||
Slide,
|
||||
StyledComponentProps
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import Post from "../components/Post";
|
||||
import { Status } from "../types/Status";
|
||||
import Mastodon, { StreamListener } from "megalodon";
|
||||
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||
import Masonry from "react-masonry-css";
|
||||
import { getUserDefaultBool } from "../utilities/settings";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
|
||||
/**
|
||||
* The basic interface for a timeline page's properties.
|
||||
*/
|
||||
interface ITimelinePageProps extends withSnackbarProps, StyledComponentProps {
|
||||
/**
|
||||
* The API endpoint for the timeline to fetch after starting
|
||||
* a stream.
|
||||
*/
|
||||
timeline: string;
|
||||
|
||||
/**
|
||||
* The API endpoint for the timeline to stream.
|
||||
*/
|
||||
stream: string;
|
||||
classes?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base interface for the timeline page's state.
|
||||
*/
|
||||
interface ITimelinePageState {
|
||||
/**
|
||||
* The list of posts from the timeline.
|
||||
*/
|
||||
posts?: [Status];
|
||||
|
||||
/**
|
||||
* The list of posts stored temporarily while viewing the timeline.
|
||||
*
|
||||
* Can be cleared when user pushes "Show x posts" button.
|
||||
*/
|
||||
backlogPosts?: [Status] | null;
|
||||
|
||||
/**
|
||||
* Whether the view is currently loading.
|
||||
*/
|
||||
viewIsLoading: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view loaded successfully.
|
||||
*/
|
||||
viewDidLoad?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the view errored.
|
||||
*/
|
||||
viewDidError?: boolean;
|
||||
|
||||
/**
|
||||
* The view's error code, if it errored.
|
||||
*/
|
||||
viewDidErrorCode?: any;
|
||||
|
||||
/**
|
||||
* Whether or not to use the masonry layout as defined in
|
||||
* the user settings.
|
||||
*/
|
||||
isMasonryLayout?: boolean;
|
||||
|
||||
/**
|
||||
* Whether posts should automatically load when scrolling.
|
||||
*/
|
||||
isInfiniteScroll?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class for a timeline page.
|
||||
*
|
||||
* The timeline page streams a specific timeline. When the stream is connected,
|
||||
* the page will fetch a particular timeline list of posts. The timeline page will
|
||||
* also off-load incoming posts from the stream into a backlog that the user can
|
||||
* then insert by clicking a button.
|
||||
*/
|
||||
class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
|
||||
/**
|
||||
* The client to use.
|
||||
*/
|
||||
client: Mastodon;
|
||||
|
||||
/**
|
||||
* The page's stream listener.
|
||||
*/
|
||||
streamListener: StreamListener;
|
||||
|
||||
/**
|
||||
* Construct the timeline page.
|
||||
* @param props The timeline page's properties
|
||||
*/
|
||||
constructor(props: ITimelinePageProps) {
|
||||
super(props);
|
||||
|
||||
// Initialize the state.
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null,
|
||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout"),
|
||||
isInfiniteScroll: getUserDefaultBool("isInfiniteScroll"),
|
||||
};
|
||||
|
||||
// Generate the client.
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
// Create the stream listener from the properties.
|
||||
this.streamListener = this.client.stream(this.props.stream);
|
||||
|
||||
this.loadMoreTimelinePieces = this.loadMoreTimelinePieces.bind(this);
|
||||
this.shouldLoadMorePosts = this.shouldLoadMorePosts.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the stream listener and listen for new posts.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.streamListener.on("connect", () => {
|
||||
// Get the latest posts from this timeline.
|
||||
this.client
|
||||
.get(this.props.timeline, { limit: 50 })
|
||||
// If we succeeded, update the state and turn off loading.
|
||||
.then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
})
|
||||
|
||||
// Otherwise, update the state in error.
|
||||
.catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
});
|
||||
|
||||
// Notify the user with a snackbar.
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Store incoming posts into a backlog if possible.
|
||||
this.streamListener.on("update", (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) {
|
||||
queue.unshift(status);
|
||||
} else {
|
||||
queue = [status];
|
||||
}
|
||||
this.setState({ backlogPosts: queue });
|
||||
});
|
||||
|
||||
// When a post is deleted in the backend, find the post in the list
|
||||
// and remove it from the list.
|
||||
this.streamListener.on("delete", (id: number) => {
|
||||
let posts = this.state.posts;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
});
|
||||
this.setState({ posts });
|
||||
}
|
||||
});
|
||||
|
||||
// Display an error if the stream encounters and error.
|
||||
this.streamListener.on("error", (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
});
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
|
||||
this.streamListener.on("heartbeat", () => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a delay between repeated function calls
|
||||
* codeburst.io/throttling-and-debouncing-in-javascript-646d076d0a44
|
||||
* @param delay How long to wait before calling function (ms)
|
||||
* @param fn The function to call
|
||||
*/
|
||||
debounced(delay: number, fn: Function) {
|
||||
let lastCall = 0
|
||||
return function(...args: any) {
|
||||
const now = (new Date).getTime();
|
||||
if (now - lastCall < delay) {
|
||||
return
|
||||
}
|
||||
lastCall = now;
|
||||
return fn(...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for when scroll position changes
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.state.isInfiniteScroll) {
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
this.debounced(200, this.shouldLoadMorePosts),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Halt the stream and scroll listeners when unmounting the component.
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.streamListener.stop();
|
||||
if (this.state.isInfiniteScroll) {
|
||||
window.removeEventListener(
|
||||
"scroll",
|
||||
this.shouldLoadMorePosts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the posts from the backlog into the current list of posts
|
||||
* and clear the backlog.
|
||||
*/
|
||||
insertBacklog() {
|
||||
window.scrollTo(0, 0);
|
||||
let posts = this.state.posts;
|
||||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next set of posts, if it exists.
|
||||
*/
|
||||
loadMoreTimelinePieces() {
|
||||
// Reinstate the loading status.
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true });
|
||||
|
||||
// If there are any posts, get the next set.
|
||||
if (this.state.posts) {
|
||||
this.client
|
||||
.get(this.props.timeline, {
|
||||
max_id: this.state.posts[this.state.posts.length - 1].id,
|
||||
limit: 50
|
||||
})
|
||||
|
||||
// If we succeeded, append them to the end of the list of posts.
|
||||
.then((resp: any) => {
|
||||
let newPosts: [Status] = resp.data;
|
||||
let posts = this.state.posts as [Status];
|
||||
newPosts.forEach((post: Status) => {
|
||||
posts.push(post);
|
||||
});
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
});
|
||||
})
|
||||
|
||||
// If we errored, display the error and don't do anything.
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
});
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more posts when scroll is near the end of the page
|
||||
*/
|
||||
shouldLoadMorePosts(e: Event) {
|
||||
let difference =
|
||||
document.body.clientHeight - window.scrollY - window.innerHeight;
|
||||
if (difference < 10000 && this.state.viewIsLoading === false) {
|
||||
this.loadMoreTimelinePieces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the timeline page.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
||||
}`;
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${
|
||||
this.state.backlogPosts.length
|
||||
} new post${
|
||||
this.state.backlogPosts.length > 1
|
||||
? "s"
|
||||
: ""
|
||||
}`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
clickable
|
||||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.isMasonryLayout ? (
|
||||
<Masonry
|
||||
breakpointCols={{
|
||||
default: 4,
|
||||
2000: 3,
|
||||
1400: 2,
|
||||
1050: 1
|
||||
}}
|
||||
className={classes.masonryGrid}
|
||||
columnClassName={
|
||||
classes["my-masonry-grid_column"]
|
||||
}
|
||||
>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.masonryGrid_item}
|
||||
key={post.id}
|
||||
>
|
||||
<Post
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
) : (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return (
|
||||
<Post
|
||||
key={post.id}
|
||||
post={post}
|
||||
client={this.client}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<br />
|
||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||
<div
|
||||
style={{ textAlign: "center" }}
|
||||
onClick={() => this.loadMoreTimelinePieces()}
|
||||
>
|
||||
<Button variant="contained">Load more</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{this.state.viewDidError ? (
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">
|
||||
Something went wrong when loading this timeline.
|
||||
</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(TimelinePage));
|
|
@ -413,51 +413,42 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
let clientLoginSession: SaveClientSession = JSON.parse(
|
||||
loginData
|
||||
);
|
||||
|
||||
getConfig().then((resp: any) => {
|
||||
if (resp == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let conf: Config = resp;
|
||||
|
||||
let redirectUrl: string | undefined =
|
||||
this.state.emergencyMode ||
|
||||
clientLoginSession.authUrl.includes(
|
||||
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
||||
)
|
||||
? undefined
|
||||
: getRedirectAddress(conf.location);
|
||||
|
||||
Mastodon.fetchAccessToken(
|
||||
clientLoginSession.clientId,
|
||||
clientLoginSession.clientSecret,
|
||||
code,
|
||||
localStorage.getItem("baseurl") as string,
|
||||
redirectUrl
|
||||
)
|
||||
.then((tokenData: any) => {
|
||||
localStorage.setItem(
|
||||
"access_token",
|
||||
tokenData.access_token
|
||||
);
|
||||
window.location.href =
|
||||
window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: this.state.defaultRedirectAddress;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't authorize ${
|
||||
this.state.brandName
|
||||
? this.state.brandName
|
||||
: "Hyperspace"
|
||||
}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
});
|
||||
Mastodon.fetchAccessToken(
|
||||
clientLoginSession.clientId,
|
||||
clientLoginSession.clientSecret,
|
||||
code,
|
||||
localStorage.getItem("baseurl") as string,
|
||||
this.state.emergencyMode
|
||||
? undefined
|
||||
: clientLoginSession.authUrl.includes(
|
||||
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
||||
)
|
||||
? undefined
|
||||
: window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}`
|
||||
)
|
||||
.then((tokenData: any) => {
|
||||
localStorage.setItem(
|
||||
"access_token",
|
||||
tokenData.access_token
|
||||
);
|
||||
window.location.href =
|
||||
window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}/#/`;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't authorize ${
|
||||
this.state.brandName
|
||||
? this.state.brandName
|
||||
: "Hyperspace"
|
||||
}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export type Account = {
|
|||
followers_count: number;
|
||||
following_count: number;
|
||||
statuses_count: number;
|
||||
last_status_at: string;
|
||||
note: string;
|
||||
url: string;
|
||||
avatar: string;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
type: "unknown" | "image" | "gifv" | "audio" | "video";
|
||||
type: "unknown" | "image" | "gifv" | "video";
|
||||
url: string;
|
||||
remote_url: string | null;
|
||||
preview_url: string;
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* Base draft type for a cached draft.
|
||||
*/
|
||||
export type Draft = {
|
||||
/**
|
||||
* The contents of the draft (i.e, its post text).
|
||||
*/
|
||||
contents: string;
|
||||
/**
|
||||
* The ID of the post it replies to, if applicable. If there isn't one, it should be set to -999.
|
||||
*/
|
||||
replyId: number;
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
export type History = {
|
||||
day: string;
|
||||
uses: number;
|
||||
accounts: number;
|
||||
};
|
|
@ -1,7 +1,4 @@
|
|||
import { History } from "./History";
|
||||
|
||||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
history?: [History];
|
||||
};
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
import { isDarwinApp } from "./desktop";
|
||||
|
||||
/**
|
||||
* A list containing the types of child views.
|
||||
*
|
||||
* This list is used to help determine if a back button is necessary, usually because there
|
||||
* is no defined way of returning to the parent view without using the menu bar or keyboard
|
||||
* shortcut in desktop apps.
|
||||
*/
|
||||
export const childViews = ["#/profile", "#/conversation"];
|
||||
|
||||
/**
|
||||
* Determine whether the title bar is being displayed.
|
||||
* This might be useful in cases where styles are dependent on the title bar's visibility, such as heights.
|
||||
|
@ -18,20 +9,3 @@ export const childViews = ["#/profile", "#/conversation"];
|
|||
export function isAppbarExpanded(): boolean {
|
||||
return isDarwinApp() || process.env.NODE_ENV === "development";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a path is considered a "child view".
|
||||
*
|
||||
* This is often used to determine whether a back button should be rendered or not.
|
||||
* @param path The path of the page, usually its hash
|
||||
* @returns Boolean distating if the view is a child view.
|
||||
*/
|
||||
export function isChildView(path: string): boolean {
|
||||
let protocolMatched = false;
|
||||
childViews.forEach((childViewProtocol: string) => {
|
||||
if (path.startsWith(childViewProtocol)) {
|
||||
protocolMatched = true;
|
||||
}
|
||||
});
|
||||
return protocolMatched;
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { Draft } from "../types/Draft";
|
||||
|
||||
/**
|
||||
* Check whether a cached draft exists.
|
||||
*/
|
||||
export function draftExists(): boolean {
|
||||
return sessionStorage.getItem("cachedDraft") !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a draft to session storage.
|
||||
* @param draft The text of the post.
|
||||
* @param replyId The post's reply ID, if available.
|
||||
*/
|
||||
export function writeDraft(draft: string, replyId?: number) {
|
||||
let cachedDraft = {
|
||||
contents: draft,
|
||||
replyId: replyId ? replyId : -999
|
||||
};
|
||||
sessionStorage.setItem("cachedDraft", JSON.stringify(cachedDraft));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cached draft and remove it from session storage.
|
||||
* @returns A Draft object with the draft's contents and reply ID (or -999).
|
||||
*/
|
||||
export function loadDraft(): Draft {
|
||||
let contents = "";
|
||||
let replyId = -999;
|
||||
if (draftExists()) {
|
||||
let draft = sessionStorage.getItem("cachedDraft");
|
||||
sessionStorage.removeItem("cachedDraft");
|
||||
if (draft != null) {
|
||||
const draftObject = JSON.parse(draft);
|
||||
contents = draftObject.contents;
|
||||
replyId = draftObject.replyId;
|
||||
}
|
||||
}
|
||||
return {
|
||||
contents: contents,
|
||||
replyId: replyId
|
||||
};
|
||||
}
|
|
@ -44,18 +44,18 @@ export function createHyperspaceApp(
|
|||
|
||||
/**
|
||||
* Gets the appropriate redirect address.
|
||||
* @param url The address or configuration to use
|
||||
* @param type The address or configuration to use
|
||||
*/
|
||||
export function getRedirectAddress(
|
||||
url: "desktop" | "dynamic" | string
|
||||
type: "desktop" | "dynamic" | string
|
||||
): string {
|
||||
switch (url) {
|
||||
switch (type) {
|
||||
case "desktop":
|
||||
return "hyperspace://hyperspace/app/";
|
||||
case "dynamic":
|
||||
return `https://${window.location.host}`;
|
||||
default:
|
||||
return url;
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ type SettingsTemplate = {
|
|||
clearNotificationsOnRead: boolean;
|
||||
displayAllOnNotificationBadge: boolean;
|
||||
defaultVisibility: string;
|
||||
imposeCharacterLimit: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -100,9 +99,7 @@ export function createUserDefaults() {
|
|||
enablePushNotifications: true,
|
||||
clearNotificationsOnRead: false,
|
||||
displayAllOnNotificationBadge: false,
|
||||
defaultVisibility: "public",
|
||||
imposeCharacterLimit: true,
|
||||
isMasonryLayout: false
|
||||
defaultVisibility: "public"
|
||||
};
|
||||
|
||||
let settings = [
|
||||
|
@ -110,9 +107,7 @@ export function createUserDefaults() {
|
|||
"systemDecidesDarkMode",
|
||||
"clearNotificationsOnRead",
|
||||
"displayAllOnNotificationBadge",
|
||||
"defaultVisibility",
|
||||
"imposeCharacterLimit",
|
||||
"isMasonryLayout"
|
||||
"defaultVisibility"
|
||||
];
|
||||
|
||||
migrateExistingSettings();
|
||||
|
|
Loading…
Reference in New Issue