forked from cybrespace/pinafore
Compare commits
372 Commits
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
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
.sapper
|
/__sapper__
|
||||||
yarn.lock
|
|
||||||
templates/.*
|
|
||||||
assets/*.css
|
|
||||||
/mastodon
|
/mastodon
|
||||||
mastodon.log
|
/mastodon.log
|
||||||
assets/robots.txt
|
/src/template.html
|
||||||
/inline-script-checksum.json
|
/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
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "8"
|
- "10"
|
||||||
dist: trusty # needed for chrome headless
|
dist: trusty # needed for chrome headless
|
||||||
sudo: required # needed for chrome headless
|
sudo: required # needed for various sudo operations
|
||||||
addons:
|
addons:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
postgresql: "10"
|
postgresql: "10"
|
||||||
apt:
|
apt:
|
||||||
packages:
|
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
|
- autoconf
|
||||||
- bison
|
- bison
|
||||||
- build-essential
|
- build-essential
|
||||||
- libssl-dev
|
- file
|
||||||
- libyaml-dev
|
- g++
|
||||||
- libreadline6-dev
|
- gcc
|
||||||
- zlib1g-dev
|
- imagemagick
|
||||||
- libncurses5-dev
|
|
||||||
- libffi-dev
|
- libffi-dev
|
||||||
- libgdbm3
|
|
||||||
- libgdbm-dev
|
- libgdbm-dev
|
||||||
- redis-tools
|
- libgdbm3
|
||||||
- libidn11-dev
|
|
||||||
- libicu-dev
|
- libicu-dev
|
||||||
services:
|
- libidn11-dev
|
||||||
- redis-server
|
- 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:
|
before_install:
|
||||||
- npm install -g npm@5
|
|
||||||
- npm install -g greenkeeper-lockfile@1
|
- 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
|
- ./bin/setup-mastodon-in-travis.sh
|
||||||
before_script:
|
before_script:
|
||||||
- npm run lint
|
- yarn run lint
|
||||||
- greenkeeper-lockfile-update
|
- greenkeeper-lockfile-update
|
||||||
after_script:
|
after_script:
|
||||||
- greenkeeper-lockfile-upload
|
- greenkeeper-lockfile-upload
|
||||||
install:
|
script: travis_retry yarn run $COMMAND
|
||||||
- npm ci || npm i
|
|
||||||
script: travis_retry npm run $COMMAND
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- PGPORT=5433
|
- PGPORT=5433
|
||||||
|
@ -59,16 +57,17 @@ matrix:
|
||||||
include:
|
include:
|
||||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
||||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
||||||
- env: COMMAND=deploy-dev-travis
|
- env: COMMAND=test-unit
|
||||||
|
- env: COMMAND=deploy-all-travis
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: COMMAND=deploy-dev-travis
|
- env: COMMAND=deploy-all-travis
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- /^greenkeeper/.*$/
|
- /^greenkeeper/.*$/
|
||||||
cache:
|
cache:
|
||||||
|
yarn: true
|
||||||
|
bundler: true
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.npm
|
- /home/travis/.rvm/
|
||||||
- $HOME/.rvm
|
- /home/travis/ffmpeg-static/
|
||||||
- $HOME/.bundle
|
|
||||||
- $HOME/.yarn-cache
|
|
||||||
|
|
|
@ -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
|
# Contributing to Pinafore
|
||||||
|
|
||||||
## Caveats
|
## Dev server
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
To run a dev server with hot reloading:
|
To run a dev server with hot reloading:
|
||||||
|
|
||||||
npm run dev
|
yarn run dev
|
||||||
|
|
||||||
Now it's running at `localhost:4002`.
|
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
|
## Linting
|
||||||
|
|
||||||
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
||||||
|
|
||||||
Lint:
|
Lint:
|
||||||
|
|
||||||
npm run lint
|
yarn run lint
|
||||||
|
|
||||||
Automatically fix most linting issues:
|
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:
|
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:
|
Run tests for a particular browser:
|
||||||
|
|
||||||
BROWSER=chrome npm run test-browser
|
BROWSER=chrome yarn run test-browser
|
||||||
BROWSER=chrome:headless npm run test-browser
|
BROWSER=chrome:headless yarn run test-browser
|
||||||
BROWSER=firefox npm run test-browser
|
BROWSER=firefox yarn run test-browser
|
||||||
BROWSER=firefox:headless npm run test-browser
|
BROWSER=firefox:headless yarn run test-browser
|
||||||
BROWSER=safari npm run test-browser
|
BROWSER=safari yarn run test-browser
|
||||||
BROWSER=edge npm 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:
|
In separate terminals:
|
||||||
|
|
||||||
1\. Run a Mastodon dev server:
|
1\. Run a Mastodon dev server:
|
||||||
|
|
||||||
npm run run-mastodon
|
yarn run run-mastodon
|
||||||
|
|
||||||
2\. Run a Pinafore dev server:
|
2\. Run a Pinafore dev server:
|
||||||
|
|
||||||
npm run dev
|
yarn run dev
|
||||||
|
|
||||||
3\. Run a debuggable TestCafé instance:
|
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,
|
### Test conventions
|
||||||
so that it can be loaded later, run:
|
|
||||||
|
|
||||||
npm run backup-mastodon-data
|
The tests have a naming convention:
|
||||||
|
|
||||||
## Writing tests
|
* `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)
|
||||||
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
|
|
||||||
|
|
||||||
In principle the `0-` tests don't have to worry about
|
In principle the `0-` tests don't have to worry about
|
||||||
clobbering each other, whereas the `1-` ones do.
|
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
|
## Debugging Webpack
|
||||||
|
|
||||||
The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicly via e.g.:
|
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/report.html](https://dev.pinafore.social/report.html)
|
||||||
- [dev.pinafore.social/stats.json](https://dev.pinafore.social/stats.json)
|
- [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
|
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
||||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js` to avoid actually populating the database with statuses/favorites/etc.
|
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
||||||
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
|
|
||||||
|
|
||||||
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
|
# Install updates and NodeJS+Dependencies
|
||||||
RUN apk update && apk upgrade
|
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
|
# Install yarn
|
||||||
RUN npm i npm@latest -g
|
RUN npm i yarn -g
|
||||||
|
|
||||||
# Install Pinafore
|
# Install Pinafore
|
||||||
RUN npm install
|
RUN yarn --pure-lockfile
|
||||||
RUN npm run build
|
RUN yarn build
|
||||||
|
|
||||||
# Expose port 4002
|
# Expose port 4002
|
||||||
EXPOSE 4002
|
EXPOSE 4002
|
||||||
|
|
||||||
# Setting run-command
|
# 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.
|
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
|
## Browser support
|
||||||
|
|
||||||
|
@ -24,51 +24,68 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
||||||
### Goals
|
### Goals
|
||||||
|
|
||||||
- Support the most common use cases
|
- Support the most common use cases
|
||||||
- Fast even on low-end phones
|
- Small page weight
|
||||||
- Works offline in read-only mode
|
- Fast even on low-end devices
|
||||||
|
- Accessibility
|
||||||
|
- Offline support in read-only mode
|
||||||
- Progressive Web App features
|
- Progressive Web App features
|
||||||
- Multi-instance support
|
- Multi-instance support
|
||||||
- Support latest versions of Chrome, Edge, Firefox, and Safari
|
- 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
|
- Support for Pleroma or other non-Mastodon backends
|
||||||
- Android/iOS apps (using Cordova or similar)
|
- Serve as an alternative frontend tied to a particular instance
|
||||||
- Support Pleroma/non-Mastodon backends
|
- Support for non-English languages (i18n)
|
||||||
- i18n
|
|
||||||
- Offline search
|
- Offline search
|
||||||
- Full emoji keyboard
|
|
||||||
- Keyboard shortcuts
|
|
||||||
|
|
||||||
### Non-goals
|
### Non-goals
|
||||||
|
|
||||||
- Supporting old browsers, proxy browsers, or text-based browsers
|
- Supporting old browsers, proxy browsers, or text-based browsers
|
||||||
- React Native / NativeScript / hybrid-native version
|
- React Native / NativeScript / hybrid-native version
|
||||||
|
- Android/iOS apps (using Cordova or similar)
|
||||||
- Full functionality with JavaScript disabled
|
- Full functionality with JavaScript disabled
|
||||||
- Emoji support beyond the built-in system emoji
|
- Emoji support beyond the built-in system emoji
|
||||||
- Multi-column support
|
- Multi-column support
|
||||||
- Admin/moderation panel
|
- 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
|
## Building
|
||||||
|
|
||||||
|
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
|
||||||
|
|
||||||
To build Pinafore for production:
|
To build Pinafore for production:
|
||||||
|
|
||||||
npm install
|
yarn --pure-lockfile
|
||||||
npm run build
|
yarn build
|
||||||
PORT=4002 npm start
|
PORT=4002 yarn start
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
To build a docker image for production:
|
To build a Docker image for production:
|
||||||
|
|
||||||
docker build .
|
docker build .
|
||||||
docker run -d -p 4002:4002 [your-image]
|
docker run -d -p 4002:4002 [your-image]
|
||||||
|
|
||||||
Now Pinafore is running at `localhost:4002`.
|
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
|
## Developing and testing
|
||||||
|
|
||||||
|
@ -78,3 +95,9 @@ how to run Pinafore in dev mode and run tests.
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
For a changelog, see the [GitHub releases](http://github.com/nolanlawson/pinafore/releases/).
|
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 writeFile = promisify(fs.writeFile)
|
||||||
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')
|
|
||||||
|
|
||||||
async function main () {
|
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
|
||||||
let headScriptFilepath = path.join(__dirname, '../inline-script.js')
|
|
||||||
let headScript = await readFile(headScriptFilepath, 'utf8')
|
|
||||||
headScript = `(function () {'use strict'; ${headScript}})()`
|
|
||||||
|
|
||||||
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')
|
let bundle = await rollup({
|
||||||
await writeFile(checksumFilepath, JSON.stringify({checksum}), 'utf8')
|
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 { code, map } = output[0]
|
||||||
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
|
||||||
html2xxFile = html2xxFile.replace(
|
let fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
|
||||||
/<!-- insert inline script here -->[\s\S]+<!-- end insert inline script here -->/,
|
let checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
|
||||||
'<!-- insert inline script here --><script>' + headScript + '</script><!-- end insert inline script here -->'
|
|
||||||
)
|
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
|
||||||
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
|
`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 writeFile = promisify(fs.writeFile)
|
||||||
const chokidar = require('chokidar')
|
const readdir = promisify(fs.readdir)
|
||||||
const argv = require('yargs').argv
|
const render = promisify(sass.render.bind(sass))
|
||||||
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 globalScss = path.join(__dirname, '../scss/global.scss')
|
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
||||||
const defaultThemeScss = path.join(__dirname, '../scss/themes/_default.scss')
|
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
|
||||||
const offlineThemeScss = path.join(__dirname, '../scss/themes/_offline.scss')
|
const offlineThemeScss = path.join(__dirname, '../src/scss/themes/_offline.scss')
|
||||||
const html2xxFile = path.join(__dirname, '../templates/2xx.html')
|
const customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
|
||||||
const scssDir = path.join(__dirname, '../scss')
|
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
||||||
const themesScssDir = path.join(__dirname, '../scss/themes')
|
const assetsDir = path.join(__dirname, '../static')
|
||||||
const assetsDir = path.join(__dirname, '../assets')
|
|
||||||
|
|
||||||
function doWatch () {
|
async function renderCss (file) {
|
||||||
let start = now()
|
return (await render({ file, outputStyle: 'compressed' })).css
|
||||||
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 compileGlobalSass () {
|
async function compileGlobalSass () {
|
||||||
let results = await Promise.all([
|
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
||||||
render({file: defaultThemeScss, outputStyle: 'compressed'}),
|
let offlineStyle = (await renderCss(offlineThemeScss))
|
||||||
render({file: globalScss, outputStyle: 'compressed'}),
|
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
||||||
render({file: offlineThemeScss, outputStyle: 'compressed'})
|
|
||||||
])
|
|
||||||
|
|
||||||
let css = results.map(_ => _.css).join('')
|
return `<style>\n${mainStyle}</style>\n` +
|
||||||
|
`<style media="only x" id="theOfflineStyle">\n${offlineStyle}</style>\n` +
|
||||||
let html = await readFile(html2xxFile, 'utf8')
|
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
|
||||||
html = html.replace(/<style>[\s\S]+?<\/style>/,
|
|
||||||
`<style>\n/* auto-generated w/ build-sass.js */\n${css}\n</style>`)
|
|
||||||
|
|
||||||
await writeFile(html2xxFile, html, 'utf8')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileThemesSass () {
|
async function compileThemesSass () {
|
||||||
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
||||||
await Promise.all(files.map(async file => {
|
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')
|
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 () {
|
export async function buildSass () {
|
||||||
await Promise.all([compileGlobalSass(), compileThemesSass()])
|
let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||||
if (argv.watch) {
|
return result
|
||||||
doWatch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 svgo = new SVGO()
|
||||||
const $ = require('cheerio')
|
const readFile = promisify(fs.readFile)
|
||||||
|
|
||||||
const readFile = pify(fs.readFile.bind(fs))
|
export async function buildSvg () {
|
||||||
const writeFile = pify(fs.writeFile.bind(fs))
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
let result = (await Promise.all(svgs.map(async svg => {
|
let result = (await Promise.all(svgs.map(async svg => {
|
||||||
let filepath = path.join(__dirname, '../', svg.src)
|
let filepath = path.join(__dirname, '../', svg.src)
|
||||||
let content = await readFile(filepath, 'utf8')
|
let content = await readFile(filepath, 'utf8')
|
||||||
|
@ -21,23 +18,9 @@ async function main () {
|
||||||
let $symbol = $('<symbol></symbol>')
|
let $symbol = $('<symbol></symbol>')
|
||||||
.attr('id', svg.id)
|
.attr('id', svg.id)
|
||||||
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
||||||
.append($('<title></title>').text(svg.title))
|
|
||||||
.append($path)
|
.append($path)
|
||||||
return $.xml($symbol)
|
return $.xml($symbol)
|
||||||
}))).join('\n')
|
}))).join('\n')
|
||||||
|
|
||||||
result = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">\n${result}\n</svg>`
|
return `<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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { actions } from './mastodon-data'
|
||||||
import { users } from '../tests/users'
|
import { users } from '../tests/users'
|
||||||
import { postStatus } from '../routes/_api/statuses'
|
import { postStatus } from '../src/routes/_api/statuses'
|
||||||
import { followAccount } from '../routes/_api/follow'
|
import { followAccount } from '../src/routes/_api/follow'
|
||||||
import { favoriteStatus } from '../routes/_api/favorite'
|
import { favoriteStatus } from '../src/routes/_api/favorite'
|
||||||
import { reblogStatus } from '../routes/_api/reblog'
|
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import FileApi from 'file-api'
|
import FileApi from 'file-api'
|
||||||
import path from 'path'
|
import { pinStatus } from '../src/routes/_api/pin'
|
||||||
import fs from 'fs'
|
import { submitMedia } from '../tests/submitMedia'
|
||||||
import FormData from 'form-data'
|
|
||||||
import { auth } from '../routes/_api/utils'
|
|
||||||
import { pinStatus } from '../routes/_api/pin'
|
|
||||||
|
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
global.FormData = FileApi.FormData
|
global.FormData = FileApi.FormData
|
||||||
global.fetch = fetch
|
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 () {
|
export async function restoreMastodonData () {
|
||||||
console.log('Restoring mastodon data...')
|
console.log('Restoring mastodon data...')
|
||||||
let internalIdsToIds = {}
|
let internalIdsToIds = {}
|
||||||
for (let action of actions) {
|
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))
|
console.log(JSON.stringify(action))
|
||||||
let accessToken = users[action.user].accessToken
|
let accessToken = users[action.user].accessToken
|
||||||
|
|
||||||
if (action.post) {
|
if (action.post) {
|
||||||
let { text, media, sensitive, spoiler, privacy, inReplyTo, internalId } = 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 mediaIds = media && await Promise.all(media.map(async mediaItem => {
|
||||||
let mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
let mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
||||||
return mediaResponse.id
|
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')
|
sensitive, spoiler, privacy || 'public')
|
||||||
if (typeof internalId !== 'undefined') {
|
if (typeof internalId !== 'undefined') {
|
||||||
internalIdsToIds[internalId] = status.id
|
internalIdsToIds[internalId] = status.id
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { restoreMastodonData } from './restore-mastodon-data'
|
import { restoreMastodonData } from './restore-mastodon-data'
|
||||||
import pify from 'pify'
|
import { promisify } from 'util'
|
||||||
import childProcessPromise from 'child-process-promise'
|
import childProcessPromise from 'child-process-promise'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
@ -8,13 +8,13 @@ import mkdirpCB from 'mkdirp'
|
||||||
|
|
||||||
const exec = childProcessPromise.exec
|
const exec = childProcessPromise.exec
|
||||||
const spawn = childProcessPromise.spawn
|
const spawn = childProcessPromise.spawn
|
||||||
const mkdirp = pify(mkdirpCB)
|
const mkdirp = promisify(mkdirpCB)
|
||||||
const stat = pify(fs.stat.bind(fs))
|
const stat = promisify(fs.stat)
|
||||||
const writeFile = pify(fs.writeFile.bind(fs))
|
const writeFile = promisify(fs.writeFile)
|
||||||
const dir = __dirname
|
const dir = __dirname
|
||||||
|
|
||||||
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
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_NAME = 'pinafore_development'
|
||||||
const DB_USER = 'pinafore'
|
const DB_USER = 'pinafore'
|
||||||
|
@ -43,6 +43,7 @@ async function cloneMastodon () {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Cloning mastodon...')
|
console.log('Cloning mastodon...')
|
||||||
await exec(`git clone --single-branch --branch master ${GIT_URL} "${mastodonDir}"`)
|
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 exec(`git checkout ${GIT_TAG}`, { cwd: mastodonDir })
|
||||||
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||||
}
|
}
|
||||||
|
@ -56,24 +57,24 @@ async function setupMastodonDatabase () {
|
||||||
try {
|
try {
|
||||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||||
cwd: mastodonDir,
|
cwd: mastodonDir,
|
||||||
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||||
})
|
})
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||||
cwd: mastodonDir,
|
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}"`, {
|
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
||||||
cwd: mastodonDir,
|
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')
|
let systemDir = path.join(mastodonDir, 'public/system')
|
||||||
await mkdirp(systemDir)
|
await mkdirp(systemDir)
|
||||||
await exec(`tar -xzf "${tgzFile}"`, {cwd: systemDir})
|
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runMastodon () {
|
async function runMastodon () {
|
||||||
|
@ -95,12 +96,20 @@ async function runMastodon () {
|
||||||
'yarn --pure-lockfile'
|
'yarn --pure-lockfile'
|
||||||
]
|
]
|
||||||
|
|
||||||
for (let cmd of cmds) {
|
const installedFile = path.join(mastodonDir, 'installed.txt')
|
||||||
console.log(cmd)
|
try {
|
||||||
await exec(cmd, {cwd, env})
|
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 promise = spawn('foreman', ['start'], { cwd, env })
|
||||||
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
const log = fs.createWriteStream('mastodon.log', { flags: 'a' })
|
||||||
childProc = promise.childProcess
|
childProc = promise.childProcess
|
||||||
childProc.stdout.pipe(log)
|
childProc.stdout.pipe(log)
|
||||||
childProc.stderr.pipe(log)
|
childProc.stderr.pipe(log)
|
||||||
|
|
|
@ -2,20 +2,39 @@
|
||||||
|
|
||||||
set -e
|
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
|
exit 0 # no need to setup mastodon in this case
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# install ruby
|
||||||
source "$HOME/.rvm/scripts/rvm"
|
source "$HOME/.rvm/scripts/rvm"
|
||||||
rvm install 2.5.1
|
rvm install 2.6.0
|
||||||
rvm use 2.5.1
|
rvm use 2.6.0
|
||||||
|
|
||||||
sudo -E add-apt-repository -y ppa:mc3man/trusty-media
|
# fix for redis IPv6 issue
|
||||||
sudo -E apt-get update
|
# https://travis-ci.community/t/trusty-environment-redis-server-not-starting-with-redis-tools-installed/650/2
|
||||||
sudo -E apt-get install -y ffmpeg
|
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
|
ruby --version
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
yarn --version
|
||||||
postgres --version
|
postgres --version
|
||||||
redis-server --version
|
redis-server --version
|
||||||
ffmpeg -version
|
ffmpeg -version
|
||||||
|
|
83
bin/svgs.js
83
bin/svgs.js
|
@ -1,40 +1,47 @@
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{id: 'pinafore-logo', src: 'original-assets/sailboat.svg', title: 'Home'},
|
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg' },
|
||||||
{id: 'fa-bell', src: 'node_modules/font-awesome-svg-png/white/svg/bell.svg', title: 'Notifications'},
|
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg' },
|
||||||
{id: 'fa-users', src: 'node_modules/font-awesome-svg-png/white/svg/users.svg', title: 'Local'},
|
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg' },
|
||||||
{id: 'fa-globe', src: 'node_modules/font-awesome-svg-png/white/svg/globe.svg', title: 'Federated'},
|
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
|
||||||
{id: 'fa-gear', src: 'node_modules/font-awesome-svg-png/white/svg/gear.svg', title: 'Settings'},
|
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg' },
|
||||||
{id: 'fa-reply', src: 'node_modules/font-awesome-svg-png/white/svg/reply.svg', title: 'Reply'},
|
{ id: 'fa-reply', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply.svg' },
|
||||||
{id: 'fa-reply-all', src: 'node_modules/font-awesome-svg-png/white/svg/reply-all.svg', title: 'Reply to thread'},
|
{ id: 'fa-reply-all', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply-all.svg' },
|
||||||
{id: 'fa-retweet', src: 'node_modules/font-awesome-svg-png/white/svg/retweet.svg', title: 'Boost'},
|
{ id: 'fa-retweet', src: 'src/thirdparty/font-awesome-svg-png/white/svg/retweet.svg' },
|
||||||
{id: 'fa-star', src: 'node_modules/font-awesome-svg-png/white/svg/star.svg', title: 'Favorite'},
|
{ id: 'fa-star', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star.svg' },
|
||||||
{id: 'fa-ellipsis-h', src: 'node_modules/font-awesome-svg-png/white/svg/ellipsis-h.svg', title: 'More'},
|
{ id: 'fa-star-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star-o.svg' },
|
||||||
{id: 'fa-spinner', src: 'node_modules/font-awesome-svg-png/white/svg/spinner.svg', title: 'Spinner'},
|
{ id: 'fa-ellipsis-h', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ellipsis-h.svg' },
|
||||||
{id: 'fa-user', src: 'node_modules/font-awesome-svg-png/white/svg/user.svg', title: 'Empty user profile'},
|
{ id: 'fa-spinner', src: 'src/thirdparty/font-awesome-svg-png/white/svg/spinner.svg' },
|
||||||
{id: 'fa-play-circle', src: 'node_modules/font-awesome-svg-png/white/svg/play-circle.svg', title: 'Play'},
|
{ id: 'fa-user', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user.svg' },
|
||||||
{id: 'fa-eye', src: 'node_modules/font-awesome-svg-png/white/svg/eye.svg', title: 'Show Sensitive Content'},
|
{ id: 'fa-play-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/play-circle.svg' },
|
||||||
{id: 'fa-eye-slash', src: 'node_modules/font-awesome-svg-png/white/svg/eye-slash.svg', title: 'Hide Sensitive Content'},
|
{ id: 'fa-eye', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye.svg' },
|
||||||
{id: 'fa-lock', src: 'node_modules/font-awesome-svg-png/white/svg/lock.svg', title: 'Locked'},
|
{ id: 'fa-eye-slash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye-slash.svg' },
|
||||||
{id: 'fa-unlock', src: 'node_modules/font-awesome-svg-png/white/svg/unlock.svg', title: 'Unlocked'},
|
{ id: 'fa-lock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/lock.svg' },
|
||||||
{id: 'fa-envelope', src: 'node_modules/font-awesome-svg-png/white/svg/envelope.svg', title: 'Sealed Envelope'},
|
{ id: 'fa-unlock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/unlock.svg' },
|
||||||
{id: 'fa-user-times', src: 'node_modules/font-awesome-svg-png/white/svg/user-times.svg', title: 'Stop Following'},
|
{ id: 'fa-envelope', src: 'src/thirdparty/font-awesome-svg-png/white/svg/envelope.svg' },
|
||||||
{id: 'fa-user-plus', src: 'node_modules/font-awesome-svg-png/white/svg/user-plus.svg', title: 'Follow'},
|
{ id: 'fa-user-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-times.svg' },
|
||||||
{id: 'fa-external-link', src: 'node_modules/font-awesome-svg-png/white/svg/external-link.svg', title: 'External Link'},
|
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
|
||||||
{id: 'fa-search', src: 'node_modules/font-awesome-svg-png/white/svg/search.svg', title: 'Search'},
|
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
|
||||||
{id: 'fa-comments', src: 'node_modules/font-awesome-svg-png/white/svg/comments.svg', title: 'Conversations'},
|
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg' },
|
||||||
{id: 'fa-paperclip', src: 'node_modules/font-awesome-svg-png/white/svg/paperclip.svg', title: 'Paperclip'},
|
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg' },
|
||||||
{id: 'fa-thumb-tack', src: 'node_modules/font-awesome-svg-png/white/svg/thumb-tack.svg', title: 'Thumbtack'},
|
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
|
||||||
{id: 'fa-bars', src: 'node_modules/font-awesome-svg-png/white/svg/bars.svg', title: 'List'},
|
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
|
||||||
{id: 'fa-ban', src: 'node_modules/font-awesome-svg-png/white/svg/ban.svg', title: 'Ban'},
|
{ id: 'fa-bars', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bars.svg' },
|
||||||
{id: 'fa-camera', src: 'node_modules/font-awesome-svg-png/white/svg/camera.svg', title: 'Add media'},
|
{ id: 'fa-ban', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ban.svg' },
|
||||||
{id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'},
|
{ id: 'fa-camera', src: 'src/thirdparty/font-awesome-svg-png/white/svg/camera.svg' },
|
||||||
{id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'},
|
{ id: 'fa-smile', src: 'src/thirdparty/font-awesome-svg-png/white/svg/smile-o.svg' },
|
||||||
{id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'},
|
{ id: 'fa-exclamation-triangle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/exclamation-triangle.svg' },
|
||||||
{id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'},
|
{ id: 'fa-check', src: 'src/thirdparty/font-awesome-svg-png/white/svg/check.svg' },
|
||||||
{id: 'fa-hourglass', src: 'node_modules/font-awesome-svg-png/white/svg/hourglass.svg', title: 'Follow requested'},
|
{ id: 'fa-trash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/trash-o.svg' },
|
||||||
{id: 'fa-pencil', src: 'node_modules/font-awesome-svg-png/white/svg/pencil.svg', title: 'Compose'},
|
{ id: 'fa-hourglass', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hourglass.svg' },
|
||||||
{id: 'fa-times', src: 'node_modules/font-awesome-svg-png/white/svg/times.svg', title: 'Close'},
|
{ id: 'fa-pencil', src: 'src/thirdparty/font-awesome-svg-png/white/svg/pencil.svg' },
|
||||||
{id: 'fa-volume-off', src: 'node_modules/font-awesome-svg-png/white/svg/volume-off.svg', title: 'Mute'},
|
{ id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
|
||||||
{id: 'fa-volume-up', src: 'node_modules/font-awesome-svg-png/white/svg/volume-up.svg', title: 'Unmute'},
|
{ id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
|
||||||
{id: 'fa-link', src: 'node_modules/font-awesome-svg-png/white/svg/link.svg', title: 'Link'}
|
{ 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 fetch from 'node-fetch'
|
||||||
import { actions } from './mastodon-data'
|
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 () {
|
async function waitForMastodonData () {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
## Theming
|
## 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
|
```scss
|
||||||
@import "_base.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`.
|
Then, Add your theme to `src/routes/_static/themes.js`
|
||||||
```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`
|
|
||||||
```js
|
```js
|
||||||
const themes = [
|
const themes = [
|
||||||
...
|
...
|
||||||
{
|
{
|
||||||
name: 'foobar',
|
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`.
|
Start the development server (`yarn run dev`), go to
|
||||||
```js
|
`http://localhost:4002/settings/instances/your-instance-name` and select your
|
||||||
window.__themeColors = {
|
newly-created theme. Once you've done that, you can update your theme, and refresh
|
||||||
'default': "royalblue",
|
the page to see the change (you don't have to restart the server).
|
||||||
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).
|
|
||||||
|
|
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",
|
"name": "pinafore",
|
||||||
"description": "Alternative web client for Mastodon",
|
"description": "Alternative web client for Mastodon",
|
||||||
"version": "0.5.2",
|
"version": "1.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "standard && standard --plugin html 'routes/**/*.html'",
|
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||||
"lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
|
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
||||||
"dev": "run-s build-svg build-inline-script serve-dev",
|
"dev": "run-s build-template-html build-third-party-assets serve-dev",
|
||||||
"serve-dev": "run-p --race build-sass-watch serve",
|
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
|
||||||
"serve": "node server.js",
|
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
|
||||||
"build": "cross-env NODE_ENV=production npm run build-steps",
|
"sapper-prod": "cross-env PORT=4002 node __sapper__/build",
|
||||||
"build-steps": "run-s globalize-css build-sass build-svg build-inline-script sapper-build deglobalize-css",
|
"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",
|
"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-and-start": "run-s build start",
|
||||||
"build-svg": "node ./bin/build-svg.js",
|
"build-template-html": "node -r esm ./bin/build-template-html.js",
|
||||||
"build-inline-script": "node ./bin/build-inline-script.js",
|
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
|
||||||
"build-sass": "node ./bin/build-sass.js",
|
"build-third-party-assets": "node -r esm ./bin/build-third-party-assets.js",
|
||||||
"build-sass-watch": "node ./bin/build-sass.js --watch",
|
|
||||||
"run-mastodon": "node -r esm ./bin/run-mastodon.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-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-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",
|
"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": "run-s testcafe-suite0 testcafe-suite1",
|
||||||
"testcafe-suite0": "cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*",
|
"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*",
|
"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-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",
|
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
|
||||||
"globalize-css": "node ./bin/globalize-css.js",
|
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
|
||||||
"deglobalize-css": "node ./bin/globalize-css.js --reverse",
|
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
|
||||||
"stage-dev": "printf 'User-agent: *\nDisallow: /' > assets/robots.txt",
|
"deploy-all-travis": "./bin/deploy-all-travis.sh",
|
||||||
"stage-prod": "rm -f assets/robots.txt",
|
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
|
||||||
"launch": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson && sleep 60",
|
"sapper-export": "sapper export",
|
||||||
"launch-travis": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson --token $NOW_TOKEN && sleep 60",
|
"print-export-info": "node ./bin/print-export-info.js",
|
||||||
"alias-prod": "now alias pinafore.social --team nolanlawson",
|
"export-steps": "run-s before-build sapper-export print-export-info",
|
||||||
"alias-dev": "now alias dev.pinafore.social --team nolanlawson",
|
"export": "cross-env NODE_ENV=production run-s export-steps"
|
||||||
"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 ."
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gamestdio/websocket": "^0.2.7",
|
"@gamestdio/websocket": "^0.2.8",
|
||||||
"a11y-dialog": "^4.0.1",
|
"@webcomponents/custom-elements": "^1.2.1",
|
||||||
"browserslist": "^4.0.2",
|
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
"chokidar": "^2.0.4",
|
"chokidar": "^2.0.4",
|
||||||
|
"circular-dependency-plugin": "^5.0.2",
|
||||||
|
"clean-css": "^4.2.1",
|
||||||
|
"compression": "^1.7.3",
|
||||||
"cross-env": "^5.2.0",
|
"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",
|
"escape-html": "^1.0.3",
|
||||||
"esm": "^3.0.77",
|
"esm": "^3.1.4",
|
||||||
"events": "^3.0.0",
|
"events-light": "^1.0.5",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.4",
|
||||||
"fg-loadcss": "^2.0.1",
|
|
||||||
"file-api": "^0.10.4",
|
"file-api": "^0.10.4",
|
||||||
"font-awesome-svg-png": "^1.2.2",
|
"file-drop-element": "0.0.9",
|
||||||
"form-data": "^2.3.2",
|
"form-data": "^2.3.3",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.3",
|
||||||
"helmet": "^3.13.0",
|
"helmet": "^3.15.0",
|
||||||
|
"idb-keyval": "^3.1.0",
|
||||||
"indexeddb-getall-shim": "^1.3.5",
|
"indexeddb-getall-shim": "^1.3.5",
|
||||||
"intersection-observer": "^0.5.0",
|
"inferno-compat": "^7.1.0",
|
||||||
"lodash-es": "^4.17.10",
|
"intersection-observer": "^0.5.1",
|
||||||
|
"localstorage-memory": "^1.0.3",
|
||||||
|
"lodash-es": "^4.17.11",
|
||||||
"lodash-webpack-plugin": "^0.11.5",
|
"lodash-webpack-plugin": "^0.11.5",
|
||||||
"mini-css-extract-plugin": "^0.4.1",
|
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"node-fetch": "^2.2.0",
|
"node-fetch": "^2.3.0",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.11.0",
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.5",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
|
||||||
"p-any": "^1.1.0",
|
"p-any": "^1.1.0",
|
||||||
"page-lifecycle": "^0.1.1",
|
"page-lifecycle": "^0.1.1",
|
||||||
"performance-now": "^2.1.0",
|
"performance-now": "^2.1.0",
|
||||||
"pify": "^4.0.0",
|
"pinch-zoom-element": "^1.1.0",
|
||||||
"quick-lru": "^1.1.0",
|
"prop-types": "^15.6.2",
|
||||||
|
"quick-lru": "^2.0.0",
|
||||||
|
"remount": "^0.9.3",
|
||||||
"requestidlecallback": "^0.3.0",
|
"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",
|
"serve-static": "^1.13.2",
|
||||||
"shrink-ray-current": "^2.1.2",
|
|
||||||
"stringz": "^1.0.0",
|
"stringz": "^1.0.0",
|
||||||
"style-loader": "^0.22.1",
|
"svelte": "^2.16.0",
|
||||||
"svelte": "^2.11.0",
|
|
||||||
"svelte-extras": "^2.0.2",
|
"svelte-extras": "^2.0.2",
|
||||||
"svelte-loader": "^2.10.1",
|
"svelte-loader": "^2.12.0",
|
||||||
"svelte-transitions": "^1.2.0",
|
"svelte-transitions": "^1.2.0",
|
||||||
"svgo": "^1.0.5",
|
"svgo": "^1.1.1",
|
||||||
"timeago.js": "^3.0.2",
|
"terser-webpack-plugin": "^1.2.1",
|
||||||
|
"text-encoding": "^0.7.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
"uuid": "^3.3.2",
|
||||||
"web-animations-js": "^2.3.1",
|
"web-animations-js": "^2.3.1",
|
||||||
"webpack": "^4.16.5",
|
"webpack": "^4.29.0",
|
||||||
"webpack-bundle-analyzer": "^2.13.1",
|
"webpack-bundle-analyzer": "^3.0.3"
|
||||||
"yargs": "^12.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-html": "^4.0.5",
|
"assert": "^1.4.1",
|
||||||
"now": "^11.3.10",
|
"eslint-plugin-html": "^5.0.0",
|
||||||
"standard": "^11.0.1",
|
"mocha": "^5.2.0",
|
||||||
"testcafe": "^0.21.1"
|
"now": "^13.1.2",
|
||||||
|
"standard": "^12.0.1",
|
||||||
|
"testcafe": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
|
@ -136,12 +142,18 @@
|
||||||
"btoa",
|
"btoa",
|
||||||
"Blob",
|
"Blob",
|
||||||
"Element",
|
"Element",
|
||||||
"Image"
|
"Image",
|
||||||
|
"NotificationEvent",
|
||||||
|
"NodeList",
|
||||||
|
"DOMParser",
|
||||||
|
"CSS",
|
||||||
|
"customElements"
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"dist",
|
"dist",
|
||||||
"routes/_utils/asyncModules.js",
|
"src/routes/_utils/asyncModules.js",
|
||||||
"routes/_components/dialog/asyncDialogs.js"
|
"src/routes/_utils/asyncPolyfills.js",
|
||||||
|
"src/routes/_components/dialog/asyncDialogs.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"esm": {
|
"esm": {
|
||||||
|
@ -154,27 +166,26 @@
|
||||||
"NODE_ENV": "production"
|
"NODE_ENV": "production"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"assets",
|
|
||||||
"bin",
|
"bin",
|
||||||
"original-assets",
|
|
||||||
"routes",
|
|
||||||
"scss",
|
|
||||||
"templates",
|
|
||||||
"package.json",
|
|
||||||
"package-lock.json",
|
|
||||||
"server.js",
|
|
||||||
"inline-script.js",
|
"inline-script.js",
|
||||||
"webpack.client.config.js",
|
"original-static",
|
||||||
"webpack.server.config.js"
|
"scss",
|
||||||
|
"src",
|
||||||
|
"src-build",
|
||||||
|
"static",
|
||||||
|
"package.json",
|
||||||
|
"thirdparty",
|
||||||
|
"webpack",
|
||||||
|
"webpack.config.js",
|
||||||
|
"yarn.lock"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^8.0.0"
|
"node": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"greenkeeper": {
|
"greenkeeper": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"sapper",
|
"sapper"
|
||||||
"a11y-dialog"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"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 { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
|
||||||
import { getInstanceInfo } from '../_api/instance'
|
import { getInstanceInfo } from '../_api/instance'
|
||||||
import { goto } from 'sapper/runtime.js'
|
import { goto } from '../../../__sapper__/client'
|
||||||
import { switchToTheme } from '../_utils/themeEngine'
|
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { updateVerifyCredentialsForInstance } from './instances'
|
import { updateVerifyCredentialsForInstance } from './instances'
|
||||||
import { updateCustomEmojiForInstance } from './emoji'
|
import { updateCustomEmojiForInstance } from './emoji'
|
||||||
import { setInstanceInfo as setInstanceInfoInDatabase } from '../_database/meta'
|
import { database } from '../_database/database'
|
||||||
|
|
||||||
const REDIRECT_URI = (typeof location !== 'undefined'
|
const REDIRECT_URI = (typeof location !== 'undefined'
|
||||||
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
||||||
|
@ -14,12 +14,13 @@ async function redirectToOauth () {
|
||||||
let { instanceNameInSearch, loggedInInstances } = store.get()
|
let { instanceNameInSearch, loggedInInstances } = store.get()
|
||||||
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
||||||
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
||||||
store.set({logInToInstanceError: `You've already logged in to ${instanceNameInSearch}`})
|
let err = new Error(`You've already logged in to ${instanceNameInSearch}`)
|
||||||
return
|
err.knownError = true
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
||||||
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||||
await setInstanceInfoInDatabase(instanceNameInSearch, instanceInfo) // cache for later
|
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
||||||
let instanceData = await registrationPromise
|
let instanceData = await registrationPromise
|
||||||
store.set({
|
store.set({
|
||||||
currentRegisteredInstanceName: instanceNameInSearch,
|
currentRegisteredInstanceName: instanceNameInSearch,
|
||||||
|
@ -44,16 +45,17 @@ export async function logInToInstance () {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
let error = `${err.message || err.name}. ` +
|
let error = `${err.message || err.name}. ` +
|
||||||
(navigator.onLine
|
(err.knownError ? '' : (navigator.onLine
|
||||||
? `Is this a valid Mastodon instance? Is a browser extension blocking the request?`
|
? `Is this a valid Mastodon instance? Is a browser extension
|
||||||
: `Are you offline?`)
|
blocking the request? Are you in private browsing mode?`
|
||||||
|
: `Are you offline?`))
|
||||||
let { instanceNameInSearch } = store.get()
|
let { instanceNameInSearch } = store.get()
|
||||||
store.set({
|
store.set({
|
||||||
logInToInstanceError: error,
|
logInToInstanceError: error,
|
||||||
logInToInstanceErrorForText: instanceNameInSearch
|
logInToInstanceErrorForText: instanceNameInSearch
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
store.set({logInToInstanceLoading: false})
|
store.set({ logInToInstanceLoading: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +69,7 @@ async function registerNewInstance (code) {
|
||||||
REDIRECT_URI
|
REDIRECT_URI
|
||||||
)
|
)
|
||||||
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
||||||
instanceThemes[currentRegisteredInstanceName] = 'default'
|
instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
|
||||||
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
||||||
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
||||||
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
||||||
|
@ -82,7 +84,7 @@ async function registerNewInstance (code) {
|
||||||
instanceThemes: instanceThemes
|
instanceThemes: instanceThemes
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
switchToTheme('default')
|
switchToTheme(DEFAULT_THEME)
|
||||||
// fire off these requests so they're cached
|
// fire off these requests so they're cached
|
||||||
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
||||||
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
||||||
|
@ -91,11 +93,11 @@ async function registerNewInstance (code) {
|
||||||
|
|
||||||
export async function handleOauthCode (code) {
|
export async function handleOauthCode (code) {
|
||||||
try {
|
try {
|
||||||
store.set({logInToInstanceLoading: true})
|
store.set({ logInToInstanceLoading: true })
|
||||||
await registerNewInstance(code)
|
await registerNewInstance(code)
|
||||||
} catch (err) {
|
} 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 {
|
} 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 { mark, stop } from '../_utils/marks'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import uniqBy from 'lodash-es/uniqBy'
|
import uniqBy from 'lodash-es/uniqBy'
|
||||||
import uniq from 'lodash-es/uniq'
|
import uniq from 'lodash-es/uniq'
|
||||||
import isEqual from 'lodash-es/isEqual'
|
import isEqual from 'lodash-es/isEqual'
|
||||||
import {
|
import { database } from '../_database/database'
|
||||||
insertTimelineItems as insertTimelineItemsInDatabase
|
import { concat } from '../_utils/arrays'
|
||||||
} from '../_database/timelines/insertion'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask'
|
|
||||||
|
|
||||||
const STREAMING_THROTTLE_DELAY = 3000
|
|
||||||
|
|
||||||
function getExistingItemIdsSet (instanceName, timelineName) {
|
function getExistingItemIdsSet (instanceName, timelineName) {
|
||||||
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
||||||
|
@ -29,14 +25,31 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await insertTimelineItemsInDatabase(instanceName, timelineName, updates)
|
await database.insertTimelineItems(instanceName, timelineName, updates)
|
||||||
|
|
||||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
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)) {
|
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||||
'items to itemIdsToAdd for timeline', timelineName)
|
'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)
|
let threads = store.getThreads(instanceName)
|
||||||
|
let timelineNames = Object.keys(threads)
|
||||||
for (let timelineName of Object.keys(threads)) {
|
for (let timelineName of timelineNames) {
|
||||||
let thread = threads[timelineName]
|
let thread = threads[timelineName]
|
||||||
let updatesForThisThread = updates.filter(
|
|
||||||
status => thread.includes(status.in_reply_to_id) && !thread.includes(status.id)
|
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||||
)
|
let validUpdates = updates.filter(isValidStatusForThread(thread, timelineName, itemIdsToAdd))
|
||||||
if (!updatesForThisThread.length) {
|
if (!validUpdates.length) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
let newItemIdsToAdd = uniq(concat(itemIdsToAdd, validUpdates.map(_ => _.id)))
|
||||||
let newItemIdsToAdd = uniq([].concat(itemIdsToAdd).concat(updatesForThisThread.map(_ => _.id)))
|
|
||||||
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||||
'items to itemIdsToAdd for thread', timelineName)
|
'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')
|
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates')
|
||||||
if (freshUpdates && freshUpdates.length) {
|
if (freshUpdates && freshUpdates.length) {
|
||||||
let updates = freshUpdates.slice()
|
let updates = freshUpdates.slice()
|
||||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: []})
|
store.setForTimeline(instanceName, timelineName, { freshUpdates: [] })
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
||||||
|
@ -80,11 +92,11 @@ async function processFreshUpdates (instanceName, timelineName) {
|
||||||
stop('processFreshUpdates')
|
stop('processFreshUpdates')
|
||||||
}
|
}
|
||||||
|
|
||||||
const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => {
|
function lazilyProcessFreshUpdates (instanceName, timelineName) {
|
||||||
runMediumPriorityTask(() => {
|
scheduleIdleTask(() => {
|
||||||
/* no await */ processFreshUpdates(instanceName, timelineName)
|
/* no await */ processFreshUpdates(instanceName, timelineName)
|
||||||
})
|
})
|
||||||
}, STREAMING_THROTTLE_DELAY)
|
}
|
||||||
|
|
||||||
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
||||||
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
||||||
|
@ -93,8 +105,8 @@ export function addStatusOrNotification (instanceName, timelineName, newStatusOr
|
||||||
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
||||||
console.log('addStatusesOrNotifications', Date.now())
|
console.log('addStatusesOrNotifications', Date.now())
|
||||||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
||||||
freshUpdates = [].concat(freshUpdates).concat(newStatusesOrNotifications)
|
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
|
||||||
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
||||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: freshUpdates})
|
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
|
||||||
lazilyProcessFreshUpdates(instanceName, timelineName)
|
lazilyProcessFreshUpdates(instanceName, timelineName)
|
||||||
}
|
}
|
|
@ -6,8 +6,8 @@ export async function insertUsername (realm, username, startIndex, endIndex) {
|
||||||
let pre = oldText.substring(0, startIndex)
|
let pre = oldText.substring(0, startIndex)
|
||||||
let post = oldText.substring(endIndex)
|
let post = oldText.substring(endIndex)
|
||||||
let newText = `${pre}@${username} ${post}`
|
let newText = `${pre}@${username} ${post}`
|
||||||
store.setComposeData(realm, {text: newText})
|
store.setComposeData(realm, { text: newText })
|
||||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||||
|
@ -29,8 +29,8 @@ export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
||||||
let pre = oldText.substring(0, startIndex)
|
let pre = oldText.substring(0, startIndex)
|
||||||
let post = oldText.substring(endIndex)
|
let post = oldText.substring(endIndex)
|
||||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||||
store.setComposeData(realm, {text: newText})
|
store.setComposeData(realm, { text: newText })
|
||||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
export async function clickSelectedAutosuggestionEmoji (realm) {
|
|
@ -1,18 +1,19 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { blockAccount, unblockAccount } from '../_api/block'
|
import { blockAccount, unblockAccount } from '../_api/block'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { updateProfileAndRelationship } from './accounts'
|
import { updateLocalRelationship } from './accounts'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
|
let relationship
|
||||||
if (block) {
|
if (block) {
|
||||||
await blockAccount(currentInstance, accessToken, accountId)
|
relationship = await blockAccount(currentInstance, accessToken, accountId)
|
||||||
} else {
|
} else {
|
||||||
await unblockAccount(currentInstance, accessToken, accountId)
|
relationship = await unblockAccount(currentInstance, accessToken, accountId)
|
||||||
}
|
}
|
||||||
await updateProfileAndRelationship(accountId)
|
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (block) {
|
if (block) {
|
||||||
toast.say('Blocked account')
|
toast.say('Blocked account')
|
|
@ -1,14 +1,14 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { postStatus as postStatusToServer } from '../_api/statuses'
|
import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||||
import { getStatus as getStatusFromDatabase } from '../_database/timelines/getStatusOrNotification'
|
import { database } from '../_database/database'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
import { putMediaDescription } from '../_api/media'
|
import { putMediaDescription } from '../_api/media'
|
||||||
|
|
||||||
export async function insertHandleForReply (statusId) {
|
export async function insertHandleForReply (statusId) {
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
let status = await getStatusFromDatabase(currentInstance, statusId)
|
let status = await database.getStatus(currentInstance, statusId)
|
||||||
let { currentVerifyCredentials } = store.get()
|
let { currentVerifyCredentials } = store.get()
|
||||||
let originalStatus = status.reblog || status
|
let originalStatus = status.reblog || status
|
||||||
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
||||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
||||||
|
|
||||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
sensitive, spoilerText, visibility,
|
sensitive, spoilerText, visibility,
|
||||||
mediaDescriptions = [], inReplyToUuid) {
|
mediaDescriptions, inReplyToUuid) {
|
||||||
let { currentInstance, accessToken, online } = store.get()
|
let { currentInstance, accessToken, online } = store.get()
|
||||||
|
|
||||||
if (!online) {
|
if (!online) {
|
||||||
|
@ -30,6 +30,9 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text = text || ''
|
||||||
|
mediaDescriptions = mediaDescriptions || []
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
postingStatus: true
|
postingStatus: true
|
||||||
})
|
})
|
||||||
|
@ -46,7 +49,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say('Unable to post status: ' + (e.message || ''))
|
toast.say('Unable to post status: ' + (e.message || ''))
|
||||||
} finally {
|
} 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]
|
let visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
|
||||||
? replyVisibility
|
? replyVisibility
|
||||||
: defaultVisibility
|
: 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 { store } from '../_store/store'
|
||||||
import { deleteStatus } from '../_api/delete'
|
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) {
|
export async function doDeleteStatus (statusId) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
await deleteStatus(currentInstance, accessToken, statusId)
|
await deleteStatus(currentInstance, accessToken, statusId)
|
||||||
|
deleteStatusLocally(currentInstance, statusId)
|
||||||
toast.say('Status deleted.')
|
toast.say('Status deleted.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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 { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
|
||||||
import isEqual from 'lodash-es/isEqual'
|
import isEqual from 'lodash-es/isEqual'
|
||||||
import {
|
import { database } from '../_database/database'
|
||||||
deleteStatusesAndNotifications as deleteStatusesAndNotificationsFromDatabase
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
} from '../_database/timelines/deletion'
|
|
||||||
|
|
||||||
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||||
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
||||||
|
@ -18,6 +16,7 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||||
}
|
}
|
||||||
let filteredIds = ids.filter(idFilter)
|
let filteredIds = ids.filter(idFilter)
|
||||||
if (!isEqual(ids, filteredIds)) {
|
if (!isEqual(ids, filteredIds)) {
|
||||||
|
console.log('deleting an item from timelineName', timelineName, 'for key', key)
|
||||||
store.setForTimeline(instanceName, timelineName, {
|
store.setForTimeline(instanceName, timelineName, {
|
||||||
[key]: filteredIds
|
[key]: filteredIds
|
||||||
})
|
})
|
||||||
|
@ -45,7 +44,7 @@ function deleteNotificationIdsFromStore (instanceName, idsToDelete) {
|
||||||
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
||||||
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
||||||
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
||||||
await deleteStatusesAndNotificationsFromDatabase(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
await database.deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doDeleteStatus (instanceName, statusId) {
|
async function doDeleteStatus (instanceName, statusId) {
|
|
@ -1,30 +1,28 @@
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import {
|
import { database } from '../_database/database'
|
||||||
getCustomEmoji as getCustomEmojiFromDatabase,
|
|
||||||
setCustomEmoji as setCustomEmojiInDatabase
|
|
||||||
} from '../_database/meta'
|
|
||||||
import { getCustomEmoji } from '../_api/emoji'
|
import { getCustomEmoji } from '../_api/emoji'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
export async function updateCustomEmojiForInstance (instanceName) {
|
export async function updateCustomEmojiForInstance (instanceName) {
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getCustomEmoji(instanceName),
|
() => getCustomEmoji(instanceName),
|
||||||
() => getCustomEmojiFromDatabase(instanceName),
|
() => database.getCustomEmoji(instanceName),
|
||||||
emoji => setCustomEmojiInDatabase(instanceName, emoji),
|
emoji => database.setCustomEmoji(instanceName, emoji),
|
||||||
emoji => {
|
emoji => {
|
||||||
let { customEmoji } = store.get()
|
let { customEmoji } = store.get()
|
||||||
customEmoji[instanceName] = emoji
|
customEmoji[instanceName] = emoji
|
||||||
store.set({customEmoji: customEmoji})
|
store.set({ customEmoji: customEmoji })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertEmoji (realm, emoji) {
|
export function insertEmoji (realm, emoji) {
|
||||||
|
let emojiText = emoji.custom ? emoji.colons : emoji.native
|
||||||
let { composeSelectionStart } = store.get()
|
let { composeSelectionStart } = store.get()
|
||||||
let idx = composeSelectionStart || 0
|
let idx = composeSelectionStart || 0
|
||||||
let oldText = store.getComposeData(realm, 'text') || ''
|
let oldText = store.getComposeData(realm, 'text') || ''
|
||||||
let pre = oldText.substring(0, idx)
|
let pre = oldText.substring(0, idx)
|
||||||
let post = oldText.substring(idx)
|
let post = oldText.substring(idx)
|
||||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
let newText = `${pre}${emojiText} ${post}`
|
||||||
store.setComposeData(realm, {text: newText})
|
store.setComposeData(realm, { text: newText })
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import {
|
import { database } from '../_database/database'
|
||||||
setStatusFavorited as setStatusFavoritedInDatabase
|
|
||||||
} from '../_database/timelines/updateStatus'
|
|
||||||
|
|
||||||
export async function setFavorited (statusId, favorited) {
|
export async function setFavorited (statusId, favorited) {
|
||||||
let { online } = store.get()
|
let { online } = store.get()
|
||||||
|
@ -18,7 +16,7 @@ export async function setFavorited (statusId, favorited) {
|
||||||
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
||||||
try {
|
try {
|
||||||
await networkPromise
|
await networkPromise
|
||||||
await setStatusFavoritedInDatabase(currentInstance, statusId, favorited)
|
await database.setStatusFavorited(currentInstance, statusId, favorited)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
|
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) {
|
export async function getFollowRequests (instanceName, accessToken) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests`
|
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) {
|
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
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) {
|
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
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 { getVerifyCredentials } from '../_api/user'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { switchToTheme } from '../_utils/themeEngine'
|
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { goto } from 'sapper/runtime.js'
|
import { goto } from '../../../__sapper__/client'
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import { getInstanceInfo } from '../_api/instance'
|
import { getInstanceInfo } from '../_api/instance'
|
||||||
import { clearDatabaseForInstance } from '../_database/clear'
|
import { database } from '../_database/database'
|
||||||
import {
|
|
||||||
getInstanceVerifyCredentials as getInstanceVerifyCredentialsFromDatabase,
|
|
||||||
setInstanceVerifyCredentials as setInstanceVerifyCredentialsInDatabase,
|
|
||||||
getInstanceInfo as getInstanceInfoFromDatabase,
|
|
||||||
setInstanceInfo as setInstanceInfoInDatabase
|
|
||||||
} from '../_database/meta'
|
|
||||||
|
|
||||||
export function changeTheme (instanceName, newTheme) {
|
export function changeTheme (instanceName, newTheme) {
|
||||||
let { instanceThemes } = store.get()
|
let { instanceThemes } = store.get()
|
||||||
instanceThemes[instanceName] = newTheme
|
instanceThemes[instanceName] = newTheme
|
||||||
store.set({instanceThemes: instanceThemes})
|
store.set({ instanceThemes: instanceThemes })
|
||||||
store.save()
|
store.save()
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
if (instanceName === currentInstance) {
|
if (instanceName === currentInstance) {
|
||||||
|
@ -61,15 +55,15 @@ export async function logOutOfInstance (instanceName) {
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
toast.say(`Logged out of ${instanceName}`)
|
toast.say(`Logged out of ${instanceName}`)
|
||||||
switchToTheme(instanceThemes[newInstance] || 'default')
|
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
|
||||||
await clearDatabaseForInstance(instanceName)
|
/* no await */ database.clearDatabaseForInstance(instanceName)
|
||||||
goto('/settings/instances')
|
goto('/settings/instances')
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||||
let { verifyCredentials } = store.get()
|
let { verifyCredentials } = store.get()
|
||||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||||
store.set({verifyCredentials: verifyCredentials})
|
store.set({ verifyCredentials: verifyCredentials })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||||
|
@ -77,8 +71,8 @@ export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||||
let accessToken = loggedInInstances[instanceName].access_token
|
let accessToken = loggedInInstances[instanceName].access_token
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getVerifyCredentials(instanceName, accessToken),
|
() => getVerifyCredentials(instanceName, accessToken),
|
||||||
() => getInstanceVerifyCredentialsFromDatabase(instanceName),
|
() => database.getInstanceVerifyCredentials(instanceName),
|
||||||
verifyCredentials => setInstanceVerifyCredentialsInDatabase(instanceName, verifyCredentials),
|
verifyCredentials => database.setInstanceVerifyCredentials(instanceName, verifyCredentials),
|
||||||
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -91,12 +85,12 @@ export async function updateVerifyCredentialsForCurrentInstance () {
|
||||||
export async function updateInstanceInfo (instanceName) {
|
export async function updateInstanceInfo (instanceName) {
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getInstanceInfo(instanceName),
|
() => getInstanceInfo(instanceName),
|
||||||
() => getInstanceInfoFromDatabase(instanceName),
|
() => database.getInstanceInfo(instanceName),
|
||||||
info => setInstanceInfoInDatabase(instanceName, info),
|
info => database.setInstanceInfo(instanceName, info),
|
||||||
info => {
|
info => {
|
||||||
let { instanceInfos } = store.get()
|
let { instanceInfos } = store.get()
|
||||||
instanceInfos[instanceName] = info
|
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 { store } from '../_store/store'
|
||||||
import { uploadMedia } from '../_api/media'
|
import { uploadMedia } from '../_api/media'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
|
|
||||||
export async function doMediaUpload (realm, file) {
|
export async function doMediaUpload (realm, file) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
store.set({uploadingMedia: true})
|
store.set({ uploadingMedia: true })
|
||||||
try {
|
try {
|
||||||
let response = await uploadMedia(currentInstance, accessToken, file)
|
let response = await uploadMedia(currentInstance, accessToken, file)
|
||||||
let composeMedia = store.getComposeData(realm, 'media') || []
|
let composeMedia = store.getComposeData(realm, 'media') || []
|
||||||
|
if (composeMedia.length === 4) {
|
||||||
|
throw new Error('Only 4 media max are allowed')
|
||||||
|
}
|
||||||
composeMedia.push({
|
composeMedia.push({
|
||||||
data: response,
|
data: response,
|
||||||
file: { name: file.name }
|
file: { name: file.name },
|
||||||
|
description: ''
|
||||||
})
|
})
|
||||||
let composeText = store.getComposeData(realm, 'text') || ''
|
|
||||||
composeText += ' ' + response.text_url
|
|
||||||
store.setComposeData(realm, {
|
store.setComposeData(realm, {
|
||||||
media: composeMedia,
|
media: composeMedia
|
||||||
text: composeText
|
|
||||||
})
|
})
|
||||||
scheduleIdleTask(() => store.save())
|
scheduleIdleTask(() => store.save())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say('Failed to upload media: ' + (e.message || ''))
|
toast.say('Failed to upload media: ' + (e.message || ''))
|
||||||
} finally {
|
} finally {
|
||||||
store.set({uploadingMedia: false})
|
store.set({ uploadingMedia: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMedia (realm, i) {
|
export function deleteMedia (realm, i) {
|
||||||
let composeMedia = store.getComposeData(realm, 'media')
|
let composeMedia = store.getComposeData(realm, 'media')
|
||||||
let deletedMedia = composeMedia.splice(i, 1)[0]
|
composeMedia.splice(i, 1)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
store.setComposeData(realm, {
|
store.setComposeData(realm, {
|
||||||
media: composeMedia,
|
media: composeMedia
|
||||||
text: composeText,
|
|
||||||
mediaDescriptions: mediaDescriptions
|
|
||||||
})
|
})
|
||||||
scheduleIdleTask(() => store.save())
|
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 { store } from '../_store/store'
|
||||||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { updateProfileAndRelationship } from './accounts'
|
import { updateLocalRelationship } from './accounts'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
|
let relationship
|
||||||
if (mute) {
|
if (mute) {
|
||||||
await muteAccount(currentInstance, accessToken, accountId)
|
relationship = await muteAccount(currentInstance, accessToken, accountId)
|
||||||
} else {
|
} else {
|
||||||
await unmuteAccount(currentInstance, accessToken, accountId)
|
relationship = await unmuteAccount(currentInstance, accessToken, accountId)
|
||||||
}
|
}
|
||||||
await updateProfileAndRelationship(accountId)
|
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (mute) {
|
if (mute) {
|
||||||
toast.say('Muted account')
|
toast.say('Muted account')
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { setStatusMuted as setStatusMutedInDatabase } from '../_database/timelines/updateStatus'
|
import { database } from '../_database/database'
|
||||||
|
|
||||||
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
@ -11,7 +11,7 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||||
} else {
|
} else {
|
||||||
await unmuteConversation(currentInstance, accessToken, statusId)
|
await unmuteConversation(currentInstance, accessToken, statusId)
|
||||||
}
|
}
|
||||||
await setStatusMutedInDatabase(currentInstance, statusId, mute)
|
await database.setStatusMuted(currentInstance, statusId, mute)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (mute) {
|
if (mute) {
|
||||||
toast.say('Muted conversation')
|
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 { store } from '../_store/store'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { pinStatus, unpinStatus } from '../_api/pin'
|
import { pinStatus, unpinStatus } from '../_api/pin'
|
||||||
import { setStatusPinned as setStatusPinnedInDatabase } from '../_database/timelines/updateStatus'
|
import { database } from '../_database/database'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
||||||
|
@ -19,7 +19,8 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
|
||||||
toast.say('Unpinned status')
|
toast.say('Unpinned status')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await setStatusPinnedInDatabase(currentInstance, statusId, pinned)
|
store.setStatusPinned(currentInstance, statusId, pinned)
|
||||||
|
await database.setStatusPinned(currentInstance, statusId, pinned)
|
||||||
emit('updatePinnedStatuses')
|
emit('updatePinnedStatuses')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
|
@ -1,9 +1,6 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import {
|
import { database } from '../_database/database'
|
||||||
getPinnedStatuses as getPinnedStatusesFromDatabase,
|
|
||||||
insertPinnedStatuses as insertPinnedStatusesInDatabase
|
|
||||||
} from '../_database/timelines/pinnedStatuses'
|
|
||||||
import {
|
import {
|
||||||
getPinnedStatuses
|
getPinnedStatuses
|
||||||
} from '../_api/pinnedStatuses'
|
} from '../_api/pinnedStatuses'
|
||||||
|
@ -13,13 +10,13 @@ export async function updatePinnedStatusesForAccount (accountId) {
|
||||||
|
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||||
() => getPinnedStatusesFromDatabase(currentInstance, accountId),
|
() => database.getPinnedStatuses(currentInstance, accountId),
|
||||||
statuses => insertPinnedStatusesInDatabase(currentInstance, accountId, statuses),
|
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
||||||
statuses => {
|
statuses => {
|
||||||
let { pinnedStatuses } = store.get()
|
let { pinnedStatuses } = store.get()
|
||||||
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
||||||
pinnedStatuses[currentInstance][accountId] = statuses
|
pinnedStatuses[currentInstance][accountId] = statuses
|
||||||
store.set({pinnedStatuses: pinnedStatuses})
|
store.set({ pinnedStatuses: pinnedStatuses })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,5 +2,5 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
export function setPostPrivacy (realm, postPrivacyKey) {
|
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 { store } from '../_store/store'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
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) {
|
export async function setReblogged (statusId, reblogged) {
|
||||||
let online = store.get()
|
let online = store.get()
|
||||||
|
@ -16,7 +16,7 @@ export async function setReblogged (statusId, reblogged) {
|
||||||
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
||||||
try {
|
try {
|
||||||
await networkPromise
|
await networkPromise
|
||||||
await setStatusRebloggedInDatabase(currentInstance, statusId, reblogged)
|
await database.setStatusReblogged(currentInstance, statusId, reblogged)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
|
|
||||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||||
let {
|
let {
|
|
@ -1,10 +1,10 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_utils/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { search } from '../_api/search'
|
import { search } from '../_api/search'
|
||||||
|
|
||||||
export async function doSearch () {
|
export async function doSearch () {
|
||||||
let { currentInstance, accessToken, queryInSearch } = store.get()
|
let { currentInstance, accessToken, queryInSearch } = store.get()
|
||||||
store.set({searchLoading: true})
|
store.set({ searchLoading: true })
|
||||||
try {
|
try {
|
||||||
let results = await search(currentInstance, accessToken, queryInSearch)
|
let results = await search(currentInstance, accessToken, queryInSearch)
|
||||||
let { queryInSearch: newQueryInSearch } = store.get() // avoid race conditions
|
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 || ''))
|
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} 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 {
|
import { database } from '../_database/database'
|
||||||
getNotificationIdsForStatuses as getNotificationIdsForStatusesFromDatabase,
|
|
||||||
getReblogsForStatus as getReblogsForStatusFromDatabase
|
|
||||||
} from '../_database/timelines/lookup'
|
|
||||||
import {
|
|
||||||
getStatus as getStatusFromDatabase
|
|
||||||
} from '../_database/timelines/getStatusOrNotification'
|
|
||||||
|
|
||||||
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
|
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
|
return status.reblog && status.reblog.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +13,9 @@ export async function getIdsThatTheseStatusesReblogged (instanceName, statusIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
||||||
return getReblogsForStatusFromDatabase(instanceName, statusId)
|
return database.getReblogsForStatus(instanceName, statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNotificationIdsForStatuses (instanceName, statusIds) {
|
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