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
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
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
|
- name: Install dependencies and build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build --if-present
|
npm run build --if-present
|
||||||
npm run build-desktop-win
|
npm run build-desktop-win
|
||||||
- name: Upload Windows executable
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
if: success()
|
|
||||||
with:
|
|
||||||
name: 'Windows executable (output dir)'
|
|
||||||
path: dist
|
|
|
@ -1,24 +1,10 @@
|
||||||
Hyperspace Desktop
|
Hyperspace
|
||||||
Copyright Hyperspace Developers 2020
|
Copyright Hyperspace developers 2019
|
||||||
|
|
||||||
NON-VIOLENT PUBLIC LICENSE v4
|
NON-VIOLENT PUBLIC LICENSE v1
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
|
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
|
COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN
|
||||||
AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY
|
AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY
|
||||||
EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE
|
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
|
timed-relation with a moving image ("synching") will be
|
||||||
considered an Adaptation for the purpose of this License.
|
considered an Adaptation for the purpose of this License.
|
||||||
|
|
||||||
c. "Bodily Harm" means any physical hurt or injury to a person that
|
c. "Bodily Harm" means any action of one person towards another
|
||||||
interferes with the health or comfort of the person and that is more
|
in an intentional manner.
|
||||||
more than merely transient or trifling in nature.
|
|
||||||
|
|
||||||
d. "Collection" means a collection of literary or artistic
|
d. "Collection" means a collection of literary or artistic
|
||||||
works, such as encyclopedias and anthologies, or performances,
|
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
|
f. "Incarceration" means confinement in a jail, prison, or any
|
||||||
other place where individuals of any kind are held against
|
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
|
g. "Licensor" means the individual, individuals, entity or
|
||||||
entities that offer(s) the Work under the terms of this License.
|
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
|
through which the Original Author and/or Distributor originally
|
||||||
created, derived, and/or modified it.
|
created, derived, and/or modified it.
|
||||||
|
|
||||||
o. "Surveilling" means the use of the Work to either
|
o. "Surveilling" means the use of the Work to
|
||||||
overtly or covertly observe and record persons and or their
|
overtly or covertly observe persons or their activities.
|
||||||
activities.
|
|
||||||
|
|
||||||
p. "Web Service" means the use of a piece of Software to
|
p. "Web Service" means the use of a piece of Software to
|
||||||
interpret or modify information that is subsequently and directly
|
interpret or modify information that is subsequently and directly
|
||||||
served to users over the Internet.
|
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
|
2. FAIR DEALING RIGHTS
|
||||||
|
|
||||||
Nothing in this License is intended to reduce, limit, or restrict any
|
Nothing in this License is intended to reduce, limit, or restrict any
|
||||||
|
@ -202,6 +178,7 @@ AND CONDITIONS OF THIS LICENSE.
|
||||||
Section 8(g), all rights not expressly granted by Licensor are
|
Section 8(g), all rights not expressly granted by Licensor are
|
||||||
hereby reserved.
|
hereby reserved.
|
||||||
|
|
||||||
|
|
||||||
4. RESTRICTIONS
|
4. RESTRICTIONS
|
||||||
|
|
||||||
The license granted in Section 3 above is expressly made subject to and
|
The license granted in Section 3 above is expressly made subject to and
|
||||||
|
@ -255,15 +232,15 @@ AND CONDITIONS OF THIS LICENSE.
|
||||||
or tracking individuals for financial gain.
|
or tracking individuals for financial gain.
|
||||||
iii. You do not use the Work in an Act of War.
|
iii. You do not use the Work in an Act of War.
|
||||||
iv. You do not use the Work for the purpose of supporting
|
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.
|
v. You do not use the Work for the purpose of Incarceration.
|
||||||
vi. You do not use the Work for the purpose of extracting
|
vi. You do not use the Work for the purpose of extracting
|
||||||
oil, gas, or coal.
|
oil, gas, or coal.
|
||||||
vii. You do not use the Work for the purpose of
|
vii. You do not use the Work for the purpose of
|
||||||
expediting, coordinating, or facilitating paid work
|
expediting, coordinating, or facilitating paid work
|
||||||
undertaken by individuals under the age of 12 years.
|
undertaken by individuals under the age of 12 years.
|
||||||
viii. You do not use the Work to either Discriminate or
|
viii. You do not use the Work to either discriminate or
|
||||||
spread Hate Speech on the basis of sex, sexual orientation,
|
spread hate speech on the basis of sex, sexual orientation,
|
||||||
gender identity, race, age, disability, color, national origin,
|
gender identity, race, age, disability, color, national origin,
|
||||||
religion, or lower economic status.
|
religion, or lower economic status.
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
[![Matrix room](https://img.shields.io/matrix/hypermasto:matrix.org.svg)](https://matrix.to/#/#hypermasto:matrix.org)
|
[![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)
|
[![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.
|
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
|
## 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.
|
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
|
## 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).
|
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",
|
"name": "hyperspace",
|
||||||
"version": "1.1.0-beta2",
|
"version": "1.0.4",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1055,23 +1055,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@material-ui/icons": {
|
"@material-ui/icons": {
|
||||||
"version": "4.5.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-3.0.2.tgz",
|
||||||
"integrity": "sha512-YZ/BgJbXX4a0gOuKWb30mBaHaoXRqPanlePam83JQPZ/y4kl+3aW0Wv9tlR70hB5EGAkEJGW5m4ktJwMgxQAeA==",
|
"integrity": "sha512-QY/3gJnObZQ3O/e6WjH+0ah2M3MOgLOzCy8HTUoUx9B6dDrS18vP7Ycw3qrDEKlB6q1KNxy6CZHm5FCauWGy2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.4.4"
|
"@babel/runtime": "^7.2.0",
|
||||||
},
|
"recompose": "0.28.0 - 0.30.0"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@material-ui/system": {
|
"@material-ui/system": {
|
||||||
|
@ -9461,9 +9451,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.5.3",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
|
||||||
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
|
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
|
@ -10448,9 +10438,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invert-kv": {
|
"invert-kv": {
|
||||||
"version": "2.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
|
||||||
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
|
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ip": {
|
"ip": {
|
||||||
|
@ -11848,12 +11838,12 @@
|
||||||
"integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q=="
|
"integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q=="
|
||||||
},
|
},
|
||||||
"lcid": {
|
"lcid": {
|
||||||
"version": "2.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
|
||||||
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
|
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"invert-kv": "^2.0.0"
|
"invert-kv": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"left-pad": {
|
"left-pad": {
|
||||||
|
@ -12275,22 +12265,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mem": {
|
"mem": {
|
||||||
"version": "4.3.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
|
||||||
"integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
|
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"map-age-cleaner": "^0.1.1",
|
"mimic-fn": "^1.0.0"
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"memory-fs": {
|
"memory-fs": {
|
||||||
|
@ -13256,40 +13236,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"os-locale": {
|
"os-locale": {
|
||||||
"version": "3.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
|
||||||
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
|
"integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"execa": "^1.0.0",
|
"execa": "^0.7.0",
|
||||||
"lcid": "^2.0.0",
|
"lcid": "^1.0.0",
|
||||||
"mem": "^4.0.0"
|
"mem": "^1.1.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"os-tmpdir": {
|
"os-tmpdir": {
|
||||||
|
@ -16822,11 +16776,6 @@
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||||
"dev": true
|
"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": {
|
"react-router": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||||
|
@ -19912,9 +19861,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "3.7.2",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz",
|
||||||
"integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
|
"integrity": "sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
|
@ -21476,16 +21425,16 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "11.1.1",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
|
||||||
"integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==",
|
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"cliui": "^4.0.0",
|
"cliui": "^4.0.0",
|
||||||
"decamelize": "^1.1.1",
|
"decamelize": "^1.1.1",
|
||||||
"find-up": "^2.1.0",
|
"find-up": "^2.1.0",
|
||||||
"get-caller-file": "^1.0.1",
|
"get-caller-file": "^1.0.1",
|
||||||
"os-locale": "^3.1.0",
|
"os-locale": "^2.0.0",
|
||||||
"require-directory": "^2.1.1",
|
"require-directory": "^2.1.1",
|
||||||
"require-main-filename": "^1.0.1",
|
"require-main-filename": "^1.0.1",
|
||||||
"set-blocking": "^2.0.0",
|
"set-blocking": "^2.0.0",
|
||||||
|
|
215
package.json
215
package.json
|
@ -1,115 +1,114 @@
|
||||||
{
|
{
|
||||||
"name": "hyperspace",
|
"name": "hyperspace",
|
||||||
"productName": "Hyperspace Desktop",
|
"productName": "Hyperspace Desktop",
|
||||||
"version": "1.1.0-beta4",
|
"version": "1.0.4",
|
||||||
"description": "A beautiful, fluffy client for the fediverse",
|
"description": "A beautiful, fluffy client for the fediverse",
|
||||||
"author": "Marquis Kurt <hyperspacedev@marquiskurt.net>",
|
"author": "Marquis Kurt <hyperspacedev@marquiskurt.net>",
|
||||||
"repository": "https://github.com/hyperspacedev/hyperspace.git",
|
"repository": "https://github.com/hyperspacedev/hyperspace.git",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@date-io/moment": "^1.3.11",
|
"@date-io/moment": "^1.3.11",
|
||||||
"@material-ui/core": "^3.9.3",
|
"@material-ui/core": "^3.9.3",
|
||||||
"@material-ui/icons": "^4.5.1",
|
"@material-ui/icons": "^3.0.2",
|
||||||
"@types/emoji-mart": "^2.11.0",
|
"@types/emoji-mart": "^2.11.0",
|
||||||
"@types/jest": "^24.0.18",
|
"@types/jest": "^24.0.18",
|
||||||
"@types/node": "11.11.6",
|
"@types/node": "11.11.6",
|
||||||
"@types/react": "16.8.8",
|
"@types/react": "16.8.8",
|
||||||
"@types/react-dom": "16.8.3",
|
"@types/react-dom": "16.8.3",
|
||||||
"@types/react-router-dom": "^4.3.5",
|
"@types/react-router-dom": "^4.3.5",
|
||||||
"@types/react-swipeable-views": "latest",
|
"@types/react-swipeable-views": "latest",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"electron": "^6.0.11",
|
"electron": "^6.0.11",
|
||||||
"electron-builder": "^21.2.0",
|
"electron-builder": "^21.2.0",
|
||||||
"emoji-mart": "^2.11.1",
|
"emoji-mart": "^2.11.1",
|
||||||
"file-dialog": "^0.0.7",
|
"file-dialog": "^0.0.7",
|
||||||
"material-ui-pickers": "^2.2.4",
|
"material-ui-pickers": "^2.2.4",
|
||||||
"mdi-material-ui": "^5.18.0",
|
"mdi-material-ui": "^5.18.0",
|
||||||
"megalodon": "^0.6.4",
|
"megalodon": "^0.6.4",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"notistack": "^0.5.1",
|
"notistack": "^0.5.1",
|
||||||
"prettier": "1.18.2",
|
"prettier": "1.18.2",
|
||||||
"query-string": "^6.8.3",
|
"query-string": "^6.8.3",
|
||||||
"react": "^16.10.2",
|
"react": "^16.10.2",
|
||||||
"react-dom": "^16.10.2",
|
"react-dom": "^16.10.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "^2.1.8",
|
"react-scripts": "^2.1.8",
|
||||||
"react-swipeable-views": "^0.13.3",
|
"react-swipeable-views": "^0.13.3",
|
||||||
"react-web-share-api": "^0.0.2",
|
"react-web-share-api": "^0.0.2",
|
||||||
"typescript": "^3.7.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": {
|
"mac": {
|
||||||
"electron-notarize": "^0.1.1",
|
"category": "public.app-category.social-networking",
|
||||||
"electron-updater": "^4.1.2",
|
"icon": "desktop/app.icns",
|
||||||
"electron-window-state": "^5.0.3",
|
"target": [
|
||||||
"react-masonry-css": "^1.0.14"
|
"dmg",
|
||||||
|
"mas"
|
||||||
|
],
|
||||||
|
"darkModeSupport": true,
|
||||||
|
"hardenedRuntime": true
|
||||||
},
|
},
|
||||||
"main": "public/electron.js",
|
"mas": {
|
||||||
"scripts": {
|
"entitlements": "desktop/entitlements.mas.plist",
|
||||||
"start": "react-scripts start",
|
"entitlementsInherit": "desktop/entitlements.mas.inherit.plist",
|
||||||
"electrify": "npm run build; electron .",
|
"provisioningProfile": "desktop/embedded.provisionprofile"
|
||||||
"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": {
|
"dmg": {
|
||||||
"extends": "react-app"
|
"sign": false
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"win": {
|
||||||
">0.2%",
|
"target": [
|
||||||
"not dead",
|
"nsis"
|
||||||
"not ie <= 11",
|
],
|
||||||
"not op_mini all"
|
"icon": "desktop/app.ico"
|
||||||
],
|
},
|
||||||
"build": {
|
"linux": {
|
||||||
"appId": "net.marquiskurt.hyperspace",
|
"target": [
|
||||||
"afterSign": "desktop/notarize.js",
|
"${@:1}"
|
||||||
"directories": {
|
],
|
||||||
"buildResources": "desktop"
|
"icon": "linux",
|
||||||
},
|
"category": "Network"
|
||||||
"mac": {
|
},
|
||||||
"category": "public.app-category.social-networking",
|
"snap": {
|
||||||
"icon": "desktop/app.icns",
|
"confinement": "strict",
|
||||||
"target": [
|
"summary": "A beautiful, fluffy client for the fediverse"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.0.4",
|
||||||
"location": "https://hyperspaceapp-next.herokuapp.com",
|
"location": "https://hyperspaceapp.herokuapp.com",
|
||||||
"branding": {
|
"branding": {
|
||||||
"name": "Hyperspace",
|
"name": "Hyperspace",
|
||||||
"logo": "logo.svg",
|
"logo": "logo.svg",
|
||||||
"background": "background.png"
|
"background": "background.png"
|
||||||
},
|
},
|
||||||
"developer": true,
|
"developer": false,
|
||||||
"federation": {
|
"federation": {
|
||||||
"universalLogin": true,
|
"universalLogin": true,
|
||||||
"allowPublicPosts": true,
|
"allowPublicPosts": true,
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"account": "774314"
|
"account": "774314"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "Non-violent Public License v4+",
|
"name": "Non-violent Public License",
|
||||||
"url": "https://thufie.lain.haus/NPL.html"
|
"url": "https://thufie.lain.haus/NPL.html"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/hyperspacedev/hyperspace"
|
"repository": "https://github.com/hyperspacedev/hyperspace"
|
||||||
|
|
|
@ -312,14 +312,6 @@ function createMenubar() {
|
||||||
click() {
|
click() {
|
||||||
safelyGoTo("hyperspace://hyperspace/app/#/messages")
|
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",
|
accelerator: "Alt+CmdOrCtrl+R",
|
||||||
click() {
|
click() {
|
||||||
safelyGoTo("hyperspace://hyperspace/app/#/recommended")
|
safelyGoTo("hyperspace://hyperspace/app/#/recommended")
|
||||||
|
@ -348,13 +340,6 @@ function createMenubar() {
|
||||||
safelyGoTo("hyperspace://hyperspace/app/#/you")
|
safelyGoTo("hyperspace://hyperspace/app/#/you")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Follow Requests',
|
|
||||||
accelerator: "Alt+CmdOrCtrl+E",
|
|
||||||
click() {
|
|
||||||
safelyGoTo("hyperspace://hyperspace/app/#/requests")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Blocked Servers',
|
label: 'Blocked Servers',
|
||||||
accelerator: "Shift+CmdOrCtrl+B",
|
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 Settings from "./pages/Settings";
|
||||||
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
|
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
|
||||||
import ProfilePage from "./pages/ProfilePage";
|
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 Conversation from "./pages/Conversation";
|
||||||
import NotificationsPage from "./pages/Notifications";
|
import NotificationsPage from "./pages/Notifications";
|
||||||
import SearchPage from "./pages/Search";
|
import SearchPage from "./pages/Search";
|
||||||
|
@ -19,8 +21,6 @@ import RecommendationsPage from "./pages/Recommendations";
|
||||||
import Missingno from "./pages/Missingno";
|
import Missingno from "./pages/Missingno";
|
||||||
import Blocked from "./pages/Blocked";
|
import Blocked from "./pages/Blocked";
|
||||||
import You from "./pages/You";
|
import You from "./pages/You";
|
||||||
import RequestsPage from "./pages/Requests";
|
|
||||||
import ActivityPage from "./pages/Activity";
|
|
||||||
import { withSnackbar } from "notistack";
|
import { withSnackbar } from "notistack";
|
||||||
import { PrivateRoute } from "./interfaces/overrides";
|
import { PrivateRoute } from "./interfaces/overrides";
|
||||||
import { userLoggedIn } from "./utilities/accounts";
|
import { userLoggedIn } from "./utilities/accounts";
|
||||||
|
@ -96,47 +96,10 @@ class App extends Component<any, IAppState> {
|
||||||
<Route path="/welcome" component={WelcomePage} />
|
<Route path="/welcome" component={WelcomePage} />
|
||||||
<div>
|
<div>
|
||||||
{this.state.showLayout ? <AppLayout /> : null}
|
{this.state.showLayout ? <AppLayout /> : null}
|
||||||
<PrivateRoute
|
<PrivateRoute exact path="/" component={HomePage} />
|
||||||
exact
|
<PrivateRoute path="/home" component={HomePage} />
|
||||||
path="/"
|
<PrivateRoute path="/local" component={LocalPage} />
|
||||||
render={(props: any) => (
|
<PrivateRoute path="/public" component={PublicPage} />
|
||||||
<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 path="/messages" component={MessagesPage} />
|
<PrivateRoute path="/messages" component={MessagesPage} />
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
path="/notifications"
|
path="/notifications"
|
||||||
|
@ -160,8 +123,6 @@ class App extends Component<any, IAppState> {
|
||||||
path="/recommended"
|
path="/recommended"
|
||||||
component={RecommendationsPage}
|
component={RecommendationsPage}
|
||||||
/>
|
/>
|
||||||
<PrivateRoute path="/requests" component={RequestsPage} />
|
|
||||||
<PrivateRoute path="/activity" component={ActivityPage} />
|
|
||||||
</div>
|
</div>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,8 +35,7 @@ export const styles = (theme: Theme) =>
|
||||||
titleBarText: {
|
titleBarText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
paddingTop: 2,
|
paddingTop: 2,
|
||||||
paddingBottom: 1,
|
paddingBottom: 1
|
||||||
color: theme.palette.getContrastText(theme.palette.primary.main)
|
|
||||||
},
|
},
|
||||||
appBar: {
|
appBar: {
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
|
@ -58,10 +57,6 @@ export const styles = (theme: Theme) =>
|
||||||
display: "none"
|
display: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
appBarBackButton: {
|
|
||||||
marginLeft: -12,
|
|
||||||
marginRight: 20
|
|
||||||
},
|
|
||||||
appBarTitle: {
|
appBarTitle: {
|
||||||
display: "none",
|
display: "none",
|
||||||
[theme.breakpoints.up("md")]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import {
|
||||||
ListItem,
|
ListItem,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
|
||||||
import MenuIcon from "@material-ui/icons/Menu";
|
import MenuIcon from "@material-ui/icons/Menu";
|
||||||
import SearchIcon from "@material-ui/icons/Search";
|
import SearchIcon from "@material-ui/icons/Search";
|
||||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
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 CreateIcon from "@material-ui/icons/Create";
|
||||||
import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
|
import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
|
||||||
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
|
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 { styles } from "./AppLayout.styles";
|
||||||
import { MultiAccount, UAccount } from "../../types/Account";
|
import { MultiAccount, UAccount } from "../../types/Account";
|
||||||
import {
|
import {
|
||||||
|
@ -68,81 +63,30 @@ import {
|
||||||
getAccountRegistry,
|
getAccountRegistry,
|
||||||
removeAccountFromRegistry
|
removeAccountFromRegistry
|
||||||
} from "../../utilities/accounts";
|
} from "../../utilities/accounts";
|
||||||
import { isChildView } from "../../utilities/appbar";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pre-define state interface for the app layout.
|
|
||||||
*/
|
|
||||||
interface IAppLayoutState {
|
interface IAppLayoutState {
|
||||||
/**
|
|
||||||
* Whether the account menu is open or not.
|
|
||||||
*/
|
|
||||||
acctMenuOpen: boolean;
|
acctMenuOpen: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the drawer is open (mobile-only).
|
|
||||||
*/
|
|
||||||
drawerOpenOnMobile: boolean;
|
drawerOpenOnMobile: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The current user signed in.
|
|
||||||
*/
|
|
||||||
currentUser?: UAccount;
|
currentUser?: UAccount;
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of notifications received.
|
|
||||||
*/
|
|
||||||
notificationCount: number;
|
notificationCount: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the log out dialog is open.
|
|
||||||
*/
|
|
||||||
logOutOpen: boolean;
|
logOutOpen: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether federation has been enabled in the config.
|
|
||||||
*/
|
|
||||||
enableFederation?: boolean;
|
enableFederation?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The brand name of the app, if not "Hyperspace".
|
|
||||||
*/
|
|
||||||
brandName?: string;
|
brandName?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the app is in development mode.
|
|
||||||
*/
|
|
||||||
developerMode?: boolean;
|
developerMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The base app layout class. Responsible for the search bar, navigation menus, etc.
|
|
||||||
*/
|
|
||||||
export class AppLayout extends Component<any, IAppLayoutState> {
|
export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
/**
|
|
||||||
* The Mastodon client to operate with.
|
|
||||||
*/
|
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
|
|
||||||
/**
|
|
||||||
* A stream listener to listen for new streaming events from Mastodon.
|
|
||||||
*/
|
|
||||||
streamListener: any;
|
streamListener: any;
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct the app layout.
|
|
||||||
* @param props The properties to pass in.
|
|
||||||
*/
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Create the Mastodon client
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
localStorage.getItem("access_token") as string,
|
localStorage.getItem("access_token") as string,
|
||||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize the state
|
|
||||||
this.state = {
|
this.state = {
|
||||||
drawerOpenOnMobile: false,
|
drawerOpenOnMobile: false,
|
||||||
acctMenuOpen: false,
|
acctMenuOpen: false,
|
||||||
|
@ -150,20 +94,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
logOutOpen: false
|
logOutOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bind functions as properties to this class for reference
|
|
||||||
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
||||||
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
||||||
this.clearBadge = this.clearBadge.bind(this);
|
this.clearBadge = this.clearBadge.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run post-mount tasks such as getting account data and refreshing the config file.
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Get the account data.
|
|
||||||
this.getAccountData();
|
this.getAccountData();
|
||||||
|
|
||||||
// Read the config file and then update the state.
|
|
||||||
getConfig().then((result: any) => {
|
getConfig().then((result: any) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
let config: Config = result;
|
let config: Config = result;
|
||||||
|
@ -177,25 +115,18 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for notifications.
|
|
||||||
this.streamNotifications();
|
this.streamNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get updated credentials from Mastodon or pull information from local storage.
|
|
||||||
*/
|
|
||||||
getAccountData() {
|
getAccountData() {
|
||||||
// Try to get updated credentials from Mastodon.
|
|
||||||
this.client
|
this.client
|
||||||
.get("/accounts/verify_credentials")
|
.get("/accounts/verify_credentials")
|
||||||
.then((resp: any) => {
|
.then((resp: any) => {
|
||||||
// Update the account if possible.
|
|
||||||
let data: UAccount = resp.data;
|
let data: UAccount = resp.data;
|
||||||
this.setState({ currentUser: data });
|
this.setState({ currentUser: data });
|
||||||
sessionStorage.setItem("id", data.id);
|
sessionStorage.setItem("id", data.id);
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
// Otherwise, pull from local storage.
|
|
||||||
this.props.enqueueSnackbar(
|
this.props.enqueueSnackbar(
|
||||||
"Couldn't find profile info: " + err.name
|
"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() {
|
streamNotifications() {
|
||||||
// Set up the stream listener.
|
|
||||||
this.streamListener = this.client.stream("/streaming/user");
|
this.streamListener = this.client.stream("/streaming/user");
|
||||||
|
|
||||||
// Set the count if the user asked to display the total count.
|
|
||||||
if (getUserDefaultBool("displayAllOnNotificationBadge")) {
|
if (getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||||
this.client.get("/notifications").then((resp: any) => {
|
this.client.get("/notifications").then((resp: any) => {
|
||||||
let notifArray = resp.data;
|
let notifArray = resp.data;
|
||||||
|
@ -220,17 +146,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for notifications.
|
|
||||||
this.streamListener.on("notification", (notif: Notification) => {
|
this.streamListener.on("notification", (notif: Notification) => {
|
||||||
const notificationCount = this.state.notificationCount + 1;
|
const notificationCount = this.state.notificationCount + 1;
|
||||||
this.setState({ notificationCount });
|
this.setState({ notificationCount });
|
||||||
|
|
||||||
// Update the badge on the desktop.
|
|
||||||
if (isDesktopApp()) {
|
if (isDesktopApp()) {
|
||||||
getElectronApp().setBadgeCount(notificationCount);
|
getElectronApp().setBadgeCount(notificationCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up a push notification if the window isn't in focus.
|
|
||||||
if (!document.hasFocus()) {
|
if (!document.hasFocus()) {
|
||||||
let primaryMessage = "";
|
let primaryMessage = "";
|
||||||
let secondaryMessage = "";
|
let secondaryMessage = "";
|
||||||
|
@ -289,54 +212,34 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respectfully send the notification request.
|
|
||||||
sendNotificationRequest(primaryMessage, secondaryMessage);
|
sendNotificationRequest(primaryMessage, secondaryMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the account menu.
|
|
||||||
*/
|
|
||||||
toggleAcctMenu() {
|
toggleAcctMenu() {
|
||||||
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the app drawer, if on mobile.
|
|
||||||
*/
|
|
||||||
toggleDrawerOnMobile() {
|
toggleDrawerOnMobile() {
|
||||||
this.setState({
|
this.setState({
|
||||||
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the logout dialog.
|
|
||||||
*/
|
|
||||||
toggleLogOutDialog() {
|
toggleLogOutDialog() {
|
||||||
this.setState({ logOutOpen: !this.state.logOutOpen });
|
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) {
|
searchForQuery(what: string) {
|
||||||
what = what.replace(/^#/g, "tag:");
|
|
||||||
console.log(what);
|
|
||||||
window.location.href = isDesktopApp()
|
window.location.href = isDesktopApp()
|
||||||
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
|
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
|
||||||
: "/#/search?query=" + what;
|
: "/#/search?query=" + what;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear login information, remove the account from the registry, and reload the web page.
|
|
||||||
*/
|
|
||||||
logOutAndRestart() {
|
logOutAndRestart() {
|
||||||
let loginData = localStorage.getItem("login");
|
let loginData = localStorage.getItem("login");
|
||||||
if (loginData) {
|
if (loginData) {
|
||||||
// Remove account from the registry.
|
|
||||||
let registry = getAccountRegistry();
|
let registry = getAccountRegistry();
|
||||||
|
|
||||||
registry.forEach((registryItem: MultiAccount, index: number) => {
|
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"];
|
let items = ["login", "account", "baseurl", "access_token"];
|
||||||
items.forEach(entry => {
|
items.forEach(entry => {
|
||||||
localStorage.removeItem(entry);
|
localStorage.removeItem(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Finally, reload.
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the notifications badge.
|
|
||||||
*/
|
|
||||||
clearBadge() {
|
clearBadge() {
|
||||||
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
|
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||||
this.setState({ notificationCount: 0 });
|
this.setState({ notificationCount: 0 });
|
||||||
|
@ -372,9 +270,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the title bar.
|
|
||||||
*/
|
|
||||||
titlebar() {
|
titlebar() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
if (isDarwinApp()) {
|
if (isDarwinApp()) {
|
||||||
|
@ -395,20 +290,13 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
return (
|
return (
|
||||||
<div className={classes.titleBarRoot}>
|
<div className={classes.titleBarRoot}>
|
||||||
<Typography className={classes.titleBarText}>
|
<Typography className={classes.titleBarText}>
|
||||||
<BuildIcon
|
🛠 Careful: you're running in developer mode.
|
||||||
color="inherit"
|
|
||||||
style={{ fontSize: "1em", verticalAlign: "middle" }}
|
|
||||||
/>{" "}
|
|
||||||
Careful: you're running in developer mode.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the app drawer. On the desktop, this appears as a sidebar in larger layouts.
|
|
||||||
*/
|
|
||||||
appDrawer() {
|
appDrawer() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -542,13 +430,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
</LinkableListItem>
|
</LinkableListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<ListSubheader>Community</ListSubheader>
|
<ListSubheader>More</ListSubheader>
|
||||||
<LinkableListItem button key="activity" to="/activity">
|
|
||||||
<ListItemIcon>
|
|
||||||
<TrendingUpIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Activity" />
|
|
||||||
</LinkableListItem>
|
|
||||||
<LinkableListItem
|
<LinkableListItem
|
||||||
button
|
button
|
||||||
key="recommended"
|
key="recommended"
|
||||||
|
@ -557,10 +439,8 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<GroupIcon />
|
<GroupIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Recommended" />
|
<ListItemText primary="Who to follow" />
|
||||||
</LinkableListItem>
|
</LinkableListItem>
|
||||||
<Divider />
|
|
||||||
<ListSubheader>More</ListSubheader>
|
|
||||||
<LinkableListItem button key="settings" to="/settings">
|
<LinkableListItem button key="settings" to="/settings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
|
@ -578,9 +458,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the entire layout.
|
|
||||||
*/
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -589,18 +466,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
{this.titlebar()}
|
{this.titlebar()}
|
||||||
<AppBar className={classes.appBar} position="static">
|
<AppBar className={classes.appBar} position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
{isDesktopApp() &&
|
|
||||||
isChildView(window.location.hash) ? (
|
|
||||||
<IconButton
|
|
||||||
className={classes.appBarBackButton}
|
|
||||||
color="inherit"
|
|
||||||
aria-label="Go back"
|
|
||||||
onClick={() => window.history.back()}
|
|
||||||
>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</IconButton>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classes.appBarMenuButton}
|
className={classes.appBarMenuButton}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
@ -752,15 +617,6 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
Edit profile
|
Edit profile
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</LinkableListItem>
|
</LinkableListItem>
|
||||||
<LinkableListItem
|
|
||||||
button={true}
|
|
||||||
to={"/requests"}
|
|
||||||
>
|
|
||||||
<ListItemText>
|
|
||||||
Manage follow requests
|
|
||||||
</ListItemText>
|
|
||||||
</LinkableListItem>
|
|
||||||
<Divider />
|
|
||||||
<LinkableListItem
|
<LinkableListItem
|
||||||
to={"/welcome"}
|
to={"/welcome"}
|
||||||
button={true}
|
button={true}
|
||||||
|
@ -792,7 +648,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
anchor={"left"}
|
anchor={"left"}
|
||||||
open={this.state.drawerOpenOnMobile}
|
open={this.state.drawerOpenOnMobile}
|
||||||
onClick={this.toggleDrawerOnMobile}
|
onClose={this.toggleDrawerOnMobile}
|
||||||
classes={{ paper: classes.drawerPaper }}
|
classes={{ paper: classes.drawerPaper }}
|
||||||
>
|
>
|
||||||
{this.appDrawer()}
|
{this.appDrawer()}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { styles } from "./Attachment.styles";
|
import { styles } from "./Attachment.styles";
|
||||||
import { Attachment } from "../../types/Attachment";
|
import { Attachment } from "../../types/Attachment";
|
||||||
import AudioPlayer from "../AudioPlayer";
|
|
||||||
import SwipeableViews from "react-swipeable-views";
|
import SwipeableViews from "react-swipeable-views";
|
||||||
|
|
||||||
interface IAttachmentProps {
|
interface IAttachmentProps {
|
||||||
|
@ -77,15 +76,11 @@ class AttachmentComponent extends Component<
|
||||||
className={classes.mediaObject}
|
className={classes.mediaObject}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "audio":
|
|
||||||
return <AudioPlayer src={slide.url} id={slide.id} />;
|
|
||||||
case "gifv":
|
case "gifv":
|
||||||
return (
|
return (
|
||||||
<video
|
<img
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
src={slide.url}
|
src={slide.url}
|
||||||
title={slide.description ? slide.description : ""}
|
alt={slide.description ? slide.description : ""}
|
||||||
className={classes.mediaObject}
|
className={classes.mediaObject}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -111,36 +106,33 @@ class AttachmentComponent extends Component<
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SwipeableViews>
|
</SwipeableViews>
|
||||||
{this.state.totalSteps > 1 ? (
|
<MobileStepper
|
||||||
<MobileStepper
|
steps={this.state.totalSteps}
|
||||||
steps={this.state.totalSteps}
|
position="static"
|
||||||
position="static"
|
activeStep={this.state.currentStep}
|
||||||
activeStep={this.state.currentStep}
|
className={classes.mobileStepper}
|
||||||
className={classes.mobileStepper}
|
nextButton={
|
||||||
nextButton={
|
<Button
|
||||||
<Button
|
size="small"
|
||||||
size="small"
|
onClick={() => this.moveForward()}
|
||||||
onClick={() => this.moveForward()}
|
disabled={
|
||||||
disabled={
|
this.state.currentStep ===
|
||||||
this.state.currentStep ===
|
this.state.totalSteps - 1
|
||||||
this.state.totalSteps - 1
|
}
|
||||||
}
|
>
|
||||||
>
|
Next
|
||||||
Next
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
backButton={
|
||||||
backButton={
|
<Button
|
||||||
<Button
|
size="small"
|
||||||
size="small"
|
onClick={() => this.moveBack()}
|
||||||
onClick={() => this.moveBack()}
|
disabled={this.state.currentStep === 0}
|
||||||
disabled={this.state.currentStep === 0}
|
>
|
||||||
>
|
Back
|
||||||
Back
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<br />
|
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{mediaItem.description
|
{mediaItem.description
|
||||||
? 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,
|
paddingTop: theme.spacing.unit,
|
||||||
paddingBottom: 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: {
|
postAuthorEmoji: {
|
||||||
height: theme.typography.fontSize,
|
height: theme.typography.fontSize,
|
||||||
verticalAlign: "middle"
|
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() {
|
togglePostMenu() {
|
||||||
this.setState({ menuIsOpen: !this.state.menuIsOpen });
|
this.setState({ menuIsOpen: !this.state.menuIsOpen });
|
||||||
}
|
}
|
||||||
|
@ -401,58 +396,24 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
|
|
||||||
getReblogAuthors(post: Status) {
|
getReblogAuthors(post: Status) {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
|
if (post.reblog) {
|
||||||
let author = post.reblog ? post.reblog.account : post.account;
|
let author = post.reblog.account;
|
||||||
let emojis = author.emojis;
|
let origString = `<span>${author.display_name ||
|
||||||
let reblogger = post.reblog ? post.account : undefined;
|
author.username} (@${author.acct}) 🔄 ${post.account
|
||||||
|
.display_name || post.account.username}</span>`;
|
||||||
if (reblogger != undefined) {
|
let emojis = author.emojis;
|
||||||
emojis.concat(reblogger.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]) {
|
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) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
toggleFavorited(post: Status) {
|
||||||
* Tell server a post has been un/favorited and update post state
|
let _this = this;
|
||||||
* @param post The post to un/favorite
|
if (post.favourited) {
|
||||||
*/
|
this.client
|
||||||
async toggleFavorite(post: Status) {
|
.post(`/statuses/${post.id}/unfavourite`)
|
||||||
let action: string = post.favourited ? "unfavourite" : "favourite";
|
.then((resp: any) => {
|
||||||
try {
|
let post: Status = resp.data;
|
||||||
// favorite the original post, not the reblog
|
this.setState({ post });
|
||||||
let resp: any = await this.client.post(
|
})
|
||||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
.catch((err: Error) => {
|
||||||
);
|
_this.props.enqueueSnackbar(
|
||||||
// compensate for slow server update
|
`Couldn't unfavorite post: ${err.name}`,
|
||||||
if (action === "unfavourite") {
|
{
|
||||||
resp.data.favourites_count -= 1;
|
variant: "error"
|
||||||
// if you unlike both original and reblog before refresh
|
}
|
||||||
// and the post has only one favorite:
|
);
|
||||||
if (resp.data.favourites_count < 0) {
|
console.log(err.message);
|
||||||
resp.data.favourites_count = 0;
|
});
|
||||||
}
|
} else {
|
||||||
}
|
this.client
|
||||||
this.setState({ post: resp.data as Status });
|
.post(`/statuses/${post.id}/favourite`)
|
||||||
} catch (e) {
|
.then((resp: any) => {
|
||||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
let post: Status = resp.data;
|
||||||
console.error(e.message);
|
this.setState({ post });
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
_this.props.enqueueSnackbar(
|
||||||
|
`Couldn't favorite post: ${err.name}`,
|
||||||
|
{
|
||||||
|
variant: "error"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
toggleReblogged(post: Status) {
|
||||||
* Tell server a post has been un/reblogged and update post state
|
if (post.reblogged) {
|
||||||
* @param post The post to un/reblog
|
this.client
|
||||||
*/
|
.post(`/statuses/${post.id}/unreblog`)
|
||||||
async toggleReblog(post: Status) {
|
.then((resp: any) => {
|
||||||
let action: string =
|
let post: Status = resp.data;
|
||||||
post.reblogged || post.reblog ? "unreblog" : "reblog";
|
this.setState({ post });
|
||||||
try {
|
})
|
||||||
// modify the original post, not the reblog
|
.catch((err: Error) => {
|
||||||
let resp: any = await this.client.post(
|
this.props.enqueueSnackbar(
|
||||||
`/statuses/${post.reblog ? post.reblog.id : post.id}/${action}`
|
`Couldn't unboost post: ${err.name}`,
|
||||||
);
|
{
|
||||||
// compensate for slow server update
|
variant: "error"
|
||||||
if (action === "unreblog") {
|
}
|
||||||
resp.data.reblogs_count -= 1;
|
);
|
||||||
}
|
console.log(err.message);
|
||||||
if (resp.data.reblog) resp.data = resp.data.reblog;
|
});
|
||||||
this.setState({ post: resp.data as Status });
|
} else {
|
||||||
} catch (e) {
|
this.client
|
||||||
this.props.enqueueSnackbar(`Could not ${action} post: ${e.name}`);
|
.post(`/statuses/${post.id}/reblog`)
|
||||||
console.error(e.message);
|
.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 { classes } = this.props;
|
||||||
const post = this.state.post;
|
const post = this.state.post;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Zoom in={true}>
|
||||||
className={classes.post}
|
<Card
|
||||||
id={`post_${post.id}`}
|
className={classes.post}
|
||||||
elevation={this.props.threadHeader ? 0 : 1}
|
id={`post_${post.id}`}
|
||||||
>
|
elevation={this.props.threadHeader ? 0 : 1}
|
||||||
<CardHeader
|
>
|
||||||
avatar={
|
<CardHeader
|
||||||
<LinkableAvatar
|
avatar={
|
||||||
to={`/profile/${
|
<LinkableAvatar
|
||||||
post.reblog
|
to={`/profile/${
|
||||||
? post.reblog.account.id
|
post.reblog
|
||||||
: post.account.id
|
? post.reblog.account.id
|
||||||
}`}
|
: post.account.id
|
||||||
src={
|
}`}
|
||||||
post.reblog
|
src={
|
||||||
? post.reblog.account.avatar_static
|
post.reblog
|
||||||
: post.account.avatar_static
|
? 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 : ""
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
}
|
||||||
</Tooltip>
|
action={
|
||||||
<Typography>{post.favourites_count}</Typography>
|
<Tooltip title="More" placement="left">
|
||||||
<Tooltip title="Boost">
|
<IconButton
|
||||||
<IconButton onClick={() => this.toggleReblog(post)}>
|
key={`${post.id}_submenu`}
|
||||||
<AutorenewIcon
|
id={`${post.id}_submenu`}
|
||||||
className={
|
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
|
||||||
? 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
|
? classes.postDidAction
|
||||||
: ""
|
: ""
|
||||||
: post.reblogged
|
}
|
||||||
? classes.postDidAction
|
/>
|
||||||
: ""
|
</IconButton>
|
||||||
}
|
</Tooltip>
|
||||||
/>
|
<Typography>
|
||||||
</IconButton>
|
{post.reblog
|
||||||
</Tooltip>
|
? post.reblog.favourites_count
|
||||||
<Typography>
|
: post.favourites_count}
|
||||||
{post.reblog
|
</Typography>
|
||||||
? post.reblog.reblogs_count
|
<Tooltip title="Boost">
|
||||||
: post.reblogs_count}
|
<IconButton
|
||||||
</Typography>
|
onClick={() => this.toggleReblogged(post)}
|
||||||
<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
|
<AutorenewIcon
|
||||||
</LinkableMenuItem>
|
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
|
<LinkableMenuItem
|
||||||
to={`/profile/${post.account.id}`}
|
to={`/profile/${post.account.id}`}
|
||||||
>
|
>
|
||||||
View reblogger profile
|
View profile
|
||||||
</LinkableMenuItem>
|
</LinkableMenuItem>
|
||||||
</div>
|
)}
|
||||||
) : (
|
<div className={classes.mobileOnly}>
|
||||||
<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>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem
|
<LinkableMenuItem
|
||||||
onClick={() => this.togglePostDeleteDialog()}
|
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>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{this.state.myAccount &&
|
||||||
{this.showDeleteDialog()}
|
post.account.id === this.state.myAccount ? (
|
||||||
</Menu>
|
<div>
|
||||||
</Card>
|
<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) => {
|
export const PrivateRoute = (props: IPrivateRouteProps) => {
|
||||||
const { component, render, ...rest } = props;
|
const { component, render, ...rest } = props;
|
||||||
const redir = (comp: any) =>
|
|
||||||
userLoggedIn() ? comp : <Redirect to="/welcome" />;
|
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...rest}
|
||||||
render={(compProps: any) =>
|
render={(compProps: any) =>
|
||||||
redir(
|
userLoggedIn() ? (
|
||||||
React.createElement(render ? render : component, compProps)
|
React.createElement(component, compProps)
|
||||||
|
) : (
|
||||||
|
<Redirect to="/welcome" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -93,6 +93,5 @@ export const PrivateRoute = (props: IPrivateRouteProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IPrivateRouteProps extends RouteProps {
|
interface IPrivateRouteProps extends RouteProps {
|
||||||
component?: any;
|
component: any;
|
||||||
render?: 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: {
|
pollWizardFlexGrow: {
|
||||||
flexGrow: 1
|
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 { styles } from "./Compose.styles";
|
||||||
import { UAccount } from "../types/Account";
|
import { UAccount } from "../types/Account";
|
||||||
import { Visibility } from "../types/Visibility";
|
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 TagFacesIcon from "@material-ui/icons/TagFaces";
|
||||||
import HowToVoteIcon from "@material-ui/icons/HowToVote";
|
import HowToVoteIcon from "@material-ui/icons/HowToVote";
|
||||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||||
|
@ -39,140 +39,55 @@ import ComposeMediaAttachment from "../components/ComposeMediaAttachment";
|
||||||
import EmojiPicker from "../components/EmojiPicker";
|
import EmojiPicker from "../components/EmojiPicker";
|
||||||
import { DateTimePicker, MuiPickersUtilsProvider } from "material-ui-pickers";
|
import { DateTimePicker, MuiPickersUtilsProvider } from "material-ui-pickers";
|
||||||
import MomentUtils from "@date-io/moment";
|
import MomentUtils from "@date-io/moment";
|
||||||
import {
|
import { getUserDefaultVisibility, getConfig } from "../utilities/settings";
|
||||||
getUserDefaultVisibility,
|
|
||||||
getConfig,
|
|
||||||
getUserDefaultBool
|
|
||||||
} from "../utilities/settings";
|
|
||||||
import { draftExists, writeDraft, loadDraft } from "../utilities/compose";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The state for the Composer page.
|
|
||||||
*/
|
|
||||||
interface IComposerState {
|
interface IComposerState {
|
||||||
/**
|
|
||||||
* The current user as an Account.
|
|
||||||
*/
|
|
||||||
account: UAccount;
|
account: UAccount;
|
||||||
|
|
||||||
/**
|
|
||||||
* The visibility of the post.
|
|
||||||
*/
|
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether there should be a content warning.
|
|
||||||
*/
|
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The content warning message.
|
|
||||||
*/
|
|
||||||
sensitiveText?: string;
|
sensitiveText?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the visibility drop-down should be visible.
|
|
||||||
*/
|
|
||||||
visibilityMenu: boolean;
|
visibilityMenu: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The text contents of the post.
|
|
||||||
*/
|
|
||||||
text: string;
|
text: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The remaining amount of characters.
|
|
||||||
*/
|
|
||||||
remainingChars: number;
|
remainingChars: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional reply ID.
|
|
||||||
*/
|
|
||||||
reply?: string;
|
reply?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The account to reply to, if it exists.
|
|
||||||
*/
|
|
||||||
acct?: string;
|
acct?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional list of media attachments.
|
|
||||||
*/
|
|
||||||
attachments?: [Attachment];
|
attachments?: [Attachment];
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional poll for the post.
|
|
||||||
*/
|
|
||||||
poll?: PollWizard;
|
poll?: PollWizard;
|
||||||
|
|
||||||
/**
|
|
||||||
* The expiration date of a poll, if it exists.
|
|
||||||
*/
|
|
||||||
pollExpiresDate?: any;
|
pollExpiresDate?: any;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the emoji picker should be visible.
|
|
||||||
*/
|
|
||||||
showEmojis: boolean;
|
showEmojis: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the account's instance is federated.
|
|
||||||
*/
|
|
||||||
federated: boolean;
|
federated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The Compose page contains all of the information to create a UI for post creation.
|
|
||||||
*/
|
|
||||||
class Composer extends Component<any, IComposerState> {
|
class Composer extends Component<any, IComposerState> {
|
||||||
/**
|
|
||||||
* The Mastodon client to work with.
|
|
||||||
*/
|
|
||||||
client: Mastodon;
|
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) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Generate the Mastodon client
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
localStorage.getItem("access_token") as string,
|
localStorage.getItem("access_token") as string,
|
||||||
localStorage.getItem("baseurl") + "/api/v1"
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the initial state
|
|
||||||
this.state = {
|
this.state = {
|
||||||
account: JSON.parse(localStorage.getItem("account") as string),
|
account: JSON.parse(localStorage.getItem("account") as string),
|
||||||
visibility: getUserDefaultVisibility(),
|
visibility: getUserDefaultVisibility(),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
visibilityMenu: false,
|
visibilityMenu: false,
|
||||||
text: "",
|
text: "",
|
||||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
remainingChars: 500,
|
||||||
? 500
|
|
||||||
: 9999999999999,
|
|
||||||
showEmojis: false,
|
showEmojis: false,
|
||||||
federated: true
|
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() {
|
componentDidMount() {
|
||||||
// Parse the parameters and get the account information if available.
|
|
||||||
let state = this.getComposerParams(this.props);
|
let state = this.getComposerParams(this.props);
|
||||||
let text = state.acct ? `@${state.acct}: ` : "";
|
let text = state.acct ? `@${state.acct}: ` : "";
|
||||||
this.client.get("/accounts/verify_credentials").then((resp: any) => {
|
this.client.get("/accounts/verify_credentials").then((resp: any) => {
|
||||||
let account: UAccount = resp.data;
|
let account: UAccount = resp.data;
|
||||||
this.setState({ account });
|
this.setState({ account });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the configuration and load the config values.
|
|
||||||
getConfig().then((config: any) => {
|
getConfig().then((config: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
federated: config.federation.allowPublicPosts,
|
federated: config.federation.allowPublicPosts,
|
||||||
|
@ -180,43 +95,11 @@ class Composer extends Component<any, IComposerState> {
|
||||||
acct: state.acct,
|
acct: state.acct,
|
||||||
visibility: state.visibility,
|
visibility: state.visibility,
|
||||||
text,
|
text,
|
||||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
remainingChars: 500 - text.length
|
||||||
? 500 - text.length
|
|
||||||
: 99999999
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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) {
|
componentWillReceiveProps(props: any) {
|
||||||
let state = this.getComposerParams(props);
|
let state = this.getComposerParams(props);
|
||||||
let text = state.acct ? `@${state.acct}: ` : "";
|
let text = state.acct ? `@${state.acct}: ` : "";
|
||||||
|
@ -225,42 +108,10 @@ class Composer extends Component<any, IComposerState> {
|
||||||
acct: state.acct,
|
acct: state.acct,
|
||||||
visibility: state.visibility,
|
visibility: state.visibility,
|
||||||
text,
|
text,
|
||||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
remainingChars: 500 - text.length
|
||||||
? 500 - text.length
|
|
||||||
: 99999999
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
checkComposerParams(location?: string): ParsedQuery {
|
||||||
let params = "";
|
let params = "";
|
||||||
if (location !== undefined && typeof location === "string") {
|
if (location !== undefined && typeof location === "string") {
|
||||||
|
@ -271,11 +122,6 @@ class Composer extends Component<any, IComposerState> {
|
||||||
return parseParams(params);
|
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) {
|
getComposerParams(props: any) {
|
||||||
let params = this.checkComposerParams(props.location);
|
let params = this.checkComposerParams(props.location);
|
||||||
let reply: string = "";
|
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) {
|
updateTextFromField(text: string) {
|
||||||
this.setState({
|
this.setState({ text, remainingChars: 500 - text.length });
|
||||||
text,
|
|
||||||
remainingChars: getUserDefaultBool("imposeCharacterLimit")
|
|
||||||
? 500 - text.length
|
|
||||||
: 99999999
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the content warning text in the state
|
|
||||||
* @param sensitiveText The text to update the state to
|
|
||||||
*/
|
|
||||||
updateWarningFromField(sensitiveText: string) {
|
updateWarningFromField(sensitiveText: string) {
|
||||||
this.setState({ sensitiveText });
|
this.setState({ sensitiveText });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the visibility in the state
|
|
||||||
* @param visibility The visibility to update the state to
|
|
||||||
*/
|
|
||||||
changeVisibility(visibility: Visibility) {
|
changeVisibility(visibility: Visibility) {
|
||||||
this.setState({ visibility });
|
this.setState({ visibility });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
uploadMedia() {
|
||||||
* Open a file dialog to let the user choose files to upload to the server and then upload them.
|
|
||||||
*/
|
|
||||||
promptMediaDialog() {
|
|
||||||
filedialog({
|
filedialog({
|
||||||
multiple: false,
|
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) => {
|
.catch((err: Error) => {
|
||||||
this.props.enqueueSnackbar("Couldn't get media: " + err.name, {
|
this.props.enqueueSnackbar("Couldn't get media: " + err.name, {
|
||||||
variant: "error"
|
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() {
|
getOnlyMediaIds() {
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
if (this.state.attachments) {
|
if (this.state.attachments) {
|
||||||
|
@ -402,10 +208,6 @@ class Composer extends Component<any, IComposerState> {
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the list of attachments by inserting an attachment.
|
|
||||||
* @param attachment The attachment to insert into the attachments list.
|
|
||||||
*/
|
|
||||||
fetchAttachmentAfterUpdate(attachment: Attachment) {
|
fetchAttachmentAfterUpdate(attachment: Attachment) {
|
||||||
let attachments = this.state.attachments;
|
let attachments = this.state.attachments;
|
||||||
if (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) {
|
deleteMediaAttachment(attachment: Attachment) {
|
||||||
let attachments = this.state.attachments;
|
let attachments = this.state.attachments;
|
||||||
if (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) {
|
insertEmoji(e: any) {
|
||||||
if (e.custom) {
|
if (e.custom) {
|
||||||
let text = this.state.text + e.colons;
|
let text = this.state.text + e.colons;
|
||||||
|
@ -455,9 +249,6 @@ class Composer extends Component<any, IComposerState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an empty poll.
|
|
||||||
*/
|
|
||||||
createPoll() {
|
createPoll() {
|
||||||
if (this.state.poll === undefined) {
|
if (this.state.poll === undefined) {
|
||||||
let expiration = new Date();
|
let expiration = new Date();
|
||||||
|
@ -477,9 +268,6 @@ class Composer extends Component<any, IComposerState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a new poll item into the poll.
|
|
||||||
*/
|
|
||||||
addPollItem() {
|
addPollItem() {
|
||||||
if (
|
if (
|
||||||
this.state.poll !== undefined &&
|
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) {
|
editPollItem(position: number, newTitle: any) {
|
||||||
if (this.state.poll !== undefined) {
|
if (this.state.poll !== undefined) {
|
||||||
let poll = this.state.poll;
|
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) {
|
removePollItem(item: string) {
|
||||||
if (
|
if (
|
||||||
this.state.poll !== undefined &&
|
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) {
|
setPollExpires(date: string) {
|
||||||
let currentDate = new Date();
|
let currentDate = new Date();
|
||||||
let newDate = new Date(date);
|
let newDate = new Date(date);
|
||||||
let poll = this.state.poll;
|
let poll = this.state.poll;
|
||||||
if (poll) {
|
if (poll) {
|
||||||
let expiry = (newDate.getTime() - currentDate.getTime()) / 1000;
|
let expiry = (newDate.getTime() - currentDate.getTime()) / 1000;
|
||||||
|
console.log(expiry);
|
||||||
if (expiry >= 1800) {
|
if (expiry >= 1800) {
|
||||||
poll.expires_at = expiry.toString();
|
poll.expires_at = expiry.toString();
|
||||||
this.setState({ poll, pollExpiresDate: date });
|
this.setState({ poll, pollExpiresDate: date });
|
||||||
|
@ -577,38 +353,25 @@ class Composer extends Component<any, IComposerState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the poll from the post.
|
|
||||||
*/
|
|
||||||
removePoll() {
|
removePoll() {
|
||||||
this.setState({
|
this.setState({
|
||||||
poll: undefined
|
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) {
|
postViaKeyboard(event: any) {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) {
|
if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) {
|
||||||
this.post();
|
this.post();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the post to Mastodon and return to the previous page, if possible.
|
|
||||||
*/
|
|
||||||
post() {
|
post() {
|
||||||
// First, finalize the poll.
|
|
||||||
let pollOptions: string[] = [];
|
let pollOptions: string[] = [];
|
||||||
if (this.state.poll) {
|
if (this.state.poll) {
|
||||||
this.state.poll.options.forEach((option: PollWizardOption) => {
|
this.state.poll.options.forEach((option: PollWizardOption) => {
|
||||||
pollOptions.push(option.title);
|
pollOptions.push(option.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a post request to Mastodon.
|
|
||||||
this.client
|
this.client
|
||||||
.post("/statuses", {
|
.post("/statuses", {
|
||||||
status: this.state.text,
|
status: this.state.text,
|
||||||
|
@ -625,52 +388,31 @@ class Composer extends Component<any, IComposerState> {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// If we succeed, send a success message, clear the status
|
|
||||||
// text field, and go back.
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.props.enqueueSnackbar("Posted!");
|
this.props.enqueueSnackbar("Posted!");
|
||||||
|
|
||||||
// This is necessary to prevent session drafts from saving
|
|
||||||
// posts that were already posted.
|
|
||||||
this.setState({ text: "" });
|
|
||||||
|
|
||||||
window.history.back();
|
window.history.back();
|
||||||
})
|
})
|
||||||
|
|
||||||
// Otherwise, show an error message and don't do anything.
|
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
||||||
console.error(err.message);
|
console.log(err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the content warning section.
|
|
||||||
*/
|
|
||||||
toggleSensitive() {
|
toggleSensitive() {
|
||||||
this.setState({ sensitive: !this.state.sensitive });
|
this.setState({ sensitive: !this.state.sensitive });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the visibility drop down menu.
|
|
||||||
*/
|
|
||||||
toggleVisibilityMenu() {
|
toggleVisibilityMenu() {
|
||||||
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the emoji picker.
|
|
||||||
*/
|
|
||||||
toggleEmojis() {
|
toggleEmojis() {
|
||||||
this.setState({ showEmojis: !this.state.showEmojis });
|
this.setState({ showEmojis: !this.state.showEmojis });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render all of the components on the page given a set of classes.
|
|
||||||
*/
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
|
console.log(this.state);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -727,28 +469,18 @@ class Composer extends Component<any, IComposerState> {
|
||||||
}}
|
}}
|
||||||
value={this.state.text}
|
value={this.state.text}
|
||||||
/>
|
/>
|
||||||
{getUserDefaultBool("imposeCharacterLimit") ? (
|
<Typography
|
||||||
<Typography
|
variant="caption"
|
||||||
variant="caption"
|
className={
|
||||||
className={
|
this.state.remainingChars <= 100
|
||||||
this.state.remainingChars <= 100
|
? classes.charsReachingLimit
|
||||||
? classes.charsReachingLimit
|
: null
|
||||||
: null
|
}
|
||||||
}
|
>
|
||||||
>
|
{`${this.state.remainingChars} character${
|
||||||
{`${this.state.remainingChars} character${
|
this.state.remainingChars === 1 ? "" : "s"
|
||||||
this.state.remainingChars === 1 ? "" : "s"
|
} remaining`}
|
||||||
} remaining`}
|
</Typography>
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.attachments &&
|
{this.state.attachments &&
|
||||||
this.state.attachments.length > 0 ? (
|
this.state.attachments.length > 0 ? (
|
||||||
<div className={classes.composeAttachmentArea}>
|
<div className={classes.composeAttachmentArea}>
|
||||||
|
@ -873,13 +605,13 @@ class Composer extends Component<any, IComposerState> {
|
||||||
) : null}
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<Toolbar className={classes.dialogActions}>
|
<Toolbar className={classes.dialogActions}>
|
||||||
<Tooltip title="Add photos, videos, or audio">
|
<Tooltip title="Add photos or videos">
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={this.state.poll !== undefined}
|
disabled={this.state.poll !== undefined}
|
||||||
onClick={() => this.promptMediaDialog()}
|
onClick={() => this.uploadMedia()}
|
||||||
id="compose-media"
|
id="compose-media"
|
||||||
>
|
>
|
||||||
<AttachFileIcon />
|
<CameraAltIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Insert emoji">
|
<Tooltip title="Insert emoji">
|
||||||
|
@ -962,21 +694,6 @@ class Composer extends Component<any, IComposerState> {
|
||||||
) : null}
|
) : null}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Toolbar>
|
</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>
|
<DialogActions>
|
||||||
<Button color="secondary" onClick={() => this.post()}>
|
<Button color="secondary" onClick={() => this.post()}>
|
||||||
Post
|
Post
|
||||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
||||||
import { Status } from "../types/Status";
|
import { Status } from "../types/Status";
|
||||||
import Mastodon, { StreamListener } from "megalodon";
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
import { withSnackbar } from "notistack";
|
import { withSnackbar } from "notistack";
|
||||||
import Masonry from "react-masonry-css";
|
|
||||||
import { getUserDefaultBool } from "../utilities/settings";
|
|
||||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface IHomePageState {
|
interface IHomePageState {
|
||||||
|
@ -25,14 +23,8 @@ interface IHomePageState {
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
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> {
|
class HomePage extends Component<any, IHomePageState> {
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
streamListener: StreamListener;
|
streamListener: StreamListener;
|
||||||
|
@ -42,8 +34,7 @@ class HomePage extends Component<any, IHomePageState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
backlogPosts: null,
|
backlogPosts: null
|
||||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
|
@ -163,11 +154,9 @@ class HomePage extends Component<any, IHomePageState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
|
||||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
|
||||||
}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
{this.state.backlogPosts ? (
|
{this.state.backlogPosts ? (
|
||||||
<div className={classes.pageTopChipContainer}>
|
<div className={classes.pageTopChipContainer}>
|
||||||
<div className={classes.pageTopChips}>
|
<div className={classes.pageTopChips}>
|
||||||
|
@ -196,46 +185,15 @@ class HomePage extends Component<any, IHomePageState> {
|
||||||
) : null}
|
) : null}
|
||||||
{this.state.posts ? (
|
{this.state.posts ? (
|
||||||
<div>
|
<div>
|
||||||
{this.state.isMasonryLayout ? (
|
{this.state.posts.map((post: Status) => {
|
||||||
<Masonry
|
return (
|
||||||
breakpointCols={{
|
<Post
|
||||||
default: 4,
|
key={post.id}
|
||||||
2000: 3,
|
post={post}
|
||||||
1400: 2,
|
client={this.client}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<br />
|
<br />
|
||||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
||||||
import { Status } from "../types/Status";
|
import { Status } from "../types/Status";
|
||||||
import Mastodon, { StreamListener } from "megalodon";
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
import { withSnackbar } from "notistack";
|
import { withSnackbar } from "notistack";
|
||||||
import Masonry from "react-masonry-css";
|
|
||||||
import { getUserDefaultBool } from "../utilities/settings";
|
|
||||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface ILocalPageState {
|
interface ILocalPageState {
|
||||||
|
@ -25,14 +23,8 @@ interface ILocalPageState {
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
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> {
|
class LocalPage extends Component<any, ILocalPageState> {
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
streamListener: StreamListener;
|
streamListener: StreamListener;
|
||||||
|
@ -42,8 +34,7 @@ class LocalPage extends Component<any, ILocalPageState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
backlogPosts: null,
|
backlogPosts: null
|
||||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
|
@ -164,12 +155,9 @@ class LocalPage extends Component<any, ILocalPageState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
|
||||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
{this.state.backlogPosts ? (
|
{this.state.backlogPosts ? (
|
||||||
<div className={classes.pageTopChipContainer}>
|
<div className={classes.pageTopChipContainer}>
|
||||||
<div className={classes.pageTopChips}>
|
<div className={classes.pageTopChips}>
|
||||||
|
@ -198,50 +186,15 @@ class LocalPage extends Component<any, ILocalPageState> {
|
||||||
) : null}
|
) : null}
|
||||||
{this.state.posts ? (
|
{this.state.posts ? (
|
||||||
<div>
|
<div>
|
||||||
{this.state.isMasonryLayout ? (
|
{this.state.posts.map((post: Status) => {
|
||||||
<Masonry
|
return (
|
||||||
breakpointCols={{
|
<Post
|
||||||
default: 4,
|
key={post.id}
|
||||||
2000: 3,
|
post={post}
|
||||||
1400: 2,
|
client={this.client}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<br />
|
<br />
|
||||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -10,12 +10,10 @@ import {
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
Avatar,
|
Avatar,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
Tooltip,
|
Tooltip
|
||||||
Typography
|
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import PersonIcon from "@material-ui/icons/Person";
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
import ForumIcon from "@material-ui/icons/Forum";
|
import ForumIcon from "@material-ui/icons/Forum";
|
||||||
import MailIcon from "@material-ui/icons/Mail";
|
|
||||||
import { styles } from "./PageLayout.styles";
|
import { styles } from "./PageLayout.styles";
|
||||||
import Mastodon from "megalodon";
|
import Mastodon from "megalodon";
|
||||||
import { Status } from "../types/Status";
|
import { Status } from "../types/Status";
|
||||||
|
@ -72,82 +70,67 @@ class MessagesPage extends Component<any, IMessagesState> {
|
||||||
return innerContent;
|
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() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classes.pageLayoutConstraints}>
|
<div className={classes.pageLayoutConstraints}>
|
||||||
{this.state.viewDidLoad ? (
|
{this.state.viewDidLoad ? (
|
||||||
<div className={classes.pageListContsraints}>
|
<div className={classes.pageListContsraints}>
|
||||||
{this.state.posts && this.state.posts.length > 0 ? (
|
<ListSubheader>Recent messages</ListSubheader>
|
||||||
<div>
|
<Paper className={classes.pageListConstraints}>
|
||||||
<ListSubheader>Recent messages</ListSubheader>
|
<List>
|
||||||
<Paper className={classes.pageListConstraints}>
|
{this.state.posts
|
||||||
<List>
|
? this.state.posts.map(
|
||||||
{this.state.posts
|
(message: Status) => {
|
||||||
? this.state.posts.map(
|
return (
|
||||||
(message: Status) =>
|
<ListItem>
|
||||||
this.renderMessage(
|
<ListItemAvatar>
|
||||||
message
|
<LinkableAvatar
|
||||||
)
|
to={`/profile/${message.account.id}`}
|
||||||
)
|
alt={
|
||||||
: null}
|
message
|
||||||
</List>
|
.account
|
||||||
</Paper>
|
.username
|
||||||
<br />
|
}
|
||||||
</div>
|
src={
|
||||||
) : (
|
message
|
||||||
<div>
|
.account
|
||||||
<div
|
.avatar_static
|
||||||
className={
|
}
|
||||||
classes.pageLayoutEmptyTextConstraints
|
>
|
||||||
}
|
<PersonIcon />
|
||||||
style={{ textAlign: "center" }}
|
</LinkableAvatar>
|
||||||
>
|
</ListItemAvatar>
|
||||||
<MailIcon
|
<ListItemText
|
||||||
color="action"
|
primary={
|
||||||
style={{ fontSize: 48 }}
|
message.account
|
||||||
/>
|
.display_name ||
|
||||||
<Typography variant="h6">
|
"@" +
|
||||||
You don't have any messages.
|
message
|
||||||
</Typography>
|
.account
|
||||||
<Typography paragraph>
|
.acct
|
||||||
Why not interact with the fediverse a
|
}
|
||||||
bit by sending a message?
|
secondary={this.removeHTMLContent(
|
||||||
</Typography>
|
message.content
|
||||||
<br />
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<ListItemSecondaryAction>
|
||||||
)}
|
<Tooltip title="View conversation">
|
||||||
|
<LinkableIconButton
|
||||||
|
to={`/conversation/${message.id}`}
|
||||||
|
>
|
||||||
|
<ForumIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{this.state.viewIsLoading ? (
|
{this.state.viewIsLoading ? (
|
||||||
|
|
|
@ -17,128 +17,56 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Tooltip,
|
Tooltip
|
||||||
Menu,
|
|
||||||
MenuItem
|
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
|
||||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
import PersonIcon from "@material-ui/icons/Person";
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
import { styles } from "./PageLayout.styles";
|
import { styles } from "./PageLayout.styles";
|
||||||
import {
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
LinkableIconButton,
|
|
||||||
LinkableAvatar,
|
|
||||||
LinkableMenuItem
|
|
||||||
} from "../interfaces/overrides";
|
|
||||||
import ForumIcon from "@material-ui/icons/Forum";
|
import ForumIcon from "@material-ui/icons/Forum";
|
||||||
import ReplyIcon from "@material-ui/icons/Reply";
|
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 Mastodon from "megalodon";
|
||||||
import { Notification } from "../types/Notification";
|
import { Notification } from "../types/Notification";
|
||||||
import { Account } from "../types/Account";
|
import { Account } from "../types/Account";
|
||||||
import { Relationship } from "../types/Relationship";
|
|
||||||
import { withSnackbar } from "notistack";
|
import { withSnackbar } from "notistack";
|
||||||
import { Dictionary } from "../interfaces/utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The state interface for the notifications page.
|
|
||||||
*/
|
|
||||||
interface INotificationsPageState {
|
interface INotificationsPageState {
|
||||||
/**
|
|
||||||
* The list of notifications, if it exists.
|
|
||||||
*/
|
|
||||||
notifications?: [Notification];
|
notifications?: [Notification];
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the view is still loading.
|
|
||||||
*/
|
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the view has loaded.
|
|
||||||
*/
|
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the view has loaded but in error.
|
|
||||||
*/
|
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The error code for an errored state, if possible.
|
|
||||||
*/
|
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the delete confirmation dialog should be open.
|
|
||||||
*/
|
|
||||||
deleteDialogOpen: boolean;
|
deleteDialogOpen: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the menu should be open on smaller devices.
|
|
||||||
*/
|
|
||||||
mobileMenuOpen: Dictionary<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The notifications page.
|
|
||||||
*/
|
|
||||||
class NotificationsPage extends Component<any, INotificationsPageState> {
|
class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
/**
|
|
||||||
* The Mastodon object to perform notification operations on.
|
|
||||||
*/
|
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
|
|
||||||
/**
|
|
||||||
* The stream listener for tuning in to notifications.
|
|
||||||
*/
|
|
||||||
streamListener: any;
|
streamListener: any;
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct the notifications page.
|
|
||||||
* @param props The properties to pass in
|
|
||||||
*/
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Create the Mastodon object.
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
localStorage.getItem("access_token") as string,
|
localStorage.getItem("access_token") as string,
|
||||||
localStorage.getItem("baseurl") + "/api/v1"
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize the state.
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
deleteDialogOpen: false,
|
deleteDialogOpen: false
|
||||||
mobileMenuOpen: {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform pre-mount tasks.
|
|
||||||
*/
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
// Get the list of notifications and update the state.
|
|
||||||
this.client
|
this.client
|
||||||
.get("/notifications")
|
.get("/notifications")
|
||||||
.then((resp: any) => {
|
.then((resp: any) => {
|
||||||
let notifications: [Notification] = resp.data;
|
let notifications: [Notification] = resp.data;
|
||||||
let notifMenus: Dictionary<boolean> = {};
|
|
||||||
|
|
||||||
notifications.forEach((notif: Notification) => {
|
|
||||||
notifMenus[notif.id] = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
notifications,
|
notifications,
|
||||||
viewIsLoading: false,
|
viewIsLoading: false,
|
||||||
viewDidLoad: true,
|
viewDidLoad: true
|
||||||
mobileMenuOpen: notifMenus
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
|
@ -151,17 +79,10 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform post-mount tasks.
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Start listening for new notifications after fetching.
|
|
||||||
this.streamNotifications();
|
this.streamNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up a stream listener and keep updating notifications.
|
|
||||||
*/
|
|
||||||
streamNotifications() {
|
streamNotifications() {
|
||||||
this.streamListener = this.client.stream("/streaming/user");
|
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() {
|
toggleDeleteDialog() {
|
||||||
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
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) {
|
removeHTMLContent(text: string) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.innerHTML = text;
|
div.innerHTML = text;
|
||||||
|
@ -202,10 +108,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
return innerContent;
|
return innerContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a notification from the server.
|
|
||||||
* @param id The notification's ID
|
|
||||||
*/
|
|
||||||
removeNotification(id: string) {
|
removeNotification(id: string) {
|
||||||
this.client
|
this.client
|
||||||
.post(`/notifications/${id}/dismiss`)
|
.post(`/notifications/${id}/dismiss`)
|
||||||
|
@ -237,9 +139,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge all notifications from the server.
|
|
||||||
*/
|
|
||||||
removeAllNotifications() {
|
removeAllNotifications() {
|
||||||
this.client
|
this.client
|
||||||
.post("/notifications/clear")
|
.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) {
|
createNotification(notif: Notification) {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
let primary = "";
|
let primary = "";
|
||||||
|
@ -333,108 +228,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<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" ? (
|
{notif.type === "follow" ? (
|
||||||
<span>
|
<span>
|
||||||
<Tooltip title="View profile">
|
<Tooltip title="View profile">
|
||||||
|
@ -492,14 +285,28 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</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() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -530,20 +337,12 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={classes.pageLayoutEmptyTextConstraints}>
|
||||||
className={classes.pageLayoutEmptyTextConstraints}
|
<Typography variant="h4">All clear!</Typography>
|
||||||
style={{ textAlign: "center" }}
|
|
||||||
>
|
|
||||||
<NotificationsIcon
|
|
||||||
color="action"
|
|
||||||
style={{ fontSize: 48 }}
|
|
||||||
/>
|
|
||||||
<Typography variant="h6">All clear!</Typography>
|
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
It looks like you have no notifications. Why not
|
It looks like you have no notifications. Why not
|
||||||
get the conversation going with a new post?
|
get the conversation going with a new post?
|
||||||
</Typography>
|
</Typography>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : null}
|
) : 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 { isDarwinApp } from "../utilities/desktop";
|
||||||
import { isAppbarExpanded } from "../utilities/appbar";
|
import { isAppbarExpanded } from "../utilities/appbar";
|
||||||
|
|
||||||
|
@ -323,21 +323,5 @@ export const styles = (theme: Theme) =>
|
||||||
display: "block"
|
display: "block"
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.primary.main
|
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 { withSnackbar } from "notistack";
|
||||||
import { LinkableIconButton } from "../interfaces/overrides";
|
import { LinkableIconButton } from "../interfaces/overrides";
|
||||||
import { emojifyString } from "../utilities/emojis";
|
import { emojifyString } from "../utilities/emojis";
|
||||||
import Masonry from "react-masonry-css";
|
|
||||||
import { getUserDefaultBool } from "..//utilities/settings";
|
|
||||||
|
|
||||||
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
|
@ -46,7 +44,6 @@ interface IProfilePageState {
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
blockDialogOpen: boolean;
|
blockDialogOpen: boolean;
|
||||||
isMasonryLayout?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfilePage extends Component<any, IProfilePageState> {
|
class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
|
@ -62,8 +59,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
blockDialogOpen: false,
|
blockDialogOpen: false
|
||||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageContentLayoutConstraints} ${
|
|
||||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
|
||||||
}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.pageLayoutMinimalConstraints}>
|
<div className={classes.pageLayoutMinimalConstraints}>
|
||||||
<div className={classes.pageHeroBackground}>
|
<div className={classes.pageHeroBackground}>
|
||||||
|
@ -496,7 +464,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={containerClasses}>
|
<div className={classes.pageContentLayoutConstraints}>
|
||||||
{this.state.viewDidError ? (
|
{this.state.viewDidError ? (
|
||||||
<Paper className={classes.errorCard}>
|
<Paper className={classes.errorCard}>
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
@ -514,7 +482,15 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
)}
|
)}
|
||||||
{this.state.posts ? (
|
{this.state.posts ? (
|
||||||
<div>
|
<div>
|
||||||
{this.renderPosts(this.state.posts)}
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return (
|
||||||
|
<Post
|
||||||
|
key={post.id}
|
||||||
|
post={post}
|
||||||
|
client={this.client}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<br />
|
<br />
|
||||||
{this.state.viewDidLoad &&
|
{this.state.viewDidLoad &&
|
||||||
!this.state.viewDidError ? (
|
!this.state.viewDidError ? (
|
||||||
|
|
|
@ -14,8 +14,6 @@ import Post from "../components/Post";
|
||||||
import { Status } from "../types/Status";
|
import { Status } from "../types/Status";
|
||||||
import Mastodon, { StreamListener } from "megalodon";
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
import { withSnackbar } from "notistack";
|
import { withSnackbar } from "notistack";
|
||||||
import Masonry from "react-masonry-css";
|
|
||||||
import { getUserDefaultBool } from "../utilities/settings";
|
|
||||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface IPublicPageState {
|
interface IPublicPageState {
|
||||||
|
@ -25,14 +23,8 @@ interface IPublicPageState {
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
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> {
|
class PublicPage extends Component<any, IPublicPageState> {
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
streamListener: StreamListener;
|
streamListener: StreamListener;
|
||||||
|
@ -42,8 +34,7 @@ class PublicPage extends Component<any, IPublicPageState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
backlogPosts: null,
|
backlogPosts: null
|
||||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.client = new Mastodon(
|
this.client = new Mastodon(
|
||||||
|
@ -163,12 +154,9 @@ class PublicPage extends Component<any, IPublicPageState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageLayoutMaxConstraints}${
|
|
||||||
this.state.isMasonryLayout ? " " + classes.pageLayoutMasonry : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
{this.state.backlogPosts ? (
|
{this.state.backlogPosts ? (
|
||||||
<div className={classes.pageTopChipContainer}>
|
<div className={classes.pageTopChipContainer}>
|
||||||
<div className={classes.pageTopChips}>
|
<div className={classes.pageTopChips}>
|
||||||
|
@ -197,50 +185,15 @@ class PublicPage extends Component<any, IPublicPageState> {
|
||||||
) : null}
|
) : null}
|
||||||
{this.state.posts ? (
|
{this.state.posts ? (
|
||||||
<div>
|
<div>
|
||||||
{this.state.isMasonryLayout ? (
|
{this.state.posts.map((post: Status) => {
|
||||||
<Masonry
|
return (
|
||||||
breakpointCols={{
|
<Post
|
||||||
default: 4,
|
key={post.id}
|
||||||
2000: 3,
|
post={post}
|
||||||
1400: 2,
|
client={this.client}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<br />
|
<br />
|
||||||
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -6,22 +6,25 @@ import {
|
||||||
ListItem,
|
ListItem,
|
||||||
Paper,
|
Paper,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
Avatar,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Divider,
|
||||||
Link
|
Tooltip
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { styles } from "./PageLayout.styles";
|
import { styles } from "./PageLayout.styles";
|
||||||
import Mastodon from "megalodon";
|
import Mastodon from "megalodon";
|
||||||
import { Account } from "../types/Account";
|
import { Account } from "../types/Account";
|
||||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
|
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
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 { withSnackbar, withSnackbarProps } from "notistack";
|
||||||
import GroupIcon from "@material-ui/icons/Group";
|
|
||||||
|
|
||||||
interface IRecommendationsPageProps extends withSnackbarProps {
|
interface IRecommendationsPageProps extends withSnackbarProps {
|
||||||
classes: any;
|
classes: any;
|
||||||
|
@ -32,6 +35,7 @@ interface IRecommendationsPageState {
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: Boolean;
|
viewDidError?: Boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
|
requestedFollows?: [Account];
|
||||||
followSuggestions?: [Account];
|
followSuggestions?: [Account];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +57,21 @@ class RecommendationsPage extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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
|
this.client
|
||||||
.get("/suggestions")
|
.get("/suggestions")
|
||||||
.then((resp: any) => {
|
.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() {
|
showFollowSuggestions() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -172,6 +295,23 @@ class RecommendationsPage extends Component<
|
||||||
<div className={classes.pageLayoutConstraints}>
|
<div className={classes.pageLayoutConstraints}>
|
||||||
{this.state.viewDidLoad ? (
|
{this.state.viewDidLoad ? (
|
||||||
<div>
|
<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 &&
|
||||||
this.state.followSuggestions.length > 0 ? (
|
this.state.followSuggestions.length > 0 ? (
|
||||||
this.showFollowSuggestions()
|
this.showFollowSuggestions()
|
||||||
|
@ -180,35 +320,23 @@ class RecommendationsPage extends Component<
|
||||||
className={
|
className={
|
||||||
classes.pageLayoutEmptyTextConstraints
|
classes.pageLayoutEmptyTextConstraints
|
||||||
}
|
}
|
||||||
style={{ textAlign: "center" }}
|
|
||||||
>
|
>
|
||||||
<GroupIcon
|
<Typography variant="h5">
|
||||||
color="action"
|
|
||||||
style={{ fontSize: 48 }}
|
|
||||||
/>
|
|
||||||
<Typography variant="h6">
|
|
||||||
We don't have any suggestions for you.
|
We don't have any suggestions for you.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
Take a look around the fediverse or check
|
Why not interact with the fediverse a bit by
|
||||||
out the Activity page for more.
|
creating a new post?
|
||||||
</Typography>
|
</Typography>
|
||||||
<br />
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{this.state.viewDidError ? (
|
{this.state.viewDidError ? (
|
||||||
<Paper className={classes.errorCard}>
|
<Paper className={classes.errorCard}>
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
Something went wrong when loading recommendations.
|
Something went wrong when loading this timeline.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
{this.state.viewDidErrorCode
|
{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 Post from "../components/Post";
|
||||||
import { Status } from "../types/Status";
|
import { Status } from "../types/Status";
|
||||||
import { Account } from "../types/Account";
|
import { Account } from "../types/Account";
|
||||||
import Masonry from "react-masonry-css";
|
|
||||||
import { getUserDefaultBool } from "../utilities/settings";
|
|
||||||
|
|
||||||
interface ISearchPageState {
|
interface ISearchPageState {
|
||||||
query: string[] | string;
|
query: string[] | string;
|
||||||
|
@ -38,7 +36,6 @@ interface ISearchPageState {
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
isMasonryLayout: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchPage extends Component<any, ISearchPageState> {
|
class SearchPage extends Component<any, ISearchPageState> {
|
||||||
|
@ -57,8 +54,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
viewIsLoading: true,
|
viewIsLoading: true,
|
||||||
query: searchParams.query,
|
query: searchParams.query,
|
||||||
type: searchParams.type,
|
type: searchParams.type
|
||||||
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (searchParams.type === "tag") {
|
if (searchParams.type === "tag") {
|
||||||
|
@ -199,7 +195,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
||||||
showAllAccountsFromQuery() {
|
showAllAccountsFromQuery() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classes.pageLayoutConstraints}>
|
<div>
|
||||||
<ListSubheader>Accounts</ListSubheader>
|
<ListSubheader>Accounts</ListSubheader>
|
||||||
|
|
||||||
{this.state.results &&
|
{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() {
|
showAllPostsFromQuery() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageLayoutConstraints} ${
|
|
||||||
this.state.isMasonryLayout
|
|
||||||
? classes.pageLayoutMasonry + " " + classes.noTopPaddingMargin
|
|
||||||
: ""
|
|
||||||
}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div>
|
||||||
<ListSubheader>Posts</ListSubheader>
|
<ListSubheader>Posts</ListSubheader>
|
||||||
{this.state.results ? (
|
{this.state.results ? (
|
||||||
this.state.results.statuses.length > 0 ? (
|
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
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
|
@ -317,15 +291,20 @@ class SearchPage extends Component<any, ISearchPageState> {
|
||||||
|
|
||||||
showAllPostsWithTag() {
|
showAllPostsWithTag() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
const containerClasses = `${classes.pageLayoutMaxConstraints} ${
|
|
||||||
this.state.isMasonryLayout ? classes.pageLayoutMasonry : ""
|
|
||||||
}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div>
|
||||||
<ListSubheader>Tagged posts</ListSubheader>
|
<ListSubheader>Tagged posts</ListSubheader>
|
||||||
{this.state.tagResults ? (
|
{this.state.tagResults ? (
|
||||||
this.state.tagResults.length > 0 ? (
|
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
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
|
@ -342,7 +321,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.pageLayoutConstraints}>
|
||||||
{this.state.type && this.state.type === "tag" ? (
|
{this.state.type && this.state.type === "tag" ? (
|
||||||
this.showAllPostsWithTag()
|
this.showAllPostsWithTag()
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -62,11 +62,6 @@ import BellAlertIcon from "mdi-material-ui/BellAlert";
|
||||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||||
import UndoIcon from "@material-ui/icons/Undo";
|
import UndoIcon from "@material-ui/icons/Undo";
|
||||||
import DomainDisabledIcon from "@material-ui/icons/DomainDisabled";
|
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 { Config } from "../types/Config";
|
||||||
import { Account } from "../types/Account";
|
import { Account } from "../types/Account";
|
||||||
import Mastodon from "megalodon";
|
import Mastodon from "megalodon";
|
||||||
|
@ -87,9 +82,6 @@ interface ISettingsState {
|
||||||
brandName: string;
|
brandName: string;
|
||||||
federated: boolean;
|
federated: boolean;
|
||||||
currentUser?: Account;
|
currentUser?: Account;
|
||||||
imposeCharacterLimit: boolean;
|
|
||||||
masonryLayout?: boolean;
|
|
||||||
infiniteScroll?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsPage extends Component<any, ISettingsState> {
|
class SettingsPage extends Component<any, ISettingsState> {
|
||||||
|
@ -120,10 +112,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
setHyperspaceTheme(defaultTheme),
|
setHyperspaceTheme(defaultTheme),
|
||||||
defaultVisibility: getUserDefaultVisibility() || "public",
|
defaultVisibility: getUserDefaultVisibility() || "public",
|
||||||
brandName: "Hyperspace",
|
brandName: "Hyperspace",
|
||||||
federated: true,
|
federated: true
|
||||||
imposeCharacterLimit: getUserDefaultBool("imposeCharacterLimit"),
|
|
||||||
masonryLayout: getUserDefaultBool("isMasonryLayout"),
|
|
||||||
infiniteScroll: getUserDefaultBool("isInfiniteScroll")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
||||||
|
@ -132,8 +121,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
this.toggleBadgeCount = this.toggleBadgeCount.bind(this);
|
this.toggleBadgeCount = this.toggleBadgeCount.bind(this);
|
||||||
this.toggleThemeDialog = this.toggleThemeDialog.bind(this);
|
this.toggleThemeDialog = this.toggleThemeDialog.bind(this);
|
||||||
this.toggleVisibilityDialog = this.toggleVisibilityDialog.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.changeThemeName = this.changeThemeName.bind(this);
|
||||||
this.changeTheme = this.changeTheme.bind(this);
|
this.changeTheme = this.changeTheme.bind(this);
|
||||||
this.setVisibility = this.setVisibility.bind(this);
|
this.setVisibility = this.setVisibility.bind(this);
|
||||||
|
@ -174,6 +161,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
getConfig().then((result: any) => {
|
getConfig().then((result: any) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
let config: Config = result;
|
let config: Config = result;
|
||||||
|
console.log(!config.federation.allowPublicPosts);
|
||||||
this.setState({
|
this.setState({
|
||||||
federated: config.federation.allowPublicPosts
|
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() {
|
toggleResetDialog() {
|
||||||
this.setState({
|
this.setState({
|
||||||
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
|
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
|
||||||
|
@ -248,16 +226,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
this.setState({ resetSettingsDialog: !this.state.resetSettingsDialog });
|
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() {
|
changeTheme() {
|
||||||
setUserDefaultTheme(this.state.selectThemeName);
|
setUserDefaultTheme(this.state.selectThemeName);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
@ -534,7 +502,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.pageGrow} />
|
<div className={classes.pageGrow} />
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Tooltip title="Edit profile">
|
<Tooltip title="Edit Profile">
|
||||||
<LinkableIconButton
|
<LinkableIconButton
|
||||||
to={"/you"}
|
to={"/you"}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
@ -550,14 +518,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
<DomainDisabledIcon />
|
<DomainDisabledIcon />
|
||||||
</LinkableIconButton>
|
</LinkableIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Manage follow requests">
|
|
||||||
<LinkableIconButton
|
|
||||||
to={"/requests"}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<AccountSettingsIcon />
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Configure on Mastodon">
|
<Tooltip title="Configure on Mastodon">
|
||||||
<IconButton
|
<IconButton
|
||||||
href={
|
href={
|
||||||
|
@ -576,36 +536,7 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null}
|
||||||
<div className={classes.pageHeroBackground}>
|
|
||||||
<div className={classes.pageHeroBackgroundImage} />
|
|
||||||
<div className={classes.profileContent}>
|
|
||||||
<br />
|
|
||||||
<Avatar className={classes.settingsAvatar} />
|
|
||||||
<div
|
|
||||||
className={classes.profileUserBox}
|
|
||||||
style={{ margin: "auto" }}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
className={classes.settingsHeaderText}
|
|
||||||
color="inherit"
|
|
||||||
component="h1"
|
|
||||||
>
|
|
||||||
{"Loading..."}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
color="inherit"
|
|
||||||
className={classes.settingsDetailText}
|
|
||||||
component="p"
|
|
||||||
>
|
|
||||||
@{"..."}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={classes.pageGrow} />
|
|
||||||
<Toolbar />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={classes.pageContentLayoutConstraints}>
|
<div className={classes.pageContentLayoutConstraints}>
|
||||||
<ListSubheader>Appearance</ListSubheader>
|
<ListSubheader>Appearance</ListSubheader>
|
||||||
<Paper className={classes.pageListConstraints}>
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
@ -667,38 +598,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</Button>
|
</Button>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</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>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br />
|
<br />
|
||||||
|
@ -723,25 +622,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</Button>
|
</Button>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</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>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br />
|
<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(
|
let clientLoginSession: SaveClientSession = JSON.parse(
|
||||||
loginData
|
loginData
|
||||||
);
|
);
|
||||||
|
Mastodon.fetchAccessToken(
|
||||||
getConfig().then((resp: any) => {
|
clientLoginSession.clientId,
|
||||||
if (resp == undefined) {
|
clientLoginSession.clientSecret,
|
||||||
return;
|
code,
|
||||||
}
|
localStorage.getItem("baseurl") as string,
|
||||||
|
this.state.emergencyMode
|
||||||
let conf: Config = resp;
|
? undefined
|
||||||
|
: clientLoginSession.authUrl.includes(
|
||||||
let redirectUrl: string | undefined =
|
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
||||||
this.state.emergencyMode ||
|
)
|
||||||
clientLoginSession.authUrl.includes(
|
? undefined
|
||||||
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
: window.location.protocol === "hyperspace:"
|
||||||
)
|
? "hyperspace://hyperspace/app/"
|
||||||
? undefined
|
: `https://${window.location.host}`
|
||||||
: getRedirectAddress(conf.location);
|
)
|
||||||
|
.then((tokenData: any) => {
|
||||||
Mastodon.fetchAccessToken(
|
localStorage.setItem(
|
||||||
clientLoginSession.clientId,
|
"access_token",
|
||||||
clientLoginSession.clientSecret,
|
tokenData.access_token
|
||||||
code,
|
);
|
||||||
localStorage.getItem("baseurl") as string,
|
window.location.href =
|
||||||
redirectUrl
|
window.location.protocol === "hyperspace:"
|
||||||
)
|
? "hyperspace://hyperspace/app/"
|
||||||
.then((tokenData: any) => {
|
: `https://${window.location.host}/#/`;
|
||||||
localStorage.setItem(
|
})
|
||||||
"access_token",
|
.catch((err: Error) => {
|
||||||
tokenData.access_token
|
this.props.enqueueSnackbar(
|
||||||
);
|
`Couldn't authorize ${
|
||||||
window.location.href =
|
this.state.brandName
|
||||||
window.location.protocol === "hyperspace:"
|
? this.state.brandName
|
||||||
? "hyperspace://hyperspace/app/"
|
: "Hyperspace"
|
||||||
: this.state.defaultRedirectAddress;
|
}: ${err.name}`,
|
||||||
})
|
{ variant: "error" }
|
||||||
.catch((err: Error) => {
|
);
|
||||||
this.props.enqueueSnackbar(
|
console.error(err.message);
|
||||||
`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;
|
followers_count: number;
|
||||||
following_count: number;
|
following_count: number;
|
||||||
statuses_count: number;
|
statuses_count: number;
|
||||||
last_status_at: string;
|
|
||||||
note: string;
|
note: string;
|
||||||
url: string;
|
url: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
export type Attachment = {
|
export type Attachment = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "unknown" | "image" | "gifv" | "audio" | "video";
|
type: "unknown" | "image" | "gifv" | "video";
|
||||||
url: string;
|
url: string;
|
||||||
remote_url: string | null;
|
remote_url: string | null;
|
||||||
preview_url: string;
|
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 = {
|
export type Tag = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
history?: [History];
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,5 @@
|
||||||
import { isDarwinApp } from "./desktop";
|
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.
|
* 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.
|
* 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 {
|
export function isAppbarExpanded(): boolean {
|
||||||
return isDarwinApp() || process.env.NODE_ENV === "development";
|
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.
|
* Gets the appropriate redirect address.
|
||||||
* @param url The address or configuration to use
|
* @param type The address or configuration to use
|
||||||
*/
|
*/
|
||||||
export function getRedirectAddress(
|
export function getRedirectAddress(
|
||||||
url: "desktop" | "dynamic" | string
|
type: "desktop" | "dynamic" | string
|
||||||
): string {
|
): string {
|
||||||
switch (url) {
|
switch (type) {
|
||||||
case "desktop":
|
case "desktop":
|
||||||
return "hyperspace://hyperspace/app/";
|
return "hyperspace://hyperspace/app/";
|
||||||
case "dynamic":
|
case "dynamic":
|
||||||
return `https://${window.location.host}`;
|
return `https://${window.location.host}`;
|
||||||
default:
|
default:
|
||||||
return url;
|
return type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ type SettingsTemplate = {
|
||||||
clearNotificationsOnRead: boolean;
|
clearNotificationsOnRead: boolean;
|
||||||
displayAllOnNotificationBadge: boolean;
|
displayAllOnNotificationBadge: boolean;
|
||||||
defaultVisibility: string;
|
defaultVisibility: string;
|
||||||
imposeCharacterLimit: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,9 +99,7 @@ export function createUserDefaults() {
|
||||||
enablePushNotifications: true,
|
enablePushNotifications: true,
|
||||||
clearNotificationsOnRead: false,
|
clearNotificationsOnRead: false,
|
||||||
displayAllOnNotificationBadge: false,
|
displayAllOnNotificationBadge: false,
|
||||||
defaultVisibility: "public",
|
defaultVisibility: "public"
|
||||||
imposeCharacterLimit: true,
|
|
||||||
isMasonryLayout: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let settings = [
|
let settings = [
|
||||||
|
@ -110,9 +107,7 @@ export function createUserDefaults() {
|
||||||
"systemDecidesDarkMode",
|
"systemDecidesDarkMode",
|
||||||
"clearNotificationsOnRead",
|
"clearNotificationsOnRead",
|
||||||
"displayAllOnNotificationBadge",
|
"displayAllOnNotificationBadge",
|
||||||
"defaultVisibility",
|
"defaultVisibility"
|
||||||
"imposeCharacterLimit",
|
|
||||||
"isMasonryLayout"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
migrateExistingSettings();
|
migrateExistingSettings();
|
||||||
|
|
Loading…
Reference in New Issue