Compare commits
372 Commits
2234ed3144
...
74ee427201
Author | SHA1 | Date |
---|---|---|
khr | 74ee427201 | |
Nolan Lawson | 243f0fd71d | |
Nolan Lawson | 3aee6fb050 | |
Nolan Lawson | 30048a7f12 | |
Nolan Lawson | 652ffffec4 | |
Nolan Lawson | 4bf3c2fd28 | |
Nolan Lawson | 8179c1b53f | |
Nolan Lawson | de4016029f | |
Nolan Lawson | 73182552d4 | |
Nolan Lawson | 795999e5ac | |
Nolan Lawson | fdcaa864af | |
Nolan Lawson | 37a95c04ab | |
Nolan Lawson | 9963473eaa | |
Nolan Lawson | 734857d3bf | |
Nolan Lawson | 37c85ec7e2 | |
Nolan Lawson | 56f5a45221 | |
Nolan Lawson | 2884955d67 | |
Nolan Lawson | 9cb15a3396 | |
Nolan Lawson | 135fb24873 | |
Nolan Lawson | e82066dcc2 | |
greenkeeper[bot] | b05855f7ca | |
Nolan Lawson | 73eb9fba2c | |
Nolan Lawson | 180055da70 | |
Nolan Lawson | 2a96e0eeda | |
Nolan Lawson | 157f5db690 | |
Isabelle Knott | 503378a400 | |
Nolan Lawson | 6e0f2ef6bb | |
Nolan Lawson | 10b14abcdb | |
Nolan Lawson | 7583d488a0 | |
Nolan Lawson | e17d3974d5 | |
Nolan Lawson | f5be28d99a | |
Nolan Lawson | 6d2b3ec072 | |
Nolan Lawson | 4c430bd1c9 | |
Ivan Kupalov | 437236bf3c | |
Nolan Lawson | 84e9bfc8e5 | |
Nolan Lawson | 9231e66612 | |
Nolan Lawson | 5e082e5f5f | |
Nolan Lawson | 9d594f0bac | |
Nolan Lawson | 2ef4743b3c | |
Nolan Lawson | d198250eab | |
Nolan Lawson | 58b0c56ad8 | |
Nolan Lawson | 7a8be06412 | |
Nolan Lawson | 14932e2479 | |
Nolan Lawson | 3dfab37f53 | |
Nolan Lawson | 2f743299ec | |
Nolan Lawson | 648d9a3cf6 | |
Nolan Lawson | 109022fab9 | |
Nolan Lawson | 0b1efab0c1 | |
Stephane Zermatten | 2656e11bb0 | |
Nolan Lawson | d976b621b8 | |
Nolan Lawson | eb5437e32a | |
Nolan Lawson | 95336e0657 | |
Nolan Lawson | c5394524df | |
Nolan Lawson | 74ab056f18 | |
Nolan Lawson | c1f6c1582d | |
Nolan Lawson | 45d70e8e6b | |
Nolan Lawson | b014778761 | |
Nolan Lawson | 031caec406 | |
Nolan Lawson | 6b3d53a795 | |
Nolan Lawson | 4a8f65b7fc | |
Nolan Lawson | ae918a226c | |
Nolan Lawson | a508f494f0 | |
Nolan Lawson | cb58a49c04 | |
Will Pearson | cb35a088f4 | |
Nolan Lawson | ef44c19e8a | |
Nolan Lawson | 8f84ae5a51 | |
Nolan Lawson | 4a6f7b74a4 | |
Nolan Lawson | 6d1bb64bbb | |
Nolan Lawson | 29a2892dd0 | |
Will Pearson | aa69e651ac | |
Stephane Zermatten | c2bd2f306a | |
Nolan Lawson | 981af04c6d | |
Nolan Lawson | 610f5be1e9 | |
Nolan Lawson | f2d1054af6 | |
Nolan Lawson | 39e77eeb4a | |
Nolan Lawson | adf04aa1ad | |
Nolan Lawson | 55879362a4 | |
Nolan Lawson | a39c57af8d | |
Nolan Lawson | af827d1338 | |
Nolan Lawson | dfd53c056d | |
Nolan Lawson | 14faed41e5 | |
Nolan Lawson | ec01534e00 | |
Nolan Lawson | a5a6c49269 | |
greenkeeper[bot] | b90bcbcfef | |
Nathan Minchow | d7aaec16d3 | |
greenkeeper[bot] | 5bb48e89e2 | |
Nolan Lawson | 8b26fe0eab | |
greenkeeper[bot] | 26d0b827bc | |
Nolan Lawson | e5ef4b9bb1 | |
Nolan Lawson | 795d6bce35 | |
Nolan Lawson | 52d1ab5703 | |
Nolan Lawson | 49b85623d5 | |
Nolan Lawson | e666eb5955 | |
Nolan Lawson | cf94e7d61e | |
Nolan Lawson | 4ab9687200 | |
Nolan Lawson | 59f9be448d | |
Nolan Lawson | a98fb4f7f6 | |
Nolan Lawson | 27da387a01 | |
Nolan Lawson | d5eac4e119 | |
Nolan Lawson | 32981ffeb2 | |
Nolan Lawson | d047a265a3 | |
Nolan Lawson | 7596d905ab | |
Nolan Lawson | cd44e33a7e | |
Nolan Lawson | e6ca246527 | |
Nolan Lawson | 2d32a91145 | |
Nolan Lawson | 7da2076791 | |
Nolan Lawson | 098a20db49 | |
Nolan Lawson | 943a1ed5e6 | |
Nolan Lawson | 93c2358a71 | |
Nolan Lawson | bb7fe6e30a | |
Nolan Lawson | fc30ef1c8c | |
Carlos Fernández | 669be2e32b | |
Nolan Lawson | 049bbba639 | |
Nolan Lawson | 4ca5b4611f | |
Nolan Lawson | bf9ba22c35 | |
Nolan Lawson | 14a618f374 | |
Nolan Lawson | beb4d6e119 | |
greenkeeper[bot] | 77b84d44f4 | |
Nolan Lawson | 146ac8d4aa | |
Nolan Lawson | 4220df9418 | |
Nolan Lawson | 3ae532aee5 | |
Nolan Lawson | f2f5508144 | |
Nolan Lawson | 3a335a9f4a | |
Nolan Lawson | 260f6acf0e | |
Nolan Lawson | dbd6c35a88 | |
Nolan Lawson | 89566cacaa | |
Nolan Lawson | b4164653db | |
Nolan Lawson | 7ddfe3830a | |
Nolan Lawson | fd1310c2c1 | |
Nolan Lawson | c90ad17686 | |
Nolan Lawson | d5c0268ef2 | |
Nolan Lawson | 25793e2fec | |
Nolan Lawson | 15e8840eb6 | |
Nolan Lawson | 319a158deb | |
Nolan Lawson | 0fa0658b59 | |
Nolan Lawson | a442b5ef43 | |
Nolan Lawson | 3462113c2f | |
Nolan Lawson | 5e089ba27d | |
Nolan Lawson | 381d1dd120 | |
Nolan Lawson | dc93685c18 | |
Nolan Lawson | 1940260631 | |
Nolan Lawson | 631603b0b7 | |
sgenoud | 30705da19d | |
Nolan Lawson | 76a8072e04 | |
Nolan Lawson | e3f7b3e65c | |
Nolan Lawson | 42978c3c84 | |
Nolan Lawson | 5d3ceb9eb5 | |
Nolan Lawson | 4bd181d3cc | |
Nolan Lawson | 4d3a2ded2a | |
Nolan Lawson | 852d86b9e3 | |
sgenoud | 94d0590070 | |
Nolan Lawson | b2f5f36207 | |
Nolan Lawson | 6a69b193d5 | |
Nolan Lawson | 1d34d45da7 | |
Nolan Lawson | 481a567807 | |
Nolan Lawson | 8eb30d02e9 | |
Nolan Lawson | b73dd548ae | |
Nolan Lawson | ee45c07314 | |
Nolan Lawson | dd349e2ae3 | |
Nolan Lawson | 7876f82871 | |
Nolan Lawson | 34cfaf27b3 | |
sgenoud | 530ad6b35c | |
Nolan Lawson | ab548a0a5d | |
Nolan Lawson | 7954a63588 | |
Nolan Lawson | 99c44f348a | |
Nolan Lawson | 4b028b1a62 | |
greenkeeper[bot] | 2280ff2832 | |
greenkeeper[bot] | 495d9b7438 | |
Nolan Lawson | 0e524f3e9a | |
Nolan Lawson | c0f857336a | |
Nolan Lawson | f7164dd4c0 | |
Nolan Lawson | ef32bfb278 | |
sgenoud | 03d883423c | |
Nolan Lawson | 0f0db010eb | |
Nolan Lawson | aae73f0cc6 | |
Nolan Lawson | d83d7322dc | |
Nolan Lawson | 618ea31a57 | |
Nolan Lawson | 09f3281e36 | |
Nolan Lawson | 60751b3339 | |
Nolan Lawson | 92edb3d835 | |
Nolan Lawson | da7a29d503 | |
Nolan Lawson | e894e031fb | |
Nolan Lawson | ee3dfd8e28 | |
Nolan Lawson | b22a1ec90c | |
Nolan Lawson | 26b84c435a | |
Nolan Lawson | 36d90d34e5 | |
Nolan Lawson | ef656301f6 | |
Nolan Lawson | 945c1e7a23 | |
Nolan Lawson | 537ad208a3 | |
Nolan Lawson | ce61b821c5 | |
Nolan Lawson | f3254bb22d | |
Nolan Lawson | a760687c6d | |
Nolan Lawson | 153e4f4fcd | |
Nolan Lawson | 0515133ece | |
Nolan Lawson | 12892d0032 | |
Nolan Lawson | ea4e21281f | |
Nolan Lawson | e44cafb5fb | |
Nolan Lawson | 58f9c09bb8 | |
Nolan Lawson | 7f1ec6036d | |
Nolan Lawson | 9c74a072bf | |
Nolan Lawson | 41d7e40662 | |
Nolan Lawson | cc81a7bec6 | |
Nolan Lawson | 2db06ea472 | |
Nolan Lawson | bfa37f5105 | |
Nolan Lawson | 2569b59b32 | |
Nolan Lawson | 48a1bd47b3 | |
Nolan Lawson | f0b3115be1 | |
Nolan Lawson | e3debcc5e1 | |
Nolan Lawson | 999d560703 | |
Nolan Lawson | bae367da7b | |
Nolan Lawson | 673e7b951c | |
Nolan Lawson | 83d92711e1 | |
Nolan Lawson | 689dae5d39 | |
Nolan Lawson | 5fdba9366a | |
Nolan Lawson | 3dae883761 | |
Nolan Lawson | 35a42c9fd3 | |
Nolan Lawson | d9e79daa6a | |
Nolan Lawson | 5f5cb0d36d | |
Nolan Lawson | 20ae390308 | |
Nolan Lawson | 4124da2439 | |
Nolan Lawson | 4e35c82f94 | |
Nolan Lawson | 639c6eaed7 | |
Nolan Lawson | b7f5d04b4c | |
Nolan Lawson | c1820f62f7 | |
Nolan Lawson | 255bd3b341 | |
Nolan Lawson | 92d2dbddfc | |
Nolan Lawson | c99cc7ed67 | |
Nolan Lawson | 62ac7330fc | |
Nolan Lawson | eee2eb288b | |
Nolan Lawson | c54aaf2fa4 | |
Nolan Lawson | 94baf9e396 | |
Nolan Lawson | 0964442815 | |
Nolan Lawson | 1fa37df59a | |
Nolan Lawson | 8ff174b42d | |
Nolan Lawson | 31c6f152c1 | |
greenkeeper[bot] | bf0812df6a | |
Nolan Lawson | d36dfc0ee8 | |
Nolan Lawson | 5b5c6937d0 | |
Nolan Lawson | dd824822cb | |
Nolan Lawson | ae6ae34b7d | |
Nolan Lawson | 00cafece8c | |
Nolan Lawson | 6bb4c80450 | |
Nolan Lawson | 8dd9f00135 | |
greenkeeper[bot] | cedf33b2cb | |
greenkeeper[bot] | db4ab87adc | |
Nolan Lawson | 924885e532 | |
Nolan Lawson | 819c1e6b8d | |
Nolan Lawson | 4fe0cf3f18 | |
Nolan Lawson | 4519a3fe2d | |
Nolan Lawson | 6f4c7e6f4e | |
Nolan Lawson | 754e4da638 | |
Nolan Lawson | b3a31aa21a | |
Nolan Lawson | f591b90629 | |
Nolan Lawson | 2cf35e58eb | |
Nolan Lawson | 16d21947a4 | |
Nolan Lawson | 398fb2fcd7 | |
Nolan Lawson | cf7ec984e1 | |
Nolan Lawson | 951c2b6527 | |
Nolan Lawson | 7fdf8ca721 | |
Nolan Lawson | 599f56ab02 | |
greenkeeper[bot] | 5936e978dd | |
Nolan Lawson | df91057334 | |
Nolan Lawson | dfacbdaaa5 | |
Nolan Lawson | 68c2dc47b9 | |
Nolan Lawson | c1c3c755ce | |
greenkeeper[bot] | d4a208bf20 | |
Nolan Lawson | ee942df1e3 | |
Nolan Lawson | e11738a711 | |
Nolan Lawson | bc3a74bbcb | |
Pheng Heong TAN | c305a9827a | |
Sorin Davidoi | c1917318ca | |
Nolan Lawson | bf0eb99fe4 | |
Sorin Davidoi | e45af16bf9 | |
Nolan Lawson | 50f2cadf50 | |
Nolan Lawson | 568352bcd5 | |
Nolan Lawson | 5c204b8001 | |
Nolan Lawson | 9b2b90b46e | |
Nolan Lawson | 2387a18ddb | |
Nolan Lawson | 85a4df0c04 | |
Nolan Lawson | 289c7eb7a7 | |
Nolan Lawson | fbd57d67a7 | |
Nolan Lawson | 1cc22fee7a | |
Nolan Lawson | ec1d50f998 | |
Nolan Lawson | d1a666aa4f | |
Nolan Lawson | 56190efce1 | |
Nolan Lawson | 0402d825bc | |
Nolan Lawson | 40336dbf41 | |
Nolan Lawson | e2ab92107e | |
Nolan Lawson | f92f6f7261 | |
Nolan Lawson | ce2c23463a | |
Nolan Lawson | c449d3a209 | |
Nolan Lawson | 7588ff2cb8 | |
Nolan Lawson | cb4c7b18c0 | |
Nolan Lawson | 1fecbb4c8e | |
Nolan Lawson | 2fc9053322 | |
Nolan Lawson | 07c48f23a5 | |
Nolan Lawson | a026f395ce | |
greenkeeper[bot] | 15d8137f6c | |
greenkeeper[bot] | 7a705c83ba | |
Nolan Lawson | 24dc3ad2ae | |
pianycist | 9dac979cb6 | |
Sorin Davidoi | 1852f4842f | |
Nolan Lawson | fc46835dec | |
Nolan Lawson | a30bd23155 | |
Nolan Lawson | c16718982f | |
Nolan Lawson | 431d1e1051 | |
Nolan Lawson | fd43dc6e5d | |
greenkeeper[bot] | 334a6e1e74 | |
Nolan Lawson | 65c026a32a | |
Nolan Lawson | 20dda272ba | |
Nolan Lawson | 9d27ba6c10 | |
Nolan Lawson | e92bed8e58 | |
Nolan Lawson | 9641b7ad1e | |
Nolan Lawson | 2f1e4077ea | |
Nolan Lawson | 96c2858d7a | |
Nolan Lawson | 6d8f4e22ef | |
greenkeeper[bot] | 8dbc1b0503 | |
Nolan Lawson | d599f2f308 | |
Nolan Lawson | 2449a27767 | |
Nolan Lawson | b55c042ff4 | |
Nolan Lawson | 1c20c6b762 | |
Nolan Lawson | 01b1d083a9 | |
Nolan Lawson | 6d50c65352 | |
Nolan Lawson | 120f50919e | |
Nolan Lawson | 46fa65f25a | |
Nolan Lawson | 8334598786 | |
Nolan Lawson | 464ed5ab71 | |
greenkeeper[bot] | 29dca5d8f4 | |
Nolan Lawson | 02bce843aa | |
Nolan Lawson | b59f544efb | |
Nolan Lawson | b60d636ee2 | |
Nolan Lawson | d49af06fbd | |
Nolan Lawson | 270df188cb | |
Nolan Lawson | 543536409b | |
Nolan Lawson | 95665f6d74 | |
Nolan Lawson | 47315c7f6d | |
Nolan Lawson | 17b80e5a79 | |
Nolan Lawson | 8959cdaeb1 | |
Nolan Lawson | 4a0cfb8d6e | |
Nolan Lawson | d6af3b69a7 | |
Nolan Lawson | dc091f1360 | |
Nolan Lawson | 73c99904cf | |
Nolan Lawson | ed1813fd53 | |
Nolan Lawson | 9bdb723edb | |
Nolan Lawson | 4edec81a0f | |
Nolan Lawson | 1423a6b14b | |
Nolan Lawson | 8d2e0636d6 | |
Nolan Lawson | 91a92b0003 | |
Spanky | 698d8f5730 | |
Nolan Lawson | 32ea30f4bb | |
Nolan Lawson | 1753e20f29 | |
Nolan Lawson | c4c128030e | |
Nolan Lawson | 5fdde8c63f | |
Nolan Lawson | 8949b36873 | |
Nolan Lawson | d10f924620 | |
Nolan Lawson | 39cd96da70 | |
Nolan Lawson | e9c704c7fc | |
Nolan Lawson | d30ffc6683 | |
Nolan Lawson | 2956e20d18 | |
Nolan Lawson | 65ac7e22f4 | |
Nolan Lawson | 6ad20e72a7 | |
Nolan Lawson | c4c70dfd89 | |
Nolan Lawson | aea952daf0 | |
Nolan Lawson | af1d4b63d3 | |
Nolan Lawson | 37e12e8d73 | |
Nolan Lawson | 350667e5df | |
Nolan Lawson | c660c7d3a3 | |
Nolan Lawson | f732bd44ab | |
Nolan Lawson | 1aeb57fb57 | |
Nolan Lawson | a6039f6247 | |
Nolan Lawson | 9a79a6ff7f | |
koyu | a1b89805e7 |
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -1,10 +1,12 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
templates/.*
|
||||
assets/*.css
|
||||
/__sapper__
|
||||
/mastodon
|
||||
mastodon.log
|
||||
assets/robots.txt
|
||||
/inline-script-checksum.json
|
||||
/mastodon.log
|
||||
/src/template.html
|
||||
/static/*.css
|
||||
/static/robots.txt
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-mart-all.json
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
|
|
73
.travis.yml
73
.travis.yml
|
@ -1,55 +1,53 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
- "10"
|
||||
dist: trusty # needed for chrome headless
|
||||
sudo: required # needed for chrome headless
|
||||
sudo: required # needed for various sudo operations
|
||||
addons:
|
||||
chrome: stable
|
||||
postgresql: "10"
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-10
|
||||
- postgresql-client-10
|
||||
- postgresql-contrib-10
|
||||
# the following are mastodon dependencies
|
||||
- imagemagick
|
||||
- libpq-dev
|
||||
- libxml2-dev
|
||||
- libxslt1-dev
|
||||
- file
|
||||
- g++
|
||||
- libprotobuf-dev
|
||||
- protobuf-compiler
|
||||
- pkg-config nodejs
|
||||
- gcc
|
||||
- autoconf
|
||||
- bison
|
||||
- build-essential
|
||||
- libssl-dev
|
||||
- libyaml-dev
|
||||
- libreadline6-dev
|
||||
- zlib1g-dev
|
||||
- libncurses5-dev
|
||||
- file
|
||||
- g++
|
||||
- gcc
|
||||
- imagemagick
|
||||
- libffi-dev
|
||||
- libgdbm3
|
||||
- libgdbm-dev
|
||||
- redis-tools
|
||||
- libidn11-dev
|
||||
- libgdbm3
|
||||
- libicu-dev
|
||||
services:
|
||||
- redis-server
|
||||
- libidn11-dev
|
||||
- libncurses5-dev
|
||||
- libpq-dev
|
||||
- libprotobuf-dev
|
||||
- libreadline6-dev
|
||||
- libssl-dev
|
||||
- libxml2-dev
|
||||
- libxslt1-dev
|
||||
- libyaml-dev
|
||||
- pkg-config nodejs
|
||||
- postgresql-10
|
||||
- postgresql-client-10
|
||||
- postgresql-contrib-10
|
||||
- protobuf-compiler
|
||||
- redis-server
|
||||
- redis-tools
|
||||
- zlib1g-dev
|
||||
before_install:
|
||||
- npm install -g npm@5
|
||||
- npm install -g greenkeeper-lockfile@1
|
||||
# install yarn
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- ./bin/setup-mastodon-in-travis.sh
|
||||
before_script:
|
||||
- npm run lint
|
||||
- yarn run lint
|
||||
- greenkeeper-lockfile-update
|
||||
after_script:
|
||||
- greenkeeper-lockfile-upload
|
||||
install:
|
||||
- npm ci || npm i
|
||||
script: travis_retry npm run $COMMAND
|
||||
script: travis_retry yarn run $COMMAND
|
||||
env:
|
||||
global:
|
||||
- PGPORT=5433
|
||||
|
@ -59,16 +57,17 @@ matrix:
|
|||
include:
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
||||
- env: COMMAND=deploy-dev-travis
|
||||
- env: COMMAND=test-unit
|
||||
- env: COMMAND=deploy-all-travis
|
||||
allow_failures:
|
||||
- env: COMMAND=deploy-dev-travis
|
||||
- env: COMMAND=deploy-all-travis
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^greenkeeper/.*$/
|
||||
cache:
|
||||
yarn: true
|
||||
bundler: true
|
||||
directories:
|
||||
- $HOME/.npm
|
||||
- $HOME/.rvm
|
||||
- $HOME/.bundle
|
||||
- $HOME/.yarn-cache
|
||||
- /home/travis/.rvm/
|
||||
- /home/travis/ffmpeg-static/
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Breaking changes
|
||||
|
||||
This document contains a list of _breaking changes_ for Pinafore. For a full changelog, see [the GitHub release page](https://github.com/nolanlawson/pinafore/releases).
|
||||
|
||||
## 1.0.0
|
||||
|
||||
**Breaking change:** This version [switches Pinafore from npm to yarn](https://github.com/nolanlawson/pinafore/pull/927). Those who self-host Pinafore will need to make the following changes:
|
||||
|
||||
1. [Install yarn](https://yarnpkg.com/en/docs/install) if you haven't already.
|
||||
2. Instead of `npm install`, run `yarn --pure-lockfile`.
|
||||
|
||||
This change fixes [a functional bug in Webpack](https://github.com/nolanlawson/pinafore/pull/926). If you use npm instead of yarn, Pinafore may not build correctly.
|
||||
|
||||
### Notes:
|
||||
|
||||
- Using `yarn start` instead of `npm start` should not make a difference.
|
||||
- Those using Docker shouldn't need to change anything.
|
182
CONTRIBUTING.md
182
CONTRIBUTING.md
|
@ -1,41 +1,40 @@
|
|||
# Contributing to Pinafore
|
||||
|
||||
## Caveats
|
||||
|
||||
Please note that this project is _very_ beta right now, and I'm
|
||||
not in a good position to accept large PRs for
|
||||
big new features.
|
||||
|
||||
I'm making my code open-source for the sake of
|
||||
transparency and because it's the right thing to do, but I'm hesitant
|
||||
to start nurturing a community because of
|
||||
[all that entails](https://nolanlawson.com/2017/03/05/what-it-feels-like-to-be-an-open-source-maintainer/).
|
||||
|
||||
So I may not be very responsive to PRs or issues. Thanks for understanding.
|
||||
|
||||
## Development
|
||||
## Dev server
|
||||
|
||||
To run a dev server with hot reloading:
|
||||
|
||||
npm run dev
|
||||
yarn run dev
|
||||
|
||||
Now it's running at `localhost:4002`.
|
||||
|
||||
**Linux users:** for file changes to work,
|
||||
you'll probably want to run `export CHOKIDAR_USEPOLLING=1`
|
||||
because of [this issue](https://github.com/paulmillr/chokidar/issues/237).
|
||||
|
||||
## Linting
|
||||
|
||||
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
||||
|
||||
Lint:
|
||||
|
||||
npm run lint
|
||||
yarn run lint
|
||||
|
||||
Automatically fix most linting issues:
|
||||
|
||||
npm run lint-fix
|
||||
yarn run lint-fix
|
||||
|
||||
## Testing
|
||||
## Integration tests
|
||||
|
||||
Testing requires running Mastodon itself, meaning the [Mastodon development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) is relevant here. In particular, you'll need a recent version of Ruby, Redis, and Postgres running.
|
||||
Integration tests use [TestCafé](https://devexpress.github.io/testcafe/) and a live local Mastodon instance
|
||||
running on `localhost:3000`.
|
||||
|
||||
### Running integration tests
|
||||
|
||||
The integration tests require running Mastodon itself,
|
||||
meaning the[Mastodon development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md)
|
||||
is relevant here. In particular, you'll need a recent
|
||||
version of Ruby, Redis, and Postgres running. For a full list of deps, see `bin/setup-mastodon-in-travis.sh`.
|
||||
|
||||
Run integration tests, using headless Chrome by default:
|
||||
|
||||
|
@ -43,44 +42,92 @@ Run integration tests, using headless Chrome by default:
|
|||
|
||||
Run tests for a particular browser:
|
||||
|
||||
BROWSER=chrome npm run test-browser
|
||||
BROWSER=chrome:headless npm run test-browser
|
||||
BROWSER=firefox npm run test-browser
|
||||
BROWSER=firefox:headless npm run test-browser
|
||||
BROWSER=safari npm run test-browser
|
||||
BROWSER=edge npm run test-browser
|
||||
BROWSER=chrome yarn run test-browser
|
||||
BROWSER=chrome:headless yarn run test-browser
|
||||
BROWSER=firefox yarn run test-browser
|
||||
BROWSER=firefox:headless yarn run test-browser
|
||||
BROWSER=safari yarn run test-browser
|
||||
BROWSER=edge yarn run test-browser
|
||||
|
||||
## Testing in development mode
|
||||
If the script isn't able to set up the Postgres database, try running:
|
||||
|
||||
sudo su - postgres
|
||||
|
||||
Then:
|
||||
|
||||
psql -d template1 -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;"
|
||||
|
||||
### Testing in development mode
|
||||
|
||||
In separate terminals:
|
||||
|
||||
1\. Run a Mastodon dev server:
|
||||
|
||||
npm run run-mastodon
|
||||
yarn run run-mastodon
|
||||
|
||||
2\. Run a Pinafore dev server:
|
||||
|
||||
npm run dev
|
||||
yarn run dev
|
||||
|
||||
3\. Run a debuggable TestCafé instance:
|
||||
|
||||
npx testcafe --hostname localhost --skip-js-errors --debug-mode firefox tests/spec
|
||||
npx testcafe --hostname localhost --skip-js-errors --debug-mode chrome tests/spec
|
||||
|
||||
If you want to export the current data in the Mastodon instance as canned data,
|
||||
so that it can be loaded later, run:
|
||||
### Test conventions
|
||||
|
||||
npm run backup-mastodon-data
|
||||
The tests have a naming convention:
|
||||
|
||||
## Writing tests
|
||||
|
||||
Tests use [TestCafé](https://devexpress.github.io/testcafe/). The tests have a naming convention:
|
||||
|
||||
* `0xx-test-name.js`: tests that don't modify the Mastodon database (post, delete, follow, etc.)
|
||||
* `1xx-test-name.js`: tests that do modify the Mastodon database
|
||||
* `0xx-test-name.js`: tests that don't modify the Mastodon database (read-only)
|
||||
* `1xx-test-name.js`: tests that do modify the Mastodon database (read-write)
|
||||
|
||||
In principle the `0-` tests don't have to worry about
|
||||
clobbering each other, whereas the `1-` ones do.
|
||||
|
||||
### Mastodon used for testing
|
||||
|
||||
There are two parts to the Mastodon data used for testing:
|
||||
|
||||
1. A Postgres dump and a tgz containing the media files, located in `fixtures`
|
||||
2. A script that populates the Mastodon backend with test data (`restore-mastodon-data.js`).
|
||||
|
||||
The reason we don't use a Postgres dump for everything
|
||||
is that Mastodon will ignore changes made after a certain period of time, and we
|
||||
don't want our tests to randomly start breaking one day. Running the script ensures that statuses,
|
||||
favorites, boosts, etc. are all "fresh".
|
||||
|
||||
### Updating the test data
|
||||
|
||||
You probably don't want to do this, as the `0xx` tests are pretty rigidly defined against the test data.
|
||||
Write a `1xx` test instead and insert what you need on-the-fly.
|
||||
|
||||
If you really need to, though, you can either:
|
||||
|
||||
1. Add new test data to `mastodon-data.js`
|
||||
|
||||
or
|
||||
|
||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js`
|
||||
2. Make your changes manually to the live Mastodon
|
||||
3. Run the steps in the next section to back it up to `fixtures/`
|
||||
|
||||
### Updating the Mastodon version
|
||||
|
||||
1. Run `rm -fr mastodon` to clear out all Mastodon data
|
||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js` to avoid actually populating the database with statuses/favorites/etc.
|
||||
2. Update the `GIT_TAG` in `run-mastodon.js` to whatever you want
|
||||
3. Run `yarn run run-mastodon`
|
||||
4. Run `yarn run backup-mastodon-data` to overwrite the data in `fixtures/`
|
||||
5. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
|
||||
6. Commit all changed files
|
||||
7. Run `rm -fr mastodon/` and `yarn run run-mastodon` to confirm everything's working
|
||||
|
||||
Check `mastodon.log` if you have any issues.
|
||||
|
||||
## Unit tests
|
||||
|
||||
There are also some unit tests that run in Node using Mocha. You can find them in `tests/unit` and
|
||||
run them using `yarn run test-unit`.
|
||||
|
||||
## Debugging Webpack
|
||||
|
||||
The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicly via e.g.:
|
||||
|
@ -88,17 +135,54 @@ The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicl
|
|||
- [dev.pinafore.social/report.html](https://dev.pinafore.social/report.html)
|
||||
- [dev.pinafore.social/stats.json](https://dev.pinafore.social/stats.json)
|
||||
|
||||
This is also available locally after `npm run build` at `.sapper/client/report.html`.
|
||||
This is also available locally after `yarn run build` at `.sapper/client/report.html`.
|
||||
|
||||
## Updating Mastodon used for testing
|
||||
## Codebase overview
|
||||
|
||||
1. Run `rm -fr mastodon` to clear out all Mastodon data
|
||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js` to avoid actually populating the database with statuses/favorites/etc.
|
||||
2. Update the `GIT_TAG` in `run-mastodon.js` to whatever you want
|
||||
3. Run `npm run run-mastodon`
|
||||
4. Run `npm run backup-mastodon-data` to overwrite the data in `fixtures/`
|
||||
5. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
|
||||
6. Commit all changed files
|
||||
7. Run `rm -fr mastodon/` and `npm run run-mastodon` to confirm everything's working
|
||||
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
||||
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
||||
|
||||
Check `mastodon.log` if you have any issues.
|
||||
### Prebuild process
|
||||
|
||||
The `template.html` is itself templated. The "template template" has some inline scripts, CSS, and SVGs
|
||||
injected into it during the build process. SCSS is used for global CSS and themed CSS, but inside of the
|
||||
components themselves, it's just vanilla CSS because I couldn't figure out how to get Svelte to run a SCSS
|
||||
preprocessor.
|
||||
|
||||
### Lots of small files
|
||||
|
||||
Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and
|
||||
code-splitting, as well as avoiding circular dependencies.
|
||||
|
||||
### Inferno is loaded dynamically
|
||||
|
||||
This is a Svelte project, but `emoji-mart` is used for the emoji picker, and it's written in React. So we
|
||||
lazy-load the React-compatible Inferno library when we load `emoji-mart`.
|
||||
|
||||
### Some third-party code is bundled
|
||||
|
||||
For various reasons, `a11y-dialog`, `autosize`, and `timeago` are forked and bundled into the source code.
|
||||
This was either because something needed to be tweaked or fixed, or I was trimming unused code and didn't
|
||||
see much value in contributing it back, because it was too Pinafore-specific.
|
||||
|
||||
### Every Sapper page is "duplicated"
|
||||
|
||||
To get a nice animation on the nav bar when you switch columns, every page is lazy-loaded as `LazyPage.html`.
|
||||
This "lazy page" is merely delayed a few frames to let the animation run. Therefore there is a duplication
|
||||
between `src/routes` and `src/routes/_pages`. The "lazy page" is in the former, and the actual page is in the
|
||||
latter. One imports the other.
|
||||
|
||||
### There are multiple stores
|
||||
|
||||
Originally I conceived of separating out the virtual list into a separate npm package, so I gave it its
|
||||
own Svelte store (`virtualListStore.js`). This never happened, but it still has its own store. This is useful
|
||||
anyway, because each store has its state maintained in an LRU cache that allows us to keep the scroll position
|
||||
in the virtual list e.g. when the user hits the back button.
|
||||
|
||||
Also, the main `store.js` store is explicitly
|
||||
loaded by every component that uses it. So there's no `store` inheritance; every component just declares
|
||||
whatever store it uses. The main `store.js` is the primary one.
|
||||
|
||||
### There is a global event bus
|
||||
|
||||
It's in `eventBus.js`. This is useful for some stuff that is hard to do with standard Svelte or DOM events.
|
||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -7,17 +7,17 @@ ADD . /app
|
|||
|
||||
# Install updates and NodeJS+Dependencies
|
||||
RUN apk update && apk upgrade
|
||||
RUN apk add nodejs git python build-base clang
|
||||
RUN apk add nodejs npm git python build-base clang
|
||||
|
||||
# Upgrading NPM
|
||||
RUN npm i npm@latest -g
|
||||
# Install yarn
|
||||
RUN npm i yarn -g
|
||||
|
||||
# Install Pinafore
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN yarn --pure-lockfile
|
||||
RUN yarn build
|
||||
|
||||
# Expose port 4002
|
||||
EXPOSE 4002
|
||||
|
||||
# Setting run-command
|
||||
CMD PORT=4002 npm start
|
||||
CMD PORT=4002 yarn start
|
||||
|
|
61
README.md
61
README.md
|
@ -2,11 +2,11 @@
|
|||
|
||||
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
|
||||
|
||||
Pinafore is available at [pinafore.social](https://pinafore.social). Bleeding-edge releases are at [dev.pinafore.social](https://dev.pinafore.social).
|
||||
Pinafore is available at [pinafore.social](https://pinafore.social). Beta releases are at [dev.pinafore.social](https://dev.pinafore.social).
|
||||
|
||||
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) to troubleshoot instance compatibility issues.
|
||||
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) if Pinafore cannot connect to your instance.
|
||||
|
||||
For updates and support, follow us at [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
|
||||
For updates and support, follow [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
|
||||
|
||||
## Browser support
|
||||
|
||||
|
@ -24,51 +24,68 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
|||
### Goals
|
||||
|
||||
- Support the most common use cases
|
||||
- Fast even on low-end phones
|
||||
- Works offline in read-only mode
|
||||
- Small page weight
|
||||
- Fast even on low-end devices
|
||||
- Accessibility
|
||||
- Offline support in read-only mode
|
||||
- Progressive Web App features
|
||||
- Multi-instance support
|
||||
- Support latest versions of Chrome, Edge, Firefox, and Safari
|
||||
- a11y (keyboard navigation, screen readers)
|
||||
|
||||
### Possible future goals
|
||||
### Secondary / possible future goals
|
||||
|
||||
- Works as an alternative frontend self-hosted by instances
|
||||
- Android/iOS apps (using Cordova or similar)
|
||||
- Support Pleroma/non-Mastodon backends
|
||||
- i18n
|
||||
- Support for Pleroma or other non-Mastodon backends
|
||||
- Serve as an alternative frontend tied to a particular instance
|
||||
- Support for non-English languages (i18n)
|
||||
- Offline search
|
||||
- Full emoji keyboard
|
||||
- Keyboard shortcuts
|
||||
|
||||
### Non-goals
|
||||
|
||||
- Supporting old browsers, proxy browsers, or text-based browsers
|
||||
- React Native / NativeScript / hybrid-native version
|
||||
- Android/iOS apps (using Cordova or similar)
|
||||
- Full functionality with JavaScript disabled
|
||||
- Emoji support beyond the built-in system emoji
|
||||
- Multi-column support
|
||||
- Admin/moderation panel
|
||||
- Works offline in read-write mode (would require sophisticated sync logic)
|
||||
- Offline support in read-write mode (would require sophisticated sync logic)
|
||||
|
||||
## Building
|
||||
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
|
||||
|
||||
To build Pinafore for production:
|
||||
|
||||
npm install
|
||||
npm run build
|
||||
PORT=4002 npm start
|
||||
yarn --pure-lockfile
|
||||
yarn build
|
||||
PORT=4002 yarn start
|
||||
|
||||
### Docker
|
||||
|
||||
To build a docker image for production:
|
||||
To build a Docker image for production:
|
||||
|
||||
docker build .
|
||||
docker run -d -p 4002:4002 [your-image]
|
||||
|
||||
Now Pinafore is running at `localhost:4002`.
|
||||
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+.
|
||||
### Updating
|
||||
|
||||
To keep your version of Pinafore up to date, you can use `git` to check out the latest tag:
|
||||
|
||||
git checkout $(git tag -l | sort -Vr | head -n 1)
|
||||
|
||||
### Exporting
|
||||
|
||||
You can export Pinafore as a static site. Run:
|
||||
|
||||
yarn run export
|
||||
|
||||
Static files will be written to `__sapper__/export`.
|
||||
|
||||
Note that this is not the recommended method, because
|
||||
[CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers are not
|
||||
currently supported for the exported version.
|
||||
|
||||
## Developing and testing
|
||||
|
||||
|
@ -78,3 +95,9 @@ how to run Pinafore in dev mode and run tests.
|
|||
## Changelog
|
||||
|
||||
For a changelog, see the [GitHub releases](http://github.com/nolanlawson/pinafore/releases/).
|
||||
|
||||
For a list of breaking changes, see [BREAKING_CHANGES.md](https://github.com/nolanlawson/pinafore/blob/master/BREAKING_CHANGES.md).
|
||||
|
||||
## What's with the name?
|
||||
|
||||
Pinafore is named after the [Gilbert and Sullivan play](https://en.wikipedia.org/wiki/Hms_pinafore). The [soundtrack](https://www.allmusic.com/album/gilbert-sullivan-hms-pinafore-1949-mw0001830483) is very good.
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
PGPASSWORD=pinafore pg_dump -U pinafore -w pinafore_development -h 127.0.0.1 > tests/fixtures/dump.sql
|
||||
cd mastodon/public/system
|
||||
tar -czf ../../../tests/fixtures/system.tgz .
|
|
@ -1,32 +1,47 @@
|
|||
#!/usr/bin/env node
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import path from 'path'
|
||||
import { rollup } from 'rollup'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import replace from 'rollup-plugin-replace'
|
||||
import fromPairs from 'lodash-es/fromPairs'
|
||||
import { themes } from '../src/routes/_static/themes'
|
||||
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const readFile = pify(fs.readFile.bind(fs))
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
const path = require('path')
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
async function main () {
|
||||
let headScriptFilepath = path.join(__dirname, '../inline-script.js')
|
||||
let headScript = await readFile(headScriptFilepath, 'utf8')
|
||||
headScript = `(function () {'use strict'; ${headScript}})()`
|
||||
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
|
||||
|
||||
let checksum = crypto.createHash('sha256').update(headScript).digest('base64')
|
||||
export async function buildInlineScript () {
|
||||
let inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
|
||||
|
||||
let checksumFilepath = path.join(__dirname, '../inline-script-checksum.json')
|
||||
await writeFile(checksumFilepath, JSON.stringify({checksum}), 'utf8')
|
||||
let bundle = await rollup({
|
||||
input: inlineScriptPath,
|
||||
plugins: [
|
||||
replace({
|
||||
'process.browser': true,
|
||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
||||
}),
|
||||
terser({
|
||||
mangle: true,
|
||||
compress: true
|
||||
})
|
||||
]
|
||||
})
|
||||
let { output } = await bundle.generate({
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
})
|
||||
|
||||
let html2xxFilepath = path.join(__dirname, '../templates/2xx.html')
|
||||
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
||||
html2xxFile = html2xxFile.replace(
|
||||
/<!-- insert inline script here -->[\s\S]+<!-- end insert inline script here -->/,
|
||||
'<!-- insert inline script here --><script>' + headScript + '</script><!-- end insert inline script here -->'
|
||||
)
|
||||
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
|
||||
let { code, map } = output[0]
|
||||
|
||||
let fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
|
||||
let checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
|
||||
|
||||
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
|
||||
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
|
||||
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
|
||||
map.toString(), 'utf8')
|
||||
|
||||
return '<script>' + fullCode + '</script>'
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -1,73 +1,46 @@
|
|||
#!/usr/bin/env node
|
||||
import sass from 'node-sass'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import cssDedoupe from 'css-dedoupe'
|
||||
import { TextDecoder } from 'text-encoding'
|
||||
|
||||
const sass = require('node-sass')
|
||||
const chokidar = require('chokidar')
|
||||
const argv = require('yargs').argv
|
||||
const path = require('path')
|
||||
const debounce = require('lodash/debounce')
|
||||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
const readdir = pify(fs.readdir.bind(fs))
|
||||
const readFile = pify(fs.readFile.bind(fs))
|
||||
const render = pify(sass.render.bind(sass))
|
||||
const now = require('performance-now')
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const readdir = promisify(fs.readdir)
|
||||
const render = promisify(sass.render.bind(sass))
|
||||
|
||||
const globalScss = path.join(__dirname, '../scss/global.scss')
|
||||
const defaultThemeScss = path.join(__dirname, '../scss/themes/_default.scss')
|
||||
const offlineThemeScss = path.join(__dirname, '../scss/themes/_offline.scss')
|
||||
const html2xxFile = path.join(__dirname, '../templates/2xx.html')
|
||||
const scssDir = path.join(__dirname, '../scss')
|
||||
const themesScssDir = path.join(__dirname, '../scss/themes')
|
||||
const assetsDir = path.join(__dirname, '../assets')
|
||||
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
||||
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
|
||||
const offlineThemeScss = path.join(__dirname, '../src/scss/themes/_offline.scss')
|
||||
const customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
|
||||
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
||||
const assetsDir = path.join(__dirname, '../static')
|
||||
|
||||
function doWatch () {
|
||||
let start = now()
|
||||
chokidar.watch(scssDir).on('change', debounce(() => {
|
||||
console.log('Recompiling SCSS...')
|
||||
Promise.all([
|
||||
compileGlobalSass(),
|
||||
compileThemesSass()
|
||||
]).then(() => {
|
||||
console.log('Recompiled SCSS in ' + (now() - start) + 'ms')
|
||||
})
|
||||
}, 500))
|
||||
chokidar.watch()
|
||||
async function renderCss (file) {
|
||||
return (await render({ file, outputStyle: 'compressed' })).css
|
||||
}
|
||||
|
||||
async function compileGlobalSass () {
|
||||
let results = await Promise.all([
|
||||
render({file: defaultThemeScss, outputStyle: 'compressed'}),
|
||||
render({file: globalScss, outputStyle: 'compressed'}),
|
||||
render({file: offlineThemeScss, outputStyle: 'compressed'})
|
||||
])
|
||||
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
||||
let offlineStyle = (await renderCss(offlineThemeScss))
|
||||
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
||||
|
||||
let css = results.map(_ => _.css).join('')
|
||||
|
||||
let html = await readFile(html2xxFile, 'utf8')
|
||||
html = html.replace(/<style>[\s\S]+?<\/style>/,
|
||||
`<style>\n/* auto-generated w/ build-sass.js */\n${css}\n</style>`)
|
||||
|
||||
await writeFile(html2xxFile, html, 'utf8')
|
||||
return `<style>\n${mainStyle}</style>\n` +
|
||||
`<style media="only x" id="theOfflineStyle">\n${offlineStyle}</style>\n` +
|
||||
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
|
||||
}
|
||||
|
||||
async function compileThemesSass () {
|
||||
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
||||
await Promise.all(files.map(async file => {
|
||||
let res = await render({file: path.join(themesScssDir, file), outputStyle: 'compressed'})
|
||||
let css = await renderCss(path.join(themesScssDir, file))
|
||||
css = cssDedoupe(new TextDecoder('utf-8').decode(css)) // remove duplicate custom properties
|
||||
let outputFilename = 'theme-' + path.basename(file).replace(/\.scss$/, '.css')
|
||||
await writeFile(path.join(assetsDir, outputFilename), res.css, 'utf8')
|
||||
await writeFile(path.join(assetsDir, outputFilename), css, 'utf8')
|
||||
}))
|
||||
}
|
||||
|
||||
async function main () {
|
||||
await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||
if (argv.watch) {
|
||||
doWatch()
|
||||
}
|
||||
export async function buildSass () {
|
||||
let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||
return result
|
||||
}
|
||||
|
||||
Promise.resolve().then(main).catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
#!/usr/bin/env node
|
||||
import svgs from './svgs'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import SVGO from 'svgo'
|
||||
import $ from 'cheerio'
|
||||
|
||||
const svgs = require('./svgs')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const SVGO = require('svgo')
|
||||
const svgo = new SVGO()
|
||||
const $ = require('cheerio')
|
||||
const readFile = promisify(fs.readFile)
|
||||
|
||||
const readFile = pify(fs.readFile.bind(fs))
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
|
||||
async function main () {
|
||||
export async function buildSvg () {
|
||||
let result = (await Promise.all(svgs.map(async svg => {
|
||||
let filepath = path.join(__dirname, '../', svg.src)
|
||||
let content = await readFile(filepath, 'utf8')
|
||||
|
@ -21,23 +18,9 @@ async function main () {
|
|||
let $symbol = $('<symbol></symbol>')
|
||||
.attr('id', svg.id)
|
||||
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
||||
.append($('<title></title>').text(svg.title))
|
||||
.append($path)
|
||||
return $.xml($symbol)
|
||||
}))).join('\n')
|
||||
|
||||
result = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">\n${result}\n</svg>`
|
||||
|
||||
let html2xxFilepath = path.join(__dirname, '../templates/2xx.html')
|
||||
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
||||
html2xxFile = html2xxFile.replace(
|
||||
/<!-- insert svg here -->[\s\S]+<!-- end insert svg here -->/,
|
||||
'<!-- insert svg here -->' + result + '<!-- end insert svg here -->'
|
||||
)
|
||||
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">\n${result}\n</svg>`
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import chokidar from 'chokidar'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { promisify } from 'util'
|
||||
import { buildSass } from './build-sass'
|
||||
import { buildInlineScript } from './build-inline-script'
|
||||
import { buildSvg } from './build-svg'
|
||||
import now from 'performance-now'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
const DEBOUNCE = 500
|
||||
|
||||
const builders = [
|
||||
{
|
||||
watch: 'src/scss',
|
||||
comment: '<!-- inline CSS -->',
|
||||
rebuild: buildSass
|
||||
},
|
||||
{
|
||||
watch: 'src/inline-script/inline-script.js',
|
||||
comment: '<!-- inline JS -->',
|
||||
rebuild: buildInlineScript
|
||||
},
|
||||
{
|
||||
watch: 'bin/svgs.js',
|
||||
comment: '<!-- inline SVG -->',
|
||||
rebuild: buildSvg
|
||||
}
|
||||
]
|
||||
|
||||
// array of strings and builder functions, we build this on-the-fly
|
||||
const partials = buildPartials()
|
||||
|
||||
function buildPartials () {
|
||||
let rawTemplate = fs.readFileSync(path.resolve(__dirname, '../src/build/template.html'), 'utf8')
|
||||
|
||||
let partials = [rawTemplate]
|
||||
|
||||
builders.forEach(builder => {
|
||||
for (let i = 0; i < partials.length; i++) {
|
||||
let partial = partials[i]
|
||||
if (typeof partial !== 'string') {
|
||||
continue
|
||||
}
|
||||
let idx = partial.indexOf(builder.comment)
|
||||
if (idx !== -1) {
|
||||
partials.splice(
|
||||
i,
|
||||
1,
|
||||
partial.substring(0, idx),
|
||||
builder,
|
||||
partial.substring(idx + builder.comment.length)
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return partials
|
||||
}
|
||||
|
||||
function doWatch () {
|
||||
// rebuild each of the partials on-the-fly if something changes
|
||||
partials.forEach(partial => {
|
||||
if (typeof partial === 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
chokidar.watch(partial.watch).on('change', debounce(path => {
|
||||
console.log(`Detected change in ${path}...`)
|
||||
delete partial.result
|
||||
buildAll()
|
||||
}), DEBOUNCE)
|
||||
})
|
||||
}
|
||||
|
||||
async function buildAll () {
|
||||
let start = now()
|
||||
let html = (await Promise.all(partials.map(async partial => {
|
||||
if (typeof partial === 'string') {
|
||||
return partial
|
||||
}
|
||||
if (!partial.result) {
|
||||
partial.result = partial.comment + '\n' + (await partial.rebuild())
|
||||
}
|
||||
return partial.result
|
||||
}))).join('')
|
||||
|
||||
await writeFile(path.resolve(__dirname, '../src/template.html'), html, 'utf8')
|
||||
let end = now()
|
||||
console.log(`Built template.html in ${(end - start).toFixed(2)}ms`)
|
||||
}
|
||||
|
||||
async function main () {
|
||||
if (process.argv.includes('--watch')) {
|
||||
doWatch()
|
||||
} else {
|
||||
await buildAll()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import CleanCSS from 'clean-css'
|
||||
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const readFile = promisify(fs.readFile)
|
||||
const copyFile = promisify(fs.copyFile)
|
||||
|
||||
async function compileThirdPartyCss () {
|
||||
let css = await readFile(path.resolve(__dirname, '../node_modules/emoji-mart/css/emoji-mart.css'), 'utf8')
|
||||
css = `/* compiled from emoji-mart.css */` + new CleanCSS().minify(css).styles
|
||||
await writeFile(path.resolve(__dirname, '../static/emoji-mart.css'), css, 'utf8')
|
||||
}
|
||||
|
||||
async function compileThirdPartyJson () {
|
||||
await copyFile(
|
||||
path.resolve(__dirname, '../node_modules/emoji-mart/data/all.json'),
|
||||
path.resolve(__dirname, '../static/emoji-mart-all.json')
|
||||
)
|
||||
}
|
||||
|
||||
async function main () {
|
||||
await Promise.all([
|
||||
compileThirdPartyCss(),
|
||||
compileThirdPartyJson()
|
||||
])
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [ "$TRAVIS_BRANCH" = master -a "$TRAVIS_PULL_REQUEST" = false ]; then
|
||||
yarn run deploy-dev
|
||||
fi
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
# set up robots.txt
|
||||
if [[ "$DEPLOY_TYPE" == "dev" ]]; then
|
||||
printf 'User-agent: *\nDisallow: /' > static/robots.txt
|
||||
else
|
||||
rm -f static/robots.txt
|
||||
fi
|
||||
|
||||
# if in travis, use the $NOW_TOKEN
|
||||
NOW_COMMAND="now --team nolanlawson"
|
||||
if [[ ! -z "$NOW_TOKEN" ]]; then
|
||||
NOW_COMMAND="$NOW_COMMAND --token $NOW_TOKEN"
|
||||
fi
|
||||
|
||||
# launch
|
||||
URL=$($NOW_COMMAND -e SAPPER_TIMESTAMP=$(date +%s%3N))
|
||||
|
||||
# fixes issues with now being unavailable immediately
|
||||
sleep 60
|
||||
|
||||
# choose the right alias
|
||||
NOW_ALIAS="dev.pinafore.social"
|
||||
|
||||
if [[ "$DEPLOY_TYPE" == "prod" ]]; then
|
||||
NOW_ALIAS="pinafore.social"
|
||||
fi
|
||||
|
||||
# alias
|
||||
$NOW_COMMAND alias "$URL" "$NOW_ALIAS"
|
||||
|
||||
# cleanup
|
||||
$NOW_COMMAND rm pinafore --safe --yes
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Change all the Svelte CSS to just use globals everywhere,
|
||||
// to reduce CSS size and complexity.
|
||||
|
||||
const argv = require('yargs').argv
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
const readFile = pify(fs.readFile.bind(fs))
|
||||
const glob = pify(require('glob'))
|
||||
const rimraf = pify(require('rimraf'))
|
||||
|
||||
const selectorRegex = /\n[ \t\n]*([0-9\w\- \t\n.:#,]+?)[ \t\n]*{/g
|
||||
const styleRegex = /<style>[\s\S]+?<\/style>/
|
||||
|
||||
async function main () {
|
||||
if (argv.reverse) { // reverse the operation we just did
|
||||
let tmpComponents = await glob('./routes/**/.tmp-*.html')
|
||||
for (let filename of tmpComponents) {
|
||||
let text = await readFile(filename, 'utf8')
|
||||
await rimraf(filename)
|
||||
let originalFilename = path.join(path.dirname(filename), path.basename(filename).substring(5))
|
||||
await writeFile(originalFilename, text, 'utf8')
|
||||
}
|
||||
} else { // read all files, copy to tmp files, rewrite files to include global CSS everywhere
|
||||
let components = await glob('./routes/**/*.html')
|
||||
for (let filename of components) {
|
||||
let text = await readFile(filename, 'utf8')
|
||||
let newText = text.replace(styleRegex, style => {
|
||||
return style.replace(selectorRegex, selectorMatch => {
|
||||
return selectorMatch.replace(/\S[^{]+/, selector => `:global(${selector})`)
|
||||
})
|
||||
})
|
||||
let newFilename = path.join(path.dirname(filename), '.tmp-' + path.basename(filename))
|
||||
|
||||
await writeFile(newFilename, text, 'utf8')
|
||||
await writeFile(filename, newText, 'utf8')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Promise.resolve().then(main).catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
|
@ -0,0 +1,28 @@
|
|||
console.log(`
|
||||
|
||||
,((*
|
||||
,((* (,
|
||||
,((* (((*
|
||||
,((* (((((.
|
||||
* ,((* ((((((*
|
||||
.(/ ,((* (((((((/
|
||||
.((/ ,((* ((((((((/
|
||||
,(((/ ,((* (((((((((*
|
||||
.(((((/ ,((* ((((((((((
|
||||
,((*
|
||||
//////////((((/////////////
|
||||
/((((((((((((((((((((((((((
|
||||
/((((((((((((((((((((((((,
|
||||
*(((((((((((((((((((((/.
|
||||
./((((((((((((((((.
|
||||
|
||||
|
||||
P I N A F O R E
|
||||
|
||||
|
||||
Export successful! Static files are in:
|
||||
|
||||
__sapper__/export/
|
||||
|
||||
Enjoy Pinafore!
|
||||
`)
|
|
@ -1,64 +1,38 @@
|
|||
import { actions } from './mastodon-data'
|
||||
import { users } from '../tests/users'
|
||||
import { postStatus } from '../routes/_api/statuses'
|
||||
import { followAccount } from '../routes/_api/follow'
|
||||
import { favoriteStatus } from '../routes/_api/favorite'
|
||||
import { reblogStatus } from '../routes/_api/reblog'
|
||||
import { postStatus } from '../src/routes/_api/statuses'
|
||||
import { followAccount } from '../src/routes/_api/follow'
|
||||
import { favoriteStatus } from '../src/routes/_api/favorite'
|
||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||
import fetch from 'node-fetch'
|
||||
import FileApi from 'file-api'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import FormData from 'form-data'
|
||||
import { auth } from '../routes/_api/utils'
|
||||
import { pinStatus } from '../routes/_api/pin'
|
||||
import { pinStatus } from '../src/routes/_api/pin'
|
||||
import { submitMedia } from '../tests/submitMedia'
|
||||
|
||||
global.File = FileApi.File
|
||||
global.FormData = FileApi.FormData
|
||||
global.fetch = fetch
|
||||
|
||||
async function submitMedia (accessToken, filename, alt) {
|
||||
let form = new FormData()
|
||||
form.append('file', fs.createReadStream(path.join(__dirname, '../tests/images/' + filename)))
|
||||
form.append('description', alt)
|
||||
return new Promise((resolve, reject) => {
|
||||
form.submit({
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
path: '/api/v1/media',
|
||||
headers: auth(accessToken)
|
||||
}, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
let data = ''
|
||||
|
||||
res.on('data', chunk => {
|
||||
data += chunk
|
||||
})
|
||||
|
||||
res.on('end', () => resolve(JSON.parse(data)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function restoreMastodonData () {
|
||||
console.log('Restoring mastodon data...')
|
||||
let internalIdsToIds = {}
|
||||
for (let action of actions) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // delay so that notifications have proper order
|
||||
if (!action.post) {
|
||||
// If the action is a boost, favorite, etc., then it needs to
|
||||
// be delayed, otherwise it may appear in an unpredictable order and break the tests.
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
console.log(JSON.stringify(action))
|
||||
let accessToken = users[action.user].accessToken
|
||||
|
||||
if (action.post) {
|
||||
let { text, media, sensitive, spoiler, privacy, inReplyTo, internalId } = action.post
|
||||
if (typeof inReplyTo !== 'undefined') {
|
||||
inReplyTo = internalIdsToIds[inReplyTo]
|
||||
}
|
||||
let mediaIds = media && await Promise.all(media.map(async mediaItem => {
|
||||
let mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
||||
return mediaResponse.id
|
||||
}))
|
||||
let status = await postStatus('localhost:3000', accessToken, text, inReplyTo, mediaIds,
|
||||
let inReplyToId = inReplyTo && internalIdsToIds[inReplyTo]
|
||||
let status = await postStatus('localhost:3000', accessToken, text, inReplyToId, mediaIds,
|
||||
sensitive, spoiler, privacy || 'public')
|
||||
if (typeof internalId !== 'undefined') {
|
||||
internalIdsToIds[internalId] = status.id
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { restoreMastodonData } from './restore-mastodon-data'
|
||||
import pify from 'pify'
|
||||
import { promisify } from 'util'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
@ -8,13 +8,13 @@ import mkdirpCB from 'mkdirp'
|
|||
|
||||
const exec = childProcessPromise.exec
|
||||
const spawn = childProcessPromise.spawn
|
||||
const mkdirp = pify(mkdirpCB)
|
||||
const stat = pify(fs.stat.bind(fs))
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
const mkdirp = promisify(mkdirpCB)
|
||||
const stat = promisify(fs.stat)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const dir = __dirname
|
||||
|
||||
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
||||
const GIT_TAG = 'v2.4.0'
|
||||
const GIT_TAG = 'v2.7.0'
|
||||
|
||||
const DB_NAME = 'pinafore_development'
|
||||
const DB_USER = 'pinafore'
|
||||
|
@ -43,6 +43,7 @@ async function cloneMastodon () {
|
|||
} catch (e) {
|
||||
console.log('Cloning mastodon...')
|
||||
await exec(`git clone --single-branch --branch master ${GIT_URL} "${mastodonDir}"`)
|
||||
await exec(`git fetch origin --tags`, { cwd: mastodonDir }) // may already be cloned, e.g. in CI
|
||||
await exec(`git checkout ${GIT_TAG}`, { cwd: mastodonDir })
|
||||
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||
}
|
||||
|
@ -56,24 +57,24 @@ async function setupMastodonDatabase () {
|
|||
try {
|
||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
} catch (e) { /* ignore */ }
|
||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
let dumpFile = path.join(dir, '../fixtures/dump.sql')
|
||||
let dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
|
||||
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
})
|
||||
|
||||
let tgzFile = path.join(dir, '../fixtures/system.tgz')
|
||||
let tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
|
||||
let systemDir = path.join(mastodonDir, 'public/system')
|
||||
await mkdirp(systemDir)
|
||||
await exec(`tar -xzf "${tgzFile}"`, {cwd: systemDir})
|
||||
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
||||
}
|
||||
|
||||
async function runMastodon () {
|
||||
|
@ -95,12 +96,20 @@ async function runMastodon () {
|
|||
'yarn --pure-lockfile'
|
||||
]
|
||||
|
||||
for (let cmd of cmds) {
|
||||
console.log(cmd)
|
||||
await exec(cmd, {cwd, env})
|
||||
const installedFile = path.join(mastodonDir, 'installed.txt')
|
||||
try {
|
||||
await stat(installedFile)
|
||||
console.log('Already installed Mastodon')
|
||||
} catch (e) {
|
||||
console.log('Installing Mastodon...')
|
||||
for (let cmd of cmds) {
|
||||
console.log(cmd)
|
||||
await exec(cmd, { cwd, env })
|
||||
}
|
||||
await writeFile(installedFile, '', 'utf8')
|
||||
}
|
||||
const promise = spawn('foreman', ['start'], {cwd, env})
|
||||
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
||||
const promise = spawn('foreman', ['start'], { cwd, env })
|
||||
const log = fs.createWriteStream('mastodon.log', { flags: 'a' })
|
||||
childProc = promise.childProcess
|
||||
childProc.stdout.pipe(log)
|
||||
childProc.stderr.pipe(log)
|
||||
|
|
|
@ -2,20 +2,39 @@
|
|||
|
||||
set -e
|
||||
|
||||
if [[ "$COMMAND" = deploy-dev-travis ]]; then
|
||||
if [[ "$COMMAND" = deploy-all-travis || "$COMMAND" = test-unit ]]; then
|
||||
exit 0 # no need to setup mastodon in this case
|
||||
fi
|
||||
|
||||
# install ruby
|
||||
source "$HOME/.rvm/scripts/rvm"
|
||||
rvm install 2.5.1
|
||||
rvm use 2.5.1
|
||||
rvm install 2.6.0
|
||||
rvm use 2.6.0
|
||||
|
||||
sudo -E add-apt-repository -y ppa:mc3man/trusty-media
|
||||
sudo -E apt-get update
|
||||
sudo -E apt-get install -y ffmpeg
|
||||
# fix for redis IPv6 issue
|
||||
# https://travis-ci.community/t/trusty-environment-redis-server-not-starting-with-redis-tools-installed/650/2
|
||||
sudo sed -e 's/^bind.*/bind 127.0.0.1/' /etc/redis/redis.conf > redis.conf
|
||||
sudo mv redis.conf /etc/redis
|
||||
sudo service redis-server start
|
||||
echo PING | nc localhost 6379 # check redis running
|
||||
|
||||
# install ffmpeg because it's not in Trusty
|
||||
if [ ! -f /home/travis/ffmpeg-static/ffmpeg ]; then
|
||||
rm -fr /home/travis/ffmpeg-static
|
||||
mkdir -p /home/travis/ffmpeg-static
|
||||
curl -sL \
|
||||
-A 'https://github.com/nolanlawson/pinafore' \
|
||||
-o ffmpeg.tar.xz \
|
||||
'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz'
|
||||
tar -x -C /home/travis/ffmpeg-static --strip-components 1 -f ffmpeg.tar.xz --wildcards '*/ffmpeg' --wildcards '*/ffprobe'
|
||||
fi
|
||||
sudo ln -s /home/travis/ffmpeg-static/ffmpeg /usr/local/bin/ffmpeg
|
||||
sudo ln -s /home/travis/ffmpeg-static/ffprobe /usr/local/bin/ffprobe
|
||||
|
||||
# check versions
|
||||
ruby --version
|
||||
node --version
|
||||
npm --version
|
||||
yarn --version
|
||||
postgres --version
|
||||
redis-server --version
|
||||
ffmpeg -version
|
||||
|
|
83
bin/svgs.js
83
bin/svgs.js
|
@ -1,40 +1,47 @@
|
|||
module.exports = [
|
||||
{id: 'pinafore-logo', src: 'original-assets/sailboat.svg', title: 'Home'},
|
||||
{id: 'fa-bell', src: 'node_modules/font-awesome-svg-png/white/svg/bell.svg', title: 'Notifications'},
|
||||
{id: 'fa-users', src: 'node_modules/font-awesome-svg-png/white/svg/users.svg', title: 'Local'},
|
||||
{id: 'fa-globe', src: 'node_modules/font-awesome-svg-png/white/svg/globe.svg', title: 'Federated'},
|
||||
{id: 'fa-gear', src: 'node_modules/font-awesome-svg-png/white/svg/gear.svg', title: 'Settings'},
|
||||
{id: 'fa-reply', src: 'node_modules/font-awesome-svg-png/white/svg/reply.svg', title: 'Reply'},
|
||||
{id: 'fa-reply-all', src: 'node_modules/font-awesome-svg-png/white/svg/reply-all.svg', title: 'Reply to thread'},
|
||||
{id: 'fa-retweet', src: 'node_modules/font-awesome-svg-png/white/svg/retweet.svg', title: 'Boost'},
|
||||
{id: 'fa-star', src: 'node_modules/font-awesome-svg-png/white/svg/star.svg', title: 'Favorite'},
|
||||
{id: 'fa-ellipsis-h', src: 'node_modules/font-awesome-svg-png/white/svg/ellipsis-h.svg', title: 'More'},
|
||||
{id: 'fa-spinner', src: 'node_modules/font-awesome-svg-png/white/svg/spinner.svg', title: 'Spinner'},
|
||||
{id: 'fa-user', src: 'node_modules/font-awesome-svg-png/white/svg/user.svg', title: 'Empty user profile'},
|
||||
{id: 'fa-play-circle', src: 'node_modules/font-awesome-svg-png/white/svg/play-circle.svg', title: 'Play'},
|
||||
{id: 'fa-eye', src: 'node_modules/font-awesome-svg-png/white/svg/eye.svg', title: 'Show Sensitive Content'},
|
||||
{id: 'fa-eye-slash', src: 'node_modules/font-awesome-svg-png/white/svg/eye-slash.svg', title: 'Hide Sensitive Content'},
|
||||
{id: 'fa-lock', src: 'node_modules/font-awesome-svg-png/white/svg/lock.svg', title: 'Locked'},
|
||||
{id: 'fa-unlock', src: 'node_modules/font-awesome-svg-png/white/svg/unlock.svg', title: 'Unlocked'},
|
||||
{id: 'fa-envelope', src: 'node_modules/font-awesome-svg-png/white/svg/envelope.svg', title: 'Sealed Envelope'},
|
||||
{id: 'fa-user-times', src: 'node_modules/font-awesome-svg-png/white/svg/user-times.svg', title: 'Stop Following'},
|
||||
{id: 'fa-user-plus', src: 'node_modules/font-awesome-svg-png/white/svg/user-plus.svg', title: 'Follow'},
|
||||
{id: 'fa-external-link', src: 'node_modules/font-awesome-svg-png/white/svg/external-link.svg', title: 'External Link'},
|
||||
{id: 'fa-search', src: 'node_modules/font-awesome-svg-png/white/svg/search.svg', title: 'Search'},
|
||||
{id: 'fa-comments', src: 'node_modules/font-awesome-svg-png/white/svg/comments.svg', title: 'Conversations'},
|
||||
{id: 'fa-paperclip', src: 'node_modules/font-awesome-svg-png/white/svg/paperclip.svg', title: 'Paperclip'},
|
||||
{id: 'fa-thumb-tack', src: 'node_modules/font-awesome-svg-png/white/svg/thumb-tack.svg', title: 'Thumbtack'},
|
||||
{id: 'fa-bars', src: 'node_modules/font-awesome-svg-png/white/svg/bars.svg', title: 'List'},
|
||||
{id: 'fa-ban', src: 'node_modules/font-awesome-svg-png/white/svg/ban.svg', title: 'Ban'},
|
||||
{id: 'fa-camera', src: 'node_modules/font-awesome-svg-png/white/svg/camera.svg', title: 'Add media'},
|
||||
{id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'},
|
||||
{id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'},
|
||||
{id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'},
|
||||
{id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'},
|
||||
{id: 'fa-hourglass', src: 'node_modules/font-awesome-svg-png/white/svg/hourglass.svg', title: 'Follow requested'},
|
||||
{id: 'fa-pencil', src: 'node_modules/font-awesome-svg-png/white/svg/pencil.svg', title: 'Compose'},
|
||||
{id: 'fa-times', src: 'node_modules/font-awesome-svg-png/white/svg/times.svg', title: 'Close'},
|
||||
{id: 'fa-volume-off', src: 'node_modules/font-awesome-svg-png/white/svg/volume-off.svg', title: 'Mute'},
|
||||
{id: 'fa-volume-up', src: 'node_modules/font-awesome-svg-png/white/svg/volume-up.svg', title: 'Unmute'},
|
||||
{id: 'fa-link', src: 'node_modules/font-awesome-svg-png/white/svg/link.svg', title: 'Link'}
|
||||
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg' },
|
||||
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg' },
|
||||
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg' },
|
||||
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
|
||||
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg' },
|
||||
{ id: 'fa-reply', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply.svg' },
|
||||
{ id: 'fa-reply-all', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply-all.svg' },
|
||||
{ id: 'fa-retweet', src: 'src/thirdparty/font-awesome-svg-png/white/svg/retweet.svg' },
|
||||
{ id: 'fa-star', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star.svg' },
|
||||
{ id: 'fa-star-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star-o.svg' },
|
||||
{ id: 'fa-ellipsis-h', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ellipsis-h.svg' },
|
||||
{ id: 'fa-spinner', src: 'src/thirdparty/font-awesome-svg-png/white/svg/spinner.svg' },
|
||||
{ id: 'fa-user', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user.svg' },
|
||||
{ id: 'fa-play-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/play-circle.svg' },
|
||||
{ id: 'fa-eye', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye.svg' },
|
||||
{ id: 'fa-eye-slash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye-slash.svg' },
|
||||
{ id: 'fa-lock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/lock.svg' },
|
||||
{ id: 'fa-unlock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/unlock.svg' },
|
||||
{ id: 'fa-envelope', src: 'src/thirdparty/font-awesome-svg-png/white/svg/envelope.svg' },
|
||||
{ id: 'fa-user-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-times.svg' },
|
||||
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
|
||||
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
|
||||
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg' },
|
||||
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg' },
|
||||
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
|
||||
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
|
||||
{ id: 'fa-bars', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bars.svg' },
|
||||
{ id: 'fa-ban', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ban.svg' },
|
||||
{ id: 'fa-camera', src: 'src/thirdparty/font-awesome-svg-png/white/svg/camera.svg' },
|
||||
{ id: 'fa-smile', src: 'src/thirdparty/font-awesome-svg-png/white/svg/smile-o.svg' },
|
||||
{ id: 'fa-exclamation-triangle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/exclamation-triangle.svg' },
|
||||
{ id: 'fa-check', src: 'src/thirdparty/font-awesome-svg-png/white/svg/check.svg' },
|
||||
{ id: 'fa-trash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/trash-o.svg' },
|
||||
{ id: 'fa-hourglass', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hourglass.svg' },
|
||||
{ id: 'fa-pencil', src: 'src/thirdparty/font-awesome-svg-png/white/svg/pencil.svg' },
|
||||
{ id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
|
||||
{ id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
|
||||
{ id: 'fa-volume-up', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-up.svg' },
|
||||
{ id: 'fa-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/link.svg' },
|
||||
{ id: 'fa-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle.svg' },
|
||||
{ id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
|
||||
{ id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
|
||||
{ id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
|
||||
{ id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
|
||||
{ id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' }
|
||||
]
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import fetch from 'node-fetch'
|
||||
import { actions } from './mastodon-data'
|
||||
|
||||
const numStatuses = actions.filter(_ => _.post || _.boost).length
|
||||
const numStatuses = actions
|
||||
.map(_ => _.post || _.boost)
|
||||
.filter(Boolean)
|
||||
.filter(_ => _.privacy !== 'direct')
|
||||
.length
|
||||
|
||||
async function waitForMastodonData () {
|
||||
while (true) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
## Theming
|
||||
|
||||
Create a file `scss/themes/foobar.scss`, write some SCSS inside and add the following at the bottom of `scss/themes/foobar.scss`.
|
||||
This document describes how to write your own theme for Pinafore.
|
||||
|
||||
First, create a file `scss/themes/foobar.scss`, write some SCSS inside and add
|
||||
the following at the bottom of `scss/themes/foobar.scss`.
|
||||
```scss
|
||||
@import "_base.scss";
|
||||
|
||||
|
@ -9,50 +12,23 @@ body.theme-foobar {
|
|||
}
|
||||
```
|
||||
|
||||
> Note: You can find all the SCSS variables available in `scss/themes/_default.scss` while the all CSS Custom Properties available are listed in `scss/themes/_base.scss`.
|
||||
> Note: You can find all the SCSS variables available in `scss/themes/_default.scss`
|
||||
> while the all CSS Custom Properties available are listed in `scss/themes/_base.scss`.
|
||||
|
||||
Add the CSS class you just define to `scss/themes/_offlines`.
|
||||
```scss
|
||||
...
|
||||
body.offline,
|
||||
body.theme-foobar.offline, // <-
|
||||
body.theme-hotpants.offline,
|
||||
body.theme-majesty.offline,
|
||||
body.theme-oaken.offline,
|
||||
body.theme-scarlet.offline,
|
||||
body.theme-seafoam.offline,
|
||||
body.theme-gecko.offline {
|
||||
@include baseTheme();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Add your theme to `routes/_static/themes.js`
|
||||
Then, Add your theme to `src/routes/_static/themes.js`
|
||||
```js
|
||||
const themes = [
|
||||
...
|
||||
{
|
||||
name: 'foobar',
|
||||
label: 'Foobar'
|
||||
label: 'Foobar', // user-visible name
|
||||
color: 'magenta', // main theme color
|
||||
dark: true // whether it's a dark theme or not
|
||||
}
|
||||
]
|
||||
|
||||
export { themes }
|
||||
```
|
||||
|
||||
Add your theme in `inline-script.js`.
|
||||
```js
|
||||
window.__themeColors = {
|
||||
'default': "royalblue",
|
||||
scarlet: "#e04e41",
|
||||
seafoam: "#177380",
|
||||
hotpants: "hotpink",
|
||||
oaken: "saddlebrown",
|
||||
majesty: "blueviolet",
|
||||
gecko: "#4ab92f",
|
||||
foobar: "#BADA55", // <-
|
||||
offline: "#999999"
|
||||
}
|
||||
```
|
||||
|
||||
Start the development server (`npm run dev`), go to `http://localhost:4002/settings/instances/your-instance-name` and select your newly created theme. Once you've done that, you can update your theme, and refresh the page to see the change (you don't have to restart the server).
|
||||
Start the development server (`yarn run dev`), go to
|
||||
`http://localhost:4002/settings/instances/your-instance-name` and select your
|
||||
newly-created theme. Once you've done that, you can update your theme, and refresh
|
||||
the page to see the change (you don't have to restart the server).
|
||||
|
|
Binary file not shown.
|
@ -1,37 +0,0 @@
|
|||
// For perf reasons, this script is run inline to quickly set certain styles.
|
||||
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
||||
// the build process and write it to inline-script-checksum.json.
|
||||
window.__themeColors = {
|
||||
'default': '#1ea21e',
|
||||
royal: 'royalblue',
|
||||
scarlet: '#e04e41',
|
||||
seafoam: '#177380',
|
||||
hotpants: 'hotpink',
|
||||
oaken: 'saddlebrown',
|
||||
majesty: 'blueviolet',
|
||||
gecko: '#4ab92f',
|
||||
ozark: '#5263af',
|
||||
cobalt: '#08439b',
|
||||
sorcery: '#ae91e8',
|
||||
offline: '#999999'
|
||||
}
|
||||
if (localStorage.store_currentInstance && localStorage.store_instanceThemes) {
|
||||
let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str)
|
||||
let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]
|
||||
if (theme && theme !== 'default') {
|
||||
document.body.classList.add(`theme-${theme}`)
|
||||
let link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = `/theme-${theme}.css`
|
||||
document.head.appendChild(link)
|
||||
if (window.__themeColors[theme]) {
|
||||
document.getElementById('theThemeColor').content = window.__themeColors[theme]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!localStorage.store_currentInstance) {
|
||||
// if not logged in, show all these 'hidden-from-ssr' elements
|
||||
let style = document.createElement('style')
|
||||
style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
|
||||
document.head.appendChild(style)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
173
package.json
173
package.json
|
@ -1,24 +1,25 @@
|
|||
{
|
||||
"name": "pinafore",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "0.5.2",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
|
||||
"dev": "run-s build-svg build-inline-script serve-dev",
|
||||
"serve-dev": "run-p --race build-sass-watch serve",
|
||||
"serve": "node server.js",
|
||||
"build": "cross-env NODE_ENV=production npm run build-steps",
|
||||
"build-steps": "run-s globalize-css build-sass build-svg build-inline-script sapper-build deglobalize-css",
|
||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
||||
"dev": "run-s build-template-html build-third-party-assets serve-dev",
|
||||
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
|
||||
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
|
||||
"sapper-prod": "cross-env PORT=4002 node __sapper__/build",
|
||||
"before-build": "run-s build-template-html build-third-party-assets",
|
||||
"build": "cross-env NODE_ENV=production run-s build-steps",
|
||||
"build-steps": "run-s before-build sapper-build",
|
||||
"sapper-build": "sapper build",
|
||||
"start": "cross-env NODE_ENV=production npm run serve",
|
||||
"start": "cross-env NODE_ENV=production run-s sapper-prod",
|
||||
"build-and-start": "run-s build start",
|
||||
"build-svg": "node ./bin/build-svg.js",
|
||||
"build-inline-script": "node ./bin/build-inline-script.js",
|
||||
"build-sass": "node ./bin/build-sass.js",
|
||||
"build-sass-watch": "node ./bin/build-sass.js --watch",
|
||||
"build-template-html": "node -r esm ./bin/build-template-html.js",
|
||||
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
|
||||
"build-third-party-assets": "node -r esm ./bin/build-third-party-assets.js",
|
||||
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
|
||||
"test": "cross-env BROWSER=chrome:headless npm run test-browser",
|
||||
"test": "cross-env BROWSER=chrome:headless run-s test-browser",
|
||||
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
|
||||
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
|
||||
"test-browser-suite0": "run-p --race run-mastodon build-and-start test-mastodon-suite0",
|
||||
|
@ -28,82 +29,87 @@
|
|||
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
|
||||
"testcafe-suite0": "cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*",
|
||||
"testcafe-suite1": "cross-env-shell testcafe --hostname localhost --skip-js-errors $BROWSER tests/spec/1*",
|
||||
"test-unit": "mocha -r esm tests/unit/",
|
||||
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
|
||||
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
|
||||
"globalize-css": "node ./bin/globalize-css.js",
|
||||
"deglobalize-css": "node ./bin/globalize-css.js --reverse",
|
||||
"stage-dev": "printf 'User-agent: *\nDisallow: /' > assets/robots.txt",
|
||||
"stage-prod": "rm -f assets/robots.txt",
|
||||
"launch": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson && sleep 60",
|
||||
"launch-travis": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson --token $NOW_TOKEN && sleep 60",
|
||||
"alias-prod": "now alias pinafore.social --team nolanlawson",
|
||||
"alias-dev": "now alias dev.pinafore.social --team nolanlawson",
|
||||
"alias-dev-travis": "now alias dev.pinafore.social --team nolanlawson --token $NOW_TOKEN",
|
||||
"cleanup": "now rm pinafore --safe --yes --team nolanlawson",
|
||||
"cleanup-travis": "now rm pinafore --safe --yes --team nolanlawson --token $NOW_TOKEN",
|
||||
"deploy-prod": "run-s stage-prod launch alias-prod cleanup",
|
||||
"deploy-dev": "run-s stage-dev launch alias-dev cleanup",
|
||||
"deploy-dev-travis": "if [ $TRAVIS_BRANCH = master -a $TRAVIS_PULL_REQUEST = false ]; then run-s stage-dev launch-travis alias-dev-travis cleanup-travis; fi",
|
||||
"backup-mastodon-data": "PGPASSWORD=pinafore pg_dump -U pinafore -w mastodon_development > fixtures/dump.sql && cd mastodon/public/system && tar -czf ../../../fixtures/system.tgz ."
|
||||
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
|
||||
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
|
||||
"deploy-all-travis": "./bin/deploy-all-travis.sh",
|
||||
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
|
||||
"sapper-export": "sapper export",
|
||||
"print-export-info": "node ./bin/print-export-info.js",
|
||||
"export-steps": "run-s before-build sapper-export print-export-info",
|
||||
"export": "cross-env NODE_ENV=production run-s export-steps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gamestdio/websocket": "^0.2.7",
|
||||
"a11y-dialog": "^4.0.1",
|
||||
"browserslist": "^4.0.2",
|
||||
"@gamestdio/websocket": "^0.2.8",
|
||||
"@webcomponents/custom-elements": "^1.2.1",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^2.0.4",
|
||||
"circular-dependency-plugin": "^5.0.2",
|
||||
"clean-css": "^4.2.1",
|
||||
"compression": "^1.7.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"css-dedoupe": "^0.1.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"emoji-mart": "github:nolanlawson/emoji-mart#for-pinafore-1",
|
||||
"emoji-regex": "^7.0.3",
|
||||
"encoding": "^0.1.12",
|
||||
"escape-html": "^1.0.3",
|
||||
"esm": "^3.0.77",
|
||||
"events": "^3.0.0",
|
||||
"express": "^4.16.3",
|
||||
"fg-loadcss": "^2.0.1",
|
||||
"esm": "^3.1.4",
|
||||
"events-light": "^1.0.5",
|
||||
"express": "^4.16.4",
|
||||
"file-api": "^0.10.4",
|
||||
"font-awesome-svg-png": "^1.2.2",
|
||||
"form-data": "^2.3.2",
|
||||
"glob": "^7.1.2",
|
||||
"helmet": "^3.13.0",
|
||||
"file-drop-element": "0.0.9",
|
||||
"form-data": "^2.3.3",
|
||||
"glob": "^7.1.3",
|
||||
"helmet": "^3.15.0",
|
||||
"idb-keyval": "^3.1.0",
|
||||
"indexeddb-getall-shim": "^1.3.5",
|
||||
"intersection-observer": "^0.5.0",
|
||||
"lodash-es": "^4.17.10",
|
||||
"inferno-compat": "^7.1.0",
|
||||
"intersection-observer": "^0.5.1",
|
||||
"localstorage-memory": "^1.0.3",
|
||||
"lodash-es": "^4.17.11",
|
||||
"lodash-webpack-plugin": "^0.11.5",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-fetch": "^2.2.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"node-sass": "^4.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-any": "^1.1.0",
|
||||
"page-lifecycle": "^0.1.1",
|
||||
"performance-now": "^2.1.0",
|
||||
"pify": "^4.0.0",
|
||||
"quick-lru": "^1.1.0",
|
||||
"pinch-zoom-element": "^1.1.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"quick-lru": "^2.0.0",
|
||||
"remount": "^0.9.3",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"sapper": "github:nolanlawson/sapper#for-pinafore-7",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-terser": "^4.0.3",
|
||||
"sapper": "^0.25.0",
|
||||
"serve-static": "^1.13.2",
|
||||
"shrink-ray-current": "^2.1.2",
|
||||
"stringz": "^1.0.0",
|
||||
"style-loader": "^0.22.1",
|
||||
"svelte": "^2.11.0",
|
||||
"svelte": "^2.16.0",
|
||||
"svelte-extras": "^2.0.2",
|
||||
"svelte-loader": "^2.10.1",
|
||||
"svelte-loader": "^2.12.0",
|
||||
"svelte-transitions": "^1.2.0",
|
||||
"svgo": "^1.0.5",
|
||||
"timeago.js": "^3.0.2",
|
||||
"svgo": "^1.1.1",
|
||||
"terser-webpack-plugin": "^1.2.1",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
||||
"uuid": "^3.3.2",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"yargs": "^12.0.1"
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-bundle-analyzer": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-html": "^4.0.5",
|
||||
"now": "^11.3.10",
|
||||
"standard": "^11.0.1",
|
||||
"testcafe": "^0.21.1"
|
||||
"assert": "^1.4.1",
|
||||
"eslint-plugin-html": "^5.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"now": "^13.1.2",
|
||||
"standard": "^12.0.1",
|
||||
"testcafe": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
|
@ -136,12 +142,18 @@
|
|||
"btoa",
|
||||
"Blob",
|
||||
"Element",
|
||||
"Image"
|
||||
"Image",
|
||||
"NotificationEvent",
|
||||
"NodeList",
|
||||
"DOMParser",
|
||||
"CSS",
|
||||
"customElements"
|
||||
],
|
||||
"ignore": [
|
||||
"dist",
|
||||
"routes/_utils/asyncModules.js",
|
||||
"routes/_components/dialog/asyncDialogs.js"
|
||||
"src/routes/_utils/asyncModules.js",
|
||||
"src/routes/_utils/asyncPolyfills.js",
|
||||
"src/routes/_components/dialog/asyncDialogs.js"
|
||||
]
|
||||
},
|
||||
"esm": {
|
||||
|
@ -154,27 +166,26 @@
|
|||
"NODE_ENV": "production"
|
||||
},
|
||||
"files": [
|
||||
"assets",
|
||||
"bin",
|
||||
"original-assets",
|
||||
"routes",
|
||||
"scss",
|
||||
"templates",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"server.js",
|
||||
"inline-script.js",
|
||||
"webpack.client.config.js",
|
||||
"webpack.server.config.js"
|
||||
"original-static",
|
||||
"scss",
|
||||
"src",
|
||||
"src-build",
|
||||
"static",
|
||||
"package.json",
|
||||
"thirdparty",
|
||||
"webpack",
|
||||
"webpack.config.js",
|
||||
"yarn.lock"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.0.0"
|
||||
"node": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"sapper",
|
||||
"a11y-dialog"
|
||||
"sapper"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import { getAccount, getRelationship } from '../_api/user'
|
||||
import {
|
||||
getAccount as getAccountFromDatabase,
|
||||
setAccount as setAccountInDatabase,
|
||||
getRelationship as getRelationshipFromDatabase,
|
||||
setRelationship as setRelationshipInDatabase
|
||||
} from '../_database/accountsAndRelationships'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
async function updateAccount (accountId, instanceName, accessToken) {
|
||||
let localPromise = getAccountFromDatabase(instanceName, accountId)
|
||||
let remotePromise = getAccount(instanceName, accessToken, accountId).then(account => {
|
||||
/* no await */ setAccountInDatabase(instanceName, account)
|
||||
return account
|
||||
})
|
||||
|
||||
try {
|
||||
store.set({currentAccountProfile: (await localPromise)})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
store.set({currentAccountProfile: (await remotePromise)})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRelationship (accountId, instanceName, accessToken) {
|
||||
let localPromise = getRelationshipFromDatabase(instanceName, accountId)
|
||||
let remotePromise = getRelationship(instanceName, accessToken, accountId).then(relationship => {
|
||||
/* no await */ setRelationshipInDatabase(instanceName, relationship)
|
||||
return relationship
|
||||
})
|
||||
try {
|
||||
store.set({currentAccountRelationship: (await localPromise)})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
store.set({currentAccountRelationship: (await remotePromise)})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearProfileAndRelationship () {
|
||||
store.set({
|
||||
currentAccountProfile: null,
|
||||
currentAccountRelationship: null
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateProfileAndRelationship (accountId) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
||||
await Promise.all([
|
||||
updateAccount(accountId, currentInstance, accessToken),
|
||||
updateRelationship(accountId, currentInstance, accessToken)
|
||||
])
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import { store } from '../_store/store'
|
||||
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import {
|
||||
getRelationship as getRelationshipFromDatabase
|
||||
} from '../_database/accountsAndRelationships'
|
||||
|
||||
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let account
|
||||
if (follow) {
|
||||
account = await followAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
account = await unfollowAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateProfileAndRelationship(accountId)
|
||||
let relationship = await getRelationshipFromDatabase(currentInstance, accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (follow) {
|
||||
if (account.locked && relationship.requested) {
|
||||
toast.say('Requested to follow account')
|
||||
} else {
|
||||
toast.say('Followed account')
|
||||
}
|
||||
} else {
|
||||
toast.say('Unfollowed account')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { store } from '../_store/store'
|
||||
import { getLists } from '../_api/lists'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import {
|
||||
getLists as getListsFromDatabase,
|
||||
setLists as setListsInDatabase
|
||||
} from '../_database/meta'
|
||||
|
||||
export async function updateLists () {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getLists(currentInstance, accessToken),
|
||||
() => getListsFromDatabase(currentInstance),
|
||||
lists => setListsInDatabase(currentInstance, lists),
|
||||
lists => {
|
||||
let { instanceLists } = store.get()
|
||||
instanceLists[currentInstance] = lists
|
||||
store.set({instanceLists: instanceLists})
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { auth, basename } from './utils'
|
||||
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||
|
||||
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses`
|
||||
|
||||
let body = {
|
||||
status: text,
|
||||
in_reply_to_id: inReplyToId,
|
||||
media_ids: mediaIds,
|
||||
sensitive: sensitive,
|
||||
spoiler_text: spoilerText,
|
||||
visibility: visibility
|
||||
}
|
||||
|
||||
for (let key of Object.keys(body)) {
|
||||
let value = body[key]
|
||||
if (!value || (Array.isArray(value) && !value.length)) {
|
||||
delete body[key]
|
||||
}
|
||||
}
|
||||
|
||||
return post(url, body, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export function getVerifyCredentials (instanceName, accessToken) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
|
||||
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||
}
|
||||
|
||||
export function getAccount (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
||||
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||
}
|
||||
|
||||
export async function getRelationship (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/relationships`
|
||||
url += '?' + paramsString({id: accountId})
|
||||
let res = await get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||
return res[0]
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<video
|
||||
class="autoplay-video {className || ''}"
|
||||
aria-label={ariaLabel || ''}
|
||||
style="background-image: url({poster});"
|
||||
{poster}
|
||||
{width}
|
||||
{height}
|
||||
{src}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
webkit-playsinline
|
||||
playsinline
|
||||
/>
|
||||
<style>
|
||||
.autoplay-video {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
|
@ -1,37 +0,0 @@
|
|||
<Nav {page} />
|
||||
|
||||
<div class="container" tabindex="0" ref:container>
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
{#if !$isUserLoggedIn && page === 'home'}
|
||||
<InformationalFooter />
|
||||
{/if}
|
||||
</div>
|
||||
<script>
|
||||
import Nav from './Nav.html'
|
||||
import { store } from '../_store/store'
|
||||
import InformationalFooter from './InformationalFooter.html'
|
||||
|
||||
// Only focus the `.container` div on first load so it does not intefere
|
||||
// with other desired behaviours (e.g. you click a toot, you navigate from
|
||||
// a timeline view to a thread view, you press the back button, and now
|
||||
// you're still focused on the toot).
|
||||
let firstTime = true
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nav,
|
||||
InformationalFooter
|
||||
},
|
||||
oncreate () {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
this.refs.container.focus()
|
||||
}
|
||||
let { page } = this.get()
|
||||
this.store.set({currentPage: page})
|
||||
},
|
||||
store: () => store
|
||||
}
|
||||
</script>
|
|
@ -1,55 +0,0 @@
|
|||
<div class="lazy-image"
|
||||
style="width: {width}px; height: {height}px; background: {background};">
|
||||
{#if displaySrc}
|
||||
<img
|
||||
class="{hidden ? 'hidden' : ''} {className || ''}"
|
||||
aria-hidden={ariaHidden || ''}
|
||||
alt={alt || ''}
|
||||
title={alt || ''}
|
||||
src={displaySrc}
|
||||
{width}
|
||||
{height}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<style>
|
||||
.lazy-image {
|
||||
overflow: hidden;
|
||||
}
|
||||
.lazy-image img {
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
mark('LazyImage oncreate()')
|
||||
let img = new Image()
|
||||
let { src } = this.get()
|
||||
let { fallback } = this.get()
|
||||
img.onload = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.set({
|
||||
displaySrc: src,
|
||||
hidden: true
|
||||
})
|
||||
requestAnimationFrame(() => {
|
||||
this.set({hidden: false})
|
||||
})
|
||||
})
|
||||
}
|
||||
img.onerror = () => {
|
||||
this.set({displaySrc: fallback})
|
||||
}
|
||||
img.src = src
|
||||
stop('LazyImage oncreate()')
|
||||
},
|
||||
data: () => ({
|
||||
displaySrc: void 0,
|
||||
hidden: false,
|
||||
ariaHidden: false
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -1,43 +0,0 @@
|
|||
{#if staticSrc === src}
|
||||
<img class={className || ''}
|
||||
aria-hidden={ariaHidden}
|
||||
alt={alt || ''}
|
||||
title={alt || ''}
|
||||
{src}
|
||||
on:imgLoad
|
||||
on:imgLoadError />
|
||||
{:else}
|
||||
<img class="{className || ''} non-autoplay-zoom-in {isLink ? 'is-link' : ''}"
|
||||
aria-hidden={ariaHidden}
|
||||
alt={alt || ''}
|
||||
title={alt || ''}
|
||||
src={staticSrc}
|
||||
on:imgLoad
|
||||
on:imgLoadError
|
||||
on:mouseover="onMouseOver(event)"
|
||||
ref:node />
|
||||
{/if}
|
||||
<style>
|
||||
.non-autoplay-zoom-in {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.non-autoplay-zoom-in.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { imgLoad, imgLoadError, mouseover } from '../_utils/events'
|
||||
export default {
|
||||
methods: {
|
||||
onMouseOver (mouseOver) {
|
||||
let { src, staticSrc } = this.get()
|
||||
this.refs.node.src = mouseOver ? src : staticSrc
|
||||
}
|
||||
},
|
||||
events: {
|
||||
imgLoad,
|
||||
imgLoadError,
|
||||
mouseover
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,91 +0,0 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
{title}
|
||||
background="var(--main-bg)"
|
||||
>
|
||||
<div class="custom-emoji-container">
|
||||
{#if emojis.length}
|
||||
<ul class="custom-emoji-list">
|
||||
{#each emojis as emoji}
|
||||
<li class="custom-emoji">
|
||||
<button type="button" on:click="onClickEmoji(emoji)">
|
||||
<img src={$autoplayGifs ? emoji.url : emoji.static_url}
|
||||
alt=":{emoji.shortcode}:"
|
||||
title=":{emoji.shortcode}:"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="custom-emoji-no-emoji">No custom emoji found for this instance.</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalDialog>
|
||||
<style>
|
||||
.custom-emoji-container {
|
||||
max-width: 100%;
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
.custom-emoji-no-emoji {
|
||||
font-size: 1.3em;
|
||||
padding: 20px;
|
||||
}
|
||||
.custom-emoji-list {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
|
||||
grid-gap: 5px;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.custom-emoji button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
}
|
||||
.custom-emoji img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import { store } from '../../../_store/store'
|
||||
import { insertEmoji } from '../../../_actions/emoji'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { close } from '../helpers/closeDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
components: {
|
||||
ModalDialog
|
||||
},
|
||||
store: () => store,
|
||||
computed: {
|
||||
emojis: ({ $currentCustomEmoji }) => {
|
||||
if (!$currentCustomEmoji) {
|
||||
return []
|
||||
}
|
||||
return $currentCustomEmoji.filter(emoji => emoji.visible_in_picker)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show,
|
||||
close,
|
||||
onClickEmoji (emoji) {
|
||||
let { realm } = this.get()
|
||||
insertEmoji(realm, emoji)
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,50 +0,0 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
background="var(--muted-modal-bg)"
|
||||
muted="true"
|
||||
className="image-modal-dialog"
|
||||
>
|
||||
{#if type === 'gifv'}
|
||||
<AutoplayVideo
|
||||
ariaLabel="Animated GIF: {description || ''}"
|
||||
{poster}
|
||||
{src}
|
||||
{width}
|
||||
{height}
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
{src}
|
||||
{width}
|
||||
{height}
|
||||
alt={description || ''}
|
||||
title={description || ''}
|
||||
/>
|
||||
{/if}
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.image-modal-dialog img, .image-modal-dialog video) {
|
||||
object-fit: contain;
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: calc(100% - 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import AutoplayVideo from '../../AutoplayVideo.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
components: {
|
||||
ModalDialog,
|
||||
AutoplayVideo
|
||||
},
|
||||
methods: {
|
||||
show
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,38 +0,0 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
background="var(--muted-modal-bg)"
|
||||
muted="true"
|
||||
className="video-modal-dialog"
|
||||
>
|
||||
<video {poster}
|
||||
{src}
|
||||
{width}
|
||||
{height}
|
||||
aria-label="Video: {description || ''}"
|
||||
controls
|
||||
/>
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.video-modal-dialog video) {
|
||||
object-fit: contain;
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: calc(100% - 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
components: {
|
||||
ModalDialog
|
||||
},
|
||||
methods: {
|
||||
show
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,20 +0,0 @@
|
|||
import ImageDialog from '../components/ImageDialog.html'
|
||||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export default function showImageDialog (poster, src, type, width, height, description) {
|
||||
let imageDialog = new ImageDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Image dialog',
|
||||
poster,
|
||||
src,
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
description
|
||||
}
|
||||
})
|
||||
imageDialog.show()
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import VideoDialog from '../components/VideoDialog.html'
|
||||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export default function showVideoDialog (poster, src, width, height, description) {
|
||||
let videoDialog = new VideoDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Video dialog',
|
||||
poster,
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
description
|
||||
}
|
||||
})
|
||||
videoDialog.show()
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
<li class="settings-list-item">
|
||||
<a {href}>
|
||||
{#if icon}
|
||||
<svg class="settings-list-item-svg">
|
||||
<use xlink:href={icon} />
|
||||
</svg>
|
||||
{/if}
|
||||
<span aria-label={ariaLabel || label} class={offsetForIcon ? 'offset-for-icon' : ''}>
|
||||
{label}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<style>
|
||||
.settings-list-item {
|
||||
border: 1px solid var(--settings-list-item-border);
|
||||
font-size: 1.3em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.settings-list-item a {
|
||||
display: flex;
|
||||
padding: 20px 40px;
|
||||
background: var(--settings-list-item-bg);
|
||||
}
|
||||
.settings-list-item a, .settings-list-item a:visited {
|
||||
color: var(--settings-list-item-text);
|
||||
}
|
||||
.settings-list-item a:hover {
|
||||
text-decoration: none;
|
||||
background: var(--settings-list-item-bg-hover);
|
||||
color: var(--settings-list-item-text-hover);
|
||||
}
|
||||
.settings-list-item a:active {
|
||||
background: var(--settings-list-item-bg-active);
|
||||
}
|
||||
.settings-list-item-svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
fill: var(--svg-fill);
|
||||
}
|
||||
.settings-list-item .offset-for-icon {
|
||||
margin-left: 44px;
|
||||
}
|
||||
.settings-list-item span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.settings-list-item a {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.settings-list-item-svg {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.settings-list-item .offset-for-icon {
|
||||
margin-left: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
icon: void 0,
|
||||
ariaLabel: void 0,
|
||||
offsetForIcon: void 0
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -1,49 +0,0 @@
|
|||
<div class="status-media {sensitive ? 'status-media-is-sensitive' : ''}"
|
||||
style="grid-template-columns: repeat(auto-fit, minmax({maxMediaWidth}px, 1fr));" >
|
||||
{#each mediaAttachments as media}
|
||||
<Media {media} {uuid} />
|
||||
{/each}
|
||||
</div>
|
||||
<style>
|
||||
.status-media {
|
||||
grid-area: media;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.status-media.status-media-is-sensitive {
|
||||
margin: 0;
|
||||
}
|
||||
.status-media {
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-media {
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.status-media {
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Media from './Media.html'
|
||||
import { DEFAULT_MEDIA_WIDTH } from '../../_static/media'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
maxMediaWidth: ({ mediaAttachments }) => {
|
||||
return Math.max.apply(Math, mediaAttachments.map(media => {
|
||||
return media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH
|
||||
}))
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Media
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,51 +0,0 @@
|
|||
{#if status}
|
||||
<Status {index} {length} {timelineType} {timelineValue} {focusSelector}
|
||||
{status} {notification} on:recalculateHeight
|
||||
/>
|
||||
{:else}
|
||||
<article class="notification-article"
|
||||
tabindex="0"
|
||||
aria-posinset={index}
|
||||
aria-setsize={length} >
|
||||
<StatusHeader {notification} {notificationId} {status} {statusId} {timelineType}
|
||||
{account} {accountId} {uuid} isStatusInNotification="true" />
|
||||
</article>
|
||||
{/if}
|
||||
<style>
|
||||
.notification-article {
|
||||
width: 560px;
|
||||
max-width: calc(100vw - 40px);
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid var(--main-border);
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.notification-article {
|
||||
padding: 10px 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
width: 580px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Status from './Status.html'
|
||||
import StatusHeader from './StatusHeader.html'
|
||||
import { store } from '../../_store/store'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Status,
|
||||
StatusHeader
|
||||
},
|
||||
store: () => store,
|
||||
computed: {
|
||||
account: ({ notification }) => notification.account,
|
||||
accountId: ({ account }) => account.id,
|
||||
notificationId: ({ notification }) => notification.id,
|
||||
status: ({ notification }) => notification.status,
|
||||
statusId: ({ status }) => status && status.id,
|
||||
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => {
|
||||
return `${$currentInstance}/${timelineType}/${timelineValue}/${notificationId}/${statusId || ''}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,123 +0,0 @@
|
|||
<div class="status-details">
|
||||
<ExternalLink className="status-absolute-date"
|
||||
href={originalStatus.url}
|
||||
showIcon="true"
|
||||
ariaLabel="{formattedDate} (opens in new window)"
|
||||
>
|
||||
<time datetime={createdAtDate} title={formattedDate}>{formattedDate}</time>
|
||||
</ExternalLink>
|
||||
<a class="status-favs-reblogs"
|
||||
href="/statuses/{originalStatusId}/reblogs"
|
||||
aria-label={reblogsLabel}>
|
||||
<svg class="status-favs-reblogs-svg">
|
||||
<use xlink:href="#fa-retweet"/>
|
||||
</svg>
|
||||
<span>{numReblogs}</span>
|
||||
</a>
|
||||
<a class="status-favs-reblogs"
|
||||
href="/statuses/{originalStatusId}/favorites"
|
||||
aria-label={favoritesLabel}>
|
||||
<svg class="status-favs-reblogs-svg">
|
||||
<use xlink:href="#fa-star" />
|
||||
</svg>
|
||||
<span>{numFavs}</span>
|
||||
</a>
|
||||
</div>
|
||||
<style>
|
||||
.status-details {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, max-content) min-content min-content;
|
||||
grid-gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin: 0 5px 10px;
|
||||
}
|
||||
:global(.status-absolute-date) {
|
||||
font-size: 1.1em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:global(.status-absolute-date time) {
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.status-favs-reblogs {
|
||||
font-size: 1.1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-favs-reblogs span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.status-favs-reblogs,
|
||||
.status-favs-reblogs:hover,
|
||||
.status-favs-reblogs:visited {
|
||||
color: var(--deemphasized-text-color);
|
||||
}
|
||||
|
||||
.status-favs-reblogs-svg {
|
||||
fill: var(--deemphasized-text-color);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
:global(.status-absolute-date, .status-absolute-date:hover, .status-absolute-date:visited) {
|
||||
color: var(--deemphasized-text-color);
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
:global(.status-absolute-date) {
|
||||
font-size: 1em;
|
||||
}
|
||||
.status-favs-reblogs {
|
||||
font-size: 1em;
|
||||
}
|
||||
.status-details {
|
||||
grid-gap: 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import ExternalLink from '../ExternalLink.html'
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
|
||||
numReblogs: ({ originalStatus }) => originalStatus.reblogs_count || 0,
|
||||
numFavs: ({ originalStatus }) => originalStatus.favourites_count || 0,
|
||||
formattedDate: ({ createdAtDate }) => formatter.format(new Date(createdAtDate)),
|
||||
reblogsLabel: ({ numReblogs }) => {
|
||||
// TODO: intl
|
||||
return numReblogs === 1
|
||||
? `Boosted ${numReblogs} time`
|
||||
: `Boosted ${numReblogs} times`
|
||||
},
|
||||
favoritesLabel: ({ numFavs }) => {
|
||||
// TODO: intl
|
||||
return numFavs === 1
|
||||
? `Favorited ${numFavs} time`
|
||||
: `Favorited ${numFavs} times`
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ExternalLink
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,46 +0,0 @@
|
|||
<SettingsLayout page='settings/general' label="General">
|
||||
<h1>General Settings</h1>
|
||||
|
||||
<h2>UI Settings</h2>
|
||||
<form class="ui-settings" aria-label="UI settings">
|
||||
<div class="setting-group">
|
||||
<input type="checkbox" id="choice-autoplay-gif"
|
||||
bind:checked="$autoplayGifs" on:change="$save()">
|
||||
<label for="choice-autoplay-gif">Autoplay GIFs</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<input type="checkbox" id="choice-mark-media-sensitive"
|
||||
bind:checked="$markMediaAsSensitive" on:change="$save()">
|
||||
<label for="choice-mark-media-sensitive">Always mark media as sensitive</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<input type="checkbox" id="choice-reduce-motion"
|
||||
bind:checked="$reduceMotion" on:change="$save()">
|
||||
<label for="choice-reduce-motion">Reduce motion in UI animations</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</SettingsLayout>
|
||||
<style>
|
||||
.ui-settings {
|
||||
background: var(--form-bg);
|
||||
border: 1px solid var(--main-border);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
line-height: 2em;
|
||||
}
|
||||
.setting-group {
|
||||
padding: 5px 0;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
||||
import { store } from '../../_store/store'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsLayout
|
||||
},
|
||||
store: () => store
|
||||
}
|
||||
</script>
|
|
@ -1,23 +0,0 @@
|
|||
<SettingsLayout page='settings' label="Settings">
|
||||
<h1>Settings</h1>
|
||||
|
||||
<SettingsList>
|
||||
<SettingsListItem href="/settings/general" label="General"/>
|
||||
<SettingsListItem href="/settings/instances" label="Instances"/>
|
||||
<SettingsListItem href="/settings/about" label="About Pinafore"/>
|
||||
</SettingsList>
|
||||
|
||||
</SettingsLayout>
|
||||
<script>
|
||||
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
||||
import SettingsList from '../../_components/settings/SettingsList.html'
|
||||
import SettingsListItem from '../../_components/settings/SettingsListItem.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsLayout,
|
||||
SettingsList,
|
||||
SettingsListItem
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,154 +0,0 @@
|
|||
<SettingsLayout page='settings/instances/{params.instanceName}' label={params.instanceName}>
|
||||
<h1 class="instance-name-h1">{params.instanceName}</h1>
|
||||
|
||||
{#if verifyCredentials}
|
||||
<h2>Logged in as:</h2>
|
||||
<div class="acct-current-user">
|
||||
<Avatar account={verifyCredentials} className="acct-avatar" size="big"/>
|
||||
<ExternalLink className="acct-handle"
|
||||
href={verifyCredentials.url}>
|
||||
{'@' + verifyCredentials.acct}
|
||||
</ExternalLink>
|
||||
<span class="acct-display-name">{verifyCredentials.display_name || verifyCredentials.acct}</span>
|
||||
</div>
|
||||
<h2>Theme:</h2>
|
||||
<form class="theme-chooser" aria-label="Choose a theme">
|
||||
{#each themes as theme}
|
||||
<div class="theme-group">
|
||||
<input type="radio" id="choice-theme-{theme.name}"
|
||||
value={theme.name} checked="$currentTheme === theme.name"
|
||||
bind:group="selectedTheme" on:change="onThemeChange()">
|
||||
<label for="choice-theme-{theme.name}">{theme.label}</label>
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
<form class="instance-actions" aria-label="Switch to or log out of this instance">
|
||||
{#if $loggedInInstancesInOrder.length > 1 && $currentInstance !== params.instanceName}
|
||||
<button class="primary"
|
||||
on:click="onSwitchToThisInstance(event)">
|
||||
Switch to this instance
|
||||
</button>
|
||||
{/if}
|
||||
<button on:click="onLogOut(event)">Log out</button>
|
||||
</form>
|
||||
{/if}
|
||||
</SettingsLayout>
|
||||
<style>
|
||||
.acct-current-user {
|
||||
background: var(--form-bg);
|
||||
border: 1px solid var(--main-border);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
font-size: 1.3em;
|
||||
grid-template-areas:
|
||||
"avatar handle"
|
||||
"avatar display-name";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-column-gap: 20px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
:global(.acct-avatar) {
|
||||
grid-area: avatar;
|
||||
}
|
||||
:global(.acct-handle) {
|
||||
grid-area: handle;
|
||||
}
|
||||
.acct-display-name {
|
||||
grid-area: display-name;
|
||||
}
|
||||
.theme-chooser {
|
||||
background: var(--form-bg);
|
||||
border: 1px solid var(--main-border);
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
line-height: 2em;
|
||||
}
|
||||
.theme-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.theme-chooser label {
|
||||
margin: 2px 10px 0;
|
||||
}
|
||||
.instance-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.instance-actions button {
|
||||
margin: 0 5px;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.instance-name-h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { store } from '../../../_store/store'
|
||||
import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
|
||||
import ExternalLink from '../../../_components/ExternalLink.html'
|
||||
import Avatar from '../../../_components/Avatar.html'
|
||||
import { importShowConfirmationDialog } from '../../../_components/dialog/asyncDialogs'
|
||||
import {
|
||||
changeTheme,
|
||||
switchToInstance,
|
||||
logOutOfInstance,
|
||||
updateVerifyCredentialsForInstance
|
||||
} from '../../../_actions/instances'
|
||||
import { themes } from '../../../_static/themes'
|
||||
|
||||
export default {
|
||||
async oncreate () {
|
||||
let { instanceName } = this.get()
|
||||
let { instanceThemes } = this.store.get()
|
||||
this.set({
|
||||
selectedTheme: instanceThemes[instanceName] || 'default'
|
||||
})
|
||||
await updateVerifyCredentialsForInstance(instanceName)
|
||||
},
|
||||
store: () => store,
|
||||
data: () => ({
|
||||
themes: themes,
|
||||
selectedTheme: 'default'
|
||||
}),
|
||||
computed: {
|
||||
instanceName: ({ params }) => params.instanceName,
|
||||
verifyCredentials: ({ $verifyCredentials, instanceName }) => $verifyCredentials && $verifyCredentials[instanceName]
|
||||
},
|
||||
methods: {
|
||||
onThemeChange () {
|
||||
let { selectedTheme, instanceName } = this.get()
|
||||
changeTheme(instanceName, selectedTheme)
|
||||
},
|
||||
onSwitchToThisInstance (e) {
|
||||
e.preventDefault()
|
||||
let { instanceName } = this.get()
|
||||
switchToInstance(instanceName)
|
||||
},
|
||||
async onLogOut (e) {
|
||||
e.preventDefault()
|
||||
let { instanceName } = this.get()
|
||||
|
||||
let showConfirmationDialog = await importShowConfirmationDialog()
|
||||
showConfirmationDialog({
|
||||
text: `Log out of ${instanceName}?`,
|
||||
onPositive () {
|
||||
logOutOfInstance(instanceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SettingsLayout,
|
||||
ExternalLink,
|
||||
Avatar
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,35 +0,0 @@
|
|||
<SettingsLayout page='settings/instances' label="Instances">
|
||||
<h1>Instances</h1>
|
||||
|
||||
{#if $isUserLoggedIn}
|
||||
<p>Instances you've logged in to:</p>
|
||||
<SettingsList label="Instances">
|
||||
{#each $loggedInInstancesAsList as instance}
|
||||
<SettingsListItem offsetForIcon={instance.name !== $currentInstance}
|
||||
icon={instance.name === $currentInstance ? '#fa-star' : ''}
|
||||
href="/settings/instances/{instance.name}"
|
||||
label={instance.name}
|
||||
ariaLabel="{instance.name} {instance.name === $currentInstance ? '(current instance)' : ''}" />
|
||||
{/each}
|
||||
</SettingsList>
|
||||
<p><a href="/settings/instances/add">Add another instance</a></p>
|
||||
{:else}
|
||||
<p>You're not logged in to any instances.</p>
|
||||
<p><a href="/settings/instances/add">Log in to an instance</a> to start using Pinafore.</p>
|
||||
{/if}
|
||||
</SettingsLayout>
|
||||
<script>
|
||||
import { store } from '../../../_store/store'
|
||||
import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
|
||||
import SettingsList from '../../../_components/settings/SettingsList.html'
|
||||
import SettingsListItem from '../../../_components/settings/SettingsListItem.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsLayout,
|
||||
SettingsList,
|
||||
SettingsListItem
|
||||
},
|
||||
store: () => store
|
||||
}
|
||||
</script>
|
|
@ -1,5 +0,0 @@
|
|||
export const DEFAULT_MEDIA_WIDTH = 300
|
||||
export const DEFAULT_MEDIA_HEIGHT = 200
|
||||
|
||||
export const ONE_TRANSPARENT_PIXEL =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
|
@ -1,48 +0,0 @@
|
|||
const themes = [
|
||||
{
|
||||
name: 'default',
|
||||
label: 'Cybre (default)'
|
||||
},
|
||||
{
|
||||
name: 'royal',
|
||||
label: 'Royal'
|
||||
},
|
||||
{
|
||||
name: 'scarlet',
|
||||
label: 'Scarlet'
|
||||
},
|
||||
{
|
||||
name: 'seafoam',
|
||||
label: 'Seafoam'
|
||||
},
|
||||
{
|
||||
name: 'hotpants',
|
||||
label: 'Hotpants'
|
||||
},
|
||||
{
|
||||
name: 'oaken',
|
||||
label: 'Oaken'
|
||||
},
|
||||
{
|
||||
name: 'majesty',
|
||||
label: 'Majesty'
|
||||
},
|
||||
{
|
||||
name: 'gecko',
|
||||
label: 'Gecko'
|
||||
},
|
||||
{
|
||||
name: 'ozark',
|
||||
label: 'Ozark'
|
||||
},
|
||||
{
|
||||
name: 'cobalt',
|
||||
label: 'Cobalt'
|
||||
},
|
||||
{
|
||||
name: 'sorcery',
|
||||
label: 'Sorcery'
|
||||
}
|
||||
]
|
||||
|
||||
export { themes }
|
|
@ -1,22 +0,0 @@
|
|||
function getStatusModifications (store, instanceName) {
|
||||
let { statusModifications } = store.get()
|
||||
statusModifications[instanceName] = statusModifications[instanceName] || {
|
||||
favorites: {},
|
||||
reblogs: {}
|
||||
}
|
||||
return statusModifications
|
||||
}
|
||||
|
||||
export function statusMixins (Store) {
|
||||
Store.prototype.setStatusFavorited = function (instanceName, statusId, favorited) {
|
||||
let statusModifications = getStatusModifications(this, instanceName)
|
||||
statusModifications[instanceName].favorites[statusId] = favorited
|
||||
this.set({statusModifications})
|
||||
}
|
||||
|
||||
Store.prototype.setStatusReblogged = function (instanceName, statusId, reblogged) {
|
||||
let statusModifications = getStatusModifications(this, instanceName)
|
||||
statusModifications[instanceName].reblogs[statusId] = reblogged
|
||||
this.set({statusModifications})
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
import { updateInstanceInfo, updateVerifyCredentialsForInstance } from '../../_actions/instances'
|
||||
import { updateLists } from '../../_actions/lists'
|
||||
import { createStream } from '../../_actions/streaming'
|
||||
import { updateCustomEmojiForInstance } from '../../_actions/emoji'
|
||||
import { addStatusesOrNotifications } from '../../_actions/addStatusOrNotification'
|
||||
import { getTimeline } from '../../_api/timelines'
|
||||
|
||||
export function instanceObservers (store) {
|
||||
// stream to watch for home timeline updates and notifications
|
||||
let currentInstanceStream
|
||||
|
||||
store.observe('currentInstance', async (currentInstance) => {
|
||||
if (!process.browser) {
|
||||
return
|
||||
}
|
||||
if (currentInstanceStream) {
|
||||
currentInstanceStream.close()
|
||||
currentInstanceStream = null
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.currentInstanceStream = null
|
||||
}
|
||||
}
|
||||
if (!currentInstance) {
|
||||
return
|
||||
}
|
||||
updateVerifyCredentialsForInstance(currentInstance)
|
||||
updateInstanceInfo(currentInstance)
|
||||
updateCustomEmojiForInstance(currentInstance)
|
||||
updateLists()
|
||||
|
||||
await updateInstanceInfo(currentInstance)
|
||||
|
||||
let currentInstanceIsUnchanged = () => {
|
||||
let { currentInstance: newCurrentInstance } = store.get()
|
||||
return newCurrentInstance === currentInstance
|
||||
}
|
||||
|
||||
if (!currentInstanceIsUnchanged()) {
|
||||
return
|
||||
}
|
||||
|
||||
let { currentInstanceInfo } = store.get()
|
||||
if (!currentInstanceInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
let homeTimelineItemIds = store.getForTimeline(currentInstance,
|
||||
'home', 'timelineItemIds')
|
||||
let firstHomeTimelineItemId = homeTimelineItemIds && homeTimelineItemIds[0]
|
||||
let notificationItemIds = store.getForTimeline(currentInstance,
|
||||
'notifications', 'timelineItemIds')
|
||||
let firstNotificationTimelineItemId = notificationItemIds && notificationItemIds[0]
|
||||
|
||||
let onOpenStream = async () => {
|
||||
if (!currentInstanceIsUnchanged()) {
|
||||
return
|
||||
}
|
||||
|
||||
// fill in the "streaming gap" – i.e. fetch the most recent 20 items so that there isn't
|
||||
// a big gap in the timeline if you haven't looked at it in awhile
|
||||
async function fillGap (timelineName, firstTimelineItemId) {
|
||||
if (!firstTimelineItemId) {
|
||||
return
|
||||
}
|
||||
let newTimelineItems = await getTimeline(currentInstance, accessToken,
|
||||
timelineName, null, firstTimelineItemId)
|
||||
if (newTimelineItems.length) {
|
||||
addStatusesOrNotifications(currentInstance, timelineName, newTimelineItems)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fillGap('home', firstHomeTimelineItemId),
|
||||
fillGap('notifications', firstNotificationTimelineItemId)
|
||||
])
|
||||
}
|
||||
|
||||
let { accessToken } = store.get()
|
||||
let streamingApi = currentInstanceInfo.urls.streaming_api
|
||||
currentInstanceStream = createStream(streamingApi,
|
||||
currentInstance, accessToken, 'home', onOpenStream)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.currentInstanceStream = currentInstanceStream
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { instanceObservers } from './instanceObservers'
|
||||
import { timelineObservers } from './timelineObservers'
|
||||
import { notificationObservers } from './notificationObservers'
|
||||
import { onlineObservers } from './onlineObservers'
|
||||
import { navObservers } from './navObservers'
|
||||
import { autosuggestObservers } from './autosuggestObservers'
|
||||
import { pageVisibilityObservers } from './pageVisibilityObservers'
|
||||
|
||||
export function observers (store) {
|
||||
instanceObservers(store)
|
||||
timelineObservers(store)
|
||||
notificationObservers(store)
|
||||
onlineObservers(store)
|
||||
navObservers(store)
|
||||
autosuggestObservers(store)
|
||||
pageVisibilityObservers(store)
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { observers } from './observers/observers'
|
||||
import { computations } from './computations/computations'
|
||||
import { mixins } from './mixins/mixins'
|
||||
import { LocalStorageStore } from './LocalStorageStore'
|
||||
import { observe } from 'svelte-extras'
|
||||
|
||||
const KEYS_TO_STORE_IN_LOCAL_STORAGE = new Set([
|
||||
'currentInstance',
|
||||
'currentRegisteredInstance',
|
||||
'currentRegisteredInstanceName',
|
||||
'instanceNameInSearch',
|
||||
'instanceThemes',
|
||||
'loggedInInstances',
|
||||
'loggedInInstancesInOrder',
|
||||
'autoplayGifs',
|
||||
'markMediaAsSensitive',
|
||||
'reduceMotion',
|
||||
'pinnedPages',
|
||||
'composeData'
|
||||
])
|
||||
|
||||
class PinaforeStore extends LocalStorageStore {
|
||||
constructor (state) {
|
||||
super(state, KEYS_TO_STORE_IN_LOCAL_STORAGE)
|
||||
}
|
||||
}
|
||||
|
||||
PinaforeStore.prototype.observe = observe
|
||||
|
||||
export const store = new PinaforeStore({
|
||||
instanceNameInSearch: '',
|
||||
queryInSearch: '',
|
||||
currentInstance: null,
|
||||
loggedInInstances: {},
|
||||
loggedInInstancesInOrder: [],
|
||||
instanceThemes: {},
|
||||
spoilersShown: {},
|
||||
sensitivesShown: {},
|
||||
repliesShown: {},
|
||||
autoplayGifs: false,
|
||||
markMediaAsSensitive: false,
|
||||
reduceMotion: false,
|
||||
pinnedPages: {},
|
||||
instanceLists: {},
|
||||
pinnedStatuses: {},
|
||||
instanceInfos: {},
|
||||
statusModifications: {},
|
||||
customEmoji: {},
|
||||
composeData: {},
|
||||
verifyCredentials: {},
|
||||
online: !process.browser || navigator.onLine
|
||||
})
|
||||
|
||||
mixins(PinaforeStore)
|
||||
computations(store)
|
||||
observers(store)
|
||||
|
||||
if (process.browser && process.env.NODE_ENV !== 'production') {
|
||||
window.store = store // for debugging
|
||||
}
|
||||
|
||||
// needed for tests
|
||||
if (process.browser) {
|
||||
window.__forceOnline = online => store.set({online})
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
export const importTimeline = () => import(
|
||||
/* webpackChunkName: 'Timeline' */ '../_components/timeline/Timeline.html'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importIntersectionObserver = () => import(
|
||||
/* webpackChunkName: 'intersection-observer' */ 'intersection-observer'
|
||||
)
|
||||
|
||||
export const importRequestIdleCallback = () => import(
|
||||
/* webpackChunkName: 'requestidlecallback' */ 'requestidlecallback'
|
||||
)
|
||||
|
||||
export const importIndexedDBGetAllShim = () => import(
|
||||
/* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim'
|
||||
)
|
||||
|
||||
export const importWebAnimationPolyfill = () => import(
|
||||
/* webpackChunkName: 'web-animations-js' */ 'web-animations-js'
|
||||
)
|
||||
|
||||
export const importWebSocketClient = () => import(
|
||||
/* webpackChunkName: '@gamestdio/websocket' */ '@gamestdio/websocket'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importVirtualList = () => import(
|
||||
/* webpackChunkName: 'VirtualList.html' */ '../_components/virtualList/VirtualList.html'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importList = () => import(
|
||||
/* webpackChunkName: 'List.html' */ '../_components/list/List.html'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importStatusVirtualListItem = () => import(
|
||||
/* webpackChunkName: 'StatusVirtualListItem.html' */ '../_components/timeline/StatusVirtualListItem.html'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importNotificationVirtualListItem = () => import(
|
||||
/* webpackChunkName: 'NotificationVirtualListItem.html' */ '../_components/timeline/NotificationVirtualListItem.html'
|
||||
).then(mod => mod.default)
|
|
@ -1,47 +0,0 @@
|
|||
// via https://github.com/tootsuite/mastodon/blob/f59ed3a4fafab776b4eeb92f805dfe1fecc17ee3/app/javascript/mastodon/scroll.js
|
||||
const easingOutQuint = (x, t, b, c, d) =>
|
||||
c * ((t = t / d - 1) * t * t * t * t + 1) + b
|
||||
|
||||
const scroll = (node, key, target) => {
|
||||
const startTime = Date.now()
|
||||
const offset = node[key]
|
||||
const gap = target - offset
|
||||
const duration = 1000
|
||||
let interrupt = false
|
||||
|
||||
const step = () => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const percentage = elapsed / duration
|
||||
|
||||
if (interrupt) {
|
||||
return
|
||||
}
|
||||
|
||||
if (percentage > 1) {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
node[key] = easingOutQuint(0, elapsed, offset, gap, duration)
|
||||
requestAnimationFrame(step)
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
interrupt = true
|
||||
cleanup()
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
node.removeEventListener('wheel', cancel)
|
||||
node.removeEventListener('touchstart', cancel)
|
||||
}
|
||||
|
||||
node.addEventListener('wheel', cancel, {passive: true})
|
||||
node.addEventListener('touchstart', cancel, {passive: true})
|
||||
|
||||
step()
|
||||
|
||||
return cancel
|
||||
}
|
||||
|
||||
export const smoothScrollToTop = node => scroll(node, 'scrollTop', 0)
|
|
@ -1,19 +0,0 @@
|
|||
import { loadCSS } from 'fg-loadcss'
|
||||
|
||||
let meta = process.browser && document.getElementById('theThemeColor')
|
||||
|
||||
export function switchToTheme (themeName) {
|
||||
let clazzList = document.body.classList
|
||||
for (let i = 0; i < clazzList.length; i++) {
|
||||
let clazz = clazzList.item(i)
|
||||
if (clazz.startsWith('theme-')) {
|
||||
clazzList.remove(clazz)
|
||||
}
|
||||
}
|
||||
let themeColor = window.__themeColors[themeName]
|
||||
meta.content = themeColor || window.__themeColors['default']
|
||||
if (themeName !== 'default') {
|
||||
clazzList.add(`theme-${themeName}`)
|
||||
loadCSS(`/theme-${themeName}.css`)
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import Toast from '../_components/Toast.html'
|
||||
|
||||
let toast
|
||||
|
||||
if (process.browser) {
|
||||
toast = new Toast({
|
||||
target: document.querySelector('#toast')
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.toast = toast // for debugging
|
||||
}
|
||||
} else {
|
||||
toast = {
|
||||
say: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
export { toast }
|
|
@ -1,21 +0,0 @@
|
|||
<svelte:head>
|
||||
<title>Pinafore – Profile</title>
|
||||
</svelte:head>
|
||||
<Layout page='tags'>
|
||||
<LazyPage {pageComponent} {params} />
|
||||
</Layout>
|
||||
<script>
|
||||
import Layout from '../_components/Layout.html'
|
||||
import LazyPage from '../_components/LazyPage.html'
|
||||
import pageComponent from '../_pages/accounts/[accountId].html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
LazyPage
|
||||
},
|
||||
data: () => ({
|
||||
pageComponent
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -1,27 +0,0 @@
|
|||
$main-theme-color: #999999;
|
||||
$body-bg-color: lighten($main-theme-color, 38%);
|
||||
$anchor-color: $main-theme-color;
|
||||
$main-text-color: #333;
|
||||
$border-color: #dadada;
|
||||
$main-bg-color: white;
|
||||
$secondary-text-color: white;
|
||||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 15%);
|
||||
$compose-background: lighten($main-theme-color, 17%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
body.offline,
|
||||
body.theme-cybre.offline,
|
||||
body.theme-hotpants.offline,
|
||||
body.theme-majesty.offline,
|
||||
body.theme-oaken.offline,
|
||||
body.theme-scarlet.offline,
|
||||
body.theme-seafoam.offline,
|
||||
body.theme-gecko.offline,
|
||||
body.theme-ozark.offline,
|
||||
body.theme-cobalt.offline,
|
||||
body.theme-sorcery.offline {
|
||||
@include baseTheme();
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8' >
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" >
|
||||
<meta id='theThemeColor' name='theme-color' content='#4169e1' >
|
||||
<meta name="description" content="An alternative web client for Mastodon, focused on speed and simplicity." >
|
||||
|
||||
%sapper.base%
|
||||
|
||||
<link id='theManifest' rel='manifest' href='/manifest.json' >
|
||||
<link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120.png" >
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" >
|
||||
<meta name="mobile-web-app-capable" content="yes" >
|
||||
<meta name="apple-mobile-web-app-title" content="Pinafore" >
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white" >
|
||||
|
||||
<!-- inline CSS -->
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.hidden-from-ssr {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
|
||||
<!-- Sapper generates a <style> tag containing critical CSS
|
||||
for the current page. CSS for the rest of the src is
|
||||
lazily loaded when it precaches secondary pages -->
|
||||
%sapper.styles%
|
||||
|
||||
<!-- This contains the contents of the <:Head> component, if
|
||||
the current page has one -->
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<!-- inline JS -->
|
||||
|
||||
<!-- The application will be rendered inside this element,
|
||||
because `templates/client.js` references it -->
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
|
||||
<!-- Toast.html gets rendered here -->
|
||||
<div id="theToast"></div>
|
||||
|
||||
<!-- LoadingMask.html gets rendered here -->
|
||||
<div id="loading-mask" aria-hidden="true"></div>
|
||||
|
||||
<!-- inline SVG -->
|
||||
|
||||
<!-- Sapper creates a <script> tag containing `templates/client.js`
|
||||
and anything else it needs to hydrate the src and
|
||||
initialise the router -->
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
import * as sapper from '../__sapper__/client.js'
|
||||
import { loadPolyfills } from './routes/_utils/loadPolyfills'
|
||||
import './routes/_utils/serviceWorkerClient'
|
||||
import './routes/_utils/historyEvents'
|
||||
import './routes/_utils/loadingMask'
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
console.log('init()')
|
||||
sapper.start({ target: document.querySelector('#sapper') })
|
||||
})
|
||||
|
||||
console.log('process.env.NODE_ENV', process.env.NODE_ENV)
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
// For perf reasons, this script is run inline to quickly set certain styles.
|
||||
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
||||
// the build process and write it to checksum.js.
|
||||
|
||||
import { testHasLocalStorageOnce } from '../routes/_utils/testStorage'
|
||||
import { DEFAULT_LIGHT_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
|
||||
import { basename } from '../routes/_api/utils'
|
||||
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
|
||||
|
||||
window.__themeColors = process.env.THEME_COLORS
|
||||
|
||||
const safeParse = str => (typeof str === 'undefined' || str === 'undefined') ? undefined : JSON.parse(str)
|
||||
const hasLocalStorage = testHasLocalStorageOnce()
|
||||
const currentInstance = hasLocalStorage && safeParse(localStorage.store_currentInstance)
|
||||
|
||||
if (currentInstance) {
|
||||
// Do prefetch if we're logged in, so we can connect faster to the other origin.
|
||||
// Note that /api/v1/instance is basically the only URL that doesn't require credentials,
|
||||
// which is why we can do this. Also we do end up calling this on loading the home page,
|
||||
// so it's not a wasted request.
|
||||
let link = document.createElement('link')
|
||||
link.setAttribute('rel', 'prefetch')
|
||||
link.setAttribute('href', `${basename(currentInstance)}/api/v1/instance`)
|
||||
link.setAttribute('crossorigin', 'anonymous')
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
let theme = (currentInstance &&
|
||||
localStorage.store_instanceThemes &&
|
||||
safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]) ||
|
||||
DEFAULT_THEME
|
||||
if (theme !== DEFAULT_LIGHT_THEME) {
|
||||
// switch theme ASAP to minimize flash of default theme
|
||||
switchToTheme(theme)
|
||||
}
|
||||
|
||||
if (!hasLocalStorage || !currentInstance) {
|
||||
// if not logged in, show all these 'hidden-from-ssr' elements
|
||||
onUserIsLoggedOut()
|
||||
}
|
||||
|
||||
if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
|
||||
// if user has disabled custom scrollbars, remove this style
|
||||
let theScrollbarStyle = document.getElementById('theScrollbarStyle')
|
||||
theScrollbarStyle.setAttribute('media', 'only x') // disables the style
|
||||
}
|
||||
|
||||
// hack to make the scrollbars rounded only on macOS
|
||||
if (/mac/i.test(navigator.platform)) {
|
||||
document.documentElement.style.setProperty('--scrollbar-border-radius', '50px')
|
||||
}
|
||||
|
||||
// TODO: remove this hack when Safari works with cross-origin window.open()
|
||||
// in a PWA: https://github.com/nolanlawson/pinafore/issues/45
|
||||
if (/iP(?:hone|ad|od)/.test(navigator.userAgent)) {
|
||||
document.head.removeChild(document.getElementById('theManifest'))
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { getAccountAccessibleName } from './getAccountAccessibleName'
|
||||
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'
|
||||
|
||||
function getNotificationText (notification, omitEmojiInDisplayNames) {
|
||||
if (!notification) {
|
||||
return
|
||||
}
|
||||
let notificationAccountDisplayName = getAccountAccessibleName(notification.account, omitEmojiInDisplayNames)
|
||||
if (notification.type === 'reblog') {
|
||||
return `${notificationAccountDisplayName} boosted your status`
|
||||
} else if (notification.type === 'favourite') {
|
||||
return `${notificationAccountDisplayName} favorited your status`
|
||||
}
|
||||
}
|
||||
|
||||
function getPrivacyText (visibility) {
|
||||
for (let option of POST_PRIVACY_OPTIONS) {
|
||||
if (option.key === visibility) {
|
||||
return option.label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getReblogText (reblog, account, omitEmojiInDisplayNames) {
|
||||
if (!reblog) {
|
||||
return
|
||||
}
|
||||
let accountDisplayName = getAccountAccessibleName(account, omitEmojiInDisplayNames)
|
||||
return `Boosted by ${accountDisplayName}`
|
||||
}
|
||||
|
||||
function cleanupText (text) {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
|
||||
timeagoFormattedDate, spoilerText, showContent,
|
||||
reblog, notification, visibility, omitEmojiInDisplayNames,
|
||||
disableLongAriaLabels) {
|
||||
let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
||||
let contentTextToShow = (showContent || !spoilerText)
|
||||
? cleanupText(plainTextContent)
|
||||
: `Content warning: ${cleanupText(spoilerText)}`
|
||||
let privacyText = getPrivacyText(visibility)
|
||||
|
||||
if (disableLongAriaLabels) {
|
||||
// Long text can crash NVDA; allow users to shorten it like we had it before.
|
||||
// https://github.com/nolanlawson/pinafore/issues/694
|
||||
return `${privacyText} status by ${originalAccountDisplayName}`
|
||||
}
|
||||
|
||||
let values = [
|
||||
getNotificationText(notification, omitEmojiInDisplayNames),
|
||||
originalAccountDisplayName,
|
||||
contentTextToShow,
|
||||
timeagoFormattedDate,
|
||||
`@${originalAccount.acct}`,
|
||||
privacyText,
|
||||
getReblogText(reblog, account, omitEmojiInDisplayNames)
|
||||
].filter(Boolean)
|
||||
|
||||
return values.join(', ')
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { removeEmoji } from '../_utils/removeEmoji'
|
||||
|
||||
export function getAccountAccessibleName (account, omitEmojiInDisplayNames) {
|
||||
let emojis = account.emojis
|
||||
let displayName = account.display_name || account.username
|
||||
if (omitEmojiInDisplayNames) {
|
||||
displayName = removeEmoji(displayName, emojis) || displayName
|
||||
}
|
||||
return displayName
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { getAccount } from '../_api/user'
|
||||
import { getRelationship } from '../_api/relationships'
|
||||
import { database } from '../_database/database'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
async function _updateAccount (accountId, instanceName, accessToken) {
|
||||
let localPromise = database.getAccount(instanceName, accountId)
|
||||
let remotePromise = getAccount(instanceName, accessToken, accountId).then(account => {
|
||||
/* no await */ database.setAccount(instanceName, account)
|
||||
return account
|
||||
})
|
||||
|
||||
try {
|
||||
store.set({ currentAccountProfile: (await localPromise) })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
store.set({ currentAccountProfile: (await remotePromise) })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function _updateRelationship (accountId, instanceName, accessToken) {
|
||||
let localPromise = database.getRelationship(instanceName, accountId)
|
||||
let remotePromise = getRelationship(instanceName, accessToken, accountId).then(relationship => {
|
||||
/* no await */ database.setRelationship(instanceName, relationship)
|
||||
return relationship
|
||||
})
|
||||
try {
|
||||
store.set({ currentAccountRelationship: (await localPromise) })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
store.set({ currentAccountRelationship: (await remotePromise) })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLocalRelationship (instanceName, accountId, relationship) {
|
||||
await database.setRelationship(instanceName, relationship)
|
||||
try {
|
||||
store.set({ currentAccountRelationship: relationship })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearProfileAndRelationship () {
|
||||
store.set({
|
||||
currentAccountProfile: null,
|
||||
currentAccountRelationship: null
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateProfileAndRelationship (accountId) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
||||
await Promise.all([
|
||||
_updateAccount(accountId, currentInstance, accessToken),
|
||||
_updateRelationship(accountId, currentInstance, accessToken)
|
||||
])
|
||||
}
|
||||
|
||||
export async function updateRelationship (accountId) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
||||
await _updateRelationship(accountId, currentInstance, accessToken)
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
|
||||
import { getInstanceInfo } from '../_api/instance'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { switchToTheme } from '../_utils/themeEngine'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||
import { store } from '../_store/store'
|
||||
import { updateVerifyCredentialsForInstance } from './instances'
|
||||
import { updateCustomEmojiForInstance } from './emoji'
|
||||
import { setInstanceInfo as setInstanceInfoInDatabase } from '../_database/meta'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
const REDIRECT_URI = (typeof location !== 'undefined'
|
||||
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
||||
|
@ -14,12 +14,13 @@ async function redirectToOauth () {
|
|||
let { instanceNameInSearch, loggedInInstances } = store.get()
|
||||
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
||||
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
||||
store.set({logInToInstanceError: `You've already logged in to ${instanceNameInSearch}`})
|
||||
return
|
||||
let err = new Error(`You've already logged in to ${instanceNameInSearch}`)
|
||||
err.knownError = true
|
||||
throw err
|
||||
}
|
||||
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
||||
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||
await setInstanceInfoInDatabase(instanceNameInSearch, instanceInfo) // cache for later
|
||||
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
||||
let instanceData = await registrationPromise
|
||||
store.set({
|
||||
currentRegisteredInstanceName: instanceNameInSearch,
|
||||
|
@ -44,16 +45,17 @@ export async function logInToInstance () {
|
|||
} catch (err) {
|
||||
console.error(err)
|
||||
let error = `${err.message || err.name}. ` +
|
||||
(navigator.onLine
|
||||
? `Is this a valid Mastodon instance? Is a browser extension blocking the request?`
|
||||
: `Are you offline?`)
|
||||
(err.knownError ? '' : (navigator.onLine
|
||||
? `Is this a valid Mastodon instance? Is a browser extension
|
||||
blocking the request? Are you in private browsing mode?`
|
||||
: `Are you offline?`))
|
||||
let { instanceNameInSearch } = store.get()
|
||||
store.set({
|
||||
logInToInstanceError: error,
|
||||
logInToInstanceErrorForText: instanceNameInSearch
|
||||
})
|
||||
} finally {
|
||||
store.set({logInToInstanceLoading: false})
|
||||
store.set({ logInToInstanceLoading: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +69,7 @@ async function registerNewInstance (code) {
|
|||
REDIRECT_URI
|
||||
)
|
||||
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
||||
instanceThemes[currentRegisteredInstanceName] = 'default'
|
||||
instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
|
||||
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
||||
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
||||
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
||||
|
@ -82,7 +84,7 @@ async function registerNewInstance (code) {
|
|||
instanceThemes: instanceThemes
|
||||
})
|
||||
store.save()
|
||||
switchToTheme('default')
|
||||
switchToTheme(DEFAULT_THEME)
|
||||
// fire off these requests so they're cached
|
||||
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
||||
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
||||
|
@ -91,11 +93,11 @@ async function registerNewInstance (code) {
|
|||
|
||||
export async function handleOauthCode (code) {
|
||||
try {
|
||||
store.set({logInToInstanceLoading: true})
|
||||
store.set({ logInToInstanceLoading: true })
|
||||
await registerNewInstance(code)
|
||||
} catch (err) {
|
||||
store.set({logInToInstanceError: `${err.message || err.name}. Failed to connect to instance.`})
|
||||
store.set({ logInToInstanceError: `${err.message || err.name}. Failed to connect to instance.` })
|
||||
} finally {
|
||||
store.set({logInToInstanceLoading: false})
|
||||
store.set({ logInToInstanceLoading: false })
|
||||
}
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
import throttle from 'lodash-es/throttle'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { store } from '../_store/store'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import uniq from 'lodash-es/uniq'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import {
|
||||
insertTimelineItems as insertTimelineItemsInDatabase
|
||||
} from '../_database/timelines/insertion'
|
||||
import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask'
|
||||
|
||||
const STREAMING_THROTTLE_DELAY = 3000
|
||||
import { database } from '../_database/database'
|
||||
import { concat } from '../_utils/arrays'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
|
||||
function getExistingItemIdsSet (instanceName, timelineName) {
|
||||
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
||||
|
@ -29,14 +25,31 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
|
|||
return
|
||||
}
|
||||
|
||||
await insertTimelineItemsInDatabase(instanceName, timelineName, updates)
|
||||
await database.insertTimelineItems(instanceName, timelineName, updates)
|
||||
|
||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||
let newItemIdsToAdd = uniq([].concat(itemIdsToAdd).concat(updates.map(_ => _.id)))
|
||||
let newItemIdsToAdd = uniq(concat(itemIdsToAdd, updates.map(_ => _.id)))
|
||||
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||
'items to itemIdsToAdd for timeline', timelineName)
|
||||
store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: newItemIdsToAdd})
|
||||
store.setForTimeline(instanceName, timelineName, { itemIdsToAdd: newItemIdsToAdd })
|
||||
}
|
||||
}
|
||||
|
||||
function isValidStatusForThread (thread, timelineName, itemIdsToAdd) {
|
||||
let focusedStatusId = timelineName.split('/')[1] // e.g. "status/123456"
|
||||
let focusedStatusIdx = thread.indexOf(focusedStatusId)
|
||||
return status => {
|
||||
let repliedToStatusIdx = thread.indexOf(status.in_reply_to_id)
|
||||
return (
|
||||
// A reply to an ancestor status is not valid for this thread, but for the focused status
|
||||
// itself or any of its descendents, it is valid.
|
||||
repliedToStatusIdx >= focusedStatusIdx &&
|
||||
// Not a duplicate
|
||||
!thread.includes(status.id) &&
|
||||
// Not already about to be added
|
||||
!itemIdsToAdd.includes(status.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,21 +59,20 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
|
|||
}
|
||||
|
||||
let threads = store.getThreads(instanceName)
|
||||
|
||||
for (let timelineName of Object.keys(threads)) {
|
||||
let timelineNames = Object.keys(threads)
|
||||
for (let timelineName of timelineNames) {
|
||||
let thread = threads[timelineName]
|
||||
let updatesForThisThread = updates.filter(
|
||||
status => thread.includes(status.in_reply_to_id) && !thread.includes(status.id)
|
||||
)
|
||||
if (!updatesForThisThread.length) {
|
||||
|
||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||
let validUpdates = updates.filter(isValidStatusForThread(thread, timelineName, itemIdsToAdd))
|
||||
if (!validUpdates.length) {
|
||||
continue
|
||||
}
|
||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||
let newItemIdsToAdd = uniq([].concat(itemIdsToAdd).concat(updatesForThisThread.map(_ => _.id)))
|
||||
let newItemIdsToAdd = uniq(concat(itemIdsToAdd, validUpdates.map(_ => _.id)))
|
||||
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||
'items to itemIdsToAdd for thread', timelineName)
|
||||
store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: newItemIdsToAdd})
|
||||
store.setForTimeline(instanceName, timelineName, { itemIdsToAdd: newItemIdsToAdd })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +82,7 @@ async function processFreshUpdates (instanceName, timelineName) {
|
|||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates')
|
||||
if (freshUpdates && freshUpdates.length) {
|
||||
let updates = freshUpdates.slice()
|
||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: []})
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: [] })
|
||||
|
||||
await Promise.all([
|
||||
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
||||
|
@ -80,11 +92,11 @@ async function processFreshUpdates (instanceName, timelineName) {
|
|||
stop('processFreshUpdates')
|
||||
}
|
||||
|
||||
const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => {
|
||||
runMediumPriorityTask(() => {
|
||||
function lazilyProcessFreshUpdates (instanceName, timelineName) {
|
||||
scheduleIdleTask(() => {
|
||||
/* no await */ processFreshUpdates(instanceName, timelineName)
|
||||
})
|
||||
}, STREAMING_THROTTLE_DELAY)
|
||||
}
|
||||
|
||||
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
||||
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
||||
|
@ -93,8 +105,8 @@ export function addStatusOrNotification (instanceName, timelineName, newStatusOr
|
|||
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
||||
console.log('addStatusesOrNotifications', Date.now())
|
||||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
||||
freshUpdates = [].concat(freshUpdates).concat(newStatusesOrNotifications)
|
||||
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
|
||||
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: freshUpdates})
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
|
||||
lazilyProcessFreshUpdates(instanceName, timelineName)
|
||||
}
|
|
@ -6,8 +6,8 @@ export async function insertUsername (realm, username, startIndex, endIndex) {
|
|||
let pre = oldText.substring(0, startIndex)
|
||||
let post = oldText.substring(endIndex)
|
||||
let newText = `${pre}@${username} ${post}`
|
||||
store.setComposeData(realm, {text: newText})
|
||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||
store.setComposeData(realm, { text: newText })
|
||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||
}
|
||||
|
||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||
|
@ -29,8 +29,8 @@ export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
|||
let pre = oldText.substring(0, startIndex)
|
||||
let post = oldText.substring(endIndex)
|
||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||
store.setComposeData(realm, {text: newText})
|
||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||
store.setComposeData(realm, { text: newText })
|
||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||
}
|
||||
|
||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
|
@ -1,18 +1,19 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockAccount, unblockAccount } from '../_api/block'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (block) {
|
||||
await blockAccount(currentInstance, accessToken, accountId)
|
||||
relationship = await blockAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
await unblockAccount(currentInstance, accessToken, accountId)
|
||||
relationship = await unblockAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateProfileAndRelationship(accountId)
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say('Blocked account')
|
|
@ -1,14 +1,14 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||
import { getStatus as getStatusFromDatabase } from '../_database/timelines/getStatusOrNotification'
|
||||
import { database } from '../_database/database'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { putMediaDescription } from '../_api/media'
|
||||
|
||||
export async function insertHandleForReply (statusId) {
|
||||
let { currentInstance } = store.get()
|
||||
let status = await getStatusFromDatabase(currentInstance, statusId)
|
||||
let status = await database.getStatus(currentInstance, statusId)
|
||||
let { currentVerifyCredentials } = store.get()
|
||||
let originalStatus = status.reblog || status
|
||||
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
|||
|
||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility,
|
||||
mediaDescriptions = [], inReplyToUuid) {
|
||||
mediaDescriptions, inReplyToUuid) {
|
||||
let { currentInstance, accessToken, online } = store.get()
|
||||
|
||||
if (!online) {
|
||||
|
@ -30,6 +30,9 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
return
|
||||
}
|
||||
|
||||
text = text || ''
|
||||
mediaDescriptions = mediaDescriptions || []
|
||||
|
||||
store.set({
|
||||
postingStatus: true
|
||||
})
|
||||
|
@ -46,7 +49,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
console.error(e)
|
||||
toast.say('Unable to post status: ' + (e.message || ''))
|
||||
} finally {
|
||||
store.set({postingStatus: false})
|
||||
store.set({ postingStatus: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,5 +84,5 @@ export function setReplyVisibility (realm, replyVisibility) {
|
|||
let visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
|
||||
? replyVisibility
|
||||
: defaultVisibility
|
||||
store.setComposeData(realm, {postPrivacy: visibility})
|
||||
store.setComposeData(realm, { postPrivacy: visibility })
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { importShowCopyDialog } from '../_components/dialog/asyncDialogs'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
|
||||
export async function copyText (text) {
|
||||
if (navigator.clipboard) { // not supported in all browsers
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
toast.say('Copied to clipboard')
|
||||
return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
let showCopyDialog = await importShowCopyDialog()
|
||||
showCopyDialog(text)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { database } from '../_database/database'
|
||||
|
||||
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
|
||||
return {
|
||||
timelineType,
|
||||
timelineValue,
|
||||
notification: await database.getNotification(instanceName, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
async function getStatus (instanceName, timelineType, timelineValue, itemId) {
|
||||
return {
|
||||
timelineType,
|
||||
timelineValue,
|
||||
status: await database.getStatus(instanceName, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||
let taskCount = 0
|
||||
let pending = []
|
||||
|
||||
// The worker-powered indexeddb promises can resolve in arbitrary order,
|
||||
// causing the timeline to load in a jerky way. With this function, we
|
||||
// wait for all promises to resolve before resolving them all in one go.
|
||||
function awaitAllTasksComplete () {
|
||||
return new Promise(resolve => {
|
||||
taskCount--
|
||||
pending.push(resolve)
|
||||
if (taskCount === 0) {
|
||||
pending.forEach(_ => _())
|
||||
pending = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (itemId) => {
|
||||
taskCount++
|
||||
let promise = timelineType === 'notifications'
|
||||
? getNotification(instanceName, timelineType, timelineValue, itemId)
|
||||
: getStatus(instanceName, timelineType, timelineValue, itemId)
|
||||
|
||||
return promise.then(res => {
|
||||
return awaitAllTasksComplete().then(() => res)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { store } from '../_store/store'
|
||||
import { deleteStatus } from '../_api/delete'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
|
||||
|
||||
export async function doDeleteStatus (statusId) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
await deleteStatus(currentInstance, accessToken, statusId)
|
||||
deleteStatusLocally(currentInstance, statusId)
|
||||
toast.say('Status deleted.')
|
||||
} catch (e) {
|
||||
console.error(e)
|
|
@ -0,0 +1,24 @@
|
|||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
|
||||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'
|
||||
import { doDeleteStatus } from './delete'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
export async function deleteAndRedraft (status) {
|
||||
let deleteStatusPromise = doDeleteStatus(status.id)
|
||||
let dialogPromise = importShowComposeDialog()
|
||||
await deleteStatusPromise
|
||||
|
||||
store.setComposeData('dialog', {
|
||||
text: statusHtmlToPlainText(status.content, status.mentions),
|
||||
contentWarningShown: !!status.spoiler_text,
|
||||
contentWarning: status.spoiler_text || '',
|
||||
postPrivacy: status.visibility,
|
||||
media: status.media_attachments && status.media_attachments.map(_ => ({
|
||||
description: _.description || '',
|
||||
data: _
|
||||
})),
|
||||
inReplyToId: status.in_reply_to_id
|
||||
})
|
||||
let showComposeDialog = await dialogPromise
|
||||
showComposeDialog()
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||
import { store } from '../_store/store'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import {
|
||||
deleteStatusesAndNotifications as deleteStatusesAndNotificationsFromDatabase
|
||||
} from '../_database/timelines/deletion'
|
||||
import { database } from '../_database/database'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
|
||||
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
||||
|
@ -18,6 +16,7 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
|||
}
|
||||
let filteredIds = ids.filter(idFilter)
|
||||
if (!isEqual(ids, filteredIds)) {
|
||||
console.log('deleting an item from timelineName', timelineName, 'for key', key)
|
||||
store.setForTimeline(instanceName, timelineName, {
|
||||
[key]: filteredIds
|
||||
})
|
||||
|
@ -45,7 +44,7 @@ function deleteNotificationIdsFromStore (instanceName, idsToDelete) {
|
|||
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
||||
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
||||
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
||||
await deleteStatusesAndNotificationsFromDatabase(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||
await database.deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||
}
|
||||
|
||||
async function doDeleteStatus (instanceName, statusId) {
|
|
@ -1,30 +1,28 @@
|
|||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import {
|
||||
getCustomEmoji as getCustomEmojiFromDatabase,
|
||||
setCustomEmoji as setCustomEmojiInDatabase
|
||||
} from '../_database/meta'
|
||||
import { database } from '../_database/database'
|
||||
import { getCustomEmoji } from '../_api/emoji'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
export async function updateCustomEmojiForInstance (instanceName) {
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getCustomEmoji(instanceName),
|
||||
() => getCustomEmojiFromDatabase(instanceName),
|
||||
emoji => setCustomEmojiInDatabase(instanceName, emoji),
|
||||
() => database.getCustomEmoji(instanceName),
|
||||
emoji => database.setCustomEmoji(instanceName, emoji),
|
||||
emoji => {
|
||||
let { customEmoji } = store.get()
|
||||
customEmoji[instanceName] = emoji
|
||||
store.set({customEmoji: customEmoji})
|
||||
store.set({ customEmoji: customEmoji })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function insertEmoji (realm, emoji) {
|
||||
let emojiText = emoji.custom ? emoji.colons : emoji.native
|
||||
let { composeSelectionStart } = store.get()
|
||||
let idx = composeSelectionStart || 0
|
||||
let oldText = store.getComposeData(realm, 'text') || ''
|
||||
let pre = oldText.substring(0, idx)
|
||||
let post = oldText.substring(idx)
|
||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||
store.setComposeData(realm, {text: newText})
|
||||
let newText = `${pre}${emojiText} ${post}`
|
||||
store.setComposeData(realm, { text: newText })
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_utils/toast'
|
||||
import {
|
||||
setStatusFavorited as setStatusFavoritedInDatabase
|
||||
} from '../_database/timelines/updateStatus'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export async function setFavorited (statusId, favorited) {
|
||||
let { online } = store.get()
|
||||
|
@ -18,7 +16,7 @@ export async function setFavorited (statusId, favorited) {
|
|||
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
||||
try {
|
||||
await networkPromise
|
||||
await setStatusFavoritedInDatabase(currentInstance, statusId, favorited)
|
||||
await database.setStatusFavorited(currentInstance, statusId, favorited)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
|
|
@ -0,0 +1,27 @@
|
|||
import { store } from '../_store/store'
|
||||
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
|
||||
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (follow) {
|
||||
relationship = await followAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
relationship = await unfollowAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (follow) {
|
||||
toast.say('Followed account')
|
||||
} else {
|
||||
toast.say('Unfollowed account')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
|
||||
}
|
||||
}
|
|
@ -3,15 +3,15 @@ import { auth, basename } from '../_api/utils'
|
|||
|
||||
export async function getFollowRequests (instanceName, accessToken) {
|
||||
let url = `${basename(instanceName)}/api/v1/follow_requests`
|
||||
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
||||
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
||||
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
|
@ -1,22 +1,16 @@
|
|||
import { getVerifyCredentials } from '../_api/user'
|
||||
import { store } from '../_store/store'
|
||||
import { switchToTheme } from '../_utils/themeEngine'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { getInstanceInfo } from '../_api/instance'
|
||||
import { clearDatabaseForInstance } from '../_database/clear'
|
||||
import {
|
||||
getInstanceVerifyCredentials as getInstanceVerifyCredentialsFromDatabase,
|
||||
setInstanceVerifyCredentials as setInstanceVerifyCredentialsInDatabase,
|
||||
getInstanceInfo as getInstanceInfoFromDatabase,
|
||||
setInstanceInfo as setInstanceInfoInDatabase
|
||||
} from '../_database/meta'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export function changeTheme (instanceName, newTheme) {
|
||||
let { instanceThemes } = store.get()
|
||||
instanceThemes[instanceName] = newTheme
|
||||
store.set({instanceThemes: instanceThemes})
|
||||
store.set({ instanceThemes: instanceThemes })
|
||||
store.save()
|
||||
let { currentInstance } = store.get()
|
||||
if (instanceName === currentInstance) {
|
||||
|
@ -61,15 +55,15 @@ export async function logOutOfInstance (instanceName) {
|
|||
})
|
||||
store.save()
|
||||
toast.say(`Logged out of ${instanceName}`)
|
||||
switchToTheme(instanceThemes[newInstance] || 'default')
|
||||
await clearDatabaseForInstance(instanceName)
|
||||
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
|
||||
/* no await */ database.clearDatabaseForInstance(instanceName)
|
||||
goto('/settings/instances')
|
||||
}
|
||||
|
||||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||
let { verifyCredentials } = store.get()
|
||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||
store.set({verifyCredentials: verifyCredentials})
|
||||
store.set({ verifyCredentials: verifyCredentials })
|
||||
}
|
||||
|
||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||
|
@ -77,8 +71,8 @@ export async function updateVerifyCredentialsForInstance (instanceName) {
|
|||
let accessToken = loggedInInstances[instanceName].access_token
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getVerifyCredentials(instanceName, accessToken),
|
||||
() => getInstanceVerifyCredentialsFromDatabase(instanceName),
|
||||
verifyCredentials => setInstanceVerifyCredentialsInDatabase(instanceName, verifyCredentials),
|
||||
() => database.getInstanceVerifyCredentials(instanceName),
|
||||
verifyCredentials => database.setInstanceVerifyCredentials(instanceName, verifyCredentials),
|
||||
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
||||
)
|
||||
}
|
||||
|
@ -91,12 +85,12 @@ export async function updateVerifyCredentialsForCurrentInstance () {
|
|||
export async function updateInstanceInfo (instanceName) {
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getInstanceInfo(instanceName),
|
||||
() => getInstanceInfoFromDatabase(instanceName),
|
||||
info => setInstanceInfoInDatabase(instanceName, info),
|
||||
() => database.getInstanceInfo(instanceName),
|
||||
info => database.setInstanceInfo(instanceName, info),
|
||||
info => {
|
||||
let { instanceInfos } = store.get()
|
||||
instanceInfos[instanceName] = info
|
||||
store.set({instanceInfos: instanceInfos})
|
||||
store.set({ instanceInfos: instanceInfos })
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { store } from '../_store/store'
|
||||
import { getLists } from '../_api/lists'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export async function updateListsForInstance (instanceName) {
|
||||
let { loggedInInstances } = store.get()
|
||||
let accessToken = loggedInInstances[instanceName].access_token
|
||||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getLists(instanceName, accessToken),
|
||||
() => database.getLists(instanceName),
|
||||
lists => database.setLists(instanceName, lists),
|
||||
lists => {
|
||||
let { instanceLists } = store.get()
|
||||
instanceLists[instanceName] = lists
|
||||
store.set({ instanceLists: instanceLists })
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,49 +1,40 @@
|
|||
import { store } from '../_store/store'
|
||||
import { uploadMedia } from '../_api/media'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
|
||||
export async function doMediaUpload (realm, file) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
store.set({uploadingMedia: true})
|
||||
store.set({ uploadingMedia: true })
|
||||
try {
|
||||
let response = await uploadMedia(currentInstance, accessToken, file)
|
||||
let composeMedia = store.getComposeData(realm, 'media') || []
|
||||
if (composeMedia.length === 4) {
|
||||
throw new Error('Only 4 media max are allowed')
|
||||
}
|
||||
composeMedia.push({
|
||||
data: response,
|
||||
file: { name: file.name }
|
||||
file: { name: file.name },
|
||||
description: ''
|
||||
})
|
||||
let composeText = store.getComposeData(realm, 'text') || ''
|
||||
composeText += ' ' + response.text_url
|
||||
store.setComposeData(realm, {
|
||||
media: composeMedia,
|
||||
text: composeText
|
||||
media: composeMedia
|
||||
})
|
||||
scheduleIdleTask(() => store.save())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Failed to upload media: ' + (e.message || ''))
|
||||
} finally {
|
||||
store.set({uploadingMedia: false})
|
||||
store.set({ uploadingMedia: false })
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteMedia (realm, i) {
|
||||
let composeMedia = store.getComposeData(realm, 'media')
|
||||
let deletedMedia = composeMedia.splice(i, 1)[0]
|
||||
|
||||
let composeText = store.getComposeData(realm, 'text') || ''
|
||||
composeText = composeText.replace(' ' + deletedMedia.data.text_url, '')
|
||||
|
||||
let mediaDescriptions = store.getComposeData(realm, 'mediaDescriptions') || []
|
||||
if (mediaDescriptions[i]) {
|
||||
mediaDescriptions[i] = null
|
||||
}
|
||||
composeMedia.splice(i, 1)
|
||||
|
||||
store.setComposeData(realm, {
|
||||
media: composeMedia,
|
||||
text: composeText,
|
||||
mediaDescriptions: mediaDescriptions
|
||||
media: composeMedia
|
||||
})
|
||||
scheduleIdleTask(() => store.save())
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'
|
||||
import { store } from '../_store/store'
|
||||
|
||||
export async function composeNewStatusMentioning (account) {
|
||||
store.setComposeData('dialog', { text: `@${account.acct} ` })
|
||||
let showComposeDialog = await importShowComposeDialog()
|
||||
showComposeDialog()
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (mute) {
|
||||
await muteAccount(currentInstance, accessToken, accountId)
|
||||
relationship = await muteAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
await unmuteAccount(currentInstance, accessToken, accountId)
|
||||
relationship = await unmuteAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateProfileAndRelationship(accountId)
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted account')
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { setStatusMuted as setStatusMutedInDatabase } from '../_database/timelines/updateStatus'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
@ -11,7 +11,7 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
|||
} else {
|
||||
await unmuteConversation(currentInstance, accessToken, statusId)
|
||||
}
|
||||
await setStatusMutedInDatabase(currentInstance, statusId, mute)
|
||||
await database.setStatusMuted(currentInstance, statusId, mute)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted conversation')
|
|
@ -0,0 +1,11 @@
|
|||
// When the user is logged out, we need to be sure to re-show all the "hidden from SSR" styles
|
||||
// so that we don't get a blank page.
|
||||
export function onUserIsLoggedOut () {
|
||||
if (document.getElementById('hiddenFromSsrStyle')) {
|
||||
return
|
||||
}
|
||||
let style = document.createElement('style')
|
||||
style.setAttribute('id', 'hiddenFromSsrStyle')
|
||||
style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
|
||||
document.head.appendChild(style)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { pinStatus, unpinStatus } from '../_api/pin'
|
||||
import { setStatusPinned as setStatusPinnedInDatabase } from '../_database/timelines/updateStatus'
|
||||
import { database } from '../_database/database'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
||||
|
@ -19,7 +19,8 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
|
|||
toast.say('Unpinned status')
|
||||
}
|
||||
}
|
||||
await setStatusPinnedInDatabase(currentInstance, statusId, pinned)
|
||||
store.setStatusPinned(currentInstance, statusId, pinned)
|
||||
await database.setStatusPinned(currentInstance, statusId, pinned)
|
||||
emit('updatePinnedStatuses')
|
||||
} catch (e) {
|
||||
console.error(e)
|
|
@ -1,9 +1,6 @@
|
|||
import { store } from '../_store/store'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import {
|
||||
getPinnedStatuses as getPinnedStatusesFromDatabase,
|
||||
insertPinnedStatuses as insertPinnedStatusesInDatabase
|
||||
} from '../_database/timelines/pinnedStatuses'
|
||||
import { database } from '../_database/database'
|
||||
import {
|
||||
getPinnedStatuses
|
||||
} from '../_api/pinnedStatuses'
|
||||
|
@ -13,13 +10,13 @@ export async function updatePinnedStatusesForAccount (accountId) {
|
|||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||
() => getPinnedStatusesFromDatabase(currentInstance, accountId),
|
||||
statuses => insertPinnedStatusesInDatabase(currentInstance, accountId, statuses),
|
||||
() => database.getPinnedStatuses(currentInstance, accountId),
|
||||
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
||||
statuses => {
|
||||
let { pinnedStatuses } = store.get()
|
||||
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
||||
pinnedStatuses[currentInstance][accountId] = statuses
|
||||
store.set({pinnedStatuses: pinnedStatuses})
|
||||
store.set({ pinnedStatuses: pinnedStatuses })
|
||||
}
|
||||
)
|
||||
}
|
|
@ -2,5 +2,5 @@
|
|||
import { store } from '../_store/store'
|
||||
|
||||
export function setPostPrivacy (realm, postPrivacyKey) {
|
||||
store.setComposeData(realm, {postPrivacy: postPrivacyKey})
|
||||
store.setComposeData(realm, { postPrivacy: postPrivacyKey })
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription'
|
||||
import { store } from '../_store/store'
|
||||
import { urlBase64ToUint8Array } from '../_utils/base64'
|
||||
|
||||
const dummyApplicationServerKey = 'BImgAz4cF_yvNFp8uoBJCaGpCX4d0atNIFMHfBvAAXCyrnn9IMAFQ10DW_ZvBCzGeR4fZI5FnEi2JVcRE-L88jY='
|
||||
|
||||
export async function updatePushSubscriptionForInstance (instanceName) {
|
||||
const { loggedInInstances, pushSubscription } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
|
||||
if (pushSubscription === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (subscription === null) {
|
||||
store.set({ pushSubscription: null })
|
||||
store.save()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const backendSubscription = await getSubscription(instanceName, accessToken)
|
||||
|
||||
// Check if applicationServerKey changed (need to get another subscription from the browser)
|
||||
if (btoa(urlBase64ToUint8Array(backendSubscription.server_key).buffer) !== btoa(subscription.options.applicationServerKey)) {
|
||||
await subscription.unsubscribe()
|
||||
await deleteSubscription(instanceName, accessToken)
|
||||
await updateAlerts(instanceName, pushSubscription.alerts)
|
||||
} else {
|
||||
store.set({ pushSubscription: backendSubscription })
|
||||
store.save()
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: Better way to detect 404
|
||||
if (e.message.startsWith('404:')) {
|
||||
await subscription.unsubscribe()
|
||||
store.set({ pushSubscription: null })
|
||||
store.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAlerts (instanceName, alerts) {
|
||||
const { loggedInInstances } = store.get()
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
let subscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (subscription === null) {
|
||||
// We need applicationServerKey in order to register a push subscription
|
||||
// but the API doesn't expose it as a constant (as it should).
|
||||
// So we need to register a subscription with a dummy applicationServerKey,
|
||||
// send it to the backend saves it and return applicationServerKey, which
|
||||
// we use to register a new subscription.
|
||||
// https://github.com/tootsuite/mastodon/issues/8785
|
||||
subscription = await registration.pushManager.subscribe({
|
||||
applicationServerKey: urlBase64ToUint8Array(dummyApplicationServerKey),
|
||||
userVisibleOnly: true
|
||||
})
|
||||
|
||||
let backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
|
||||
|
||||
await subscription.unsubscribe()
|
||||
|
||||
subscription = await registration.pushManager.subscribe({
|
||||
applicationServerKey: urlBase64ToUint8Array(backendSubscription.server_key),
|
||||
userVisibleOnly: true
|
||||
})
|
||||
|
||||
backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
|
||||
|
||||
store.set({ pushSubscription: backendSubscription })
|
||||
store.save()
|
||||
} else {
|
||||
try {
|
||||
const backendSubscription = await putSubscription(instanceName, accessToken, alerts)
|
||||
store.set({ pushSubscription: backendSubscription })
|
||||
store.save()
|
||||
} catch (e) {
|
||||
const backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
|
||||
store.set({ pushSubscription: backendSubscription })
|
||||
store.save()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
||||
import { setStatusReblogged as setStatusRebloggedInDatabase } from '../_database/timelines/updateStatus'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export async function setReblogged (statusId, reblogged) {
|
||||
let online = store.get()
|
||||
|
@ -16,7 +16,7 @@ export async function setReblogged (statusId, reblogged) {
|
|||
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
||||
try {
|
||||
await networkPromise
|
||||
await setStatusRebloggedInDatabase(currentInstance, statusId, reblogged)
|
||||
await database.setStatusReblogged(currentInstance, statusId, reblogged)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
|
||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||
let {
|
|
@ -1,10 +1,10 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { search } from '../_api/search'
|
||||
|
||||
export async function doSearch () {
|
||||
let { currentInstance, accessToken, queryInSearch } = store.get()
|
||||
store.set({searchLoading: true})
|
||||
store.set({ searchLoading: true })
|
||||
try {
|
||||
let results = await search(currentInstance, accessToken, queryInSearch)
|
||||
let { queryInSearch: newQueryInSearch } = store.get() // avoid race conditions
|
||||
|
@ -18,6 +18,6 @@ export async function doSearch () {
|
|||
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||
console.error(e)
|
||||
} finally {
|
||||
store.set({searchLoading: false})
|
||||
store.set({ searchLoading: false })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockDomain, unblockDomain } from '../_api/blockDomain'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateRelationship } from './accounts'
|
||||
|
||||
export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
if (block) {
|
||||
await blockDomain(currentInstance, accessToken, domain)
|
||||
} else {
|
||||
await unblockDomain(currentInstance, accessToken, domain)
|
||||
}
|
||||
await updateRelationship(accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say(`Hiding ${domain}`)
|
||||
} else {
|
||||
toast.say(`Unhiding ${domain}`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${block ? 'hide' : 'unhide'} domain: ` + (e.message || ''))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { store } from '../_store/store'
|
||||
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
|
||||
export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship = await setShowReblogsApi(currentInstance, accessToken, accountId, showReblogs)
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
if (toastOnSuccess) {
|
||||
if (showReblogs) {
|
||||
toast.say('Showing boosts')
|
||||
} else {
|
||||
toast.say('Hiding boosts')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${showReblogs ? 'show' : 'hide'} boosts: ` + (e.message || ''))
|
||||
}
|
||||
}
|
|
@ -1,13 +1,7 @@
|
|||
import {
|
||||
getNotificationIdsForStatuses as getNotificationIdsForStatusesFromDatabase,
|
||||
getReblogsForStatus as getReblogsForStatusFromDatabase
|
||||
} from '../_database/timelines/lookup'
|
||||
import {
|
||||
getStatus as getStatusFromDatabase
|
||||
} from '../_database/timelines/getStatusOrNotification'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
|
||||
let status = await getStatusFromDatabase(instanceName, statusId)
|
||||
let status = await database.getStatus(instanceName, statusId)
|
||||
return status.reblog && status.reblog.id
|
||||
}
|
||||
|
||||
|
@ -19,9 +13,9 @@ export async function getIdsThatTheseStatusesReblogged (instanceName, statusIds)
|
|||
}
|
||||
|
||||
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
||||
return getReblogsForStatusFromDatabase(instanceName, statusId)
|
||||
return database.getReblogsForStatus(instanceName, statusId)
|
||||
}
|
||||
|
||||
export async function getNotificationIdsForStatuses (instanceName, statusIds) {
|
||||
return getNotificationIdsForStatusesFromDatabase(instanceName, statusIds)
|
||||
return database.getNotificationIdsForStatuses(instanceName, statusIds)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue